はじめに
ある日、ふとDAC を使ってみようと思い立ち、題材を探すと 癒しのソルフェジオ音階というものが目につきました。
せっかくだから、これらの音を鳴らしてみようということで、作ってみました。
また、比較のため、12平均律も鳴るようにしました。
環境
1)ハードウェアは、手近にあった下記のものを使用しました。
・Wemos D1 32 そこそこの性能とメモリ容量をもつ Arduino互換機
・PT8211 CMOS16bit 2チャンネル D/Aコンバータ
・簡易アンプ、スピーカ
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++; // 次の音
}
}