Open9
Web Audio APIでシンセサイザーを作ろうとした
Web Audio API
MDNによくできた解説記事があります。
ウェブオーディオ API はウェブ上で音声を扱うための強力で多機能なシステムを提供します。これにより開発者は音源を選択したり、エフェクトを加えたり、視覚効果を加えたり、パンニングなどの特殊効果を適用したり、他にもたくさんのいろいろなことができるようになります。
準備
Javascriptが扱えればいいので、svelte上でいろいろ試行錯誤します。
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
で簡単。
Web Audio APIチュートリアル
MDNのチュートリアルを参考に作ります。
音楽ファイルを再生する
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;
});
基本概念
Basic concepts behind Web Audio APIではInput, Effects, Destinationの3つで解説されていましたが、APIを触っているとNodeという単語も多く出てくるなと思いました。
gainNodeやwaveShaperNodeなど入力されたものに影響を与えるNodeもたくさんあるし、oscillatorNodeなど自分がSourceのようなものになれるNodeもありました。
図を描いてからNodeはInputにもなり得るなと思ったのでちょっと違うなとも思います。
Nodeをどんどんconnectしていくので、楽器にエフェクターをつないでいくようだと自分は感じました。
シンプルなシンセサイザーのようなもの
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();
}
});
});
それっぽくなるようなところまで作りました。
やったこと
- SuperSawを鳴らすために、オシレータの追加・削除とDetuneの実装
- oscの状態管理
- エンベロープ(Attack, Decay, Hold, Sustain, Release)
- setTimeoutと再帰を駆使してごり押しでAttack, Hold, Releaseまでは実装しました
- 音階
LFOも実装できればだいぶ音作りの幅は広がりますが、今日はここで力尽きました。
エンベロープの実装が力業すぎて、音を作ろうと思うといつもと逆の作用するのが気になる...
ソースはこちらです。
最終的にはこんな感じの構成になりました。