Parser: support sompyler 10cad1f preamble (stage.*, articles.<subtype>, tuning.frequency_factors)

- Top-level dispatch switches on fqSlot (parentSlot.slot) so dotted top-level
  slots (`stage.cone`, `stage.voice`, `articles.<subtype>`) are matched.
- `stage_voice` -> `stage.cone` (new, one per score) + `stage.voice` (list).
- `article` -> `articles.<subtype>` (subtype stored on the article entry;
  first observed subtype is `defaults`).
- New `tuning.frequency_factors` child captured as `{ label, factors }`.
- buildModel un-nests preamble siblings that sompyler emits at depth 01
  under `00 tuning` via `with deeper_level("articles")` / `deeper_level("stage")`.
  Only `tuning`'s children are flattened; `instrument`'s implicit containers
  (`character`, `VOLUMES`, `TIMBRE`, `FM`, `AM`) are left untouched.
- Fixture test extended with 18 preamble assertions (172 total now pass).
This commit is contained in:
c0dev0id
2026-06-28 15:41:54 +02:00
parent 330b3788f0
commit b23e243225
2 changed files with 60 additions and 9 deletions

View File

@@ -118,24 +118,47 @@ export function buildModel(rawTree) {
info: null,
tuning: null,
articles: [],
stageCone: null,
stageVoices: [],
instruments: [],
bars: [],
};
// Upstream uses `with deeper_level("articles"):` / `deeper_level("stage"):`
// implicit containers that emit no depth-00 header. Their depth-01 lines get
// nested under the preceding `00 tuning` by the depth-stack parser even
// though they are conceptually siblings of tuning. Un-nest them here.
const topLevel = [];
for (const node of rawTree.children) {
switch (node.slot) {
if (node.parentSlot === null && node.slot === 'tuning') {
const trulyTuning = [];
const misnested = [];
for (const child of node.children) {
if (child.parentSlot === 'tuning') trulyTuning.push(child);
else misnested.push(child);
}
topLevel.push({ ...node, children: trulyTuning });
topLevel.push(...misnested);
} else {
topLevel.push(node);
}
}
for (const node of topLevel) {
const fqSlot = node.parentSlot ? `${node.parentSlot}.${node.slot}` : node.slot;
switch (fqSlot) {
case 'info':
score.info = { ...node.props };
break;
case 'tuning':
score.tuning = buildTuning(node);
break;
case 'article':
score.articles.push(buildArticle(node));
case 'stage.cone':
score.stageCone = { type: 'stage_cone', ...node.props };
break;
case 'stage_voice':
case 'stage.voice':
score.stageVoices.push({
type: 'stage_voice',
name: node.positionals[0],
direction: node.props.direction,
distance: node.props.distance,
@@ -148,7 +171,11 @@ export function buildModel(rawTree) {
score.bars.push(buildBar(node));
break;
default:
score[node.slot] = buildGeneric(node);
if (node.parentSlot === 'articles') {
score.articles.push(buildArticle(node));
} else {
score[node.slot] = buildGeneric(node);
}
}
}
@@ -156,12 +183,17 @@ export function buildModel(rawTree) {
}
function buildTuning(node) {
const t = { base: node.props.base, scales: {}, chords: {} };
const t = { type: 'tuning', base: node.props.base, scales: {}, chords: {}, frequencyFactors: null };
for (const child of node.children) {
if (child.slot === 'scales') {
t.scales[child.positionals[0]] = child.positionals.slice(1);
} else if (child.slot === 'chords') {
t.chords[child.positionals[0]] = child.positionals.slice(1);
} else if (child.slot === 'frequency_factors') {
t.frequencyFactors = {
label: child.props.label ?? null,
factors: child.positionals.slice(),
};
}
}
return t;
@@ -170,11 +202,9 @@ function buildTuning(node) {
function buildArticle(node) {
return {
type: 'article',
subtype: node.slot,
name: node.positionals[0],
props: { ...node.props },
properties: node.children
.filter(c => c.slot === 'property')
.map(c => ({ name: c.positionals[0], ...c.props })),
};
}