M5Stack Core2で複数の音を鳴らす

f:id:irieda:20210326102443p:plain 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環境をセットアップ

サンプルコード

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