文化祭で演劇をするにあたって、効果音ポン出しアプリを作っただけの話です。
作ったもの
LiveSFX(GitHub: https://github.com/MillaiStudio/LiveSFX)
iPad で効果音と BGM を出すためのアプリです。作り始めてから最初の公開までひと月半ほど、公開したあとも直し続けてなんやかんやでバージョンも1.9になりました。
なんで作ったか
iPad でまともに使える効果音ポン出しアプリが、シンプルになかった。見つかったアプリは大抵以下のような制限があって、演劇という目的に合わなかったので、作るという脳筋スタイルで行きました。
その制約は次のとおりです。
- 登録できる音数が少ない
- BGM を同時に扱いにくい
- プロジェクトファイルを共有できない
概要
開発するにあたってネイティブが一番良かったのですが、使える MacBook が Big Surで古すぎてシンプルに論外だったので、WSL 上でウェブアプリとして開発することにしました。たまたま Vue が書けたので、技術スタックは Vue、Pinia、TypeScriptを採用しました。
主な機能としては次のとおりです。
- パッドに効果音を割り当て
- 効果音のトリミング
- 効果音のグループ分け
- ミキサー
- WebRTC を用いた LAN 内の LiveSFX と接続(SideCar)
UI
構造とか
ProjectManager を中心にしていく設計です。前半は良かったのですが、後半に行くにつれて想定外レベルで肥大化し、応急的なクラス分割ばかりになり冗長な構造になってしまいました。
その他
演劇の会場は人が多い上に体育館なので、ネット品質が悪い可能性もあります。なのでオフライン対応はマストでした。
バックエンドというバックエンドはなく、Cloudflare Pages でばら撒いているだけになります。
機能の特徴
効果音は極限まで早く
効果音は読み込んだ時点で decodeAudioData して AudioBuffer にしておきます。タップされたら AudioBufferSourceNode を作って start() するだけ。ずっとメモリに展開されて多くなってくると重くはなりますが効果音レベルの長さのPCM音源の容量なんて知れてるので、タイミングをコントロールしやすい、遅延が少ないこの方式にしました。
BGM は効果音と別ルート
BGM は長い曲なので、効果音と同じように全部デコードしてしまうと流石に落ちてしまうのでHTMLAudioElement でストリームするようにしています。MediaElementAudioSourceNode で Web Audio 側につなぎ、音量だけミキサーで扱います。A / B 二つのデッキで間クロスフェードしたり次のBGMを用意しやすくしています。自己満です。
プロジェクトは1ファイル(.lvsf)
友人と演劇の音響担当をする関係でプロジェクトファイルを送るだけで再開できる仕組みが絶対に欲しかったです。今回は圧縮と解凍に手間をかけたくなかったとか色々な理由でバイナリ直書きのファイルになっています。
SideCarは基本LAN
一番自己満機能のつもりで実装したら想定以上に便利だった機能です。WebRTCのDataChannelで一台のホストと複数のビジターを接続し、ビジターからホストに音をならさせることができます。なので本来画面を切り替えないといけないBGM画面への侵入をスマホから扱えたりします。
もともと実装するならpeerJSを使おうと思っていたのですがオフラインの可能性がある場所で運用するアプリに外部依存のサービスは使いたくないと思い、iPhoneのテザリングでWebRTC使えたら実装しようと思ったら普通に接続できたので実装に踏み切りました。
詰まったところと、どうしたか
m4a が死んでいた
m4a が再生できない、ということがありました。端末が m4a に対応していないわけではなく、原因は逆で、アプリ側の対応表の問題でした。
当初の対応表は、拡張子ごとに MIME 文字列を1つだけ持っていました。m4a なら audio/mp4 だけで十分だろって思っていました。ところが実際のファイルの MIME が audio/x-m4a のような別名で来ると、対応表に無いので「知らない形式」として弾かれてエラーになっていました
対処として、拡張子ごとに MIME の候補を複数持ちどれか1つでも canPlayType() が通れば対応している扱いにしました。正直これは自分の実装よりも知識が甘かったです。
SideCar の切断に気づけない
SideCar で一番直し続けたのがここです。コミットメッセージにも「切断に safari は気づけていなかった」とそのまま残っています。切れたことに気づけないと、操作側は鳴っているつもり、ホスト側は止まっている、という一番まずい状態になります。
対処はいくつか重なっています。接続しているかどうかの判定が DataChannel の readyState だけだったのを、PeerConnection の connectionState もあわせて見るようにしたのが1つ。死活監視(watchdog)が張られていない接続経路があったので、それも直しました。
読み込みが遅い
.lvsf から音を取り出す処理が、1件ずつ順番に処理する作りになっていました。音が多いプロジェクトでも1番重い音の時間で終わるぐらいには早くなりました。
Safari が想定以上に鬼門だった話
iPad Safari 向けに開発するというのは前提だったので、FileSystem API など Chrome 前提の API は全部使えないということは最初から覚悟していました。が!!! 特に SideCar 周りで顕著だったのですが、イベントの発火タイミングが違いすぎて、iPad でデバッグした時にしか出ないバグが多すぎて大変でした。
パッドの並び替えができなかったのも、結局これでした。原因は Safari のイベント発火タイミングの違いです。パッドの順番は本番の動線そのものなので、並び替えられないのは困ります。結局これもイベント発火タイミングがサファリだけちゃうとかで原因究明に時間がかかりました
神のようなツール
iPad のデバッグのために Mac を開くのが面倒だったので、iPad Safari 向けの拡張機能で気合いのデバッグをしていましたが、console.log がうまく表示されないとか色々不都合があり、困っていたところ、
このようなツールを見つけました。結構革命でした。
おわりに
作ったものはここにあります

