LoginSignup
31
26

More than 5 years have passed since last update.

Web Audio API+Reactでブラウザで動くシーケンサを作る

Last updated at Posted at 2017-06-02

TL;DR

WebAudioAPIとは

こちらの記事をご参照下さい。
Web Audio API の基礎 - HTML5 Rocks
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には多大なる感謝を捧げます。

技術面で語れることは多くないし、記事もすでに巷にあふれてますので、とりあえず今回はコンポーネントの構造だけ。

rhythm-sequencer.jsx
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や各トラックの音の選択状況などが保持されています。レイアウトはこんな感じ。
sequncer.jpg

使ってみて、機能を追加したいとか、トラックを1つ増やしたいとか、思いついたときにすぐ実装できるのがReactの素晴らしいところだなと実感しました。今回の例だと、親コンポーネントのSequencerが大体のことを管理してるので修正が概ねこの中だけで済みました。演奏中の音番号をStateに持たせることで動的なクラス付与が簡単にできたりとかも、かなり満足度が高かったです。

遊び方

  • [▶PLAY!]を押すと再生。[■STOP]を押すと停止
  • [SHUFFLE]を押すと音をランダムに再配置
  • bpmをいじると速さが変わる。再生中でもOK。
  • swingをいじるとリズムが跳ねる。再生中でもOK。0~100で連続的に変化させられます。

ソースはこちら
動くやつはこちら

まとめ

APIとしてはごく一部の機能しか使っていないのですが、それでもかなり面白い。
音楽の覚えがあるエンジニアって結構いると思うので、Web Audio API関連の記事がもっと増えたらいいなと思いました。

31
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
26