M5Stack での Raw Streaming PCM 再生方法（Core2 & CoreS3対応）

# はじめに

M5Stack で PCM 生データ（Raw PCM）のストリーミング再生をする方法を調べてみました。

## ターゲットデバイス（Core2 / CoreS3）

ターゲットデバイスは当初、普及している Core2 にしようと思ったのですが、調査の過程で[手持ちのCore2が壊れてしまった](https://github.com/suzukiplan/micro-msx2p/pull/28#issuecomment-1637015900)ので、買い直すことにしたのですが、せっかくだから新しく発売された CoreS3 の方を書い直しました。

Core2 と CoreS3 では、オーディオを再生する仕組みが全く異なります。

### Core2のざっくりとしたオーディオ再生の仕組み

I2S で ポート0 (I2S_NUM_0) に PCM データを DMA 転送することで DAC 変換が走り、本体スピーカーで音声が鳴ります。（とてもシンプル）

### CoreS3のざっくりとしたオーディオ再生の仕組み

[AW88298](https://doc.awinic.com/doc/20230609wm/78b5dc17-223f-4371-9a31-a6640f883a0c.pdf) というオーディオアンプのチップが新たに搭載され、従来の DAC の仕組みは廃止されたようです。

## 一番簡単な方法（M5Unified）

M5Unified の Speaker を用いる方式が最も簡単だと思われます。

以下のように初期化し、

```c++
auto config = M5.Speaker.config();
config.sample_rate = 44100;
M5.Speaker.config(config);
M5.Speaker.setVolume(255);
M5.Speaker.begin();
```

以下のように Raw PCM を書き込むことで Core2 / CoreS3 のどちらでもオーディオ再生ができるようです。

```c++
while (M5.Speaker.isPlaying()) { vTaskDelay(1); }
M5.Speaker.playRaw(buf, bufSize, 44100, false);
```

ただし、M5Unified は内部的に PCM を再生するタスクへバッファを渡す形で処理しており、その処理方式だとオーディオストリーミングの用途（短いPCMデータを連続して永久に書き込み続ける用途）には向いていません。

_恐らく、固定のPCMバッファを再生する用途を想定した API だと考えられます。_

## Core2の処理方式（44100Hz, 16bits, monoral）

Core2 の場合、I2Sのポート番号０（`I2S_NUM_0`）にデータを書き込むだけで DAC 変換されるので、単純に以下のように I2S を初期化して、

```c++
i2s_config_t audioConfig = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate = 44100,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
    .communication_format = I2S_COMM_FORMAT_STAND_MSB,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 4,
    .dma_buf_len = 1024,
    .use_apll = false,
    .tx_desc_auto_clear = true};
i2s_driver_install(I2S_NUM_0, &audioConfig, 0, nullptr);
i2s_set_clk(I2S_NUM_0, 44100, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
i2s_zero_dma_buffer(I2S_NUM_0);
```

以下のように書き込み続けることでストリーミング再生ができました。

```c++
size_t wrote;
i2s_write(I2S_NUM_0, buf, bufSize, &wrote, portMAX_DELAY);
vTaskDelay(2);
```

I2S の DMA バッファサイズの上限は 1,024 バイトなので、`buf` と `bufSize` は 1,024 バイトづつで渡し続けるのがベストです。

なお、上記では 16bits (I2S_BITS_PER_SAMPLE_16BIT) の PCM を書き込むようにしていますが、Core2のDACは内部的に 8bits しか処理できないらしいので、可能であれば波形データのバッファリングを 8bits にした方が良いと考えられます。

ただし、CoreS3の AW88298 は 8bits PCM をサポートしていない（16bits 以降のみサポートしている）ので、Core2 と CoreS3 の両対応をしたい場合、16bitsでバッファリングにせざるを得ないかもしれません。

## CoreS3の処理方式（44100Hz, 16bits, monoral）

AW88298 も I2S インタフェースのオーディオチップですが、個別に AW88298 のレジスタ設定 → IS2 config設定を行う必要があり、ついでに AW88298 の GPIO へのピンアサインも必要です。

具体的には、次のように初期化します。

```c++
// setup AW88298 regisgter
M5.In_I2C.bitOn(0x36, 0x02, 0b00000100, 400000);
this->writeRegister(0x61, 0x0673);
this->writeRegister(0x04, 0x4040);
this->writeRegister(0x05, 0x0008);
this->writeRegister(0x06, 0b0001110000000111); // I2SCTL: 44.1kHz, 16bits, monoral (他は全部デフォルト値)
this->writeRegister(0x0C, 0x0064);
// I2S config
i2s_config_t config;
memset(&config, 0, sizeof(i2s_config_t));
config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX);
config.sample_rate = 48000; // dummy setting
config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT;
config.communication_format = I2S_COMM_FORMAT_STAND_I2S;
config.dma_buf_count = 4;
config.dma_buf_len = 1024;
config.tx_desc_auto_clear = true;
// I2S pin config
i2s_pin_config_t pinConfig;
memset(&pinConfig, ~0u, sizeof(i2s_pin_config_t));
pinConfig.bck_io_num = GPIO_NUM_34;
pinConfig.ws_io_num = GPIO_NUM_33;
pinConfig.data_out_num = GPIO_NUM_13;
// Setup I2S
i2s_driver_install(I2S_NUM_1, &config, 0, nullptr);
i2s_set_pin(I2S_NUM_1, &pinConfig);
i2s_zero_dma_buffer(I2S_NUM_1);
i2s_start(I2S_NUM_1);
```

ストリーミングデータの書き込み処理は、（I2Sポート番号が `I2S_NUM_0` → `I2S_NUM_1` になっている点を除き）Core2と同じです。

```c++
size_t wrote;
i2s_write(I2S_NUM_1, buf, bufSize, &wrote, portMAX_DELAY);
vTaskDelay(2);
```

## 実装例

### 本書解説処理の実装クラス

https://github.com/suzukiplan/micro-msx2p/blob/master/msx1-m5stack/include/Audio.hpp

### 前項クラス（Audio class）の利用例

以下の `audio.begin` と `audio.write` の呼び出し箇所を参照

https://github.com/suzukiplan/micro-msx2p/blob/master/msx1-m5stack/src/app.cpp

> MSX1 のエミュレータが出力した波形データを書き込み続ける形になってます。