C#だけで矩形波・三角波の音を生成して.wavファイルに書き込めるライブラリ "SoundMaker" を Blazor WebAssembly 上で実行してみた

## ライブラリ "SoundMaker"

先月、Twitter で面白そうな投稿を見つけました。

https://twitter.com/DTB_AutumnSky/status/1588806133275594752

「C# だけで矩形波・三角波の音を生成して.wavファイルに書き込めるライブラリ」、"SoundMaker" だそうです (GitHub リポジトリは下記リンク先)。

https://github.com/AutumnSky1010/SoundMaker

ライセンスは MIT ライセンスだそうです。

"C# だけで実装できる" ということは、**この "SoundMaker" は Blazor WebAssembly を使って Web ブラウザ上でも動作するのではないか?** と興味が湧きまして、実際にやってみました。

## 実装してみる

前述の GitHub リポジトリの README に **[簡単な実装例が載っていました](https://github.com/AutumnSky1010/SoundMaker#%E7%B0%A1%E5%8D%98%E3%81%AA%E4%BD%BF%E3%81%84%E6%96%B9)** ので、それをまるまる、新規に作成した Blazor WebAssembly プロジェクト内の Razor コンポーネントファイル (.razor) 中、`@code { ... }` コードブロック内に、ほぼほぼ**そのまんまコピペ**しました。

ざっくりこんな感じ。

```csharp:App.razor
...
@code {
  private static string GenerateWavFile()
  {
    // サウンドの形式を作成する。
    var soundFormat = new SoundFormat(SoundMaker.Sounds.SamplingFrequencyType.FourtyEightKHz, SoundMaker.Sounds.BitRateType.SixteenBit, SoundMaker.Sounds.ChannelType.Stereo);
    var wave = MakeStereoWave(soundFormat);

    // ファイルに書き込む。
    var sound = new SoundWaveChunk(wave.GetBytes(soundFormat.BitRate));
    var waveFileFormat = new FormatChunk(SoundMaker.WaveFile.SamplingFrequencyType.FourtyEightKHz, SoundMaker.WaveFile.BitRateType.SixteenBit, SoundMaker.WaveFile.ChannelType.Stereo);
    var writer = new WaveWriter(waveFileFormat, sound);
    var filePath = "sample.wav";
    writer.Write(filePath);

    return filePath;
  }

  private static StereoWave MakeStereoWave(SoundFormat format)
  {
    // 一分間の四分音符の個数
    var tempo = 100;
    (途中省略)
    // ミックスは'StereoMixer'クラスで行う。
    return new StereoMixer(channels).Mix();
  }
}
```

しかしちょっと待って下さい。

上記コードをよく見ると、**ファイルへの書き込み**を行なってますね。以下に該当箇所を抜粋します。

```csharp:App.razor
private static string GenerateWavFile()
{
    // ファイルに書き込む。
    ...
    var filePath = "sample.wav";
    writer.Write(filePath);
    ...
```

このプログラムは、Blazor WebAssembly プログラムであり、Web ブラウザ内に読み込まれて、そのサンドボックス内で実行されるものです。なのに、このような **ローカルファイルへの書き込み処理**を行なったら、どうなってしまうのでしょうか!? 当然、実際にその Web ブラウザを実行しているデバイスのローカルストレージに書き込まれるはずはありません。ということは、この処理は例外で落ちてしまうのでしょうか。かといって、SoundMaker ライブラリの `WaveWriter` クラスには、どうやら任意の `Stream` に書き込むようなことはできず、**ファイルパスを指定する以外の wav ファイルの書き込み手段がない**ようです。これは残念なことに "SoundMaker" ライブラリは Blazor WebAssembly 上では使えないのでしょうか!?

## Blazor WebAssembly 内ではローカルファイルの書き込みは成功します

...などと、自作自演で煽っておいて申し訳ありませんでしたが、なんと、**Blazor WebAssembly 内ではローカルファイルの書き込みは成功します。** というのも、Blazor WebAssembly はその基盤に Emscripten による WebAssembly コード生成が使われているのですが、その関係で、Empscripten によって提供されている**オンメモリ上でのファイルシステムの模擬機能** (仮想ファイルシステム) が、Blazor WebAssembly からも利用できているのです! (詳細については下記リンク先など)

https://emscripten.org/docs/porting/files/file_systems_overview.html

少し余談にはなりますが、下記リンク先の、拙作の Blazor WebAssembly による自分の CUI 風ポートフォリオサイトにて、`pwd`, `ls`, `cd`, `cat` といったコマンドを使って、Blazor WebAssembly 内の仮想ファイルシステムを見て回ることができますので、ご参考までに。

https://jsakamoto.github.io/

...ということで、"SoundMaker" ライブラリの `WaveWriter` クラスによる .wav ファイルのローカルファイルへの書き込みは成功します。および、そうして仮想ファイルシステム上に書き込まれた .wav ファイルを別途 `byte` の配列に読み取って、Web ブラウザ内でサウンド再生したり、Web ブラウザからダウンロードさせたりといったことが可能となります。

## 実際の作例

以上を踏まえて実装した作例を、下記リンク先の GitHub リポジトリで公開しています。

https://github.com/sample-by-jsakamoto/Blazor-SoundMaker-on-BlazorWasm

なお、生成される wav ファイルの保存先パスは、オリジナルのコードではカレントフォルダとなっており、それはこの Blazor WebAssembly アプリにおいては少々気になりました。そこで書き出す先のファイルパスを `/tmp/sample.wav` というように Blazor WebAssembly 内の仮想ファイルシステム中の `/tmp` フォルダ内に書き出すように変更しました。

```csharp:App.razor
private static string GenerateWavFile()
{
    // ファイルに書き込む。
    ...
    var filePath = "/tmp/sample.wav"; // 👈 "/tmp" フォルダ内に書き込むようにした
    writer.Write(filePath);
    ...
```


また、この作例は、GitHub Actions を使って GitHub Pages 上にデプロイしてあり、下記リンク先からライブデモンストレーションを動かしてみることができます。

https://sample-by-jsakamoto.github.io/Blazor-SoundMaker-on-BlazorWasm/

下図は、上記リンク先を開いてみた様子です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/ee91fbda-4972-bbe1-97cb-b4a1f6e5197f.png)

"Generate Sound and Play" をクリックすると、前述の C# 実装にてその場で生成された .wav サウンドがブラウザ上で再生されます。また、"Generate Sound and Download" をクリックすると、同じくその場で生成された .wav サウンドファイルがダウンロードされます。

あと、上記デモンストレーションサイトを、手持ちの iPhone で開いてみましたが、**iPhone 上でもちゃんと "Generate Sound and Play" をタップでサウンド生成 & 再生されました!**

また、前述のとおりこのデモンストレーションサイトは GitHub リポジトリ上でソースコード一式をホストしてますので、下図のとおり GitHub Codespaces を追加すれば、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/3cfe6d2e-3e8f-7b07-52aa-35e28b45dc6b.png)

下図のように、**開発環境の構築不要で、いきなり Web ブラウザ内でこのデモンストレーションサイトのコード編集と実行 ("dotnet run" コマンド実行) ができます!**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/d47d7be1-e897-9857-c03e-8fdab99d431e.png)

これでお手軽に "SoundMaker" によるサウンド生成プログラミングを楽しめますね!

## AOT ビルドについて

ちなみに、この GitHub Pages 上にデプロイされたデモンストレーションアプリは、**AOT (Ahead Of Time) ビルドを有効**にしてビルド・配置してあります。AOT ビルド、すなわち、ビルドして生成された .NET バイナリを MSIL インタープリタで逐次実行するのではなく、事前に WebAssembly バイナリに変換してしまうコンパイル方式であるわけですが、これにより、発行に長時間かかることと**コンテンツサイズが増加**するのと引き換えに、**インタープリタ方式よりも格段に処理速度が上がる**ことになります。詳細は公式ドキュメントサイト (下記リンク先) も参照ください。

https://learn.microsoft.com/ja-jp/aspnet/core/blazor/host-and-deploy/webassembly?view=aspnetcore-7.0#ahead-of-time-aot-compilation

実際に AOT コンパイルするかしないかで、このデモンストレーションアプリにおいてどれくらいコンテンツサイズが変わるかというと、
- AOT ありで 約 10.0 MB
- AOT なしで 約 3.3 MB
 
でした。10 MB と聞くと、これっぽっちのデモンストレーションにしては大きいサイズのように感じるかもしれません。しかし上記 GitHub Pages にデプロイした Blazor WebAssembly アプリは、**Brotli 圧縮**が効いているので (※[参考記事](https://qiita.com/jsakamoto/items/0be0005d9b30acbfe1e4#brotli-%E5%9C%A7%E7%B8%AE%E3%82%82%E6%9C%89%E5%8A%B9%E3%81%AB))、**実際のダウンロードサイズは 3.0 MB** でした。もちろん、AOT なしだともっと小さいダウンロードサイズになるわけですが、3.0 MB 程度であれば大騒ぎするほどのコンテンツサイズでないようにも思われました (あくまでも私見ではありますが)。

あと、AOT ありとなしとで、サウンドの生成にどれくらいの処理時間の差があったかというと、実のところ、**体感でほとんど差は感じられませんでした**。よーく聞き比べると、AOT なしのほうが AOT ありと比べて、"Play" ボタンをクリックしてから実際にサウンドが再生されるまでに 100 ～ 200 ミリ秒の間があるようにも感じられます。ですがとにかく、自分の体感的には、**AOT あり・なしのどちらでも、"Play" ボタンをクリックすると即座にサウンド再生されている**ように聞こえました。このデモンストレーションの程度の処理負荷では、MSIL インタープリタ方式も充分高速、とも言えそうです。

## まとめ

以上、Web ブラウザ上でも、"SoundMaker" を使用・実行して、サウンド生成と再生を行なうことができました! 🎉

なお今回は、とりあえず "SoundMaker" を Web ブラウザ上で動かすことを優先し、.wav ファイルを Blazor WebAssembly (というか Emscripten) の仮想ファイルシステムに書き出す方式のまま実装しましたが、実際には仮想ファイルシステムはブラウザのオンメモリ上で模擬されているだけですから、処理効率的にもできれば "SoundMaker" の `WaveFile` クラスにて、任意の `Stream` に書き出せるオーバーロードバージョンなど用意してもらうのが良いでしょう。この記事作成を優先して自分自身ではやれていませんが、"SoundMaker" の GitHub リポジトリ上に Issue でそのような機能要望出したり、あるいは自分で実装してプルリクエストしてみるのがよいのでしょうね。

また、今回は、Razor コンポーネント内のコードブロックに、波形生成の処理を直書きしましたが、どのような波形を生成するかをデータで持つようにして、それを編集するユーザーインターフェースなど実装すると、**Web ブラウザ上で使用できる「矩形波・三角波サウンドエディタ」が作れそうだな!** と思いました。PWA で作ればオフライン動作もできますし、Windows や macOS や各種 Linux ディストリビューションのようなデスクトップ OS に限らず、前述のとおり **iPhone や iPad でも使えますね!**

なかなかに夢の広がるライブラリの開発と公開に感謝です。

_Happy Coding :)_