前回のあらすじ
DDSを実装した
さて
今回は、前回のできなかった和音を1つのピンから出力する方法を示したいと思います。
今回のスケッチ
# define output_pin_A 11
# define output_pin_B 12
# define output_pin_C 13
# define DIV_1 1 //分周無し
# define DIV_8 2 //8分周
# define DIV_32 3 //32分周
# define DIV_64 4 //64分周
# define DIV_128 5 //128分周
# define DIV_256 6 //256分周
# define DIV_1024 7 //1024分周
# define WGM21 0b10 //タイマ2 動作モード設定
# define TOIE2 0b10 //割り込み許可
volatile int operator_A = 0;
volatile int operator_B = 0;
volatile int operator_C = 0;
volatile int operator_D = 0;
volatile int operator_A_add = 0;
volatile int operator_B_add = 0;
volatile int operator_C_add = 0;
volatile int operator_D_add = 0;
int add_arr[] = {
274 ,//ド
291 ,//ド#
308 ,//レ
326 ,//レ#
346 ,//ミ
366 ,//ファ
388 ,//ファ#
411 ,//ソ
435 ,//ソ#
461 ,//ラ
489 ,//ラ#
518 ,//シ
549//ド
};
int white_note[] = {
0, 2, 4, 5, 7, 9, 11, 12
};
void setup() {
//ピンの設定
pinMode(output_pin_A, OUTPUT);
pinMode(output_pin_B, OUTPUT);
pinMode(output_pin_C, OUTPUT);
analogWrite(11, 127);
TCCR2B = (TCCR2B & 0b11111000 ) | DIV_1;
TIMSK2 = 0b1;
sei();//割り込み許可
}
void loop() {
int i = 0;
sei();
for (i = 0; i < 8; i++) {
operator_A_add = add_arr[white_note[i]];
delay(300);
}
cli();
operator_A_add = 0;
sei();
for (i = 0; i < 8; i++) {
operator_A_add = add_arr[white_note[i]];
operator_B_add = operator_A_add >> 1;
delay(300);
}
cli();
operator_A_add = 0;
operator_B_add = 0;
sei();
for (i = 0; i < 8; i++) {
operator_A_add = add_arr[white_note[i]];
operator_B_add = operator_A_add >> 1;
operator_C_add = operator_A_add << 1;
delay(300);
}
cli();
operator_A_add = 0;
operator_B_add = 0;
operator_C_add = 0;
sei();
for (i = 0; i < 8; i++) {
operator_A_add = add_arr[white_note[i]];
operator_B_add = operator_A_add >> 1;
operator_C_add = operator_A_add << 1;
operator_D_add = operator_A_add << 2;
delay(300);
}
delay(1500);
for (i = 7; i >= 0; i--) {
operator_A_add = add_arr[white_note[i]];
operator_B_add = operator_A_add >> 1;
operator_C_add = operator_A_add << 1;
operator_D_add = operator_A_add << 2;
delay(100);
}
delay(100);
cli();
operator_A_add = 0;
operator_B_add = 0;
operator_C_add = 0;
operator_D_add = 0;
sei();
delay(1000);
}
ISR(TIMER2_OVF_vect) {
//割り込み時に実行される関数
char data = 0;
digitalWrite(output_pin_C, !digitalRead(output_pin_C));
int output_data = 0;
operator_A += operator_A_add;
operator_A &= 0x7FFF;
operator_B += operator_B_add;
operator_B &= 0x7FFF;
operator_C += operator_C_add;
operator_C &= 0x7FFF;
operator_D += operator_D_add;
operator_D &= 0x7FFF;
if (operator_A < 0x3FFF) {//矩形波 50%
output_data += -31;
} else {
output_data += 31;
}
// if (operator_B < 0x3FFF) {//矩形波 50%
// output_data += -31;
// } else {
// output_data += 31;
// }
if (operator_B < 0x3FFF) {//三角波
output_data += operator_B >> 9;
} else {
output_data += -31 + (63 - (operator_B >> 9));
}
if (operator_C < 0x1FFF) {//矩形波 25%
output_data += -31;
} else {
output_data += 31;
}
output_data += -31 + operator_D >> 9;//のこぎり波
// if (operator_D < 0x3FFF) {//矩形波 50%
// output_data += -31;
// } else {
// output_data += 31;
// }
if (output_data > 0) {
output_data >>= 1;//output_data/=2;と同じ
} else {
output_data = -((-output_data) >> 1);
}
OCR2A = output_data + 127;
digitalWrite(output_pin_C, !digitalRead(output_pin_C));
}
このスケッチを、Arduino UNO(Nanoも可)に書き込むとD11から音が出力されます。

上のようにスピーカにつなげてもいいですが、音が小さいのでアンプを用意してください。
こちらのアンプICや、ダイソーの300円スピーカセットがおすすめです。
出力波形

赤が強い周波数成分です、次々と周波数成分が増えにぎやかになっているのがわかりますね。
動作について
D11にスピーカを、つなぐとドレミファソラシドが4回繰り返されるのが分かると思います。
それぞれ音階は一緒でも聞こえ方、つまり音色は全く異なっています。
これは、出力している波形が変化したためです。
音色について
音にはいくつかの、要素があります。
今回需要なのは、音階、音色、音量
この3つです。
- 音階 いわゆるドレミファソラシド 基本となる周波数
- 音色 その音に含まれる周波数成分(倍音)
- 音量 音の波高値
ここで、詳しい説明はしません
気になる方は、矩形波や三角波、のこぎり波のフーリエ変換について調べてみてください。
スケッチ解説
それでは、スケッチの解説に参ります。
変数
int add_arr[] = {
274 ,//ド
291 ,//ド#
308 ,//レ
326 ,//レ#
346 ,//ミ
366 ,//ファ
388 ,//ファ#
411 ,//ソ
435 ,//ソ#
461 ,//ラ
489 ,//ラ#
518 ,//シ
549//ド
};
ドから高いドまでの、Add値です。
ドレミファソラシドは、#を含めて12音あり、12個上はちょうど周波数が2倍になるという関係があります。
また、この関係は440Hzを基準としています。
f=440\times 2^\frac{n}{12}\
nには、440Hzのラから、何音離れているかが入ります。(黒鍵を含みます。)
高いラは、12音上なので12/12=1で2倍になるという理屈です。
それをもとに、周波数を計算しAdd値に計算しなおしています。
周波数のままでは、すぐに使ことができないのでExcell等でこういった処理をしています。
\begin{align}
{Add} &=\frac{32767 \times f_{output}}{f_{sample}} \\
\end{align}
Add値への計算は、おなじみこの式です。
今回、サンプリング周波数は31.25kHz(後述)なのでラ440Hzだと
\begin{align}
{Add} &=\frac{32767 \times 440}{31.25\times 10^3} \\
&=461\\
\end{align}
となります。
int white_note[] = {
0, 2, 4, 5, 7, 9, 11, 12
};
単純に白鍵の音をまとめてるだけです。
全全半全全半のメジャースケールです。(シッタカ)
setup
お次は、セットアップです。
void setup() {
//ピンの設定
pinMode(output_pin_A, OUTPUT);
pinMode(output_pin_B, OUTPUT);
pinMode(output_pin_C, OUTPUT);
analogWrite(11, 127);
TCCR2B = (TCCR2B & 0b11111000 ) | 0b001;
TIMSK2 = 0b1;
sei();//割り込み許可
}
前半はピン設定ですが、後半は何やら様子が違います。
analogWrite(11, 127);
TCCR2B = (TCCR2B & 0b11111000 ) | 0b001;
TIMSK2 = 0b1;
sei();
前回同様、タイマ割り込みを使用するのですが
それと同時にPWMも利用したいです。
PWMは、短い時間で見ると二値しかないけど、長い目で見ると様々な値になる
って感じです。詳しいことは、wikiさんが教えてくれますよ。
analogWrite(11, 127);
まず、Arduino UNOのPWM出力関数であるanalogWrite()を実行します。
これによって、内部レジスタがPWMモードになります。
TCCR2B = (TCCR2B & 0b11111000 ) | 0b001;
TIMSK2 = 0b1;
次に、前回同様タイマの分周器を変更し最も周波数が高い「分周無し」を選択します。
(TCCR2B & 0b11111000)は、他のレジスタを変更させないためのマスクです。
TIMSK2 = 0b1でタイマ割り込みを有効にしています。
ISRも、TIMER2_OVF_vectに変更しています。
周波数は、
f=\frac{16\times10^6}{2\times256}=31250\\
この時に気が付いたんですが、Arduino UNOって両傾斜PWMにしてくれてたんですね。
ありがとう。
sei();//割り込み許可
cli();//割り込み禁止
割り込みを許可するか、禁止するかのマクロです。
割り込み処理を停止する時に使います。
割り込み禁止をすると、time0の割り込みも禁止され
delayが効かなくなり、Serialもパケットロスが発生する可能性があります。
できるだけ、短い期間のみ割り込み禁止をするべきです。
PWMに関する、詳しい説明は以下のサイト様をご参照ください。
loop内
int i = 0;
sei();
for (i = 0; i < 8; i++) {
operator_A_add = add_arr[white_note[i]];
operator_B_add = operator_A_add >> 1;//1オクターブ下へ
operator_C_add = operator_A_add << 1;//1オクターブ上へ
operator_D_add = operator_A_add << 2;//2オクターブ上へ
delay(300);
}
delay(1500);
for (i = 7; i >= 0; i--) {
operator_A_add = add_arr[white_note[i]];
operator_B_add = operator_A_add >> 1;
operator_C_add = operator_A_add << 1;
operator_D_add = operator_A_add << 2;
delay(100);
}
delay(100);
cli();
operator_A_add = 0;
operator_B_add = 0;
operator_C_add = 0;
operator_D_add = 0;
sei();
delay(1000);
基本的には
- 発音周波数を、12音の白鍵分を選択
- 発音時にAddを決定
- 消音時にAddをゼロ
してるだけです。
それと同時に、発音数も増やしつつ、シフト演算によってオクターブを上下しています(シフト演算については後述)。
消音時に、位相operator_Xもゼロにすると次の発音時に位相がそろい
発音時の違和感が減ります。
割り込み処理内
まず
digitalWrite(output_pin_C, !digitalRead(output_pin_C));
時間計測用です。
operator_A += operator_A_add;
operator_A &= 0x7FFF;
operator_B += operator_B_add;
operator_B &= 0x7FFF;
operator_C += operator_C_add;
operator_C &= 0x7FFF;
operator_D += operator_D_add;
operator_D &= 0x7FFF;
位相加算です、前回と同様ですね
if (operator_A < 0x3FFF) {//矩形波 50%
output_data += -31;
} else {
output_data += 31;
}
// if (operator_B < 0x3FFF) {//矩形波 50%
// output_data += -31;
// } else {
// output_data += 31;
// }
if (operator_B < 0x3FFF) {//三角波
output_data += operator_B >> 9;
} else {
output_data += -31 + (63 - (operator_B >> 9));
}
if (operator_C < 0x1FFF) {//矩形波 25%
output_data += -31;
} else {
output_data += 31;
}
output_data += -31 + operator_D >> 9;//のこぎり波
// if (operator_D < 0x3FFF) {//矩形波 50%
// output_data += -31;
// } else {
// output_data += 31;
// }
ここから、様子が変わってきます。
output_data は出力用の変数です。
出力用の波形を作っていくのですが、まず
各波形共に、正負の値を取るようにします。(この方が楽なので)
if (operator_A < 0x3FFF) {//矩形波 50%
output_data += -31;
} else {
output_data += 31;
}
前回から、いますがこれは矩形波用です。
±31の矩形波です。
位相は単調に増加し0にもどるので、ある値を境に0,1を取れば矩形波ができます。
0x3FFFは16383、つまり最大値32767の半分Duty比が50%の矩形波となります。
ちなみに、オペレータCは0x1FFFでDutyを25%にしています。
矩形波は、Duty比によって聞こえ方(音色)が変化します。
output_data += -31 + operator_D >> 9;//のこぎり波
のこぎり波です、オペレータの大きさを小さくして正負の値を取るようにしています。
>>はビットシフトで、>>N でN回右にシフトするという意味です。
右シフトは、2で割るのと同義で通常の/で記述するのより高速です。(所説あり)
<<は、2をかけるの意味です。
ここでは、>>9 つまり
2^{15} \div 2^{9}=2^6\\
となります。
0~32767のオペレータを、0~63にダウンサイズしたわけですね。
-31しているので、±31
if (operator_B < 0x3FFF) {//三角波
output_data += operator_B >> 9;
} else {
output_data += -31 + (63 - (operator_B >> 9));
}
オペレータの値をうまいこと計算して三角波にしています。
いろいと、やってたらできました。
のこぎり波と似たような処理です。
if (output_data > 0) {
output_data >>= 1;//output_data/=2;と同じ
} else {
output_data = -((-output_data) >> 1);
}
レベルリミッタです、31が波高値の最大なので
全音合わせると、124になります。
念のため、2で割って音割れしないようにしています。
出力音数を増やす場合、音割れに注意
OCR2A = output_data + 127;
OCR2Aは,タイマ2のレジスタです。
analogWriteを使った時も、最終的にはこのレジスタに格納されます。
Arduino UNOの公式サイトには、このようなタイマのOCRXXに対応するピンをまとめた便利な画像があります。
output_dataは,正負の値なので127を足しています。
常に、2.5V(電源電圧/2)のオフセットが出るので注意してください。
出力にコンデンサを入れれば大丈夫です。
まとめ
今回は、ついに和音を1つのピンで出力することができました。
おわりに
駆け足で、スケッチの説明をしましたが
おそらく、よくわからんところがあると思います。
大丈夫です。書いてる人の正直よくわかっていません。
毎回スケッチを、先に持ってきているのは
とりあえずそれで遊んでもらいたいからです。
ひとしきりあそんでから記事の内容に目を通してもらえば結構です。
次回
未定