PSG 音源を生成するテンプレートクラス
最近、R8C 関係の GitHub を整理している。
将来的に、初心者(小学生、中学生)向けの電子工作向けボードを作る予定で、それに向けたものだ。
1000円くらいで、電子工作とプログラミングの初級が学べて、何か作れるような物をリリースしたい。
そこで、R8C 用色々なコンテンツを作成している、その一環で PSG 音源を使った音楽再生を行ってみた。
詳しい記事は、Qiita にある。
とりあえず、R8C でそれなりに鳴ったので、RX マイコンにも移植してみた。
実験したのは、RX72N Envision Kit で、このデバイスには、SSIE 接続の D/A があるので、簡単だ。
サンプリング周波数は 48KHz 固定なので、多少オーバースペックかもしれない。
PSG とは?
PSG は、大昔に、矩形波を基本とする音源で、音楽を演奏するデバイスが売られていたのがルーツと思う。
AY-3-8910 とか、そんなデバイスが発売されて、Apple][ 用のボードで演奏したのを覚えている・・
※多分高校生くらいだったから、電子音楽の衝撃は今でも忘れない。
※今考えると楽譜を入力するオーサリングツールは素晴らしく良く出来ていた。
ファミコンでは、それに似た仕様の音源が内蔵されている。
C++ テンプレートの柔軟性
元は、R8C 用に実装したものだが、ほぼ無改造でそのまま再利用出来た。
※実際は、サンプリング周波数が高く、内部で演算がオーバーフローしたので、多少改修した。
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
/*!
@brief PSG Manager テンプレート class
@param[in] SAMPLE サンプリング周期
@param[in] TICK 演奏tick(通常100Hz)
@param[in] BSIZE バッファサイズ(通常512)
@param[in] CNUM チャネル数(通常4)
*/
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
template <uint16_t SAMPLE, uint16_t TICK, uint16_t BSIZE, uint16_t CNUM>
class psg_mng : public psg_base {
psg_mng テンプレートクラスのプロトタイプは上記のようになっていいて、
- サンプリング周期
- 演奏 TICK
- バッファサイズ
- チャネル数
を与えるようになっている。
※バッファサイズはこのモジュールとは直接関係無いので、除く方が良さそうだが、とりあえず、R8C 版との互換でそのままにしてある。
TICK を 100Hz、サンプリングを 48KHz とすると、480 バイトのデータ列を生成する必要があるので、バッファは 512 バイト。
※1サンプルづつ波形を引っ張るのなら、バッファサイズは含めなくて良い。
R8C では、メモリを節約する為、psg_mng で波形を生成したデータを、直接、PWM ストリームのバッファにしていたが、RX マイコンでは、構成が異なるので、一旦テンポラリに生成して、それを、サウンド出力クラスに食わすようにしている。
{
uint32_t n = SAMPLE / TICK;
psg_mng_.set_wav_pos(0);
psg_mng_.render(n);
typename SOUND_OUT::WAVE t;
for(uint32_t i = 0; i < n; ++i) {
uint8_t w = psg_mng_.get_wav(i);
w -= 0x80;
t.l_ch = t.r_ch = (static_cast<int16_t>(w) << 8) | ((w & 0x7f) << 1);
sound_out_.at_fifo().put(t);
}
if(delay > 0) {
delay--;
} else {
psg_mng_.service();
}
}
SSIE と波形出力テンプレート定義
これは、オーディオを扱うアプリで散々利用してきたテンプレートなので、定義も使い方も、変わる処が無い。
この辺りの柔軟性は C++ で実装した場合の特典みたいなものだと思う。
#ifdef USE_DAC
typedef sound::dac_stream<device::R12DA, device::TPU0, device::DMAC0, SOUND_OUT> DAC_STREAM;
DAC_STREAM dac_stream_(sound_out_);
void start_audio_()
{
uint8_t dmac_intl = 4;
uint8_t tpu_intl = 5;
if(dac_stream_.start(48'000, dmac_intl, tpu_intl)) {
utils::format("Start D/A Stream\n");
} else {
utils::format("D/A Stream Not start...\n");
}
}
#endif
#ifdef USE_SSIE
typedef device::ssie_io<device::SSIE1, device::DMAC1, SOUND_OUT> SSIE_IO;
SSIE_IO ssie_io_(sound_out_);
void start_audio_()
{
{ // SSIE 設定 RX72N Envision kit では、I2S, 48KHz, 32/16 ビットフォーマット固定
uint8_t intr = 5;
uint32_t aclk = 24'576'000;
uint32_t lrclk = 48'000;
auto ret = ssie_io_.start(aclk, lrclk, SSIE_IO::BFORM::I2S_32, intr);
if(ret) {
ssie_io_.enable_mute(false);
ssie_io_.enable_send(); // 送信開始
utils::format("SSIE Start: AUDIO_CLK: %u Hz, LRCLK: %u\n") % aclk % lrclk;
} else {
utils::format("SSIE Not start...\n");
}
}
}
#endif
SSIE インターフェース用と、内蔵 D/A 用の二種類がある。
※RX72T、RX66T、RX24T、など、D/A 内臓のデバイスでも簡単に流用出来ると思う。
他、オーディオ出力コンテキストにサンプリング周波数を設定する関数を出してある。
void set_sample_rate(uint32_t freq)
{
#ifdef USE_DAC
dac_stream_.set_sample_rate(freq);
#endif
#ifdef USE_SSIE
sound_out_.set_output_rate(freq);
#endif
}
初期化
初期化では、TICK タイマー、オーディオ出力など初期化する。
また、サンプルで入れた楽曲のスコアテーブルを設定する。
{
uint8_t intr = 4;
cmt_.start(TICK, intr);
}
start_audio_();
{ // サンプリング周期設定
set_sample_rate(SAMPLE);
}
psg_mng_.set_score(0, score0_);
psg_mng_.set_score(1, score1_);
メインループ
後は、psg_mng テンプレートクラスで波形を生成して、オーディオ出力に渡すだけとなる。
その際、8ビットの波形を16ビットに拡張している。
又、モノラルなので、ステレオにしている。
uint8_t cnt = 0;
uint8_t delay = 200;
while(1) {
cmt_.sync();
{
uint32_t n = SAMPLE / TICK;
psg_mng_.set_wav_pos(0);
psg_mng_.render(n);
typename SOUND_OUT::WAVE t;
for(uint32_t i = 0; i < n; ++i) {
uint8_t w = psg_mng_.get_wav(i);
w -= 0x80;
t.l_ch = t.r_ch = (static_cast<int16_t>(w) << 8) | ((w & 0x7f) << 1);
sound_out_.at_fifo().put(t);
}
if(delay > 0) {
delay--;
} else {
psg_mng_.service();
}
}
++cnt;
if(cnt >= 50) {
cnt = 0;
}
if(cnt < 25) {
LED::P = 0;
} else {
LED::P = 1;
}
}
楽曲の定義
最近覚えた、便利な使い方:
C++11 になって、不便で不完全な「enum」から解放された。
「enum class」は、型に厳密で、異なった型を受け付けない、もちろん、整数型から派生しているので、「static_cast」を使って変換可能だ。
厳密になった為、色々な「型」を組み合わせるようなデータ列では不便となる。
そこで「union」を使って、利用出来る型を受け付けるようにしておき、コンストラクターで、異なる型からデータ列を生成するようにした。
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
/*!
@brief スコア・コマンド構造 @n
・KEY, len @n
・TR, num @n
・TEMPO, num @n
・FOR, num
*/
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
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) { }
};
これで、スコアデータ列を構造的に作れるようになった。
※コンパイル時に値を決定して ROM 化するので「constexpr」を付加している。
この簡単な構造体で以下のように楽譜データを記述出来る。
constexpr PSG::SCORE score0_[] = {
PSG::CTRL::VOLUME, 128,
PSG::CTRL::SQ50,
PSG::CTRL::TEMPO, 80,
PSG::CTRL::ATTACK, 175,
// 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,
まとめ
やはり、C++ の柔軟性と、再利用性は優れている。
8/16 ビットマイコン用ソースコードが、RX マイコンでも、ほぼそのまま再利用出来た。
当然だが、R8C の PWM 再生に比べると、高品質だ。