たま日記

たまに書く

2号機を組み立てた

ウィンドシンセの2号機を組み立てを終えた。

f:id:androuet:20220314164753j:plain

f:id:androuet:20220314171738j:plain

使った部品

各部の説明

筐体は再びエムケーダクトの0号を。今回は縦向きに使ってみた。ふたの面に配線しなくてもいいので組み立てやすい反面、スピーカーの設置に困った。結局スピーカーは小箱に入れて両面テープで固定した。ダクトの切断・加工には超音波カッターを使った。溶かすように切断できるので便利。

バッテリーは本当はリチウムイオンとかリチウムポリマーを使いたかった。でも充電回路に自信がないので、ダクト幅に収まる小型のモバイルバッテリーを探して使った。

キーは画鋲、スズメッキ線、ホームセンターで売っているミニステーで作った。オクターブキーは横に並べてみた。左手親指の動きを小さくすればオクターブ変更時のピロ音がなくなるかな、という目論見。サムフックは100均のコードフックにシリコンチューブを巻いた。

マウスピースは以前作ったものをそのまま利用。気圧センサ+バイトセンサ。これはもうちょっとなんとかしたい。1号機は耳栓、2号機はティッシュの栓。少し格好悪い。他の人の開発事例を見るとこんな感じの気圧センサをシリコンチューブにつないでブレスセンスしている。このほうが信号線が長くならないのでよさそう。現在の信号線はI2Cで、MPR121・GY-521・LPS33HWをつないでいる。

重さは292g。1号機は電池なしで205gなので、ほぼバッテリー分だけ増加した。手持ちのアルトヴェノーヴァは283g、アルトリコーダーは203g。もう少し軽くしたい。

次回?の課題

  • プリント基板の作成:はんだ付けを楽にしたい
  • 組み立て簡単化:どうしてもキーの配線がぐちゃぐちゃになる。キーを基板に組み込むか、筐体基板とマイコン基板に分けてI2Cだけで接続するかして簡単に組み立てられるようにしたい
  • バッテリー小型化:リチウムイオン電池18650+充電モジュールを使えばよい?

まだ各部の動作テスト用のプログラムしか作っていない。ウィンドシンセとして動作させるために、これからソフト開発。

基板を作った

ここしばらくウィンドシンセ2号機を作っていた。ようやく基板だけできた。初めてポリウレタン線を使ってみた。被膜を溶かすのは結構難しい。

f:id:androuet:20220228185324j:plain

使った部品は右から順に

2号機にはディスプレイを組み込みたかった。途中まで手元にあったM5Stackを使っていたのだけれど、少しケースからはみ出るので断念した。代わりに秋月のOLEDディスプレイを使った。コントローラはSSD1331。LovyanGFXが対応していたので、ありがたく使わせていただく。ものすごい簡単に表示できる素晴らしいライブラリ。寿限無寿限無

ESP32とステレオDACは1号機と同様I2Sで接続した。ところが今回はなぜか盛大にノイズが聴こえる。金属?の机の上に置いたり線を指で触れたりするとノイズは消える。ブレッドボードで仮組したときは普通に聞こえたのに何かおかしい。調べてみると、高周波では平行に配線するとノイズが発生するとのことだった。クロストークと言うらしい。

www.miyazaki-gijutsu.com

I2SのBCLK線をつなぐのに3本のフラットケーブルを使ってみた(GND・BCLK・GND)。これで解決した。2本でもよかったかも。ディジタル回路だから接続すれば動く、というわけではないことがよく分かった。プリント基板を発注する前に覚えることがいっぱいありそうだ。 …1号機が動いていたのは偶然?

このあとはブレスセンサとアンプとスピーカーをつないで筐体に組み込む予定。謎なトラブルが起きませんように。

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;
      }
    }
  }
}

BLE MIDIライブラリを使ってみた

以前BLE MIDIの記事を書いたのだけどVSCodeでは試していなかった。改めて、VSCode + PlatformIO + M5StackでBLE MIDIを使えるようにする方法を調べてみた。

マザーボード上のBluetoothを使って実験したのだが同梱されていたWi-Fiアンテナを付けないと、Bluetoothまで接続不可能だったり不安定になったりする(付けたら解決した)

※PIO HomeのタブはVSCodeの左端の縦に並んでいるPIOアイコン→QUICK ACCESS→PIO HOME→Openで開くことができる

(1)プロジェクトの作成

PIO Homeの「New Project」で新しいプロジェクトを作る。

  • Name : BLE test
  • Board : M5Stack Core ESP32
  • Framework : Arduino

BLE testというフォルダが作成され、その中に.pioとかlibとかsrcとかいろいろフォルダが作成される。

(2)標準?のライブラリを使う

BLE MIDIにはいくつかのライブラリが選択できる。まずは何も設定しないでも使える次のライブラリを試す。これを使ってるのかな?以前使ったものと同じ。
github.com

BLE_MIDI.inoをコピーしてプロジェクトBLE test/src/main.cppにペーストするだけで使える。ただし#include <Arduino.h>を追加する必要あり。ビルドしてM5StackにUploadする。書き込みサイズは979872 bytes。

(3)Windows10との接続

メニュー→設定→Bluetoothまたはその他のデバイスを追加する→Bluetoothを押す。サンプルコードに書いてある通り「ESP32 MIDI Example」として発見できる。事前にKORG BLE-MIDI Driverをインストールしておいた。

ここまでの作業で、0.5秒おきに真ん中のドを鳴らし続けるBLE MIDIバイスになる。例えばLMMSを起動して最初に表示されるTripleOscillatorを鳴らすことができる。

(4)ESP32-BLE-MIDIライブラリ

別のBLE MIDIライブラリを使ってみる。noteOn()とかsetPitchBendCallback()とか便利な関数が用意されている。
github.com
BLE test2というプロジェクトを新たに作ってPIO Homeの左端の縦に並んでいるLibrariesアイコンをクリックする。「BLE MIDI」で検索。いくつか表示される。「ESP32-BLE-MIDI by Maxime ANDRE」をクリックしてAdd to Project→Select a projectに「BLE test2」。Addするとプロジェクトにライブラリが追加される。具体的には

  • platformio.iniに「lib_deps = max22/ESP32-BLE-MIDI@^0.2.2」が追記される
  • BLE test2\.pio\libdeps\m5stack-core-esp32\ESP32-BLE-MIDI\にライブラリがダウンロードされる

Arduino IDEと違い、ライブラリはプロジェクトごとに管理されるらしい。lib_deps = ...を記述するだけでもOKらしい

サンプルコードはgithubにもあるVSCodeのライブラリのページ内にもある。01-Basic-Midi-DeviceのコードをBLE test2/src/main.cppに張り付けてビルド&アップロードする。1秒おきに真ん中のラを鳴らし続けるMIDIバイスになる。書き込みサイズは980672 bytes。

(5)BLE-MIDIライブラリ

もう一つのライブラリを使ってみる。プロジェクトにaddすると、一緒にArduinoBLE・MIDI Library・NimBLE-Arduinoライブラリがaddされる。
github.com

サンプルコードのうちMidiBle.inoをビルドしてみる。#includeを変更。hardware/BLEMIDI_ESP32_NimBLE.hを生かして、
hardware/BLEMIDI_ESP32.hの行をコメントアウトする。ESP32-NimBLE-MIDIという名前で発見できる。真ん中のドを鳴らし続けるMIDIバイスになる。書き込みサイズは560352 bytes。小さい。

(6)まとめ

最後に試したライブラリを使ってみよう。小さいので。

バイトセンサを作った

ピッチベンドを口でするために作った。まだ音を出していないが、シリアルプロッタの出力を見るといい感じで変化している。f:id:androuet:20220113164725j:plain

バイトセンサ(リップセンサ)については色々制作事例がある。この記事ではEWIのマウスピースに圧力センサを埋め込んでいる。「大改造③」で利用しているFSR400という圧力センサがよさそう。
ameblo.jp

こちらも圧力センサを使っているみたい。テープ?で固定している。

というわけで、センサには秋月で購入しておいた圧力センサFSR400を、マウスピース?にはビックカメラで売ってた配線カバーを使うことにした。製造元はマサル工業だった。ボディのエムケーダクトでもお世話になっています。
f:id:androuet:20220113164757j:plain

配線カバーはカッターで切れる。材質はEVA。側面を斜めに切って、平らな面にセンサを設置する。センサ部分は水に弱いらしいので防水のためテープで覆う。最初は赤い絶縁テープを上に貼っていたが、歯で噛んでも(指の腹で押すよりも)反応が薄い。もう少し厚みのあるスポンジタイプのテープを貼ったら、均等に圧力がかかるのか綺麗にグラフが変化した。

平らな部分を歯で噛むと、噛んだ分だけ動いてベンド量の調整がしやすい気がする。謎なのは噛まない状態でもセンサ値が0にならないこと。テープの貼り付け圧力?なのか、板が曲がっているのを検出しているのか不明。噛み続けると大きな値になり、放置しておくと小さな値になる。板に曲がり癖?がつくのかしら。

久しぶりの作業のときに、以前の記事を見返すと色々思い出せて便利だった。こまめに記事を書こう。

年が明けた

そろそろウィンドシンセの自作を再開したくなってきた。気になっていたので改めてMAME周辺のYM2151エミュレータの実装について調べてみた。※間違えているかもしれない

現在のMAMEプロジェクトでは、Aaron Giles氏の実装したYMFMが使われている。一方でVGMPlayでは、nukeykt?氏の実装したNuked OPMが使われているらしい。

YMFMのREADME.mdからは、エミュレーションの厳密性よりコードの理解のしやすさに重きを置いており、「100% digital accuracy」を求めるならNuked OPMを使えと示唆されているように見える。Nuked OPMのREADME.mdには「Cycle accurate YM2151 emulator」と書いてある。コードを精読していないのだが、違いはその辺にあると思われる。

ウィンドシンセの音源用に作ってきたYM2414エミュレータはVGMPlayのym2151.cをベースにしているが今後は変えることも考えたほうがいいのかも。

  • ym2151.cよりも精度の高いエミューレーションをしているなら、Nuked OPMを拡張してYM2414エミュレータを作る
  • でも、YMFMのエミュレーション精度が上がるのなら、それをそのまま使うのが楽だよなぁとも思う。

www.youtube.com

…書いているうちに、これは後回しにすべき問題のように思えてきた。今年の目標は次の2つにする:

  • バイトセンサ:歯で噛むセンサでピッチベンドを実現する
  • 本体のみで音色選択:OLEDを搭載するか、いっそM5Stackに置き換えるか

YM2151のエミュレーションについては以下のサイトもためになる。
cygx.mydns.jp

アルトヴェノーヴァを手に入れた

ESP32によるWT11エミュレーションの実装は概ね終了した。実機と比較しても何となく同じような音が出ているような気がする。

FM音源的?な音色を聴いているうちに、だんだんと物理モデリング音源を鳴らしたくなってきた。STKの実装を見ていると、それほど難しくはなさそう。クラリネットとかサックスぽい音が出るみたい。

ところがパラメータを見ると、リードの固さとか息の強さとか、本物の吹奏楽器を触らないとよく理解できないものがあって実際のところが知りたくなった。特にリードの付いた楽器に興味が出てきた。リードの噛み方?によって音階が出るとか意味が分からない???

というわけでヤマハのアルトヴェノーヴァを購入してみた。思っていた以上に息を強く吹く必要があったり、リードがぶるぶる震えてたり、発見がある。まだまだ自由に音を出せる状態ではない。しばらく練習してみて、自作ウィンドシンセに反映できるといいと思う。

f:id:androuet:20211031163756j:plain