Teensy4.1 + ESP32 + PCM5102Aボード その他

## 概要 ##
Teensy4.1 + ESP32 + PCM5102Aボードを使って、Bluetooth Audioレシーバー付きのシンセサイザーを作るテスト。目的はBluetoothでオケを流しながらウインドシンセが吹ける音源モジュール。

![IMG_8275.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107818/bcfc3e2e-a4c5-42d8-afc7-71738b50eee8.jpeg)

![IMG_8304.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107818/f6655af4-370b-4eba-9107-ceeed04d9729.jpeg)

電源はUSBから。この組み合わせだと、TeensyのUSBポートを使ってもESP32のUSBポートを使っても使うことができる。普段使う分にはどうでもいいが、開発中はとても便利。普段使いの時はTeensyの方を使った方がUSB接続音源モジュールとしても使えるのでちょっといいかもしれない。

## 使ってるもの ##

* [Teensy 4.1](https://www.digikey.jp/ja/products/detail/sparkfun-electronics/DEV-20359/16688101) 
* [ESP32](https://ja.aliexpress.com/item/1005005704190069.html)
* [PCM5102A基板](https://ja.aliexpress.com/item/1005004215170923.html)
* [0.96インチOLED (SPI)](https://ja.aliexpress.com/item/32638662748.html)
* [ユニバーサル基板1](https://amzn.to/4h3Q1o2)
* [ユニバーサル基板2](https://amzn.to/41GLtzy)
* [ダイソーのハガキ整理ケース](https://jpbulk.daisonet.com/products/4973430402866)
* [タクトスイッチ x4](https://www.amazon.co.jp/dp/B0776VHTCY)
* [基板取付用2連ボリュームA10kΩ](https://akizukidenshi.com/catalog/g/g103604/)
* [3.5mm小型ステレオミニジャック 基板取付用](https://akizukidenshi.com/catalog/g/g102460/)
* [micro-B オス-メス 延長ケーブル(パネルマウント](https://amzn.to/43iyaXa)
* [MIDI IN用ジャック](https://ja.aliexpress.com/item/1005006314983600.html)
* フォトカプラ
* 抵抗
* ダイオード
* 線材色々

## 接続 ##
Teensy 4.1
![card11a_rev4_web.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107818/5f711802-786c-4ff7-bff4-d3a98414ea6d.jpeg)
ESP32
![doc-esp32-pinout-reference-wroom-devkit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107818/26a7eec7-fa96-43e2-9ebf-929bab4473a8.png)
PCM5102Aボード
![Unknown.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107818/11682616-e038-4311-a0c0-691210e859fe.jpeg)

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](https://github.com/pschatzmann/ESP32-A2DP) をライブラリに追加しておく。

### ESP32 ###
File -> Examples -> ESP32-A2DP にある bt_music_receiver_32bits にピンアサイン変更を加えただけ。
```cpp
#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](https://www.pjrc.com/teensy/gui/)で下図のように組んでExportしたものを流用する。このツールは便利で、簡単なシンセサイザーくらいはこのツールでデザインできる。ここではテストのために単にサイン波を生成してI2Sの入力に入ってきたものとミキサーでミックスしてI2Sに出力する。sgtl1500はTeensy Audio Board用だが、これがPCM5102Aを使う際にも使えるっぽいので置いておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107818/b6a55257-5dfa-47c6-9671-6192328a9923.png)

エクスポートしたコードスニペットを使ってプログラムを書く。

```cpp
#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版を使っている。

```cpp
#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で行う場合はちょっと複雑になる。
```cpp
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
}
```