Open9

Web Audio APIでシンセサイザーを作ろうとした

tkktkk

Web Audio API

MDNによくできた解説記事があります。

https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API

ウェブオーディオ API はウェブ上で音声を扱うための強力で多機能なシステムを提供します。これにより開発者は音源を選択したり、エフェクトを加えたり、視覚効果を加えたり、パンニングなどの特殊効果を適用したり、他にもたくさんのいろいろなことができるようになります。

tkktkk

準備

Javascriptが扱えればいいので、svelte上でいろいろ試行錯誤します。

https://svelte.jp/docs#getting-started

npm create svelte@latest myapp
cd myapp
npm install
npm run dev

CloudFlare workersでホストする

Adapterを導入する。

npm i -D @sveltejs/adapter-cloudflare-workers
diff --git a/svelte.config.js b/svelte.config.js
index 5627353..7256079 100644
--- a/svelte.config.js
+++ b/svelte.config.js
@@ -1,4 +1,4 @@
-import adapter from "@sveltejs/adapter-auto";
+import adapter from "@sveltejs/adapter-cloudflare-workers";
 import { vitePreprocess } from "@sveltejs/kit/vite";

 /** @type {import('@sveltejs/kit').Config} */

デプロイは wrangler publish で簡単。

tkktkk

Web Audio APIチュートリアル

MDNのチュートリアルを参考に作ります。

https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API/Using_Web_Audio_API

音楽ファイルを再生する

https://svelte-synth.tkk.workers.dev/basic

template

  <button class="on-button" class:selected={enable} on:click={onClick}>
    On
  </button>
  <h1>Parameters</h1>
  <Slider min={0} max={1} value={gain} step={0.01} on:on-change={onVolChange}
    ><h2>Master volume</h2></Slider
  >

script

  import { onMount, onDestroy } from "svelte";

  let enable = false;
  let gain = 0.2;

  // このあたりはwindowオブジェクトが欲しいのでonMountであとから代入する
  let audioElement: HTMLAudioElement | null;
  let AudioContext;
  let audioCtx: AudioContext;
  let ampNode: GainNode;
  let track;

  const onClick = (e: MouseEvent) => {
    enable = !enable;
    if (audioCtx.state === "suspended") {
      audioCtx.resume();
    }

    // 再生・停止
    if (audioElement) {
      if (enable) {
        audioElement.play();
      } else {
        audioElement.pause();
      }
    }
  };

  const onVolChange = (e: any) => {
    console.debug(e.detail);
    ampNode.gain.value = e.detail.valueAsNumber;
    gain = e.detail.valueAsNumber;
  };

  const onAudioEnded = (e: any) => {
    console.debug("ended");
    enable = false;
  };

  onMount(async () => {
    // Initialize audio
    audioElement = document.querySelector("audio");
    AudioContext = window.AudioContext;
    audioCtx = new AudioContext();

    ampNode = audioCtx.createGain();
    ampNode.gain.value = gain;
    if (audioElement) {
      track = audioCtx.createMediaElementSource(audioElement);
      track.connect(ampNode).connect(audioCtx.destination);
    }
  });

  onDestroy(() => {
    track = null;
  });
tkktkk

基本概念

Basic concepts behind Web Audio APIではInput, Effects, Destinationの3つで解説されていましたが、APIを触っているとNodeという単語も多く出てくるなと思いました。

gainNodeやwaveShaperNodeなど入力されたものに影響を与えるNodeもたくさんあるし、oscillatorNodeなど自分がSourceのようなものになれるNodeもありました。

図を描いてからNodeはInputにもなり得るなと思ったのでちょっと違うなとも思います。

Nodeをどんどんconnectしていくので、楽器にエフェクターをつないでいくようだと自分は感じました。

tkktkk

シンプルなシンセサイザーのようなもの

filterNodeのパラメータをスライダーで制御できるようにするとそれっぽくなりますね。

template

<section>
  <button class="on-button" class:selected={enable} on:click={onClick}>
    On
  </button>
  <h1>Parameters</h1>
  <Slider min={0} max={1} value={gain} step={0.01} on:on-change={onVolChange}
    ><h2>Master volume</h2></Slider
  >
  <Slider
    min={20}
    max={10_000}
    value={filterFreq}
    step={0.01}
    on:on-change={onFilterFreq}
    ><h2>Filter frequency</h2>
    <p>{filterFreq} Hz</p></Slider
  >
  <Slider
    min={0}
    max={100}
    value={filterQ}
    step={1}
    on:on-change={onFilterQ}
    ><h2>Filter Q</h2>
    <p>{filterQ}</p></Slider
  >
  <select bind:value={waveform} on:change={onChangeWaveform}>
    {#each waveforms as waveform}
      <option value={waveform}>
        {waveform}
      </option>
    {/each}
  </select>
</section>

script

   import { onMount, onDestroy } from "svelte";
  import Slider from "../Slider.svelte";

  let enable = false;
  let gain = 0.1;
  // フィルター周波数
  let filterFreq = 1000;
  let filterQ = 10;
  // 波形
  let waveform: OscillatorType = "sawtooth";
  let waveforms: OscillatorType[] = ["sawtooth", "sine", "square", "triangle"];

  let AudioContext;
  let audioCtx: AudioContext;
  let gainNode: GainNode;
  let ampNode: GainNode;
  let osc1: OscillatorNode;
  let filterNode: BiquadFilterNode;

  const onClick = (e: MouseEvent) => {
    enable = !enable;
    if (audioCtx.state === "suspended") {
      audioCtx.resume();
    }

    // osc.stop()するとその後start()するとエラーが発生してしまうため、gainで制御した
    if (enable) {
      ampNode.gain.value = 1;
    } else {
      ampNode.gain.value = 0;
    }
  };

  const onVolChange = (e: any) => {
    gainNode.gain.value = e.detail.valueAsNumber;
    gain = e.detail.valueAsNumber;
  };

  const onFilterFreq = (e: any) => {
    filterNode.frequency.setValueAtTime(
      e.detail.valueAsNumber,
      audioCtx.currentTime
    );
    filterFreq = e.detail.valueAsNumber;
  };
  
  const onFilterQ = (e: any) => {
    filterNode.Q.value = e.detail.valueAsNumber;
    filterQ = e.detail.valueAsNumber;
  }

  const onChangeWaveform = () => {
    [osc1].forEach((osc) => {
      osc.type = waveform;
    });
  };

  onMount(async () => {
    // Initialize audio
    AudioContext = window.AudioContext;
    audioCtx = new AudioContext();

    // Setup osc1
    osc1 = audioCtx.createOscillator();
    osc1.type = waveform;
    osc1.frequency.setValueAtTime(440, audioCtx.currentTime);

    gainNode = audioCtx.createGain();
    gainNode.gain.value = gain;
    ampNode = audioCtx.createGain();
    ampNode.gain.value = 0;
    filterNode = audioCtx.createBiquadFilter();
    filterNode.Q.value = filterQ;
    filterNode.frequency.setValueAtTime(1_000, audioCtx.currentTime);
    // 繋ぐ
    osc1
      .connect(gainNode)
      .connect(filterNode)
      .connect(ampNode)
      .connect(audioCtx.destination);
    [osc1].forEach((osc) => {
      osc.start();
    });
  });

  onDestroy(() => {
    [osc1].forEach((osc) => {
      if (osc) {
        osc.stop();
      }
    });
  });
tkktkk

それっぽくなるようなところまで作りました。

https://svelte-synth.tkk.workers.dev/

やったこと

  • SuperSawを鳴らすために、オシレータの追加・削除とDetuneの実装
    • oscの状態管理
  • エンベロープ(Attack, Decay, Hold, Sustain, Release)
    • setTimeoutと再帰を駆使してごり押しでAttack, Hold, Releaseまでは実装しました
  • 音階

LFOも実装できればだいぶ音作りの幅は広がりますが、今日はここで力尽きました。

tkktkk

エンベロープの実装が力業すぎて、音を作ろうと思うといつもと逆の作用するのが気になる...

tkktkk

最終的にはこんな感じの構成になりました。