diff --git a/src/js/musicxml-grooves.js b/src/js/musicxml-grooves.js
index e10eddc5..f42823f2 100755
--- a/src/js/musicxml-grooves.js
+++ b/src/js/musicxml-grooves.js
@@ -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'
@@ -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
@@ -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' ? {
@@ -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 })
}
}
@@ -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
@@ -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
})
}
}
@@ -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 ? `
@@ -462,12 +478,19 @@ function createMeasureNotes(groove, part, i) {
const stem = drum ? `${drum.getElementsByTagName('stem')[0].textContent}` : ''
const notehead = drum ? `${drum.getElementsByTagName('notehead')[0].textContent}` : ''
const duration = Math.round(note.duration * DIVISIONS)
+ const backup = (index > 0 && notes[index - 1].voice !== note.voice) ? `
+
+ ${beats * DIVISIONS}
+
+ `.trim() : ''
return `
+ ${backup}
${chord}
${pitch}
${duration}
${instrument}
+ ${note.voice}
${createNoteTiming(note, index, notes, beatType)}
${stem}
${notehead}
@@ -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 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}`)
- note.staccato = true
- break
- }
+ // TODO Detect swung notes.
}
if (elements.length < 1) {
diff --git a/src/xml/drums.xml b/src/xml/drums.xml
index 69f3de9d..dfe68745 100644
--- a/src/xml/drums.xml
+++ b/src/xml/drums.xml
@@ -108,6 +108,7 @@
down
normal
drum.bass-drum
+ 1
Kick Drum 1
@@ -116,6 +117,7 @@
down
normal
drum.bass-drum
+ 1
Pedal Hi-Hat
@@ -124,6 +126,7 @@
down
x
metal.hi-hat
+ 2
@@ -137,6 +140,7 @@
up
normal
drum.tom-tom
+ 1
Low Tom 1
@@ -145,6 +149,7 @@
up
normal
drum.tom-tom
+ 1
Mid Tom 2
@@ -153,6 +158,7 @@
up
normal
drum.tom-tom
+ 2
Mid Tom 1
@@ -161,6 +167,7 @@
up
normal
drum.tom-tom
+ 2
High Tom 2
@@ -169,6 +176,7 @@
up
normal
drum.tom-tom
+ 3
High Tom 1
@@ -177,6 +185,7 @@
up
normal
drum.tom-tom
+ 3
@@ -190,6 +199,7 @@
up
normal
drum.snare-drum
+ 1
Snare Drum 2
@@ -198,6 +208,7 @@
up
square
drum.snare-drum
+ 2
Side Stick
@@ -206,6 +217,7 @@
up
x
drum.snare-drum
+ 1
Rim Shot
@@ -214,6 +226,7 @@
up
diamond
drum.snare-drum
+ 1
@@ -227,6 +240,7 @@
up
x
metal.cymbal.ride
+ 1
Cowbell
@@ -235,6 +249,7 @@
up
triangle
metal.bells.cowbell
+ 2
Ride Cymbal 1
@@ -243,6 +258,7 @@
up
x
metal.cymbal.ride
+ 1
Ride Bell
@@ -251,6 +267,7 @@
up
diamond
metal.cymbal.ride
+ 1
Open Hi-Hat
@@ -259,6 +276,7 @@
up
circle-x
metal.hi-hat
+ 3
Closed Hi-Hat
@@ -267,6 +285,7 @@
up
x
metal.hi-hat
+ 3
Crash Cymbal 1
@@ -275,6 +294,7 @@
up
x
metal.cymbal.crash
+ 1
Crash Cymbal 2
@@ -283,6 +303,7 @@
up
x
metal.cymbal.crash
+ 1
Splash Cymbal
@@ -291,6 +312,7 @@
up
x
metal.cymbal.splash
+ 1
Chinese Cymbal
@@ -299,6 +321,7 @@
up
circle-x
metal.cymbal.chinese
+ 1
@@ -359,6 +382,7 @@
up
x
drum.conga
+ 1
Open High Conga
@@ -367,6 +391,7 @@
up
normal
drum.conga
+ 1
Low Conga
@@ -375,6 +400,7 @@
up
normal
drum.conga
+ 2
@@ -594,7 +620,7 @@
Be. Tr.
1
- Sleigh Bells
+ Bell Tree
E
4
up