自作液晶コントローラでMIDI音源のディスプレイを作る

## はじめに

こちらは鈴鹿高専 Advent Calendar 2021の2日目の記事です．

https://qiita.com/advent-calendar/2021/snct



注意事項としては，ここで記載されているコードを他のプロダクトでそのまま使うのは**禁止**とします．

何かバグがあっても責任を取れないためです．

また，実際に何かを制作する際には必ず一次情報(データシートなど)を参考にしてください．



# 作ったもの

https://twitter.com/LunaTsukinashi/status/1435604731645202440



MIDIを受け取ってその情報をディスプレイに出します．

GM/GS/XGのシステムエクスクルーシブに対応し，GM音色配列の名前とグラフィックを持っています．



## 仕組み

このMIDI音源ディスプレイは自作の液晶コントローラ"Sapphire2"上で動作します．

とはいっても，処理を軽量化するために最下層しか叩いていません．

初期では拙作のGraphicsライブラリを律儀に叩いていましたが画面の更新が間に合わず自前でデータを生成することになりました．

さて，このMIDI音源ディスプレイですが液晶コントローラ部と同様に

- MIDIパーサ
- 表示データ作成

の２つに機能を分割しています．

それぞれについて述べていきます．



### MIDIパーサ

何もいうことはありません．

MIDI1.0規格書に従ってシンプルにまとめました．

以下のエクスクルーシブに対応しています．

```
(GM SYSTEM ON)0xF0	0x7E	0x7F	0x09	0x01	0xF7
(XG SYSTEM ON)0xF0	0x43	0x01	0x4C	0x00	0x00	0x7E	0x00	0xF7
(GS RESET)		0xF0	0x41	0x10	0x42	0x12	0x40	0x00	0x7F	0x00	0x41	0xF7
(GS MODE1)		0xF0	0x41	0x10	0x42	0x12	0x00	0x00	0x7F	0x00	0x01	0xF7
```

以下のイベントを認識可能です．

- モジュレーション
- パン
- エクスプレッション
- ボリューム
- ピッチベンド
- プログラムチェンジ(with LSB)
- ノートオン
- ノートオフ
- RPN/データエントリ

表示に必要のない部分は省きました．



## 表示データの作成

拙作のLCDライブラリとsprintf互換関数を使用してハメこんでいくだけです．

あらかじめROMにフォントデータと音色配列，グラフィックデータを持っており，必要に応じて書き換えます．

そのため，コードを見ると大量の`memset`，`memcpy`が見えます...

本当はDMAを使った方が高速で良いんでしょうがこれのために1chつぶすのも勿体無い話なのでやりません，現状そこまで速度求めていませんし．

データの埋め込みはGCCの擬似命令を使用にROMに直接バイナリデータを埋め込んでいます．

STM303K8T6は64KBもFlashがあるため，フォントデータと音色データくらいなら割と入ります．残念ながら16ドットの日本語フォントは入らなかったので漢字は使えません．

後々なんとかしようと思っています．



さて，ここでは主にバーグラフの処理について述べます．

音色におけるADSRの概念を用いています．

簡易的なものなので実際の音量とは一致していませんが，雰囲気がつかめれば良いでしょう(殴)

A(アタック)段階では目標の値まで指数関数的にバーグラフの値を増加させていきます．

大体1.5乗ずつ値を増やしていきます．

次にD(ディケイ)段階では線形的にサステインレベルまで減少させていきます．

S(サステイン)段階ではノートオフが来るまでその値を維持します．

最後にR(リリース)段階ではノートオフ後に線形的に(Dよりも速く)値を減少させ，0にします．

これを16ch，全てのノーツに対して行います．

そのため，計算用のテーブルを持つことで操作を高速にしています．

例えば，アタックレートに関しては

```c
const uint8_t step_attack[8] = {
		ATTACK_RATE,
		ATTACK_RATE * pow(1.5,1),
		ATTACK_RATE * pow(1.5,2),
		ATTACK_RATE * pow(1.5,3),
		ATTACK_RATE * pow(1.5,4),
		ATTACK_RATE * pow(1.5,5),
		ATTACK_RATE * pow(1.5,6),
		ATTACK_RATE * pow(1.5,7),
};

```

こんな感じで簡単にテーブルを持っておきます，これだけでもかなり差が出ます．

```c
void Update_Value(void) {
	for (int i = 0; i < 16; i++) {
		switch (status[i]) {
			case ATTACK:
				if (current_value[i] + step_attack[step[i]] > target_value[i][ATTACK]) {
					current_value[i] = target_value[i][ATTACK];
					status[i] = DECAY;
				} else {
					current_value[i] += step_attack[step[i]];
					step[i] = (step[i] < 8)? step[i]+1 : 7;
				}
				break;
			case DECAY:
				if (current_value[i] - DECAY_RATE < target_value[i][DECAY]) {
					current_value[i] = target_value[i][DECAY];
					status[i] = SUSTAIN;
				} else current_value[i] -= DECAY_RATE;
				break;
			case SUSTAIN:
				current_value[i] = target_value[i][SUSTAIN];
				break;
			case RELEASE:
				if (current_value[i] - RELEASE_RATE < 0) current_value[i] = 0;
				else current_value[i] -= RELEASE_RATE;
				break;
			default:
				current_value[i] = 0;
		}
	}
}
```

各ステートの管理はこのように行います．

この関数を0.05秒周期で呼び出し，滑らかなグラフの変化を実現しています．

グラフの表示処理は単純にROMに持っている表示データを算出した座標に貼り付けるだけなので簡単ですね．

```c
void Show_Vertical_Graph(uint8_t ch, uint8_t val) {
	uint8_t work_graph = 0x00;
	uint8_t val_int = val >> 2;
	for (uint8_t i = 0; i < 4; i++) {
		work_graph = 0x00;
		work_graph += (val_int >= 8) ? 8 : val_int;
		val_int = (val_int - 8 >= 0) ? val_int - 8 : 0;
		for (int j = 0; j < 8; j++) {
			memcpy(&graphics_buffer[30 * (((3 - i) << 3) + j) + ch],
					&Bar_Font_V[work_graph * 8 + j], 1);
		}
	}

}
```



## 感想

めずらしく構造的にプログラミングをしましたが，正直アセンブラの方が簡単だし書いてて楽しいです．

そろそろSTM32も入手難になってきたので見切りをつけて次のマイコンに移行しようと思っています．

問題等あればコメントまで．