diff --git a/examples/accompaniment.html b/examples/accompaniment.html new file mode 100644 index 000000000..477108af6 --- /dev/null +++ b/examples/accompaniment.html @@ -0,0 +1,237 @@ + + + + + + + + + + + abcjs: Accompaniment Demo + + + + + + + + + +
+ abcjs logo +

Accompaniment

+
+
+

This shows different options for tweaking the sound of the accompaniment.

+
+

Pattern:

+ + + + +
+
+

Stress:

+ + + +
+
+

Duration:

+ + + + +
+
+

Melody:

+ + +
+
+
+

Add this to your code:

+

+		
+
+ + + \ No newline at end of file diff --git a/examples/toc.html b/examples/toc.html index 4979e4495..5c962ba89 100644 --- a/examples/toc.html +++ b/examples/toc.html @@ -1,192 +1,206 @@ + - - - - + + + + - - + + - abcjs: Table of Contents + abcjs: Table of Contents - - + + + -
abcjs logo
+
abcjs logo
-
-

Table of Contents

-

- Click on any of the demos below to get an idea of the types of things you can do with abcjs. - If this is your first time using abcjs, the demos in the first section are suggested as they are - the easiest to set up. After loading each demo, right-click the webpage and choose "view page source" - to see how the demo works. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+

Table of Contents

+

+ Click on any of the demos below to get an idea of the types of things you can do with abcjs. + If this is your first time using abcjs, the demos in the first section are suggested as they are + the easiest to set up. After loading each demo, right-click the webpage and choose "view page source" + to see how the demo works. +

+
Start Here
BasicA demo of the most bare-boned way to display sheet music.
EditorA demo of the simplest way to present the user with an editor so they can change music on the fly.
Basic SynthA demo of the most basic way to incorporate audio playback using abcjs.
AnalysisA demo of the information that is available about an ABC tune book.
Visual Demos
AnimationA demo of animated cursor effects that can be added using abcjs.
AnnotatingA demo showing how to annotate your sheet music using abcjs.
BasicA demo of the most bare-boned way to display sheet music.
Change GlyphsA demo showing how to substitute the music symbols with your own.
Line WrappingA demo of the line wrapping capabilities.
PluginA demo showing how to use the plugin version of abcjs. (Useful for sites with a CMS where a user might enter ABC code)
PrintableA demo showing how to format the music for printing.
ResponsiveA demo showing the music filling up whatever available horizontal space is on the page.
Zoom To FitA demo showing the music filling up whatever available space is on the page.
TransposeA demo showing how to transpose.
Output Transposed ABCA demo showing how to transpose and output the result of the transposition.
TablatureA demo showing how to create tablature.
Tune BookA demo showing how to pick tunes from a tune book.
Automatically Add Note NamesA demo of modifying the SVG output after drawing to add note names.
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Start Here
BasicA demo of the most bare-boned way to display sheet music.
EditorA demo of the simplest way to present the user with an editor so they can change music on the + fly.
Basic SynthA demo of the most basic way to incorporate audio playback using abcjs.
AnalysisA demo of the information that is available about an ABC tune book.
Visual Demos
AnimationA demo of animated cursor effects that can be added using abcjs.
AnnotatingA demo showing how to annotate your sheet music using abcjs.
BasicA demo of the most bare-boned way to display sheet music.
Change GlyphsA demo showing how to substitute the music symbols with your own.
Line WrappingA demo of the line wrapping capabilities.
PluginA demo showing how to use the plugin version of abcjs. (Useful for sites with a CMS where a user + might enter ABC code)
PrintableA demo showing how to format the music for printing.
ResponsiveA demo showing the music filling up whatever available horizontal space is on the page.
Zoom To FitA demo showing the music filling up whatever available space is on the page.
TransposeA demo showing how to transpose.
Output Transposed ABCA demo showing how to transpose and output the result of the transposition.
TablatureA demo showing how to create tablature.
Tune BookA demo showing how to pick tunes from a tune book.
Automatically Add Note NamesA demo of modifying the SVG output after drawing to add note names.
Interactive Demos
DraggingA demo showing how to implement a visual interface that supports dragging.
EditorA demo of the simplest way to present the user with an editor so they can change music on the fly.
Editor With SynthA demo of the editor with synth capabilities
Editor With TransposeA demo of the editor and transposing
Interactive Demos
DraggingA demo showing how to implement a visual interface that supports dragging.
EditorA demo of the simplest way to present the user with an editor so they can change music on the + fly.
Editor With SynthA demo of the editor with synth capabilities
Editor With TransposeA demo of the editor and transposing
Audio Demos
Basic SynthA demo of the most basic way to incorporate audio playback using abcjs.
KaraokeA demo of allowing the user to turn off voices.
MicrotonesA demo of creating non-western music that relies on a different scale.
Modify Synth InputA demo showing how to tweak the synth after the music has been processed.
Play On RepeatA demo showing how to loop a section of the music.
Synth OnlyA demo showing how to create sound without any visual representation.
Synth OptionsA full-featured demo with many synth options.
Synth PlayerA demo showing how to use abcjs' synth player.
Tempo ChangingShowing how to control the tempo.
Tune/Instrument SwitcherShowing how to render and switch multiple tunes and change instruments.
Swing FeelShowing how to add a swing feel to the synth.
Analysis
AnalysisA demo of the information that is available about an ABC tune book.
ParsingA demo of getting lyrics out of an abc string.
-
+ + Audio Demos + + + Basic Synth + A demo of the most basic way to incorporate audio playback using abcjs. + + + Karaoke + A demo of allowing the user to turn off voices. + + + Microtones + A demo of creating non-western music that relies on a different scale. + + + Modify Synth Input + A demo showing how to tweak the synth after the music has been processed. + + + Play On Repeat + A demo showing how to loop a section of the music. + + + Synth Only + A demo showing how to create sound without any visual representation. + + + Synth Options + A full-featured demo with many synth options. + + + Synth Player + A demo showing how to use abcjs' synth player. + + + Tempo Changing + Showing how to control the tempo. + + + Tune/Instrument Switcher + Showing how to render and switch multiple tunes and change instruments. + + + Swing Feel + Showing how to add a swing feel to the synth. + + + Swing Feel + Showing how to add a swing feel to the synth. + + + Accompaniment + Showing how to vary the sound of the generated accompaniment. + + + Analysis + + + Analysis + A demo of the information that is available about an ABC tune book. + + + Parsing + A demo of getting lyrics out of an abc string. + + + + - + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1cd514407..09205377d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "abcjs", - "version": "6.2.3", + "version": "6.4.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "abcjs", - "version": "6.2.3", + "version": "6.4.1", "license": "MIT", "devDependencies": { "@babel/core": "7.20.12", diff --git a/src/parse/abc_parse_directive.js b/src/parse/abc_parse_directive.js index 5885f1a81..9ac9f5eae 100644 --- a/src/parse/abc_parse_directive.js +++ b/src/parse/abc_parse_directive.js @@ -485,8 +485,6 @@ var parseDirective = {}; var midiCmdParam1Integer = [ "bassvol", "chordvol", - "bassprog", - "chordprog", "c", "channel", "beatmod", @@ -500,7 +498,8 @@ var parseDirective = {}; "transpose", "rtranspose", "vol", - "volinc" + "volinc", + "gchordbars" ]; var midiCmdParam1Integer1OptionalInteger = [ "program" @@ -532,6 +531,16 @@ var parseDirective = {}; "chordname" ]; + var midiCmdParamVariableFloat = [ + "gchordstress", + "gchordduration" + ]; + + var midiCmdParam1Integer1OptionalString = [ + "bassprog", "chordprog" + ]; + + var parseMidiCommand = function(midi, tune, restOfString) { var midi_cmd = midi.shift().token; var midi_params = []; @@ -673,6 +682,46 @@ var parseDirective = {}; } } } + else if (midiCmdParamVariableFloat.indexOf(midi_cmd) >= 0){ + if (midi.length < 1) warn("Expected least one float parameter in MIDI " + midi_cmd, restOfString, 0);else { + var arr = []; + while (midi.length > 0) { + p = midi.shift(); + if (p.type !== "number") warn("Expected number parameter in MIDI " + midi_cmd, restOfString, 0); + arr.push(p.floatt); + } + midi_params.push(arr); + } + } + else if (midiCmdParam1Integer1OptionalString.indexOf(midi_cmd) >= 0){ + + // ONE INT PARAMETER, ONE OPTIONAL string + if (midi.length !== 1 && midi.length !== 2) warn("Expected one or two parameters in MIDI " + midi_cmd, restOfString, 0);else if (midi[0].type !== "number") warn("Expected integer parameter in MIDI " + midi_cmd, restOfString, 0);else if (midi.length === 2 && midi[1].type !== "alpha") warn("Expected alpha parameter in MIDI " + midi_cmd, restOfString, 0);else { + midi_params.push(midi[0].intt); + + // Currently only bassprog and chordprog with optional octave shifts use this path + if (midi.length === 2){ + var cmd = midi[1].token; + if (cmd.indexOf("octave=") != -1){ + cmd = cmd.replace("octave=",""); + cmd = parseInt(cmd); + if (!isNaN(cmd)){ + // Limit range from -1 to 3 octaves + if (cmd < -1){ + cmd = -1; + } + if (cmd > 3){ + cmd = 3; + } + midi_params.push(cmd); + } + } + else{ + warn("Expected octave= in MIDI"); + } + } + } + } if (tuneBuilder.hasBeginMusic()) tuneBuilder.appendElement('midi', -1, -1, { cmd: midi_cmd, params: midi_params }); diff --git a/src/synth/abc_midi_flattener.js b/src/synth/abc_midi_flattener.js index 5ed288cfe..584af0788 100644 --- a/src/synth/abc_midi_flattener.js +++ b/src/synth/abc_midi_flattener.js @@ -209,6 +209,9 @@ var pitchesToPerc = require('./pitches-to-perc'); case "chordprog": case "bassvol": case "chordvol": + case "gchordbars": + case "gchordstress": + case "gchordduration": chordTrack.paramChange(element) break default: @@ -347,7 +350,8 @@ var pitchesToPerc = require('./pitches-to-perc'); return 0; var volume; - if (nextVolume) { + // MAE 21 Jun 2024 - This previously wasn't allowing zero volume to be applied + if (nextVolume != undefined) { volume = nextVolume; nextVolume = undefined; } else if (!doBeatAccents) { diff --git a/src/synth/abc_midi_sequencer.js b/src/synth/abc_midi_sequencer.js index 5f2f2b78a..3c717ae4b 100644 --- a/src/synth/abc_midi_sequencer.js +++ b/src/synth/abc_midi_sequencer.js @@ -389,12 +389,62 @@ var parseCommon = require("../parse/abc_common"); break; case "swing": case "gchord": - case "bassprog": - case "chordprog": case "bassvol": case "chordvol": voices[voiceNumber].push({ el_type: elem.cmd, param: elem.params[0] }); break; + + case "bassprog": // MAE 22 May 2024 + //console.log("Handle inline bassprog"); + voices[voiceNumber].push({ + el_type: 'bassprog', + value: elem.params[0], + octaveShift: elem.params[1] + }); + break; + + case "chordprog": // MAE 22 May 2024 + //console.log("Handle inline chordprog"); + voices[voiceNumber].push({ + el_type: 'chordprog', + value: elem.params[0], + octaveShift: elem.params[1] + }); + break; + + // MAE 23 Jun 2024 + case "gchordbars": + if (gUseGChord){ + voices[voiceNumber].push({ + el_type: elem.cmd, + value: elem.params[0], + }); + } + break; + + // MAE 20 Jun 2024 + case "gchordstress":{ + //console.log("Handle inline gchordstress"); + if (gUseGChord){ + voices[voiceNumber].push({ + el_type: 'gchordstress', + param: elem.params[0] + }); + } + } + break; + + // MAE 20 Jun 2024 + case "gchordduration":{ + //console.log("Handle inline gchordduration"); + if (gUseGChord){ + voices[voiceNumber].push({ + el_type: 'gchordduration', + param: elem.params[0] + }); + } + } + break; default: console.log("MIDI seq: midi cmd not handled: ", elem.cmd, elem); } diff --git a/src/synth/chord-track.js b/src/synth/chord-track.js index 7cc76c1a3..daea00568 100644 --- a/src/synth/chord-track.js +++ b/src/synth/chord-track.js @@ -20,399 +20,1118 @@ var parseCommon = require("../parse/abc_common"); var ChordTrack = function ChordTrack(numVoices, chordsOff, midiOptions, meter) { - this.chordTrack = []; - this.chordTrackFinished = false; - this.chordChannel = numVoices; // first free channel for chords - this.currentChords = []; - this.lastChord; - this.chordLastBar; - this.chordsOff = !!chordsOff - this.gChordTacet = this.chordsOff; - this.hasRhythmHead = false; - this.transpose = 0; - this.lastBarTime = 0; - this.meter = meter; - this.tempoChangeFactor = 1; - - this.bassInstrument = midiOptions.bassprog && midiOptions.bassprog.length === 1 ? midiOptions.bassprog[0] : 0; - this.chordInstrument = midiOptions.chordprog && midiOptions.chordprog.length === 1 ? midiOptions.chordprog[0] : 0; - this.boomVolume = midiOptions.bassvol && midiOptions.bassvol.length === 1 ? midiOptions.bassvol[0] : 64; - this.chickVolume = midiOptions.chordvol && midiOptions.chordvol.length === 1 ? midiOptions.chordvol[0] : 48; - - this.overridePattern = midiOptions.gchord ? parseGChord(midiOptions.gchord[0]) : undefined -}; + + //console.log("ChordTrack top"); + + this.chordTrack = []; + this.chordTrackFinished = false; + this.chordChannel = numVoices; // first free channel for chords + this.currentChords = []; + this.lastChord; + this.chordLastBar; + this.chordsOff = !!chordsOff; + this.gChordTacet = this.chordsOff; + this.hasRhythmHead = false; + this.transpose = 0; + this.lastBarTime = 0; + this.meter = meter; + this.tempoChangeFactor = 1; + + // MAE 17 Jun 2024 - To allow for bass and chord instrument octave shifts + this.bassInstrument = midiOptions.bassprog && ((midiOptions.bassprog.length === 1) || (midiOptions.bassprog.length === 2)) ? midiOptions.bassprog[0] : 0; + this.chordInstrument = midiOptions.chordprog && ((midiOptions.chordprog.length === 1) || (midiOptions.chordprog.length === 2)) ? midiOptions.chordprog[0] : 0; + + // MAE For octave shifted bass and chords + this.bassOctaveShift = midiOptions.bassprog && midiOptions.bassprog.length === 2 ? midiOptions.bassprog[1] : 0; + this.chordOctaveShift = midiOptions.chordprog && midiOptions.chordprog.length === 2 ? midiOptions.chordprog[1] : 0; + + this.boomVolume = midiOptions.bassvol && midiOptions.bassvol.length === 1 ? midiOptions.bassvol[0] : 64; + this.chickVolume = midiOptions.chordvol && midiOptions.chordvol.length === 1 ? midiOptions.chordvol[0] : 48; + + // MAE 23 Jun 2024 - Added gchordbars + this.gchordbars = midiOptions.gchordbars && midiOptions.gchordbars.length === 1 ? midiOptions.gchordbars[0] : 1; + + if (this.gchordbars < 1){ + this.gchordbars = 1; + } + + // Track measure progress through the gchord + this.currentgchordbars = 0; + + //console.log("ChordTrack gchordbars: "+this.gchordbars); + + var defaultDurationScale = undefined; + + // This allows for an initial %%MIDI gchord with no string + if (midiOptions.gchord && (midiOptions.gchord.length>0)){ + this.overridePattern = midiOptions.gchord ? parseGChord(midiOptions.gchord[0]) : undefined; + + defaultDurationScale = midiOptions.gchord ? generateDefaultDurationScale(midiOptions.gchord[0]) : undefined; + + } + else{ + this.overridePattern = undefined; + } + + // MAE Added 20 June 2024 + // Is there a gchord stress model? + this.gchordstress = (midiOptions.gchordstress && (midiOptions.gchordstress.length === 1))? midiOptions.gchordstress[0] : undefined; + + // Is there a gchord duration scale? + this.gchordduration = (midiOptions.gchordduration && (midiOptions.gchordduration.length === 1))? midiOptions.gchordduration[0] : defaultDurationScale; + +}; ChordTrack.prototype.setMeter = function (meter) { - this.meter = meter + //console.log("setMeter: "+meter.num+ " "+meter.den); + this.meter = meter; }; - ChordTrack.prototype.setTempoChangeFactor = function (tempoChangeFactor) { - this.tempoChangeFactor = tempoChangeFactor + this.tempoChangeFactor = tempoChangeFactor; }; - ChordTrack.prototype.setLastBarTime = function (lastBarTime) { - this.lastBarTime = lastBarTime + this.lastBarTime = lastBarTime; }; - ChordTrack.prototype.setTranspose = function (transpose) { - this.transpose = transpose + this.transpose = transpose; }; - ChordTrack.prototype.setRhythmHead = function (isRhythmHead, elem) { - this.hasRhythmHead = isRhythmHead - var ePitches = []; - if (isRhythmHead) { - if (this.lastChord && this.lastChord.chick) { - for (var i2 = 0; i2 < this.lastChord.chick.length; i2++) { - var note2 = parseCommon.clone(elem.pitches[0]); - note2.actualPitch = this.lastChord.chick[i2]; - ePitches.push(note2); - } - } - } - return ePitches + this.hasRhythmHead = isRhythmHead; + var ePitches = []; + if (isRhythmHead) { + if (this.lastChord && this.lastChord.chick) { + for (var i2 = 0; i2 < this.lastChord.chick.length; i2++) { + var note2 = parseCommon.clone(elem.pitches[0]); + note2.actualPitch = this.lastChord.chick[i2]; + ePitches.push(note2); + } + } + } + return ePitches; }; - ChordTrack.prototype.barEnd = function (element) { - if (this.chordTrack.length > 0 && !this.chordTrackFinished) { - this.resolveChords(this.lastBarTime, timeToRealTime(element.time)); - this.currentChords = []; - } - this.chordLastBar = this.lastChord; + if (this.chordTrack.length > 0 && !this.chordTrackFinished) { + this.resolveChords(this.lastBarTime, timeToRealTime(element.time)); + this.currentChords = []; + } + this.chordLastBar = this.lastChord; }; - ChordTrack.prototype.gChordOn = function (element) { - if (!this.chordsOff) - this.gChordTacet = element.tacet; + if (!this.chordsOff) this.gChordTacet = element.tacet; }; - ChordTrack.prototype.paramChange = function (element) { - switch (element.el_type) { - case "gchord": - this.overridePattern = parseGChord(element.param); - break; - case "bassprog": - this.bassInstrument = element.param; - break; - case "chordprog": - this.chordInstrument = element.param; - break; - case "bassvol": - this.boomVolume = element.param; - break; - case "chordvol": - this.chickVolume = element.param; - break; - default: - console.log("unhandled midi param", element) - } -}; + switch (element.el_type) { + case "gchord": + // Skips gchord elements that don't have pattern strings + if (element.param && element.param.length>0){ + + //console.log("gchord "+element.param); + + this.overridePattern = parseGChord(element.param); + + // Generate a default duration scale based on the pattern + this.gchordduration = generateDefaultDurationScale(element.param); + + } + break; + // MAE 23 Jun 2024 - Added gchordbars + case "gchordbars": + //console.log("ChordTrack gchordbars: "+element.value); + this.gchordbars = element.value; + + if (this.gchordbars < 1){ + this.gchordbars = 1; + } + // Reset measure offset + this.currentgchordbars = 0; + + break; + case "gchordstress": + this.gchordstress = element.param; + break; + case "gchordduration": + this.gchordduration = element.param; + break; + case "bassprog": + this.bassInstrument = element.value; + if ((element.octaveShift != undefined) && (element.octaveShift != null)){ + this.bassOctaveShift = element.octaveShift; + } + else{ + this.bassOctaveShift = 0; + } + break; + case "chordprog": + this.chordInstrument = element.value; + if ((element.octaveShift != undefined) && (element.octaveShift != null)){ + this.chordOctaveShift = element.octaveShift; + } + else{ + this.chordOctaveShift = 0; + } + break; + case "bassvol": + this.boomVolume = element.param; + break; + case "chordvol": + this.chickVolume = element.param; + break; + default: + console.log("unhandled midi param", element); + } +}; ChordTrack.prototype.finish = function () { - if (!this.chordTrackEmpty()) // Don't do chords on more than one track, so turn off chord detection after we create it. - this.chordTrackFinished = true; + if (!this.chordTrackEmpty()) + // Don't do chords on more than one track, so turn off chord detection after we create it. + this.chordTrackFinished = true; }; - ChordTrack.prototype.addTrack = function (tracks) { - if (!this.chordTrackEmpty()) - tracks.push(this.chordTrack); + if (!this.chordTrackEmpty()) tracks.push(this.chordTrack); }; - ChordTrack.prototype.findChord = function (elem) { - if (this.gChordTacet) - return 'break'; - - // TODO-PER: Just using the first chord if there are more than one. - if (this.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 (this.breakSynonyms.indexOf(ch.name.toLowerCase()) >= 0) - return 'break'; - } - return null; -} + if (this.gChordTacet) return 'break'; + + // TODO-PER: Just using the first chord if there are more than one. + if (this.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 (this.breakSynonyms.indexOf(ch.name.toLowerCase()) >= 0) return 'break'; + } + return null; +}; ChordTrack.prototype.interpretChord = function (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 = this.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 = this.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 = this.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 = this.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; - } + // 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: [] + }; - if (arr.length === 2) { - var explicitBass = this.basses[arr[1].substring(0, 1)]; - if (explicitBass) { - var bassAcc = arr[1].substring(1); - var bassShift = { '#': 1, '♯': 1, 'b': -1, '♭': -1 }[bassAcc] || 0; - bass = this.basses[arr[1].substring(0, 1)] + bassShift + chordTranspose; - bass2 = bass; - } - } - return { boom: bass, boom2: bass2, chick: chick }; -} + // MAE 23 May 2024 - Experimenting with chord inversions + var chordInfo = this.processInversion(name); -ChordTrack.prototype.chordNotes = function (bass, modifier) { - var intervals = this.chordIntervals[modifier]; - if (!intervals) { - if (modifier.slice(0, 2).toLowerCase() === 'ma' || modifier[0] === 'M') - intervals = this.chordIntervals.M; - else if (modifier[0] === 'm' || modifier[0] === '-') - intervals = this.chordIntervals.m; - else - intervals = this.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; + name = chordInfo.name; + var inversion = chordInfo.inversion; + + //console.log("chord name: "+name+" inversion: "+inversion); + + 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 = this.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 = this.transpose; + while (chordTranspose < -8) { + chordTranspose += 12; + } + while (chordTranspose > 8) { + chordTranspose -= 12; + } + bass += chordTranspose; + + // MAE 17 Jun 2024 - Supporting octave shifted bass and chords + var unshiftedBass = bass; + + bass += this.bassOctaveShift*12; + + var bass2 = bass - 5; // The alternating bass is a 4th below + var chick; + // if (name.length === 1){ + // console.log("simple case"); + // chick = this.chordNotes(unshiftedBass, '', inversion).notes; + // } + var remaining = name.substring(1); + var acc = remaining.substring(0, 1); + if (acc === 'b' || acc === '♭') { + unshiftedBass--; + bass--; + bass2--; + remaining = remaining.substring(1); + } else if (acc === '#' || acc === '♯') { + unshiftedBass++; + bass++; + bass2++; + remaining = remaining.substring(1); + } + var arr = remaining.split('/'); + + // MAE 22 May 2024 - For octave shifted chords + var invertedInfo = this.chordNotes(unshiftedBass, arr[0], inversion); + + chick = invertedInfo.notes; + var invertedNotes = invertedInfo.invertedNotes; + + // 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 = this.basses[arr[1].substring(0, 1)]; + if (explicitBass) { + var bassAcc = arr[1].substring(1); + var bassShift = { + '#': 1, + '♯': 1, + 'b': -1, + '♭': -1 + }[bassAcc] || 0; + bass = this.basses[arr[1].substring(0, 1)] + bassShift + chordTranspose; + + // MAE 22 May 2024 - Supporting octave shifted bass and chords + bass += this.bassOctaveShift*12; + + bass2 = bass; + } + } + return { + boom: bass, + boom2: bass2, + chick: invertedNotes + }; +}; + +// +// Chord inversions are represented by a : and a number after the chord name +// +ChordTrack.prototype.processInversion = function(chordName){ + + var theSplits = chordName.split(":"); + + var theChordName = theSplits[0]; + var theInversion = theSplits[1]; + + if (theInversion != undefined){ + + switch (theInversion){ + case "0": + theInversion = 0; + break; + case "1": + theInversion = 1; + break; + case "2": + theInversion = 2; + break; + case "3": + theInversion = 3; + break; + case "4": + theInversion = 4; + break; + case "5": + theInversion = 5; + break; + case "6": + theInversion = 6; + break; + case "7": + theInversion = 7; + break; + case "8": + theInversion = 8; + break; + case "9": + theInversion = 9; + break; + case "10": + theInversion = 10; + break; + case "11": + theInversion = 11; + break; + case "12": + theInversion = 12; + break; + case "13": + theInversion = 13; + break; + default: + theInversion = 0; + break; + } + } + else{ + theInversion = 0; + } + + return {name:theChordName,inversion:theInversion}; } +ChordTrack.prototype.chordNotes = function (bass, modifier,inversion) { + + var originalInversion = inversion; + + var intervals = this.chordIntervals[modifier]; + if (!intervals) { + if (modifier.slice(0, 2).toLowerCase() === 'ma' || modifier[0] === 'M') intervals =this.chordIntervals.M;else if (modifier[0] === 'm' || modifier[0] === '-') intervals = this.chordIntervals.m;else intervals = this.chordIntervals.M; + } + + var finalIntervals = intervals.slice(); + + if (inversion != 0){ + + var rawIntervals = intervals.slice(); + + if (inversion >= (intervals.length+1)){ + + inversion = inversion % (rawIntervals.length + 1); + + } + + for (i=0;i 0 && currentChordsExpanded[p-1] && currentChordsExpanded[p] && currentChordsExpanded[p-1].boom !== currentChordsExpanded[p].boom) - firstBoom = true - var type = thisPattern[p] - var isBoom = type.indexOf('boom') >= 0 - // If we changed chords at a time when we're not expecting a bass note, then add an extra bass note in if the first thing in the pattern is a bass note. - var newBass = !isBoom && - p !== 0 && - thisPattern[0].indexOf('boom') >= 0 && - (!currentChordsExpanded[p-1] || currentChordsExpanded[p-1].boom !== currentChordsExpanded[p].boom) - var pitches = resolvePitch(currentChordsExpanded[p], type, firstBoom, newBass) - if (isBoom) - firstBoom = false - for (var oo = 0; oo < pitches.length; oo++) { - this.writeNote(pitches[oo], - 0.125, - isBoom || newBass ? this.boomVolume : this.chickVolume, - p, - noteLength, - isBoom || newBass ? this.bassInstrument : this.chordInstrument - ) - if (newBass) - newBass = false - else - isBoom = false // only the first note in a chord is a bass note. This handles the case where bass and chord are played at the same time. - } + // If there is a rhythm head anywhere in the measure then don't add a separate rhythm track + if (this.hasRhythmHead) return; + var num = this.meter.num; + var den = this.meter.den; + var beatLength = 1 / den; + + // Auto determine the gchord divider if none specified with the gchord + if (this.overridePattern){ + + //console.log("resolveChords overridePattern: ["+this.overridePattern.join("][")+"]"); + + var nSlots; + + if (den == 8){ + nSlots = num; + } + else if (den == 4){ + nSlots = num*2; + } + + // Save this for later gchordbars slicing + var originalNSlots = nSlots; + + // Scale the slot count by the bars the pattern extends over + nSlots *= this.gchordbars; + + //console.log("resolveChords - nSlots: "+nSlots); + + var gchordDivider = 0 + + var len = this.overridePattern.length; + + if (len <= nSlots){ + + //console.log("resolveChords - auto divider: 1"); + gchordDivider = 1; + + } + else if (len <= (nSlots*2)){ + + //console.log("resolveChords - auto divider: 2"); + gchordDivider = 2; + + } + else{ + + //console.log("resolveChords - auto divider: 4"); + gchordDivider = 4; + + } } - return -} + //console.log("resolveChords - final divider: "+gchordDivider); + + // Scale the slot count by the chord divider + originalNSlots *= gchordDivider; + + // Is there a gchord timing divider? + if (gchordDivider > 1){ + beatLength /= gchordDivider; + } + + // MAE 16 Jun 2024 - For beat length extension + var noteLength = beatLength / 2; + + var thisMeasureLength = parseInt(num, 10) / parseInt(den, 10); + var portionOfAMeasure = thisMeasureLength - (endTime - startTime) / this.tempoChangeFactor; + + if (Math.abs(portionOfAMeasure) < 0.00001) portionOfAMeasure = 0; + + // there wasn't a new chord this measure, so use the last chord declared. + // also the case where there is a chord declared in the measure, but not on its first beat. + if (this.currentChords.length === 0 || this.currentChords[0].beat !== 0) { + this.currentChords.unshift({ + beat: 0, + chord: this.chordLastBar + }); + } + //console.log(this.currentChords) + var currentChordsExpanded = expandCurrentChords(this.currentChords, 8 * num / den, beatLength, gchordDivider); + //console.log(currentChordsExpanded) + + var thisPattern = this.overridePattern ? this.overridePattern : this.rhythmPatterns[num + '/' + den]; + + var thisGChordStressPattern = this.gchordstress; + + var thisGChordDuration = this.gchordduration; + + // No stress pattern? Create a unity gain version + if (!thisGChordStressPattern){ + arr = []; + for (var i=0;i (2 * gchordDivider)){ + + for (var p = 0; p < beatsPresent; p++) { + + if (p 0 && currentChordsExpanded[p - 1] && currentChordsExpanded[p] && currentChordsExpanded[p - 1].boom !== currentChordsExpanded[p].boom){ + firstBoom = true; + } + var type = thisPattern[p]; + + var stress = thisGChordStressPattern[p]; + + // Range check the stress, can't be negative + if (stress < 0){ + stress = 0; + } + + var durationScale = thisGChordDuration[p]; + + if (durationScale < 0){ + durationScale = 0; + } + + //console.log('type: '+type+" stress: "+stress); + + var isBoom = type.indexOf('boom') >= 0; + + // If we changed chords at a time when we're not expecting a bass note, then add an extra bass note in. + var newBass = !isBoom && p !== 0 && thisPattern[0].indexOf('boom') >= 0 && (!currentChordsExpanded[p - 1] || currentChordsExpanded[p - 1].boom !== currentChordsExpanded[p].boom); + + if (!isBoom){ + firstBoom = false; + } + + var pitches = resolvePitch(currentChordsExpanded[p], type, firstBoom, newBass); + + for (var oo = 0; oo < pitches.length; oo++) { + + // Allow for control of boom and chick lengths + var thisNoteLength = noteLength*durationScale; + + if (thisNoteLength < 0){ + thisNoteLength = 0; + } + + // Limit range of stressed notes to 0 - 127 + var boomVolume = Math.floor(this.boomVolume * stress); + + if (boomVolume < 0){ + boomVolume = 0; + } + + if (boomVolume > 127){ + boomVolume = 127; + } + + var chickVolume = Math.floor(this.chickVolume * stress); + + if (chickVolume < 0){ + chickVolume = 0; + } + + if (chickVolume > 127){ + chickVolume = 127; + } + + // Make sure not writing a bad pitch + if (pitches[oo]){ + + this.writeNote(pitches[oo], 0.125/gchordDivider, isBoom || newBass ? boomVolume : chickVolume, p, thisNoteLength, isBoom || newBass ? this.bassInstrument : this.chordInstrument); + + } + + if (newBass) newBass = false;else isBoom = false; // only the first note in a chord is a bass note. This handles the case where bass and chord are played at the same time. + + } + } + + return; +}; ChordTrack.prototype.processChord = function (elem) { - if (this.chordTrackFinished) - return - var chord = this.findChord(elem); - if (chord) { - var c = this.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 (this.chordTrack.length === 0) { - this.chordTrack.push({ cmd: 'program', channel: this.chordChannel, instrument: this.chordInstrument }); - } - - this.lastChord = c; - var barBeat = calcBeat(this.lastBarTime, timeToRealTime(elem.time)); - this.currentChords.push({ chord: this.lastChord, beat: barBeat, start: timeToRealTime(elem.time) }); - } - } + if (this.chordTrackFinished) return; + var chord = this.findChord(elem); + if (chord) { + var c = this.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 (this.chordTrack.length === 0) { + this.chordTrack.push({ + cmd: 'program', + channel: this.chordChannel, + instrument: this.chordInstrument + }); + } + this.lastChord = c; + var barBeat = calcBeat(this.lastBarTime, timeToRealTime(elem.time)); + this.currentChords.push({ + chord: this.lastChord, + beat: barBeat, + start: timeToRealTime(elem.time) + }); + } + } +}; + +function extendArray(arr, size) { + + // Check if the desired size is smaller than or equal to the current array size + if (size <= arr.length) { + return arr.slice(0, size); + } + + // Calculate how many times the array needs to be repeated + let repeatCount = Math.floor(size / arr.length); + let remainder = size % arr.length; + + // Create the new extended array by repeating the original array + let extendedArray = []; + for (let i = 0; i < repeatCount; i++) { + extendedArray = extendedArray.concat(arr); + } + + // Add the remaining elements to reach the desired size + extendedArray = extendedArray.concat(arr.slice(0, remainder)); + + return extendedArray; } function resolvePitch(currentChord, type, firstBoom, newBass) { - var ret = [] - if (!currentChord) - return ret - if (type.indexOf('boom') >= 0) - ret.push(firstBoom ? currentChord.boom : currentChord.boom2) - else if (newBass) - ret.push(currentChord.boom) - if (type.indexOf('chick') >= 0) { - for (var i = 0; i < currentChord.chick.length; i++) - ret.push(currentChord.chick[i]) - } - switch (type) { - case 'DO': ret.push(currentChord.chick[0]); break; - case 'MI': ret.push(currentChord.chick[1]); break; - case 'SOL': ret.push(currentChord.chick[2]); break; - case 'TI': currentChord.chick.length > 3 ? ret.push(currentChord.chick[2]) : ret.push(currentChord.chick[0]+12); break; - case 'TOP': currentChord.chick.length > 4 ? ret.push(currentChord.chick[2]) : ret.push(currentChord.chick[1]+12); break; - case 'do': ret.push(currentChord.chick[0]+12); break; - case 'mi': ret.push(currentChord.chick[1]+12); break; - case 'sol': ret.push(currentChord.chick[2]+12); break; - case 'ti': currentChord.chick.length > 3 ? ret.push(currentChord.chick[2]+12) : ret.push(currentChord.chick[0]+24); break; - case 'top': currentChord.chick.length > 4 ? ret.push(currentChord.chick[2]+12) : ret.push(currentChord.chick[1]+24); break; - } - return ret + var ret = []; + if (!currentChord) return ret; + + if (type.indexOf('boom') >= 0){ + + // Testing for breaks + if (!currentChord.boom){ + //console.log("Got break, early return 1") + return ret; + } + + ret.push(firstBoom ? currentChord.boom : currentChord.boom2) + } + else + if (newBass){ + + // Testing for breaks + if (!currentChord.boom){ + //console.log("Got break, early return 2") + return ret; + } + + ret.push(currentChord.boom); + } + + if (type.indexOf('chick') >= 0) { + for (var i = 0; i < currentChord.chick.length; i++) { + ret.push(currentChord.chick[i]); + } + } + + // Added 21 Jun 2024 for power chords + var isPowerChord = currentChord.chick.length == 2; + + if (!isPowerChord){ + switch (type) { + case 'DO': + ret.push(currentChord.chick[0]); + break; + case 'MI': + ret.push(currentChord.chick[1]); + break; + case 'SOL': + ret.push(currentChord.chick[2]); + break; + case 'TI': + currentChord.chick.length > 3 ? ret.push(currentChord.chick[3]) : ret.push(currentChord.chick[0] + 12); + break; + case 'TOP': + currentChord.chick.length > 4 ? ret.push(currentChord.chick[4]) : ret.push(currentChord.chick[1] + 12); + break; + case 'do': + ret.push(currentChord.chick[0] + 12); + break; + case 'mi': + ret.push(currentChord.chick[1] + 12); + break; + case 'sol': + ret.push(currentChord.chick[2] + 12); + break; + case 'ti': + currentChord.chick.length > 3 ? ret.push(currentChord.chick[3] + 12) : ret.push(currentChord.chick[0] + 24); + break; + case 'top': + currentChord.chick.length > 4 ? ret.push(currentChord.chick[4] + 12) : ret.push(currentChord.chick[1] + 24); + break; + } + } + else{ + //console.log("Power chord"); + switch (type) { + case 'DO': + ret.push(currentChord.chick[0]); + break; + case 'MI': + ret.push(currentChord.chick[1]); + break; + case 'SOL': + ret.push(currentChord.chick[0] + 12); + break; + case 'TI': + ret.push(currentChord.chick[1] + 12); + break; + case 'TOP': + ret.push(currentChord.chick[0] + 24); + break; + case 'do': + ret.push(currentChord.chick[0] + 12); + break; + case 'mi': + ret.push(currentChord.chick[1] + 12); + break; + case 'sol': + ret.push(currentChord.chick[0] + 24); + break; + case 'ti': + ret.push(currentChord.chick[1] + 24); + break; + case 'top': + ret.push(currentChord.chick[0] + 36); + break; + } + } + return ret; +} + +// Parse the gchord pattern and generate a default duration scale +function generateDefaultDurationScale(pattern){ + + var result = []; + var i = 0; + + while (i < pattern.length) { + + let char = pattern[i]; + let digits = ''; + + // Move to the next character + i++; + + // Collect all digits following the character + while (i < pattern.length && /\d/.test(pattern[i])) { + + digits += pattern[i]; + i++; + + } + + var thisValue = digits.length > 0 ? parseInt(digits, 10) : 1 + + // If there are digits, parse them as an integer, otherwise default to 1 + result.push(thisValue); + + // If value is not 1, pad to the full duration + if (thisValue > 1){ + + thisValue--; + + for (j=0;j 1){ + testInt--; + for (var j=0;j