Web Audio APIのOscillatorNodeを使用した簡易テストトーン発生器

楽器の簡易チューナーとして使えるかもしれません。

![ss.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618708/c8ea216f-d507-6ca0-85ed-b68ce1e24d57.jpeg)

#### [動作デモ](https://akebi.jp/temp/oscillator.html) ####

----

```html:oscillator.html
<!DOCTYPE html>
<html lang='ja'>
  <head>
    <meta charset='utf-8'>
    <meta name='viewport' content='width=device-width,initial-scale=1'>
    <title>Web Audio API OscillatorNode sample</title>
    <style>
      .unit {
        display: inline-block;
        border: 1px solid gray;
        border-radius: 5px;
        margin-top: 10px;
        padding: 8px;
        font-size: 12px;
      }
      button, input, select {
        background-color: #fff;
        color: #000;
        border: 1px solid #555;
      }
      button, select {
        border-radius: 5px;
        height: 25px;
      }
      option:checked {
        background-color: #484;
        color: #bfb;
      }
      .start {
        width: 100%;
      }
      input[type=range] {
        width: 220px;
      }
      .vt {
        margin-bottom: 8px;
      }
      .vf {
        width: 100%;
      }
      .vb {
        display: flex;
      }
    </style>
  </head>
  <body>
    <button class='add'>ADD Oscillator</button>
    <div class='container'>
    </div>
    <script>
      'use strict';
      const addButton = document.querySelector('.add');
      const container = document.querySelector('.container');
      const baseFreq = 440;

      addButton.addEventListener('click', () => addUnit());
      window.addEventListener('DOMContentLoaded', () => addUnit());

      document.addEventListener('click', e => {
        const t = e.target;
        const unit = t.closest('.unit');
        if (t.classList.contains('start')) {
          setAudioContext(unit);
        }
        if (t.classList.contains('delete')) {
          const id = unit.querySelector('.id').value;
          if (obj[id]) stop(obj, id);
          unit.remove();
        }
      });

      document.addEventListener('input', e => {
        const t = e.target;
        const unit = t.closest('.unit');
        const id = unit.querySelector('.id').value;
        const freq = unit.querySelector('.freq').value;
        const freq2 = unit.querySelector('.freq2').value;
        const tcl = t.classList;

        const f = freq * Math.pow(2, freq2 / 120);

        if (tcl.contains('freq') || tcl.contains('freq2')) {
          unit.querySelector('.freq_').textContent = f.toFixed(2);
          if (tcl.contains('freq2')) {
            unit.querySelector('.detune').textContent =
              (freq2 > 0 ? '+' : '') + (freq2 / 10).toFixed(2);
          }
        }
        if (tcl.contains('gain')) {
          unit.querySelector('.gv').textContent = (+unit.querySelector('.gain').value).toFixed(2);
        }

        if (!obj[id]) return;
        const a = obj[id].a;

        if (tcl.contains('type')) {
          a.osc.type = t.value;
        }
        if (tcl.contains('freq') || tcl.contains('freq2')) {
          a.osc.frequency.value = f;
        }
        if (tcl.contains('gain')) {
          a.gain.gain.value = t.value;
        }
      });

      const obj = {};
      const AudioContext = window.AudioContext || window.webkitAudioContext;

      const setAudioContext = e => {
        const id = e.querySelector('.id').value;

        e.style.backgroundColor = obj[id] ? '' : 'lightgreen';
        e.querySelector('.start').textContent = obj[id] ? 'START' : 'STOP';

        if (obj[id]) {
          stop(obj, id);
          return;
        }
        obj[id] = {
          type : e.querySelector('.type').value,
          freq : e.querySelector('.freq').value,
          freq2: e.querySelector('.freq2').value,
          gain : e.querySelector('.gain').value,
        };
        const o = obj[id];

        o.a = {};
        const a = o.a;

        a.context = new AudioContext();
        a.osc = a.context.createOscillator();
        a.osc.type = o.type;
        a.gain = a.context.createGain();
        a.osc.connect(a.gain);
        a.gain.connect(a.context.destination);
        a.osc.frequency.value = o.freq * Math.pow(2, o.freq2 / 120);
        a.gain.gain.value = o.gain;
        a.osc.start();
      };

      const stop = (o, id) => {
        o[id].a.osc.stop();
        o[id].a.osc.disconnect();
        o[id].a.context.close().then(() => delete o[id]);
      };

      const addUnit = () => {
        container.insertAdjacentHTML('beforeend', `
          <div class='unit'>
            <input type='hidden' class='id' value='u${Date.now()}'>
            <div class='vt'>
              Type: <select class='type'>
                <option value='sine'>Sine</option>
                <option value='square'>Square</option>
                <option value='sawtooth'>Sawtooth</option>
                <option value='triangle'>Triangle</option>
              </select>
            </div>
            <div class='vf'>
              Key: <select class='freq'></select>
              Freq: <span class='freq_'>${baseFreq.toFixed(2)}</span>Hz<br>
              Detune: <span class='detune'>0.00</span><br>
              <input type='range' class='freq2' value='0' min='-10' max='10' step='0.5'><br>
              Gain: <span class='gv'>0.20</span><br>
              <input type='range' class='gain' value='0.2' min='0' max='1' step='0.01'>
            </div>
            <br>
            <div class='vb'>
              <button class='start'>START</button>
              <button class='delete'>X</button>
            </div>
          </div>
        `);
        const keys = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
        const select = document.querySelector('.unit:last-of-type .freq');
        const l = keys.length;
        for (let o = 0; o <= 9; o++) keys.forEach((v, k) =>
          select.insertAdjacentHTML('beforeend', `
            <option value='${baseFreq * Math.pow(2, (o * l + k - 57) / l)}'>O${o}${v}</option>
          `));
        select.value = baseFreq;
      };
    </script>
  </body>
</html>
```