152 lines
6.3 KiB
JavaScript

// Template-based YAML serializer — instrument blocks only (v1).
// Each object selects a template by finding the first entry in its
// selectExportTemplate() list where all required slots have values.
// Placeholders #0, #1, ... are filled with slot values.
function fillTemplate(template, slots) {
return template.replace(/#(\d+)/g, (_, i) => slots[parseInt(i, 10)] ?? '');
}
function indent(text, level) {
const pad = ' '.repeat(level);
return text.split('\n').map((line, i) => {
if (i === 0) return line;
if (line.startsWith('- ')) return pad.slice(2) + line;
return pad + line;
}).join('\n');
}
// ── Shape ──────────────────────────────────────────────────────────────────
function exportCoord(coord) {
let s = `x=${coord.x} y=${coord.y}`;
if (coord.z !== undefined && coord.z !== 1) s += ` z=${coord.z}`;
if (coord.isSharp) s += ` is_sharp=True`;
return s;
}
function exportShape(shape, slotName, level) {
if (!shape) return '';
const coordLines = shape.coords.map(c => ` - coords: ${exportCoord(c)}`).join('\n');
let header = `${slotName}: length=${shape.length}`;
if (shape.start !== undefined) header += ` start=${shape.start}`;
if (shape.z !== undefined && shape.z !== 1) header += ` z=${shape.z}`;
const block = coordLines ? `${header}\n${coordLines}` : header;
return indent(block, level);
}
// ── BasicProperties ────────────────────────────────────────────────────────
function exportBasicProperties(bp, level) {
if (!bp) return '';
const lines = [];
if (bp.oscillator) lines.push(` O: ref=${bp.oscillator}`);
if (bp.A) lines.push(` ${exportShape(bp.A, 'A', 1)}`);
if (bp.S) lines.push(` ${exportShape(bp.S, 'S', 1)}`);
if (bp.R) lines.push(` ${exportShape(bp.R, 'R', 1)}`);
for (const fm of (bp.fmModulations ?? [])) {
const parts = Object.entries(fm).map(([k, v]) => `${k}=${v}`).join(' ');
lines.push(` FM:\n modulation: ${parts}`);
}
if (!lines.length) return '';
return indent('basic_properties:\n' + lines.join('\n'), level);
}
// ── LabelSpec ─────────────────────────────────────────────────────────────
function exportLabelSpec(ls, level) {
const label = ls.label ? ` '${ls.label}'` : '';
const bp = exportBasicProperties(ls.basicProperties, 1);
const body = bp ? `label_spec:${label}\n ${bp}` : `label_spec:${label}`;
return indent(body, level);
}
// ── Variation ─────────────────────────────────────────────────────────────
function exportVariation(v, level) {
const dep = v.dependsOn ? ` depends_on=${v.dependsOn}` : '';
const lines = [`variation:${dep}`];
if (v.basicProperties) lines.push(` ${exportBasicProperties(v.basicProperties, 1)}`);
for (const ls of (v.labelSpecs ?? [])) lines.push(` ${exportLabelSpec(ls, 1)}`);
for (const sv of (v.subvariations ?? [])) lines.push(` ${exportVariation(sv, 1)}`);
if (v.spread?.length) lines.push(` SPREAD: ${v.spread.join(' ')}`);
return indent(lines.join('\n'), level);
}
// ── Instrument ────────────────────────────────────────────────────────────
export function exportInstrument(instr) {
const lines = [];
const name = instr.name;
lines.push(`instrument: '${name}'`);
for (const v of (instr.variations ?? [])) {
lines.push(` character:\n ${exportVariation(v, 2)}`);
}
if (instr.basicProperties) {
lines.push(` character:\n ${exportBasicProperties(instr.basicProperties, 2)}`);
}
if (instr.railsbackCurve) {
lines.push(` ${exportShape(instr.railsbackCurve, 'RAILSBACK_CURVE', 1)}`);
}
if (instr.volumes) {
lines.push(` ${exportShape(instr.volumes, 'VOLUMES', 1)}`);
}
if (instr.timbre) {
lines.push(` ${exportShape(instr.timbre, 'TIMBRE', 1)}`);
}
for (const fm of (instr.fmModulations ?? [])) {
const parts = Object.entries(fm).map(([k, v]) => `${k}=${v}`).join(' ');
lines.push(` FM:\n modulation: ${parts}`);
}
return lines.join('\n');
}
// ── Score patch ────────────────────────────────────────────────────────────
// Replace instrument blocks in rawScoreText with serialized model instruments.
// Non-dirty linked instruments (NOT_CHANGED_SINCE set, not edited) are left as-is
// from rawScoreText. Embedded and dirty instruments are emitted from the model.
export function patchScore(rawScoreText, instruments) {
// Split raw text into instrument blocks and other sections.
// Strategy: locate each `^instrument:` line and replace that block
// (up to next same-indent section or EOF) with the serialized model.
const lines = rawScoreText.split('\n');
const result = [];
const instrMap = {};
for (const instr of instruments) {
const basename = instr.name.includes('/') ? instr.name.split('/').pop() : instr.name;
instrMap[basename] = instr;
instrMap[instr.name] = instr;
}
let i = 0;
while (i < lines.length) {
const line = lines[i];
const m = line.match(/^instrument:\s+'?([^']+)'?/);
if (m) {
const rawName = m[1];
const instr = instrMap[rawName];
if (instr && instr.isDirty) {
// consume the raw block
i++;
while (i < lines.length && (lines[i].startsWith(' ') || lines[i] === '')) i++;
result.push(exportInstrument(instr));
result.push('');
} else {
result.push(line);
i++;
}
} else {
result.push(line);
i++;
}
}
return result.join('\n');
}