Skip to content

Commit

Permalink
Fix #47 Generate groove from xsl
Browse files Browse the repository at this point in the history
  • Loading branch information
infojunkie committed Aug 13, 2024
1 parent 54cead0 commit 605e577
Show file tree
Hide file tree
Showing 12 changed files with 48 additions and 55 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ A suite of tools to convert MusicXML scores to MIDI via [Musical MIDI Accompanim
# Converting a MusicXML score
- `npm run --silent convert:unroll song.musicxml` to unroll a MusicXML score by expanding all jumps and repeats at `stdout`
- `npm run --silent convert:mma song.musicxml` to both unroll a score and convert it to an MMA script at `stdout`
- `npm run --silent convert:groove groove-name [chords="A,B,C"] [tempo=X] [count=Y] [keysig=Z]` to generate a groove MMA script at `stdout`
- `npm run convert:midi /path/to/song.mma` to convert an MMA script to MIDI `/path/to/song.mid`
- `npm run --silent convert:timemap song.musicxml` to convert a score to a timemap JSON file at `stdout`
- `./scripts/midi-timemap.js song.mid` to convert a MIDI file to a timemap JSON file at `stdout`
Expand Down
2 changes: 1 addition & 1 deletion build/mma.sef.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/musicxml.sef.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/timemap.sef.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/unroll.sef.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "musicxml-midi",
"version": "2.5.2",
"version": "2.6.0",
"description": "MusicXML to MIDI converter",
"type": "module",
"directories": {
Expand All @@ -19,11 +19,10 @@
"convert:unroll": "run() { xslt3 -xsl:src/xsl/unroll.xsl -s:\"$1\" ${@:2}; }; run",
"convert:timemap": "run() { xslt3 -xsl:src/xsl/timemap.xsl -s:\"$1\" ${@:2}; }; run",
"convert:mma": "run() { xslt3 -xsl:src/xsl/mma.xsl -s:\"$1\" ${@:2}; }; run",
"convert:groove": "run() { xslt3 -it:groove -xsl:src/xsl/groove.xsl groove=\"$1\" ${@:2}; }; run",
"convert:midi": "run() { ${MMA_HOME:-mma}/mma.py -II \"$1\" -f \"${1/.mma/.mid}\"; }; run",
"convert": "run() { mma=$(xslt3 -xsl:build/mma.sef.json -s:\"$1\" useSef=1 ${@:2}); echo \"$mma\" | ${MMA_HOME:-mma}/mma.py -II -f \"${1/.musicxml/.mid}\" -; }; run",
"debug:chord": "run() { echo \"PrintChord $1\" | ${MMA_HOME:-mma}/mma.py -n -; }; run",
"debug:grooves": "find ${MMA_HOME:-mma}/lib grooves -name '*.mma' | while read f; do MMA_ENCODING=utf-8 ${MMA_HOME:-mma}/mma.py -Dbo \"$f\" | tail -n +2; done",
"debug:preview": "run() { ${MMA_HOME:-mma}/mma.py -V \"$@\"; }; run",
"debug:musicxml": "run() { xslt3 -xsl:src/xsl/musicxml.xsl -s:\"$1\" ${@:2}; }; run",
"validate:musicxml": "run() { xmllint --noout --schema src/xsd/musicxml.xsd \"$1\"; }; run",
"validate:mma": "run() { ${MMA_HOME:-mma}/mma.py -II -n \"$1\"; }; run",
Expand Down
33 changes: 13 additions & 20 deletions src/js/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,27 +171,9 @@ app.post('/groove', async (req, res, next) => {
})
}

// Generate MMA.
const chords = params['chords'].split(',').map(s => s.trim())
const measures = [...Array(parseInt(params['count']))].map((_, index) => {
return `
MidiMark 0 Measure:${index}: $Duration
${chords[index % chords.length]}
`.trim()
}).join('\n')
const mma = `
MidiText Generated by musicxml-midi converter github.com/infojunkie/musicxml-midi
KeySig ${params['keysig']}
Tempo ${params['tempo']}
Groove ${params['groove']}
MidiMark Groove:${params['groove']}
Set Duration $( round( $_Time * 60000 / $_Tempo ) )
${measures}
`.trim()

// Check first in cache.
const hash = crypto.createHash('sha256')
hash.update(mma)
hash.update(JSON.stringify(params))
const sig = hash.digest('hex')
const cacheFile = path.resolve(path.join(process.env.CACHE_DIR || 'cache', `${sig}.mid`))
try {
Expand All @@ -205,8 +187,19 @@ ${measures}
}

try {
console.info(`[SaxonJS] Generating groove '${params['groove']}'...`)
const mma = await SaxonJS.transform({
stylesheetFileName: 'build/groove.sef.json',
destination: 'serialized',
stylesheetParams: params,
initialTemplate: 'groove',
}, 'async')
.catch(AbortChainError.chain(error => {
console.error(`[SaxonJS] ${error.code}: ${error.message}`)
res.status(400).send(ERROR_BAD_PARAM)
}))
const execResult = await exec('echo "$mma" | ${MMA_HOME:-mma}/mma.py -II -f "$out" -', {
env: { ...process.env, 'mma': mma, 'out': cacheFile }
env: { ...process.env, 'mma': mma.principalResult, 'out': cacheFile }
})
.catch(AbortChainError.chain(error => {
console.error(`[MMA] ${error.stdout.replace(/^\s+|\s+$/g, '')}`)
Expand Down
2 changes: 1 addition & 1 deletion src/xsl/mma.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@
-->
<xsl:template match="score-partwise">
<xsl:text>
MidiText Generated by musicxml-midi converter https://github.com/infojunkie/musicxml-midi
MidiText Generated by github.com/infojunkie/musicxml-midi

Begin Chord-Custom
Voice </xsl:text><xsl:value-of select="$chordInstrument"/><xsl:text>
Expand Down
14 changes: 7 additions & 7 deletions test/02-unroll.bats
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ set -euo pipefail

@test "unroll produces a valid file for repeats" {
unroll=$(xslt3 -xsl:src/xsl/unroll.xsl -s:test/data/repeats.musicxml)
echo "${unroll}" | xmllint --schema src/xsd/musicxml.xsd --nonet --noout -
echo "$unroll" | xmllint --schema src/xsd/musicxml.xsd --nonet --noout -
}

teardown() {
Expand All @@ -24,18 +24,18 @@ teardown() {

@test "unroll maintains implicit state" {
unroll=$(xslt3 -xsl:src/xsl/unroll.xsl -s:test/data/blue-bag-folly.musicxml)
echo "${unroll}" | xmllint --schema src/xsd/musicxml.xsd --nonet --noout -
run $(echo "${unroll}" | xmllint --xpath "count(//measure[@number='11']//sound[@tempo])" -)
echo "$unroll" | xmllint --schema src/xsd/musicxml.xsd --nonet --noout -
run $(echo "$unroll" | xmllint --xpath "count(//measure[@number='11']//sound[@tempo])" -)
assert_output --partial '1'
}

@test "unroll with renumbering produces a valid file" {
unroll=$(xslt3 -xsl:src/xsl/unroll.xsl -s:test/data/salma-ya-salama.musicxml "renumberMeasures=1")
echo "${unroll}" | xmllint --schema src/xsd/musicxml.xsd --nonet --noout -
run $(echo "${unroll}" | xmllint --xpath "count(//measure[@number='1'])" -)
echo "$unroll" | xmllint --schema src/xsd/musicxml.xsd --nonet --noout -
run $(echo "$unroll" | xmllint --xpath "count(//measure[@number='1'])" -)
assert_output --partial '1'
unroll=$(xslt3 -xsl:src/xsl/unroll.xsl -s:test/data/salma-ya-salama.musicxml "renumberMeasures=0")
echo "${unroll}" | xmllint --schema src/xsd/musicxml.xsd --nonet --noout -
run $(echo "${unroll}" | xmllint --xpath "count(//measure[@number='1'])" -)
echo "$unroll" | xmllint --schema src/xsd/musicxml.xsd --nonet --noout -
run $(echo "$unroll" | xmllint --xpath "count(//measure[@number='1'])" -)
assert_output --partial '2'
}
30 changes: 15 additions & 15 deletions test/03-mma.bats
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ set -euo pipefail

@test "mma produces a valid file for take-five with swing groove" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/take-five.musicxml)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
run echo $mma
assert_output --partial 'Groove Jazz54 MidiMark Groove:Jazz54'
assert_output --partial 'Solo Riff 576tr;384tr;'
Expand All @@ -15,7 +15,7 @@ set -euo pipefail

@test "mma produces a valid file for take-five with overridden groove" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/take-five.musicxml globalGroove=None)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
run echo $mma
assert_output --partial 'Chord-Custom Sequence { 1 576t 50; 4 384t 50; }'
assert_output --partial 'Solo Riff 576tr;384tr;'
Expand All @@ -24,7 +24,7 @@ set -euo pipefail

@test "mma produces a valid file for take-five with default groove" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/take-five.musicxml globalGroove=Default)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
run echo $mma
assert_output --partial 'Groove Jazz54 MidiMark Groove:Jazz54'
assert_output --partial 'Solo Riff 576tr;384tr;'
Expand All @@ -33,7 +33,7 @@ set -euo pipefail

@test "mma produces a valid file for take-five with unknown groove" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/take-five-unknown.musicxml)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
run echo $mma
assert_output --partial 'Chord-Custom Sequence { 1 576t 50; 4 384t 50; }'
assert_output --partial 'Solo Riff 576tr;384tr;'
Expand All @@ -42,7 +42,7 @@ set -euo pipefail

@test "mma produces a valid file for salma-ya-salama" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/salma-ya-salama.musicxml)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
run echo $mma
assert_output --partial 'Chord-Custom Sequence { 1 384t 50; 3 384t 50; }'
assert_output --partial 'Solo Riff 96tfn+;96ten+;96ten+;96tdn+;96ten+;96tg#+;96tcn++;96tbn+;'
Expand All @@ -51,33 +51,33 @@ set -euo pipefail

@test "mma produces a valid file for salma-ya-salama with custom groove" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/salma-ya-salama.musicxml globalGroove=Maqsum)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
run echo $mma
assert_output --partial 'Groove Maqsum MidiMark Groove:Maqsum'
}

@test "mma produces a correct sequence for repeats" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/repeats.musicxml)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
run echo $mma
assert_output --partial 'KeySig 0 Time 4 TimeSig 4/4 MidiMark Measure:0:2000 Solo Riff 768tr; z MidiMark Measure:1:2000 Solo Riff 768tr; z KeySig 0 Time 4 TimeSig 4/4 MidiMark Measure:0:2000 Solo Riff 768tr; z MidiMark Measure:1:2000 Solo Riff 768tr; z KeySig 0 Time 4 TimeSig 4/4 MidiMark Measure:0:2000 Solo Riff 768tr; z MidiMark Measure:1:2000 Solo Riff 768tr; z MidiMark Measure:2:2000 Solo Riff 768tr; z MidiMark Measure:3:2000 Solo Riff 768tr; z MidiMark Measure:2:2000 Solo Riff 768tr; z MidiMark Measure:3:2000 Solo Riff 768tr; z MidiMark Measure:2:2000 Solo Riff 768tr; z MidiMark Measure:4:2000 Solo Riff 768tr; z MidiMark Measure:5:2000 Solo Riff 768tr; z MidiMark Measure:2:2000 Solo Riff 768tr; z MidiMark Measure:4:2000 Solo Riff 768tr; z MidiMark Measure:5:2000 Solo Riff 768tr; z MidiMark Measure:2:2000 Solo Riff 768tr; z MidiMark Measure:4:2000 Solo Riff 768tr; z MidiMark Measure:5:2000 Solo Riff 768tr; z MidiMark Measure:2:2000 Solo Riff 768tr; z MidiMark Measure:4:2000 Solo Riff 768tr; z MidiMark Measure:5:2000 Solo Riff 768tr; z MidiMark Measure:6:2000 Solo Riff 768tr; z MidiMark Measure:7:2000 Solo Riff 768tr; z MidiMark Measure:8:2000 Solo Riff 768tr; z MidiMark Measure:7:2000 Solo Riff 768tr; z MidiMark Measure:9:2000 Solo Riff 768tr; z MidiMark Measure:7:2000 Solo Riff 768tr; z MidiMark Measure:8:2000 Solo Riff 768tr; z MidiMark Measure:7:2000 Solo Riff 768tr; z MidiMark Measure:9:2000 Solo Riff 768tr; z MidiMark Measure:7:2000 Solo Riff 768tr; z MidiMark Measure:8:2000 Solo Riff 768tr; z MidiMark Measure:7:2000 Solo Riff 768tr; z MidiMark Measure:9:2000 Solo Riff 768tr; z MidiMark Measure:7:2000 Solo Riff 768tr; z MidiMark Measure:8:2000 Solo Riff 768tr; z MidiMark Measure:7:2000 Solo Riff 768tr; z MidiMark Measure:10:2000 Solo Riff 768tr; z MidiMark Measure:11:2000 Solo Riff 768tr; z KeySig 0 Time 4 TimeSig 4/4 MidiMark Measure:0:2000 Solo Riff 768tr; z MidiMark Measure:1:2000 Solo Riff 768tr; z MidiMark Measure:2:2000 Solo Riff 768tr; z MidiMark Measure:3:2000 Solo Riff 768tr; z MidiMark Measure:12:2000 Solo Riff 768tr; z MidiMark Measure:13:2000 Solo Riff 768tr; z MidiMark Measure:6:2000 Solo Riff 768tr; z MidiMark Measure:7:2000 Solo Riff 768tr; z'
}

@test "mma produces a valid file for chords" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/chords.musicxml)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
}

@test "mma produces a valid and correct file for ties" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/ties.musicxml)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
run echo $mma
assert_output --partial 'KeySig 0 Time 4 TimeSig 4/4 MidiMark Measure:0:2000 Solo Riff 192tcn+;192tdn+;384ten+; z MidiMark Measure:1:2000 Solo Riff 336tcn+;48tr;384tdn+; z MidiMark Measure:2:2000 Solo Riff 192tcn+;192tdn+;192ten+;576tfn+~; z MidiMark Measure:3:2000 Solo Riff ~1344tcn+~; z MidiMark Measure:4:2000 Solo Riff ~<>~; z MidiMark Measure:5:2000 Solo Riff ~768tcn+,en+,gn+~; z MidiMark Measure:6:2000 Solo Riff ~576tcn+,en+,gn+; z MidiMark Measure:7:2000 Solo Riff 192tfn,an,dn+;192tan,dn+,fn+;192tan,cn+,en+;192tfn,an,dn+; z'
}

@test "mma produces a valid and correct file for key-signatures" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/key-signatures.musicxml)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
run echo $mma
assert_output --partial 'KeySig 0'
assert_output --partial 'KeySig 1# Major'
Expand Down Expand Up @@ -108,29 +108,29 @@ set -euo pipefail

@test "mma produces a valid file for aquele-um" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/aquele-um.musicxml)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
}

@test "mma produces a valid file for batwanness-beek" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/batwanness-beek.musicxml)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
run echo $mma
assert_output --partial 'Solo MidiNote PB 672 2048 Solo MidiNote PB 768 0'
}

@test "mma produces a valid file for asa-branca" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/asa-branca.musicxml)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
run echo $mma
assert_output --partial 'BeatAdjust -1'
}

@test "mma produces a valid file for that-s-what-friends-are-for" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/that-s-what-friends-are-for.musicxml)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
}

@test "mma produces a valid file for capim" {
mma=$(xslt3 -xsl:src/xsl/mma.xsl -s:test/data/capim.musicxml)
echo $mma | ${MMA_HOME:-mma}/mma.py -II -n -
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
}
6 changes: 3 additions & 3 deletions test/04-timemap.bats
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ set -euo pipefail

@test "timemap produces a valid and correct JSON file for asa-branca" {
timemap=$(xslt3 -xsl:src/xsl/timemap.xsl -s:test/data/asa-branca.musicxml)
echo "${timemap}" | jq type 1>/dev/null
select=$(echo "${timemap}" | jq '.[] | select(.measure == 1)')
run echo ${select}
echo "$timemap" | jq type 1>/dev/null
select=$(echo "$timemap" | jq '.[] | select(.measure == 1)')
run echo $select
assert_output --partial '"timestamp": 500'
assert_output --partial '"duration": 1000'
}

0 comments on commit 605e577

Please sign in to comment.