From 5324ec3f49f30f364449d2fc69ec778d7e7c7708 Mon Sep 17 00:00:00 2001 From: infojunkie Date: Sun, 25 Aug 2024 21:27:51 -0700 Subject: [PATCH] Runs without crashing on all grooves, missing some instruments and timing detection --- mma | 2 +- src/js/musicxml-grooves.js | 166 ++++++++++++++--- src/xml/drums.xml | 369 +++++++++++++++++++++++++++++-------- 3 files changed, 432 insertions(+), 105 deletions(-) diff --git a/mma b/mma index adc6373b..36154e92 160000 --- a/mma +++ b/mma @@ -1 +1 @@ -Subproject commit adc6373b89af43fb2bfb2fd43f5fb3c6978b5a9c +Subproject commit 36154e9250e8bb70d380704d018f75426b52138d diff --git a/src/js/musicxml-grooves.js b/src/js/musicxml-grooves.js index a5633046..beb67a1d 100755 --- a/src/js/musicxml-grooves.js +++ b/src/js/musicxml-grooves.js @@ -5,7 +5,7 @@ */ const MUSICXML_VERSION = '4.0' -const DIVISIONS = 192 +const DIVISIONS = 384 const INSTRUMENTS = 'src/xml/drums.xml' import fs from 'fs' @@ -40,6 +40,7 @@ const options = { }, 'tempo': { type: 'string', + short: 't', default: '100' } } @@ -55,7 +56,7 @@ const { values: args } = (function() { if ('help' in args) { console.log(` -Usage: musicxml-grooves v${version} [--output|-o /path/to/output] [--grooves|-g comma-separated-grooves] [--validate] [--version|-v] [--help|-h] +Usage: musicxml-grooves v${version} [--output|-o /path/to/output] [--grooves|-g comma-separated-grooves] [--tempo|-t beats-per-minute] [--validate] [--version|-v] [--help|-h] Converts MMA grooves to MusicXML. `.trim()) @@ -118,6 +119,11 @@ function createMusicXML(groove) { musicxml-grooves ${version} ${new Date().toJSON().slice(0, 10)} + + + + + ${createPartList(groove)} @@ -138,9 +144,10 @@ function createPartList(groove) { track.candidateInstrumentIds = [] const midi = track.midi[0] // In grooves.json, all MIDI notes are the same for each track const trackCandidates = SaxonJS.XPath.evaluate(`//instrument[drum[@midi="${midi}"]]/@id`, instruments, { resultForm: 'array' }) - if (!trackCandidates) { - console.error(`No instrument found for MIDI drum voice ${track.voice[0]} (${midi})`) - return partCandidates + if (trackCandidates.length < 1) { + console.warn(`No instrument found for MIDI drum voice ${track.voice[0]} (${midi}). Creating a new one.`) + const instrument = createInstrument(instruments, groove, track) + trackCandidates.push({ value: instrument.getAttribute('id') }) } trackCandidates.forEach(candidate => { const id = candidate.value @@ -202,7 +209,7 @@ function createParts(groove) { return Object.keys(parts).map((partId) => { return ` - ${createPartEntry(groove, parts[partId])} + ${createPartEntry(groove, partId, parts[partId])} `.trim() }).join('') @@ -250,14 +257,20 @@ function createPartListEntry(groove, instrumentId, partId) { `.trim() } -function createPartEntry(groove, part) { +function createPartEntry(groove, partId, part) { // Create part measures by combining the notes of all tracks in the part. // The notes are sorted by time and then by pitch. const instrumentId = part[0].candidateInstrumentIds[0] const instrument = SaxonJS.XPath.evaluate(`//instrument[@id="${instrumentId}"]`, instruments) const beats = parseInt(groove.timeSignature.split('/')[0]) const beatType = parseInt(groove.timeSignature.split('/')[1]) - return part[0].sequence.map((measure, i) => { + const types = { + 2: 'half', + 4: 'quarter', + 8: 'eighth', + 16: '16th' + } + return part[0].sequence.map((_, i) => { const attributes = i > 0 ? '' : ` ${DIVISIONS} @@ -267,10 +280,23 @@ function createPartEntry(groove, part) { percussion - ${instrument.getElementsByTagName('line')[0].textContent} + + ${instrument.getElementsByTagName('staff-lines')[0].textContent} + `.trim() + const direction = partId > 1 ? '' : ` + + + + ${types[beatType]} + ${args['tempo']} + + + + + `.trim() const notes = part.reduce((notes, track) => { return notes.concat(track.sequence[i].split(';').map(note => { const p = note.split(/\s+/).filter(p => !!p) @@ -315,52 +341,134 @@ function createPartEntry(groove, part) { `.trim() : '' const chord = (index > 0 && notes[index - 1].onset === note.onset) ? '' : '' - const duration = DIVISIONS * note.duration + const stem = drum ? `${drum.getElementsByTagName('stem')[0].textContent}` : '' + const notehead = drum ? `${drum.getElementsByTagName('notehead')[0].textContent}` : '' + const duration = Math.round(note.duration * DIVISIONS) return ` ${chord} ${pitch} ${duration} - ${getNoteType(note, index, notes, beatType)} - ${drum.getElementsByTagName('stem')[0].textContent} - ${drum.getElementsByTagName('notehead')[0].textContent} + ${getNoteTiming(note, index, notes, beatType)} + ${stem} + ${notehead} `.trim() }).join('') return ` ${attributes} + ${direction} ${notes} `.trim() }).join('') } -function getNoteType(note, index, notes, beatType) { +function getNoteTiming(note, _index, _notes, beatType) { const types = { - 6: '256th', - 12: '128th', - 24: '64th', - 48: '32th', - 96: '16th', - 192: 'eighth', - 384: 'quarter', - 768: 'half', - 1536: 'whole', + 3: '1024th', + 6: '512th', + 12: '256th', + 24: '128th', + 48: '64th', + 96: '32th', + 192: '16th', + 384: 'eighth', + 768: 'quarter', + 1536: 'half', + 3072: 'whole', } - const duration = note.duration * DIVISIONS * 8 / beatType + const elements = [] + const duration = Math.round(note.duration * DIVISIONS * 8 / beatType) if (duration in types) { - return `${types[duration]}` + elements.push(`${types[duration]}`) } - for (const [entry, type] of Object.entries(types).reverse()) { + else for (const [entry, type] of Object.entries(types).reverse()) { if (entry < duration) { const dots = Math.log(2 - duration / entry) / Math.log(0.5) if (Number.isInteger(dots)) { - return `${types[entry]}${[...Array(dots)].map(_ => '')}` + elements.push(`${type}`) + elements.push(...Array.from(Array(dots), _ => '')) } } } - console.error(`Could not find note duration ${note.duration} = ${entry} in duration map.`) - return ''; + + // TODO Handle odd timings, starting with triplets. + + if (elements.length < 1) { + console.error(`Could not transform note duration ${note.duration} to MusicXML.`) + } + return elements.join('') +} + +function createInstrument(document, _groove, track) { + const createElement = function (target, obj) { + const el = document.createElement(obj.tagName) + if (obj.hasOwnProperty('text')) { + el.appendChild(document.createTextNode(obj.text)) + } + if (obj.hasOwnProperty('attributes')) { + obj.attributes.forEach(attribute => { + el.setAttribute(attribute.name, attribute.value) + }) + } + target.appendChild(el) + if (obj.hasOwnProperty('children')) { + obj.children.forEach(child => { + createElement(el, child); + }); + } + return el; + } + return createElement(instruments.documentElement, { + tagName: 'instrument', + attributes: [{ + name: 'id', value: `unknown-${track.voice[0].toLowerCase()}` + }], + children: [{ + tagName: 'part-name', + attributes: [{ + name: 'lang', value: 'en' + }], + text: `Unknown ${track.voice[0]}`, + }, { + tagName: 'part-abbreviation', + attributes: [{ + name: 'lang', value: 'en' + }], + text: `Unk. ${track.voice[0].substring(0, 4)}.`, + }, { + tagName: 'staff-lines', + text: '1' + }, { + tagName: 'drum', + attributes: [{ + name: 'midi', value: track.midi[0] + }], + children: [{ + tagName: 'instrument-name', + attributes: [{ + name: 'lang', value: 'en' + }], + text: track.voice[0], + }, { + tagName: 'instrument-sound', + text: '' + }, { + tagName: 'display-step', + text: 'E' + }, { + tagName: 'display-octave', + text: '4' + }, { + tagName: 'stem', + text: 'up' + }, { + tagName: 'notehead', + text: 'normal' + }] + }] + }) } diff --git a/src/xml/drums.xml b/src/xml/drums.xml index da5671fd..1e8537ff 100644 --- a/src/xml/drums.xml +++ b/src/xml/drums.xml @@ -1,17 +1,93 @@ - - Drum Set - Dr. s. - 5 - - Pedal Hi-Hat + + High Q + Hi. Q + 1 + + High Q + E + 4 + up + normal + effect.high-q + + + + Slap + Slp. + 1 + + Slap + E + 4 + up + normal + effect.slap + + + + Scratch + Scrt. + 1 + + Scratch Push + E + 4 + up + normal + effect.scratch + + + Scratch Pull + E + 4 + up + back slashed + effect.scratch + + + + Clicks + Clck. + 1 + + Sticks D 4 - down - x - metal.hi-hat + up + normal + effect.scratch + + Square Click + E + 4 + up + square + synth.tone.square + + + Metronome Click + F + 4 + up + normal + effect.metronome-click + + + Metronome Bell + F + 4 + up + diamond + effect.metronome-bell + + + + Bass Drum + B. Dr. + 5 Kick Drum 2 E @@ -28,6 +104,19 @@ normal drum.bass-drum + + Pedal Hi-Hat + D + 4 + down + x + metal.hi-hat + + + + Concert Toms + Toms + 5 Low Tom 2 G @@ -52,90 +141,92 @@ normal drum.tom-tom - - Tambourine - B - 4 + + Mid Tom 1 + D + 5 up - triangle - drum.tambourine + normal + drum.tom-tom + + + High Tom 2 + E + 5 + up + normal + drum.tom-tom + + + High Tom 1 + F + 5 + up + normal + drum.tom-tom + + + Snare Drum + Sn. Dr. + 1 Snare Drum 1 - C - 3 + E + 4 up normal drum.snare-drum Snare Drum 2 - C - 3 + E + 4 up square drum.snare-drum Side Stick - C - 3 + E + 4 up back slashed drum.snare-drum Rim Shot - C - 3 + E + 4 up diamond drum.snare-drum - - Mid Tom 1 - D - 3 - up - normal - drum.tom-tom - + + + Cymbals + Cym. + 5 Ride Cymbal 2 D - 3 + 5 up x metal.cymbal.ride - - High Tom 2 - E - 3 - up - normal - drum.tom-tom - Cowbell E - 3 + 5 up triangle metal.bells.cowbell - - High Tom 1 - F - 3 - up - normal - drum.tom-tom - Ride Cymbal 1 F - 3 + 5 up x metal.cymbal.ride @@ -143,7 +234,7 @@ Ride Bell F - 3 + 5 up diamond metal.cymbal.ride @@ -151,7 +242,7 @@ Open Hi-Hat G - 3 + 5 up circle-x metal.hi-hat @@ -159,7 +250,7 @@ Closed Hi-Hat G - 3 + 5 up x metal.hi-hat @@ -167,7 +258,7 @@ Crash Cymbal 1 A - 3 + 5 up x metal.cymbal.crash @@ -175,7 +266,7 @@ Crash Cymbal 2 B - 3 + 5 up x metal.cymbal.crash @@ -183,7 +274,7 @@ Splash Cymbal C - 2 + 6 up x metal.cymbal.splash @@ -191,37 +282,42 @@ Chinese Cymbal C - 2 + 6 up circle-x metal.cymbal.chinese - - Triangle - Trgl. - 1 - - Mute Triangle + + Tambourine + Tamb. + 1 + + Tambourine E 4 up - x - metal.triangle + normal + drum.tambourine - - Open Triangle + + + Vibraslap + Vibslp. + 1 + + Vibraslap E 4 up normal - metal.triangle + rattle.vibraslap Bongos Bon. - 1 + 1 High Bongo F @@ -239,12 +335,101 @@ drum.bongo - - Woodblock - W.b. - 1 + + Congas + Con. + 1 + + Mute High Conga + F + 4 + up + x + drum.conga + + + Open High Conga + F + 4 + up + normal + drum.conga + + + Low Conga + F + 4 + up + normal + drum.conga + + + + Timbales + Timb. + 1 + + High Timbale + F + 4 + up + normal + drum.timbale + + + Low Timbale + D + 4 + up + normal + drum.timbale + + + + Cabasa + Cabs. + 1 + + Cabasa + E + 4 + up + normal + rattle.cabasa + + + + Maracas + Mrcs. + 1 + + Maracas + E + 4 + up + normal + rattle.maraca + + + + Claves + Clv. + 1 + + Claves + E + 4 + up + normal + wood.claves + + + + Wood Blocks + Wd. Bl. + 1 - High Woodblock + High Wood Block F 4 up @@ -252,7 +437,7 @@ wood.wood-block - Low Woodblock + Low Wood Block D 4 up @@ -260,4 +445,38 @@ wood.wood-block + + Triangle + Trgl. + 1 + + Mute Triangle + E + 4 + up + x + metal.triangle + + + Open Triangle + E + 4 + up + normal + metal.triangle + + + + Shaker + Sh. + 1 + + Shaker + E + 4 + up + normal + rattle.shaker + +