C#で昔のゲームのような音を生成する「SoundMaker」を公開しました


# はじめに
こんにちは！秋空と申します。この記事は[C# Advent Calendar 2022](https://qiita.com/advent-calendar/2022/csharplang)の記事です。
この記事を読んで「C#っていろいろできるんだなー」と感じていただくとともに、SoundMakerがどんなライブラリかをある程度理解していただければと思います。


# SoundMakerの概要
動画のような音を**C#だけ**で生成し、`.wav`ファイルに出力できます。
<iframe width="480" height="270" src="https://www.youtube.com/embed/G6Mem8dArPg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# SoundMakerでできること
- 以下の音色の音声データを出力できます。
    1. 矩形波(デューティ比50%, 25%, 12.5%)
    2. 三角波
    3. ロービットノイズ
- 音声データを`.wav`ファイルに出力できます。

## SoundMakerの特徴
- C#だけで実装されています。
- NuGetから簡単にぶちこめます。
- 依存している外部ライブラリはありません。
- オープンソースソフトウェアです。（MITライセンス）
- わりかし簡単に音を生成できます。
- 音楽の知識で音を生成できます(ドレミの音階をつかえたり、音符を追加して音を作れたりします)。
- 配列の長さを指定したり、周波数を指定したりして直接波形を生成することもできます。

## Blazorでも使えるらしいです
@jsakamoto さんにご紹介いただきました。ありがとうございます。

https://qiita.com/jsakamoto/items/67d089cd95a35e8bc1d8

## そもそも矩形波とか三角波ってなんだよ！
どんな音なのか想像できない（私はできなかった）ので実際に各波形で演奏した動画を載せます。
クリスマスが近いので「ジングルベル」を選曲しました。正直、実際に聞いたり波形を見たりする方が分かり易いと思います。
<iframe width="560" height="315" src="https://www.youtube.com/embed/IPHSB25rJfE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# 使い方
## NuGetとGithub
インストールは以下のリンクで行えます。

https://www.nuget.org/packages/SoundMaker/

ドキュメントやソースコードなどはGithubから確認できます。

https://github.com/AutumnSky1010/SoundMaker

## メインルーチン
```cs:メインルーチン
using SoundMaker.Sounds;
using SoundMaker.Sounds.Score;
using SoundMaker.Sounds.SoundChannels;
using SoundMaker.WaveFile;

namespace YourNamespace;
internal static class YourClass
{
	private static void Main()
	{
		// サウンドの形式を作成する。
		var soundFormat = new SoundFormat(SoundMaker.Sounds.SamplingFrequencyType.FourtyEightKHz, SoundMaker.Sounds.BitRateType.SixteenBit, SoundMaker.Sounds.ChannelType.Stereo);

        // 記事の都合でMakeStereoWave()の実装は後で書きます。
		StereoWave 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);
		string filePath = "sample.wav";
		writer.Write(filePath);
	}
}
```
上記のような流れで実装を行います。波形データを生成して書き込むだけです。

## `MakeStereoWave()`の実装例
```cs:MakeSoundWave()の実装例
private static StereoWave MakeStereoWave(SoundFormat format)
	{
		// 一分間の四分音符の個数
		int tempo = 100;
		// まず、音のチャンネルを作成する必要がある。
		var rightChannel = new SquareSoundChannel(tempo, format, SquareWaveRatio.Point25, PanType.Right);
		var rightChannel2 = new SquareSoundChannel(tempo, format, SquareWaveRatio.Point125, PanType.Right);
		var leftChannel = new TriangleSoundChannel(tempo, format, PanType.Left);

		// ISoundComponentを実装したクラスのオブジェクトをチャンネルに追加していく。
		// 現段階では普通の音符、休符、タイ、連符を使うことができる。
		rightChannel.Add(new Note(Scale.C, 5, LengthType.Eighth, isDotted: true));
		rightChannel.Add(new Tie(new Note(Scale.D, 5, LengthType.Eighth), LengthType.Eighth));
		var notes = new List<BasicSoundComponentBase>()
		{
			new Note(Scale.E, 5, LengthType.Eighth),
			new Note(Scale.F, 5, LengthType.Eighth),
			new Note(Scale.G, 5, LengthType.Eighth),
		};
		rightChannel.Add(new Tuplet(notes, LengthType.Quarter));

		rightChannel2.Add(new Note(Scale.C, 4, LengthType.Eighth, isDotted: true));
		rightChannel2.Add(new Note(Scale.D, 4, LengthType.Quarter));
		rightChannel2.Add(new Rest(LengthType.Quarter));

		leftChannel.Add(new Note(Scale.C, 3, LengthType.Eighth, isDotted: true));
		leftChannel.Add(new Note(Scale.D, 3, LengthType.Quarter));
		leftChannel.Add(new Rest(LengthType.Quarter));

		var channels = new List<ISoundChannel>() { rightChannel, rightChannel2, leftChannel };
		// ミックスは'StereoMixer'クラスで行う。 
		return new StereoMixer(channels).Mix();
	}
```
無駄に長いですが、やっていることは、
1. 音のチャンネルを作る
2. (1)で作ったチャンネルに音符とか休符を`Add()`する。
3. (1)で作ったチャンネルをリストにする。
4. StereoMixerクラスのインスタンスを生成し、(3)のリストをぶち込む。
5. (4)を使って`Mix()`し、リターンする。

というだけです。

# できることの詳細
## 出せる音色
### 種類
- 矩形波（デューティ比50%, 25%, 12.5%）
- 三角波
- 疑似三角波
- ロービットノイズ
### 音の高さ
ピアノが出せる高さはすべて出せます。
## 音の長さ関連
- 64分音符～全音符に対応しています（128, 256, ...のように拡張の余地はあります。）
- 連符に対応しています。
- タイもあります。
## `.wav`のフォーマット
### サンプリング周波数
- 48000Hz
- 44100Hz
### 量子化ビット数
- 16bit
- 8bit
### チャンネル数
- ステレオ(2ch)
- モノラル(1ch)

# 学んだ事
詳しく書くとかなり長くなってしまうのでかいつまんで書いていきます。
## `.wav`ファイルの仕組み
データ構造は以下のサイトを参考にしました。

https://www.youfit.co.jp/archives/1418

軽くまとめると、
`.wav`ファイルはRIFF(Resource Interchange File Format)というフォーマットで作られています。
このフォーマットではチャンクという単位でデータを書き込み、RIFFチャンクというチャンク内にいろいろなチャンクを詰め込んでデータを表現します。

`.wav`ファイルでは、
1. Formatチャンクでステレオだとかサンプリング周波数だとかいろいろ設定する。
2. Dataチャンクに波形データをぶちこむ。

という方法で書き込むことができます。かなり単純なデータ構造なので安心です。
この構造をバイナリ形式にして、リトルエンディアンでファイルに書き込めば完成です。
SoundMakerのクラスだと違う構造になっていますが(おい)。

## 音の波形はどのように生成しているのか？
リニアPCM、16bitまでのデータなので、愚直にfor文をぶん回してushort配列に書き込んでいます。
音の合成も重ね合わせの原理に従って各配列の同じインデックスの値を加算して実現しています。

また、次の要件を満たすようにフォーマットに合わせてushort配列からbyte配列に変換する必要があったので変換しています。
- リトルエンディアンで出力する
- 各数値は符号付き
- ステレオ音声の場合は、左右左右左右……と交互に組み合わせて配列を作る。
(`data = {leftWave[0], rightWave[0], leftWave[1], rightWave[1] ......}`という感じ)

## クラスライブラリの開発は難しい
私は今まで開発者向けのソフトウェアを作ったことがないため、難しく感じました。一般利用者向けのソフトウェアと違って、公開しているAPIの全てがユーザインタフェースになるため、コードの変更が直接影響するからです。そのため、大まかな設計・ライブラリが抱える問題領域の分析を早い段階で完成までもっていかないといけないことがわかりました。

## NuGetにプッシュするのは意外と簡単
プロジェクトファイルにreadmeだとか説明文だとかを追加して、リリースビルド時に生成した`.nupkg`をコマンドを使ってプッシュするだけでできます。
### readmeに関する注意
画像は下記サイトに書いてあるドメインにあるものしかレンダリングされないというのと、一度リリースしたらもう一回リリースしないとreadmeを編集できないです。しっかりやらかしました。

https://learn.microsoft.com/ja-jp/nuget/nuget-org/package-readme-on-nuget-org

# まとめ
まとめると、
- SoundMakerを使えば手軽にサウンドプログラミングができます。
- ライブラリ開発は学べる事が多いです。
- C#はいろいろできます。

ということになります。最後まで読んでいただきありがとうございました！