A quick way to sketch out musical chord progressions.
uses my Scale Generator and Arpeggio Pattern Generator and the lovely Tone.js.
I've started collecting my musical composition stuff in this collection.
A Pen by Jake Albaugh on CodePen.
| <article> | |
| <aside id="aside"></aside> | |
| <main id="main"></main> | |
| </article> |
A quick way to sketch out musical chord progressions.
uses my Scale Generator and Arpeggio Pattern Generator and the lovely Tone.js.
I've started collecting my musical composition stuff in this collection.
A Pen by Jake Albaugh on CodePen.
| console.clear(); | |
| /** | |
| * MusicalScale | |
| * generate a scale of music | |
| * http://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 | |
| 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(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(offset, octave, t) { | |
| // get the steps for this chord type | |
| let steps = this.dict.triads[t]; | |
| // instantiate the chord | |
| let chord = { type: 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; | |
| }; | |
| _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 | |
| http://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); | |
| 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(); | |
| 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'); | |
| }, '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() { | |
| let chord_container = document.createElement('section'); | |
| chord_container.classList.add('chord'); | |
| this.container.appendChild(chord_container); | |
| let title = document.createElement('h1'); | |
| title.innerHTML = 'Chord Progression'; | |
| 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); | |
| }); | |
| chord_container.appendChild(chord_el); | |
| } | |
| }; | |
| _loadKeySelector() { | |
| let key_container = document.createElement('section'); | |
| key_container.classList.add('keys'); | |
| this.container.appendChild(key_container); | |
| let title = document.createElement('h1'); | |
| title.innerHTML = '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); | |
| this.AP.patterns[this.ap_pattern_type].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.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(); |
| <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/111863/Tone.min.js"></script> |
| html, body { | |
| height: 100%; | |
| } | |
| body { | |
| background: #ddd; | |
| color: #222; | |
| } | |
| button { | |
| padding: 0.5rem 0; | |
| width: calc(80% - 1rem); | |
| max-width: 200px; | |
| transform: translateX(0.5rem); | |
| border: none; | |
| text-align: center; | |
| background: #fb0; | |
| font-size: 1.2rem; | |
| color: white; | |
| display: block; | |
| margin: 1rem auto; | |
| border-radius: 4px; | |
| animation: pulse 200ms ease-in-out infinite alternate; | |
| border: 4px solid darken(#fb0, 2%); | |
| box-shadow: 0px 0px 0px 4px rgba(0,0,0,0.1); | |
| text-transform: uppercase; | |
| letter-spacing: 0.125em; | |
| .pause { display: none; } | |
| &.active { | |
| animation: none; | |
| background: #222; | |
| border-color: #000; | |
| .pause { display: block; } | |
| .play { display: none; } | |
| } | |
| } | |
| @keyframes pulse { | |
| from { transform: translateX(0.5rem) scale(1); } | |
| to { transform: translateX(0.5rem) scale(1.1); } | |
| } | |
| .keyboard { | |
| position: fixed; | |
| top: 0; | |
| margin: 0; | |
| left: 50%; | |
| z-index: 2; | |
| width: calc(100% - 2rem); | |
| max-width: calc(1400px - 1rem); | |
| border-bottom: 4px solid #ddd; | |
| transform: translateX(-50%); | |
| div { | |
| background: #fff; | |
| height: 40px; | |
| flex: 3; | |
| margin: 1px; | |
| &[class*="is"] { | |
| flex: 2; | |
| background: #222; | |
| } | |
| &.active-b, | |
| &.active-t { | |
| background: #FB0; | |
| &[class*="is"] { | |
| background: darken(#FB0, 20%); | |
| } | |
| } | |
| } | |
| } | |
| article { | |
| max-width: 1400px; | |
| display: flex; | |
| flex-direction: column; | |
| padding-top: calc(40px + 3rem); | |
| padding-bottom: 5rem; | |
| width: calc(100% - 1rem); | |
| margin: 0 auto; | |
| } | |
| main, aside { | |
| width: 100%; | |
| } | |
| aside, | |
| main { | |
| display: flex; | |
| justify-content: space-between; | |
| flex-wrap: wrap; | |
| } | |
| aside { | |
| section { width: 100%; } | |
| } | |
| @media (min-width: 800px) { | |
| article { | |
| flex-direction: row; | |
| } | |
| main { | |
| width: calc(80% - 1rem); | |
| order: 1; | |
| } | |
| aside { | |
| display: block; | |
| order: 2; | |
| width: 20%; | |
| margin-right: 1rem; | |
| .output { | |
| h1, h2 { flex-basis: auto; } | |
| flex-direction: column; | |
| } | |
| } | |
| } | |
| section { | |
| width: 100%; | |
| margin: 0.5rem; | |
| display: flex; | |
| flex-wrap: wrap; | |
| box-sizing: border-box; | |
| background: #f0f0f0; | |
| padding: 1rem; | |
| h1, h2 { | |
| margin-top: 0; | |
| flex-basis: 100%; | |
| margin-left: 1px; | |
| margin-right: 1px; | |
| line-height: 1; | |
| text-align: left; | |
| } | |
| h1 { font-size: 1.2em; } | |
| h2 { font-size: 1em; } | |
| > span { | |
| flex: 1; | |
| box-sizing: border-box; | |
| text-align: center; | |
| padding: 0.5em; | |
| margin: 1px; | |
| border: 1px solid #ccc; | |
| color: #444; | |
| font-size: 0.6em; | |
| text-transform: uppercase; | |
| letter-spacing: 0.125em; | |
| padding-top: 0.75em; | |
| sup { font-size: 0.6em; margin-left: -2px; } | |
| } | |
| &:not(.keyboard) { | |
| margin: 0.5rem; | |
| border-radius: 4px; | |
| box-shadow: 0px 2px 0px 2px #d9d9d9; | |
| div { | |
| box-sizing: border-box; | |
| cursor: pointer; | |
| text-align: center; | |
| padding: 0.5em; | |
| margin: 1px; | |
| background: white; | |
| font-size: 0.6em; | |
| text-transform: uppercase; | |
| letter-spacing: 0.125em; | |
| flex: 1 0 auto; | |
| &:hover { | |
| background: #ccc; | |
| } | |
| &[class$="-current"] { | |
| background: #FB0; | |
| pointer-events: none; | |
| } | |
| } | |
| } | |
| &.chord { | |
| > div { | |
| background: transparent; | |
| padding: 0; | |
| margin-top: 0; margin-bottom: 0; | |
| display: flex; | |
| flex-direction: column; | |
| font-size: 1em; | |
| + div { margin-left: 0.5em; } | |
| &:hover { | |
| background: transparent; | |
| } | |
| &.active { | |
| box-shadow: 0px 0px 0px 1px #fb0; | |
| } | |
| div { | |
| flex: 1; | |
| background: white; | |
| } | |
| } | |
| } | |
| &.keys { | |
| div { | |
| flex-basis: calc(#{1/6 * 100%} - 2px); | |
| sup { | |
| pointer-events: none; | |
| font-size: 0.6em; | |
| } | |
| } | |
| } | |
| &.patterns { | |
| align-content: center; | |
| div { | |
| // flex-basis: 10%; | |
| } | |
| } | |
| @media(min-width: 800px) { | |
| // grid | |
| &.chord { | |
| flex-basis: calc(100% - 1rem); | |
| } | |
| &.keys, &.modes, &.steps, &.type { | |
| flex-basis: calc(50% - 1rem); | |
| } | |
| } | |
| &.chord > div, &.bpm, &.keys, &.modes, &.steps, &.type { | |
| > div { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; | |
| } | |
| } | |
| } | |
| svg { | |
| width: 100%; | |
| height: auto; | |
| pointer-events: none; | |
| polyline { | |
| fill: none; | |
| stroke: black; | |
| stroke-linecap: round; | |
| stroke-linejoin: round; | |
| } | |
| } |