diff --git a/src/js/musicxml-grooves.js b/src/js/musicxml-grooves.js
index 43e28c3a..c4ad8e31 100755
--- a/src/js/musicxml-grooves.js
+++ b/src/js/musicxml-grooves.js
@@ -19,6 +19,7 @@ const DIVISIONS_256th = DIVISIONS/64
const DIVISIONS_512th = DIVISIONS/128
const DIVISIONS_1024th = DIVISIONS/256
const QUANTIZATION_DEFAULT_GRID = [4, 3]
+const TUPLET_TOLERANCE = 0.025
import fs from 'fs'
import xmlFormat from 'xml-formatter'
@@ -59,6 +60,9 @@ const options = {
'grid': {
type: 'string',
default: QUANTIZATION_DEFAULT_GRID.join(',')
+ },
+ 'dashes': {
+ type: 'boolean'
}
}
const { values: args } = (() => {
@@ -536,9 +540,15 @@ function createMeasureNotes(groove, part, measure) {
`.trim() : ``
) : ''
) : ''
+ const dashes = ('dashes' in args && note.quantized.onset > 0 && note.quantized.onset % DIVISIONS < Number.EPSILON) ? `
+
+ dashed
+
+ `.trim() : ''
return `
${backup}
+ ${dashes}
${chord}
${pitch}
@@ -569,7 +579,7 @@ function quantizeNoteOnset(note, index, notes, beats, grid) {
const isFirstNote = index === 0 || notes[index-1].voice !== note.voice
const scoreDuration = Math.round(note.duration * DIVISIONS)
const scoreOnset = Math.round((note.onset - 1) * DIVISIONS)
- const onset = grid.map(unit => {
+ let onset = grid.map(unit => {
return nearestMultiple(scoreOnset, DIVISIONS/unit)
}).flat().sort((m1, m2) => {
return m1.error_abs - m2.error_abs
@@ -738,67 +748,91 @@ function createNoteTiming(note, index, notes) {
[DIVISIONS_512th, '512th'],
[DIVISIONS_1024th, '1024th'],
]
- const scoreDuration = note.quantized.duration
+ const lookupType = duration => types.find(t => t[0] === duration)[1]
// Fill in this MusicXML timing structure.
note.musicXml = {
- duration: scoreDuration
+ duration: note.quantized.duration
}
for (const [entry, type] of types) {
// Detect simple types.
- if (Math.abs(scoreDuration - entry) <= Number.EPSILON) {
+ if (Math.abs(note.quantized.duration - entry) <= Number.EPSILON) {
note.musicXml = { ...note.musicXml, type }
break
}
// Detect dotted notes, only for non-rests.
- if ('midi' in note && entry < scoreDuration) {
- const dots = Math.log(2 - scoreDuration / entry) / Math.log(0.5)
+ if ('midi' in note && entry < note.quantized.duration) {
+ const dots = Math.log(2 - note.quantized.duration / entry) / Math.log(0.5)
if (Number.isInteger(dots)) {
note.musicXml = { ...note.musicXml, type, dots }
break
}
}
- // TODO Detect 3- and 5-tuplets.
- for (const tuplet of [3, 5]) {
+ // Detect 3- and 5-tuplets.
+ if (entry === DIVISIONS_QUARTER && entry > note.quantized.duration) {
+ for (const tupletCount of [3, 5]) {
+ const tuplet = tuplets(note, index, notes, tupletCount)
+ const ratio = Math.round(entry / tupletCount)
+ const beat = Math.floor(tuplet[0].quantized.onset / entry)
+ if (
+ tuplet.length === tupletCount &&
+ Math.abs(tupletsDuration(tuplet) - entry) <= TUPLET_TOLERANCE * tupletCount &&
+ tuplet.every(n =>
+ //Math.min(n.quantized.duration % ratio, ratio - (n.quantized.duration % ratio)) <= TUPLET_TOLERANCE &&
+ Math.floor(n.quantized.onset / entry) === beat
+ )
+ ) {
+ tuplet.forEach((n, i) => {
+ n.quantized = {
+ duration: entry / tupletCount,
+ onset: note.quantized.onset + (i * entry / tupletCount)
+ }
+ n.musicXml = {
+ duration: entry / tupletCount,
+ type: lookupType(entry / 2),
+ tuplet: {
+ actualNotes: tupletCount,
+ normalNotes: 2,
+ normalType: lookupType(entry / 2),
+ startStop: i === 0 ? 'start' : i === tuplet.length - 1 ? 'stop' : undefined,
+ number: 1
+ }
+ }
+ })
+ break
+ }
+ }
}
// Detect swing 8th pair.
// To qualify, 2 consecutive notes must:
// - Sum up to a quarter
- // - Each be within a triplet factor of a quarter
- if (entry === DIVISIONS_QUARTER && entry > scoreDuration) {
+ // - Each be within a triplet multiple of a quarter
+ if (entry === DIVISIONS_QUARTER && entry > note.quantized.duration) {
const pair = tuplets(note, index, notes, 2)
- const [swingHi, swingLo] = [2 * entry / 3, entry / 3]
+ const ratio = Math.round(entry / 3)
if (
- pair.length == 2 &&
- Math.abs(tupletsDuration(pair) - entry) <= Number.EPSILON &&
- pair.every(n => Math.abs(swingHi - n.quantized.duration) <= Number.EPSILON || Math.abs(swingLo - n.quantized.duration) <= Number.EPSILON)
+ pair.length === 2 &&
+ Math.abs(tupletsDuration(pair) - entry) <= TUPLET_TOLERANCE * 2 &&
+ pair.every(n => Math.min(n.quantized.duration % ratio, ratio - (n.quantized.duration % ratio)) <= TUPLET_TOLERANCE) &&
+ Math.floor(pair[0].quantized.onset / entry) === Math.floor(pair[1].quantized.onset / entry)
) {
- note.musicXml = {
- ...note.musicXml,
- type: pair[0].quantized.duration > pair[1].quantized.duration ? 'quarter' : 'eighth',
- tuplet: {
- actualNotes: 3,
- normalNotes: 2,
- normalType: 'eighth',
- startStop: 'start',
- number: 1
- }
- }
- pair[1].musicXml = {
- duration: pair[1].quantized.duration,
- type: pair[0].quantized.duration < pair[1].quantized.duration ? 'quarter' : 'eighth',
- tuplet: {
- actualNotes: 3,
- normalNotes: 2,
- normalType: 'eighth',
- startStop: 'stop',
- number: 1
+ pair.forEach((n, i, t) => {
+ n.musicXml = {
+ duration: n.quantized.duration,
+ type: n.quantized.duration > ratio ? lookupType(entry) : lookupType(entry / 2),
+ tuplet: {
+ actualNotes: 3,
+ normalNotes: 2,
+ normalType: lookupType(entry / 2),
+ startStop: i === 0 ? 'start' : i === t.length - 1 ? 'stop' : undefined,
+ number: 1
+ }
}
- }
- break;
+ })
+ break
}
}
}
@@ -807,13 +841,13 @@ function createNoteTiming(note, index, notes) {
// - First "extra" note is actually current note.
// - All notes will be tied. First note starts a tie, last note ends a tie, middle notes have both.
// - Add a dot to notes if they are in consecutive fractional order.
- if (!('type' in note.musicXml) && 'midi' in note) {
+ if (!('type' in note.musicXml)) {
const extra = []
- let remainingDuration = scoreDuration
+ let gap = note.quantized.duration
let onset = note.quantized.onset
for (const [entry, type] of types) {
- if (remainingDuration >= entry) {
- if (extra.length > 0 && extra[extra.length-1].duration === entry * 2) {
+ if (gap >= entry) {
+ if ('midi' in note && extra.length > 0 && extra[extra.length-1].duration === entry * 2) {
extra[extra.length-1].dots += 1
extra[extra.length-1].duration += entry
}
@@ -826,11 +860,16 @@ function createNoteTiming(note, index, notes) {
tie: { start: true, stop: extra.length > 0 },
})
}
- remainingDuration -= entry
+ gap -= entry
onset += entry
}
}
+ // Check that the gap is all filled.
+ if (gap > Number.EPSILON) {
+ console.warn(`[${note.track}:${note.measure+1}] Remaining gap of ${gap} left before note at ${note.onset}.`)
+ }
+
// Close up the last tie.
extra[extra.length-1].tie.start = false
@@ -838,11 +877,9 @@ function createNoteTiming(note, index, notes) {
note.musicXml = extra.shift()
// Return extra notes.
- return extra.map((e, i, input) => {
- return {
- midi: note.midi,
+ return extra.map(e => {
+ return { ...{
velocity: note.velocity,
- partId: note.partId,
track: note.track,
voice: note.voice,
measure: note.measure,
@@ -851,7 +888,10 @@ function createNoteTiming(note, index, notes) {
onset: e.onset,
duration: e.duration
}
- }
+ }, ...('midi' in note ? {
+ midi: note.midi,
+ partId: note.partId,
+ } : {})}
})
}