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クリエイティブ・ライブラリーより