<article>
  <aside id="aside"></aside>
  <main id="main"></main>
</article>
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(16.66% - 2px);
      sup {
        pointer-events: none;
        font-size: 0.6em;
      }
    }
  }
  &.patterns {
    align-content: center;
    div { 
      max-width: 20%; flex-basis: 20%; 
      flex-basis: calc(16.66% - 2px);
    }
    @media (min-width: 800px) {
      div { max-width: 20%; }
      &.patterns-720 > div { flex-basis: calc(10% - 2px); }
      &.patterns-120 > div { flex-basis: calc(12% - 2px); }
      &.patterns-24 > div { flex-basis: calc(12% - 2px); }
      &.patterns-6 > div { flex-basis: calc(10% - 2px); }
    }
  }
  
  @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;
  }
}
View Compiled
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();
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://s3-us-west-2.amazonaws.com/s.cdpn.io/111863/Tone.min.js