１００円マイコンでＰＳＧ音楽を演奏～

https://www.youtube.com/watch?v=tQyGLMxu-qA

## 何か面白い応用をやってみよう～

R8C/Mxx の内部機能の解説ばかりでは、そろそろ退屈しているかもしれないので、ここらで、何かガジェットに応用できるアプリを紹介してみようと思います。

今回考えたのは、付加するハードが少なく単純で、何かダイナミックな動作が出来るガジェットとしてＰＳＧで音楽演奏をやってみました。

まず、単純に思いつくのは、内部タイマーで生成した周波数で圧電ブザーを鳴らすようなものだと思います。

ですが、これでは、あまりにもショボイですし、20MHz で動作するマイコンのガジェットとして相応しくないと思えます。

そこで、PWM 変調を使い、PWM 出力を８ビットの D/A コンバーターに見立て、波形ストリームを出力する物を考えてみました。

ただ、波形ストリームの演算は負荷が大きく、あまり高度な音源をソフトシンセ的に鳴らすには１００円のマイコンには厳しいとこです。

そこで、ファミコン音源並の PSG 音源をソフトで生成して実験してみました。

音楽だけにリソースを割けば、もう少し高度な事も考えられますが、何か別のガジェットにも使うには頃合いと思います。

## PWM 変調

R8C/Mxx のタイマーＣチャネルは、ＰＷＭ出力が出来る仕様です。
※タイマーＣの解説は、また改めて行う予定です。

音楽の場合、８オクターブで、「ラ」の音は３５２０Ｈｚなので、ＰＷＭ変調の周期はそれより大きい周波数にする必要があります。
タイマーＣは２０ＭＨｚで駆動するので、８ビットの分解能とするなら～
20MHz / 4 / 256 = 19531Hz が良さそうです。
※20MHz / 8 / 256 = 9765Hz でも可能ですが、やはり品質は低くなります、しかし、処理負荷が少なく、メモリの使用量も少なくなりますので、リソースを節約する場合には妥協する事も出来ます。

```cpp
	static constexpr uint16_t TRC_LIM = 256;  // サンプリング分解能（２５６固定）
	// サンプリング周期を変更すると、アタックやリリースを変更する必要がある。
// #define LOW_PROFILE
#ifdef LOW_PROFILE
	static constexpr auto TRC_DIV = device::trc_base::DIVIDE::F8;  // TRC の分周器パラメーター（１／８）
	static constexpr uint16_t SAMPLE = F_CLK / 8 / TRC_LIM;  // サンプリング周期（F_CLK は CPU の動作周波数）
	static constexpr uint16_t BSIZE = 256;  // SAMPLE / TICK * 2 以上で、２のｎ乗倍になる値
#else
	// サンプリングを倍にした、より高音質な設定
	static constexpr auto TRC_DIV = device::trc_base::DIVIDE::F4;  // TRC の分周器パラメーター（１／４）
	static constexpr uint16_t SAMPLE = F_CLK / 4 / TRC_LIM;  // サンプリング周期（F_CLK は CPU の動作周波数）
	static constexpr uint16_t BSIZE = 512;  // SAMPLE / TICK * 2 以上で、２のｎ乗倍になる値
#endif
	static constexpr uint16_t TICK = 100;	// サンプルの楽曲では、１００を前提にしている。
	static constexpr uint16_t CNUM = 3;		// 同時発音数（大きくすると処理負荷が増えるので注意）
	typedef utils::psg_base PSG;
	typedef utils::psg_mng<SAMPLE, TICK, BSIZE, CNUM> PSG_MNG;
	PSG_MNG	psg_mng_;
```

アプリでは、上記のように切り替えが簡単に出来るようにしてあります。

PWM 出力に、抵抗とコンデンサなどの簡単なローパスフィルターを接続すれば、簡単にアナログ的な出力が得られます。
※品質に関しては、「それなり」です。

オーディオの出力としては、PWM が５０％の時、無音で、０％でマイナス最大、１００％でプラス最大とします。
なので、マイコンの電源電圧の中間電位をオーディオ信号の「０V」とする必要があります。

PWM 信号は TRCIOB 端子（１８番）から出力されます、Ｒ８Ｃは３．３Ｖ駆動でテストしています。

ＲＣフィルターの参考回路：
![audio_if.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58667/5042dee5-0af0-42bf-7ad2-24487adb1dd2.png)

矩形波５０％（４４０Ｈｚ）：
![SDS00001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58667/fe1a0e9d-4949-82ec-1024-b16cd08b9fae.png)

三角波（４４０Ｈｚ）
![SDS00002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58667/6703b889-d977-4468-1012-998c3645bbfb.png)

- かなりリップルがあるので、もう少しＣ（0.1uF）を増やした方が良さそうです。
- このリップルは、周波数が高いので、あまり聞こえないのですけど・・

---

## 音源の仕様

音源としては、PSG 並の簡単な物を考えます。

- 処理負荷を低く抑える。（「音」を利用するガジェットで利用可能なように）
- 実装がある程度簡単で、それなりの音楽が演奏出来る。（ファミコン並）
- メモリー消費が少ない（ＰＷＭストリーム再生で５１２バイトを消費してしまうけど・・）

### 出力可能な波形

とりあえず、大きく二つの波形出力をサポートしました。

- 矩形波（デューティーが２５％、５０％、７５％）
- 三角波

※効果音などに「ノイズ」が必要ですが、それは、今後のアップデートで追加しようと思います。

### ボリューム

０～１２８までとしてあります。
※内部の構成に都合が良い値となっています。
※実際には、７ビットの分解能はありません。

### チャネル数

とりあえず同時３チャネル。

※テンプレートクラスなので、自由に変えられますが、多くのチャネルを使うと、処理負荷が大きくなります。

### 演奏の分解能

演奏のスコアを処理する単位です。

１００Ｈｚ

※これもテンプレートなので自由に変えられます。
※ゲームでは、画面の更新が６０Ｈｚなので、６０Ｈｚが使われていましたが、現在は、ＣＰＵの能力が高いので、より短い周期が使われています。

この分解能は、スコアの記述やテンポに直接影響するので、最初に決めたら自由に変更する事は出来ません。

１００Ｈｚでは、０．０１秒単位で計算しやすく、小さなシステムに丁度良い周期だと思います。

---

## 演奏の方法論（俗に言うサウンドドライバーとは？）

自分は、小学生の音楽の授業には良い思い出がありません・・
※リコーダーの練習が嫌で、古典音楽にあまり興味がありませんでした・・（今はクラシックから初音ミクまで色々聴いています～ｗ）
今になって思えば、あの頃もっとちゃんと勉強しておけばと思う事が多々あります、楽器もちゃんとやっていれば・・・

最初にサウンドドライバーらしき物を実装したのは、社会に出てからですが、その時に覚えた知識が今も生きています。
ゲームを作り、実際にサウンドをサポートするとなると、音楽理論や楽譜、楽器、MIDI、サウンド系ツールなど、色々な知識が必要です。

### １２平均音階率

世の中にある「音楽」は、ほぼ、この理論で創られています。

「ドレミファソラシド」ってやつですｗ
この中で「半音」とゆーのが出てきます、ピアノで言う「黒鍵」です。

しかし、実際（数学的）には、半音を含めた音階は、平均的に並んでいます。
※「ミ」の半音上が「ファ」で、この間には「黒鍵」はありません。
※そもそも、「半音」と言う表現は適切では無いと思います。

各音階の周波数を計算するには、１２乗すると２になる定数を求めて、それを乗算していくだけです。

１２回掛け算すると周波数が２倍（オクターブ）になります。

※定数「1.059463094」を１２乗すると「2」になります。

```cpp
		// 12 平均音階率の計算：
		// 2^(1/12) の定数、１２乗すると２倍（１オクターブ上がる）となる。
		static constexpr uint16_t key_tbl_[12] = {
			static_cast<uint16_t>((3520 * 65536.0 * 1.000000000) / SAMPLE),  ///< A  ラ
			static_cast<uint16_t>((3520 * 65536.0 * 1.059463094) / SAMPLE),  ///< A#
			static_cast<uint16_t>((3520 * 65536.0 * 1.122462048) / SAMPLE),  ///< B  シ
			static_cast<uint16_t>((3520 * 65536.0 * 1.189207115) / SAMPLE),  ///< C  ド
			static_cast<uint16_t>((3520 * 65536.0 * 1.25992105 ) / SAMPLE),  ///< C#
			static_cast<uint16_t>((3520 * 65536.0 * 1.334839854) / SAMPLE),  ///< D  レ
			static_cast<uint16_t>((3520 * 65536.0 * 1.414213562) / SAMPLE),  ///< D#
			static_cast<uint16_t>((3520 * 65536.0 * 1.498307077) / SAMPLE),  ///< E  ミ
			static_cast<uint16_t>((3520 * 65536.0 * 1.587401052) / SAMPLE),  ///< F  ファ
			static_cast<uint16_t>((3520 * 65536.0 * 1.681792831) / SAMPLE),  ///< F#
			static_cast<uint16_t>((3520 * 65536.0 * 1.781797436) / SAMPLE),  ///< G  ソ
			static_cast<uint16_t>((3520 * 65536.0 * 1.887748625) / SAMPLE),  ///< G#
		};
```

このプログラムでは、８オクターブのテーブルだけ用意してあり、シフト演算で、低いオクターブのパラメーターを計算しています。

楽器の調音では、和音の響きを良くする為、微妙に変えているようで、色々な主義があるようです。

### エンベロープ

一般的な楽器には、音の強弱があり、電子楽器でそれを模倣する仕組みとしてエンベロープがあります。

このＰＳＧ音源でも、非常に簡単な仕組みとして、ATTACK、RELEASE、と二つのパラメーターを使って強弱を表現しています。

![Envelope.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58667/cf5f1992-a8c8-14d1-f29c-07808c49bfcf.png)

標準値として、以下のように初期化されています。

```cpp
				attack_ = 150;  // アタック初期値
				release_ = 10; // リリース減衰初期値
				rel_frame_ = 6; // リリース TICK 標準
```

計算式として、以下のようなもので、1/100 秒毎に計算されます。
※TICK が 100Hzの場合

```cpp
					if(rel_count_ > 0) {
						rel_count_--;
						// +エンベロープ
						env_ += static_cast<uint16_t>((volume_ - env_) * attack_) >> 8;
					} else {
						// -エンベロープ
						uint8_t n = static_cast<uint16_t>(env_ * release_) >> 8;
						if(n > 0) env_ -= n;
						else env_ = 0;
					}
```

---

## C++ による演奏データ構築

演奏データは、C++ を活用して構築しています。

一般的な スタンダード MIDI データのような物は、大掛かり過ぎて、データ量も大きくなるので小回りが利きません。
そこで、ファミコン時代に良く使っていた手法を応用した簡易な構造と構成にしています。

- 音階データと、音長データ
- 制御データ

### 音階データ

音階データは、８８鍵のピアノに相当するもので、「enum class KEY」として定義してあります。

```cpp
		//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
		/*!
			@brief  １２平均音階率によるキーテーブル @n
					(0)27.5, (1)55, (2)110, (3)220, (4)440, (5)880, (6)1760, (7)3520
		*/
		//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
		enum class KEY : uint8_t {
			A_0,		///< A  ラ  27.5Hz (0)
			As0,		///< A#
			Bb0 = As0,	///< Bb
			B_0,		///< B  シ
			C_1,		///< C  ド
			Cs1,		///< C#
			Db1 = Cs1,	///< Db
			D_1,		///< D  レ
			Ds1,		///< D#
			Eb1 = Ds1,	///< Eb
			E_1,		///< E  ミ
			F_1,		///< F  ファ
			Fs1,		///< F#
			Gb1 = Fs1,	///< Gb
			G_1,		///< G  ソ
			Gs1,		///< G#
...
```

約８オクターブ分あります。
※フラット（ｂ）も定義してあります。

### 音長データ

音長は、整数となっていて、テンポと、TICK（100Hz）により計算されます。

音調×２５６÷テンポ×（１／１００）秒

### 制御データ

楽譜全体を制御するコマンドを設定して実装してあります。

```cpp
		enum class CTRL : uint8_t {
			TR = 89,	///< (2) トランスポーズ, num(-88 ~ 0 ~ 88)
			SQ25,		///< (1) 波形 SQ25
			SQ50,		///< (1) 波形 SQ50
			SQ75,		///< (1) 波形 SQ75
			TRI,		///< (1) 波形 TRI
			VOLUME,		///< (2) ボリューム, num(0 ~ 128)
			TEMPO,		///< (2) テンポ, num(1 ~ 255)
			FOR,		///< (2) ループ開始, num(1 ~ 255)
			BEFORE,		///< (1) ループ終了
			END,		///< (1) 終了
			REPEAT,		///< (1) リピート
			ATTACK,		///< (2) 音のアタック, num(0 ~ 255)
			RELEASE,	///< (3) 音のリリース, frame(n), num(0 ~ 255)
			TEST,
		};
```

※ＫＥＹが８８、休符１、８９から始まり、２５５までが制御コマンドとして登録可能です。

### union と constexpr を利用したスコア構築

楽譜データは、単純な整数としないで、ある程度構造化する事で、間違いを減らし、見やすいコードとなります。

そこで、C++ の機能を利用して、データ列を定義します。

「enum class」では、型が明確になり便利になりましたが、色々な型を混ぜるような使い方が出来ません。
そこで、「union」を使い、以下のようなクラスを定義する事で、自由度を受け付けるようにしています。

- コンストラクターの受け取る型が異なる為、型の異なる値を格納出来ます。
- union では、最も大きなサイズが採用されるので、uint8_t 型を使っています。
- 16 ビット以上の数値を設定するような場合、また、別の手法が必要となります。
- #define を駆使すると、このような場合に無理やりなデータ列を生成する事も可能ですが、安全性や可読性が犠牲になると思われます。（C++では使わない！）

```cpp
		struct SCORE {
			union {
				KEY		key;
				CTRL	ctrl;
				uint8_t	len;
			};
			constexpr SCORE(KEY k) noexcept : key(k) { }
			constexpr SCORE(CTRL c) noexcept : ctrl(c) { }
			constexpr SCORE(uint8_t l) noexcept : len(l) { }
		};
```

C++11 以降、コンパイル時定数で、ROM 領域にデータを配置するには「constexpr」を使う事になりました。
※その為、コンストラクターに「constexpr」を付加しています。

この細工により、スコアを構造的に記述出来ます。
このデータ列は、「constexpr」を使っている為、コンパイル時に決定され、ROM 領域に配置されます。

```cpp
	constexpr PSG::SCORE score0_[] = {
		PSG::CTRL::VOLUME, 128,
		PSG::CTRL::SQ50,
		PSG::CTRL::TEMPO, 80,
		// 1
		PSG::KEY::Q,   8,
		PSG::KEY::E_5, 8,
		PSG::KEY::D_5, 8,
		PSG::KEY::E_5, 8,
		PSG::KEY::C_5, 8,
		PSG::KEY::E_5, 8,
		PSG::KEY::B_4, 8,
		PSG::KEY::E_5, 8,
```

※単なる「const」だと、データ列は、RAM 上に配置され、起動時に ROM 領域から RAM 領域へコピーされます。

## psg_mng クラスの定義

psg_mng テンプレートクラスは、サンプリングにより、８ビット幅のデータ列を生成します。
生成されたデータ列は、タイマーＣのＰＷＭデータとして順次送られます。

```cpp
	static constexpr uint16_t TRC_LIM = 256;  // サンプリング分解能（２５６固定）
	// サンプリング周期を変更すると、アタックやリリースを変更する必要がある。
// #define LOW_PROFILE
#ifdef LOW_PROFILE
	static constexpr auto TRC_DIV = device::trc_base::DIVIDE::F8;  // TRC の分周器パラメーター（１／８）
	static constexpr uint16_t SAMPLE = F_CLK / 8 / TRC_LIM;  // サンプリング周期（F_CLK は CPU の動作周波数）
	static constexpr uint16_t BSIZE = 256;  // SAMPLE / TICK * 2 以上で、２のｎ乗倍になる値
#else
	// サンプリングを倍にした、より高音質な設定
	static constexpr auto TRC_DIV = device::trc_base::DIVIDE::F4;  // TRC の分周器パラメーター（１／４）
	static constexpr uint16_t SAMPLE = F_CLK / 4 / TRC_LIM;  // サンプリング周期（F_CLK は CPU の動作周波数）
	static constexpr uint16_t BSIZE = 512;  // SAMPLE / TICK * 2 以上で、２のｎ乗倍になる値
#endif
	static constexpr uint16_t TICK = 100;	// サンプルの楽曲では、１００を前提にしている。
	static constexpr uint16_t CNUM = 3;		// 同時発音数（大きくすると処理負荷が増えるので注意）
	typedef utils::psg_base PSG;
	typedef utils::psg_mng<SAMPLE, TICK, BSIZE, CNUM> PSG_MNG;
	PSG_MNG	psg_mng_;
```

## タイマーＣの定義

タイマーＣはＰＷＭを生成し、TRCIOB に出力するようにしてあります。
割り込み処理により、PSG のストリームからデータを取り出して、PWM の Duty を変える為、レジスターに書き込みます。
ストリームバッファは、リングバッファになっていて、PSG で生成するデータと、PWM 割り込みで、読み出すデータがぶつからないようにしています。

```cpp
volatile uint16_t pwm_pos_;

	class pwm_task {
	public:
		void operator () ()
		{
			device::TRCGRB = psg_mng_.get_wav((pwm_pos_ + (BSIZE / 2)) & (BSIZE - 1));
			++pwm_pos_;
			pwm_pos_ &= BSIZE - 1;
		}
	};

	typedef device::trc_io<pwm_task> TIMER_C;
	TIMER_C	timer_c_;
```

## 初期化

初期化では、楽譜の運用で利用する 100Hz のタイマーＢ、PWM を生成するタイマーＣチャネルを初期化します。

```cpp
	// タイマーＢ初期化
	{
		uint8_t ir_level = 2;
		timer_b_.start(TICK, ir_level);
	}

	// タイマーＣ初期化ＰＳＧ用
	{
		// SAMPLE 周期
		utils::PORT_MAP(utils::port_map::P12::TRCIOB);
		uint8_t ir_lvl = 2;
		timer_c_.start_psg(TRC_LIM, TRC_DIV, ir_lvl);
	}
```

## メインループ

- スコア構造体のポインターを、サウンドマネージャーに登録します。
- メインループは、１００Ｈｚで動かしておき、ＰＳＧのデータ生成を行いバッファに格納します。
- ここで、重要なのは、１００Ｈｚで動作するループは、ＰＷＭのループと非同期になっています。
- ＰＷＭで使われたデータ量を取得して、ＰＳＧのデータ生成を行います。
- この工夫により、データが途切れる事なく、溢れる事も無く、潤沢に必要な分だけデータを生成する事が出来ます。

```cpp

	psg_mng_.set_score(0, score0_);
	psg_mng_.set_score(1, score1_);

	auto pos = pwm_pos_;
	uint8_t delay = 100;
	while(1) {
		timer_b_.sync();

		auto org = pwm_pos_;
		auto n = org - pos;
		psg_mng_.render(n & (BSIZE - 1));
		pos = org;

		if(delay > 0) {
			delay--;
		} else {
			psg_mng_.service();
		}
	}
```

---

### プロジェクト

https://github.com/hirakuni45/R8C/tree/master/PSG_sample

## まとめ

C++ では、テンプレートを活用する事で、柔軟で構造的なモジュールとして実装する事が出来るので、再利用がしやすいと思います。

psg_mng.hpp クラスは、RL78、RX マイコンでも同じように利用可能です。

C++14 以降のコンパイラが使える環境なら、別のプラットホームでも利用可能だと思います。

---

## サンプルの楽曲

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58667/8c2098a0-3a67-13d7-1e77-99b329733021.png" width="50%">

サンプルとして「ドラゴンクエスト１・ラダトーム城」（ピアノバージョン）のスコアを入れてあります。
※途中まで・・

この楽曲データを参考にすれば、他の楽曲でも、又は、自分で作曲した楽譜でも、要領がつかめると思います。
※制御コマンドは殆ど使っていません。
※制御コマンドは今後改修の予定があります。

https://www.youtube.com/watch?v=tQyGLMxu-qA

---

## 参考リンク

前の記事：

https://qiita.com/hira_kuni_45/items/84d7490caab772776a93

開発環境の構築など：

https://qiita.com/hira_kuni_45/items/ce7c493756daa5fc1a41

format テンプレートクラス：

https://qiita.com/hira_kuni_45/items/790d379034c747e66d40

constexprとconstを正しく使い分ける:

https://qiita.com/saltheads/items/dd65935878a0901fe9e7

楽譜の読み方：

https://www.soundhouse.co.jp/howto/keyboard/sheet/