概要
Teensy4.1 + ESP32 + PCM5102Aボードを使って、Bluetooth Audioレシーバー付きのシンセサイザーを作るテスト。目的はBluetoothでオケを流しながらウインドシンセが吹ける音源モジュール。
電源はUSBから。この組み合わせだと、TeensyのUSBポートを使ってもESP32のUSBポートを使っても使うことができる。普段使う分にはどうでもいいが、開発中はとても便利。普段使いの時はTeensyの方を使った方がUSB接続音源モジュールとしても使えるのでちょっといいかもしれない。
使ってるもの
- Teensy 4.1
- ESP32
- PCM5102A基板
- 0.96インチOLED (SPI)
- ユニバーサル基板1
- ユニバーサル基板2
- ダイソーのハガキ整理ケース
- タクトスイッチ x4
- 基板取付用2連ボリュームA10kΩ
- 3.5mm小型ステレオミニジャック 基板取付用
- micro-B オス-メス 延長ケーブル(パネルマウント
- MIDI IN用ジャック
- フォトカプラ
- 抵抗
- ダイオード
- 線材色々
接続
Teensy4.1はI2Sのマスターとスレーブどちらでも動作するが、ESP32はマスターでしか動かないので、ESP32をマスターとする。すべてのボードの5V、GND、BCLK、LRCLKをそれぞれ接続し、ESP32のDATA OUTをTeensy4.1のDATA INに、Teensy4.1のDATA OUTをPCM5102AのDATA INに接続する。
ESP32はBLE-MIDIも動かして、受けたデータをそのままTeensyに受け流し、Teensyが受けたものをESP32に受け流す。よってこれらのSerialはMIDIのbps(31250)で使う。Teensy側はMIDIとしてピンを使うが、ESP32側は単にSerialとして使う(なんでそうしたんだったか忘れた)。もしかしたらもっと高いレートでやってしまってもいいのかもしれない(未確認)。
ESP32 → Teensy4.1
GPIO26 → 21 (BCLK1)
GPIO25 → 20 (LRCLK1)
GPIO22 → 8 (IN1)
GPIO36 (RXD2として) → 17 (TX4)
GPIO18 (TXD2として) → 16 (RX4)
5V → Vin
GND → GND
GPIO26,25,22 を使っているのは、以前M5Stamp Picoで使っていた時にそうしていたから。
Teensy4.1 → PCM5102A
21 (BCLK1) → BCK
20 (LRCLK1) → LCK
7 (OUT1A) → DIN
GND → SCK
GND → GND
5V → VIN
Teensy4.1 → SSD1306(SPI)
GND → GND
5V → VCC
14 → D0
11 → D1
15 → RES
9 → DC
10 → CS
Teensy4.1 → タクトスイッチ
2 → SW1
3 → SW2
4 → SW3
5 → SW4
プログラム
Arduinoを使う。ESP32とTeensy4.1はボードマネージャーで追加して、ESP32-A2DP をライブラリに追加しておく。
ESP32
File -> Examples -> ESP32-A2DP にある bt_music_receiver_32bits にピンアサイン変更を加えただけ。
#include "AudioTools.h"
#include "BluetoothA2DPSink.h"
AudioInfo info(44100, 2, 32);
I2SStream out;
NumberFormatConverterStream convert(out);
BluetoothA2DPSink a2dp_sink(convert);
void setup() {
Serial.begin(115200);
// AudioToolsLogger.begin(Serial, AudioLogger::Info);
// Configure i2s to use 32 bits
auto cfg = out.defaultConfig();
cfg.copyFrom(info);
cfg.pin_bck = 26;
cfg.pin_ws = 25;
cfg.pin_data = 22;
out.begin(cfg);
// Convert from 16 to 32 bits
convert.begin(16, 32);
// start a2dp
a2dp_sink.start("AudioKit");
}
void loop() {
delay(1000); // do nothing
}
Teensy4.1
Audio Design Toolで下図のように組んでExportしたものを流用する。このツールは便利で、簡単なシンセサイザーくらいはこのツールでデザインできる。ここではテストのために単にサイン波を生成してI2Sの入力に入ってきたものとミキサーでミックスしてI2Sに出力する。sgtl1500はTeensy Audio Board用だが、これがPCM5102Aを使う際にも使えるっぽいので置いておく。
エクスポートしたコードスニペットを使ってプログラムを書く。
#define SLAVE_MODE 1
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
AudioSynthWaveformSine sine1;
#if SLAVE_MODE
AudioInputI2Sslave i2sInputs;
AudioOutputI2Sslave i2sOutputs;
#else
AudioInputI2S i2sInputs;
AudioOutputI2S i2sOutputs;
#endif
AudioMixer4 mixerL;
AudioMixer4 mixerR;
AudioConnection patchCord1(sine1, 0, mixerL, 0);
AudioConnection patchCord2(sine1, 0, mixerR, 0);
AudioConnection patchCord3(i2sInputs, 0, mixerL, 1);
AudioConnection patchCord4(i2sInputs, 1, mixerR, 1);
AudioConnection patchCord5(mixerR, 0, i2sOutputs, 1);
AudioConnection patchCord6(mixerL, 0, i2sOutputs, 0);
AudioControlSGTL5000 sgtl5000_1;
void setup() {
AudioMemory(20);
sgtl5000_1.enable();
sgtl5000_1.volume(0.5); // これは実はPCM5102Aボードでは効果がない
sine1.amplitude(0.5);
sine1.frequency(1000);
}
void loop() {
delay(1000);
}
OLED
小さくて安価な単色OLEDディスプレイにはI2C版とSPI版の2種類があるが、波形表示などをするにはSPI版でないと速度的に厳しいのでSPI版を使っている。
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define OLED_MOSI 11 // = DOUT = Display SDA
#define OLED_CLK 14 // = SCK = Display SCL
#define OLED_DC 9 // = Display DC
#define OLED_CS 10 // = Display CS
#define OLED_RESET 15 // = Display RES
Adafruit_SSD1306 display(128, 64, OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
void initDisplay()
{
if (!display.begin(SSD1306_SWITCHCAPVCC)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
// ディスプレイをクリア
display.clearDisplay();
display.setTextSize(1); // 出力する文字の大きさ
display.setTextColor(WHITE); // 出力する文字の色
display.setCursor(0, 0); // 文字の一番端の位置
display.println("STUDIO-R"); // 出力する文字列
display.println("TWI2000");
// ディスプレイへの表示
display.display();
}
BLE-MIDI
Teensy4.1にないBluetoothの機能はESP32が担う。BLE-MIDIを使ってESP32が送受信を行うが、データはすべてTeensyが処理を行う。
ESP32 → Teensy4.1への送信
Core0で行う場合は単に送ればいいが、負荷分散のためにCore1で行う場合はちょっと複雑になる。
void setup()
{
// Serial (USB) for debug output
Serial.begin (115200);
// Serial2 をTeensyとのMIDIデータ送受信に使う。Teensy側がMIDIクラスでSerialを使うので速度を合わせている。
// SERIAL_8N1 の意味
// 8 : データビット数(8ビット)
// N : パリティ(None = なし)
// 1 : ストップビット数(1ビット)
Serial2.begin (31250, SERIAL_8N1, RXD2, TXD2);
a2dp_sink.start ("TWV2000M"); // これはBluetooth Audio
// BLE-MIDIの初期化
BLEMidiServer.begin ("TWV2000M");
BLEMidiServer.setOnConnectCallback ([](){
Serial.println ("BLE-MIDI Connected");
});
BLEMidiServer.setOnDisconnectCallback([](){
Serial.println ("BLE-MIDI Disconnected");
});
BLEMidiServer.setNoteOnCallback (onNoteOn);
BLEMidiServer.setNoteOffCallback (onNoteOff);
BLEMidiServer.setControlChangeCallback (onControlChange);
//BLEMidiServer.setSystemExclusiveCallback (onSysex); // SysEx使うなら…
// Core1で処理を行う場合は Queue を作成する
#if USE_CORE1_TO_SEND_MIDI
xQueue = xQueueCreate (50, sizeof(int32_t));
if(xQueue != NULL)
{
xTaskCreatePinnedToCore (MidiTask, "MidiTask", 4096, NULL, 1, NULL, 1);
}
else
{
Serial.println ("Error: could not create queue.");
}
#endif
debugOutput("End setup");
}
#if USE_CORE1_TO_SEND_MIDI
void MidiTask (void* arg)
{
BaseType_t xStatus;
int32_t receivedValue = 0;
const TickType_t xTicksToWait = 1U; // [ms]
while (1)
{
xStatus = xQueueReceive(xQueue, &receivedValue, xTicksToWait);
if(xStatus == pdPASS) // receive error check
{
Serial.print ("received data : ");
Serial.println (receivedValue);
uint8_t data[3];
data[0] = (receivedValue >> 16) & 0xff;
data[1] = (receivedValue >> 8) & 0xff;
data[2] = receivedValue & 0xff;
Serial2.write(data, 3); // 2バイトメッセージや1バイトメッセージを考慮してないじゃん…
}
else
{
if (uxQueueMessagesWaiting(xQueue) != 0)
{
while(1)
{
Serial.println("rtos queue receive error, stopped");
delay (1000);
}
}
}
}
}
#endif
void sendMidiMessage (uint8_t status, uint8_t data1, uint8_t data2)
{
#if USE_CORE1_TO_SEND_MIDI
BaseType_t xStatus;
int32_t sendValue = (status << 16) |
(data1 << 8) |
data2;
xStatus = xQueueSend(xQueue, &sendValue, 0);
if (xStatus != pdPASS)
{
Serial.println("Error: Could not send data to core1");
}
#else
uint8_t data[3];
data[0] = status;
data[1] = data1;
data[2] = data2;
Serial2.write (data, 3);
#endif
}