ソフトウェアシンセサイザーを作る　その１：サイン波でMIDIを再生する

# はじめに
この記事は、シンセによくある機能を自分で実装することで、音の作り方についての理解を深めようと作成したチュートリアルです。そのため、理論的な部分も含めできるだけ省略せずに説明していきたいと思います。

実装するのはウェーブテーブル方式のリアルタイムシンセサイザーです。構成はこのようにしました。
![構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/814aae0c-a2cb-3bd2-80b2-8fa5013db648.png)

:::note warn
VST プラグインについては扱いません。MIDI 形式の譜面データを読み込んでサウンドが鳴るようなアプリケーションとして実装します。
:::

## ソースコード
記事中の各ステップで作成したコードはこちらにまとめました。

https://github.com/agehama/SynthesizerTutorial

## 開発環境
- Visual Studio 2022
- OpenSiv3D v0.6.6

## 記事の構成

全部で4つの記事になる予定です。この記事は導入として、サイン波を使って MIDI 譜面を再生するところまで行います。リポジトリでの以下のファイルに相当する実装を順に追っていきます。
- `Chapter1_1_SinWave.cpp`
- `Chapter1_2_Envelope.cpp`
- `Chapter1_3_Chord.cpp`
- `Chapter1_4_Synthesizer.cpp`
- `Chapter1_5_MIDI.cpp`

# １．サイン波を再生する

指定された周波数と振幅でサイン波を計算するコードは次のように書けます。
```cpp
sin(Math::TwoPi * frequency * t) * amplitude;
```

`t`は秒数で、1サンプルあたりに **サンプリング周波数の逆数** だけ時間を進めます。以降ではサンプリング周波数は全て 44100 [Hz] 固定とします。

例として、サイン波の波形を生成して返す関数を以下に定義します。
```cpp
# include <Siv3D.hpp> // OpenSiv3D v0.6.6

Wave RenderWave(uint32 seconds, double amplitude, double frequency)
{
	const auto lengthOfSamples = seconds * Wave::DefaultSampleRate;

	Wave wave(lengthOfSamples);

	for (uint32 i = 0; i < lengthOfSamples; ++i)
	{
		const double sec = 1.0f * i / Wave::DefaultSampleRate;
		const double w = sin(Math::TwoPiF * frequency * sec) * amplitude;
		wave[i].left = wave[i].right = static_cast<float>(w);
	}

	return wave;
}
```

これを使って、サイン波を3秒間再生するプログラムを以下のように書けます。

```cpp
// GUIの描画用
const auto SliderHeight = 36;
const auto SliderWidth = 400;
const auto LabelWidth = 200;

void Main()
{
	double amplitude = 0.2;
	double frequency = 440.0;

	uint32 seconds = 3;

	Audio audio(RenderWave(seconds, amplitude, frequency));
	audio.play();

	while (System::Update())
	{
		Vec2 pos(20, 20 - SliderHeight);
		SimpleGUI::Slider(U"amplitude : {:.2f}"_fmt(amplitude), amplitude, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);
		SimpleGUI::Slider(U"frequency : {:.0f}"_fmt(frequency), frequency, 100.0, 1000.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);

		if (SimpleGUI::Button(U"波形を再生成", Vec2{ pos.x, pos.y += SliderHeight }))
		{
			audio = Audio(RenderWave(seconds, amplitude, frequency));
			audio.play();
		}
	}
}
```
生成した波形データを `Audio` クラスに渡して再生を行います。また、振幅と周波数を変更したときはボタンを押すことで波形を再生成するようにしました。

## ここまでのコード
https://github.com/agehama/SynthesizerTutorial/blob/main/Chapter1_1_SinWave.cpp

# ２．ADSR エンベロープを実装する

キーのそれぞれの入力状態における音量の時間変化を**エンベロープ**と呼びます。

- キーを押した直後
- 押し続けている間
- 離した後

同じ波形でもエンベロープの形によってピアノっぽい音やストリングスっぽい音になったりします。ほとんどのシンセは、これを設定するエンベロープジェネレーターを備えています。細かい設定はシンセによって異なりますが、少なくとも以下の4つのパラメータによって構成されています。

![envelope.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/2638f57b-cd3e-99c9-97bd-ee60a3f5e602.png)

- `attackTime` : 音が鳴り始めてからピークに達するまでの時間
- `decayTime` : ピークに達してから`sustainLevel`まで減衰するのにかかる時間
- `sustainLevel` : 音を鳴らし続けている間の音量レベル
- `releaseTime` : 音を止めてから減衰しきるまでにかかる時間

```cpp
struct ADSRConfig
{
	double attackTime = 0.1;
	double decayTime = 0.1;
	double sustainLevel = 0.6;
	double releaseTime = 0.4;
};
```

この4つのパラメータを使用して、エンベロープジェネレーターを実装することができます。

## EnvGenerator の実装
`EnvGenerator` クラスの全体像は以下のようになります。`enum` で定義した各状態を `update` 関数の中で遷移させることで更新を行います。

```cpp
class EnvGenerator
{
public:

	enum class State
	{
		Attack, Decay, Sustain, Release
	};

    // ノートの入力が終了した
    void noteOff()
	{
		if (m_state != State::Release)
		{
			m_elapsed = 0;
			m_state = State::Release;
		}
	}

    // エンベロープの更新
	void update(const ADSRConfig& adsr, double dt)
	{
		switch (m_state)
		{
			// m_currentLevel の更新、ステート遷移を行う
		}

		m_elapsed += dt;
	}

    // 現在のレベルを取得する
	double currentLevel() const
	{
		return m_currentLevel;
	}

private:

	State m_state = State::Attack;
	double m_elapsed = 0; // ステート変更からの経過秒数
	double m_currentLevel = 0; // 現在のレベル [0, 1]
};
```

`m_elapsed` は遷移してからの経過時間で、遷移するたびに0にリセットします。更新処理の`switch`文の中身の実装について以下で説明します。

### Attack
`0.0` から `1.0` まで `attackTime` かけて増幅します。`attackTime` を超えていたら、そのまま次のDecay処理に移ります。
```cpp
case State::Attack:
	if (m_elapsed < adsr.attackTime)
	{
		m_currentLevel = m_elapsed / adsr.attackTime;
		break;
	}
	m_elapsed -= adsr.attackTime;
	m_state = State::Decay;
	[[fallthrough]]; // Decay処理にそのまま続く
```

### Decay
`1.0` から `sustainLevel` まで `decayTime` かけて減衰します。これについても `decayTime` を超えていたら、そのまま次の Sustain 処理に移ります。

```cpp
case State::Decay:
	if (m_elapsed < adsr.decayTime)
	{
		m_currentLevel = 
            Math::Lerp(1.0, adsr.sustainLevel, m_elapsed / adsr.decayTime);
		break;
	}
	m_elapsed -= adsr.decayTime;
	m_state = State::Sustain;
	[[fallthrough]]; // Sustain処理にそのまま続く
```

### Sustain
`noteOff()` が呼び出されるまで `sustainLevel` を維持します。

```cpp
case State::Sustain:
	m_currentLevel = adsr.sustainLevel;
	break;
```

### Release
`sustainLevel` から `0.0` まで `releaseTime` かけて減衰します。`releaseTime` を超えたら常に `0.0` を返すようにします。

```cpp
case State::Release:
	m_currentLevel = m_elapsed < adsr.releaseTime
		? Math::Lerp(adsr.sustainLevel, 0.0, m_elapsed / adsr.releaseTime)
		: 0.0;
	break;
```

:::note info
上記の処理は Attack や Decay 中に `noteOff()` が呼ばれることを考慮していません。これを考慮する場合は、 Release 処理の開始点を `adsr.sustainLevel` ではなく、`noteOff()` が呼ばれた時点の `m_currentLevel` を別の変数に保存して、そこから `0.0` まで減衰させるようにすると滑らかに繋がります。
:::

## エンベロープを適用した波形生成

生成した波形に `EnvGenerator` を適用して音量を変化させるサンプルです。

```cpp
Wave RenderWave(uint32 seconds, double amplitude,
    double frequency, const ADSRConfig& adsr)
{
	const auto lengthOfSamples = seconds * Wave::DefaultSampleRate;

	Wave wave(lengthOfSamples);

	// 0サンプル目でノートオン
	EnvGenerator envelope;

	// 半分経過したところでノートオフ
	const auto noteOffSample = lengthOfSamples / 2;

	const float deltaT = 1.0f / Wave::DefaultSampleRate;
	float time = 0;
	for (uint32 i = 0; i < lengthOfSamples; ++i)
	{
		if (i == noteOffSample)
		{
			envelope.noteOff();
		}

		const auto w = sin(Math::TwoPiF * frequency * time)
			* amplitude * envelope.currentLevel();
		wave[i].left = wave[i].right = static_cast<float>(w);

        // エンベロープの更新
		time += deltaT;
		envelope.update(adsr, deltaT);
	}

	return wave;
}
```

サイン波を生成した後に `envelope.currentLevel()` を掛けることでエンベロープを適用しています。また、サンプルが `noteOffSample` に到達したら `envelope.noteOff()` を呼んで Release 状態に遷移させています。

```cpp
void Main()
{
	double amplitude = 0.2;
	double frequency = 440.0;

	uint32 seconds = 3;

	ADSRConfig adsr;
	adsr.attackTime = 0.1;
	adsr.decayTime = 0.1;
	adsr.sustainLevel = 0.8;
	adsr.releaseTime = 0.5;

	Audio audio(RenderWave(seconds, amplitude, frequency, adsr));
	audio.play();

	while (System::Update())
	{
		Vec2 pos(20, 20 - SliderHeight);
		SimpleGUI::Slider(U"amplitude : {:.2f}"_fmt(amplitude), amplitude, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);
		SimpleGUI::Slider(U"frequency : {:.0f}"_fmt(frequency), frequency, 100.0, 1000.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);

		adsr.updateGUI(pos);

		if (SimpleGUI::Button(U"波形を再生成", Vec2{ pos.x, pos.y += SliderHeight }))
		{
			audio = Audio(RenderWave(seconds, amplitude, frequency, adsr));
			audio.play();
		}
	}
}
```

## エイリアシング

デジタル波形で表現できる最大周波数はサンプリング周波数の半分の値で、これを**ナイキスト周波数**と呼びます。波形にナイキスト周波数を超えるような瞬間的な変化があると、再生したときに意図しない周波数のノイズが生じます。このノイズのことを**エイリアシング**（折り返し雑音）と呼びます。

![aliasing.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/8da8eb5f-db6a-cd5a-9b68-b6ffe2bbdb17.png)

### エイリアシングあり
試しにアタックとリリースを0にして再生すると、音の開始と終了でプツッというノイズが聞こえるのが確認できます。

```cpp
adsr.attackTime = 0.0;
adsr.releaseTime = 0.0;
```

### エイリアシングなし
これを `0.01` にするとノイズが消えることが確認できると思います。エイリアシングを避けるには、アタックとリリースには数ミリ秒程度は確保しておくと安全です。
```cpp
adsr.attackTime = 0.01; //Attack:10ミリ秒
adsr.releaseTime = 0.01; //Release:10ミリ秒
```

## ここまでのコード
### 差分のみ

https://github.com/agehama/SynthesizerTutorial#%EF%BC%91%EF%BC%92-adsr-%E3%82%A8%E3%83%B3%E3%83%99%E3%83%AD%E3%83%BC%E3%83%97%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B

### コード全体

https://github.com/agehama/SynthesizerTutorial/blob/main/Chapter1_2_Envelope.cpp

# ３．音階に対応させる
音階に対応した周波数で音を鳴らせるようにします。ここでは音階を $Ｃ_{4}$ のように $階名_{オクターブ数}$ の形で表記することにします。

MIDI の仕様では音階 $d$ と周波数 $f$ は次のように対応します[^1]。

[^1]: https://en.wikipedia.org/wiki/MIDI_tuning_standard

$$
f=2^{(d-69)/12} 440 \text{ Hz}
$$

この式は $[0, 127]$ のノート番号 $d$ を取り、$Ｃ_{-1}$ から $Ｇ_9$ までの128音に対応した周波数に変換します。これは一般的な調律の標準音[^2]である $Ａ_4$ ($d=69$) の音を基準として、1オクターブごとに周波数が倍になるように定義したものです。

[^2]: https://ja.m.wikipedia.org/wiki/A440

```cpp
float NoteNumberToFrequency(int8_t d)
{
    return 440.0f * pow(2.0f, (d - 69) / 12.0f);
}
```

`RenderWave` 関数を和音を鳴らせるように書き変えてみます。引数で周波数を受け取る代わりに、ノート番号の `Array` を受け取るようにしました。

```cpp
Wave RenderWave(uint32 seconds, double amplitude,
    const Array<int8_t>& noteNumbers, const ADSRConfig& adsr)
{
	const auto lengthOfSamples = seconds * Wave::DefaultSampleRate;

	Wave wave(lengthOfSamples);

	// 0サンプル目でノートオン
	EnvGenerator envelope;

	// 半分経過したところでノートオフ
	const auto noteOffSample = lengthOfSamples / 2;

	const float deltaT = 1.0f / Wave::DefaultSampleRate;
	float time = 0;

	for (uint32 i = 0; i < lengthOfSamples; ++i)
	{
		if (i == noteOffSample)
		{
			envelope.noteOff();
		}

		// 和音の各波形を加算合成する
		double w = 0;
		for (auto note : noteNumbers)
		{
			const auto freq = NoteNumberToFrequency(note);
			w += sin(Math::TwoPiF * freq * time)
				* amplitude * envelope.currentLevel();
		}

		wave[i].left = wave[i].right = static_cast<float>(w);
		time += deltaT;
		envelope.update(adsr, deltaT);
	}

	return wave;
}
```

これを使ってCメジャーの和音を鳴らしてみます。各ノート番号は $Ｃ_4$ = 60, $Ｅ_4$ = 64, $Ｇ_4$ = 67 です。`Main`関数の中身を以下のように書き変えます。
```diff_cpp
-       Audio audio(RenderWave(seconds, amplitude, frequency, adsr));
+       const Array<int8_t> noteNumbers =
+       {
+               60, // C_4
+               64, // E_4
+               67, // G_4
+       };
+
+       Audio audio(RenderWave(seconds, amplitude, noteNumbers, adsr));
```

## ここまでのコード
### 差分のみ

https://github.com/agehama/SynthesizerTutorial#%EF%BC%91%EF%BC%93-%E9%9F%B3%E9%9A%8E%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%95%E3%81%9B%E3%82%8B

### コード全体

https://github.com/agehama/SynthesizerTutorial/blob/main/Chapter1_3_Chord.cpp

# ４．シンセサイザーを定義する
`RenderWave` が複雑になってきたので、波形を生成する機能をまとめた `Synthesizer` クラスを作りたいと思います。このクラスの役目は、入力されたノート番号を受け取って波形を生成して返すことです。

### `Synthesizer` クラスに実装する機能

- ノートオンを受け取って、入力されたノート情報を保持しておく
- ノートオフを受け取って、リリース音を鳴らした後にノート情報を破棄する
- 入力中のノート情報を参照し波形を生成して返す

### ノートの入力状態

ノートの入力状態を管理する必要があるので、以下のように定義します。

```cpp
struct NoteState
{
	EnvGenerator m_envelope;
};

std::multimap<int8_t, NoteState> m_noteState;
```

`m_noteState` はノート番号をキーにして、再生に必要なノート状態（ここでは`EnvGenerator`）を返します。先ほどの例では `EnvGenerator` を1つだけ使って和音を鳴らしていました。しかし、実際のオンオフはノートごとに独立して起きるので、個別に管理する必要があります。

:::note info
ノート状態を `std::map` でなく `std::multimap` で定義する理由は、ノートを離した後のリリース中に再度同じノートの入力があった時に、二重に再生できるようにするためです。この仕様自体は別に必要ないのですが、もし `std::map` で定義した場合、リリース中に再入力があった時には同じエンベロープを使うことになるので、音量レベルが飛ばないように遷移させる処理を挟む必要があります。ここではシンプルさを優先して `std::multimap` で管理することにします。
:::

### Synthesizer クラスを実装する

`Synthesizer` に実装する関数は以下の3つです。

```cpp
class Synthesizer
{
public:

	// ノートオンを受け取って入力情報を記録する
	void noteOn(int8_t noteNumber)
	{
	}

	// ノートオフを受け取ってリリース状態に遷移させる
	void noteOff(int8_t noteNumber)
	{
	}

	// 入力中のノートの波形を1サンプル生成して返す
	WaveSample renderSample()
	{
	}

private:

	std::multimap<int8_t, NoteState> m_noteState;

	ADSRConfig m_adsr;
};
```

#### Synthesizer::noteOn()

`noteOn()` では `m_noteState` に要素を追加してノート番号の入力を記録します。

```cpp
void noteOn(int8_t noteNumber)
{
	m_noteState.emplace(noteNumber, NoteState());
}
```

#### Synthesizer::noteOff()

`noteOff()` で行うことは、受け取ったノート番号を `m_noteState` から検索して、その要素のエンベロープをリリース状態に遷移させることで入力の終了を伝えます。1回の `noteOff()` で複数のノート状態を変更するとまずいので、一応最初に見つかった入力中のノートを終了させることにしています。

```cpp
void noteOff(int8_t noteNumber)
{
    auto [beginIt, endIt] = m_noteState.equal_range(noteNumber);

    for (auto it = beginIt; it != endIt; ++it)
    {
        auto& envelope = it->second.m_envelope;

        // noteOnになっている最初の要素をnoteOffにする
        if (envelope.state() != EnvGenerator::State::Release)
        {
            envelope.noteOff();
            break;
        }
    }
}
```

#### Synthesizer::renderSample()

`renderSample()` 関数には波形生成ループの1回に相当する処理を実装します。

- 入力中のノートのエンベロープを更新する
- リリース処理が終わったノートの情報を削除する
- 入力中のノートの波形を加算して計算する
- 波形のサンプルを返す

```cpp
WaveSample renderSample()
{
    // 1サンプルで進む時間 ＝ サンプリング周波数の逆数
    const auto deltaT = 1.0 / Wave::DefaultSampleRate;

    // エンベロープの更新
    for (auto& [noteNumber, noteState] : m_noteState)
    {
        noteState.m_envelope.update(m_adsr, deltaT);
    }

    // リリースが終了したノートを削除する
    std::erase_if(m_noteState, [&](const auto& noteState) {
        return noteState.second.m_envelope.isReleased(m_adsr); });

    // 入力中の波形を加算して書き込む
    WaveSample sample(0, 0);
    for (auto& [noteNumber, noteState] : m_noteState)
    {
        const auto envLevel = noteState.m_envelope.currentLevel();
        const auto frequency = NoteNumberToFrequency(noteNumber);

        const auto w = static_cast<float>(
            sin(Math::TwoPiF * frequency * m_time) * envLevel);

        sample.left += w;
        sample.right += w;
    }

    // 時間を1サンプル分進める
    m_time += deltaT;

    return sample * m_amplitude;
}
```

時間を管理する `m_time` と音量パラメータの `m_amplitude` をメンバ変数に追加しました。

```cpp
double m_amplitude = 0.2;
double m_time = 0;
```

先ほどの `RenderWave` 関数を `Synthesizer` クラスで書き直すと以下のようになりました。

```cpp
Wave RenderWave(uint32 seconds, Synthesizer& synth)
{
	const auto lengthOfSamples = seconds * Wave::DefaultSampleRate;

	Wave wave(lengthOfSamples);

	// 半分経過したところでノートオフ
	const auto noteOffSample = lengthOfSamples / 2;

	synth.noteOn(60); // C_4
	synth.noteOn(64); // E_4
	synth.noteOn(67); // G_4

	for (uint32 i = 0; i < lengthOfSamples; ++i)
	{
		if (i == noteOffSample)
		{
			synth.noteOff(60);
			synth.noteOff(64);
			synth.noteOff(67);
		}

		wave[i] = synth.renderSample();
	}

	return wave;
}
```

```cpp
void Main()
{
	uint32 seconds = 3;

	Synthesizer synth;

	Audio audio(RenderWave(seconds, synth));
	audio.play();

	while (System::Update())
	{
		Vec2 pos(20, 20 - SliderHeight);

		synth.updateGUI(pos);

		if (SimpleGUI::Button(U"波形を再生成", Vec2{ pos.x, pos.y += SliderHeight }))
		{
			synth.clear();

			audio = Audio(RenderWave(seconds, synth));
			audio.play();
		}
	}
}
```

## ここまでのコード
### 差分のみ

https://github.com/agehama/SynthesizerTutorial#%EF%BC%91%EF%BC%94-%E3%82%B7%E3%83%B3%E3%82%BB%E3%82%B5%E3%82%A4%E3%82%B6%E3%83%BC%E3%82%92%E5%AE%9A%E7%BE%A9%E3%81%99%E3%82%8B

### コード全体

https://github.com/agehama/SynthesizerTutorial/blob/main/Chapter1_4_Synthesizer.cpp

# ５．譜面を再生する
実際に **SMF(Standard MIDI File)** から譜面を読み込んで再生できるようにします。SMF の読み込み処理は煩雑なので省略します。このサイトが詳しくてわかりやすかったです。

https://sites.google.com/site/yyagisite/material/smfspec

ここではサンプルリポジトリに上げた `SoundTools.hpp` の `LoadMidi()` 関数からファイルを読み込んだ前提で、その後の MIDI データの解釈について説明をします。

## SMF のデータ構造

SMF は複数のトラックから構成されており、それぞれのトラックは譜面を MIDI イベントのリストの形式で待っています。トラックの分け方についての規定はありませんが、楽器ごとだったり高音と低音で分けたりするのが一般的です。また、最初のトラックは慣例的にコンダクタートラック（テンポトラック）と呼ばれ、譜面データの定義はせずに拍子とテンポの設定だけを行うものとして扱われます[^3]。

[^3]: https://www.g200kg.com/jp/docs/dic/tempotrack.html

SMF のバイナリの構造を midi-dump[^4] で可視化した例：
![smf_format.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/3b9a384c-83e1-773c-6ae1-b6d5c66d3554.png)

[^4]:https://g200kg.github.io/midi-dump/


## 時間の単位
MIDI の時間単位は `delta_time` という全体設定で定義されます。これは**一拍（四分音符）の分解能**を示す値で[^5]、例えば `delta_time` = 480 の場合、四分音符の長さは 480 tick、十六分音符の長さは 120 tick となります。

各 MIDI イベントは、イベントの発生時刻を tick の値で持っています。したがって、アプリ側で tick を進めていって発生時刻を経過した MIDI イベントを実行することで、譜面の演奏をすることができます。

1秒間に何 tick 進めればいいかは、その時点での BPM の値に依存します。BPM はセットテンポというメタイベントによって動的に変化します。

[^5]: delta_time は分解能のほかに秒数で定義することも一応可能ですが、実際に使われているところを見たことはありません

## MIDI イベント

譜面データは MIDI イベント[^6]として定義されています。

[^6]: MIDIイベントというのはSMF固有の呼び方で、MIDI規格ではチャンネルボイスメッセージと呼ばれます

- **ノートオン**
- **ノートオフ**
- ポリフォニックキープレッシャー
- **コントロールチェンジ**
- **プログラムチェンジ**
- チャンネルプレッシャー
- **ピッチベンド**

MIDI イベントは全部で7種類あります。ここでの実装にとりあえず必要なのはノートオンとノートオフの二つだけです。以下に主要なイベントについて説明をします。

### ノートオン
キー入力が開始したことを示すイベントです。キーのノート番号と入力の強さを示すベロシティーを待っています。

```cpp
struct NoteOnEvent
{
	uint8 channel;
	uint8 note_number;
	uint8 velocity;
};
```

### ノートオフ
キー入力が終了したことを示すイベントです。キーのノート番号を持っています。

```cpp
struct NoteOffEvent
{
	uint8 channel;
	uint8 note_number;
};
```

### コントロールチェンジ
コントロールチェンジはいろいろなパラメータを変更するためのイベントです。この中でさらにたくさんの種類に分けられ[^7]、変更するパラメータごとにコントロール番号が振られています。この中で`CC10:パンポット`と`CC11:エクスプレッション`はよく使われるので重要です。

```cpp
struct ControlChangeEvent
{
	uint8 channel;
	uint8 type;
	uint8 value;
};
```

[^7]: https://ja.m.wikipedia.org/wiki/コントロールチェンジ

`CC10:パンポット` ：左右のパン振りを変更する

`CC11:エクスプレッション` ：音量を変更する

### プログラムチェンジ
プログラムチェンジはトラックの音源を設定するイベントです。原則として各トラックの先頭で一回だけ呼ばれます。

```cpp
struct ProgramChangeEvent
{
	uint8 channel;
	uint8 type;
};
```

### ピッチベンド
音の高さを変えるイベントです。指定したチャンネルについて、ノート番号から計算された周波数を相対値で上下に動かします。

```cpp
struct PitchBendEvent
{
	uint8 channel;
	uint16 value;
};
```

## プログラムとチャンネルとトラック

上述の通り、全ての MIDI イベントは設定先として **チャンネル** という値をパラメータに持っています。しかし、トラックはすでに楽器ごとに分かれていて、トラックごとに MIDI イベントを持っているはずなので、設定先はトラックに対して紐付けるのが最も自然に思えます。

ここで、チャンネルという概念がややこしいので一旦 MIDI について整理したいと思います。MIDI はもともと楽器同士の通信規格として定義されたもので、その仕様の上に MIDI データを記録した SMF というファイルフォーマットが定義されました。そして、トラックは SMF にしかないものなので、音源とトラックを結び付けるにはチャンネルを介して情報を受け渡す必要があります。

![smf_spec.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/ad455c1f-f3c0-aa10-6c0e-1c2b43d25884.png)

それぞれの要素について以下で簡単に説明をします。

### プログラム
プログラムは音源リストの定義です。音源リストが楽器によって完全に独立だと、楽器ごとに送る信号を変えないといけなくなります。これを避けるために、128個の音源の共通仕様として **GM(General MIDI)** という標準規格が定義されています。楽器メーカーが独自に定義した GS や XG といったプログラムも存在しますが、いずれも GM 規格をもとに拡張したものです。

### チャンネル
チャンネルとは、楽器同士が信号をやり取りする際に使う通信路の番号のことです。使用する音源のそれぞれに1～16までの値を設定しておき、そのチャンネルに向けて MIDI 信号を送ること楽器ごとの演奏を行います。音源とチャンネルの結びつきはプログラムチェンジイベントで設定します。また、**チャンネル番号=10はパーカッション用**に予め確保されています。

### トラック
演奏データを MIDI イベントの形式で定義したものです。各トラックはプログラムチェンジイベントを発行することで、その**トラックが使用するチャンネル**と、**チャンネルからプログラムへの対応**を同時に設定します。

## MIDI 譜面を波形に書き込む関数を作る

実際に譜面データをサイン波で`Wave`に書き込む関数を作ってみます。

まず、MIDI における時間は tick で管理されているので、波形のサンプル位置から tick への変換を行う必要があります。これは少し面倒なので `midiData.secondsToTicks` 関数に定義しました。

```cpp
const auto currentTime = 1.0 * i / wave.sampleRate();
const auto currentTick = midiData.secondsToTicks(currentTime);
```

tick はサンプリング周波数よりもずっと粗い単位なので、毎サンプル MIDI イベントが発生することはありません。現在のサンプルと次のサンプルの tick を比較して、進んでいた時だけ MIDI イベントをチェックするようにすれば十分です。

```cpp
if (currentTick != nextTick)
{
    // MIDIイベントの取得処理
}
```

また、チャンネル番号=10はパーカッション用という説明をしましたが、これはトラックについても同様です。チャンネル番号=10を使用するトラックは **パーカッショントラック** として扱われます。このトラックにはドラムなど打楽器の譜面が入っており、サイン波で再生すると大変なことになってしまうので、今回はスキップします。
```cpp
if (track.isPercussionTrack())
{
    continue;
}
```

`MidiData` クラスの各トラックについて `getMIDIEvent()` 関数を呼ぶことで、トラックの MIDI イベントを取得できるようになっています。これを実行すると、引数に指定した `currentTick` 以上 `nextTick` 未満の範囲にあるイベントが返ります。

```cpp
const auto noteOnEvents = track.getMIDIEvent<NoteOnEvent>(currentTick, nextTick);
const auto noteOffEvents = track.getMIDIEvent<NoteOffEvent>(currentTick, nextTick);
```

以上の処理とまとめて、MIDI 譜面を `Wave` に書き込む関数を以下のように定義できます。

```cpp
Wave RenderWave(Synthesizer& synth, const MidiData& midiData)
{
	const auto lengthOfSamples = static_cast<int64>(ceil(midiData.lengthOfTime() * Wave::DefaultSampleRate));

	Wave wave(lengthOfSamples);

	for (int64 i = 0; i < lengthOfSamples; ++i)
	{
		const auto currentTime = 1.0 * i / wave.sampleRate();
		const auto nextTime = 1.0 * (i + 1) / wave.sampleRate();

		const auto currentTick = midiData.secondsToTicks(currentTime);
		const auto nextTick = midiData.secondsToTicks(nextTime);

		// tick が進んだら MIDI イベントの処理を更新する
		if (currentTick != nextTick)
		{
			for (const auto& track : midiData.tracks())
			{
				if (track.isPercussionTrack())
				{
					continue;
				}

				// 発生したノートオンイベントをシンセに登録
				const auto noteOnEvents = track.getMIDIEvent<NoteOnEvent>(currentTick, nextTick);
				for (auto& [tick, noteOn] : noteOnEvents)
				{
					synth.noteOn(noteOn.note_number, noteOn.velocity);
				}

				// 発生したノートオフイベントをシンセに登録
				const auto noteOffEvents = track.getMIDIEvent<NoteOffEvent>(currentTick, nextTick);
				for (auto& [tick, noteOff] : noteOffEvents)
				{
					synth.noteOff(noteOff.note_number);
				}
			}
		}

		// シンセを1サンプル更新して波形を書き込む
		wave[i] = synth.renderSample();
	}

	return wave;
}
```

MIDI ファイルを読み込んで再生するプログラムのサンプルです。

```cpp
void Main()
{
	auto midiDataOpt = LoadMidi(U"short_loop.mid");
	if (!midiDataOpt)
	{
		// ファイルが見つからない or 読み込みエラー
		return;
	}

	const MidiData& midiData = midiDataOpt.value();

	Synthesizer synth;

	Audio audio(RenderWave(synth, midiData));
	audio.play();

	while (System::Update())
	{
		Vec2 pos(20, 20 - SliderHeight);

		synth.updateGUI(pos);

		if (SimpleGUI::Button(U"波形を再生成", Vec2{ pos.x, pos.y += SliderHeight }))
		{
			audio.stop();
			synth.clear();

			audio = Audio(RenderWave(synth, midiData));
			audio.play();
		}
	}
}
```

## ここまでのコード
### 差分のみ

https://github.com/agehama/SynthesizerTutorial#%EF%BC%91%EF%BC%95-%E8%AD%9C%E9%9D%A2%E3%82%92%E5%86%8D%E7%94%9F%E3%81%99%E3%82%8B

### コード全体

https://github.com/agehama/SynthesizerTutorial/blob/main/Chapter1_5_MIDI.cpp

# 次回

https://qiita.com/agehama_/items/863933459ca44ca5dbe0