Strudelはビートのスケッチには最強、曲の完成には弱い — そしてlazy importするとエフェクトが静かに消える
アプリに組み込んで使った感想と、「次のmousedown」を待つオーディオ初期化の罠
Strudel(ブラウザでコードを書いて音楽を作るライブコーディングツール)は、ビートをその場でスケッチする道具としては今まで使った中で一番速い。そのかわり、曲を最後まで仕上げたりサービスに組み込んだりするのは弱い。そしてアプリにlazy importで入れると、エフェクトがエラーなしに抜ける罠がある。読み終えれば、Strudelをどこで使いどこで使わないか、アプリに組み込むとき何を避けるかがわかる。
なぜ使ってみたか
ThreadsでStrudelを使って140 BPMのビートをライブコーディングする動画を見た。自分でも試そうとデモアプリにStrudelのページを付け、ついでに学習用アプリまで作った。
s("bd(3,8), hh*8, ~ sd ~ sd").shape(0.4)
この一行で、キックを8マスに3回均等に散らし、ハイハットを8回、2拍目と4拍目にスネアを置き、音を少し歪ませる。音作り・拍の計算・予約が一行に全部入っている。
使ってみて強いところ、弱いところ
強いところははっきりしていた。インストール不要で、コードがURLに入るのでリンク一本で共有できる。bd(3,8)、<a b>、[x x] のような数文字で複雑なリズムが出る。演奏を止めずにコードを差し替えられる。ライブ演奏向けに設計されたTidalCycles系だからだ。
弱いところは、サイクル(繰り返し)中心であることから来る。イントロ → Aメロ → サビのような直線的な曲の構成は組みにくく、長いメロディや和声は音を並べるしかなくて冗長になった。ただ、曲構成用の機能や録音・書き出し、モバイルでの動作はまだきちんと試していない。ここまでは使ってみた感触だ。
アプリに入れるときに確かめた事実はこうだ。
@strudel/web1.3.0、ライセンスはAGPL-3.0-or-later(npmの表記)。公開サービスにバンドルするとき何が求められるかは、ここでは断定しない。使うならライセンスの解説を別に確認する本番ビルドでStrudelのチャンクは659KB(dynamic importで切り出したとき)
ドラムのサンプルは再生時にraw.githubusercontent.comから取ってくる。オフラインでは動かず、初回再生が遅い
| やりたいこと | 選ぶもの |
|---|---|
| ビートのスケッチ、リズムの実験、ライブ | Strudel |
| 曲の完成、ミックス・マスタリング | DAW(Ableton、Logicなど) |
| アプリに効果音・インタラクティブな音楽 | Tone.jsかWeb Audioを直接 |
罠 — lazy importするとエフェクトが静かに抜ける
バンドルが大きいので「再生ボタンを最初に押したときにimport」にした。再生はできたし、画面にエラーも出なかった。ところがコンソールには1秒ちょっとの間隔でこれが出続けていた。
[getTrigger] error: Failed to construct 'AudioWorkletNode': AudioWorkletNode cannot be created:
AudioWorklet does not have a valid AudioWorkletGlobalScope. Load a script via audioWorklet.addModule() first.
initStrudel() は中で initAudioOnFirstClick() を呼ぶ。ソース(superdough)を追うと名前のとおりで、document にmousedownリスナーを付け、次のmousedownでAudioWorkletを読み込む。
順に見るとこうなる。ユーザーが再生ボタンをmousedownした時点では、Strudelはまだない。clickでimportして initStrudel() を呼ぶと、ようやくリスナーが付く。そのmousedownはもう過ぎている。サンプルは鳴るが、shape のようにAudioWorkletを使うノードは毎回生成に失敗して、その音だけが抜ける。ユーザーがどこかをもう一度クリックすると、そこで読み込まれる。だから「たまに効いてたまに効かない」バグに見えた。
直し方は、取りこぼしたmousedownを自分で発火させることだ。
const strudel = await import('@strudel/web')
const ready = strudel.initStrudel()
document.dispatchEvent(new MouseEvent('mousedown')) // クリックはもう済んでいるので今発火
await Promise.all([ready, strudel.initAudioOnFirstClick()])
await strudel.evaluate('s("bd*4").shape(0.4)')
ローカルのChrome(2026-09-19)で、直す前は getTrigger エラーが出続け、[superdough] AudioWorklets loaded のログがなかった。直した後は初回再生で AudioWorklets loaded が出て、getTrigger エラーは0になった。
もうひとつ紛らわしかったことがある。Viteの開発サーバーでは、最初のdynamic importのときに依存関係の最適化でページが一度リロードされる。最初のクリックが「効かなかった」ように見える原因がもうひとつあったわけだ。
dispatchEvent で作ったイベントは isTrusted = false なのに音が生き返る理由は、まだ整理できていない。クリックハンドラの中なのでユーザーアクティベーションが残っているからだろうと見ている。shape 以外にAudioWorkletを使う関数が何かも、まだ一覧で確かめていない。
音楽・オーディオのライブラリをWebサービスに組み込んでいて詰まったら、negabaroに聞いてみてほしい。上の罠は、うちのアプリに入れるときに実際に踏んだものだ。
キーポイント
Strudelはループのスケッチとライブ用。曲の完成はDAW、アプリの効果音はTone.js・Web Audio
組み込む前にバンドルサイズ(約659KB)・AGPL・サンプルのネットワーク依存を確認する
initStrudelは「次のmousedown」でAudioWorkletを読み込む
クリック後にimportしたならdocument.dispatchEvent(new MouseEvent("mousedown"))で自分で発火