たま日記

たまに書く

NimBLE + BLE MIDIで音を出した

今回の実験

M5Stackで次を調べてみた。

  • BLE MIDIで接続しながらcore0でリバーブ処理+I2S出力が可能か?
  • core1とcore0をキューでつないで音を出したときに遅延などは有るか?

1つ目は処理能力の調査。esp32ではcore0とcore1でマルチタスクができる。「無線関連の処理はcore0で行うので、core0には大きな負荷をかけられない」等の記述をよく見るが、どの程度の負荷だとまずいのかがよくわからなかった。今回はFs=44100HzでFreeverb※を動かし、かつ、I2S出力したときにノイズが乗ったりしないかを調べてみた。

2つ目はキューの調査。波形作成とリバーブを別のcoreで実行したときに聴覚的?な遅延が起きないかどうかを調べてみた。

どちらも特に問題なさそうだった。

※これまでの工作で、1つのcoreでYM2414エミュレータで8ch出音させながらステレオFreeverbをかけると処理が間に合わないことが判明している(Fs=44100Hzでノイズが乗ってしまう)。音声合成とリバーブを分けることはできるか?ついでにBLE MIDIで音色エディットしたいなできるかな?というのが今回の実験の動機。

機材

M5Stackuda1334a I2SステレオDACモジュールを使う。M5StackのM-BUSとブレッドボードをピンヘッダでつないで固定した。アンプにはつないでない。

M5Stack uda1334a
G13 BCLK
G5 WSEL
G17 DIN
3.3V VIN
GND GND

f:id:androuet:20220126162923j:plain

プログラムの動作

  • core1で正弦波・矩形波・鋸歯状波を作成して、キューでcore0のタスクに送る。ボタンを押して波形を選択する。押すたびに周波数が高くなる。
  • core1では125msおきにMIDIのnoteOn/Offを繰り返している。Windows10上のLMMSとiPad上のKORG iM1で音が出るのを確認できた。
  • core0ではFreeverbでリバーブをかけてI2Sで出力する。
  • core0ではNimBLEでBLE MIDI通信をしている(はず)。

BLE MIDIのレイテンシ

BLE MIDIには特有の遅延があるらしい。この記事の実験ではesp32+NimBLEで概ね40msの遅延があることがわかる。 pointofviewpoint.linclip.com

「40ms」というと短い時間で問題なさげに感じたのだが、1秒間に等間隔に4回noteOn/Offするような演奏だと結構な影響がある。今回の実験でも(詳細に計測はしていないが)ぎこちないというか不ぞろいな音がPCからもiPadからも聴こえてくる。

原因はBLEの送信間隔にあるらしく解決方法はこれを短くすることにあるらしい。次の記事では解決方法を提示している。 www.reddit.com

今回利用したライブラリにも適用できないか調べてみた。プロジェクトフォルダ/.pio/libdeps/m5stack-core-esp32/BLE-MIDI/src/hardware/BLEMIDI_ESP32_NimBLE.hの中に、接続時にコールバックされるMyServerCallbacksというクラスがある。これを次のように変更してみた。

    /* コメントアウトする
    void onConnect(BLEServer *)
    {
        if (_bluetoothEsp32)
            _bluetoothEsp32->connected();
    };
    */
    // これを追加する
    void onConnect(BLEServer *pServer, ble_gap_conn_desc* desc) {
        if (_bluetoothEsp32)
            _bluetoothEsp32->connected();
        pServer->updateConnParams(desc->conn_handle, 6, 8, 0, 400);
    }

ずいぶんマシになった。

その他

  • Windows10にはKORG BLE-MIDE Driverをインストールしている。無いと接続できないかどうかは試していない。
  • Windows10の設定→デバイスBluetoothまたはその他のデバイスを追加する→Bluetoothで接続する。「接続済み」になってしばらくすると「ペアリング済み」になってしまうが、LMMSでMIDI入力を有効にしていれば「接続済み」のままになる。
  • iPadと接続する方法:設定→Bluetoothからつなぐのではなく、iM1のSETTINGSからつなぐ(知らなかった)。

プログラム

platformio.ini

[env:m5stack-core-esp32]
platform = espressif32
board = m5stack-core-esp32
framework = arduino
lib_deps = 
    lathoub/BLE-MIDI@^2.2
    m5stack/M5Stack@^0.3.9
monitor_speed = 115200

main.cpp

#include <Arduino.h>
#include <M5Stack.h>
#include <BLEMIDI_Transport.h>
#include <hardware/BLEMIDI_ESP32_NimBLE.h>
#include <driver/i2s.h>
#include <FreeRTOS.h>

// Freeverb使うなら有効にする
//#define USE_FREEVERB

#define SAMPLE_RATE (44100)
#define FRAMES_PER_BUFFER (64)
#define VOLMIN (1/32768.0f)

BLEMIDI_CREATE_DEFAULT_INSTANCE()

unsigned long t0 = millis();
bool isConnected = false;
QueueHandle_t queue;
volatile int doReverb = false;

#ifdef USE_FREEVERB
#include "revmodel.hpp"
    revmodel *freeverb;
#endif

/* core0で動かすタスク */
void playbuf(void *param) {
    int16_t* i2sBuf = (int16_t*)malloc(FRAMES_PER_BUFFER * sizeof(int16_t) *2); // 右+左で *2
    int count = 0;
    size_t written;
    float revInput1[FRAMES_PER_BUFFER], revInput2[FRAMES_PER_BUFFER];
    float revOutput1[FRAMES_PER_BUFFER], revOutput2[FRAMES_PER_BUFFER];
    float *input1, *input2;
    int buf[FRAMES_PER_BUFFER];
#ifdef USE_FREEVERB    
    freeverb = new revmodel();
#endif

    while (1) {
        xQueueReceive(queue, buf, 10);
#ifdef USE_FREEVERB
        if (doReverb) {
          input1 = revInput1;
          input2 = revInput2;
          for (int i=0; i<FRAMES_PER_BUFFER; i++) {
            *input1++ = *input2++ = buf[i] * VOLMIN;
          }
          freeverb->processreplace(revInput1, revInput2, revOutput1, revOutput2, FRAMES_PER_BUFFER, 1);
          for (int i=0; i<FRAMES_PER_BUFFER; i++) {
            i2sBuf[i*2]   = revOutput1[i] * 32767; // 左
            i2sBuf[i*2+1] = revOutput2[i] * 32767; // 右
          }
        } else {
#endif
          for (int i=0; i<FRAMES_PER_BUFFER; i++) {
            i2sBuf[i*2] = i2sBuf[i*2+1] = buf[i];
          }
#ifdef USE_FREEVERB
        }
#endif
        i2s_write((i2s_port_t)0, (const void *)i2sBuf, (size_t)(FRAMES_PER_BUFFER*4), &written, 1000);
        if ((++count & 0x3ff) == 0) {
            vTaskDelay(1); // 必要?
        }
    }
}

void setup()
{
  Serial.begin(115200);

  M5.begin();
  M5.Power.begin(); //Init Power module.

  MIDI.begin();
  BLEMIDI.setHandleConnected([]() {
    isConnected = true;
  });
  BLEMIDI.setHandleDisconnected([]() {
    isConnected = false;
  });
  MIDI.setHandleNoteOn([](byte channel, byte note, byte velocity) {
  });
  MIDI.setHandleNoteOff([](byte channel, byte note, byte velocity) {
  });

  i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
    .intr_alloc_flags = 0,
    .dma_buf_count = 4,
    .dma_buf_len = 64
  };
  i2s_pin_config_t pin_config = {
    .bck_io_num = 13,   // BCLK
    .ws_io_num = 5,     // WSEL
    .data_out_num = 17, // DIN
    .data_in_num = -1   // Not used
  };
  i2s_driver_install((i2s_port_t)0, &i2s_config, 0, NULL);
  i2s_set_pin((i2s_port_t)0, &pin_config);
  i2s_set_clk((i2s_port_t)0, SAMPLE_RATE, (i2s_bits_per_sample_t)16, (i2s_channel_t)2);

  queue = xQueueCreate(2, 4*64);
  xTaskCreatePinnedToCore(playbuf, "Task0", 4096, NULL, 1, NULL, 0);
}

int on = 0;
int count = 0;

void loop()
{
  size_t written;
  int16_t* i2sBuf = (int16_t*)malloc(FRAMES_PER_BUFFER * sizeof(int16_t) *2); // 右+左で *2
  int sendbuf[FRAMES_PER_BUFFER];
  int wave = 0;
  float phase = 0;
  float step0 = (float)(TWO_PI / 200);
  float step = step0;
  float rate = 1.06;
  float vol = 500;
  int lastt0;

  while (true) {
    int pressed = false;
    M5.update();
    if (M5.BtnA.wasPressed()) {
      wave = 0; pressed = true;
    } else if (M5.BtnB.wasPressed()) {
      wave = 1; pressed = true;
    } else if (M5.BtnC.wasPressed()) {
      wave = 2; pressed = true;
    }
    if (pressed) {
      step *= rate;
      if (step > step0 * 4) step = step0;
      doReverb = doReverb ? false : true;
    }

    for (int i=0; i<FRAMES_PER_BUFFER; i++) {
      switch (wave) {
        case 0: sendbuf[i] = (int16_t)(vol * sin(phase)); break;
        case 1: sendbuf[i] = phase < PI ? -vol : vol; break;
        case 2: sendbuf[i] = vol * 2 * phase / TWO_PI - vol; break;
      }
      phase += step;
      if (phase >= TWO_PI) phase -= TWO_PI;
      vol *= 0.9998;
      if (vol < 10) vol = 500;
    }
    xQueueSend(queue, sendbuf, (TickType_t)10);

    if ((++count & 0x3ff) == 0) {
      vTaskDelay(1); // 必要?
    }

    MIDI.read();    
    if (isConnected && (millis() - t0) >= 125)
    {
      t0 = millis();
      if (on) {
        MIDI.sendNoteOn(84, 100, 1); // note 84, velocity 100 on channel 1
        on = 0;
        Serial.printf("connect: %d\n", t0 - lastt0);
        lastt0 = t0;
      } else {
        MIDI.sendNoteOff(84, 0, 1); // note 84, velocity 100 on channel 1
        on = 1;
      }
    }
  }
}