ASP.NET Core Blazor WebAssembly でメトロノームを作る その2(音鳴らす編)

  • C#
  • Blazor
  • Web Audio API

Part1から1ヶ月以上経ってしまいましたが、続きやっていきます。
今回はついに(といってもPart2ですが)音を鳴らしてみたいと思います。

ブラウザ上で音を鳴らすには<audio>要素か、Web Audio APIというのを使えばいいみたいですね。
とりあえず音を鳴らすだけなら<audio>要素を使った方法でよさげですが、
今回は作るのはメトロノームです。正確なリズムで音が鳴ってくれないと困ります。
なので、より高度な音声操作が出来そうなWeb Audio APIを使うことにしました。

JavaScriptで音を鳴らす関数を作る

それではまずJavaScriptでWeb Audio APIを使って、メトロノームのカチカチ音(クリック音)を鳴らす関数を作ります。
MDN Web Docsなどを参考にしています。

// ガード節などは省略しています。

/**
 * クリック音を鳴らす
 * @param {number} bpm BPM。
 * @param {boolean} emphasize 音を強調するかどうか。
 */
function playClick(bpm, emphasize)
{
    var audioContext = new AudioContext();
    var oscillator = audioContext.createOscillator();
    oscillator.type = 'square';
    // 強調するときは1オクターブ上の音を鳴らす
    oscillator.frequency.value = emphasize ? 880 : 440; // Hz
    oscillator.connect(audioContext.destination);
    oscillator.start();
    // 16分の長さが経過したら音を止める
    var noteDuration = (60.0 / bpm) * (4.0 / 16);
    oscillator.stop(audioContext.currentTime + noteDuration);
}

440Hzの音(ラの音)を、BPMに応じた16分音符の長さで鳴らすようにしました。
emphasizetrueが指定されていた場合は1オクターブ上の音を鳴らすようにしています。
これは拍の頭などでアクセントをつけるときに使います。

BlazorでJavaScriptの関数を呼び出す

さて、先ほど作ったJavaScript関数をBlazorで呼び出してみます。
Blazorでは JavaScript相互運用 という機能が用意されていて、
割と簡単にJavaScript関数を.NETから呼び出すことができるみたいですね。
Call JavaScript from .NETに呼び出す方法が記載されているので、この通りに実装してみます。

@page "/metronome"
@inject IJSRuntime JS;

<h3>Metronome</h3>

<div>
    <span id="click">@click</span>
</div>
<div>
    <input id="bpm" @bind="bpm" /><label id="bpm">BPM</label>
</div>
<div>
    <input id="beat" @bind="beat" /><label id="beat">拍子</label>
</div>

<div>
    <button class="btn btn-primary" @onclick="StartAsync">Start</button>
</div>
@code
{
    private int bpm = 100;
    private int beat = 4;
    private int click = 0;

    async void StartAsync()
    {
        // BPMから1拍の秒数を求める
        var interval = TimeSpan.FromSeconds(60.0 / this.bpm);

        while (true)
        {

            // 拍子の終わりまでインクリメント
            if (this.click++ >= this.beat)
            {
                // 頭に戻る
                this.click = 1;
            }
            StateHasChanged();

            // 音を鳴らす。拍の頭では音を強調する。
#pragma warning disable 4014
            JS.InvokeVoidAsync("playClick", this.bpm, this.click == 1);
#pragma warning restore 4014

            // 1拍分待機
            await Task.Delay(interval);
        }
    }
}

Part1と比較して、
@inject IJSRuntime JS;IJSRuntimeをインジェクトするコードと、
IJSRuntime.InvokeVoidAsync()メソッドでJavaScript関数を呼び出すコードが追加になっているだけです。

@injectに関しては、Dependency injectionを読めば大体分かるかと思います。

IJSRuntime.InvokeVoidAsync()メソッドはJavaScript関数を実行する関数で、
第一引数に呼び出したいJavaScript関数名を、第二引数にJavaScript関数の引数に渡す値のobject配列を指定します。
これは非同期メソッドのためawaitしないと警告が出ますが、
音が鳴り終わるのをいちいち待機していたらどんどんリズムがずれていく気がしたので、awaitせずに警告を無視しています。(リアルタイム性重視!)

実行

これで準備が整いました。
さっそく実行してみます。

音が鳴りました!成功ですね!
音が鳴るタイミングも拍の切り替わりと合っているのが確認出来ますね、これがWeb Audio APIの力...!

まとめ

音が鳴るようになって一気にメトロノーム感が出ましたね。
後はメトロノームを停止できるようにして、UIをイイ感じにすれば(ほぼ)完成ですが、
デザインとか考えるの苦手なのでどうしようか考え中です。。。