0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

はじめに

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

 

環境

1)ハードウェアは、手近にあった下記のものを使用しました。
 
・Wemos D1 32 そこそこの性能とメモリ容量をもつ Arduino互換機
・PT8211 CMOS16bit 2チャンネル D/Aコンバータ
・簡易アンプ、スピーカ

schematic3.png

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

スケッチ

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

	コマンド

    文節 [, 文節]

  	文節
	    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

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

//==========================================
//    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;                  // シーケンス保存中を示すフラグ

//------------------------------------------
//    2進文字列から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;                              // 区切りがないのですべてを1文節にする
    str = "";
  }
  else {
    s = str.substring(0, i);              // 区切りの直前までを1文節にする
    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;                              // 空白がないのですべてを1単語にする
    str = "";
  }
  else {
    s = str.substring(0, i);              // 空白の直前までを1単語にする
    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];               // 1音の長さを取得
    startTimer(t);                        // 計時開始
    while (checkTimer()) {
      setPattern(buff[i].sorufejio, buff[i].scale);   // 音の長さになるまで、波形を再生する
      writeDAC();
    }
    i++;                                  // 次の音
  }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?