今回の実験
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で音色エディットしたいなできるかな?というのが今回の実験の動機。
機材
M5Stackとuda1334a I2SステレオDACモジュールを使う。M5StackのM-BUSとブレッドボードをピンヘッダでつないで固定した。アンプにはつないでない。
M5Stack |
|
uda1334a |
G13 |
→ |
BCLK |
G5 |
→ |
WSEL |
G17 |
→ |
DIN |
3.3V |
→ |
VIN |
GND |
→ |
GND |

プログラムの動作
- 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 *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>
#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
void playbuf(void *param) {
int16_t* i2sBuf = (int16_t*)malloc(FRAMES_PER_BUFFER * sizeof(int16_t) *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();
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,
.ws_io_num = 5,
.data_out_num = 17,
.data_in_num = -1
};
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);
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);
on = 0;
Serial.printf("connect: %d\n", t0 - lastt0);
lastt0 = t0;
} else {
MIDI.sendNoteOff(84, 0, 1);
on = 1;
}
}
}
}