Skip to content

Commit

Permalink
Add recognizable names to MIDI track names
Browse files Browse the repository at this point in the history
  • Loading branch information
infojunkie committed Oct 14, 2024
1 parent a9f57f7 commit 02064fa
Show file tree
Hide file tree
Showing 13 changed files with 50 additions and 41 deletions.
2 changes: 1 addition & 1 deletion build/filter.sef.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"N":"package","version":"10","packageVersion":"1","saxonVersion":"SaxonJS 2.6","target":"JS","targetVersion":"2","name":"TOP-LEVEL","relocatable":"true","buildDateTime":"2024-10-11T21:36:41.379-07:00","ns":"xml=~ xsl=~","C":[{"N":"co","binds":"","id":"0","vis":"PUBLIC","ex:uniform":"true","C":[{"N":"globalParam","name":"Q{}filter","sType":"* ","slots":"200","module":"filter.xsl","flags":"r","as":"","ns":"xml=~ xsl=~","C":[{"N":"str","sType":"1AS ","val":"","role":"select"}]}]},{"N":"co","id":"1","binds":"0 1","C":[{"N":"mode","onNo":"TC","flags":"","patternSlots":"0","prec":"","C":[{"N":"templateRule","rank":"0","prec":"0","seq":"1","ns":"xml=~ xsl=~","minImp":"0","flags":"s","slots":"200","line":"23","module":"filter.xsl","expand-text":"false","match":"*[local-name()=tokenize($filter,'\\|')]","prio":"0.5","matches":"NE","C":[{"N":"p.withPredicate","role":"match","sType":"1NE","ns":"= xml=~ fn=~ xsl=~ ","C":[{"N":"p.nodeTest","test":"NE"},{"N":"gc","op":"=","comp":"GAC|http://www.w3.org/2005/xpath-functions/collation/codepoint","card":"1:1","C":[{"N":"fn","name":"local-name","C":[{"N":"dot"}]},{"N":"fn","name":"tokenize","C":[{"N":"treat","as":"AS","diag":"0|0||tokenize","C":[{"N":"check","card":"?","diag":"0|0||tokenize","C":[{"N":"cvUntyped","to":"AS","diag":"0|0||tokenize","C":[{"N":"check","card":"?","diag":"0|0||tokenize","C":[{"N":"data","diag":"0|0||tokenize","C":[{"N":"gVarRef","name":"Q{}filter","bSlot":"0"}]}]}]}]}]},{"N":"str","val":"\\|"}]}]}]},{"N":"empty","sType":"0 ","role":"action"}]},{"N":"templateRule","rank":"1","prec":"0","seq":"0","ns":"xml=~ xsl=~","minImp":"0","flags":"s","slots":"200","line":"17","module":"filter.xsl","expand-text":"false","match":"node()|@*","prio":"-0.5","matches":"N u[NT,NP,NC,NE]","C":[{"N":"p.nodeTest","role":"match","test":"N u[NT,NP,NC,NE]","sType":"1N u[NT,NP,NC,NE]"},{"N":"copy","sType":"1N u[1NT ,1NP ,1NC ,1NE ] ","flags":"cin","role":"action","line":"18","C":[{"N":"applyT","sType":"* ","line":"19","mode":"#unnamed","bSlot":"1","C":[{"N":"docOrder","sType":"*N u[N u[N u[N u[NT,NP],NC],NE],NA]","role":"select","line":"19","C":[{"N":"union","op":"|","sType":"*N u[N u[N u[N u[NT,NP],NC],NE],NA]","ns":"= xml=~ fn=~ xsl=~ ","C":[{"N":"axis","name":"child","nodeTest":"*N u[NT,NP,NC,NE]"},{"N":"axis","name":"attribute","nodeTest":"*NA"}]}]}]}]}]},{"N":"templateRule","rank":"2","prec":"0","seq":"0","ns":"xml=~ xsl=~","minImp":"0","flags":"s","slots":"200","line":"17","module":"filter.xsl","expand-text":"false","match":"node()|@*","prio":"-0.5","matches":"NA","C":[{"N":"p.nodeTest","role":"match","test":"NA","sType":"1NA"},{"N":"copy","sType":"1NA ","flags":"cin","role":"action","line":"18","C":[{"N":"applyT","sType":"* ","line":"19","mode":"#unnamed","bSlot":"1","C":[{"N":"docOrder","sType":"*N u[N u[N u[N u[NT,NP],NC],NE],NA]","role":"select","line":"19","C":[{"N":"union","op":"|","sType":"*N u[N u[N u[N u[NT,NP],NC],NE],NA]","ns":"= xml=~ fn=~ xsl=~ ","C":[{"N":"axis","name":"child","nodeTest":"*N u[NT,NP,NC,NE]"},{"N":"axis","name":"attribute","nodeTest":"*NA"}]}]}]}]}]}]}]},{"N":"overridden"},{"N":"output","C":[{"N":"property","name":"Q{http://saxon.sf.net/}stylesheet-version","value":"10"},{"N":"property","name":"omit-xml-declaration","value":"no"},{"N":"property","name":"indent","value":"yes"}]},{"N":"decimalFormat"}],"Σ":"c7482637"}
{"N":"package","version":"10","packageVersion":"1","saxonVersion":"SaxonJS 2.6","target":"JS","targetVersion":"2","name":"TOP-LEVEL","relocatable":"true","buildDateTime":"2024-10-13T21:18:19.965-07:00","ns":"xml=~ xsl=~","C":[{"N":"co","binds":"","id":"0","vis":"PUBLIC","ex:uniform":"true","C":[{"N":"globalParam","name":"Q{}filter","sType":"* ","slots":"200","module":"filter.xsl","flags":"r","as":"","ns":"xml=~ xsl=~","C":[{"N":"str","sType":"1AS ","val":"","role":"select"}]}]},{"N":"co","id":"1","binds":"0 1","C":[{"N":"mode","onNo":"TC","flags":"","patternSlots":"0","prec":"","C":[{"N":"templateRule","rank":"0","prec":"0","seq":"1","ns":"xml=~ xsl=~","minImp":"0","flags":"s","slots":"200","line":"23","module":"filter.xsl","expand-text":"false","match":"*[local-name()=tokenize($filter,'\\|')]","prio":"0.5","matches":"NE","C":[{"N":"p.withPredicate","role":"match","sType":"1NE","ns":"= xml=~ fn=~ xsl=~ ","C":[{"N":"p.nodeTest","test":"NE"},{"N":"gc","op":"=","comp":"GAC|http://www.w3.org/2005/xpath-functions/collation/codepoint","card":"1:1","C":[{"N":"fn","name":"local-name","C":[{"N":"dot"}]},{"N":"fn","name":"tokenize","C":[{"N":"treat","as":"AS","diag":"0|0||tokenize","C":[{"N":"check","card":"?","diag":"0|0||tokenize","C":[{"N":"cvUntyped","to":"AS","diag":"0|0||tokenize","C":[{"N":"check","card":"?","diag":"0|0||tokenize","C":[{"N":"data","diag":"0|0||tokenize","C":[{"N":"gVarRef","name":"Q{}filter","bSlot":"0"}]}]}]}]}]},{"N":"str","val":"\\|"}]}]}]},{"N":"empty","sType":"0 ","role":"action"}]},{"N":"templateRule","rank":"1","prec":"0","seq":"0","ns":"xml=~ xsl=~","minImp":"0","flags":"s","slots":"200","line":"17","module":"filter.xsl","expand-text":"false","match":"node()|@*","prio":"-0.5","matches":"N u[NT,NP,NC,NE]","C":[{"N":"p.nodeTest","role":"match","test":"N u[NT,NP,NC,NE]","sType":"1N u[NT,NP,NC,NE]"},{"N":"copy","sType":"1N u[1NT ,1NP ,1NC ,1NE ] ","flags":"cin","role":"action","line":"18","C":[{"N":"applyT","sType":"* ","line":"19","mode":"#unnamed","bSlot":"1","C":[{"N":"docOrder","sType":"*N u[N u[N u[N u[NT,NP],NC],NE],NA]","role":"select","line":"19","C":[{"N":"union","op":"|","sType":"*N u[N u[N u[N u[NT,NP],NC],NE],NA]","ns":"= xml=~ fn=~ xsl=~ ","C":[{"N":"axis","name":"child","nodeTest":"*N u[NT,NP,NC,NE]"},{"N":"axis","name":"attribute","nodeTest":"*NA"}]}]}]}]}]},{"N":"templateRule","rank":"2","prec":"0","seq":"0","ns":"xml=~ xsl=~","minImp":"0","flags":"s","slots":"200","line":"17","module":"filter.xsl","expand-text":"false","match":"node()|@*","prio":"-0.5","matches":"NA","C":[{"N":"p.nodeTest","role":"match","test":"NA","sType":"1NA"},{"N":"copy","sType":"1NA ","flags":"cin","role":"action","line":"18","C":[{"N":"applyT","sType":"* ","line":"19","mode":"#unnamed","bSlot":"1","C":[{"N":"docOrder","sType":"*N u[N u[N u[N u[NT,NP],NC],NE],NA]","role":"select","line":"19","C":[{"N":"union","op":"|","sType":"*N u[N u[N u[N u[NT,NP],NC],NE],NA]","ns":"= xml=~ fn=~ xsl=~ ","C":[{"N":"axis","name":"child","nodeTest":"*N u[NT,NP,NC,NE]"},{"N":"axis","name":"attribute","nodeTest":"*NA"}]}]}]}]}]}]}]},{"N":"overridden"},{"N":"output","C":[{"N":"property","name":"Q{http://saxon.sf.net/}stylesheet-version","value":"10"},{"N":"property","name":"omit-xml-declaration","value":"no"},{"N":"property","name":"indent","value":"yes"}]},{"N":"decimalFormat"}],"Σ":"c7787bb7"}
2 changes: 1 addition & 1 deletion build/groove.sef.json

Large diffs are not rendered by default.

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.

2 changes: 1 addition & 1 deletion mma
Submodule mma updated 1 files
+5 −5 MMA/midifuncs.py
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.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "musicxml-midi",
"version": "2.9.0",
"version": "2.10.0",
"description": "MusicXML to MIDI converter",
"type": "module",
"directories": {
Expand Down
47 changes: 24 additions & 23 deletions src/js/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
process.chdir(path.join(__dirname, '..', '..'))

// Import package.json the "easy" way.
// Import package.json
// https://www.stefanjudis.com/snippets/how-to-import-json-files-in-es-modules-node-js/
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
Expand All @@ -43,8 +43,9 @@ class AbortChainError extends Error {
}

const LIMIT_FILE_SIZE = process.env.LIMIT_FILE_SIZE || 1 * 1024 * 1024
const ERROR_BAD_PARAM = 'Expecting a POST multipart/form-data request with `musicXml` field containing a valid MusicXML file.'
const ERROR_MMA_CRASH = 'Conversion failed unexpectedly. Please contact the server operator.'
const ERROR_BAD_MUSICXML = 'Expecting a POST multipart/form-data request with `musicXml` parameter containing a valid MusicXML file.'
const ERROR_UNEXPECTED_ERROR = 'Conversion failed unexpectedly. Please contact the server operator.'
const ERROR_BAD_GROOVE = 'Expecting a `groove` parameter containing a valid MMA groove.'

export const app = express()
app.use(cors())
Expand All @@ -57,7 +58,7 @@ app.use(fileUpload({
}))
app.use(morgan('combined'))

app.get('/', (req, res) => res.json({ name, version, description, author }))
app.get('/', (_, res) => res.json({ name, version, description, author }))

app.get('/grooves', async (req, res) => {
jq.run('.[] | .groove, .description', path.resolve('build/grooves.json'), { raw: true })
Expand Down Expand Up @@ -103,11 +104,13 @@ async function tryCompressedMusicXml(buffer) {

app.post('/convert', async (req, res) => {
if (!req.files || !('musicXml' in req.files) ) {
return res.status(400).json(ERROR_BAD_PARAM)
return res.status(400).json(ERROR_BAD_MUSICXML)
}

// Assemble parameters.
const params = {}
const params = {
filename: req.files.musicXml.name
}
if (req.body) {
['globalGroove'].forEach(param => {
if (param in req.body) params[param] = req.body[param]
Expand All @@ -133,7 +136,7 @@ app.post('/convert', async (req, res) => {
await validateXMLWithXSD(xml, 'src/xsd/musicxml.xsd')
.catch(AbortChainError.chain(error => {
console.error(`[xmllint] ${error.message}`)
res.status(400).send(ERROR_BAD_PARAM)
res.status(400).send(ERROR_BAD_MUSICXML)
}))
const doc = await SaxonJS.getResource({
type: 'xml',
Expand All @@ -142,10 +145,9 @@ app.post('/convert', async (req, res) => {
})
.catch(AbortChainError.chain(error => {
console.error(`[SaxonJS] ${error.code}: ${error.message}`)
res.status(400).send(ERROR_BAD_PARAM)
res.status(400).send(ERROR_BAD_MUSICXML)
}))
const title = SaxonJS.XPath.evaluate('//work/work-title/text()', doc)?.nodeValue || '(untitled)'
console.info(`[SaxonJS] Transforming document '${title}'...`)
console.info(`[SaxonJS] Transforming document ${req.files.musicXml.name}...`)
const mma = await SaxonJS.transform({
stylesheetFileName: 'build/mma.sef.json',
sourceNode: doc,
Expand All @@ -154,14 +156,14 @@ app.post('/convert', async (req, res) => {
}, 'async')
.catch(AbortChainError.chain(error => {
console.error(`[SaxonJS] ${error.code}: ${error.message}`)
res.status(400).send(ERROR_BAD_PARAM)
res.status(400).send(ERROR_UNEXPECTED_ERROR)
}))
const execResult = await exec('echo "$mma" | ${MMA_HOME:-mma}/mma.py -II -f "$out" -', {
env: { ...process.env, 'mma': mma.principalResult, 'out': cacheFile }
})
.catch(AbortChainError.chain(error => {
console.error(`[MMA] ${error.stdout.replace(/^\s+|\s+$/g, '')}`)
res.status(500).send(ERROR_MMA_CRASH)
res.status(500).send(ERROR_UNEXPECTED_ERROR)
}))
console.info('[MMA] ' + execResult.stdout.replace(/^\s+|\s+$/g, ''))
return res.status(200).sendFile(cacheFile)
Expand All @@ -171,24 +173,23 @@ app.post('/convert', async (req, res) => {
}
})

app.post('/groove', async (req, res, next) => {
app.post('/groove', async (req, res) => {
if (!req.body || !('groove' in req.body) ) {
return res.status(400).json(ERROR_BAD_PARAM)
return res.status(400).json(ERROR_BAD_GROOVE)
}

// Assemble parameters.
const params = {
'groove': null,
'groove': req.body.groove,
'chords': 'z',
'tempo': '120',
'count': '4',
'keysig': 'C'
}
if (req.body) {
['groove', 'chords', 'tempo', 'count', 'keysig'].forEach(param => {
if (param in req.body) params[param] = req.body[param]
})
}
};

['chords', 'tempo', 'count', 'keysig'].forEach(param => {
if (param in req.body) params[param] = req.body[param]
})

// Check first in cache.
const hash = crypto.createHash('sha256')
Expand All @@ -214,14 +215,14 @@ app.post('/groove', async (req, res, next) => {
}, 'async')
.catch(AbortChainError.chain(error => {
console.error(`[SaxonJS] ${error.code}: ${error.message}`)
res.status(400).send(ERROR_BAD_PARAM)
res.status(400).send(ERROR_UNEXPECTED_ERROR)
}))
const execResult = await exec('echo "$mma" | ${MMA_HOME:-mma}/mma.py -II -f "$out" -', {
env: { ...process.env, 'mma': mma.principalResult, 'out': cacheFile }
})
.catch(AbortChainError.chain(error => {
console.error(`[MMA] ${error.stdout.replace(/^\s+|\s+$/g, '')}`)
res.status(500).send(ERROR_MMA_CRASH)
res.status(500).send(ERROR_UNEXPECTED_ERROR)
}))
console.info('[MMA] ' + execResult.stdout.replace(/^\s+|\s+$/g, ''))
return res.status(200).sendFile(cacheFile)
Expand Down
11 changes: 9 additions & 2 deletions src/xsl/mma.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<xsl:param name="melodyVoice" select="1"/>
<xsl:param name="globalGroove" select="''"/>
<xsl:param name="renumberMeasures" as="xs:boolean" select="false()"/>
<xsl:param name="filename" select="tokenize(document-uri(/), '/')[last()]"/>

<!--
Global state.
Expand Down Expand Up @@ -187,15 +188,21 @@
<xsl:text>
MidiText Generated by github.com/infojunkie/musicxml-midi

Begin Chord-Custom
MidiTName Metadata track for </xsl:text><xsl:value-of select="$filename"/><xsl:text>

Begin Chord-Sequence
Voice </xsl:text><xsl:value-of select="$chordInstrument"/><xsl:text>
Octave 5
Articulate 80
Volume f
End

Chord-Sequence MidiTName Chord track for </xsl:text><xsl:value-of select="$filename"/><xsl:text>

Solo Voice </xsl:text><xsl:value-of select="$melodyInstrument"/><xsl:text>

Solo MidiTName Melody track for </xsl:text><xsl:value-of select="$filename"/><xsl:text>

DefChord mb6 (0, 3, 7, 8) (0, 2, 3, 5, 7, 8, 10)
DefChord 7(add6) (0, 4, 7, 9, 10) (0, 2, 4, 5, 7, 9, 10)
DefChord +(addM7)(add9) (0, 4, 8, 11, 14) (0, 2, 4, 5, 8, 9, 11)
Expand Down Expand Up @@ -355,7 +362,7 @@ BeatAdjust <xsl:value-of select="$durationDifference"/>
<xsl:template match="harmony" mode="sequence">
<xsl:param name="start"/>
<xsl:if test="$start = 1">
Chord-Custom Sequence { </xsl:if>
Chord-Sequence Sequence { </xsl:if>
<xsl:value-of select="$start"/><xsl:text> </xsl:text>
<!--
Calculate this chord's duration, which is the total duration of following non-chord notes until next harmony element.
Expand Down
7 changes: 4 additions & 3 deletions test/03-mma.bats
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ set -euo pipefail
echo "$mma" |${MMA_HOME:-mma}/mma.py -II -n -
run echo $mma
assert_output --partial 'Groove Jazz54 MidiMark Groove:Jazz54'
assert_output --partial 'MidiTName Metadata track for take-five.musicxml'
}

@test "mma produces a valid file for take-five with null 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 -
run echo $mma
assert_output --partial 'Chord-Custom Sequence { 1 576t 50; 4 384t 50; } MidiMark Measure:1:2500 Solo Riff 576tr;384tr; Ebm@1 Bbm7@4'
assert_output --partial 'Chord-Sequence Sequence { 1 576t 50; 4 384t 50; } MidiMark Measure:1:2500 Solo Riff 576tr;384tr; Ebm@1 Bbm7@4'
}

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

@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 -
run echo $mma
assert_output --partial 'Chord-Custom Sequence { 1 384t 50; 3 384t 50; }'
assert_output --partial 'Chord-Sequence Sequence { 1 384t 50; 3 384t 50; }'
assert_output --partial 'Solo Riff 96tfn+;96ten+;96ten+;96tdn+;96ten+;96tg#+;96tcn++;96tbn+;'
assert_output --partial 'E+@1 E7@3'
}
Expand Down
6 changes: 3 additions & 3 deletions test/server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('MusicXML to MIDI conversion server', () => {

test('should convert valid MusicXML files', async () => {
const file = 'test/data/take-five.musicxml'
const cacheFile = getCacheFile(file, {})
const cacheFile = getCacheFile(file, { filename: 'take-five.musicxml' })
try { fs.unlinkSync(cacheFile) } catch {}
const res = await request(app).post('/convert').attach('musicXml', file).responseType('blob')
expect(res.statusCode).toEqual(200)
Expand All @@ -49,7 +49,7 @@ describe('MusicXML to MIDI conversion server', () => {

test('should convert valid compressed MusicXML files', async () => {
const file = 'test/data/take-five.mxl'
const cacheFile = getCacheFile(file, {})
const cacheFile = getCacheFile(file, { filename: 'take-five.mxl' })
try { fs.unlinkSync(cacheFile) } catch {}
const res = await request(app).post('/convert').attach('musicXml', file).responseType('blob')
expect(res.statusCode).toEqual(200)
Expand All @@ -59,7 +59,7 @@ describe('MusicXML to MIDI conversion server', () => {

test('should cache valid MusicXML files and use the cache', async () => {
const file = 'test/data/take-five.musicxml'
const cacheFile = getCacheFile(file, {})
const cacheFile = getCacheFile(file, { filename: 'take-five.musicxml' })
try { fs.unlinkSync(cacheFile) } catch {}
await request(app).post('/convert').attach('musicXml', file)
expect(fs.existsSync(cacheFile)).toBeTruthy()
Expand Down

0 comments on commit 02064fa

Please sign in to comment.