< >

glocken_spiel_und_so.php


Quell Code


<html>

<head>
  <style>
    html,
body,
#container {
  height: 100%;
  margin: 0;
  padding: 0;
  background: linear-gradient(0.25turn, #752525, #050505, #252575);
  color: #f3f3f3;
}

#container {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-evenly;
}
#vis {
  width: 95vmin;
  height: 95vmin;
  overflow: visible;
}

.halo {
  opacity: 0;
}
.note {
  stroke-width: 0.5;
  stroke: #151515;
  fill: rgba(255, 255, 255, 1);
  opacity: 0.4;
}
.hover .note {
  opacity: 0.7;
}
.on .note {
  fill: #e91e63;
  opacity: 1;
}

.pointer-area {
  stroke: none;
  opacity: 0;
}

.controls {
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-around;
}
.control-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.controls button {
  width: 100%;
  min-width: 7vw;
  height: 4vh;
  margin: 2px;
  background: none;
  color: white;
  border: 1px solid white;
}
.controls button:hover {
  background: rgba(255, 255, 255, 0.5);
}
.controls button.active {
  background: white;
  color: black;
}

#generating,
#loading {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;

  font-size: 32px;
}

    
  </style>



<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.js"></script>
<script src="https://teropa.info/libs/musicvae-1.1.3-with-tf.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tone@0.12.80/build/Tone.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/web-animations-js@2.3.1/web-animations.min.js"></script>
<script src="https://cdn.rawgit.com/tambien/StartAudioContext/8da8637e/StartAudioContext.js"></script>
</head>
<body>
<div id="container">
  <div class="controls">
    <div class="control-group">
      <button class="tonic-left active" data-tonic="0">C</button>
      <button class="tonic-left" data-tonic="1">C&#x266F; / D&#x266d;</button>
      <button class="tonic-left" data-tonic="2">D</button>
      <button class="tonic-left" data-tonic="3">E&#x266F; / E&#x266d;</button>
      <button class="tonic-left" data-tonic="4">E</button>
      <button class="tonic-left" data-tonic="5">F</button>
      <button class="tonic-left" data-tonic="6">F&#x266F; / G&#x266d;</button>
      <button class="tonic-left" data-tonic="7">G</button>
      <button class="tonic-left" data-tonic="8">G&#x266F; / A&#x266d;</button>
      <button class="tonic-left" data-tonic="9">A</button>
      <button class="tonic-left" data-tonic="10">A&#x266F; / B&#x266d;</button>
      <button class="tonic-left" data-tonic="11">B</button>
    </div>
    <div class="control-group">
      <button class="chord-left active" data-chord="major">Major</button>
      <button class="chord-left" data-chord="minor">Minor</button>
      <button class="chord-left" data-chord="major7th">Major 7th</button>
      <button class="chord-left" data-chord="minor7th">Minor 7th</button>
      <button class="chord-left" data-chord="dominant7th">Dominant 7th</button>
      <button class="chord-left" data-chord="sus2">Sus2</button>
      <button class="chord-left" data-chord="sus4">Sus4</button>
    </div>
  </div>
  <svg id="vis" viewBox="0 0 1000 1000">
    <defs>
      <radialGradient id="halo">
        <stop offset="0%" stop-color="rgba(255, 255, 255, 0.5)" />
        <stop offset="95%" stop-color="rgba(255, 255, 255, 0.5)" />
        <stop offset="100%" stop-color="rgba(255, 255, 255, 0)" />
      </radialGradient>
    </defs>
    <g id="vis-halos"></g>
    <g id="vis-elements"></g>
  </svg>
  <div class="controls">
    <div class="control-group">
      <button class="tonic-right active" data-tonic="0">C</button>
      <button class="tonic-right" data-tonic="1">C&#x266F; / D&#x266d;</button>
      <button class="tonic-right" data-tonic="2">D</button>
      <button class="tonic-right" data-tonic="3">E&#x266F; / E&#x266d;</button>
      <button class="tonic-right" data-tonic="4">E</button>
      <button class="tonic-right" data-tonic="5">F</button>
      <button class="tonic-right" data-tonic="6">F&#x266F; / G&#x266d;</button>
      <button class="tonic-right" data-tonic="7">G</button>
      <button class="tonic-right" data-tonic="8">G&#x266F; / A&#x266d;</button>
      <button class="tonic-right" data-tonic="9">A</button>
      <button class="tonic-right" data-tonic="10">A&#x266F; / B&#x266d;</button>
      <button class="tonic-right" data-tonic="11">B</button>
    </div>
    <div class="control-group">
      <button class="chord-right active" data-chord="major">Major</button>
      <button class="chord-right" data-chord="minor">Minor</button>
      <button class="chord-right" data-chord="major7th">Major 7th</button>
      <button class="chord-right" data-chord="minor7th">Minor 7th</button>
      <button class="chord-right" data-chord="dominant7th">Dominant 7th</button>
      <button class="chord-right" data-chord="sus2">Sus2</button>
      <button class="chord-right" data-chord="sus4">Sus4</button>
    </div>
  </div>
</div>
<div id="loading">Loading models&hellip;</div>
<div id="generating" style="display: none">Generating&hellip;</div>


<script>
const MIN_NOTE = 48;
const MAX_NOTE = 84;
const SEQ_LENGTH = 32;
const HUMANIZE_TIMING = 0.0085;
const N_INTERPOLATIONS = 10;
const CHORD_INTERVALS = {
  major: [0, 4, 7],
  minor: [0, 3, 7],
  major7th: [0, 4, 7, 11],
  minor7th: [0, 3, 7, 10],
  dominant7th: [0, 4, 7, 10],
  sus2: [0, 2, 7],
  sus4: [0, 5, 7]
};
const SAMPLE_SCALE = [
  'C3',
  'D#3',
  'F#3',
  'A3',
  'C4',
  'D#4',
  'F#4',
  'A4',
  'C5',
  'D#5',
  'F#5',
  'A5'
];

Tone.Transport.bpm.value = 90;

let mvae = new musicvae.MusicVAE('https://storage.googleapis.com/download.magenta.tensorflow.org/models/music_vae/dljs/mel_small');
let tf = musicvae.tf;

// Using the Improv RNN pretrained model from https://github.com/tensorflow/magenta/tree/master/magenta/models/improv_rnn
let lstmLoader = new musicvae.CheckpointLoader('https://teropa.info/improv_rnn_pretrained_checkpoint/');
let lstm;
let rnnLoadPromise = lstmLoader.getAllVariables().then(vars => {
  lstm = {
    kernel1: vars['RNN/MultiRNNCell/Cell0/BasicLSTMCell/Linear/Matrix'],
    bias1: vars['RNN/MultiRNNCell/Cell0/BasicLSTMCell/Linear/Bias'],
    kernel2: vars['RNN/MultiRNNCell/Cell1/BasicLSTMCell/Linear/Matrix'],
    bias2: vars['RNN/MultiRNNCell/Cell1/BasicLSTMCell/Linear/Bias'],
    kernel3: vars['RNN/MultiRNNCell/Cell2/BasicLSTMCell/Linear/Matrix'],
    bias3: vars['RNN/MultiRNNCell/Cell2/BasicLSTMCell/Linear/Bias'],
    fullyConnectedBiases: vars['fully_connected/biases'],
    fullyConnectedWeights: vars['fully_connected/weights']
  };
});

let reverb = new Tone.Convolver('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/hm2_000_ortf_48k.mp3').toMaster();
reverb.wet.value = 0.15;
let samplers = [
  {
    high: buildSampler('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/marimba-classic-').connect(
      new Tone.Panner(-0.4).connect(reverb)
    ),
    mid: buildSampler('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/marimba-classic-mid-').connect(
      new Tone.Panner(-0.4).connect(reverb)
    ),
    low: buildSampler('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/marimba-classic-low-').connect(
      new Tone.Panner(-0.4).connect(reverb)
    )
  },
  {
    high: buildSampler('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/xylophone-dark-').connect(
      new Tone.Panner(0.4).connect(reverb)
    ),
    mid: buildSampler('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/xylophone-dark-mid-').connect(
      new Tone.Panner(0.4).connect(reverb)
    ),
    low: buildSampler('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/xylophone-dark-low-').connect(
      new Tone.Panner(0.4).connect(reverb)
    )
  }
];

let sixteenth = Tone.Time('16n').toSeconds();
let quarter = Tone.Time('4n').toSeconds();
let temperature = tf.variable(tf.scalar(1.1));
let loadingIndicator = document.querySelector('#loading');
let generatingIndicator = document.querySelector('#generating');
let container = document.querySelector('#vis-elements');
let haloContainer = document.querySelector('#vis-halos');
let tonicLeftButtons = document.querySelectorAll('.tonic-left');
let tonicRightButtons = document.querySelectorAll('.tonic-right');
let chordLeftButtons = document.querySelectorAll('.chord-left');
let chordRightButtons = document.querySelectorAll('.chord-right');
let sequences = [];
let mouseDown = false;
let chordLeft = CHORD_INTERVALS['major'],
  chordRight = CHORD_INTERVALS['major'];
let tonicLeft = 0,
  tonicRight = 0;

function buildSampler(urlPrefix) {
  return new Tone.Sampler(
    _.fromPairs(
      SAMPLE_SCALE.map(n => [
        n,
        new Tone.Buffer(`${urlPrefix}${n.toLowerCase().replace('#', 's')}.mp3`)
      ])
    )
  );
}

function encodePitchChord(chord) {
  let oneHot = _.times(3 * 12 + 1, () => 0);
  if (chord === null) {
    oneHot[0] = 1;
    return oneHot;
  }
  let chordPcs = chord.map(n => n % 12);
  // Root pc
  oneHot[1 + chordPcs[0]] = 1;
  // Pitches
  for (let pc of chordPcs) {
    oneHot[1 + 12 + pc] = 1;
  }
  // Bass pc (=root)
  oneHot[1 + 12 + 12 + chordPcs[0]] = 1;
  return oneHot;
}

// Melodies encoded in a one-hot vector, where 0 = no event, 1 = note off, the rest are note ons.
function encodeMelodyIndex(note) {
  return note < 0 ? note + 2 : note - MIN_NOTE + 2;
}
function decodeMelodyIndex(index) {
  if (index - 2 < 0) {
    return index - 2;
  } else {
    return index - 2 + MIN_NOTE;
  }
}

function generateSeq(chord, startNotes, conditionSeq = []) {
  let seq = tf.tidy(() => {
    let forgetBias = tf.scalar(1.0);
    let state = [
      tf.zeros([1, lstm.bias1.shape[0] / 4]),
      tf.zeros([1, lstm.bias2.shape[0] / 4]),
      tf.zeros([1, lstm.bias3.shape[0] / 4])
    ];
    let output = [
      tf.zeros([1, lstm.bias1.shape[0] / 4]),
      tf.zeros([1, lstm.bias2.shape[0] / 4]),
      tf.zeros([1, lstm.bias3.shape[0] / 4])
    ];
    let lstm1 = tf.basicLSTMCell.bind(tf, forgetBias, lstm.kernel1, lstm.bias1);
    let lstm2 = tf.basicLSTMCell.bind(tf, forgetBias, lstm.kernel2, lstm.bias2);
    let lstm3 = tf.basicLSTMCell.bind(tf, forgetBias, lstm.kernel3, lstm.bias3);

    let seq = [tf.tensor1d([encodeMelodyIndex(startNotes[0])])];

    while (seq.length < SEQ_LENGTH) {
      let condChord;
      if (seq.length < conditionSeq.length && conditionSeq[seq.length] > 0) {
        condChord = chord.concat([conditionSeq[seq.length]]);
      } else {
        condChord = chord;
      }
      let pitchChord = tf.tensor1d(encodePitchChord(condChord));
      let input = pitchChord.concat(
        tf.oneHot(_.last(seq), MAX_NOTE - MIN_NOTE + 2).flatten()
      );

      let nextOutput = tf.multiRNNCell(
        [lstm1, lstm2, lstm3],
        input.as2D(1, -1),
        state,
        output
      );
      state = nextOutput[0];
      output = nextOutput[1];
      let outputH = output[2];
      let weightedResult = tf.matMul(outputH, lstm.fullyConnectedWeights);
      let logits = tf.add(weightedResult, lstm.fullyConnectedBiases);
      let softmax = tf.softmax(tf.div(logits.as1D(), temperature));
      if (seq.length >= startNotes.length) {
        seq.push(tf.multinomial(softmax, 1, null, true));
      } else {
        seq.push(tf.tensor1d([encodeMelodyIndex(startNotes[seq.length])]));
      }
    }

    return seq.map(s => s.asScalar());
  });
  return Promise.all(seq.map(s => s.data())).then(s =>
    s.map(decodeMelodyIndex)
  );
}

function toNoteSequence(seq) {
  let notes = [];
  for (let i = 0; i < seq.length; i++) {
    if (seq[i] === -1 && notes.length) {
      _.last(notes).quantizedEndStep = i;
    } else if (seq[i] > 0) {
      if (notes.length && !_.last(notes).quantizedEndStep) {
        _.last(notes).quantizedEndStep = i;
      }
      notes.push({ pitch: seq[i], quantizedStartStep: i });
    }
  }
  if (notes.length && !_.last(notes).quantizedEndStep) {
    _.last(notes).quantizedEndStep = SEQ_LENGTH;
  }
  return { notes };
}

function isValidNote(note, forgive = 0) {
  return note <= MAX_NOTE + forgive && note >= MIN_NOTE - forgive;
}

function octaveShift(chord) {
  let root = _.min(chord);
  let shift = MAX_NOTE - root > root - MIN_NOTE ? 12 : -12;
  let delta = 0;
  while (isValidNote(root + delta + shift)) {
    delta += shift;
  }
  return chord.map(n => n + delta).map(transposeIntoRange);
}

function transposeIntoRange(note) {
  while (note > MAX_NOTE) {
    note -= 12;
  }
  while (note < MIN_NOTE) {
    note += 12;
  }
  return note;
}

function mountChord(tonic, chord) {
  return chord.map(d => MIN_NOTE + tonic + d);
}

function restPad(note) {
  if (Math.random() < 0.6) {
    return [note, -2];
  } else if (Math.random() < 0.8) {
    return [note];
  } else {
    return [note, -2, -2];
  }
}

function playStep(time, step) {
  let notesToPlay = distributeNotesToPlay(collectNotesToPlay(step));
  for (let { delay, notes } of notesToPlay) {
    let voice = 0;
    let stepSamplers = _.shuffle(samplers);
    for (let { pitch, path, halo } of notes) {
      let freq = Tone.Frequency(pitch, 'midi');
      let playTime = time + delay + HUMANIZE_TIMING * Math.random();
      let velocity;
      if (delay === 0) velocity = 'high';
      else if (delay === sixteenth / 2) velocity = 'mid';
      else velocity = 'low';
      stepSamplers[voice++ % stepSamplers.length][velocity].triggerAttack(
        freq,
        playTime
      );
      Tone.Draw.schedule(() => animatePlay(path, halo), playTime);
    }
  }
}

function collectNotesToPlay(step) {
  let notesToPlay = [];
  for (let seq of sequences) {
    if (!seq.on) continue;
    if (seq.notes.has(step)) {
      notesToPlay.push(seq.notes.get(step));
    }
  }
  return _.shuffle(notesToPlay);
}

function distributeNotesToPlay(notes) {
  let subdivisions = [
    { delay: 0, notes: [] },
    { delay: sixteenth / 2, notes: [] },
    { delay: sixteenth, notes: [] },
    { delay: sixteenth * 3 / 2, notes: [] }
  ];
  if (notes.length) {
    subdivisions[0].notes.push(notes.pop());
  }
  if (notes.length) {
    subdivisions[2].notes.push(notes.pop());
  }
  while (notes.length && Math.random() < Math.min(notes.length, 6) / 10) {
    let rnd = Math.random();
    let subdivision;
    if (rnd < 0.4) {
      subdivision = 0;
    } else if (rnd < 0.6) {
      subdivision = 1;
    } else if (rnd < 0.8) {
      subdivision = 2;
    } else {
      subdivision = 3;
    }
    subdivisions[subdivision].notes.push(notes.pop());
  }
  return subdivisions;
}

function animatePlay(pathEl, haloEl) {
  pathEl.animate([{ fill: 'white' }, { fill: '#e91e63' }], {
    duration: quarter * 1000,
    easing: 'ease-out'
  });
  haloEl.animate([{ opacity: 1 }, { opacity: 0 }], {
    duration: quarter * 1000,
    easing: 'ease-out'
  });
}

function toggleSeq(seqObj) {
  if (seqObj.on) {
    seqObj.on = false;
    seqObj.group.setAttribute('class', '');
  } else {
    seqObj.on = true;
    seqObj.group.setAttribute('class', 'on');
  }
}

function toggleHover(seqObj, on) {
  let cls = seqObj.group.getAttribute('class') || '';
  if (on && cls.indexOf('hover') < 0) {
    seqObj.group.setAttribute('class', cls + ' hover');
  } else if (!on && cls.indexOf('hover') >= 0) {
    seqObj.group.setAttribute('class', cls.replace('hover', ''));
  }
}

function buildSlice(centerX, centerY, startAngle, endAngle, radius) {
  let startX = centerX + Math.cos(startAngle) * radius;
  let startY = centerY + Math.sin(startAngle) * radius;
  let endX = centerX + Math.cos(endAngle) * radius;
  let endY = centerY + Math.sin(endAngle) * radius;
  let pathString = `M ${centerX} ${centerY} L ${startX} ${startY} A ${radius} ${radius} 0 0 1 ${endX} ${endY} Z`;
  let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  path.setAttribute('d', pathString);
  path.setAttribute('style', `transform-origin: ${centerX}px ${centerY}px`);
  return path;
}

function generateSpace() {
  let previouslyOn = _.fromPairs(sequences.map((s, idx) => [idx, s.on]));
  let chords = [
    octaveShift(mountChord(tonicLeft, chordLeft)),
    mountChord(tonicLeft, chordLeft),
    octaveShift(mountChord(tonicRight, chordRight)),
    mountChord(tonicRight, chordRight)
  ];
  let seed1 = _.flatMap(_.shuffle(chords[0]), restPad);
  let seed2 = _.flatMap(_.shuffle(chords[1]), restPad);
  let seed3 = _.flatMap(_.shuffle(chords[2]), restPad);
  let seed4 = _.flatMap(_.shuffle(chords[3]), restPad);
  return Promise.all([
    generateSeq(chords[0], seed1),
    generateSeq(chords[1], seed2),
    generateSeq(chords[2], seed3),
    generateSeq(chords[3], seed4)
  ])
    .then(seqs => seqs.map(toNoteSequence))
    .then(noteSeqs => mvae.interpolate(noteSeqs, N_INTERPOLATIONS))
    .then(res => {
      while (container.firstChild) {
        container.firstChild.remove();
      }
      while (haloContainer.firstChild) {
        haloContainer.firstChild.remove();
      }
      let cellSize = 1000 / N_INTERPOLATIONS;
      let margin = cellSize / 30;

      sequences = res.map((noteSeq, idx) => {
        let row = Math.floor(idx / N_INTERPOLATIONS);
        let col = idx - row * N_INTERPOLATIONS;
        let centerX = (col + 0.5) * cellSize + margin;
        let centerY = (row + 0.5) * cellSize + margin;
        let maxInterval = MAX_NOTE;
        let maxRadius = cellSize / 2 - 2 * margin;

        let group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        if (previouslyOn[idx]) {
          group.setAttribute('class', 'on');
        }
        group.style.transformOrigin = `${centerX}px ${centerY}px`;
        group.style.transform = 'scale(0)';
        group.animate([{ transform: 'scale(0)' }, { transform: 'scale(1)' }], {
          duration: 200,
          delay:
            (N_INTERPOLATIONS / 2 - Math.abs(row - N_INTERPOLATIONS / 2)) * 25 +
            (N_INTERPOLATIONS / 2 - Math.abs(col - N_INTERPOLATIONS / 2)) * 25,
          fill: 'forwards'
        });
        container.appendChild(group);

        let halo = document.createElementNS(
          'http://www.w3.org/2000/svg',
          'circle'
        );
        halo.setAttribute('class', 'halo');
        halo.setAttribute('fill', 'url(#halo');
        halo.setAttribute('cx', centerX);
        halo.setAttribute('cy', centerY);
        halo.setAttribute('r', maxRadius + 2);
        haloContainer.appendChild(halo);

        let notes = new Map();
        for ({ pitch, quantizedStartStep, quantizedEndStep } of noteSeq.notes) {
          if (!isValidNote(pitch, 4)) {
            continue;
          }
          let relPitch = (maxInterval - (pitch - MIN_NOTE)) / maxInterval;
          let radius = relPitch * maxRadius;
          let startAngle = quantizedStartStep / SEQ_LENGTH * Math.PI * 2;
          let endAngle = quantizedEndStep / SEQ_LENGTH * Math.PI * 2;

          let path = buildSlice(centerX, centerY, startAngle, endAngle, radius);
          path.setAttribute('class', 'note');
          group.appendChild(path);
          notes.set(quantizedStartStep, {
            pitch,
            path,
            halo
          });
        }

        let pointerArea = document.createElementNS(
          'http://www.w3.org/2000/svg',
          'rect'
        );
        pointerArea.setAttribute('x', col * cellSize);
        pointerArea.setAttribute('y', row * cellSize);
        pointerArea.setAttribute('width', cellSize);
        pointerArea.setAttribute('height', cellSize);
        pointerArea.setAttribute('class', 'pointer-area');
        group.appendChild(pointerArea);

        let seqObj = { notes, group, on: previouslyOn[idx] };

        pointerArea.addEventListener('mousedown', () => toggleSeq(seqObj));
        pointerArea.addEventListener('mouseover', () => {
          toggleHover(seqObj, true);
          mouseDown && toggleSeq(seqObj);
        });
        pointerArea.addEventListener('mouseout', () =>
          toggleHover(seqObj, false)
        );
        return seqObj;
      });
    });
}

function regenerateSpace() {
  // Pause Tone timeline while regenerating so events don't pile up if it's laggy.
  Tone.Transport.pause();
  generatingIndicator.style.display = 'flex';
  setTimeout(() => {
    generateSpace().then(() => {
      generatingIndicator.style.display = 'none';
      setTimeout(() => Tone.Transport.start(), 0);
    });
  }, 0);
}

Promise.all([
  rnnLoadPromise,
  mvae.initialize(),
  new Promise(res => Tone.Buffer.on('load', res))
])
  .then(generateSpace)
  .then(() => (loadingIndicator.style.display = 'none'))
  .then(() => {
    new Tone.Sequence(playStep, _.range(SEQ_LENGTH), '16n').start();
    Tone.Transport.start();
  });

document.documentElement.addEventListener(
  'mousedown',
  () => (mouseDown = true)
);
document.documentElement.addEventListener('mouseup', () => (mouseDown = false));

tonicLeftButtons.forEach(el =>
  el.addEventListener('click', evt => {
    tonicLeft = +evt.target.dataset.tonic;
    tonicLeftButtons.forEach(b =>
      b.classList.toggle('active', b === evt.target)
    );
    regenerateSpace();
  })
);
tonicRightButtons.forEach(el =>
  el.addEventListener('click', evt => {
    tonicRight = +evt.target.dataset.tonic;
    tonicRightButtons.forEach(b =>
      b.classList.toggle('active', b === evt.target)
    );
    regenerateSpace();
  })
);
chordLeftButtons.forEach(el =>
  el.addEventListener('click', evt => {
    chordLeft = CHORD_INTERVALS[evt.target.dataset.chord];
    chordLeftButtons.forEach(b =>
      b.classList.toggle('active', b === evt.target)
    );
    regenerateSpace();
  })
);
chordRightButtons.forEach(el =>
  el.addEventListener('click', evt => {
    chordRight = CHORD_INTERVALS[evt.target.dataset.chord];
    chordRightButtons.forEach(b =>
      b.classList.toggle('active', b === evt.target)
    );
    regenerateSpace();
  })
);

StartAudioContext(Tone.context, container);

</script>
</body>
</html>