144Labの入江田です。 今回はM5Stack Core2で複数の音素材を同時に鳴らす方法をまとめてみました。
素材の入手
音素材の入手はフリー素材を公開しているサイトから。 素材の利用規約をよく読んで利用しましょう。 サンプリングレートは44.1KHzのものを入手しましょう。 (異なる場合はAudacityなどでサンプリングレートのリサンプルで変換を)
ffmpeg -i sound1.wav -f s16le -acodec pcm_s16le sound1.raw xxd -i sound1.raw sound1.h
xxd -i
はバイナリファイルをCコンパイラで解釈可能なunsigned char
配列定義のソースコードに変換してくれます。
出力されたヘッダに著作権表示とconst
指定を追加します。
// Copyright NHK const unsigned char sound1_raw[] = {...}
constを付けない場合コンパイラがRAM領域にこのデータを置こうとしてサイズがでかい場合すぐにメモリが不足します。constをつけておくとコード領域(FLASHメモリ)に置くのでこちらは比較的容量に余裕があります。上記の手法で2〜3サンプル用意しましょう。
M5Core2用Arduino環境をセットアップ
- https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json をパッケージURLsに追加
- IDEの場合:
M5Stack by M5Stack official
サポートをインストールします。 - CLI版の場合: arduino-cli core update-index; arduino-cli core install m5stack:esp32
- IDEの場合: M5Core2ライブラリをインストール
- CLIの場合: arduino-cli lib update-index;arduino-cli lib install M5Core2
サンプルコード
InitI2SSpeakOrMic関数や定数宣言のほとんどはM5Core2のSpeakerサンプルのコードそのままです。今回のキモになるのはSourceクラスの定義とspk_output関数の処理です。 sourcesに複数のSourceインスタンスを初期化していますが、これが最大同時発音数になっています。
- SourceのNext()メソッドは次のサンプルデータを取り出す処理。
- SourceのStart()メソッドはptrを音素材の先頭に巻き戻して音の再生を開始します。
- spk_outputでは3つのSoundのNext()結果を加算合成して32サンプルの出力波形をbuffに詰め込んでそれをi2s_writeで出力します。
- SourceのNext()では音素材を全て出力し終えたらゼロを返します(無音になる)。
#include <M5Core2.h> #include <driver/i2s.h> #include "sound1.h" #include "sound2.h" #include "sound3.h" #define CONFIG_I2S_BCK_PIN 12 #define CONFIG_I2S_LRCK_PIN 0 #define CONFIG_I2S_DATA_PIN 2 #define CONFIG_I2S_DATA_IN_PIN 34 #define Speak_I2S_NUMBER I2S_NUM_0 #define SAMPLE_RATE 44100 #define MODE_MIC 0 #define MODE_SPK 1 #define DATA_SIZE 32 bool InitI2SSpeakOrMic(int mode) { esp_err_t err = ESP_OK; i2s_driver_uninstall(Speak_I2S_NUMBER); i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER), .sample_rate = SAMPLE_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // is fixed at 12bit, stereo, MSB .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT, .communication_format = I2S_COMM_FORMAT_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 2, .dma_buf_len = 128, }; if (mode == MODE_MIC) { i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM); } else { i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX); i2s_config.use_apll = false; i2s_config.tx_desc_auto_clear = true; } err += i2s_driver_install(Speak_I2S_NUMBER, &i2s_config, 0, NULL); i2s_pin_config_t tx_pin_config; tx_pin_config.bck_io_num = CONFIG_I2S_BCK_PIN; tx_pin_config.ws_io_num = CONFIG_I2S_LRCK_PIN; tx_pin_config.data_out_num = CONFIG_I2S_DATA_PIN; tx_pin_config.data_in_num = CONFIG_I2S_DATA_IN_PIN; err += i2s_set_pin(Speak_I2S_NUMBER, &tx_pin_config); err += i2s_set_clk(Speak_I2S_NUMBER, SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO); return true; } class Source { const int16_t *start; int length; int16_t *ptr; int remain; public: Source(const unsigned char *buff, int length) { this->start = (const int16_t *)(buff); this->length = length / sizeof(int16_t); this->remain = 0; }; int16_t Next() { int16_t res = 0; if (this->remain > 0) { res = (*(this->ptr++)) / 2; this->remain--; } return res; }; void Start() { this->ptr = (int16_t *)(this->start); this->remain = this->length; }; }; Source *sources[] = { new Source(sound1_raw, sound1_raw_len), new Source(sound2_raw, sound2_raw_len), new Source(sound3_raw, sound3_raw_len), }; void spk_output() { static int16_t buff[DATA_SIZE]; for (int i = 0; i < DATA_SIZE; i++) { int16_t output = 0; for (int n = 0; n < 3; n++) { output += sources[n]->Next(); } buff[i] = output; } size_t bytes_written = 0; i2s_write(Speak_I2S_NUMBER, buff, DATA_SIZE, &bytes_written, portMAX_DELAY); } void setup() { Serial.begin(115200); M5.begin(true, true, true, true); M5.Axp.SetSpkEnable(true); InitI2SSpeakOrMic(MODE_SPK); M5.Lcd.setTextSize(3); M5.Lcd.print("Audio Example"); } void loop() { M5.update(); if (M5.BtnA.wasPressed()) sources[0]->Start(); if (M5.BtnB.wasPressed()) sources[1]->Start(); if (M5.BtnC.wasPressed()) sources[2]->Start(); spk_output(); }
発展
- 今回は3つの素材で3つの同時発音を固定で紐づけて簡略化しています
- 本来は同時発音数を決めて、Sourceと素材との紐付けは動的に行うのが理想です
- 最終出力波形には不連続なものを出してはいけません
- 音素材は鳴った後、無音になるまでの波形が含まれるのでフェードアウトの処理を省略しています
- 素材鳴動を中断する要件がある場合フェードアウトする処理が必須です
- 素材鳴動を途中から再開する必要があるならフェードインする処理が必要です
動作例
音素材は NHKクリエイティブ・ライブラリーより