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

@@ -31,6 +31,27 @@ ok('has bars', model.bars.length > 0);
ok('432 bars', model.bars.length === 432);
console.log(` bars: ${model.bars.length}, instruments: ${model.instruments.length}`);
// ── Preamble (info, tuning, stage, articles) ───────────────────────────────
section('Preamble');
ok('info parsed', model.info && typeof model.info.title === 'string');
ok('info composer', model.info?.composer === 'Ludwig van Beethoven');
ok('tuning parsed', model.tuning && model.tuning.base === 'tones_euro_de+en');
ok('tuning has scales', Object.keys(model.tuning.scales).length > 0);
ok('tuning.scales hm7', Array.isArray(model.tuning.scales.hm7));
ok('tuning has chords', Object.keys(model.tuning.chords).length > 0);
ok('tuning frequencyFactors object', model.tuning.frequencyFactors && typeof model.tuning.frequencyFactors === 'object');
ok('frequencyFactors label', model.tuning.frequencyFactors.label === 'just5lim');
ok('frequencyFactors 12 ratios', model.tuning.frequencyFactors.factors.length === 12);
ok('stageCone parsed', model.stageCone && model.stageCone.type === 'stage_cone');
ok('stageCone has minvol', model.stageCone.minvol !== undefined);
ok('stageVoices parsed', model.stageVoices.length === 3);
ok('stageVoices[0] is "pi"', model.stageVoices[0].name === 'pi');
ok('stageVoices[0].direction', model.stageVoices[0].direction === '1|1');
ok('articles array non-empty', model.articles.length > 0);
ok('articles[0].subtype defaults', model.articles[0].subtype === 'defaults');
ok('articles[0].name "f"', model.articles[0].name === 'f');
ok('articles[0].props.add_stress', model.articles[0].props.add_stress === 3);
// ── Bar IDs ────────────────────────────────────────────────────────────────
// Bar IDs are opaque auto-increment strings; only the raw id string matters.
section('Bar IDs');