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 | |
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 | |
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, 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); | |
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._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(); |
<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; | |
text-transform: none; | |
} | |
} | |
} | |
&.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; | |
} | |
} |