152 lines
6.3 KiB
JavaScript
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');
|
|
}
|