console.clear(); document.documentElement.addEventListener('mousedown', () => { if (Tone.context.state !== 'running') Tone.context.resume(); }); /** * MusicalScale * generate a scale of music * https://codepen.io/jakealbaugh/pen/NrdEYL/ * * @param key {String} the root of the key. flats will be converted to sharps. C, C#, D, D#, E, F, F#, G, G#, A, A#, B * @param mode {String} desired mode. ionian, dorian, phrygian, lydian, mixolydian, aeolian, locrian, can also pass in: major, minor, melodic, harmonic * * @return {Object} _scale: scale info key: the scale key mode: the scale mode id notes: an array of notes step: index of note in key note: the actual note rel_octave: 0 || 1, in root octave or next triad: major, minor, diminished, or augmented triad for this note interval: I, ii, etc type: min, maj, dim, aug notes: array of notes in the triad note: the note rel_octave: 0 || 1 || 2, relative to key root octave */ class MusicalScale { constructor(params) { this.dict = this._loadDictionary(); let errors = this._errors(params); if(errors) return; this.updateScale = this.pubUpdateScale; this._loadScale(params); }; pubUpdateScale(params) { let errors = this._errors(params); if(errors) return; this._loadScale(params); }; _loadScale(params) { // clean up the key param this.key = this._paramKey(params.key); // set the mode this.mode = params.mode; this.notes = []; this._scale = this.dict.scales[this._paramMode(this.mode)]; // notes to cycle through let keys = this.dict.keys; // starting index for key loop let offset = keys.indexOf(this.key); for(let s = 0; s < this._scale.steps.length; s++) { let step = this._scale.steps[s]; let idx = (offset + step) % keys.length; // relative octave. 0 = same as root, 1 = next ocave up let rel_octave = (offset + step) > keys.length - 1 ? 1 : 0; // generate the relative triads let triad = this._genTriad(s, idx, rel_octave, this._scale.triads[s]); // define the note let note = { step: s, note: keys[idx], rel_octave: rel_octave, triad: triad }; // add the note this.notes.push(note); } }; // create a chord of notes based on chord type _genTriad(s, offset, octave, t) { // get the steps for this chord type let steps = this.dict.triads[t]; // instantiate the chord let chord = { type: t, interval: this._intervalFromType(s, t), notes: [] }; // load the notes let keys = this.dict.keys; for(let i = 0; i < steps.length; i++) { let step = steps[i]; let idx = (offset + step) % keys.length; // relative octave to base let rel_octave = (offset + step) > keys.length - 1 ? octave + 1 : octave; // define the note chord.notes.push({ note: keys[idx], rel_octave: rel_octave }); } return chord; }; // proper interval notation from the step and type _intervalFromType(step, type) { let steps = 'i ii iii iv v vi vii'.split(' '); let s = steps[step]; switch(type) { case 'maj': s = s.toUpperCase(); break; case 'min': s = s; break; case 'aug': s = s.toUpperCase() + '+'; break; case 'dim': s = s + '°'; break; } return s; }; _errors(params) { if(this.dict.keys.indexOf(params.key) === -1) { if(Object.keys(this.dict.flat_sharp).indexOf(params.key) === -1) { return console.error(`${params.key} is an invalid key. ${this.dict.keys.join(', ')}`); } } else if(this.dict.modes.indexOf(params.mode) === -1) { return console.error(`${params.mode} is an invalid mode. ${this.dict.modes.join(', ')}`); } else { return false; } }; _loadDictionary() { return { keys: 'C C# D D# E F F# G G# A A# B'.split(' '), scales: { ion: { name: 'Ionian', steps: this._genSteps('W W H W W W H'), dominance: [3,0,1,0,2,0,1], triads: this._genTriads(0) }, dor: { name: 'Dorian', steps: this._genSteps('W H W W W H W'), dominance: [3,0,1,0,2,2,1], triads: this._genTriads(1) }, phr: { name: 'Phrygian', steps: this._genSteps('H W W W H W W'), dominance: [3,2,1,0,2,0,1], triads: this._genTriads(2) }, lyd: { name: 'Lydian', steps: this._genSteps('W W W H W W H'), dominance: [3,0,1,2,2,0,1], triads: this._genTriads(3) }, mix: { name: 'Mixolydian', steps: this._genSteps('W W H W W H W'), dominance: [3,0,1,0,2,0,2], triads: this._genTriads(4) }, aeo: { name: 'Aeolian', steps: this._genSteps('W H W W H W W'), dominance: [3,0,1,0,2,0,1], triads: this._genTriads(5) }, loc: { name: 'Locrian', steps: this._genSteps('H W W H W W W'), dominance: [3,0,1,0,3,0,0], triads: this._genTriads(6) }, mel: { name: 'Melodic Minor', steps: this._genSteps('W H W W W W H'), dominance: [3,0,1,0,3,0,0], triads: 'min min aug maj maj dim dim'.split(' ') }, har: { name: 'Harmonic Minor', steps: this._genSteps('W H W W H WH H'), dominance: [3,0,1,0,3,0,0], triads: 'min dim aug min maj maj dim'.split(' ') } }, modes: [ 'ionian', 'dorian', 'phrygian', 'lydian', 'mixolydian', 'aeolian', 'locrian', 'major', 'minor', 'melodic', 'harmonic' ], flat_sharp: { Cb: 'B', Db: 'C#', Eb: 'D#', Fb: 'E', Gb: 'F#', Ab: 'G#', Bb: 'A#' }, triads: { maj: [0,4,7], min: [0,3,7], dim: [0,3,6], aug: [0,4,8] } }; }; _paramMode(mode) { return { minor: 'aeo', major: 'ion', ionian: 'ion', dorian: 'dor', phrygian: 'phr', lydian: 'lyd', mixolydian: 'mix', aeolian: 'aeo', locrian: 'loc', melodic: 'mel', harmonic: 'har' }[mode]; }; _paramKey(key) { if(this.dict.flat_sharp[key]) return this.dict.flat_sharp[key]; return key; }; _genTriads(offset) { // this is ionian, each mode bumps up one offset. let base = 'maj min min maj maj min dim'.split(' '); let triads = []; for(let i = 0; i < base.length; i++) { triads.push(base[(i + offset) % base.length]); } return triads; }; _genSteps(steps_str) { let arr = steps_str.split(' '); let steps = [0]; let step = 0; for(let i = 0; i < arr.length - 1; i++) { let inc = 0; switch(arr[i]) { case 'W': inc = 2; break; case 'H': inc = 1; break; case 'WH': inc = 3; break; } step += inc; steps.push(step); } return steps; }; }; /** ArpeggioPatterns https://codepen.io/jakealbaugh/pen/PzpzEO/ returns arrays of arpeggio patterns for a given length of notes @param steps {Integer} number of steps @return {Object} patterns: {Array} of arpeggiated index patterns */ class ArpeggioPatterns { constructor(params) { this.steps = params.steps; this._loadPatterns(); this.updatePatterns = this.pubUpdatePatterns; }; pubUpdatePatterns(params) { this.steps = params.steps; this._loadPatterns(); }; _loadPatterns() { this.arr = []; this.patterns = []; for(let i = 0; i < this.steps; i++) { this.arr.push(i); } this._used = []; this.permutations = this._permute(this.arr); this.looped = this._loop(); this.patterns = { straight: this.permutations, looped: this.looped }; }; _permute(input, permutations) { permutations = permutations || []; var i, ch; for (i = 0; i < input.length; i++) { ch = input.splice(i, 1)[0]; this._used.push(ch); if (input.length === 0) { permutations.push(this._used.slice()); } this._permute(input, permutations); input.splice(i, 0, ch); this._used.pop(); } return permutations; }; _loop() { let looped = []; for(let p = 0; p < this.permutations.length; p++) { let perm = this.permutations[p]; let arr = Array.from(perm); for(let x = 1; x < perm.length - 1; x++) { arr.push(perm[perm.length - 1 - x]); } looped.push(arr); } return looped; }; }; /** ArpPlayer the main app */ class ArpPlayer { constructor(params) { this.container = document.querySelector('#main'); this.aside = document.querySelector('#aside'); this.chords = [0,2,6,3,4,2,5,1]; this.ms_key = 'G'; this.ms_mode = 'locrian'; this.ap_steps = 6; this.ap_pattern_type = 'straight'; // || 'looped' this.ap_pattern_id = 0; this.player = { chord_step: 0, octave_base: 4, arp_repeat: 2, bass_on: false, triad_step: 0, step: 0, playing: false, bpm: 135 }; this.chord_count = this.chords.length; this._setMusicalScale(); this._setArpeggioPatterns(); this._drawKeyboard(); this._drawOutput(); this._loadChordSelector(); this._loadBPMSelector(); this._loadKeySelector(); this._loadModeSelector(); this._loadStepsSelector(); this._loadTypeSelector(); this._loadPatternSelector(); this._loadSynths(); this._loadTransport(); // change tabs, pause player document.addEventListener('visibilitychange', () => { this.player.playing = true; this.playerToggle(); }); console.log(this.MS); }; _loadSynths() { this.channel = { master: new Tone.Gain(0.7), treb: new Tone.Gain(0.7), bass: new Tone.Gain(0.8), }; this.fx = { distortion: new Tone.Distortion(0.8), reverb: new Tone.Freeverb(0.1, 3000), delay: new Tone.PingPongDelay('16n', 0.1), }; this.synths = { treb: new Tone.PolySynth(1, Tone.SimpleAM), bass: new Tone.DuoSynth() }; this.synths.bass.vibratoAmount.value = 0.1; this.synths.bass.harmonicity.value = 1.5; this.synths.bass.voice0.oscillator.type = 'triangle'; this.synths.bass.voice0.envelope.attack = 0.05; this.synths.bass.voice1.oscillator.type = 'triangle'; this.synths.bass.voice1.envelope.attack = 0.05; // fx mixes this.fx.distortion.wet.value = 0.2; this.fx.reverb.wet.value = 0.2; this.fx.delay.wet.value = 0.3; // gain levels this.channel.master.toMaster(); this.channel.treb.connect(this.channel.master); this.channel.bass.connect(this.channel.master); // fx chains this.synths.treb.chain(this.fx.delay, this.fx.reverb, this.channel.treb); this.synths.bass.chain(this.fx.distortion, this.channel.bass); }; _loadTransport() { this.playerUpdateBPM = (e) => { let el = e.target; let bpm = el.getAttribute('data-value'); this.player.bpm = parseInt(bpm); Tone.Transport.bpm.value = this.player.bpm; this._utilClassToggle(e.target, 'bpm-current'); }; this.playerToggle = () => { if(this.player.playing) { Tone.Transport.pause(); this.channel.master.gain.value = 0; this.play_toggle.classList.remove('active'); } else { Tone.Transport.start(); this.channel.master.gain.value = 1; this.play_toggle.classList.add('active'); } this.player.playing = !this.player.playing; }; this.play_toggle = document.createElement('button'); this.play_toggle.innerHTML = `<span class="play">Play</span><span class="pause">Pause</span>`; this.aside.appendChild(this.play_toggle); this.play_toggle.addEventListener('touchstart', (e) => { Tone.startMobile(); }); this.play_toggle.addEventListener('click', (e) => { this.playerToggle(); }); Tone.Transport.bpm.value = this.player.bpm; Tone.Transport.scheduleRepeat((time) => { let curr_chord = this.player.chord_step % this.chord_count; let prev = document.querySelector('.chord > div.active'); if(prev) prev.classList.remove('active'); let curr = document.querySelector(`.chord > div:nth-of-type(${curr_chord + 1})`); if(curr) curr.classList.add('active'); let chord = this.MS.notes[this.chords[curr_chord]]; // finding the current note let notes = chord.triad.notes; for(let i = 0; i < Math.ceil(this.ap_steps / 3); i++) { notes = notes.concat(notes.map((n) => { return { note: n.note, rel_octave: n.rel_octave + (i + 1)}})); } let note = notes[this.arpeggio[this.player.step % this.arpeggio.length]]; // setting bass notes let bass_o = chord.rel_octave + 2; let bass_1 = chord.note + bass_o; // slappin da bass if(!this.player.bass_on) { this.player.bass_on = true; this.synths.bass.triggerAttack(bass_1, time); this._utilActiveNoteClassToggle([bass_1.replace('#', 'is')], 'active-b'); } // bump the step this.player.step++; // changing chords if(this.player.step % (this.arpeggio.length * this.player.arp_repeat) === 0) { this.player.chord_step++; this.player.bass_on = false; this.synths.bass.triggerRelease(time); this.player.triad_step++; } // arpin' let note_ref = `${note.note}${note.rel_octave + this.player.octave_base}`; this._utilActiveNoteClassToggle([note_ref.replace('#', 'is')], 'active-t'); this.synths.treb.triggerAttackRelease(note_ref, '16n', time); }, '16n'); }; _drawKeyboard() { let octaves = [2,3,4,5,6,7]; let keyboard = document.createElement('section'); keyboard.classList.add('keyboard'); this.container.appendChild(keyboard); octaves.forEach((octave) => { this.MS.dict.keys.forEach((key) => { let el = document.createElement('div'); let classname = key.replace('#', 'is') + octave; el.classList.add(classname); keyboard.appendChild(el); }); }); }; _drawOutput() { this.output = document.createElement('section'); this.output.classList.add('output'); this.aside.appendChild(this.output); this._updateOutput(); }; _updateOutput() { this.output.innerHTML = ''; let title = document.createElement('h1'); title.innerHTML = 'Output'; this.output.appendChild(title); let description = document.createElement('h2'); description.innerHTML = `${this.MS.key} ${this.MS._scale.name}`; this.output.appendChild(description); this.chords.forEach((chord) => { let note = this.MS.notes[chord]; let el = document.createElement('span'); el.innerHTML = `${note.note.replace('#', '<sup>♯</sup>')} <small>${note.triad.type}</small>`; this.output.appendChild(el); }); }; _loadBPMSelector() { let bpm_container = document.createElement('section'); bpm_container.classList.add('bpm'); this.aside.appendChild(bpm_container); let title = document.createElement('h1'); title.innerHTML = 'Beats Per Minute'; bpm_container.appendChild(title); [45,60,75,90,105,120,135,150].forEach((bpm) => { let el = document.createElement('div'); el.setAttribute('data-value', bpm); if(bpm === this.player.bpm) el.classList.add('bpm-current'); el.innerHTML = bpm; el.addEventListener('click', (e) => { this.playerUpdateBPM(e); }); bpm_container.appendChild(el); }); }; _loadChordSelector() { this.chord_container = document.createElement('section'); this.chord_container.classList.add('chord'); this.container.appendChild(this.chord_container); let title = document.createElement('h1'); title.innerHTML = 'Chord Progression'; this.chord_container.appendChild(title); this.msUpdateChords = (e) => { let el = e.target; let chord = el.getAttribute('data-chord'); let value = el.getAttribute('data-value'); this.chords[parseInt(chord)] = value; this._utilClassToggle(e.target, `chord-${chord}-current`); this._updateOutput(); }; for(let c = 0; c < this.chord_count; c++) { let chord_el = document.createElement('div'); this.MS.notes.forEach((note, i) => { let el = document.createElement('div'); el.setAttribute('data-value', i); el.setAttribute('data-chord', c); if(i === this.chords[c]) el.classList.add(`chord-${c}-current`); el.innerHTML = 'i ii iii iv v vi vii'.split(' ')[i]; el.addEventListener('click', (e) => { this.msUpdateChords(e); }); chord_el.appendChild(el); }); this.chord_container.appendChild(chord_el); } this._updateChords(); }; _updateChords() { this.MS.notes.forEach((note, i) => { let updates = document.querySelectorAll(`.chord div > div:nth-child(${i + 1})`); for(let u = 0; u < updates.length; u++) { updates[u].innerHTML = note.triad.interval; } }); }; _loadKeySelector() { let key_container = document.createElement('section'); key_container.classList.add('keys'); this.container.appendChild(key_container); let title = document.createElement('h1'); title.innerHTML = 'Tonic / Root'; key_container.appendChild(title); this.MS.dict.keys.forEach((key) => { let el = document.createElement('div'); el.setAttribute('data-value', key); if(key === this.ms_key) el.classList.add('key-current'); el.innerHTML = key; el.addEventListener('click', (e) => { this.msUpdateKey(e); }); key_container.appendChild(el); }); }; _loadModeSelector() { let mode_container = document.createElement('section'); mode_container.classList.add('modes'); this.container.appendChild(mode_container); let title = document.createElement('h1'); title.innerHTML = 'Mode'; mode_container.appendChild(title); this.MS.dict.modes.forEach((mode) => { let el = document.createElement('div'); el.setAttribute('data-value', mode); if(mode === this.ms_mode) el.classList.add('mode-current'); el.innerHTML = mode; el.addEventListener('click', (e) => { this.msUpdateMode(e); }); mode_container.appendChild(el); }); }; _loadTypeSelector() { let type_container = document.createElement('section'); type_container.classList.add('type'); this.container.appendChild(type_container); let title = document.createElement('h1'); title.innerHTML = 'Arpeggio Type'; type_container.appendChild(title); ['straight', 'looped'].forEach((step) => { let el = document.createElement('div'); el.setAttribute('data-value', step); if(step === this.ap_pattern_type) el.classList.add('type-current'); el.innerHTML = step; el.addEventListener('click', (e) => { this.apUpdatePatternType(e); }); type_container.appendChild(el); }); }; _loadStepsSelector() { let steps_container = document.createElement('section'); steps_container.classList.add('steps'); this.container.appendChild(steps_container); let title = document.createElement('h1'); title.innerHTML = 'Arpeggio Steps'; steps_container.appendChild(title); [3,4,5,6].forEach((step) => { let el = document.createElement('div'); el.setAttribute('data-value', step); if(step === this.ap_steps) el.classList.add('step-current'); el.innerHTML = step; el.addEventListener('click', (e) => { this.apUpdateSteps(e); }); steps_container.appendChild(el); }); }; _loadPatternSelector() { this.pattern_container = document.createElement('section'); this.pattern_container.classList.add('patterns'); this.container.appendChild(this.pattern_container); this._updatePatternSelector(); }; _updatePatternSelector() { this.pattern_container.innerHTML = ''; // reset if the id is over this.ap_pattern_id = this.ap_pattern_id > this.AP.patterns[this.ap_pattern_type].length - 1 ? 0 : this.ap_pattern_id; this.arpeggio = this.AP.patterns[this.ap_pattern_type][this.ap_pattern_id]; let title = document.createElement('h1'); title.innerHTML = 'Arpeggio Style'; this.pattern_container.appendChild(title); let patterns = this.AP.patterns[this.ap_pattern_type]; [720, 120, 24, 6].forEach((count) => { this.pattern_container.classList.remove(`patterns-${count}`); }); this.pattern_container.classList.add(`patterns-${patterns.length}`); patterns.forEach((pattern, i) => { let el = document.createElement('div'); el.setAttribute('data-value', i); if(i === this.ap_pattern_id) el.classList.add('id-current'); el.innerHTML = pattern.join(''); el.appendChild(this._genPatternSvg(pattern)); el.addEventListener('click', (e) => { this.apUpdatePatternId(e); }); this.pattern_container.appendChild(el); }); }; _genPatternSvg(pattern) { let hi = Array.from(pattern).sort()[pattern.length - 1]; let spacing = 2; let svgns = 'http://www.w3.org/2000/svg'; let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); let width = pattern.length * spacing + (spacing); let height = hi + (spacing * 2); svg.setAttribute('height', height); svg.setAttribute('width', width); svg.setAttribute('viewBox', `0 0 ${width} ${height}`); svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'); let polyline = document.createElementNS(svgns, 'polyline'); let points = []; let x = spacing; for(let i = 0; i < pattern.length; i++) { let y = height - pattern[i] - spacing; points.push(x + ',' + y); x += spacing; } polyline.setAttribute('points', points.join(' ')); svg.appendChild(polyline); return svg; }; _setMusicalScale() { this.MS = new MusicalScale({ key: this.ms_key, mode: this.ms_mode }); this.msUpdateKey = (e) => { this._utilClassToggle(e.target, 'key-current'); this.ms_key = e.target.getAttribute('data-value'); this.msUpdateScale(); }; this.msUpdateMode = (e) => { this._utilClassToggle(e.target, 'mode-current'); this.ms_mode = e.target.getAttribute('data-value'); this.msUpdateScale(); this._updateChords(); }; this.msUpdateScale = () => { this.MS.updateScale({ key: this.ms_key, mode: this.ms_mode }); this._updateOutput(); }; }; _setArpeggioPatterns() { this.AP = new ArpeggioPatterns({ steps: this.ap_steps }); this.apUpdateSteps = (e) => { this._utilClassToggle(e.target, 'step-current'); let steps = e.target.getAttribute('data-value'); this.ap_steps = parseInt(steps); this.AP.updatePatterns({ steps: steps }); this.apUpdate(); this._updatePatternSelector(); }; this.apUpdatePatternType = (e) => { this._utilClassToggle(e.target, 'type-current'); this.ap_pattern_type = e.target.getAttribute('data-value'); this.apUpdate(); this._updatePatternSelector(); }; this.apUpdatePatternId = (e) => { this._utilClassToggle(e.target, 'id-current'); this.ap_pattern_id = parseInt(e.target.getAttribute('data-value')); this.apUpdate(); }; this.apUpdate = () => { this.arpeggio = this.AP.patterns[this.ap_pattern_type][this.ap_pattern_id]; }; this.apUpdate(); }; _utilClassToggle(el, classname) { let curr = document.querySelectorAll('.' + classname); for(let i = 0; i < curr.length; i++) curr[i].classList.remove(classname); el.classList.add(classname); }; /** utilActiveNoteClassToggle removes all classnames on existing, then adds to an array of note classes @param note_classes {Array} [A3, B4] @param classname {String} 'active-treble' */ _utilActiveNoteClassToggle = (note_classes, classname) => { let removals = document.querySelectorAll(`.${classname}`); for(let r = 0; r < removals.length; r++) removals[r].classList.remove(classname); let adds = document.querySelectorAll(note_classes.map((n) => { return `.${n}`; }).join(', ')); for(let a = 0; a < adds.length; a++) adds[a].classList.add(classname); }; } let app = new ArpPlayer();

!