Teensy4.0を使ってハードウェアのデジタルシンセサイザーを作る


WIP。もうちょっと具体的なとこを後で追記するつもり。

動機

Teensyなるものがあり、それを使うと簡単にオーディオ再生を行うハードウェアが作れるらしい。しかも、スイッチやノブを簡単に繋いで使えると聞いた。基本ARMだがCortex-M7というDSP的な命令セットがあって効率的に信号処理が行えるらしい。しかも小さい。ならばこれを使ってコンパクトなハードウェアのシンセサイザーを作らない手はない。

必要スキル

  • ソフトウェアの開発はまぁまぁわかってる。C++が特に苦ではない。
  • ハードウェアに関しては超初心者。ハンダ付けは簡単なものならできるが、電子回路に関してはほぼ無知。

用意するもの

MIDI INを扱う場合は下記も必要。

  • フォトカプラー (PC900Vまたは似たようなやつ)
  • 220Ωの抵抗 x2
  • ダイオード x1 (IN914または似たようなやつ)
  • DIN 5pinソケット x1

Teensyとオーディオボードはスイッチサイエンスで買うのが比較的早く届きつつ価格もそこそこリーズナブルかと。その他のものはAmazonで買えば早いし、遅くても良いならAliExpressで買えば安い。分割ロングピンソケットは秋月電子でしか売ってないっぽい。

ピンヘッダとピンソケットのハンダ付け

ここを参考にした。めちゃくちゃわかりやすくて助かった。

  • オーディオボードにピンソケットをハンダ付けする。片側13ピンなので、分割ロングピンソケットを折って使う。
  • Teensyには、オーディオボードに必要な箇所だけ裏側にハンダ付け。残りのピンは表側にハンダ付け。

組み立て、接続

ここの説明にあるキットを再現する。

  • Teensyとオーディオボードを合体。
  • ブレットボードにはTeensyから5VとGNDを繋いでおく。
  • タクトスイッチを3つブレットボードにつけて、片方の足はブレットボードのGNDに、もう片方はTeensy上の0、1、2のピンにそれぞれ接続。
  • ポテンションメーターを2つブレットボードにつけて、向かって右端の足をブレットボード上の5Vに、左端の足をGNDに、真ん中の足をTeensy上ののA2、A3ピンにそれぞれ接続。

ジャンパ線を使ってうまいこと繋ぐ。オフィシャルのキットはうまいことブレットボードにくっついているみたいけど、今回の構成だとくっつかないので、Teensyとブレットボードはジャンパ線で繋いでる。安定しないのでオーディオボードの穴に金属の六角スペーサーを足としてくっつけてなんとか机の上に置いてる。

Arduino IDEとTeensyのインストール

Macの場合は単にそれぞれのアプリをアプリケーションフォルダにコピーするだけ。めちゃくちゃ簡単。

ハードウェアのテスト

組み立てたハードウェアがきちんと動作するか確認。ここにある説明の通りに行う。

最初のハードウェアテストを行うと、ヘッドホンからテスト信号が聞こえて、スイッチを押したりポテンションメーターを回したりするとログに表示される。ここまで来ればあとは気合でソースを読んでシンセのプログラムを書くだけ。

Teensy Audio Library

TeensyにはGUIで操作できるTeensy Audio Libraryというものがあり、これは何かというとWeb上のツールでオシレーターとかフィルターとかを配置すると、ソースコードを吐き出してくれる大変便利なもの。これだけで簡単なシンセも作れるし、吐き出されたコードをベースに自分で改造していける。

MIDI

下記を参考に。
https://www.pjrc.com/teensy/td_libs_MIDI.html

回路はこのサイトにある下記の回路図の通りに組んで、とFPGAとなっている部分をTeensyの0番に、電源(3.3v!!)とGNDを繋げばOK。
Qiita

簡易ウインドシンセ音源

Audio Libraryを使うと簡単に音を出せるが、バッファ処理の中身は自分で書きたい。なので、最小限のテンプレートをExportして、あとは自分で作る。

完全なコードは下記。適当なMIDIウインドコントローラーを繋ぐと音階、音量がコントロールできる。

AudioTest1.ino
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
#include <MIDI.h>

MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI);

// GUItool: begin automatically generated code
AudioPlayQueue           queueOutR;         //xy=435,337
AudioPlayQueue           queueOutL;         //xy=443,285
AudioOutputI2S           i2s1;           //xy=671,303
AudioConnection          patchCord1(queueOutR, 0, i2s1, 1);
AudioConnection          patchCord2(queueOutL, 0, i2s1, 0);
AudioControlSGTL5000     sgtl5000_1;     //xy=492,404
// GUItool: end automatically generated code

float noteDeltaTable[128];
float noteDeltaFineTable[128];
float phase = 0.0f;
float delta = 0.0f;
float amp = 0.0f;             //  0.0 ~ 1.0
float pitchBendRange = 2.0f;  //  0.0 ~ 24.0
float midiNote = 0.0f;        //  0.0 ~ 127.0
float midiPitchBend = 0.0f;   //  -1.0 ~ +1.0

#define SAMPLERATE 44100.0

int16_t buffer[128 * 2];

void setup() {
  MIDI.begin(MIDI_CHANNEL_OMNI);
  Serial.begin(57600);
  //Serial.println("MIDI Input Test");

  //Serial.begin(9600);
  AudioMemory(40);
  sgtl5000_1.enable();
  sgtl5000_1.volume(0.32);
  pinMode(13,OUTPUT);

  phase = 0;
  delta = 500;

  //  Calculate tables
  for (int i = 0; i < 128; i++)
  {
    double freq = 440.0 * pow(2.0, ((double)(i - 69)) / 12.0);
    noteDeltaTable[i] = (float)(freq / SAMPLERATE);
  }

  for (int i = 0; i < 128; i++)
  {
    double cent = 100.0 * ((double)i) / 128.0;
    noteDeltaFineTable[i] = (float)pow(2.0, cent / 1200.0);
  }
}

void updateBuffer()
{
  for (int i = 0; i < 128; i++) {
    float note = midiNote + midiPitchBend * pitchBendRange;
    int noteInt = (int)note;
    int noteFract = (int)((note - (float)noteInt) * 128.0f);
    delta = noteDeltaTable[noteInt] * noteDeltaFineTable[noteFract]; 
    phase += delta;
    if (phase >= 1.0f) phase -= 2.0f;
    auto sig = phase * amp;
    buffer[i] = buffer[i + 128] = (int16_t)(sig * 32767.0f);
  }

  memcpy(queueOutL.getBuffer(), buffer      , 128 * sizeof(int16_t));
  memcpy(queueOutR.getBuffer(), buffer + 128, 128 * sizeof(int16_t));
  queueOutL.playBuffer();
  queueOutR.playBuffer();
}

void loop() {

  // Audio
  if (queueOutL.available() && queueOutL.available()) {
    updateBuffer();
  }

  // Midi
  while (MIDI.read()) {
    int note, velocity, channel, d1, d2;
    byte type = MIDI.getType();
    switch (type) {
      case midi::NoteOn:
        note = MIDI.getData1();
        velocity = MIDI.getData2();
        channel = MIDI.getChannel();
        if (velocity > 0) {
          midiNote = (float)note;
          Serial.println(String("Note On:  ch=") + channel + ", note=" + note + ", velocity=" + velocity);
        } else {
          Serial.println(String("Note Off: ch=") + channel + ", note=" + note);
        }

        break;
      case midi::NoteOff:
        note = MIDI.getData1();
        velocity = MIDI.getData2();
        channel = MIDI.getChannel();
        //Serial.println(String("Note Off: ch=") + channel + ", note=" + note + ", velocity=" + velocity);
        break;

      case midi::PitchBend:
        {
          int data = (MIDI.getData1() | (MIDI.getData2() << 7)) - 8192;
          //Serial.println(String("Pitchbend=") + data);
          midiPitchBend = ((float)data) / 8192.0f;
        }
        break;

      case midi::ControlChange:
        d1 = MIDI.getData1();
        d2 = MIDI.getData2();
        //Serial.println(String("Control Change:") + d1 + " " + d2);
        if (d1 == 2)
        {
          amp = ((float)d2) / 127.0f;
          amp *= amp;
        }
        break;

      default:
        //d1 = MIDI.getData1();
        //d2 = MIDI.getData2();
        //Serial.println(String("Message, type=") + type + ", data = " + d1 + " " + d2);
        break;
    }
  }
}

参考にしたページ

https://hackaday.io/project/8292-microcontroller-audio-workshop-had-supercon-2015
https://htmlspecial.net/2020/01/30/teensy4come/
http://www.strellis.com/fpga2.shtml