// Copyright (c) 2016-17 Walter Bender // // This program is free software; you can redistribute it and/or // modify it under the terms of the The GNU Affero General Public // License as published by the Free Software Foundation; either // version 3 of the License, or (at your option) any later version. // // You should have received a copy of the GNU Affero General Public // License along with this library; if not, write to the Free Software // Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA // Scalable sinewave graphic const SYNTHSVG = ' '; // Notes graphics const WHOLENOTE = ' '; const HALFNOTE = ' '; const QUARTERNOTE = ' '; const EIGHTHNOTE = ' '; const SIXTEENTHNOTE = ' '; const THIRTYSECONDNOTE = ' '; const SIXTYFOURTHNOTE = ' '; const SHARP = '♯'; const FLAT = '♭'; const BTOFLAT = {'Eb': 'E♭', 'Gb': 'G♭', 'Ab': 'A♭', 'Bb': 'B♭', 'Db': 'D♭', 'Cb': 'B', 'Fb': 'E', 'eb': 'E♭', 'gb': 'G♭', 'ab': 'A♭', 'bb': 'B♭', 'db': 'D♭', 'cb': 'B', 'fb': 'E'}; const STOSHARP = {'E#': 'F', 'G#': 'G♯', 'A#': 'A♯', 'B#': 'C', 'D#': 'D♯', 'C#': 'C♯', 'F#': 'F♯', 'e#': 'F', 'g#': 'G♯', 'a#': 'A♯', 'b#': 'C', 'd#': 'D♯', 'c#': 'C♯', 'f#': 'F♯'}; const NOTESSHARP = ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B']; const NOTESFLAT = ['C', 'D♭', 'D', 'E♭', 'E', 'F', 'G♭', 'G', 'A♭', 'A', 'B♭', 'B']; const NOTESFLAT2 = ['c', 'd♭', 'd', 'e♭', 'e', 'f', 'g♭', 'g', 'a♭', 'a', 'b♭', 'b']; const EQUIVALENTNOTES = {'C♯': 'D♭', 'D♯': 'E♭', 'F♯': 'G♭', 'G♯': 'A♭', 'A♯': 'B♭', 'D♭': 'C♯', 'E♭': 'D♯', 'G♭': 'F♯', 'A♭': 'G♯', 'B♭': 'A♯'}; const EXTRATRANSPOSITIONS = {'E♯': ['F', 0], 'B♯': ['C', 1], 'C♭': ['B', -1], 'F♭': ['E', 0], 'e♯': ['F', 0], 'b♯': ['C', 1], 'c♭': ['B', -1], 'f♭': ['E', 0]}; const SOLFEGENAMES = ['do', 're', 'mi', 'fa', 'sol', 'la', 'ti']; const SOLFEGECONVERSIONTABLE = {'C': 'do', 'C♯': 'do' + '♯', 'D': 're', 'D♯': 're' + '♯', 'E': 'mi', 'F': 'fa', 'F♯': 'fa' + '♯', 'G': 'sol', 'G♯': 'sol' + '♯', 'A': 'la', 'A♯': 'la' + '♯', 'B': 'ti', 'D♭': 're' + '♭', 'E♭': 'mi' + '♭', 'G♭': 'sol' + '♭', 'A♭': 'la' + '♭', 'B♭': 'ti' + '♭', 'R': _('rest')}; const WESTERN2EISOLFEGENAMES = {'do': 'sa', 're': 're', 'mi': 'ga', 'fa': 'ma', 'sol': 'pa', 'la': 'dha', 'ti': 'ni'}; const PITCHES = ['C', 'D♭', 'D', 'E♭', 'E', 'F', 'G♭', 'G', 'A♭', 'A', 'B♭', 'B']; const PITCHES1 = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']; const PITCHES2 = ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B']; const PITCHES3 = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; const NOTESTABLE = {1: "do", 2: "do♯", 3: "re", 4: "re♯", 5: "mi", 6: "fa", 7: "fa♯", 8: "sol", 9: "sol♯", 10: "la", 11: "la♯", 0: "ti"}; const NOTESTEP = {'C': 1, 'D': 3, 'E': 5, 'F': 6, 'G': 8, 'A': 10, 'B': 12}; // Halfsteps used in calculating absolute intervals const AUGMENTED = {1: 1, 2: 2, 3: 5, 4: 6, 5: 8, 6: 9, 7: 11, 8: 13}; const PERFECT = {1: 0, 4: 5, 5: 7, 8: 12}; const DIMINISHED = {1: -1, 2: 0, 3: 2, 4: 4, 5: 6, 6: 7, 7: 9, 8: 11}; const MAJOR = {2: 2, 3: 4, 6: 9, 7: 11}; const MINOR = {2: 1, 3: 3, 6: 8, 7: 10}; // SOLFNOTES is the internal representation used in selectors const SOLFNOTES = ['ti', 'la', 'sol', 'fa', 'mi', 're', 'do']; const EASTINDIANSOLFNOTES = ['ni', 'dha', 'pa', 'ma', 'ga', 're', 'sa'] const SOLFATTRS = ['♯♯', '♯', '♮', '♭', '♭♭']; function mod12(a) { while (a < 0) { a += 12; } return a % 12; } function calcAugmented(obj) { var interval = obj[0]; var deltaOctave = obj[1]; if (interval < 0) { return -AUGMENTED[-interval] + (12 * deltaOctave); } else { return AUGMENTED[interval] + (12 * deltaOctave); } } function calcPerfect(obj) { var interval = obj[0]; var deltaOctave = obj[1]; if (interval < 0) { return -PERFECT[-interval] + (12 * deltaOctave); } else { return PERFECT[interval] + (12 * deltaOctave); } } function calcDiminished(obj) { var interval = obj[0]; var deltaOctave = obj[1]; if (interval < 0) { return -DIMINISHED[-interval] + (12 * deltaOctave); } else { return DIMINISHED[interval] + (12 * deltaOctave); } } function calcMajor(obj) { var interval = obj[0]; var deltaOctave = obj[1]; if (interval < 0) { return -MAJOR[-interval] + (12 * deltaOctave); } else { return MAJOR[interval] + (12 * deltaOctave); } } function calcMinor(obj) { var interval = obj[0]; var deltaOctave = obj[1]; if (interval < 0) { return -MINOR[-interval] + (12 * deltaOctave); } else { return MINOR[interval] + (12 * deltaOctave); } } const SEMITONES = 12; const POWER2 = [1, 2, 4, 8, 16, 32, 64, 128]; const TWELTHROOT2 = 1.0594630943592953; const TWELVEHUNDRETHROOT2 = 1.0005777895065549; const A0 = 27.5; const C8 = 4186.01; const RHYTHMRULERHEIGHT = 100; const SLIDERHEIGHT = 200; const SLIDERWIDTH = 50; const MATRIXBUTTONCOLOR = '#c374e9'; const MATRIXLABELCOLOR = '#90c100'; const MATRIXNOTECELLCOLOR = '#b1db00'; const MATRIXTUPLETCELLCOLOR = '#57e751'; const MATRIXRHYTHMCELLCOLOR = '#c8c8c8'; const MATRIXBUTTONCOLORHOVER = '#c894e0'; const MATRIXNOTECELLCOLORHOVER = '#c2e820'; const MATRIXSOLFEWIDTH = 52; const EIGHTHNOTEWIDTH = 24; const MATRIXBUTTONHEIGHT = 40; const MATRIXBUTTONHEIGHT2 = 66; const MATRIXSOLFEHEIGHT = 30; const wholeNoteImg = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(WHOLENOTE))); const halfNoteImg = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(HALFNOTE))); const quarterNoteImg = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(QUARTERNOTE))); const eighthNoteImg = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(EIGHTHNOTE))); const sixteenthNoteImg = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(SIXTEENTHNOTE))); const thirtysecondNoteImg = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(THIRTYSECONDNOTE))); const sixtyfourthNoteImg = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(SIXTYFOURTHNOTE))); const NOTESYMBOLS = {1: wholeNoteImg, 2: halfNoteImg, 4: quarterNoteImg, 8: eighthNoteImg, 16: sixteenthNoteImg, 32: thirtysecondNoteImg, 64: sixtyfourthNoteImg}; // The table contains the intervals that define the modes. // All of these modes assume 12 semitones per octave. // See http://www.pianoscales.org const MUSICALMODES = { // 12 notes in an octave 'chromatic': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], // 8 notes in an octave 'algerian': [2, 1, 2, 1, 1, 1, 3, 1], 'diminished': [2, 1, 2, 1, 2, 1, 2, 1], 'spanish': [1, 2, 1, 1, 1, 2, 2, 2], 'ocatonic': [1, 2, 1, 2, 1, 2, 1, 2], // 7 notes in an octave 'major': [2, 2, 1, 2, 2, 2, 1], 'ionian': [2, 2, 1, 2, 2, 2, 1], 'dorian': [2, 1, 2, 2, 2, 1, 2], 'phrygian': [1, 2, 2, 2, 1, 2, 2], 'lydian': [2, 2, 2, 1, 2, 2, 1], 'mixolydian': [2, 2, 1, 2, 2, 1, 2], 'minor': [2, 1, 2, 2, 1, 2, 2], 'aeolian': [2, 1, 2, 2, 1, 2, 2], 'locrian': [1, 2, 2, 1, 2, 2, 2], 'jazz minor': [2, 1, 2, 2, 2, 2, 1], 'bebop': [1, 1, 1, 2, 2, 1, 2], 'arabic': [2, 2, 1, 1, 2, 2, 2], 'byzantine': [1, 3, 1, 2, 1, 3, 1], 'enigmatic': [1, 3, 2, 2, 2, 1, 1], 'ethiopian': [2, 1, 2, 2, 1, 2, 2], 'geez': [2, 1, 2, 2, 1, 2, 2], 'hindu': [2, 2, 1, 2, 1, 2, 2], 'hungarian': [2, 1, 3, 1, 1, 3, 1], 'maqam': [1, 3, 1, 2, 1, 3, 1], 'romanian minor': [2, 1, 3, 1, 2, 1, 2], 'spanish gypsy': [1, 3, 1, 2, 1, 2, 2], // 6 notes in an octave 'blues': [3, 2, 1, 1, 3, 2], 'major blues': [2, 1, 1, 3, 2, 2], 'whole tone': [2, 2, 2, 2, 2, 2], // 5 notes in an octave 'pentatonic': [3, 2, 2, 3, 2], 'chinese': [4, 2, 1, 4, 1], 'egyptian': [2, 3, 2, 3, 2], 'hirajoshi': [1, 4, 1, 4, 2], 'japanese': [1, 4, 2, 3, 2], 'fibonacci': [1, 1, 2, 3, 5], // User definition overrides this constant 'custom': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], }; const MAQAMTABLE = { 'hijaz kar': 'C maqam', 'hijaz kar maqam': 'C maqam', 'shahnaz': 'D maqam', 'maqam mustar': 'Eb maqam', 'maqam jiharkah': 'F maqam', 'shadd araban': 'G maqam', 'suzidil': 'A maqam', 'ajam': 'Bb maqam', 'ajam maqam': 'Bb maqam', }; var MODENAMES = [ //.TRANS: twelve semi-tone scale for music [_('chromatic'), 'chromatic'], [_('algerian'), 'algerian'], //.TRANS: modal scale for music [_('diminished'), 'diminished'], [_('spanish'), 'spanish'], //.TRANS: modal scale for music [_('octatonic'), 'octatonic'], //.TRANS: major scales in music [_('major'), 'major'], //.TRANS: modal scale for music [_('ionian'), 'ionian'], //.TRANS: modal scale for music [_('dorian'), 'dorian'], //.TRANS: modal scale for music [_('phrygian'), 'phrygian'], //.TRANS: modal scale for music [_('lydian'), 'lydian'], //.TRANS: modal scale for music [_('mixolydian'), 'mixolydian'], //.TRANS: natural minor scales in music [_('minor'), 'minor'], //.TRANS: modal scale for music [_('aeolian'), 'aeolian'], //.TRANS: modal scale for music [_('locrian'), 'locrian'], //.TRANS: minor jazz scale for music [_('jazz minor'), 'jazz minor'], //.TRANS: bebop scale for music [_('bebop'), 'bebop'], [_('arabic'), 'arabic'], [_('byzantine'), 'byzantine'], //.TRANS: musical scale for music by Verdi [_('enigmatic'), 'enigmatic'], [_('ethiopian'), 'ethiopian'], //.TRANS: Ethiopic scale for music [_('geez'), 'geez'], [_('hindu'), 'hindu'], [_('hungarian'), 'hungarian'], //.TRANS: minor Romanian scale for music [_('romanian minor'), 'romanian minor'], [_('spanish gypsy'), 'spanish gypsy'], //.TRANS: musical scale for Mid-Eastern music [_('maqam'), 'maqam'], //.TRANS: minor blues scale for music [_('blues'), 'blues'], //.TRANS: major blues scale for music [_('major blues'), 'major blues'], [_('whole tone'), 'whole tone'], //.TRANS: pentatonic scale in music [_('pentatonic'), 'pentatonic'], [_('chinese'), 'chinese'], [_('egyptian'), 'egyptian'], //.TRANS: Japanese pentatonic scale for music [_('hirajoshi'), 'hirajoshi'], [_('japanese'), 'japanese'], //.TRANS: Italian mathematician [_('fibonacci'), 'fibonacci'], [_('custom'), 'custom'], ]; var VOICENAMES = [ //.TRANS: musical instrument [_('violin'), 'violin', 'images/voices.svg'], //.TRANS: musical instrument [_('cello'), 'cello', 'images/voices.svg'], //.TRANS: musical instrument // [_('basse'), 'basse', 'images/voices.svg'], //.TRANS: polytone synthesizer [_('poly'), 'poly', 'images/synth.svg'], //.TRANS: sine wave [_('sine'), 'sine', 'images/synth.svg'], //.TRANS: square wave [_('square'), 'square', 'images/synth.svg'], //.TRANS: sawtooth wave [_('sawtooth'), 'sawtooth', 'images/synth.svg'], //.TRANS: triangle wave [_('triangle'), 'triangle', 'images/synth.svg'], ]; var DRUMNAMES = [ //.TRANS: musical instrument [_('snare drum'), 'snare drum', 'images/snaredrum.svg'], //.TRANS: musical instrument [_('kick drum'), 'kick drum', 'images/kick.svg'], //.TRANS: musical instrument [_('tom tom'), 'tom tom', 'images/tom.svg'], //.TRANS: musical instrument [_('floor tom tom'), 'floor tom tom', 'images/floortom.svg'], //.TRANS: a drum made from an inverted cup [_('cup drum'), 'cup drum', 'images/cup.svg'], //.TRANS: musical instrument [_('darbuka drum'), 'darbuka drum', 'images/darbuka.svg'], //.TRANS: musical instrument [_('hi hat'), 'hi hat', 'images/hihat.svg'], //.TRANS: a small metal bell [_('ride bell'), 'ride bell', 'images/ridebell.svg'], //.TRANS: musical instrument [_('cow bell'), 'cow bell', 'images/cowbell.svg'], //.TRANS: musical instrument [_('triangle bell'), 'trianglebell', 'images/trianglebell.svg'], //.TRANS: musical instrument [_('finger cymbals'), 'finger cymbals', 'images/fingercymbals.svg'], //.TRANS: a musically tuned set of bells [_('chine'), 'chine', 'images/chine.svg'], //.TRANS: sound effect [_('clang'), 'clang', 'images/clang.svg'], //.TRANS: sound effect [_('crash'), 'crash', 'images/crash.svg'], //.TRANS: sound effect [_('bottle'), 'bottle', 'images/bottle.svg'], //.TRANS: sound effect [_('clap'), 'clap', 'images/clap.svg'], //.TRANS: sound effect [_('slap'), 'slap', 'images/slap.svg'], //.TRANS: sound effect [_('splash'), 'splash', 'images/splash.svg'], //.TRANS: sound effect [_('bubbles'), 'bubbles', 'images/bubbles.svg'], //.TRANS: animal sound effect [_('cat'), 'cat', 'images/cat.svg'], //.TRANS: animal sound effect [_('cricket'), 'cricket', 'images/cricket.svg'], //.TRANS: animal sound effect [_('dog'), 'dog', 'images/dog.svg'], //.TRANS: animal sound effect [_('duck'), 'duck', 'images/duck.svg'], ]; const DEFAULTVOICE = 'sine'; const DEFAULTDRUM = 'kick drum'; const DEFAULTMODE = 'major'; var customMode = MUSICALMODES['custom']; // The sample has a pitch which is subsequently transposed. This // number is that starting pitch. const SAMPLECENTERNO = {'violin': 63, 'cello': 39, 'basse': 15}; function getModeName(name) { for (var mode in MODENAMES) { if (MODENAMES[mode][0] === name || MODENAMES[mode][1].toLowerCase() === name.toLowerCase()) { if (MODENAMES[mode][0] != '') { return MODENAMES[mode][0]; } else { console.log('I18n for mode name is misbehaving.'); console.log(name + ' ' + name.toLowerCase() + ' ' + MODENAMES[mode][0].toLowerCase() + ' ' + MODENAMES[mode][1].toLowerCase()); return MODENAMES[mode][1]; } } } console.log(name + ' not found in MODENAMES'); return name; }; function initModeI18N() { for (var i = 0; i < MODENAMES.length; i++) { if (MODENAMES[i][0] == null) { MODENAMES[i][0] = _(MODENAMES[i][1]); } if (MODENAMES[i][0] == null) { MODENAMES[i][0] = MODENAMES[i][1]; } } }; function initVoiceI18N() { for (var i = 0; i < VOICENAMES.length; i++) { if (VOICENAMES[i][0] == null) { VOICENAMES[i][0] = _(VOICENAMES[i][1]); } if (VOICENAMES[i][0] == null) { VOICENAMES[i][0] = VOICENAMES[i][1]; } } }; function initDrumI18N() { for (var i = 0; i < DRUMNAMES.length; i++) { if (DRUMNAMES[i][0] == null || DRUMNAMES[i][0] === '') { DRUMNAMES[i][0] = _(DRUMNAMES[i][1]); } if (DRUMNAMES[i][0] == null) { DRUMNAMES[i][0] = DRUMNAMES[i][1]; } } }; function getDrumName(name) { if (name === '') { console.log('getDrumName passed blank name. Returning ' + DEFAULTDRUM); name = DEFAULTDRUM; } else if (name.slice(0, 4) == 'http') { return null; } for (var drum = 0; drum < DRUMNAMES.length; drum++) { if (DRUMNAMES[drum][0].toLowerCase() === name.toLowerCase() || DRUMNAMES[drum][1].toLowerCase() === name.toLowerCase()) { if (DRUMNAMES[drum][0] != '') { return DRUMNAMES[drum][0]; } else { console.log('I18n is misbehaving when parsing drum name: ' + name); return DRUMNAMES[drum][1]; } } } return null; }; function getDrumIcon(name) { if (name === '') { console.log('getDrumIcon passed blank name. Returning ' + DEFAULTDRUM); name = DEFAULTDRUM; } else if (name.slice(0, 4) == 'http') { return 'images/drum.svg'; } for (var i = 0; i < DRUMNAMES.length; i++) { // if (DRUMNAMES[i].indexOf(name) !== -1) { if (DRUMNAMES[i][0] === name || DRUMNAMES[i][1].toLowerCase() === name.toLowerCase()) { return DRUMNAMES[i][2]; } } return 'images/drum.svg'; }; function getDrumSynthName(name) { if (name == null || name == undefined) { console.log('getDrumSynthName passed null name. Returning null'); return null; } else if (name === '') { console.log('getDrumSynthName passed blank name. Returning ' + DEFAULTDRUM); name = DEFAULTDRUM; } else if (name.slice(0, 4) == 'http') { return name; } for (var i = 0; i < DRUMNAMES.length; i++) { // if (DRUMNAMES[i].indexOf(name) !== -1) { if (DRUMNAMES[i][0] === name || DRUMNAMES[i][1].toLowerCase() === name.toLowerCase()) { return DRUMNAMES[i][1]; } } return null; }; function getVoiceName(name) { if (name === '') { console.log('getVoiceName passed blank name. Returning ' + DEFAULTVOICE); name = DEFAULTVOICE; } else if (name.slice(0, 4) == 'http') { return null; } for (var i = 0; i < VOICENAMES.length; i++) { if (VOICENAMES[i][0] === name || VOICENAMES[i][1] === name) { if (VOICENAMES[i][0] != '') { return VOICENAMES[i][0]; } else { console.log('I18n is misbehaving when parsing voice name: ' + name); return VOICENAMES[i][1]; } } } return null; }; function getVoiceIcon(name) { if (name === '') { console.log('getVoiceIcon passed blank name. Returning ' + DEFAULTVOICE); name = DEFAULTVOICE; } else if (name.slice(0, 4) == 'http') { return 'images/voices.svg'; } for (var i = 0; i < VOICENAMES.length; i++) { if (VOICENAMES[i][0] === name || VOICENAMES[i][1] === name) { return VOICENAMES[i][2]; } } return 'images/voices.svg'; }; function getVoiceSynthName(name) { if (name == null || name == undefined) { console.log('getVoiceSynthName passed null name. Returning null'); return null; } else if (name === '') { console.log('getVoiceSynthName passed blank name. Returning ' + DEFAULTVOICE); name = DEFAULTVOICE; } else if (name.slice(0, 4) == 'http') { return name; } for (var i = 0; i < VOICENAMES.length; i++) { if (VOICENAMES[i][0] === name || VOICENAMES[i][1] === name) { return VOICENAMES[i][1]; } } return null; }; function keySignatureToMode(keySignature) { // Convert from "A Minor" to "A" and "MINOR" if (keySignature === '') { console.log('No key signature provided; reverting to C major.'); return ['C', 'major']; } // Maqams have special names for certain keys. if (keySignature.toLowerCase() in MAQAMTABLE) { keySignature = MAQAMTABLE[keySignature.toLowerCase()]; } var parts = keySignature.split(' '); // A special case to test: m used for minor. var minorMode = false; if (parts.length === 1 && parts[0][parts[0].length - 1] === 'm') { minorMode = true; parts[0] = parts[0].slice(0, parts[0].length - 1); } if (parts[0] in BTOFLAT) { var key = BTOFLAT[parts[0]]; } else if (parts[0] in STOSHARP) { var key = STOSHARP[parts[0]]; } else { var key = parts[0]; } if (NOTESSHARP.indexOf(key) === -1 && NOTESFLAT.indexOf(key) === -1) { console.log('Invalid key or missing name; reverting to C.'); // Is is possible that the key was left out? var keySignature = 'C ' + keySignature; var parts = keySignature.split(' '); key = 'C'; } if (minorMode) { return [key, 'minor']; } // Reassemble remaining parts to get mode name var mode = ''; for (var i = 1; i < parts.length; i++) { if (parts[i] !== '') { if (mode === '') { mode = parts[i]; } else { mode += ' ' + parts[i]; } } } if (mode === '') { mode = 'major'; } else { mode = mode.toLowerCase(); } mode = getModeName(mode); for (var i = 0; i < MODENAMES.length; i++) { if (MODENAMES[i][0] === mode) { mode = MODENAMES[i][1]; break; } } if (mode in MUSICALMODES) { return [key, mode]; } else { console.log('Invalid mode name: ' + mode + ' reverting to major.'); return [key, 'major']; } }; function getStepSizeUp(keySignature, pitch) { return _getStepSize(keySignature, pitch, 'up'); }; function getStepSizeDown(keySignature, pitch) { return _getStepSize(keySignature, pitch, 'down'); }; function _getStepSize(keySignature, pitch, direction) { // Returns how many half-steps to the next note in this key. var obj = _buildScale(keySignature); var scale = obj[0]; var halfSteps = obj[1]; if (pitch in BTOFLAT) { pitch = BTOFLAT[pitch]; } else if (pitch in STOSHARP) { pitch = STOSHARP[pitch]; } ii = scale.indexOf(pitch); if (ii !== -1) { if (direction === 'up') { return halfSteps[ii]; } else { if (ii > 0) { return -halfSteps[ii - 1]; } else { return -last(halfSteps); } } } if (pitch in EQUIVALENTNOTES) { pitch = EQUIVALENTNOTES[pitch]; } ii = scale.indexOf(pitch); if (ii !== -1) { if (direction === 'up') { return halfSteps[ii]; } else { if (ii > 0) { return -halfSteps[ii - 1]; } else { return -last(halfSteps); } } } // current Note not in the consonant scale if this key. console.log(pitch + ' not found in key of ' + keySignature); return 1; }; function _buildScale(keySignature) { var obj = keySignatureToMode(keySignature); var myKeySignature = obj[0]; if (obj[1] === 'CUSTOM') { var halfSteps = customMode; } else { var halfSteps = MUSICALMODES[obj[1]]; } if (NOTESFLAT.indexOf(myKeySignature) !== -1) { var thisScale = NOTESFLAT; } else { var thisScale = NOTESSHARP; } var idx = thisScale.indexOf(myKeySignature); if (idx === -1) { idx = 0; } var scale = [myKeySignature]; var ii = idx; for (var i = 0; i < halfSteps.length; i++) { ii += halfSteps[i]; scale.push(thisScale[ii % SEMITONES]); } return [scale, halfSteps]; } function scaleDegreeToPitch(keySignature, scaleDegree) { // Returns note corresponding to scale degree in current key // signature. Used for moveable solfege. var obj = _buildScale(keySignature); var scale = obj[0]; // Scale degree is specified as do == 1, re == 2, etc., so we need // to subtract 1 to make it zero-based. scaleDegree -= 1; // We mod to ensure we don't run out of notes. // FixMe: bump octave if we wrap. scaleDegree %= (scale.length - 1); return (scale[scaleDegree]); }; function getScaleAndHalfSteps(keySignature) { // Determine scale and half-step pattern from key signature var obj = keySignatureToMode(keySignature); var myKeySignature = obj[0]; if (obj[1] === 'CUSTOM') { var halfSteps = customMode; } else { var halfSteps = MUSICALMODES[obj[1]]; } var solfege = []; for (var i = 0; i < halfSteps.length; i++) { solfege.push(SOLFEGENAMES[i]); for (var j = 1; j < halfSteps[i]; j++) { solfege.push(''); } } if (NOTESFLAT.indexOf(myKeySignature) !== -1) { var thisScale = NOTESFLAT; } else { var thisScale = NOTESSHARP; } if (myKeySignature in EXTRATRANSPOSITIONS) { myKeySignature = EXTRATRANSPOSITIONS[myKeySignature][0]; } return [thisScale, solfege, myKeySignature, obj[1]]; }; // Relative interval (used by the Interval Block) is based on the // steps within the current key and mode. function getInterval (interval, keySignature, pitch) { // Step size interval based on the position (pitch) in the scale var obj = _buildScale(keySignature); var scale = obj[0]; var halfSteps = obj[1]; if (pitch in BTOFLAT) { pitch = BTOFLAT[pitch]; ii = scale.indexOf(pitch); } else if (pitch in STOSHARP) { pitch = STOSHARP[pitch]; ii = scale.indexOf(pitch); } else if (scale.indexOf(pitch) !== -1) { ii = scale.indexOf(pitch); } else if (pitch in EQUIVALENTNOTES) { pitch = EQUIVALENTNOTES[pitch]; if (scale.indexOf(pitch) !== -1) { ii = scale.indexOf(pitch); } else { console.log('Note ' + pitch + ' not in scale ' + keySignature); ii = 0; } } else { // In case pitch is solfege, convert it. var ii = SOLFEGENAMES.indexOf(pitch); } if (interval > 0) { var myOctave = Math.floor(interval / SEMITONES); var myInterval = Math.floor(interval) % SEMITONES; var j = 0; for (var i = 0; i < (myInterval - 1); i++) { j += halfSteps[(ii + i) % halfSteps.length]; } return j + myOctave * SEMITONES; } else { var myOctave = Math.ceil(interval / SEMITONES); var myInterval = Math.ceil(interval) % SEMITONES; var j = 0; for (var i = 0; i > myInterval + 1; i--) { var z = (ii + i - 1) % halfSteps.length; while (z < 0) { z += halfSteps.length; } j -= halfSteps[z]; } return j + myOctave * SEMITONES; } }; function calcNoteValueToDisplay(a, b) { var noteValue = a / b; var noteValueToDisplay = null; if (NOTESYMBOLS != undefined && noteValue in NOTESYMBOLS) { noteValueToDisplay = '1

' + noteValue.toString() + '
' + ''; } else { noteValueToDisplay = reducedFraction(b, a); } if (parseInt(noteValue) < noteValue) { noteValueToDisplay = parseInt((noteValue * 1.5)) if (NOTESYMBOLS != undefined && noteValueToDisplay in NOTESYMBOLS) { noteValueToDisplay = '1.5

' + noteValueToDisplay.toString() + '
' + ' .'; } else { noteValueToDisplay = parseInt((noteValue * 1.75)) if (NOTESYMBOLS != undefined && noteValueToDisplay in NOTESYMBOLS) { noteValueToDisplay = '1.75

' + noteValueToDisplay.toString() + '
' + ' ..'; } else { noteValueToDisplay = reducedFraction(b, a); } } } return noteValueToDisplay; }; function durationToNoteValue(duration) { // returns [note value, no. of dots, tuplet factor] // Try to find a match or a dotted match. for (var dotCount = 0; dotCount < 3; dotCount++) { var currentDotFactor = 2 - (1 / Math.pow(2, dotCount)); var d = duration * currentDotFactor; if (POWER2.indexOf(d) !== -1) { return [d, dotCount, null]; } } // First, round down. var roundDown = duration; for (var i = 1; i < POWER2.length; i++) { // Rounding down if (roundDown < POWER2[i]) { roundDown = POWER2[i - 1]; break; } } if (POWER2.indexOf(roundDown) === -1) { roundDown = 128; } // Next, see if the note has a factor of 2. var factorOfTwo = 1; var tupletValue = duration; while (Math.floor(tupletValue / 2) * 2 === tupletValue) { factorOfTwo *= 2; tupletValue /= 2; } if (factorOfTwo > 1) { // We have a tuplet of sorts return [duration, 0, tupletValue, roundDown]; } // Next, generate a fauve tuplet for a singleton. return [1, 0, duration, roundDown]; }; function toFraction(d) { // Convert float to its approximate fractional representation. if (d > 1) { var flip = true; d = 1 / d; } else { var flip = false; } var df = 1.0; var top = 1; var bot = 1; while (Math.abs(df - d) > 0.00000001) { if (df < d) { top += 1 } else { bot += 1 top = parseInt(d * bot); } df = top / bot; } if (flip) { var tmp = top; top = bot; bot = tmp; } return([top, bot]); }; function frequencyToPitch(hz) { // Calculate the pitch and octave based on frequency, rounding to // the nearest cent. if (hz < A0) { return ['A', 0]; } else if (hz > C8) { // FIXME: set upper bound of C10 return ['C', 8]; } // Calculate cents to keep track of drift var cents = 0; for (var i = 0; i < 8800; i++) { var f = A0 * Math.pow(TWELVEHUNDRETHROOT2, i); if (hz < f * 1.0003 && hz > f * 0.9997) { var cents = i % 100; var j = Math.floor(i / 100); return [PITCHES[(j + PITCHES.indexOf('A')) % 12], Math.floor((j + PITCHES.indexOf('A')) / 12), cents]; } } console.log('Could not find note/octave/cents for ' + hz); return ['?', -1, 0]; }; function numberToPitch(i) { // Calculate the pitch and octave based on index. // We start at A0. if (i < 0) { var n = 0; while (i < 0) { i += 12; n += 1; // Count octave bump ups. } return [PITCHES[(i + PITCHES.indexOf('A')) % 12], Math.floor((i + PITCHES.indexOf('A')) / 12) - n]; } else { return [PITCHES[(i + PITCHES.indexOf('A')) % 12], Math.floor((i + PITCHES.indexOf('A')) / 12)]; } }; function noteToPitchOctave(note) { var len = note.length; var octave = last(note); var pitch = note.substring(0, len - 1); return [pitch, Number(octave)]; }; function noteToFrequency(note, keySignature) { var obj = noteToPitchOctave(note); return pitchToFrequency(obj[0], obj[1], 0, keySignature); }; function pitchToFrequency(pitch, octave, cents, keySignature) { // Calculate the frequency based on pitch and octave. var pitchNumber = pitchToNumber(pitch, octave, keySignature); if (cents === 0) { return A0 * Math.pow(TWELTHROOT2, pitchNumber); } else { return A0 * Math.pow(TWELVEHUNDRETHROOT2, pitchNumber * 100 + cents); } }; function pitchToNumber(pitch, octave, keySignature) { // Calculate the pitch index based on pitch and octave. if (pitch.toUpperCase() === 'R') { return 0; } // Check for flat, sharp, double flat, or double sharp. var transposition = 0; var len = pitch.length; if (len > 1) { if (len > 2) { var lastTwo = pitch.slice(len - 2); if (lastTwo === 'bb' || lastTwo === '♭♭') { pitch = pitch.slice(0, len - 2); transposition -= 2; } else if (lastTwo === '##' || lastTwo === '♯♯') { pitch = pitch.slice(0, len - 2); transposition += 2; } else if (lastTwo === '#b' || lastTwo === '♯♭' || lastTwo === 'b#' || lastTwo === '♭♯') { // Not sure this could occur... but just in case. pitch = pitch.slice(0, len - 2); } } if (pitch.length > 1) { var lastOne = pitch.slice(len - 1); if (lastOne === 'b' || lastOne === '♭') { pitch = pitch.slice(0, len - 1); transposition -= 1; } else if (lastOne === '#' || lastOne === '♯') { pitch = pitch.slice(0, len - 1); transposition += 1; } } } var pitchNumber = 0; if (PITCHES.indexOf(pitch) !== -1) { pitchNumber = PITCHES.indexOf(pitch.toUpperCase()); } else { // obj[1] is the solfege mapping for the current key/mode var obj = getScaleAndHalfSteps(keySignature) if (obj[1].indexOf(pitch.toLowerCase()) !== -1) { pitchNumber = obj[1].indexOf(pitch.toLowerCase()); } else { console.log('pitch ' + pitch + ' not found.'); pitchNumber = 0; } } // We start at A0. return octave * 12 + pitchNumber - PITCHES.indexOf('A') + transposition; }; function noteIsSolfege(note) { if (SOLFEGECONVERSIONTABLE[note] == undefined) { return true; } else { return false; } }; function getSolfege(note) { // FIXME: Use mode-specific conversion. if (noteIsSolfege(note)) { return note; } else { return SOLFEGECONVERSIONTABLE[note]; } }; function i18nSolfege(note) { // solfnotes_ is used in the interface for i18n //.TRANS: the note names must be separated by single spaces var solfnotes_ = _('ti la sol fa mi re do').split(' '); var obj = splitSolfege(note); var i = SOLFNOTES.indexOf(obj[0]); if (i !== -1) { return solfnotes_[i] + obj[1]; } else { console.log(note + ' not found.'); return note; } }; function splitSolfege(value) { // Separate the pitch from any attributes, e.g., # or b if (value != null) { if (SOLFNOTES.indexOf(value) !== -1) { var note = value; var attr = ''; } else if (value.slice(0, 3) === 'sol') { var note = 'sol'; if (value.length === 4) { var attr = value[3]; } else { var attr = value[3] + value[3]; } } else { var note = value.slice(0, 2); if (value.length === 3) { var attr = value[2]; } else { var attr = value[2] + value[2]; } } } else { var note = 'sol'; var attr = '' } return [note, attr]; }; function getNumber(notename, octave) { // Converts a note, e.g., C, and octave to a number if (octave < 0) { var num = 0; } else if (octave > 10) { var num = 9 * 12; } else { var num = 12 * (octave - 1); } notename = String(notename); if (notename.substring(0, 1) in NOTESTEP) { num += NOTESTEP[notename.substring(0, 1)]; if (notename.length >= 1) { var delta = notename.substring(1); if (delta === 'bb' || delta === '♭♭') { num -= 2; } else if (delta === '##' || delta === '♯♯') { num += 2; } else if (delta === 'b' || delta === '♭') { num -= 1; } else if (delta === '#' || delta === '♯') { num += 1; } } } return num; }; function getNumNote(value, delta) { // Converts from number to note var num = value + delta; /* if (num < 0) { num = 1; var octave = 1; } else if (num > 10 * 12) { num = 12; var octave = 10; } else { var octave = Math.floor(num / 12); num = num % 12; } */ var octave = Math.floor(num / 12); num = num % 12; var note = NOTESTABLE[num]; if (note[num] === "ti") { octave -= 1; } return [note, octave + 1]; }; calcOctave = function (current, arg) { switch(arg) { case _('next'): case 'next': return Math.min(current + 1, 10); case _('previous'): case 'previous': return Math.max(current - 1, 1); case _('current'): case 'current': return current; default: return Math.floor(arg); } }; calcOctaveInterval = function (arg) { // Used by intervals to determine octave to use in an interval. var value = 0; switch(arg) { case 1: case _('next'): case 'next': value = 1; break; case -1: case _('previous'): case 'previous': value = -1; break; case _('current'): case 'current': case 0: value = 0; break; case 2: value = 2; break; case -2: value = -2; break; default: console.log('Interval octave must be between -2 and 2.'); value = 0; break; } return value; }; function isInt(value) { return !isNaN(value) && parseInt(Number(value)) == value && !isNaN(parseInt(value, 10)); }; function reducedFraction(a, b) { greatestCommonMultiple = function (a, b) { return b === 0 ? a : greatestCommonMultiple(b, a % b); } var gcm = greatestCommonMultiple(a, b); if (NOTESYMBOLS != undefined && [1, 2, 4, 8, 16, 32, 64].indexOf(b/gcm) !== -1) { return (a / gcm) + '

' + (b / gcm) + '
'; } else { return (a / gcm) + '

' + (b / gcm) + '

'; } }; function Synth () { // Isolate synth functions here. if (_THIS_IS_MUSIC_BLOCKS_) { // Using Tone.js this.tone = new Tone(); } this.synthset = { // builtin synths 'poly': [null, null], 'sine': [null, null], 'triangle': [null, null], 'sawtooth': [null, null], 'square': [null, null], 'pluck': [null, null], // voiced samples 'violin': [VIOLINSOUNDSAMPLE, null], 'cello': [CELLOSOUNDSAMPLE, null], 'basse': [BASSESOUNDSAMPLE, null], // drum samples 'bottle': [BOTTLESOUNDSAMPLE, null], 'clap': [CLAPSOUNDSAMPLE, null], 'darbuka drum': [DARBUKASOUNDSAMPLE, null], 'hi hat': [HIHATSOUNDSAMPLE, null], 'splash': [SPLASHSOUNDSAMPLE, null], 'bubbles': [BUBBLESSOUNDSAMPLE, null], 'cow bell': [COWBELLSOUNDSAMPLE, null], 'dog': [DOGSOUNDSAMPLE, null], 'kick drum': [KICKSOUNDSAMPLE, null], 'tom tom': [TOMSOUNDSAMPLE, null], 'cat': [CATSOUNDSAMPLE, null], 'crash': [CRASHSOUNDSAMPLE, null], 'duck': [DUCKSOUNDSAMPLE, null], 'ride bell': [RIDEBELLSOUNDSAMPLE, null], 'triangle bell': [TRIANGLESOUNDSAMPLE, null], 'chine': [CHINESOUNDSAMPLE, null], 'cricket': [CRICKETSOUNDSAMPLE, null], 'finger cymbals': [FINGERCYMBALSSOUNDSAMPLE, null], 'slap': [SLAPSOUNDSAMPLE, null], 'clang': [CLANGSOUNDSAMPLE, null], 'cup drum': [CUPSOUNDSAMPLE, null], 'floor tom tom': [FLOORTOMSOUNDSAMPLE, null], 'snare drum': [SNARESOUNDSAMPLE, null], }; if (_THIS_IS_MUSIC_BLOCKS_) { Tone.Buffer.onload = function (){ console.log('drum loaded'); }; } this.getSynthByName = function (name) { if (name == null || name == undefined) { return this.synthset['poly'][1]; } switch (name) { case 'pluck': case 'triangle': case 'square': case 'sawtooth': case 'sine': return this.synthset[name][1]; break; case 'violin': case 'cello': case 'basse': return this.synthset[name][1]; break; case 'default': case 'poly': return this.synthset['poly'][1]; break; default: var drumName = getDrumSynthName(name); if (name.slice(0, 4) == 'http') { if (name in this.synthset) { return this.synthset[name][1]; } else { console.log('no synth by that name'); return null; } } else if (drumName != null) { return this.synthset[drumName][1]; } else if (name === 'drum') { return this.synthset[DEFAULTDRUM][1]; } break; } // Use polysynth if all else fails. return this.synthset['poly'][1]; }; this.loadSynth = function (name) { var thisSynth = this.getSynthByName(name); if (thisSynth == null) { console.log('loading synth for ' + name); switch (name) { case 'pluck': this.synthset['pluck'][1] = new Tone.PluckSynth(); break; case 'triangle': case 'square': case 'sawtooth': case 'sine': var synthOptions = { oscillator: { type: name }, envelope: { attack: 0.03, decay: 0, sustain: 1, release: 0.03 }, }; this.synthset[name][1] = new Tone.Synth(synthOptions); break; case 'poly': case 'default': this.synthset['poly'][1] = new Tone.PolySynth(6, Tone.AMSynth); break; case 'violin': case 'cello': case 'basse': this.synthset[name][1] = new Tone.Sampler(this.synthset[name][0]); break; default: if (name.slice(0, 4) == 'http') { this.synthset[name] = [name, new Tone.Sampler(name)]; } else if (name.slice(0, 4) == 'file') { this.synthset[name] = [name, new Tone.Sampler(name)]; } else { this.synthset[name][1] = new Tone.Sampler(this.synthset[name][0]); } break; } } this.getSynthByName(name).toMaster(); }; this.performNotes = function (synth, notes, beatValue, doVibrato, vibratoIntensity, vibratoFrequency) { if (doVibrato) { var vibrato = new Tone.Vibrato(1 / vibratoFrequency, vibratoIntensity); synth.chain(vibrato, Tone.Master); synth.triggerAttackRelease(notes, beatValue); setTimeout(function () { vibrato.dispose(); }, beatValue * 1000); //disable vibrato effect when beat is over } else { synth.triggerAttackRelease(notes, beatValue); } } this.trigger = function (notes, beatValue, name, vibratoArgs) { var doVibrato = false; var vibratoIntensity = 0; var vibratoFrequency = 0; if (vibratoArgs.length == 2 && vibratoArgs[0] != 0) { doVibrato = true; vibratoIntensity = vibratoArgs[0]; vibratoFrequency = vibratoArgs[1]; } switch (name) { case 'pluck': case 'triangle': case 'square': case 'sawtooth': case 'sine': if (typeof(notes) === 'object') { var noteToPlay = notes[0]; } else { var noteToPlay = notes; } this.performNotes(this.synthset[name][1], noteToPlay, beatValue, doVibrato, vibratoIntensity, vibratoFrequency); break; case 'violin': case 'cello': case 'basse': // The violin sample is tuned to C6 // The cello sample is tuned to C4??? // The basse sample is tuned to C2??? var centerNo = SAMPLECENTERNO[name]; var obj = noteToPitchOctave(notes); var noteNo = pitchToNumber(obj[0], obj[1], 'C Major'); this.performNotes(this.synthset[name][1], noteNo - centerNo, beatValue, doVibrato, vibratoIntensity, vibratoFrequency); break; case 'default': case 'poly': this.performNotes(this.synthset['poly'][1], notes, beatValue, doVibrato, vibratoIntensity, vibratoFrequency); break; default: var drumName = getDrumSynthName(name); if (drumName != null) { // Work around i8n bug in Firefox. if (drumName === '' && name in this.synthset) { this.synthset[name][1].triggerAttack(0, beatValue); } else if (drumName in this.synthset) { if (this.synthset[drumName][1] == null) { console.log('Something has gone terribly wrong: ' + name + ', ' + drumName); } else { this.synthset[drumName][1].triggerAttack(0); } } else if (name.slice(0, 4) == 'http') { this.synthset[name][1].triggerAttack(0, beatValue); } else if (name.slice(0, 4) == 'file') { this.synthset[name][1].triggerAttack(0, beatValue); } else { console.log('Something has gone terribly wrong: ' + name + ', ' + drumName); } } else if (name === 'drum') { this.synthset[DEFAULTDRUM][1].triggerAttack(0, beatValue, 1); } else if (name.slice(0, 4) == 'http') { this.synthset[name][1].triggerAttack(0, beatValue, 1); } else if (name.slice(0, 4) == 'file') { this.synthset[name][1].triggerAttack(0, beatValue, 1); } else { this.performNotes(this.synthset['poly'][1], notes, beatValue, doVibrato, vibratoIntensity, vibratoFrequency); } break; } }; this.stopSound = function (name) { this.getSynthByName(name).triggerRelease(); }; this.start = function () { Tone.Transport.start(); }; this.stop = function () { Tone.Transport.stop(); }; this.setVolume = function (vol) { var db = this.tone.gainToDb(vol / 100); Tone.Master.volume.rampTo(db, 0.01); }; this.getOscillator = function (oscillatorName, frequency) { return new Tone.Oscillator(oscillatorName, frequency).toMaster(); }; };