DAコンバータでソルフェジオ音階を鳴らしてみる

# はじめに

　ある日、ふとDAC を使ってみようと思い立ち、題材を探すと 癒しのソルフェジオ音階というものが目につきました。
　せっかくだから、これらの音を鳴らしてみようということで、作ってみました。
　また、比較のため、12平均律も鳴るようにしました。

　
# 環境

１）ハードウェアは、手近にあった下記のものを使用しました。
　
	・Wemos D1 32		そこそこの性能とメモリ容量をもつ Arduino互換機
	・PT8211			CMOS16bit 2チャンネル D/Aコンバータ
	・簡易アンプ、スピーカ
	
![schematic3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245347/01d7c518-2bc7-8357-3b4d-bba1527237da.png)


２）ソフトウェアは、下記のとおりです。
　
	・Arduino IDE 1.8.13
　　　　　ボード：ESP32 Dev Module
　　　　　CPU Frequency：240MHz

# スケッチ

　起動すると、シリアルポートからの入力待ちになるので、下記コマンドを送信することで、動作します。
　鳴らす音階は、ビットで指定するので、複数ビットを１にすることで、和音にすることができます。

```
	コマンド

    文節 [, 文節]

  	文節
	    R m: random chord
	    S: start of seaquence
	    E: end of sequence
	    P: play seaquence
	    bbbbbbbbb bbbbbbbbbbbbb m: binary code（000000000～111111111 0000000000000～1111111111111）
        ソルフェジオ
	      000000001:               174hz
	      000000010:               285hz
	      000000100:               396hz  Ut
	      000001000:               417hz  Re
	      000010000:               528hz  Mi
	      000100000:               639hz  Fa
	      001000000:               741hz  Sol
	      010000000:               852hz  La
	      100000000:               963hz
        12平均律
	      0000000000001:           C
	      0000000000010:           C#
	      0000000000100:           D
	      0000000001000:           D#
	      0000000010000:           E
	      0000000100000:           F
	      0000001000000:           F#
	      0000010000000:           G
	      0000100000000:           G#
	      0001000000000:           A
	      0010000000000:           A#
	      0100000000000:           B
	      1000000000000:           C

	      m:  milliseconds
	        1   5000
	        2   4000
	        3   3000
	        4   2000
	        5   1500
	        6   1000
	        7   500
	        8   250
	        9   125
```

　作成したスケッチは、つぎのとおりです。
　特に、変わったことはしていないので、コメントを見ていただければ、何をやっているかはわかると思います。
　ただし、あくまで、近似音であることは、ご了解してください。
　また、音圧を考慮していないので、周波数が高いほど大きく聞こえます。

```C++
//==========================================
//    Sofufejio box
//
//------------------------------------------
//  command sentence
//    phrase [, phrase]
//
//  phrase
//    R m: random chord
//    S: start of seaquence
//    E: end of sequence
//    P: play seaquence
//    bbbbbbbbb bbbbbbbbbbbbb m: binary code（000000000～111111111 0000000000000～1111111111111）
//      000000001:              // 174hz
//      000000010:              // 285hz
//      000000100:              // 396hz  Ut
//      000001000:              // 417hz  Re
//      000010000:              // 528hz  Mi
//      000100000:              // 639hz  Fa
//      001000000:              // 741hz  Sol
//      010000000:              // 852hz  La
//      100000000:              // 963hz
//
//      0000000000001:          // C
//      0000000000010:          // C#
//      0000000000100:          // D
//      0000000001000:          // D#
//      0000000010000:          // E
//      0000000100000:          // F
//      0000001000000:          // F#
//      0000010000000:          // G
//      0000100000000:          // G#
//      0001000000000:          // A
//      0010000000000:          // A#
//      0100000000000:          // B
//      1000000000000:          // C
//
//      m:  milliseconds
//        1   5000
//        2   4000
//        3   3000
//        4   2000
//        5   1500
//        6   1000
//        7   500
//        8   250
//        9   125
//------------------------------------------

#include <limits.h>

#define DAC_PT8211

//------------------------------------------
// Wemos d1 r32 （ESP32 Dev Module）
//------------------------------------------
#define PIN_BCK           5
#define PIN_WS           13
#define PIN_DIN          12

#define FNC_RANDOM_L    'R'
#define FNC_RANDOM_S    'r'
#define FNC_START_L     'S'
#define FNC_START_S     's'
#define FNC_END_L       'E'
#define FNC_END_S       'e'
#define FNC_PLAY_L      'P'
#define FNC_PLAY_S      'p'

//------------------------------------------
#define NOP __asm__ __volatile__ ("nop\n\t")

#define NUM_BASE          560
#define NUM_BUFF          128
#define NUM_SEQUENCE      128

#define NUM_CHANNEL       2
#define INX_LEFT          0
#define INX_RIGHT         1

#define INX_SORUFEJIO     0
#define INX_SCALE         1

//------------------------------------------
//  ソルフェジオ音階の周波数ごとの要素数
//------------------------------------------
#define NUM_SORUFEJIO     9
uint16_t  patSorufejio[] = {
  359,                    // 174hz
  219,                    // 285hz
  158,                    // 396hz  Ut
  149,                    // 417hz  Re
  118,                    // 528hz  Mi
  98,                     // 639hz  Fa
  84,                     // 741hz  Sol
  73,                     // 852hz  La
  64,                     // 963hz
};

//------------------------------------------
//  12平均律の周波数ごとの要素数
//------------------------------------------
#define NUM_SCALE         13
uint16_t  patScale[] = {
  239,                    // C
  224,                    // C#
  212,                    // D
  200,                    // D#
  189,                    // E
  179,                    // F
  169,                    // F#
  159,                    // G
  150,                    // G#
  142,                    // A
  133,                    // A#
  126,                    // B
  119,                    // C
};

//------------------------------------------
//      音の長さ（ミリ秒）
//------------------------------------------
int16_t note[] = {
  6000,
  5000,
  4000,
  3000,
  2000,
  1500,
  1000,
  500,
  250,
  125,
};

//------------------------------------------
//      音の情報 構造体
//------------------------------------------
typedef struct _DataInfo {
  uint16_t  sorufejio;                          // ソルフェジオ
  uint16_t  scale;                              // 12平均律
  int16_t   note;                               // 音の長さ
} DataInfo;

//------------------------------------------
//      各値の保存領域
//------------------------------------------
int16_t   base    [2][NUM_SCALE][NUM_BASE];     // 音の周波数毎の波の高さ
int16_t   inxBase [2][NUM_SCALE];               // 再生中の波の位置

int16_t   data    [NUM_CHANNEL];                // 合成した波の高さ

DataInfo  buff    [NUM_BUFF];                   // 入力された音の情報
int16_t   numBuff;                              // 音情報の個数
int16_t   inxBuff;                              // 再生中の音情報の位置

DataInfo  sequence[NUM_SEQUENCE];               // シーケンスの保存領域
int16_t   numSequence = 0;                      // シーケンスの個数

bool      bSequence   = false;                  // シーケンス保存中を示すフラグ

//------------------------------------------
//    ２進文字列から10進数への変換
//------------------------------------------
uint32_t  bin2dec(String str) {
  uint32_t  n = 0;

  for (int i = 0; i < str.length(); i++) {
    n <<= 1;
    if (str.charAt(i) != '0') {
      n |= 1;
    }
  }
  return n;
}

//------------------------------------------
//      文節の取得
//------------------------------------------
String  getPhrase(String& str) {
  String  s = "";

  int i = str.indexOf(',');               // 文節の区切りを検索する
  if (i < 0) {
    s = str;                              // 区切りがないのですべてを１文節にする
    str = "";
  }
  else {
    s = str.substring(0, i);              // 区切りの直前までを１文節にする
    s.trim();                             // 前後の空白を削除する
    str = str.substring(i + 1);           // 区切り直後から最後までを新たな文字列にする
    str.trim();
  }
  return  s;
}

//------------------------------------------
//      単語の取得
//------------------------------------------
String  getWord(String& str) {
  String  s = "";

  int i = str.indexOf(' ');               // 空白を検索する
  if (i < 0) {
    s = str;                              // 空白がないのですべてを１単語にする
    str = "";
  }
  else {
    s = str.substring(0, i);              // 空白の直前までを１単語にする
    str = str.substring(i + 1);           // 空白の直後から最後までを新たな文字列にする
    str.trim();
  }
  return  s;
}

//------------------------------------------
//      無作為に音を設定
//------------------------------------------
void  fncRandom(String& str) {
  
  if (numBuff < NUM_BUFF -1) {
    String  s;
    
    s = getWord(str);                                           // コマンドの取得
    buff[numBuff].sorufejio = random(1, 1 << NUM_SORUFEJIO);    // 無作為のソルフェジオ
    buff[numBuff].scale     = random(1, 1 << NUM_SCALE);        // 無作為の12平均律

    s = getWord(str);                                           // 音の長さ 
    buff[numBuff].note      = s.toInt();
    numBuff++;
  }
}

//------------------------------------------
//        シーケンスの開始
//------------------------------------------
void  fncStart(String& str) {
  String  c = getWord(str);             // コマンドの取得

  numSequence = 0;
  bSequence   = true;                   // シーケンスフラグ オン
}

//------------------------------------------
//        シーケンスの終了
//------------------------------------------
void  fncEnd(String& str) {
  String  c = getWord(str);             // コマンドの取得

  bSequence = false;                    //  シーケンスフラグ オフ
}

//------------------------------------------
//        シーケンスの再生
//------------------------------------------
void  fncPlay(String& str) {
  String  c = getWord(str);             // コマンドの取得

  bSequence = false;                    // シーケンスフラグ オフ（シーケンス格納中の場合に強制オフする）

  if (numSequence >= 0) {
    // シーケンスが格納されていれば、演奏バッファへコピーする
    for (int i = 0; i < numSequence; i++) {
      if (numBuff > NUM_BUFF - 1)
        break;
        
      buff[numBuff].sorufejio = sequence[i].sorufejio;
      buff[numBuff].scale     = sequence[i].scale;
      buff[numBuff].note      = sequence[i].note;
      numBuff++;
    }
  }
}

//------------------------------------------
//        音情報を格納する
//------------------------------------------
void  fncStore(String& str) {
  String  s1 = getWord(str);
  String  s2 = getWord(str);
  String  s3 = getWord(str);

  if (bSequence) {
    // シーケンスへ保存
    if (numSequence < NUM_SEQUENCE - 1) {
      sequence[numSequence].sorufejio = bin2dec(s1);
      sequence[numSequence].scale     = bin2dec(s2);
      sequence[numSequence].note      = s3.toInt();
      numSequence++;
    }
  }
  else {
    // 演奏バッファへ保存
    if (numBuff < NUM_BUFF - 1) {
      buff[numBuff].sorufejio = bin2dec(s1);
      buff[numBuff].scale     = bin2dec(s2);
      buff[numBuff].note      = s3.toInt();
      numBuff++;
    }
  }
}

//------------------------------------------
//      コマンドの取得
//------------------------------------------
bool  getCommand(String str) {
  bool    b = false;
  String  s;
  String  t;

  s = str;
  while (s.length() > 0) {
    t = getPhrase(s);                 // 文節を取り出す
    while (t.length() > 0) {
      switch (t.charAt(0)) {          // 先頭文字で振り分ける
        case FNC_RANDOM_L:
        case FNC_RANDOM_S:
          fncRandom(t);
          break;
        case FNC_START_L:
        case FNC_START_S:
          fncStart(t);
          break;
        case FNC_END_L:
        case FNC_END_S:
          fncEnd(t);
          break;
        case FNC_PLAY_L:
        case FNC_PLAY_S:
          fncPlay(t);
          b = true;                 // シーケンスの再生の場合は、即時に演奏する
          break;
        default:
          fncStore(t);
          break;
      }
      if (b)
        goto Exit;                  // 繰り返しの強制終了
    }
  }
Exit:
  return  b;
}

//------------------------------------------
//      シリアル通信からコマンドの取得
//------------------------------------------
void  input() {

  Serial.println();
  Serial.println("--------------");

  Serial.print("command? ");

  numBuff = 0;
  while (true) {
    if (Serial.available()) {
      String str = Serial.readStringUntil('\n');      // 改行コード（Lf）まで取得する
      str.replace("\r", "");                          // 復帰コード（Cr）を削除する（Cr・Lf対応）
      str.trim();                         // 前後の空白削除
      if (str.length() == 0)              // 長さがゼロならば、文字列入力を待つ
        continue;
      
      Serial.println(str);                // 入力された文字列をシリアル出力する
      if (getCommand(str))                // コマンド処理
        break;
      if (!bSequence)                     // シーケンスフラグがオンならば、次の文字列入力を待つ
        break;
    }
  }
}

//------------------------------------------
//        DAC へ 16 ビット出力
//------------------------------------------
void writeDACChannel(int16_t waveData) {

  for (int16_t p = 15; p > -1; p--) {
    digitalWrite(PIN_BCK, LOW);
    digitalWrite(PIN_DIN, (waveData & (1 << p)) ? HIGH: LOW);
    NOP;

    digitalWrite(PIN_BCK, HIGH);
    NOP;
  }
  digitalWrite(PIN_BCK, LOW);
}

//------------------------------------------
//        DAC へステレオ出力
//------------------------------------------
void writeDAC() {
  
  // DAC のライトチャネルを出力する
  digitalWrite(PIN_WS,  LOW);
  writeDACChannel(data[INX_RIGHT]);
  
  // DAC のレフトチャネルを出力する
  digitalWrite(PIN_WS, HIGH);
  writeDACChannel(data[INX_LEFT]);
}

//------------------------------------------
//        周波数ごとの高さの設定
//------------------------------------------
void  setData() {

  // ソルフェジオ
  for (int s = 0; s < NUM_SORUFEJIO; s++) {
    int     n = patSorufejio[s];
    double  m = 360.0 / (double)n;
    double  a = 0.0;
  
    double  r = SHRT_MAX; 
    for (int i = 0; i < n; i++) {
      base[INX_SORUFEJIO][s][i]  = (int16_t)(sin(radians(a)) * r);
      a += m;
    }
  }

  // 12平均律
  for (int s = 0; s < NUM_SCALE; s++) {
    int     n = patScale[s];
    double  m = 360.0 / (double)n;
    double  a = 0.0;
  
    double  r = SHRT_MAX; 
    for (int i = 0; i < n; i++) {
      base[INX_SCALE][s][i]  = (int16_t)(sin(radians(a)) * r);
      a += m;
    }
  }
}

//------------------------------------------
void setup() {
  Serial.begin(115200);
//  Serial.setTimeout(5000);

  randomSeed(analogRead(0));
  
  setData();
  
  pinMode(PIN_BCK, OUTPUT);
  pinMode(PIN_WS,  OUTPUT);
  pinMode(PIN_DIN, OUTPUT);
}

//------------------------------------------
void loop() {

  input();

  curve();
}

//------------------------------------------
//      計時開始
//------------------------------------------
uint32_t  sTimer = 0;
void  startTimer(uint32_t tim) {
  sTimer = millis() + tim;
}

//------------------------------------------
//      計時確認
//------------------------------------------
bool  checkTimer() {
  bool  b = true;

  if (millis() > sTimer)
    b = false;

  return  b;
}

//------------------------------------------
//      再生位置の初期化
//------------------------------------------
void  initPattern() {

  for (int i = 0; i < NUM_SORUFEJIO; i++) {
    inxBase[INX_SORUFEJIO][i] = 0;
  }
  
  for (int i = 0; i < NUM_SCALE; i++) {
    inxBase[INX_SCALE][i] = 0;
  }
}

//------------------------------------------
//      再生する音の波形の合成
//------------------------------------------
void  setPattern(uint16_t srf, uint16_t scl) {
  int32_t   p = 0;
  int16_t   n = 0;
  int16_t   i;
  uint16_t  f;

  // ソルフェジオ
  f = 1;                                                  // 合成する周波数の確認用
  for (int s = 0; s < NUM_SORUFEJIO; s++) {
    i = inxBase[INX_SORUFEJIO][s]++;                      // 合成する波形の位置
    if (inxBase[INX_SORUFEJIO][s] > patSorufejio[s] - 1)
      inxBase[INX_SORUFEJIO][s] = 0;                      // 波形の終わりになったので先頭に戻す
      
    if ((srf & f) != 0) {
      p += base[INX_SORUFEJIO][s][i];       // 波形の合成
      n++;                                  // 合成した個数
    }
    else {
      p += 0;                               // 処理時間を調整するためのダミー
    }
    f <<= 1;
  }

  // 12平均律
  f = 1;
  for (int s = 0; s < NUM_SCALE; s++) {
    i = inxBase[INX_SCALE][s]++;
    if (inxBase[INX_SCALE][s] > patScale[s] - 1)
      inxBase[INX_SCALE][s] = 0;
      
    if ((scl & f) != 0) {
      p += base[INX_SCALE][s][i];
      n++;
    }
    else {
      p += 0;
    }
    f <<= 1;
  }

  if (n == 0) n = 1;
  data[INX_RIGHT] = p / n;                // 合成した個数で割ることで、波の大きさを調整する
  data[INX_LEFT]  = p / n;
}

//------------------------------------------
//      音の再生
//------------------------------------------
void  curve() {
  uint32_t  t;
  int       i = 0;

  while (i < numBuff) {
    initPattern();
    t = note[buff[i].note];               // １音の長さを取得
    startTimer(t);                        // 計時開始
    while (checkTimer()) {
      setPattern(buff[i].sorufejio, buff[i].scale);   // 音の長さになるまで、波形を再生する
      writeDAC();
    }
    i++;                                  // 次の音
  }
}
```