<div class="progress pink">
    <div class="indeterminate white"></div>
  </div>
  <div class="app" style="display: none;">
    <div class="sequencer">
      <div class="steps"></div>
    </div>
    <a class="regenerate btn-floating btn-large waves-effect waves-light pink darken-2 pulse">
      <i class="material-icons">refresh</i>
    </a>
    <div class="controls">
      <div class="control">
        <a class="playpause btn-floating btn-large waves-effect waves-light blue">
          <i class="material-icons play-icon">play_arrow</i>
          <i class="material-icons pause-icon" style="display: none">pause</i>
        </a>
      </div>
      <div class="control">
        <div class="input-field grey-text">
          <select id="pattern-length">
            <option>4</option>
            <option>8</option>
            <option selected>16</option>
            <option>32</option>
          </select>
          Pattern length
        </div>
      </div>
      <div class="control">
        <p class="range-field grey-text">
          <input type="range" id="tempo" min="20" max="240" value="120" step="1"> Tempo
        </p>
      </div>
      <div class="control">
        <p class="range-field grey-text">
          <input type="range" id="swing" min="0.5" max="0.7" value="0.55" step="0.05"> Swing
        </p>
      </div>
      <div class="control">
        <p class="range-field grey-text">
          <input type="range" id="temperature" class="tooltipped" min="0.5" max="2" value="1.1" step="0.1" data-tooltip="Higher temperatures will make the neural network generates wilder patterns"
                 data-delay="500"> Temperature
        </p>
      </div>
    </div>
  </div>
  <div class="info">
    <p class="webmidi-enabled" style="display: none;">
      Output:
      <select class="midi-output"></select>
    </p>
    <p class="webmidi-enabled" style="display: none;">
      MIDI clock output:
      <select class="midi-clock-output"></select>
    </p>
    <p class="webmidi-enabled" style="display: none;">
      MIDI clock input:
      <select class="midi-clock-input"></select>
    </p>
    <p>This is an experimental drum machine powered by a deep neural network. To use it, define a seed pattern on the left,
      and use the &ldquo;generate&rdquo; button. A neural network will dream up a continuation to your seed pattern.</p>
    <p>Using the
      <a href="https://github.com/tensorflow/magenta/tree/master/magenta/models/drums_rnn">Drums RNN</a> (pretrained) model from
      <a href="https://magenta.tensorflow.org/">Google Magenta</a>, and
      <a href="https://goo.gl/magenta/js">Magenta.js</a> + 
      <a href="https://js.tensorflow.org/">TensorFlow.js</a> +
      <a href="https://tonejs.github.io/">Tone.js</a>.</p>
    <p>A pen by
      <a href="https://twitter.com/teropa">@teropa</a>.</p>
  </div>
html,
body,
.app {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;

  background-color: #111;
  color: white;
}

.progress {
  max-width: 30%;
  margin: 45vh auto 55vh auto;
}

.app {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;

  background: linear-gradient(to right, #010305, #131516);
}

.sequencer,
.controls {
  width: 80vw;
}
.controls {
  display: flex;
  flex-direction: row;
  justify-content: space-around;
}
.control {
  text-align: center;
}
.control input {
  margin-bottom: 20px;
}
.select-wrapper span.caret {
  color: inherit; /* Override materialize.css black */
}
.select-wrapper input.select-dropdown {
  margin-bottom: 10px;
}
.input-field {
  margin-top: 0;
}
input[type='range'] {
  border: 0;
}
input[type='range'] + .thumb {
  background-color: #2196f3;
}
input[type='range']::-webkit-slider-thumb {
  background-color: #2196f3;
}
input[type='range']::-moz-range-thumb {
  background-color: #2196f3;
}
input[type='range']::-ms-thumb {
  background-color: #2196f3;
}
.control .share {
  margin-top: 10px;
}
.midi-output,
.midi-clock-input,
.select-wrapper {
  max-width: 200px;
  margin: 0 auto;
}
.select-wrapper input {
  color: white;
}
.sequencer {
  position: relative;
  height: 70vh;
  margin: 20px 0;
  display: flex;
  flex-direction: row;
  perspective: 1000px;
  transform-style: preserve-3d;
}
.sequencer .legend {
  flex: 0.75;
  display: flex;
  flex-direction: column;
}
.drum-class {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  margin: 2.5px;

  font-size: 12px;
  color: #999;
}
.sequencer .steps {
  flex: 9;
  display: flex;
  flex-direction: row;
}
.step {
  flex: 1;
  display: flex;
  flex-direction: column;
  transform-style: preserve-3d;
}
.steps {
  transform-style: preserve-3d;
}
.gutter {
  width: 5px;
}
.gutter.seed-marker {
  background-color: #aaa;
  margin: 0 10px;
  cursor: move;
}
.gutter.seed-marker:hover {
  background-color: white;
}
.cell {
  margin: 2.5px 0;
  background-color: rgba(100, 100, 100, 0.1);
  border: 1px solid rgba(100, 100, 100, 0.2);
  border-radius: 3px;
  z-index: 0;
  transition: background-color 0.3s ease;
}

.cell:hover {
  background-color: rgba(100, 100, 100, 0.5);
}
.step:not(.regenerating) .cell.on {
  background-color: #64b5f6;
  opacity: 1;
}

.step.seed .cell.on {
  background-color: #e91e63;
}

.regenerate {
  position: absolute;
  visibility: hidden;
  font-size: 30px;
  line-height: 56px;
}

.kick {
  flex: 2;
  margin-bottom: 10px;
}
.snare {
  flex: 1;
  margin-top: 10px;
  margin-bottom: 10px;
}
.hi-hat-closed {
  flex: 1;
  margin-top: 10px;
}
.hi-hat-open {
  flex: 1;
  margin-bottom: 10px;
}
.tom-low {
  flex: 1;
  margin-top: 10px;
}
.tom-mid {
  flex: 1;
}
.tom-high {
  flex: 1;
  margin-bottom: 10px;
}
.clap {
  flex: 1;
  margin-top: 10px;
}
.rim {
  flex: 1;
}

.info {
  max-width: 50%;
  margin: 100px auto;
  text-align: center;
}
.info p {
  margin-top: 50px;
}
const DRUM_CLASSES = [
  'Kick',
  'Snare',
  'Hi-hat closed',
  'Hi-hat open',
  'Tom low',
  'Tom mid',
  'Tom high',
  'Clap',
  'Rim'
];
const TIME_HUMANIZATION = 0.01;

let Tone = mm.Player.tone;

let sampleBaseUrl = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699';

let reverb = new Tone.Convolver(
  `${sampleBaseUrl}/small-drum-room.wav`
).toMaster();
reverb.wet.value = 0.35;

let snarePanner = new Tone.Panner().connect(reverb);
new Tone.LFO(0.13, -0.25, 0.25).connect(snarePanner.pan).start();

let drumKit = [
  new Tone.Players({
    high: `${sampleBaseUrl}/808-kick-vh.mp3`,
    med: `${sampleBaseUrl}/808-kick-vm.mp3`,
    low: `${sampleBaseUrl}/808-kick-vl.mp3`
  }).toMaster(),
  new Tone.Players({
    high: `${sampleBaseUrl}/flares-snare-vh.mp3`,
    med: `${sampleBaseUrl}/flares-snare-vm.mp3`,
    low: `${sampleBaseUrl}/flares-snare-vl.mp3`
  }).connect(snarePanner),
  new Tone.Players({
    high: `${sampleBaseUrl}/808-hihat-vh.mp3`,
    med: `${sampleBaseUrl}/808-hihat-vm.mp3`,
    low: `${sampleBaseUrl}/808-hihat-vl.mp3`
  }).connect(new Tone.Panner(-0.5).connect(reverb)),
  new Tone.Players({
    high: `${sampleBaseUrl}/808-hihat-open-vh.mp3`,
    med: `${sampleBaseUrl}/808-hihat-open-vm.mp3`,
    low: `${sampleBaseUrl}/808-hihat-open-vl.mp3`
  }).connect(new Tone.Panner(-0.5).connect(reverb)),
  new Tone.Players({
    high: `${sampleBaseUrl}/slamdam-tom-low-vh.mp3`,
    med: `${sampleBaseUrl}/slamdam-tom-low-vm.mp3`,
    low: `${sampleBaseUrl}/slamdam-tom-low-vl.mp3`
  }).connect(new Tone.Panner(-0.4).connect(reverb)),
  new Tone.Players({
    high: `${sampleBaseUrl}/slamdam-tom-mid-vh.mp3`,
    med: `${sampleBaseUrl}/slamdam-tom-mid-vm.mp3`,
    low: `${sampleBaseUrl}/slamdam-tom-mid-vl.mp3`
  }).connect(reverb),
  new Tone.Players({
    high: `${sampleBaseUrl}/slamdam-tom-high-vh.mp3`,
    med: `${sampleBaseUrl}/slamdam-tom-high-vm.mp3`,
    low: `${sampleBaseUrl}/slamdam-tom-high-vl.mp3`
  }).connect(new Tone.Panner(0.4).connect(reverb)),
  new Tone.Players({
    high: `${sampleBaseUrl}/909-clap-vh.mp3`,
    med: `${sampleBaseUrl}/909-clap-vm.mp3`,
    low: `${sampleBaseUrl}/909-clap-vl.mp3`
  }).connect(new Tone.Panner(0.5).connect(reverb)),
  new Tone.Players({
    high: `${sampleBaseUrl}/909-rim-vh.wav`,
    med: `${sampleBaseUrl}/909-rim-vm.wav`,
    low: `${sampleBaseUrl}/909-rim-vl.wav`
  }).connect(new Tone.Panner(0.5).connect(reverb))
];
let midiDrums = [36, 38, 42, 46, 41, 43, 45, 49, 51];
let reverseMidiMapping = new Map([
  [36, 0],
  [35, 0],
  [38, 1],
  [27, 1],
  [28, 1],
  [31, 1],
  [32, 1],
  [33, 1],
  [34, 1],
  [37, 1],
  [39, 1],
  [40, 1],
  [56, 1],
  [65, 1],
  [66, 1],
  [75, 1],
  [85, 1],
  [42, 2],
  [44, 2],
  [54, 2],
  [68, 2],
  [69, 2],
  [70, 2],
  [71, 2],
  [73, 2],
  [78, 2],
  [80, 2],
  [46, 3],
  [67, 3],
  [72, 3],
  [74, 3],
  [79, 3],
  [81, 3],
  [45, 4],
  [29, 4],
  [41, 4],
  [61, 4],
  [64, 4],
  [84, 4],
  [48, 5],
  [47, 5],
  [60, 5],
  [63, 5],
  [77, 5],
  [86, 5],
  [87, 5],
  [50, 6],
  [30, 6],
  [43, 6],
  [62, 6],
  [76, 6],
  [83, 6],
  [49, 7],
  [55, 7],
  [57, 7],
  [58, 7],
  [51, 8],
  [52, 8],
  [53, 8],
  [59, 8],
  [82, 8]
]);

let temperature = 1.0;

let outputs = {
  internal: {
    play: (drumIdx, velocity, time) => {
      drumKit[drumIdx].get(velocity).start(time);
    }
  }
};

let rnn = new mm.MusicRNN(
  'https://storage.googleapis.com/download.magenta.tensorflow.org/tfjs_checkpoints/music_rnn/drum_kit_rnn'
);
Promise.all([
  rnn.initialize(),
  new Promise(res => Tone.Buffer.on('load', res))
]).then(([vars]) => {
  let state = {
    patternLength: 32,
    seedLength: 4,
    swing: 0.55,
    pattern: [[0], [], [2]].concat(_.times(32, i => [])),
    tempo: 120
  };
  let stepEls = [],
    hasBeenStarted = false,
    oneEighth = Tone.Time('8n').toSeconds(),
    activeOutput = 'internal',
    midiClockSender = null,
    midiClockStartSent = false,
    activeClockInput = 'none',
    currentSchedulerId,
    stepCounter;

  function generatePattern(seed, length) {
    let seedSeq = toNoteSequence(seed);
    return rnn
      .continueSequence(seedSeq, length, temperature)
      .then(r => seed.concat(fromNoteSequence(r, length)));
  }

  function getStepVelocity(step) {
    if (step % 4 === 0) {
      return 'high';
    } else if (step % 2 === 0) {
      return 'med';
    } else {
      return 'low';
    }
  }

  function humanizeTime(time) {
    return time - TIME_HUMANIZATION / 2 + Math.random() * TIME_HUMANIZATION;
  }

  function tick(time = Tone.now() - Tone.context.lookAhead) {
    if (_.isNumber(stepCounter) && state.pattern) {
      stepCounter++;
      if (midiClockSender) midiClockSender(time, stepCounter);

      let stepIdx = stepCounter % state.pattern.length;
      let isSwung = stepIdx % 2 !== 0;
      if (isSwung) {
        time += (state.swing - 0.5) * oneEighth;
      }
      let velocity = getStepVelocity(stepIdx);
      let drums = state.pattern[stepIdx];
      drums.forEach(d => {
        let humanizedTime = stepIdx === 0 ? time : humanizeTime(time);
        outputs[activeOutput].play(d, velocity, humanizedTime);
        visualizePlay(humanizedTime, stepIdx, d);
      });
    }
  }

  function startPattern() {
    stepCounter = -1;
    midiClockStartSent = false;
    updatePlayPauseIcons();
  }

  function stopPattern() {
    stepCounter = null;
    updatePlayPauseIcons();
  }

  function visualizePlay(time, stepIdx, drumIdx) {
    Tone.Draw.schedule(() => {
      if (!stepEls[stepIdx]) return;
      let animTime = oneEighth * 4 * 1000;
      let cellEl = stepEls[stepIdx].cellEls[drumIdx];
      if (cellEl.classList.contains('on')) {
        let baseColor = stepIdx < state.seedLength ? '#e91e63' : '#64b5f6';
        cellEl.animate(
          [
            {
              transform: 'translateZ(-100px)',
              backgroundColor: '#fad1df'
            },
            {
              transform: 'translateZ(50px)',
              offset: 0.7
            },
            { transform: 'translateZ(0)', backgroundColor: baseColor }
          ],
          { duration: animTime, easing: 'cubic-bezier(0.23, 1, 0.32, 1)' }
        );
      }
    }, time);
  }

  function renderPattern(regenerating = false) {
    let seqEl = document.querySelector('.sequencer .steps');
    while (stepEls.length > state.pattern.length) {
      let { stepEl, gutterEl } = stepEls.pop();
      stepEl.remove();
      if (gutterEl) gutterEl.remove();
    }
    for (let stepIdx = 0; stepIdx < state.pattern.length; stepIdx++) {
      let step = state.pattern[stepIdx];
      let stepEl, gutterEl, cellEls;
      if (stepEls[stepIdx]) {
        stepEl = stepEls[stepIdx].stepEl;
        gutterEl = stepEls[stepIdx].gutterEl;
        cellEls = stepEls[stepIdx].cellEls;
      } else {
        stepEl = document.createElement('div');
        stepEl.classList.add('step');
        stepEl.dataset.stepIdx = stepIdx;
        seqEl.appendChild(stepEl);
        cellEls = [];
      }

      stepEl.style.flex = stepIdx % 2 === 0 ? state.swing : 1 - state.swing;

      if (!gutterEl && stepIdx < state.pattern.length - 1) {
        gutterEl = document.createElement('div');
        gutterEl.classList.add('gutter');
        seqEl.insertBefore(gutterEl, stepEl.nextSibling);
      } else if (gutterEl && stepIdx >= state.pattern.length) {
        gutterEl.remove();
        gutterEl = null;
      }

      if (gutterEl && stepIdx === state.seedLength - 1) {
        gutterEl.classList.add('seed-marker');
      } else if (gutterEl) {
        gutterEl.classList.remove('seed-marker');
      }

      for (let cellIdx = 0; cellIdx < DRUM_CLASSES.length; cellIdx++) {
        let cellEl;
        if (cellEls[cellIdx]) {
          cellEl = cellEls[cellIdx];
        } else {
          cellEl = document.createElement('div');
          cellEl.classList.add('cell');
          cellEl.classList.add(_.kebabCase(DRUM_CLASSES[cellIdx]));
          cellEl.dataset.stepIdx = stepIdx;
          cellEl.dataset.cellIdx = cellIdx;
          stepEl.appendChild(cellEl);
          cellEls[cellIdx] = cellEl;
        }
        if (step.indexOf(cellIdx) >= 0) {
          cellEl.classList.add('on');
        } else {
          cellEl.classList.remove('on');
        }
      }
      stepEls[stepIdx] = { stepEl, gutterEl, cellEls };

      let stagger = stepIdx * (300 / (state.patternLength - state.seedLength));
      setTimeout(() => {
        if (stepIdx < state.seedLength) {
          stepEl.classList.add('seed');
        } else {
          stepEl.classList.remove('seed');
          if (regenerating) {
            stepEl.classList.add('regenerating');
          } else {
            stepEl.classList.remove('regenerating');
          }
        }
      }, stagger);
    }

    setTimeout(repositionRegenerateButton, 0);
  }

  function repositionRegenerateButton() {
    let regenButton = document.querySelector('.regenerate');
    let sequencerEl = document.querySelector('.sequencer');
    let seedMarkerEl = document.querySelector('.gutter.seed-marker');
    let regenLeft =
      sequencerEl.offsetLeft +
      seedMarkerEl.offsetLeft +
      seedMarkerEl.offsetWidth / 2 -
      regenButton.offsetWidth / 2;
    let regenTop =
      sequencerEl.offsetTop +
      seedMarkerEl.offsetTop +
      seedMarkerEl.offsetHeight / 2 -
      regenButton.offsetHeight / 2;
    regenButton.style.left = `${regenLeft}px`;
    regenButton.style.top = `${regenTop}px`;
    regenButton.style.visibility = 'visible';
  }

  function regenerate() {
    let seed = _.take(state.pattern, state.seedLength);
    renderPattern(true);
    return generatePattern(seed, state.patternLength - seed.length).then(
      result => {
        state.pattern = result;
        onPatternUpdated();
      }
    );
  }

  function onPatternUpdated() {
    stopPattern();
    renderPattern();
  }

  function toggleStep(cellEl) {
    if (state.pattern && cellEl.classList.contains('cell')) {
      let stepIdx = +cellEl.dataset.stepIdx;
      let cellIdx = +cellEl.dataset.cellIdx;
      let isOn = cellEl.classList.contains('on');
      if (isOn) {
        _.pull(state.pattern[stepIdx], cellIdx);
        cellEl.classList.remove('on');
      } else {
        state.pattern[stepIdx].push(cellIdx);
        cellEl.classList.add('on');
      }
    }
  }

  function toNoteSequence(pattern) {
    return mm.sequences.quantizeNoteSequence(
      {
        ticksPerQuarter: 220,
        totalTime: pattern.length / 2,
        timeSignatures: [
          {
            time: 0,
            numerator: 4,
            denominator: 4
          }
        ],
        tempos: [
          {
            time: 0,
            qpm: 120
          }
        ],
        notes: _.flatMap(pattern, (step, index) =>
          step.map(d => ({
            pitch: midiDrums[d],
            startTime: index * 0.5,
            endTime: (index + 1) * 0.5
          }))
        )
      },
      1
    );
  }

  function fromNoteSequence(seq, patternLength) {
    let res = _.times(patternLength, () => []);
    for (let { pitch, quantizedStartStep } of seq.notes) {
      res[quantizedStartStep].push(reverseMidiMapping.get(pitch));
    }
    return res;
  }

  function setSwing(newSwing) {
    state.swing = newSwing;
    renderPattern();
  }

  function updatePlayPauseIcons() {
    if (_.isNumber(stepCounter)) {
      document.querySelector('.playpause .pause-icon').style.display = null;
      document.querySelector('.playpause .play-icon').style.display = 'none';
    } else {
      document.querySelector('.playpause .play-icon').style.display = null;
      document.querySelector('.playpause .pause-icon').style.display = 'none';
    }
  }

  WebMidi.enable(err => {
    if (err) {
      console.error('WebMidi could not be enabled', err);
      return;
    }
    document
      .querySelectorAll('.webmidi-enabled')
      .forEach(e => (e.style.display = 'block'));
    let outputSelector = document.querySelector('.midi-output');
    let clockOutputSelector = document.querySelector('.midi-clock-output');
    let clockInputSelector = document.querySelector('.midi-clock-input');
    let activeClockOutput,
      midiClockCounter = 0,
      eighthsCounter = 0,
      lastEighthAt;

    function onDevicesChanged() {
      while (outputSelector.firstChild) {
        outputSelector.firstChild.remove();
      }
      let internalOption = document.createElement('option');
      internalOption.value = 'internal';
      internalOption.innerText = 'Internal drumkit';
      outputSelector.appendChild(internalOption);
      for (let output of WebMidi.outputs) {
        let option = document.createElement('option');
        option.value = output.id;
        option.innerText = output.name;
        outputSelector.appendChild(option);
      }
      $(outputSelector).formSelect();
      onActiveOutputChange('internal');

      while (clockOutputSelector.firstChild) {
        clockOutputSelector.firstChild.remove();
      }
      let noneOption = document.createElement('option');
      noneOption.value = 'none';
      noneOption.innerText = 'Not sending';
      clockOutputSelector.appendChild(noneOption);
      for (let output of WebMidi.outputs) {
        let option = document.createElement('option');
        option.value = output.id;
        option.innerText = output.name;
        clockOutputSelector.appendChild(option);
      }
      $(clockOutputSelector).formSelect();
      onActiveClockOutputChange('none');

      while (clockInputSelector.firstChild) {
        clockInputSelector.firstChild.remove();
      }
      noneOption = document.createElement('option');
      noneOption.value = 'none';
      noneOption.innerText = 'None (using internal clock)';
      clockInputSelector.appendChild(noneOption);
      for (let input of WebMidi.inputs) {
        let option = document.createElement('option');
        option.value = input.id;
        option.innerText = input.name;
        clockInputSelector.appendChild(option);
      }
      $(clockInputSelector).formSelect();
      onActiveClockInputChange('none');
    }

    function onActiveOutputChange(id) {
      if (activeOutput !== 'internal') {
        outputs[activeOutput] = null;
      }
      activeOutput = id;
      if (activeOutput !== 'internal') {
        let output = WebMidi.getOutputById(id);
        outputs[id] = {
          play: (drumIdx, velo, time) => {
            let delay = (time - Tone.now()) * 1000;
            let duration = (oneEighth / 2) * 1000;
            let velocity = { high: 1, med: 0.75, low: 0.5 };
            output.playNote(midiDrums[drumIdx], 1, {
              time: delay > 0 ? `+${delay}` : WebMidi.now,
              velocity,
              duration
            });
          }
        };
      }
      for (let option of Array.from(outputSelector.children)) {
        option.selected = option.value === id;
      }
    }

    function onActiveClockOutputChange(id) {
      if (activeClockOutput !== 'none') {
        stopSendingMidiClock();
      }
      activeClockOutput = id;
      if (activeClockOutput !== 'none') {
        startSendingMidiClock();
      }
      for (let option of Array.from(clockOutputSelector.children)) {
        option.selected = option.value === id;
      }
    }

    function startSendingMidiClock() {
      let output = WebMidi.getOutputById(activeClockOutput);
      midiClockSender = function(time, stepCounter) {
        let startDelay = time - Tone.now() + Tone.context.lookAhead;
        let sixteenth = Tone.Time('16n').toSeconds();
        for (let i = 0; i < 6; i++) {
          let tickDelay = startDelay + (sixteenth / 6) * i;
          if (i === 0 && stepCounter === 0 && !midiClockStartSent) {
            console.log('sending clock start');
            output.sendStart({ time: `+${tickDelay * 1000}` });
            midiClockStartSent = true;
          }
          output.sendClock({ time: `+${tickDelay * 1000}` });
        }
      };
    }

    function stopSendingMidiClock() {
      midiClockSender = null;
      midiClockStartSent = false;
    }

    function incomingMidiClockStart() {
      midiClockCounter = 0;
      eighthsCounter = 0;
      startPattern();
    }

    function incomingMidiClockStop() {
      midiClockCounter = 0;
      eighthsCounter = 0;
      lastEighthAt = null;
      stopPattern();
    }

    function incomingMidiClockTick(evt) {
      if (midiClockCounter % 6 === 0) {
        tick();
      }
      if (eighthsCounter % 12 === 0) {
        if (lastEighthAt) {
          oneEighth = (evt.timestamp - lastEighthAt) / 1000;
        }
        lastEighthAt = evt.timestamp;
      }
      midiClockCounter++;
      eighthsCounter++;
    }

    function onActiveClockInputChange(id) {
      if (activeClockInput === 'none') {
        Tone.Transport.clear(currentSchedulerId);
        currentSchedulerId = null;
      } else if (activeClockInput) {
        let input = WebMidi.getInputById(activeClockInput);
        input.removeListener('start', 'all', incomingMidiClockStart);
        input.removeListener('stop', 'all', incomingMidiClockStop);
        input.removeListener('clock', 'all', incomingMidiClockTick);
      }
      activeClockInput = id;
      if (activeClockInput === 'none') {
        currentSchedulerId = Tone.Transport.scheduleRepeat(tick, '16n');
        oneEighth = Tone.Time('8n').toSeconds();
      } else {
        let input = WebMidi.getInputById(id);
        input.addListener('start', 'all', incomingMidiClockStart);
        input.addListener('stop', 'all', incomingMidiClockStop);
        input.addListener('clock', 'all', incomingMidiClockTick);
      }
    }

    onDevicesChanged();
    WebMidi.addListener('connected', onDevicesChanged);
    WebMidi.addListener('disconnected', onDevicesChanged);

    $(outputSelector)
      .on('change', evt => onActiveOutputChange(evt.target.value))
      .formSelect();
    $(clockOutputSelector)
      .on('change', evt => onActiveClockOutputChange(evt.target.value))
      .formSelect();
    $(clockInputSelector)
      .on('change', evt => onActiveClockInputChange(evt.target.value))
      .formSelect();
  });

  document.querySelector('.app').addEventListener('click', event => {
    if (event.target.classList.contains('cell')) {
      toggleStep(event.target);
    }
  });
  document.querySelector('.regenerate').addEventListener('click', event => {
    event.preventDefault();
    event.currentTarget.classList.remove('pulse');
    document.querySelector('.playpause').classList.remove('pulse');
    regenerate().then(() => {
      if (!hasBeenStarted) {
        Tone.context.resume();
        Tone.Transport.start();
        hasBeenStarted = true;
      }
      if (Tone.Transport.state === 'started') {
        setTimeout(startPattern, 0);
      }
    });
  });
  document.querySelector('.playpause').addEventListener('click', event => {
    event.preventDefault();
    document.querySelector('.playpause').classList.remove('pulse');
    if (_.isNumber(stepCounter)) {
      stopPattern();
      Tone.Transport.pause();
    } else {
      Tone.context.resume();
      Tone.Transport.start();
      startPattern();
      hasBeenStarted = true;
    }
  });

  let draggingSeedMarker = false;
  document.querySelector('.app').addEventListener('mousedown', evt => {
    let el = evt.target;
    if (
      el.classList.contains('gutter') &&
      el.classList.contains('seed-marker')
    ) {
      draggingSeedMarker = true;
      evt.preventDefault();
    }
  });
  document.querySelector('.app').addEventListener('mouseup', () => {
    draggingSeedMarker = false;
  });
  document.querySelector('.app').addEventListener('mouseover', evt => {
    if (draggingSeedMarker) {
      let el = evt.target;
      while (el) {
        if (el.classList.contains('step')) {
          let stepIdx = +el.dataset.stepIdx;
          if (stepIdx > 0) {
            state.seedLength = stepIdx;
            renderPattern();
          }
          break;
        }
        el = el.parentElement;
      }
    }
  });
  document
    .querySelector('#swing')
    .addEventListener('input', evt => setSwing(+evt.target.value));
  document
    .querySelector('#temperature')
    .addEventListener('input', evt => (temperature = +evt.target.value));
  document.querySelector('#tempo').addEventListener('input', evt => {
    Tone.Transport.bpm.value = state.tempo = +evt.target.value;
    oneEighth = Tone.Time('8n').toSeconds();
  });

  $('#pattern-length')
    .on('change', evt => setPatternLength(+evt.target.value))
    .formSelect();

  window.addEventListener('resize', repositionRegenerateButton);

  renderPattern();

  document.querySelector('.progress').remove();
  document.querySelector('.app').style.display = null;
});
View Compiled
Run Pen

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/css/materialize.min.css
  2. https://fonts.googleapis.com/icon?family=Material+Icons

External JavaScript

  1. https://cdn.jsdelivr.net/npm/babel-regenerator-runtime@6.5.0/runtime.min.js
  2. https://cdn.jsdelivr.net/npm/lodash@4.17.5/lodash.min.js
  3. https://cdn.jsdelivr.net/npm/@magenta/music@1.1.11/dist/magentamusic.min.js
  4. https://code.jquery.com/jquery-3.2.1.min.js
  5. https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/js/materialize.min.js
  6. https://cdn.jsdelivr.net/npm/web-animations-js@2.3.1/web-animations.min.js
  7. https://cdn.jsdelivr.net/npm/webmidi@2.0.0/webmidi.min.js