TL;DR
- Web Audio APIとReactを使ってブラウザ上で動くリズムシーケンサを作ってみた
- Chrome, Safariで動作確認済み。Edgeも何とか動きます
- ランダムにリズム生成したり、「跳ね具合」をいじったりして遊んだり
- Web Audio API面白いですね
WebAudioAPIとは
こちらの記事をご参照下さい。
[Web Audio API の基礎 - HTML5 Rocks]
(https://www.html5rocks.com/ja/tutorials/webaudio/intro/)
Web Audio API - Web API インターフェイス | MDN
Web Audio APIは音声を処理・合成するためのWebアプリケーション向けのハイレベルなJavaScript APIです。 このAPIの目指すところは、今日のゲームのオーディオエンジンが備えている機能や、DAWに見られるようなミキシング、編集、フィルタリング等の機能を実現することです。
けっこう前からあるようですが、最近知りました。
オシレータを使った音響合成、サンプルを読み込んでの発音、エフェクト処理など、Webページ上で音を扱うための強力なjavascript APIです。
サンプルを鳴らすまで
AudioContextの初期化
//audio context initialization
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = new AudioContext();
AudioContextオブジェクトがすべての長です。まだブラウザによってはベンダープレフィクスが必要なため、上のように判定を入れつつ初期化しておくと安全です。
サウンドファイルのロード
サウンドファイルをXmlHttpRequestで取得→デコード→AudioBufferに格納。
var audioBuffer = null;
var request = new XMLHttpRequest();
request.open("GET", '../sounds/your-sound.wav', true);
request.responseType = "arraybuffer";
request.onload = function() {
audioContext.decodeAudioData(
request.response,
function(buffer) {
audioBuffer = buffer;
},
function(error) {
console.error('decodeAudioData error', error);
}
);
}
request.send();
コールバックとか書くのがめんどいのと、よく使う処理なので、実際には共通化しましょう。BufferLoaderなどの名前でクラスを作るか、もしくはwebpackなどを使うなら既存のモジュールをinstallしてしまってもいいですね。今回は使わなかったけど、webaudio-buffer-loaderとかいいかも。
鳴らす
AudioBufferSourceNodeを生成し、鳴らします。
var source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start(0);
この時点で、ロードしたwavがWebページ上で鳴ります。夢が膨らみますね。
ビートを鳴らすまで
ここが最初の難関でした。
今回はあくまでビートとして鳴らしたいので、BPMに基づいた間隔でもってBufferを再生し続ける仕組みが必要です。最初はsetInterval()
とか駆使するのかな?と思ってましたが、そう単純でもありませんでした。この辺のお話は以下の記事に詳しく書いてあります。
2 つの時計のお話 - Web Audio の正確なスケジューリングについて
window.setTimeout() もしくは window.setInterval() で登録したコールバック関数の呼び出しタイミングに関しては、数10ミリ秒かそれ以上のズレが簡単に生じ得ます。
メインスレッドは簡単に数ミリ秒遅延するので、JavaScriptのsetTimeoutを直接オーディオ再生イベントに使用するのは得策とは言えません
つまりsetInterval()
だけでビートを制御した場合、ウインドウリサイズしまくって再描画走らせたりとかすると簡単にリズムが狂ってしまうわけですね。つらい。
解決策としては、「協調スケジューリング」というテクニックを使います。
setTimeout のタイマーが……Web Audioのスケジューリングを行うことで、個々の音を設定しています。setTimeout のタイマーは現在のテンポを基準に、「今すぐ」スケジューリングされるべき音が存在するかチェックします。
要約すれば、「setTimeoutのタイマーによって、『次の音を正確にスケジューリングする』メソッドを定期実行する」という寸法です。実装はここでは割愛しますが、興味のある方はぜひ先の記事をご覧ください。実装例も見れますし、Workerの使用やスケジューリング時間のチューニングなど興味深い議論もあります。
そしてReact.jsへ
ちなみに、モダンなフロントエンド開発については全くの素人でして、Reactに関しては公式のチュートリアルをこなしたくらいです。で案の定、環境構築する際に躓きまくりました。けど使ってみたかったんです。React公式とQiita, StackOverflowには多大なる感謝を捧げます。
技術面で語れることは多くないし、記事もすでに巷にあふれてますので、とりあえず今回はコンポーネントの構造だけ。
class Sequencer extends React.Component {
constructor() {
super();
this.state = {
tracks: [
{name:"hihat-open",
steps: [null,null,null,null,null,null,null,null,null,null,'■',null,null,null,null,null]},
{name:"hihat-close",
steps: ['■','■','■',null,'■',null,'■',null,'■',null,null,null,'■',null,'■',null]},
{name:"snare",
steps: [null,null,null,null,'■',null,null,null,null,null,null,null,'■',null,null,'■']},
{name:"kick",
steps: ['■',null,null,null,null,null,null,'■',null,'■','■',null,null,'■',null,null]},
],
bpm: 100,
isPlaying: false,
idxCurrent16thNote: 0,
startTime: 0.0,
nextNoteTime: 0.0,
swing: 0
};
timerWorker.onmessage = function(e) {
if(e.data=="tick"){
this.schedule();
}
}.bind(this);
}
render() {
return (
<div className="sequencer">
<div className="area-tracks">
{Array(4).fill().map((x,i) =>
<Track
key={i}
name={this.state.tracks[i].name}
squares={this.state.tracks[i].steps}
handler={(idx)=>this.toggleStep(i, idx)}
/>
)
}
<LEDLine
isPlaying={this.state.isPlaying}
idxCurrent16thNote={this.state.idxCurrent16thNote}
/>
</div>
<hr />
<div className="area-play">
<button className="button-play" onClick={()=>this.togglePlayButton()}>
{this.state.isPlaying ? '■STOP' : '▶PLAY!'}
</button>
</div>
<div className="area-shuffle">
<button className="button-shuffle" onClick={()=>this.shuffleNotes()}>SHUFFLE</button>
</div>
<div className="area-bpm">
<span className="label-bpm">[bpm]</span>
<div style={{display: 'inline-block', width: '200px'}}>
<Slider min={40} max={250} step={1}
editable pinned value={this.state.bpm} onChange={this.handleSliderChange.bind(this, 'bpm')}/>
</div>
</div>
<div className="area-swing">
<span className="label-swing">[swing]</span>
<div style={{display: 'inline-block', width: '200px'}}>
<Slider min={0} max={100} step={1}
editable pinned value={this.state.swing} onChange={this.handleSliderChange.bind(this, 'swing')}/>
</div>
</div>
</div>
);
}
Sequencerが子コンポーネントとしてTrack4つとLEDLineを持ち、Stateにはbpmや各トラックの音の選択状況などが保持されています。レイアウトはこんな感じ。
使ってみて、機能を追加したいとか、トラックを1つ増やしたいとか、思いついたときにすぐ実装できるのがReactの素晴らしいところだなと実感しました。今回の例だと、親コンポーネントのSequencerが大体のことを管理してるので修正が概ねこの中だけで済みました。演奏中の音番号をStateに持たせることで動的なクラス付与が簡単にできたりとかも、かなり満足度が高かったです。
遊び方
- [▶PLAY!]を押すと再生。[■STOP]を押すと停止
- [SHUFFLE]を押すと音をランダムに再配置
- bpmをいじると速さが変わる。再生中でもOK。
- swingをいじるとリズムが跳ねる。再生中でもOK。0~100で連続的に変化させられます。
まとめ
APIとしてはごく一部の機能しか使っていないのですが、それでもかなり面白い。
音楽の覚えがあるエンジニアって結構いると思うので、Web Audio API関連の記事がもっと増えたらいいなと思いました。