exporter: remove stage voice entries for deleted instruments
This commit is contained in:
@@ -246,6 +246,66 @@ function _patchInstrumentHeader(text, instruments) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Remove stage voice entries whose referenced instrument is no longer in the model.
|
||||||
|
// String form: ` voicename: LEFT|RIGHT DIST [Instrument]` — 3rd token is instrument.
|
||||||
|
// Mapping form: ` voicename:\n instrument: Name` — look for `instrument:` child.
|
||||||
|
// When no explicit instrument is present, the voice name is the implicit instrument name.
|
||||||
|
|
||||||
|
function _patchStageSection(text, instruments) {
|
||||||
|
const instrNames = new Set(instruments.flatMap(i => {
|
||||||
|
const names = [i.name];
|
||||||
|
if (i.name.includes('/')) names.push(i.name.split('/').pop());
|
||||||
|
return names;
|
||||||
|
}));
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const stageIdx = lines.findIndex(l => /^stage\s*:/.test(l));
|
||||||
|
if (stageIdx === -1) return text;
|
||||||
|
|
||||||
|
let stageEnd = lines.length;
|
||||||
|
for (let j = stageIdx + 1; j < lines.length; j++) {
|
||||||
|
if (lines[j] !== '' && !/^\s/.test(lines[j])) { stageEnd = j; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const stageLines = lines.slice(stageIdx + 1, stageEnd);
|
||||||
|
const firstIndented = stageLines.find(l => l !== '' && /^\s/.test(l));
|
||||||
|
if (!firstIndented) return text;
|
||||||
|
const voiceIndentLen = firstIndented.match(/^(\s+)/)[1].length;
|
||||||
|
|
||||||
|
// Group stageLines into per-voice entries
|
||||||
|
const entries = [];
|
||||||
|
let cur = null;
|
||||||
|
for (const line of stageLines) {
|
||||||
|
if (line === '') { if (cur) cur.lines.push(line); continue; }
|
||||||
|
const indentLen = (line.match(/^(\s+)/) ?? ['', ''])[1].length;
|
||||||
|
if (indentLen === voiceIndentLen) {
|
||||||
|
if (cur) entries.push(cur);
|
||||||
|
const m = line.match(/^\s+(\w[\w./]*)\s*:(.*)/);
|
||||||
|
cur = m ? { lines: [line], name: m[1], rest: m[2].trim() } : { lines: [line], name: null };
|
||||||
|
} else if (cur) {
|
||||||
|
cur.lines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cur) entries.push(cur);
|
||||||
|
|
||||||
|
const keptLines = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.name || entry.name === '_space') { keptLines.push(...entry.lines); continue; }
|
||||||
|
let instrRef = null;
|
||||||
|
if (entry.rest) {
|
||||||
|
const parts = entry.rest.split(/\s+/);
|
||||||
|
if (parts.length >= 3) instrRef = parts[2];
|
||||||
|
} else {
|
||||||
|
for (const l of entry.lines.slice(1)) {
|
||||||
|
const im = l.match(/^\s+instrument\s*:\s*(\S+)/);
|
||||||
|
if (im) { instrRef = im[1]; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (instrNames.has(instrRef ?? entry.name)) keptLines.push(...entry.lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...lines.slice(0, stageIdx + 1), ...keptLines, ...lines.slice(stageEnd)].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
function _patchBarMeta(doc, bar) {
|
function _patchBarMeta(doc, bar) {
|
||||||
const props = [];
|
const props = [];
|
||||||
const sp = stressorToString(bar.stressor);
|
const sp = stressorToString(bar.stressor);
|
||||||
@@ -304,6 +364,7 @@ export function patchScore(rawScoreText, instruments, bars = [], info = null, ar
|
|||||||
}
|
}
|
||||||
|
|
||||||
patchedHeader = _patchInstrumentHeader(patchedHeader, instruments);
|
patchedHeader = _patchInstrumentHeader(patchedHeader, instruments);
|
||||||
|
patchedHeader = _patchStageSection(patchedHeader, instruments);
|
||||||
instruments.forEach(i => {
|
instruments.forEach(i => {
|
||||||
if (!(i.isLinked && !i._modified)) log.push({ level: 'changed', path: `instrument / ${i.name}` });
|
if (!(i.isLinked && !i._modified)) log.push({ level: 'changed', path: `instrument / ${i.name}` });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -570,6 +570,52 @@ ok('deleted instrument removed from output', !delInstrPatched.includes('instrume
|
|||||||
ok('non-deleted instrument preserved', delInstrPatched.includes('instrument ki:'));
|
ok('non-deleted instrument preserved', delInstrPatched.includes('instrument ki:'));
|
||||||
ok('ki re-serialized and logged', delInstrLog.some(e => e.level === 'changed' && e.path?.includes('ki')));
|
ok('ki re-serialized and logged', delInstrLog.some(e => e.level === 'changed' && e.path?.includes('ki')));
|
||||||
|
|
||||||
|
// ── patchScore stage section cleanup ─────────────────────────────────────
|
||||||
|
section('patchScore stage section: deleted instrument removes voice entry');
|
||||||
|
// pi → mapping form, instrument: dev/piano (present) → keep
|
||||||
|
// ki → mapping form, instrument: alpha (deleted) → remove
|
||||||
|
// alpha → string form, implicit name=alpha (deleted) → remove
|
||||||
|
const stageScore = [
|
||||||
|
'stage:',
|
||||||
|
' pi:',
|
||||||
|
' direction: 1|1',
|
||||||
|
' distance: 0',
|
||||||
|
' instrument: dev/piano',
|
||||||
|
' ki:',
|
||||||
|
' direction: 2|1',
|
||||||
|
' distance: 1',
|
||||||
|
' instrument: alpha',
|
||||||
|
' alpha: 2|3 0',
|
||||||
|
'',
|
||||||
|
'instrument dev/piano:',
|
||||||
|
' character:',
|
||||||
|
' A: "1:0,10;1,0"',
|
||||||
|
].join('\n');
|
||||||
|
const stageInstrList = [
|
||||||
|
{ name: 'dev/piano', variations: [], basicProperties: null, volumes: null, timbre: null, fmModulations: [], amModulations: [] },
|
||||||
|
];
|
||||||
|
const { text: stagePatched } = patchScore(stageScore, stageInstrList);
|
||||||
|
ok('voice with explicit deleted instrument (mapping form) removed', !stagePatched.includes(' ki:'));
|
||||||
|
ok('voice with implicit deleted name (string form) removed', !stagePatched.includes(' alpha:'));
|
||||||
|
ok('voice with explicit surviving instrument (mapping form) kept', stagePatched.includes(' pi:'));
|
||||||
|
ok('instrument dev/piano block re-serialized', stagePatched.includes('instrument dev/piano:'));
|
||||||
|
|
||||||
|
const stageScoreKeep = [
|
||||||
|
'stage:',
|
||||||
|
' pi:',
|
||||||
|
' direction: 1|1',
|
||||||
|
' distance: 0',
|
||||||
|
' instrument: dev/piano',
|
||||||
|
' ki: 2|1 1',
|
||||||
|
].join('\n');
|
||||||
|
const stageInstrKeep = [
|
||||||
|
{ name: 'dev/piano', variations: [], basicProperties: null, volumes: null, timbre: null, fmModulations: [], amModulations: [] },
|
||||||
|
{ name: 'ki', variations: [], basicProperties: null, volumes: null, timbre: null, fmModulations: [], amModulations: [] },
|
||||||
|
];
|
||||||
|
const { text: stageKeptPatched } = patchScore(stageScoreKeep, stageInstrKeep);
|
||||||
|
ok('retained voice with explicit instrument key kept', stageKeptPatched.includes(' pi:'));
|
||||||
|
ok('retained voice with implicit instrument name kept', stageKeptPatched.includes(' ki:'));
|
||||||
|
|
||||||
// ── Motif parsing ─────────────────────────────────────────────────────────
|
// ── Motif parsing ─────────────────────────────────────────────────────────
|
||||||
section('Motif parsing — dynamic motif');
|
section('Motif parsing — dynamic motif');
|
||||||
const DYN_MOTIF = `00 bar '001P1L1M1'
|
const DYN_MOTIF = `00 bar '001P1L1M1'
|
||||||
|
|||||||
Reference in New Issue
Block a user