Skip to content

Commit

Permalink
Support multiple voices per part
Browse files Browse the repository at this point in the history
  • Loading branch information
infojunkie committed Sep 3, 2024
1 parent b50651a commit fb6244b
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 25 deletions.
63 changes: 39 additions & 24 deletions src/js/musicxml-grooves.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const DURATION_256th = DIVISIONS*8/256
const DURATION_512th = DIVISIONS*8/512
const DURATION_1024th = DIVISIONS*8/1024
const INSTRUMENTS = 'src/xml/drums.xml'
const STACCATO = 0.2 // 20% less than regular duration

import fs from 'fs'
import xmlFormat from 'xml-formatter'
Expand Down Expand Up @@ -269,7 +268,7 @@ function createPartListEntry(groove, instrumentId, partId) {
const scoreInstrumentId = `P${partId}-I${drum.getAttribute('midi')}`
groove.tracks.filter(t => t.midi[0].toString() === drum.getAttribute('midi').toString()).forEach((track, index) => {
if (index > 0) {
console.warn(`[${track.track}] Found a track with duplicate drum voice ${track.voice[0]}. This may indicate a name mismatch in the source groove.`)
console.warn(`[${track.track}] Found a track with duplicate drum voice ${track.voice[0]}. This may indicate a name mismatch in the MMA groove.`)
}
track.partId = partId
track.scoreInstrumentId = scoreInstrumentId
Expand Down Expand Up @@ -374,7 +373,10 @@ function createMeasureNotes(groove, part, i) {
const beatType = parseInt(groove.timeSignature.split('/')[1])

// Step 1: Gather all notes and parse them.
// Ignore full rests and notes with velocity 0.
// TODO Handle case of empty measure.
return part.reduce((notes, track) => {
const voice = SaxonJS.XPath.evaluate(`//instrument[@id="${instrumentId}"]/drum[@midi="${track.midi[0]}"]/voice/text()`, instruments) ?? '1'
return notes.concat(track.sequence[i].split(';').map(note => {
const parts = note.split(/\s+/).filter(part => !!part)
return parts[0] !== 'z' ? {
Expand All @@ -383,32 +385,42 @@ function createMeasureNotes(groove, part, i) {
duration: undefined,
velocity: parseInt(parts[2]),
partId: track.partId,
track: track.track
track: track.track,
voice: parseInt(voice)
} : undefined
}).filter(note => !!note))
}).filter(note => !!note && note.velocity > 0))
}, [])

// Step 2: Sort the notes.
// Step 2: Sort the notes, first by voice, then by onset.
.sort((n1, n2) => {
return n1.onset - n2.onset
return n1.voice !== n2.voice ?
n1.voice - n2.voice :
n1.onset - n2.onset
})

// Step 3: Calculate note durations.
.reduce((notes, note, index, source) => {
const previous = notes.length > 0 ? notes[notes.length-1].onset : 1
// A note's duration is the difference between the next note's onset and its own onset.
// Therefore, at each note, we can calculate the previous note's duration.
// At the first note of each voice, if the onset is > 1, we insert a rest to start the measure.
// At the last note of each voice, the duration is the remaining time until the measure end.
.reduce((notes, note, index, input) => {
const isFirstNote = notes.length === 0 || notes[notes.length - 1].voice !== note.voice
const isLastNote = index === input.length - 1 || input[index + 1].voice !== note.voice
const previous = isFirstNote ? 1 : notes[notes.length-1].onset
const duration = note.onset - previous
if (duration > 0) {
// If the first note starts later than 1, insert a rest before the note.
if (notes.length === 0) {
if (isFirstNote) {
notes.push({
midi: undefined, // rest
onset: previous,
duration,
track: note.track
track: note.track,
voice: note.voice
})
}
else {
notes.filter(note => note.onset === previous).forEach(note => { note.duration = duration })
notes.filter(n => n.onset === previous && n.voice === note.voice).forEach(note => { note.duration = duration })
}
}

Expand All @@ -418,18 +430,20 @@ function createMeasureNotes(groove, part, i) {
}

// If we're at the end of the measure, calculate the duration of all remaining notes.
if (index === source.length - 1) {
if (isLastNote) {
const duration = beats + 1 - note.onset
if (duration <= 0) {
console.warn(`[${note.track}] Found note with duration <= 0. Ignoring.`)
}
notes.filter(note => note.duration === undefined).forEach(note => { note.duration = duration })
notes.filter(n => n.duration === undefined && n.voice === note.voice).forEach(note => { note.duration = duration })
}
return notes
}, []).filter(note => note.duration > 0)

// Step 4. Insert extra rests where needed.
.reduce((notes, note, index, source) => {
// Each note is at most 1 beat, and does not cross beat boundaries.
// Any note with duration that crosses beat boundaries get truncated and the rest of the time is filled with rests.
.reduce((notes, note) => {
const extra = []
const boundary = Math.floor(note.onset) + 1 - note.onset
const spillover = note.duration - boundary
Expand All @@ -440,7 +454,8 @@ function createMeasureNotes(groove, part, i) {
midi: undefined,
duration: Math.min(1, s),
onset: Math.floor(note.onset) + 1 + Math.ceil(spillover - s),
track: note.track
track: note.track,
voice: note.voice
})
}
}
Expand All @@ -449,6 +464,7 @@ function createMeasureNotes(groove, part, i) {
}, [])

// Step 5: Generate MusicXML.
// When voices change, we backup to the beginning of the measure.
.map((note, index, notes) => {
const drum = note.midi ? SaxonJS.XPath.evaluate(`//instrument[@id="${instrumentId}"]/drum[@midi="${note.midi}"]`, instruments) : undefined
const pitch = drum ? `
Expand All @@ -462,12 +478,19 @@ function createMeasureNotes(groove, part, i) {
const stem = drum ? `<stem>${drum.getElementsByTagName('stem')[0].textContent}</stem>` : ''
const notehead = drum ? `<notehead>${drum.getElementsByTagName('notehead')[0].textContent}</notehead>` : ''
const duration = Math.round(note.duration * DIVISIONS)
const backup = (index > 0 && notes[index - 1].voice !== note.voice) ? `
<backup>
<duration>${beats * DIVISIONS}</duration>
</backup>
`.trim() : ''
return `
${backup}
<note>
${chord}
${pitch}
<duration>${duration}</duration>
${instrument}
<voice>${note.voice}</voice>
${createNoteTiming(note, index, notes, beatType)}
${stem}
${notehead}
Expand Down Expand Up @@ -530,15 +553,7 @@ function createNoteTiming(note, _index, _notes, beatType) {
}
}

// Detect swung notes.

// Detect staccato on simple types as within 20% of the original duration.
// The <staccato> element occurs later in the note's MusicXML, so we just remember it here.
if (entry >= DURATION_32nd && entry - scoreDuration <= entry * STACCATO) {
elements.push(`<type>${type}</type>`)
note.staccato = true
break
}
// TODO Detect swung notes.
}

if (elements.length < 1) {
Expand Down
28 changes: 27 additions & 1 deletion src/xml/drums.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
<stem>down</stem>
<notehead>normal</notehead>
<instrument-sound>drum.bass-drum</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="36">
<instrument-name lang="en">Kick Drum 1</instrument-name>
Expand All @@ -116,6 +117,7 @@
<stem>down</stem>
<notehead>normal</notehead>
<instrument-sound>drum.bass-drum</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="44">
<instrument-name lang="en">Pedal Hi-Hat</instrument-name>
Expand All @@ -124,6 +126,7 @@
<stem>down</stem>
<notehead>x</notehead>
<instrument-sound>metal.hi-hat</instrument-sound>
<voice>2</voice>
</drum>
</instrument>
<instrument id="tom-tom">
Expand All @@ -137,6 +140,7 @@
<stem>up</stem>
<notehead>normal</notehead>
<instrument-sound>drum.tom-tom</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="43">
<instrument-name lang="en">Low Tom 1</instrument-name>
Expand All @@ -145,6 +149,7 @@
<stem>up</stem>
<notehead>normal</notehead>
<instrument-sound>drum.tom-tom</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="45">
<instrument-name lang="en">Mid Tom 2</instrument-name>
Expand All @@ -153,6 +158,7 @@
<stem>up</stem>
<notehead>normal</notehead>
<instrument-sound>drum.tom-tom</instrument-sound>
<voice>2</voice>
</drum>
<drum midi="47">
<instrument-name lang="en">Mid Tom 1</instrument-name>
Expand All @@ -161,6 +167,7 @@
<stem>up</stem>
<notehead>normal</notehead>
<instrument-sound>drum.tom-tom</instrument-sound>
<voice>2</voice>
</drum>
<drum midi="48">
<instrument-name lang="en">High Tom 2</instrument-name>
Expand All @@ -169,6 +176,7 @@
<stem>up</stem>
<notehead>normal</notehead>
<instrument-sound>drum.tom-tom</instrument-sound>
<voice>3</voice>
</drum>
<drum midi="50">
<instrument-name lang="en">High Tom 1</instrument-name>
Expand All @@ -177,6 +185,7 @@
<stem>up</stem>
<notehead>normal</notehead>
<instrument-sound>drum.tom-tom</instrument-sound>
<voice>3</voice>
</drum>
</instrument>
<instrument id="snare-drum">
Expand All @@ -190,6 +199,7 @@
<stem>up</stem>
<notehead>normal</notehead>
<instrument-sound>drum.snare-drum</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="40">
<instrument-name lang="en">Snare Drum 2</instrument-name>
Expand All @@ -198,6 +208,7 @@
<stem>up</stem>
<notehead>square</notehead>
<instrument-sound>drum.snare-drum</instrument-sound>
<voice>2</voice>
</drum>
<drum midi="37">
<instrument-name lang="en">Side Stick</instrument-name>
Expand All @@ -206,6 +217,7 @@
<stem>up</stem>
<notehead>x</notehead>
<instrument-sound>drum.snare-drum</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="91">
<instrument-name lang="en">Rim Shot</instrument-name>
Expand All @@ -214,6 +226,7 @@
<stem>up</stem>
<notehead filled="no">diamond</notehead>
<instrument-sound>drum.snare-drum</instrument-sound>
<voice>1</voice>
</drum>
</instrument>
<instrument id="cymbals">
Expand All @@ -227,6 +240,7 @@
<stem>up</stem>
<notehead>x</notehead>
<instrument-sound>metal.cymbal.ride</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="56">
<instrument-name lang="en">Cowbell</instrument-name>
Expand All @@ -235,6 +249,7 @@
<stem>up</stem>
<notehead>triangle</notehead>
<instrument-sound>metal.bells.cowbell</instrument-sound>
<voice>2</voice>
</drum>
<drum midi="51">
<instrument-name lang="en">Ride Cymbal 1</instrument-name>
Expand All @@ -243,6 +258,7 @@
<stem>up</stem>
<notehead>x</notehead>
<instrument-sound>metal.cymbal.ride</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="53">
<instrument-name lang="en">Ride Bell</instrument-name>
Expand All @@ -251,6 +267,7 @@
<stem>up</stem>
<notehead filled="yes">diamond</notehead>
<instrument-sound>metal.cymbal.ride</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="46">
<instrument-name lang="en">Open Hi-Hat</instrument-name>
Expand All @@ -259,6 +276,7 @@
<stem>up</stem>
<notehead>circle-x</notehead>
<instrument-sound>metal.hi-hat</instrument-sound>
<voice>3</voice>
</drum>
<drum midi="42">
<instrument-name lang="en">Closed Hi-Hat</instrument-name>
Expand All @@ -267,6 +285,7 @@
<stem>up</stem>
<notehead>x</notehead>
<instrument-sound>metal.hi-hat</instrument-sound>
<voice>3</voice>
</drum>
<drum midi="49">
<instrument-name lang="en">Crash Cymbal 1</instrument-name>
Expand All @@ -275,6 +294,7 @@
<stem>up</stem>
<notehead>x</notehead>
<instrument-sound>metal.cymbal.crash</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="57">
<instrument-name lang="en">Crash Cymbal 2</instrument-name>
Expand All @@ -283,6 +303,7 @@
<stem>up</stem>
<notehead>x</notehead>
<instrument-sound>metal.cymbal.crash</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="55">
<instrument-name lang="en">Splash Cymbal</instrument-name>
Expand All @@ -291,6 +312,7 @@
<stem>up</stem>
<notehead>x</notehead>
<instrument-sound>metal.cymbal.splash</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="52">
<instrument-name lang="en">Chinese Cymbal</instrument-name>
Expand All @@ -299,6 +321,7 @@
<stem>up</stem>
<notehead>circle-x</notehead>
<instrument-sound>metal.cymbal.chinese</instrument-sound>
<voice>1</voice>
</drum>
</instrument>
<instrument id="tambourine">
Expand Down Expand Up @@ -359,6 +382,7 @@
<stem>up</stem>
<notehead>x</notehead>
<instrument-sound>drum.conga</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="63">
<instrument-name lang="en">Open High Conga</instrument-name>
Expand All @@ -367,6 +391,7 @@
<stem>up</stem>
<notehead>normal</notehead>
<instrument-sound>drum.conga</instrument-sound>
<voice>1</voice>
</drum>
<drum midi="64">
<instrument-name lang="en">Low Conga</instrument-name>
Expand All @@ -375,6 +400,7 @@
<stem>up</stem>
<notehead>normal</notehead>
<instrument-sound>drum.conga</instrument-sound>
<voice>2</voice>
</drum>
</instrument>
<instrument id="timbales">
Expand Down Expand Up @@ -594,7 +620,7 @@
<part-abbreviation lang="en">Be. Tr.</part-abbreviation>
<staff-lines>1</staff-lines>
<drum midi="84">
<instrument-name lang="en">Sleigh Bells</instrument-name>
<instrument-name lang="en">Bell Tree</instrument-name>
<display-step>E</display-step>
<display-octave>4</display-octave>
<stem>up</stem>
Expand Down

0 comments on commit fb6244b

Please sign in to comment.