AI Dev Lab
PatchPad

PatchPad ができるまで — WebAudio でモジュラーシンセを組む

ブラウザで動くモジュラーシンセの開発ログ。 WebAudio でオシレーターからローパスフィルター、 ADSR、 LFO までをつなぎ、 14 鍵の仮想鍵盤で鳴らせるようにするまでの実装を記録しています。

·decision改善·stage公開中

PatchPad は、 WebAudio で動く ブラウザ モジュラー シンセ プレイグラウンド。 4 波形 OSC + ローパス フィルター + ADSR + LFO + 6 プリセット + 14-key 仮想鍵盤。 ラボ 29本目、 ジェネレーター ジャンル 3本目 (HueDeck = 配色、 TypeForge = 立体タイポ に続く)。

#なぜこの形にしたか

直近 5本は (doodle-drop: シミュレーター / side-tax: 計算 / atlas-quest: 学習 / hue-deck: ジェネレーター / astro-cast: データ可視化)。 直近 5本に シミュレーター/計算/学習/ジェネレーター/データ可視化 が各1。 避けるのは 3 本続いたときだけで、 ジェネレーター が hue-deck の 4本前のため再選択 OK。

候補比較:

  • ブラウザ シンセ (採用)
  • SVG モノグラム ジェネレーター (wow 薄い)
  • 暗算 / 漢字 学習 3本目 (SEO 強だが tech wow 薄い)
  • 動画 GIF レコーダー (doodle-drop の派生に見える)

選んだ理由:

  • wow 強い: 純ブラウザでリアルタイム フィルター + LFO 変調 + ポリフォニー = 「これの?」 と言わせる王道
  • WebAudio は ToneQuest で 1 回踏んでいる ので、 ノード接続 / 自動化 / Analyzer の知見が再利用できる
  • SNS / 教育文脈: 「シンセ 入門」 「ADSR とは」 系コンテンツ需要が継続
  • 思想: HueDeck (色) / TypeForge (字) と並ぶ 「ジェネレーター 3 兄弟」 で 「色 / 字 / 音」 が揃う

#見た目をどう決めたか

直近 5本の 見た目 を書き出す:

side-tax — ledger green + cream + stamp red (帳簿事務所)
atlas-quest — dark navy + amber departure (空港)
hue-deck — warm gray + ink + lipstick (Pantone)
astro-cast — midnight + 惑星 (天文台)
doodle-drop — blackboard green + chalk pastels (学校黒板)

題材 「シンセ / 機材」 → モチーフ 候補:

  • 70s モジュラー シンセ (Moog/ARP 風 walnut + brass) ← 採用
  • 80s シンセウェーブ (neon + magenta + cyan grid) — astro-cast の rainbow に近い
  • アナログ ミキサー (silver knobs) — neutral 過ぎ
  • ピアノ + クラシック (黒漆 + 真鍮) — 単音楽器寄り

採用 3 要素:

  • 配色: walnut wood #3d2914 + cream panel #ebd9a7 + brass #b78a36 + chrome knob #d8d3c3 + cable orange #e8632a / teal #1a8a8a / amber #f1c873 + LED readout (black + 蛍光オレンジ)
  • 直近 5本どれとも 配色 別。 walnut wood は唯一。
  • モチーフ: 70s モジュラー シンセ前面パネル — 木枠 + cream パネル + brass jacks + chrome knobs + LCD-like readout + 5 module sections (OSC / FILTER / ENV / LFO / OUT) + ホワイト鍵盤
  • typography: Space Grotesk 700/800 (機材ラベルの工業的見栄え) + JetBrains Mono (数値 readout + module label / ノブ値)

#技術スタック

#Signal chain: OSC → Filter → VCA → Master → Analyser

ts
const ctx = new AudioContext
const master = ctx.createGain
const analyser = ctx.createAnalyser
analyser.fftSize = 2048
master.connect(analyser)
analyser.connect(ctx.destination)

// LFO は filter.frequency を AudioParam 変調
const lfo = ctx.createOscillator
const lfoDepth = ctx.createGain
lfo.connect(lfoDepth)
lfo.start

ポイントは lfoDepth.connect(filter.frequency) の AudioParam 変調。 LFO の振幅 (gain) を 0〜3000 (Hz) に スケール して、 cutoff を周期的に揺らす。 これでアシッド / ワブル の音作りが成立。

#ADSR エンベロープ

ts
function noteOn(midi: number) {
 const now = ctx.currentTime
 const vca = ctx.createGain
 vca.gain.setValueAtTime(0, now)
 vca.gain.linearRampToValueAtTime(0.6, now + params.envAttack)
 vca.gain.linearRampToValueAtTime(
 0.6 * params.envSustain,
 now + params.envAttack + params.envDecay,
 )
 // release は noteOff で
}

function noteOff(v: Voice) {
 const now = ctx.currentTime
 const currentGain = v.vca.gain.value
 v.vca.gain.cancelScheduledValues(now)
 v.vca.gain.setValueAtTime(currentGain, now)
 v.vca.gain.linearRampToValueAtTime(0, now + params.envRelease)
 v.osc.stop(now + params.envRelease + 0.05)
}

AudioParam の linearRampToValueAtTime を使えば サンプル正確なエンベロープが scheduling できる。 Note-off 時は cancelScheduledValues で進行中の curve をキャンセル → 現在値から release。

#ノブの drag rotate

ts
function onPointerMove(e: PointerEvent) {
 if (!dragRef.current) return
 const dy = e.clientY - dragRef.current.startY
 const next = fromDelta(dragRef.current.startVal, dy)
 onChange(next)
}
// -135deg ~ 135deg、 240px ドラッグで full range
const toAngle = (v: number) => -135 + ((v - min) / (max - min)) * 270

Pointer Events + setPointerCapture で マウスがノブ外に出ても継続。 cutoff など log scale が欲しいパラメータは log: true フラグで Math.log 補間。

#QWERTY 鍵盤入力

ts
const KEYBOARD = [
 { midi: 60, label: "C4", black: false, qwerty: "a" },
 { midi: 61, label: "C#", black: true, qwerty: "w" },
 // ...
]

window.addEventListener("keydown", (e) => {
 if (e.repeat) return
 const k = KEYBOARD.find((kk) => kk.qwerty === e.key.toLowerCase)
 if (!k) return
 engine.noteOn(k.midi)
})

e.repeat で長押しの再発火を抑止 (= note-retrigger を防ぐ)。 piano roll に倣って 白鍵 = A S D F G H J K、 黒鍵 = W E T Y U。

#Polyphony

activeVoices: Map<midi, Voice> で 同時押し対応。 Note-off 時は該当 Voice の VCA を release → 一定時間後に OscillatorNode.stop + disconnect。 同 midi の再発音は既存 Voice を即時 release してから新規生成 (= mono-retrig)。

#まだやっていないこと

  • WebMIDI 対応 (navigator.requestMIDIAccess + MIDIInput → noteOn/noteOff)
  • WAV / WebM 録音 (MediaRecorder で master 出力を録音 → ダウンロード)
  • ステップ シーケンサー (16-step grid + tempo + 拍子)
  • エフェクト チェーン (ConvolverNode リバーブ / DelayNode ディレイ / WaveShaper ディストーション)
  • オシレーター 2 個目 (mix knob + 2nd OSC でデチューン感)
  • 共有 URL で パッチ を base64 シリアライズ (preset 共有)
  • タッチ 鍵盤の オクターブシフト (鍵盤を上下にスワイプ)
  • AudioWorklet 化 (現状は標準ノードのみ、 カスタム DSP は未使用)
  • Sub Bass / Acid Lead 系 50+ プリセット ライブラリ

[ ./next_action ]

読んだら、 PatchPad を実際に動かす。

この開発ログは PatchPad をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
AkinFind

AkinFind ができるまで — ブラウザ内で完結する embeddings 意味検索

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →