abcjs
Version:
Renderer for abc music notation
1,309 lines (1,215 loc) • 44.6 kB
JavaScript
// abc_midi_flattener.js: Turn a linear series of events into a series of MIDI commands.
// We input a set of voices, but the notes are still complex. This pass changes the logical definitions
// of the grace notes, decorations, ties, triplets, rests, transpositions, keys, and accidentals into actual note durations.
// It also extracts guitar chords to a separate voice and resolves their rhythm.
var flatten;
var parseCommon = require("../parse/abc_common");
var pitchesToPerc = require('./pitches-to-perc');
(function() {
"use strict";
var barAccidentals;
var accidentals;
var transpose;
var bagpipes;
var tracks;
var startingTempo;
var startingMeter;
var tempoChangeFactor = 1;
var instrument;
var currentInstrument;
// var channel;
var currentTrack;
var lastNoteDurationPosition;
var currentTrackName;
var lastEventTime;
var meter = { num: 4, den: 4 };
var chordTrack;
var chordSourceTrack;
var chordTrackFinished;
var chordChannel;
var bassInstrument = 0;
var chordInstrument = 0;
var drumInstrument = 128;
var boomVolume = 64;
var chickVolume = 48;
var currentChords;
var lastChord;
var chordLastBar;
var lastBarTime;
var gChordTacet = false;
var hasRhythmHead = false;
var doBeatAccents = true;
var stressBeat1 = 105;
var stressBeatDown = 95;
var stressBeatUp = 85;
var beatFraction = 0.25;
var nextVolume;
var nextVolumeDelta;
var slurCount = 0;
var drumTrack;
var drumTrackFinished;
var drumDefinition = {};
var drumBars;
var pickupLength = 0;
var percmap;
// The gaps per beat. The first two are in seconds, the third is in fraction of a duration.
var normalBreakBetweenNotes = 0; //0.000520833333325*1.5; // for articulation (matches muse score value)
var slurredBreakBetweenNotes = -0.001; // make the slurred notes actually overlap
var staccatoBreakBetweenNotes = 0.4; // some people say staccato is half duration, some say 3/4 so this splits it
flatten = function(voices, options, percmap_, midiOptions) {
if (!options) options = {};
if (!midiOptions) midiOptions = {};
barAccidentals = [];
accidentals = [0,0,0,0,0,0,0];
bagpipes = false;
tracks = [];
startingTempo = options.qpm;
startingMeter = undefined;
tempoChangeFactor = 1;
instrument = undefined;
currentInstrument = undefined;
// channel = undefined;
currentTrack = undefined;
currentTrackName = undefined;
lastEventTime = 0;
percmap = percmap_;
// For resolving chords.
meter = { num: 4, den: 4 };
chordTrack = [];
chordSourceTrack = false;
chordChannel = voices.length; // first free channel for chords
chordTrackFinished = false;
currentChords = [];
bassInstrument = midiOptions.bassprog && midiOptions.bassprog.length === 1 ? midiOptions.bassprog[0] : 0;
chordInstrument = midiOptions.chordprog && midiOptions.chordprog.length === 1 ? midiOptions.chordprog[0] : 0;
boomVolume = midiOptions.bassvol && midiOptions.bassvol.length === 1 ? midiOptions.bassvol[0] : 64;
chickVolume = midiOptions.chordvol && midiOptions.chordvol.length === 1 ? midiOptions.chordvol[0] : 48;
lastChord = undefined;
chordLastBar = undefined;
gChordTacet = options.chordsOff ? true : false;
hasRhythmHead = false;
doBeatAccents = true;
stressBeat1 = 105;
stressBeatDown = 95;
stressBeatUp = 85;
beatFraction = 0.25;
nextVolume = undefined;
nextVolumeDelta = undefined;
slurCount = 0;
// For the drum/metronome track.
drumTrack = [];
drumTrackFinished = false;
drumDefinition = {};
drumBars = 1;
if (voices.length > 0 && voices[0].length > 0)
pickupLength = voices[0][0].pickupLength;
// First adjust the input to resolve ties, set the starting time for each note, etc. That will make the rest of the logic easier
preProcess(voices, options);
for (var i = 0; i < voices.length; i++) {
transpose = 0;
lastNoteDurationPosition = -1;
var voice = voices[i];
currentTrack = [{ cmd: 'program', channel: i, instrument: instrument }];
currentTrackName = undefined;
lastBarTime = 0;
var voiceOff = false;
if (options.voicesOff === true)
voiceOff = true;
else if (options.voicesOff && options.voicesOff.length && options.voicesOff.indexOf(i) >= 0)
voiceOff = true;
for (var j = 0; j < voice.length; j++) {
var element = voice[j];
switch (element.el_type) {
case "name":
currentTrackName = {cmd: 'text', type: "name", text: element.trackName };
break;
case "note":
var setChordTrack = writeNote(element, voiceOff);
if (setChordTrack)
chordSourceTrack = i;
break;
case "key":
accidentals = setKeySignature(element);
break;
case "meter":
if (!startingMeter)
startingMeter = element;
meter = element;
beatFraction = getBeatFraction(meter);
alignDrumToMeter();
break;
case "tempo":
if (!startingTempo)
startingTempo = element.qpm;
else
tempoChangeFactor = element.qpm ? startingTempo / element.qpm : 1;
break;
case "transpose":
transpose = element.transpose;
break;
case "bar":
if (chordTrack.length > 0 && (chordSourceTrack === false || i === chordSourceTrack)) {
resolveChords(lastBarTime, timeToRealTime(element.time));
currentChords = [];
}
barAccidentals = [];
if (i === 0) // Only write the drum part on the first voice so that it is not duplicated.
writeDrum(voices.length+1);
hasRhythmHead = false; // decide whether there are rhythm heads each measure.
chordLastBar = lastChord;
lastBarTime = timeToRealTime(element.time);
break;
case "bagpipes":
bagpipes = true;
break;
case "instrument":
if (instrument === undefined)
instrument = element.program;
currentInstrument = element.program;
if (currentTrack.length > 0 && currentTrack[currentTrack.length-1].cmd === 'program')
currentTrack[currentTrack.length-1].instrument = element.program;
else {
var ii;
for (ii = currentTrack.length-1; ii >= 0 && currentTrack[ii].cmd !== 'program'; ii--)
;
if (ii < 0 || currentTrack[ii].instrument !== element.program)
currentTrack.push({cmd: 'program', channel: 0, instrument: element.program});
}
break;
case "channel":
setChannel(element.channel);
break;
case "drum":
drumDefinition = normalizeDrumDefinition(element.params);
alignDrumToMeter();
break;
case "gchord":
if (!options.chordsOff)
gChordTacet = element.tacet;
break;
case "beat":
stressBeat1 = element.beats[0];
stressBeatDown = element.beats[1];
stressBeatUp = element.beats[2];
// TODO-PER: also use the last parameter - which changes which beats are strong.
break;
case "vol":
nextVolume = element.volume;
break;
case "volinc":
nextVolumeDelta = element.volume;
break;
case "beataccents":
doBeatAccents = element.value;
break;
default:
// This should never happen
console.log("MIDI creation. Unknown el_type: " + element.el_type + "\n");// jshint ignore:line
break;
}
}
if (currentTrack[0].instrument === undefined)
currentTrack[0].instrument = instrument ? instrument : 0;
if (currentTrackName)
currentTrack.unshift(currentTrackName);
tracks.push(currentTrack);
if (!chordTrackEmpty()) // Don't do chords on more than one track, so turn off chord detection after we create it.
chordTrackFinished = true;
if (drumTrack.length > 0) // Don't do drums on more than one track, so turn off drum after we create it.
drumTrackFinished = true;
}
// See if any notes are octaves played at the same time. If so, raise the pitch of the higher one.
if (options.detuneOctave)
findOctaves(tracks, parseInt(options.detuneOctave, 10));
if (!chordTrackEmpty())
tracks.push(chordTrack);
if (drumTrack.length > 0)
tracks.push(drumTrack);
return { tempo: startingTempo, instrument: instrument, tracks: tracks, totalDuration: lastEventTime };
};
function setChannel(channel) {
for (var i = currentTrack.length-1; i>=0; i--) {
if (currentTrack[i].cmd === "program") {
currentTrack[i].channel = channel;
return;
}
}
}
function chordTrackEmpty() {
var isEmpty = true;
for (var i = 0; i < chordTrack.length && isEmpty; i++) {
if (chordTrack[i].cmd === 'note')
isEmpty = false
}
return isEmpty;
}
function timeToRealTime(time) {
return time/1000000;
}
function durationRounded(duration) {
return Math.round(duration*tempoChangeFactor*1000000)/1000000;
}
function preProcess(voices, options) {
for (var i = 0; i < voices.length; i++) {
var voice = voices[i];
var ties = {};
var startingTempo = options.qpm;
var timeCounter = 0;
var tempoMultiplier = 1;
for (var j = 0; j < voice.length; j++) {
var element = voice[j];
if (element.el_type === 'tempo') {
if (!startingTempo)
startingTempo = element.qpm;
else
tempoMultiplier = element.qpm ? startingTempo / element.qpm : 1;
continue;
}
// For convenience, put the current time in each event so that it doesn't have to be calculated in the complicated stuff that follows.
element.time = timeCounter;
var thisDuration = element.duration ? element.duration : 0;
timeCounter += Math.round(thisDuration*tempoMultiplier*1000000); // To compensate for JS rounding problems, do all intermediate calcs on integers.
// If there are pitches then put the duration in the pitch object and if there are ties then change the duration of the first note in the tie.
if (element.pitches) {
for (var k = 0; k < element.pitches.length; k++) {
var pitch = element.pitches[k];
if (pitch) {
pitch.duration = element.duration;
if (pitch.startTie) {
//console.log(element)
if (ties[pitch.pitch] === undefined) // We might have three notes tied together - if so just add this duration.
ties[pitch.pitch] = {el: j, pitch: k};
else {
voice[ties[pitch.pitch].el].pitches[ties[pitch.pitch].pitch].duration += pitch.duration;
element.pitches[k] = null;
}
//console.log(">>> START", JSON.stringify(ties));
} else if (pitch.endTie) {
//console.log(element)
var tie = ties[pitch.pitch];
//console.log(">>> END", pitch.pitch, tie, JSON.stringify(ties));
if (tie) {
var dur = pitch.duration;
delete voice[tie.el].pitches[tie.pitch].startTie;
voice[tie.el].pitches[tie.pitch].duration += dur;
element.pitches[k] = null;
delete ties[pitch.pitch];
} else {
delete pitch.endTie;
}
}
}
}
delete element.duration;
}
}
for (var key in ties) {
if (ties.hasOwnProperty(key)) {
var item = ties[key];
delete voice[item.el].pitches[item.pitch].startTie;
}
}
// voices[0].forEach(v => delete v.elem)
// voices[1].forEach(v => delete v.elem)
// console.log(JSON.stringify(voices))
}
}
function getBeatFraction(meter) {
switch (parseInt(meter.den,10)) {
case 2: return 0.5;
case 4: return 0.25;
case 8:
if (meter.num % 3 === 0)
return 0.375;
else
return 0.125;
case 16: return 0.125;
}
return 0.25;
}
//
// The algorithm for chords is:
// - The chords are done in a separate track.
// - If there are notes before the first chord, then put that much silence to start the track.
// - The pattern of chord expression depends on the meter, and how many chords are in a measure.
// - There is a possibility that a measure will have an incorrect number of beats, if that is the case, then
// start the pattern anew on the next measure number.
// - If a chord root is not A-G, then ignore it as if the chord wasn't there at all.
// - If a chord modification isn't in our supported list, change it to a major triad.
//
// - If there is only one chord in a measure:
// - If 2/4, play root chord
// - If cut time, play root(1) chord(3)
// - If 3/4, play root chord chord
// - If 4/4 or common time, play root chord fifth chord
// - If 6/8, play root(1) chord(3) fifth(4) chord(6)
// - For any other meter, play the full chord on each beat. (TODO-PER: expand this as more support is added.)
//
// - If there is a chord specified that is not on a beat, move it earlier to the previous beat, unless there is already a chord on that beat.
// - Otherwise, move it later, unless there is already a chord on that beat.
// - Otherwise, ignore it. (TODO-PER: expand this as more support is added.)
//
// - If there is a chord on the second beat, play a chord for the first beat instead of a bass note.
// - Likewise, if there is a chord on the fourth beat of 4/4, play a chord on the third beat instead of a bass note.
//
// If there is any note in the melody that has a rhythm head, then assume the melody controls the rhythm, so that is
// the same as a break.
var breakSynonyms = [ 'break', '(break)', 'no chord', 'n.c.', 'tacet'];
function findChord(elem) {
if (gChordTacet)
return 'break';
// TODO-PER: Just using the first chord if there are more than one.
if (chordTrackFinished || !elem.chord || elem.chord.length === 0)
return null;
// Return the first annotation that is a regular chord: that is, it is in the default place or is a recognized "tacet" phrase.
for (var i = 0; i < elem.chord.length; i++) {
var ch = elem.chord[i];
if (ch.position === 'default')
return ch.name;
if (breakSynonyms.indexOf(ch.name.toLowerCase()) >= 0)
return 'break';
}
return null;
}
function calcBeat(measureStart, beatLength, currTime) {
var distanceFromStart = currTime - measureStart;
return distanceFromStart / beatLength;
}
function processVolume(beat, voiceOff) {
if (voiceOff)
return 0;
var volume;
if (nextVolume) {
volume = nextVolume;
nextVolume = undefined;
} else if (!doBeatAccents) {
volume = stressBeatDown;
} else if (pickupLength > beat) {
volume = stressBeatUp;
} else {
var barLength = meter.num / meter.den;
var barBeat = calcBeat(lastBarTime, getBeatFraction(meter), beat);
if (barBeat === 0)
volume = stressBeat1;
else if (parseInt(barBeat,10) === barBeat)
volume = stressBeatDown;
else
volume = stressBeatUp;
}
if (nextVolumeDelta) {
volume += nextVolumeDelta;
nextVolumeDelta = undefined;
}
if (volume < 0)
volume = 0;
if (volume > 127)
volume = 127;
return voiceOff ? 0 : volume;
}
function processChord(elem) {
var firstChord = false;
var chord = findChord(elem);
if (chord) {
var c = interpretChord(chord);
// If this isn't a recognized chord, just completely ignore it.
if (c) {
// If we ever have a chord in this voice, then we add the chord track.
// However, if there are chords on more than one voice, then just use the first voice.
if (chordTrack.length === 0) {
firstChord = true;
chordTrack.push({cmd: 'program', channel: chordChannel, instrument: chordInstrument});
}
lastChord = c;
var barBeat = calcBeat(lastBarTime, getBeatFraction(meter), timeToRealTime(elem.time));
currentChords.push({chord: lastChord, beat: barBeat, start: timeToRealTime(elem.time)});
}
}
return firstChord;
}
function findNoteModifications(elem, velocity) {
var ret = { };
if (elem.decoration) {
for (var d = 0; d < elem.decoration.length; d++) {
if (elem.decoration[d] === 'staccato')
ret.thisBreakBetweenNotes = 'staccato';
else if (elem.decoration[d] === 'tenuto')
ret.thisBreakBetweenNotes = 'tenuto';
else if (elem.decoration[d] === 'accent')
ret.velocity = Math.min(127, velocity * 1.5);
else if (elem.decoration[d] === 'trill')
ret.noteModification = "trill";
else if (elem.decoration[d] === 'lowermordent')
ret.noteModification = "lowermordent";
else if (elem.decoration[d] === 'uppermordent')
ret.noteModification = "mordent";
else if (elem.decoration[d] === 'mordent')
ret.noteModification = "mordent";
else if (elem.decoration[d] === 'turn')
ret.noteModification = "turn";
else if (elem.decoration[d] === 'roll')
ret.noteModification = "roll";
}
}
return ret;
}
function doModifiedNotes(noteModification, p) {
var noteTime;
var numNotes;
var start = p.start;
var pp;
var runningDuration = p.duration;
var shortestNote = durationRounded(1.0 / 32);
switch (noteModification) {
case "trill":
var note = 1;
while (runningDuration > 0) {
currentTrack.push({ cmd: 'note', pitch: p.pitch+note, volume: p.volume, start: start, duration: shortestNote, gap: 0, instrument: currentInstrument, style: 'decoration' });
note = (note === 1) ? 0 : 1;
runningDuration -= shortestNote;
start += shortestNote;
}
break;
case "mordent":
currentTrack.push({ cmd: 'note', pitch: p.pitch, volume: p.volume, start: start, duration: shortestNote, gap: 0, instrument: currentInstrument, style: 'decoration' });
runningDuration -= shortestNote;
start += shortestNote;
currentTrack.push({ cmd: 'note', pitch: p.pitch+1, volume: p.volume, start: start, duration: shortestNote, gap: 0, instrument: currentInstrument, style: 'decoration' });
runningDuration -= shortestNote;
start += shortestNote;
currentTrack.push({ cmd: 'note', pitch: p.pitch, volume: p.volume, start: start, duration: runningDuration, gap: 0, instrument: currentInstrument });
break;
case "lowermordent":
currentTrack.push({ cmd: 'note', pitch: p.pitch, volume: p.volume, start: start, duration: shortestNote, gap: 0, instrument: currentInstrument, style: 'decoration' });
runningDuration -= shortestNote;
start += shortestNote;
currentTrack.push({ cmd: 'note', pitch: p.pitch-1, volume: p.volume, start: start, duration: shortestNote, gap: 0, instrument: currentInstrument, style: 'decoration' });
runningDuration -= shortestNote;
start += shortestNote;
currentTrack.push({ cmd: 'note', pitch: p.pitch, volume: p.volume, start: start, duration: runningDuration, gap: 0, instrument: currentInstrument });
break;
case "turn":
shortestNote = p.duration / 5;
currentTrack.push({ cmd: 'note', pitch: p.pitch, volume: p.volume, start: start, duration: shortestNote, gap: 0, instrument: currentInstrument, style: 'decoration' });
currentTrack.push({ cmd: 'note', pitch: p.pitch+1, volume: p.volume, start: start+shortestNote, duration: shortestNote, gap: 0, instrument: currentInstrument, style: 'decoration' });
currentTrack.push({ cmd: 'note', pitch: p.pitch, volume: p.volume, start: start+shortestNote*2, duration: shortestNote, gap: 0, instrument: currentInstrument, style: 'decoration' });
currentTrack.push({ cmd: 'note', pitch: p.pitch+1, volume: p.volume, start: start+shortestNote*3, duration: shortestNote, gap: 0, instrument: currentInstrument, style: 'decoration' });
currentTrack.push({ cmd: 'note', pitch: p.pitch, volume: p.volume, start: start+shortestNote*4, duration: shortestNote, gap: 0, instrument: currentInstrument });
break;
case "roll":
while (runningDuration > 0) {
currentTrack.push({ cmd: 'note', pitch: p.pitch, volume: p.volume, start: start, duration: shortestNote, gap: 0, instrument: currentInstrument, style: 'decoration' });
runningDuration -= shortestNote*2;
start += shortestNote*2;
}
break;
}
}
function writeNote(elem, voiceOff) {
//
// Create a series of note events to append to the current track.
// The output event is one of: { pitchStart: pitch_in_abc_units, volume: from_1_to_64 }
// { pitchStop: pitch_in_abc_units }
// { moveTime: duration_in_abc_units }
// If there are guitar chords, then they are put in a separate track, but they have the same format.
//
var trackStartingIndex = currentTrack.length;
var velocity = processVolume(timeToRealTime(elem.time), voiceOff);
var setChordTrack = processChord(elem);
// if there are grace notes, then also play them.
// I'm not sure there is an exact rule for the length of the notes. My rule, unless I find
// a better one is: the grace notes cannot take more than 1/2 of the main note's value.
// A grace note (of 1/8 note duration) takes 1/8 of the main note's value.
var graces;
if (elem.gracenotes && elem.pitches && elem.pitches.length > 0 && elem.pitches[0]) {
graces = processGraceNotes(elem.gracenotes, elem.pitches[0].duration);
if (elem.elem)
elem.elem.midiGraceNotePitches = writeGraceNotes(graces, timeToRealTime(elem.time), velocity*2/3, currentInstrument); // make the graces a little quieter.
}
// The beat fraction is the note that gets a beat (.25 is a quarter note)
// The tempo is in minutes and we want to get to milliseconds.
// If the element is inside a repeat, there may be more than one value. If there is one value,
// then just store that as a number. If there are more than one value, then change that to
// an array and return all of them.
if (elem.elem) {
var rt = timeToRealTime(elem.time);
var ms = rt / beatFraction / startingTempo * 60 * 1000;
if (elem.elem.currentTrackMilliseconds === undefined) {
elem.elem.currentTrackMilliseconds = ms;
elem.elem.currentTrackWholeNotes = rt;
} else {
if (elem.elem.currentTrackMilliseconds.length === undefined) {
if (elem.elem.currentTrackMilliseconds !== ms) {
elem.elem.currentTrackMilliseconds = [elem.elem.currentTrackMilliseconds, ms];
elem.elem.currentTrackWholeNotes = [elem.elem.currentTrackWholeNotes, rt];
}
} else {
// There can be duplicates if there are multiple voices
var found = false;
for (var j = 0; j < elem.elem.currentTrackMilliseconds.length; j++) {
if (elem.elem.currentTrackMilliseconds[j] === ms)
found = true;
}
if (!found) {
elem.elem.currentTrackMilliseconds.push(ms);
elem.elem.currentTrackWholeNotes.push(rt);
}
}
}
}
//var tieAdjustment = 0;
if (elem.pitches) {
var thisBreakBetweenNotes = '';
var ret = findNoteModifications(elem, velocity);
if (ret.thisBreakBetweenNotes)
thisBreakBetweenNotes = ret.thisBreakBetweenNotes;
if (ret.velocity)
velocity = ret.velocity;
// TODO-PER: Can also make a different sound on style=x and style=harmonic
var ePitches = elem.pitches;
if (elem.style === "rhythm") {
hasRhythmHead = true;
if (lastChord && lastChord.chick) {
ePitches = [];
for (var i2 = 0; i2 < lastChord.chick.length; i2++) {
var note2 = parseCommon.clone(elem.pitches[0]);
note2.actualPitch = lastChord.chick[i2];
ePitches.push(note2);
}
}
}
if (elem.elem)
elem.elem.midiPitches = [];
for (var i=0; i<ePitches.length; i++) {
var note = ePitches[i];
if (!note)
continue;
if (note.startSlur)
slurCount += note.startSlur.length;
if (note.endSlur)
slurCount -= note.endSlur.length;
var actualPitch = note.actualPitch ? note.actualPitch : adjustPitch(note);
if (currentInstrument === drumInstrument && percmap) {
var name = pitchesToPerc(note)
if (name && percmap[name])
actualPitch = percmap[name].sound;
}
var p = { cmd: 'note', pitch: actualPitch, volume: velocity, start: timeToRealTime(elem.time), duration: durationRounded(note.duration), instrument: currentInstrument, startChar: elem.elem.startChar, endChar: elem.elem.endChar};
p = adjustForMicroTone(p);
if (elem.gracenotes) {
p.duration = p.duration / 2;
p.start = p.start + p.duration;
}
if (elem.elem)
elem.elem.midiPitches.push(p);
if (ret.noteModification) {
doModifiedNotes(ret.noteModification, p);
} else {
if (slurCount > 0)
p.endType = 'tenuto';
else if (thisBreakBetweenNotes)
p.endType = thisBreakBetweenNotes;
switch (p.endType) {
case "tenuto":
p.gap = slurredBreakBetweenNotes;
break;
case "staccato":
var d = p.duration * staccatoBreakBetweenNotes;
p.gap = startingTempo / 60 * d;
break;
default:
p.gap = normalBreakBetweenNotes;
break;
}
currentTrack.push(p);
}
}
lastNoteDurationPosition = currentTrack.length-1;
}
var realDur = getRealDuration(elem);
lastEventTime = Math.max(lastEventTime, timeToRealTime(elem.time)+durationRounded(realDur));
return setChordTrack;
}
function getRealDuration(elem) {
if (elem.pitches && elem.pitches.length > 0 && elem.pitches[0])
return elem.pitches[0].duration;
if (elem.elem)
return elem.elem.duration;
return elem.duration;
}
var scale = [0,2,4,5,7,9,11];
function adjustPitch(note) {
if (note.midipitch !== undefined)
return note.midipitch; // The pitch might already be known, for instance if there is a drummap.
var pitch = note.pitch;
if (note.accidental) {
switch(note.accidental) { // change that pitch (not other octaves) for the rest of the bar
case "sharp":
barAccidentals[pitch]=1; break;
case "flat":
barAccidentals[pitch]=-1; break;
case "natural":
barAccidentals[pitch]=0; break;
case "dblsharp":
barAccidentals[pitch]=2; break;
case "dblflat":
barAccidentals[pitch]=-2; break;
case "quartersharp":
barAccidentals[pitch]=0.25; break;
case "quarterflat":
barAccidentals[pitch]=-0.25; break;
}
}
var actualPitch = extractOctave(pitch) *12 + scale[extractNote(pitch)] + 60;
if ( barAccidentals[pitch]!==undefined) {
// An accidental is always taken at face value and supersedes the key signature.
actualPitch += barAccidentals[pitch];
} else { // use normal accidentals
actualPitch += accidentals[extractNote(pitch)];
}
actualPitch += transpose;
return actualPitch;
}
function setKeySignature(elem) {
var accidentals = [0,0,0,0,0,0,0];
if (!elem.accidentals) return accidentals;
for (var i = 0; i < elem.accidentals.length; i++) {
var acc = elem.accidentals[i];
var d;
switch (acc.acc) {
case "flat": d = -1; break;
case "quarterflat": d = -0.25; break;
case "sharp": d = 1; break;
case "quartersharp": d = 0.25; break;
default: d = 0; break;
}
var lowercase = acc.note.toLowerCase();
var note = extractNote(lowercase.charCodeAt(0)-'c'.charCodeAt(0));
accidentals[note]+=d;
}
return accidentals;
}
function processGraceNotes(graces, companionDuration) {
// Grace notes take up half of the note value. So if there are many of them they are all real short.
var graceDuration = 0;
var ret = [];
var grace;
for (var g = 0; g < graces.length; g++) {
grace = graces[g];
graceDuration += grace.duration;
}
var multiplier = companionDuration/2 / graceDuration;
for (g = 0; g < graces.length; g++) {
grace = graces[g];
var actualPitch = adjustPitch(grace);
if (currentInstrument === drumInstrument && percmap) {
var name = pitchesToPerc(grace)
if (name && percmap[name])
actualPitch = percmap[name].sound;
}
var pitch = { pitch: actualPitch, duration: grace.duration*multiplier };
pitch = adjustForMicroTone(pitch);
ret.push(pitch);
}
return ret;
}
function writeGraceNotes(graces, start, velocity, currentInstrument) {
var midiGrace = [];
velocity = Math.round(velocity)
for (var g = 0; g < graces.length; g++) {
var gp = graces[g];
currentTrack.push({cmd: 'note', pitch: gp.pitch, volume: velocity, start: start, duration: gp.duration, gap: 0, instrument:currentInstrument, style: 'grace'});
midiGrace.push({
pitch: gp.pitch,
durationInMeasures: gp.duration,
volume: velocity,
instrument: currentInstrument
});
start += gp.duration;
}
return midiGrace;
}
var quarterToneFactor = 0.02930223664349;
function adjustForMicroTone(description) {
// if the pitch is not a whole number then make it a whole number and add a tuning factor
var pitch = ''+description.pitch;
if (pitch.indexOf(".75") >= 0) {
description.pitch = Math.round(description.pitch);
description.cents = -50;
} else if (pitch.indexOf(".25") >= 0) {
description.pitch = Math.round(description.pitch);
description.cents = 50;
}
return description;
}
function extractOctave(pitch) {
return Math.floor(pitch/7);
}
function extractNote(pitch) {
pitch = pitch%7;
if (pitch<0) pitch+=7;
return pitch;
}
var basses = {
'A': 33, 'B': 35, 'C': 36, 'D': 38, 'E': 40, 'F': 41, 'G': 43
};
function interpretChord(name) {
// chords have the format:
// [root][acc][modifier][/][bass][acc]
// (The chord might be surrounded by parens. Just ignore them.)
// root must be present and must be from A-G.
// acc is optional and can be # or b
// The modifier can be a wide variety of things, like "maj7". As they are discovered, more are supported here.
// If there is a slash, then there is a bass note, which can be from A-G, with an optional acc.
// If the root is unrecognized, then "undefined" is returned and there is no chord.
// If the modifier is unrecognized, a major triad is returned.
// If the bass notes is unrecognized, it is ignored.
if (name.length === 0)
return undefined;
if (name === 'break')
return { chick: []};
var root = name.substring(0,1);
if (root === '(') {
name = name.substring(1,name.length-2);
if (name.length === 0)
return undefined;
root = name.substring(0,1);
}
var bass = basses[root];
if (!bass) // If the bass note isn't listed, then this was an unknown root. Only A-G are accepted.
return undefined;
// Don't transpose the chords more than an octave.
var chordTranspose = transpose;
while (chordTranspose < -8)
chordTranspose += 12;
while (chordTranspose > 8)
chordTranspose -= 12;
bass += chordTranspose;
var bass2 = bass - 5; // The alternating bass is a 4th below
var chick;
if (name.length === 1)
chick = chordNotes(bass, '');
var remaining = name.substring(1);
var acc = remaining.substring(0,1);
if (acc === 'b' || acc === '♭') {
bass--;
bass2--;
remaining = remaining.substring(1);
} else if (acc === '#' || acc === '♯') {
bass++;
bass2++;
remaining = remaining.substring(1);
}
var arr = remaining.split('/');
chick = chordNotes(bass, arr[0]);
// If the 5th is altered then the bass is altered. Normally the bass is 7 from the root, so adjust if it isn't.
if (chick.length >= 3) {
var fifth = chick[2] - chick[0];
bass2 = bass2 + fifth - 7;
}
if (arr.length === 2) {
var explicitBass = basses[arr[1].substring(0,1)];
if (explicitBass) {
var bassAcc = arr[1].substring(1);
var bassShift = {'#': 1, '♯': 1, 'b': -1, '♭': -1}[bassAcc] || 0;
bass = basses[arr[1].substring(0,1)] + bassShift + chordTranspose;
bass2 = bass;
}
}
return { boom: bass, boom2: bass2, chick: chick };
}
var chordIntervals = {
// diminished (all flat 5 chords)
'dim': [ 0, 3, 6 ],
'°': [ 0, 3, 6 ],
'˚': [ 0, 3, 6 ],
'dim7': [ 0, 3, 6, 9 ],
'°7': [ 0, 3, 6, 9 ],
'˚7': [ 0, 3, 6, 9 ],
'ø7': [ 0, 3, 6, 10 ],
'm7(b5)': [ 0, 3, 6, 10 ],
'm7b5': [ 0, 3, 6, 10 ],
'm7♭5': [ 0, 3, 6, 10 ],
'-7(b5)': [ 0, 3, 6, 10 ],
'-7b5': [ 0, 3, 6, 10 ],
'7b5': [ 0, 4, 6, 10 ],
'7(b5)': [ 0, 4, 6, 10 ],
'7♭5': [ 0, 4, 6, 10 ],
'7(b9,b5)': [ 0, 4, 6, 10, 13 ],
'7b9,b5': [ 0, 4, 6, 10, 13 ],
'7(#9,b5)': [ 0, 4, 6, 10, 15 ],
'7#9b5': [ 0, 4, 6, 10, 15 ],
'maj7(b5)': [ 0, 4, 6, 11 ],
'maj7b5': [ 0, 4, 6, 11 ],
'13(b5)': [ 0, 4, 6, 10, 14, 21 ],
'13b5': [ 0, 4, 6, 10, 14, 21 ],
// minor (all normal 5, minor 3 chords)
'm': [ 0, 3, 7 ],
'-': [ 0, 3, 7 ],
'm6': [ 0, 3, 7, 9 ],
'-6': [ 0, 3, 7, 9 ],
'm7': [ 0, 3, 7, 10 ],
'-7': [ 0, 3, 7, 10 ],
'-(b6)': [ 0, 3, 7, 8 ],
'-b6': [ 0, 3, 7, 8 ],
'-6/9': [ 0, 3, 7, 9, 14 ],
'-7(b9)': [ 0, 3, 7, 10, 13 ],
'-7b9': [ 0, 3, 7, 10, 13 ],
'-maj7': [ 0, 3, 7, 11 ],
'-9+7': [ 0, 3, 7, 11, 13 ],
'-11': [ 0, 3, 7, 11, 14, 17 ],
'm11': [ 0, 3, 7, 11, 14, 17 ],
'-maj9': [ 0, 3, 7, 11, 14 ],
'-∆9': [ 0, 3, 7, 11, 14 ],
'mM9': [ 0, 3, 7, 11, 14 ],
// major (all normal 5, major 3 chords)
'M': [ 0, 4, 7 ],
'6': [ 0, 4, 7, 9 ],
'6/9': [ 0, 4, 7, 9, 14 ],
'6add9': [ 0, 4, 7, 9, 14 ],
'69': [ 0, 4, 7, 9, 14 ],
'7': [ 0, 4, 7, 10 ],
'9': [ 0, 4, 7, 10, 14 ],
'11': [ 0, 7, 10, 14, 17 ],
'13': [ 0, 4, 7, 10, 14, 21 ],
'7b9': [ 0, 4, 7, 10, 13 ],
'7♭9': [ 0, 4, 7, 10, 13 ],
'7(b9)': [ 0, 4, 7, 10, 13 ],
'7(#9)': [ 0, 4, 7, 10, 15 ],
'7#9': [ 0, 4, 7, 10, 15 ],
'(13)': [ 0, 4, 7, 10, 14, 21 ],
'7(9,13)': [ 0, 4, 7, 10, 14, 21 ],
'7(#9,b13)': [ 0, 4, 7, 10, 15, 20 ],
'7(#11)': [ 0, 4, 7, 10, 14, 18 ],
'7#11': [ 0, 4, 7, 10, 14, 18 ],
'7(b13)': [ 0, 4, 7, 10, 20 ],
'7b13': [ 0, 4, 7, 10, 20 ],
'9(#11)': [ 0, 4, 7, 10, 14, 18 ],
'9#11': [ 0, 4, 7, 10, 14, 18 ],
'13(#11)': [ 0, 4, 7, 10, 18, 21 ],
'13#11': [ 0, 4, 7, 10, 18, 21 ],
'maj7': [ 0, 4, 7, 11 ],
'∆7': [ 0, 4, 7, 11 ],
'Δ7': [ 0, 4, 7, 11 ],
'maj9': [ 0, 4, 7, 11, 14 ],
'maj7(9)': [ 0, 4, 7, 11, 14 ],
'maj7(11)': [ 0, 4, 7, 11, 17 ],
'maj7(#11)': [ 0, 4, 7, 11, 18 ],
'maj7(13)': [ 0, 4, 7, 14, 21 ],
'maj7(9,13)': [ 0, 4, 7, 11, 14, 21 ],
'7sus4': [ 0, 5, 7, 10 ],
'm7sus4': [ 0, 3, 7, 10, 17 ],
'sus4': [ 0, 5, 7 ],
'sus2': [ 0, 2, 7 ],
'7sus2': [ 0, 2, 7, 10 ],
'9sus4': [ 0, 5, 7, 10, 14 ],
'13sus4': [ 0, 5, 7, 10, 14, 21 ],
// augmented (all sharp 5 chords)
'aug7': [ 0, 4, 8, 10 ],
'+7': [ 0, 4, 8, 10 ],
'+': [ 0, 4, 8 ],
'7#5': [ 0, 4, 8, 10 ],
'7♯5': [ 0, 4, 8, 10 ],
'7+5': [ 0, 4, 8, 10 ],
'9#5': [ 0, 4, 8, 10, 14 ],
'9♯5': [ 0, 4, 8, 10, 14 ],
'9+5': [ 0, 4, 8, 10, 14 ],
'-7(#5)': [ 0, 3, 8, 10 ],
'-7#5': [ 0, 3, 8, 10 ],
'7(#5)': [ 0, 4, 8, 10 ],
'7(b9,#5)': [ 0, 4, 8, 10, 13 ],
'7b9#5': [ 0, 4, 8, 10, 13 ],
'maj7(#5)': [ 0, 4, 8, 11 ],
'maj7#5': [ 0, 4, 8, 11 ],
'maj7(#5,#11)': [ 0, 4, 8, 11, 18 ],
'maj7#5#11': [ 0, 4, 8, 11, 18 ],
'9(#5)': [ 0, 4, 8, 10, 14 ],
'13(#5)': [ 0, 4, 8, 10, 14, 21 ],
'13#5': [ 0, 4, 8, 10, 14, 21 ]
};
function chordNotes(bass, modifier) {
var intervals = chordIntervals[modifier];
if (!intervals) {
if (modifier.slice(0,2).toLowerCase() === 'ma' || modifier[0] === 'M')
intervals = chordIntervals.M;
else if (modifier[0] === 'm' || modifier[0] === '-')
intervals = chordIntervals.m;
else
intervals = chordIntervals.M;
}
bass += 12; // the chord is an octave above the bass note.
var notes = [ ];
for (var i = 0; i < intervals.length; i++) {
notes.push(bass + intervals[i]);
}
return notes;
}
function writeBoom(boom, beatLength, volume, beat, noteLength) {
// undefined means there is a stop time.
if (boom !== undefined)
chordTrack.push({cmd: 'note', pitch: boom, volume: volume, start: lastBarTime+beat*durationRounded(beatLength), duration: durationRounded(noteLength), gap: 0, instrument: bassInstrument});
}
function writeChick(chick, beatLength, volume, beat, noteLength) {
for (var c = 0; c < chick.length; c++)
chordTrack.push({cmd: 'note', pitch: chick[c], volume: volume, start: lastBarTime+beat*durationRounded(beatLength), duration: durationRounded(noteLength), gap: 0, instrument: chordInstrument});
}
var rhythmPatterns = { "2/2": [ 'boom', 'chick' ],
"2/4": [ 'boom', 'chick' ],
"3/4": [ 'boom', 'chick', 'chick' ],
"4/4": [ 'boom', 'chick', 'boom2', 'chick' ],
"5/4": [ 'boom', 'chick', 'chick', 'boom2', 'chick' ],
"6/8": [ 'boom', '', 'chick', 'boom2', '', 'chick' ],
"9/8": [ 'boom', '', 'chick', 'boom2', '', 'chick', 'boom2', '', 'chick' ],
"12/8": [ 'boom', '', 'chick', 'boom2', '', 'chick', 'boom', '', 'chick', 'boom2', '', 'chick' ],
};
function resolveChords(startTime, endTime) {
var num = meter.num;
var den = meter.den;
var beatLength = 1/den;
var noteLength = beatLength/2;
var pattern = rhythmPatterns[num+'/'+den];
var thisMeasureLength = parseInt(num,10)/parseInt(den,10);
var portionOfAMeasure = thisMeasureLength - (endTime-startTime)/tempoChangeFactor;
if (Math.abs(portionOfAMeasure) < 0.00001)
portionOfAMeasure = false;
if (!pattern || portionOfAMeasure) { // If it is an unsupported meter, or this isn't a full bar, just chick on each beat.
pattern = [];
var beatsPresent = ((endTime-startTime)/tempoChangeFactor) / beatLength;
for (var p = 0; p < beatsPresent; p++)
pattern.push("chick");
}
//console.log(startTime, pattern, currentChords, lastChord, portionOfAMeasure)
if (currentChords.length === 0) { // there wasn't a new chord this measure, so use the last chord declared.
currentChords.push({ beat: 0, chord: lastChord});
}
if (currentChords[0].beat !== 0 && lastChord) { // this is the case where there is a chord declared in the measure, but not on its first beat.
if (chordLastBar)
currentChords.unshift({ beat: 0, chord: chordLastBar});
}
if (currentChords.length === 1) {
for (var m = currentChords[0].beat; m < pattern.length; m++) {
if (!hasRhythmHead) {
switch (pattern[m]) {
case 'boom':
writeBoom(currentChords[0].chord.boom, beatLength, boomVolume, m, noteLength);
break;
case 'boom2':
writeBoom(currentChords[0].chord.boom2, beatLength, boomVolume, m, noteLength);
break;
case 'chick':
writeChick(currentChords[0].chord.chick, beatLength, chickVolume, m, noteLength);
break;
}
}
}
return;
}
// If we are here it is because more than one chord was declared in the measure, so we have to sort out what chord goes where.
// First, normalize the chords on beats.
var mult = beatLength === 0.125 ? 3 : 1; // If this is a compound meter then the beats in the currentChords is 1/3 of the true beat
var beats = {};
for (var i = 0; i < currentChords.length; i++) {
var cc = currentChords[i];
var b = Math.round(cc.beat*mult);
beats[''+b] = cc;
}
// - If there is a chord on the second beat, play a chord for the first beat instead of a bass note.
// - Likewise, if there is a chord on the fourth beat of 4/4, play a chord on the third beat instead of a bass note.
for (var m2 = 0; m2 < pattern.length; m2++) {
var thisChord;
if (beats[''+m2])
thisChord = beats[''+m2];
var lastBoom;
if (!hasRhythmHead && thisChord) {
switch (pattern[m2]) {
case 'boom':
if (beats['' + (m2 + 1)]) // If there is not a chord change on the next beat, play a bass note.
writeChick(thisChord.chord.chick, beatLength, chickVolume, m2, noteLength);
else {
writeBoom(thisChord.chord.boom, beatLength, boomVolume, m2, noteLength);
lastBoom = thisChord.chord.boom;
}
break;
case 'boom2':
if (beats['' + (m2 + 1)])
writeChick(thisChord.chord.chick, beatLength, chickVolume, m2, noteLength);
else {
// If there is the same root as the last chord, use the alternating bass, otherwise play the root.
if (lastBoom === thisChord.chord.boom) {
writeBoom(thisChord.chord.boom2, beatLength, boomVolume, m2, noteLength);
lastBoom = undefined;
} else {
writeBoom(thisChord.chord.boom, beatLength, boomVolume, m2, noteLength);
lastBoom = thisChord.chord.boom;
}
}
break;
case 'chick':
writeChick(thisChord.chord.chick, beatLength, chickVolume, m2, noteLength);
break;
case '':
if (beats['' + m2]) // If there is an explicit chord on this beat, play it.
writeChick(thisChord.chord.chick, beatLength, chickVolume, m2, noteLength);
break;
}
}
}
}
function normalizeDrumDefinition(params) {
// Be very strict with the drum definition. If anything is not perfect,
// just turn the drums off.
// Perhaps all of this logic belongs in the parser instead.
if (params.pattern.length === 0 || params.on === false)
return { on: false };
var str = params.pattern[0];
var events = [];
var event = "";
var totalPlay = 0;
for (var i = 0; i < str.length; i++) {
if (str[i] === 'd')
totalPlay++;
if (str[i] === 'd' || str[i] === 'z') {
if (event.length !== 0) {
events.push(event);
event = str[i];
} else
event = event + str[i];
} else {
if (event.length === 0) {
// there was an error: the string should have started with d or z
return {on: false};
}
event = event + str[i];
}
}
if (event.length !== 0)
events.push(event);
// Now the events array should have one item per event.
// There should be two more params for each event: the volume and the pitch.
if (params.pattern.length !== totalPlay*2 + 1)
return { on: false };
var ret = { on: true, bars: params.bars, pattern: []};
var beatLength = getBeatFraction(meter);
var playCount = 0;
for (var j = 0; j < events.length; j++) {
event = events[j];
var len = 1;
var div = false;
var num = 0;
for (var k = 1; k < event.length; k++) {
switch(event[k]) {
case "/":
if (num !== 0)
len *= num;
num = 0;
div = true;
break;
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
case "7":
case "8":
case "9":
num = num*10 +event[k];
break;
default:
return { on: false };
}
}
if (div) {
if (num === 0) num = 2; // a slash by itself is interpreted as "/2"
len /= num;
} else if (num)
len *= num;
if (event[0] === 'd') {
ret.pattern.push({ len: len * beatLength, pitch: params.pattern[1 + playCount], velocity: params.pattern[1 + playCount + totalPlay]});
playCount++;
} else
ret.pattern.push({ len: len * beatLength, pitch: null});
}
drumBars = params.bars ? params.bars : 1;
return ret;
}
function alignDrumToMeter() {
if (!drumDefinition ||!drumDefinition.pattern) {
return;
}
var ret = drumDefinition;
// Now normalize the pattern to cover the correct number of measures. The note lengths passed are relative to each other and need to be scaled to fit a measure.
var totalTime = 0;
var measuresPerBeat = meter.num/meter.den;
for (var ii = 0; ii < ret.pattern.length; ii++)
totalTime += ret.pattern[ii].len;
var factor = totalTime / drumBars / measuresPerBeat;
for (ii = 0; ii < ret.pattern.length; ii++)
ret.pattern[ii].len = ret.pattern[ii].len / factor;
drumDefinition = ret;
}
function writeDrum(channel) {
if (drumTrack.length === 0 && !drumDefinition.on)
return;
var measureLen = meter.num/meter.den;
if (drumTrack.length === 0) {
if (lastEventTime < measureLen)
return; // This is true if there are pickup notes. The drum doesn't start until the first full measure.
drumTrack.push({cmd: 'program', channel: channel, instrument: drumInstrument});
}
if (!drumDefinition.on) {
// this is the case where there has been a drum track, but it was specifically turned off.
return;
}
var start = lastBarTime;
for (var i = 0; i < drumDefinition.pattern.length; i++) {
var len = durationRounded(drumDefinition.pattern[i].len);
if (drumDefinition.pattern[i].pitch) {
drumTrack.push({
cmd: 'note',
pitch: drumDefinition.pattern[i].pitch,
volume: drumDefinition.pattern[i].velocity,
start: start,
duration: len,
gap: 0,
instrument: drumInstrument});
}
start += len;
}
}
function findOctaves(tracks, detuneCents) {
var timing = {};
for (var i = 0; i < tracks.length; i++) {
for (var j = 0; j < tracks[i].length; j++) {
var note = tracks[i][j];
if (note.cmd === "note") {
if (timing[note.start] === undefined)
timing[note.start] = [];
timing[note.start].push({track: i, event: j, pitch: note.pitch});
}
}
}
var keys = Object.keys(timing);
for (i = 0; i < keys.length; i++) {
var arr = timing[keys[i]];
if (arr.length > 1) {
arr = arr.sort(function(a,b) {
return a.pitch - b.pitch;
});
var topEvent = arr[arr.length-1];
var topNote = topEvent.pitch % 12;
var found = false;
for (j = 0; !found && j < arr.length-1; j++) {
if (arr[j].pitch % 12 === topNote)
found = true;
}
if (found) {
var event = tracks[topEvent.track][topEvent.event];
if (!event.cents)
event.cents = 0;
event.cents += detuneCents;
}
}
}
}
})();
module.exports = flatten;