- Verovio single-system engraving, MuseScore audio/MIDI from the same source - React + Remotion player with live camera, colour and orb controls - Express pipeline, render queue and local H.264/AAC export - MIT license plus third-party notices for the reference skill
293 lines
7.5 KiB
JavaScript
293 lines
7.5 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const divisions = 4;
|
|
const typeFor = (d) =>
|
|
({16: 'whole', 8: 'half', 4: 'quarter', 2: 'eighth', 1: '16th'})[d] ?? 'quarter';
|
|
|
|
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
|
|
<score-partwise version="4.0">
|
|
<work><work-title>Morning Window</work-title></work>
|
|
<identification>
|
|
<creator type="composer">Demo · Original</creator>
|
|
<encoding><software>music-visualizer demo generator</software></encoding>
|
|
</identification>
|
|
<part-list><score-part id="P1"><part-name>Piano</part-name></score-part></part-list>
|
|
<part id="P1">
|
|
`;
|
|
|
|
function note(step, octave, duration, opts = {}) {
|
|
const {chord = false, staff = 1, voice = '1', tie, grace = false} = opts;
|
|
const parts = [];
|
|
if (grace) parts.push('<grace/>');
|
|
if (chord) parts.push('<chord/>');
|
|
parts.push(`<pitch><step>${step}</step><octave>${octave}</octave></pitch>`);
|
|
if (!grace) parts.push(`<duration>${duration}</duration>`);
|
|
if (tie === 'start') parts.push('<tie type="start"/>');
|
|
if (tie === 'stop') parts.push('<tie type="stop"/>');
|
|
parts.push(`<voice>${voice}</voice>`);
|
|
if (!grace) parts.push(`<type>${typeFor(duration)}</type>`);
|
|
else parts.push('<type>eighth</type>');
|
|
if (tie) {
|
|
parts.push(`<notations><tied type="${tie}"/></notations>`);
|
|
}
|
|
parts.push(`<staff>${staff}</staff>`);
|
|
return ` <note>${parts.join('')}</note>\n`;
|
|
}
|
|
|
|
function rest(duration, opts = {}) {
|
|
const {staff = 1, voice = '1'} = opts;
|
|
return ` <note><rest/><duration>${duration}</duration><voice>${voice}</voice><type>${typeFor(duration)}</type><staff>${staff}</staff></note>\n`;
|
|
}
|
|
|
|
function leftArpeggio(notes) {
|
|
// 8 eighth notes = 16 divisions
|
|
let out = '';
|
|
for (const [step, octave] of notes) {
|
|
out += note(step, octave, 2, {staff: 2, voice: '5'});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const measures = [
|
|
// 1
|
|
{
|
|
right:
|
|
note('C', 5, 4) +
|
|
note('E', 5, 2) +
|
|
note('G', 5, 2) +
|
|
note('A', 5, 4) +
|
|
note('G', 5, 4),
|
|
left: leftArpeggio([
|
|
['C', 3], ['G', 3], ['E', 4], ['G', 3],
|
|
['C', 3], ['G', 3], ['E', 4], ['G', 3],
|
|
]),
|
|
},
|
|
// 2
|
|
{
|
|
right:
|
|
note('F', 5, 4) +
|
|
note('E', 5, 2) +
|
|
note('D', 5, 2) +
|
|
note('C', 5, 8) +
|
|
note('E', 5, 8, {chord: true}) +
|
|
note('G', 5, 8, {chord: true}),
|
|
left: leftArpeggio([
|
|
['F', 3], ['C', 4], ['A', 3], ['C', 4],
|
|
['F', 3], ['C', 4], ['A', 3], ['C', 4],
|
|
]),
|
|
},
|
|
// 3
|
|
{
|
|
right:
|
|
rest(4) +
|
|
note('G', 5, 2) +
|
|
note('A', 5, 2) +
|
|
note('G', 5, 4) +
|
|
note('E', 5, 4),
|
|
left: leftArpeggio([
|
|
['G', 3], ['D', 4], ['B', 3], ['D', 4],
|
|
['G', 3], ['D', 4], ['B', 3], ['D', 4],
|
|
]),
|
|
},
|
|
// 4
|
|
{
|
|
right:
|
|
note('D', 5, 4) +
|
|
note('E', 5, 4) +
|
|
note('C', 5, 8),
|
|
left: leftArpeggio([
|
|
['C', 3], ['G', 3], ['E', 4], ['G', 3],
|
|
['C', 3], ['G', 3], ['E', 4], ['G', 3],
|
|
]),
|
|
},
|
|
// 5
|
|
{
|
|
right:
|
|
note('E', 5, 4) +
|
|
note('G', 5, 2) +
|
|
note('A', 5, 2) +
|
|
note('C', 6, 4) +
|
|
note('B', 5, 4),
|
|
left: leftArpeggio([
|
|
['A', 3], ['E', 4], ['C', 4], ['E', 4],
|
|
['A', 3], ['E', 4], ['C', 4], ['E', 4],
|
|
]),
|
|
},
|
|
// 6
|
|
{
|
|
right:
|
|
note('A', 5, 4) +
|
|
note('G', 5, 2) +
|
|
note('E', 5, 2) +
|
|
note('D', 5, 4) +
|
|
note('E', 5, 4),
|
|
left: leftArpeggio([
|
|
['F', 3], ['C', 4], ['A', 3], ['C', 4],
|
|
['F', 3], ['C', 4], ['A', 3], ['C', 4],
|
|
]),
|
|
},
|
|
// 7
|
|
{
|
|
right:
|
|
note('G', 5, 4) +
|
|
note('E', 5, 2) +
|
|
note('D', 5, 2) +
|
|
note('C', 5, 8),
|
|
left: leftArpeggio([
|
|
['G', 3], ['D', 4], ['B', 3], ['D', 4],
|
|
['G', 3], ['D', 4], ['B', 3], ['D', 4],
|
|
]),
|
|
},
|
|
// 8
|
|
{
|
|
right:
|
|
note('D', 5, 4) +
|
|
note('E', 5, 2) +
|
|
note('F', 5, 2) +
|
|
note('G', 5, 8, {tie: 'start'}),
|
|
left: leftArpeggio([
|
|
['C', 3], ['G', 3], ['E', 4], ['G', 3],
|
|
['C', 3], ['G', 3], ['E', 4], ['G', 3],
|
|
]),
|
|
},
|
|
// 9
|
|
{
|
|
right:
|
|
note('G', 5, 8, {tie: 'stop'}) +
|
|
note('A', 5, 4) +
|
|
note('G', 5, 4),
|
|
left: leftArpeggio([
|
|
['A', 3], ['E', 4], ['C', 4], ['E', 4],
|
|
['A', 3], ['E', 4], ['C', 4], ['E', 4],
|
|
]),
|
|
},
|
|
// 10
|
|
{
|
|
right:
|
|
note('A', 5, 2, {grace: true}) +
|
|
note('G', 5, 4) +
|
|
note('E', 5, 4) +
|
|
note('D', 5, 8),
|
|
left: leftArpeggio([
|
|
['F', 3], ['C', 4], ['A', 3], ['C', 4],
|
|
['F', 3], ['C', 4], ['A', 3], ['C', 4],
|
|
]),
|
|
},
|
|
// 11
|
|
{
|
|
right:
|
|
note('E', 5, 4) +
|
|
note('G', 5, 2) +
|
|
note('A', 5, 2) +
|
|
note('C', 6, 8),
|
|
left: leftArpeggio([
|
|
['G', 3], ['D', 4], ['B', 3], ['D', 4],
|
|
['G', 3], ['D', 4], ['B', 3], ['D', 4],
|
|
]),
|
|
},
|
|
// 12
|
|
{
|
|
right:
|
|
note('B', 5, 4) +
|
|
note('A', 5, 4) +
|
|
note('G', 5, 8),
|
|
left: leftArpeggio([
|
|
['C', 3], ['G', 3], ['E', 4], ['G', 3],
|
|
['C', 3], ['G', 3], ['E', 4], ['G', 3],
|
|
]),
|
|
},
|
|
// 13
|
|
{
|
|
right:
|
|
note('C', 6, 4) +
|
|
note('B', 5, 2) +
|
|
note('A', 5, 2) +
|
|
note('G', 5, 4) +
|
|
note('E', 5, 4),
|
|
left: leftArpeggio([
|
|
['A', 3], ['E', 4], ['C', 4], ['E', 4],
|
|
['A', 3], ['E', 4], ['C', 4], ['E', 4],
|
|
]),
|
|
},
|
|
// 14
|
|
{
|
|
right:
|
|
note('F', 5, 4) +
|
|
note('E', 5, 2) +
|
|
note('D', 5, 2) +
|
|
note('C', 5, 8),
|
|
left: leftArpeggio([
|
|
['F', 3], ['C', 4], ['A', 3], ['C', 4],
|
|
['F', 3], ['C', 4], ['A', 3], ['C', 4],
|
|
]),
|
|
},
|
|
// 15
|
|
{
|
|
right:
|
|
note('D', 5, 4) +
|
|
note('E', 5, 4) +
|
|
note('G', 5, 8),
|
|
left: leftArpeggio([
|
|
['G', 3], ['D', 4], ['B', 3], ['D', 4],
|
|
['G', 3], ['D', 4], ['B', 3], ['D', 4],
|
|
]),
|
|
},
|
|
// 16
|
|
{
|
|
right:
|
|
note('C', 5, 16) +
|
|
note('E', 5, 16, {chord: true}) +
|
|
note('G', 5, 16, {chord: true}),
|
|
left:
|
|
note('C', 3, 16, {staff: 2, voice: '5'}) +
|
|
note('E', 3, 16, {staff: 2, voice: '5', chord: true}) +
|
|
note('G', 3, 16, {staff: 2, voice: '5', chord: true}),
|
|
},
|
|
];
|
|
|
|
measures.forEach((measure, index) => {
|
|
const number = index + 1;
|
|
xml += ` <measure number="${number}">\n`;
|
|
if (number === 1) {
|
|
xml += ` <attributes>
|
|
<divisions>${divisions}</divisions>
|
|
<key><fifths>0</fifths></key>
|
|
<time><beats>4</beats><beat-type>4</beat-type></time>
|
|
<staves>2</staves>
|
|
<clef number="1"><sign>G</sign><line>2</line></clef>
|
|
<clef number="2"><sign>F</sign><line>4</line></clef>
|
|
</attributes>
|
|
<direction placement="above">
|
|
<direction-type><metronome><beat-unit>quarter</beat-unit><per-minute>96</per-minute></metronome></direction-type>
|
|
<sound tempo="96"/>
|
|
</direction>
|
|
<barline location="left"><repeat direction="forward"/></barline>
|
|
`;
|
|
}
|
|
if (number === 9) {
|
|
xml += ` <direction placement="above">
|
|
<direction-type><metronome><beat-unit>quarter</beat-unit><per-minute>104</per-minute></metronome></direction-type>
|
|
<sound tempo="104"/>
|
|
</direction>
|
|
`;
|
|
}
|
|
xml += measure.right;
|
|
xml += ` <backup><duration>16</duration></backup>\n`;
|
|
xml += measure.left;
|
|
if (number === 4) {
|
|
xml += ` <barline location="right"><repeat direction="backward" times="2"/></barline>\n`;
|
|
}
|
|
xml += ` </measure>\n`;
|
|
});
|
|
|
|
xml += ` </part>
|
|
</score-partwise>
|
|
`;
|
|
|
|
const out = path.resolve('public/demo/demo.musicxml');
|
|
fs.mkdirSync(path.dirname(out), {recursive: true});
|
|
fs.writeFileSync(out, xml, 'utf8');
|
|
console.log(`Wrote ${out} (${xml.length} bytes, ${measures.length} measures)`);
|