純粋な音とひずんだ音
純粋な音の波は上図左のような正弦波(sin波)になります。一方、第2回で説明したようにESP32マイコンにはPWM(Pulse Width Modulation)機能で出力できるのは上図右のような矩形波(方形波)となります。
正弦波の倍音成分を重ねていくと矩形波になります。ですので矩形波の音は歪んだ音となります。この正弦波の倍音成分を重ねていくと矩形波になるというのは、「フーリエ級数展開」という数学で表すことができます。これは理数系の大学で習うことなので、ニンジャが将来、理数系の大学に進んだら目にすることになるでしょう。
ドレミと周波数
ドレミの音階と周波数の関係は上表のようになります。「ラ」(A4)の音が440Hzと定められています。1オクターブ音が高くなると周波数は倍になります。(例 A4:440Hz→A5:880Hz) ドレミの音階を半音で全部表記すると12段階となります。
前の音に12回かけていくと2倍になる値12√2≒1.059463がある音から半音上がる係数となります。
(例:A4:440.000Hz×1.059463 ≒ A#4:466.164Hz)
息を吹き込むとドレミ音を出すプログラム
第2回で組み立てた、圧電スピーカを接続した回路に対して、制御プログラムを作成します。圧力センサに息を吹き込むことで音を出し、タッチセンサで音階を決めるプログラムを作成します。プログラム全体はこの記事の一番最後にダウンロードできるところを用意しています。
①初期化部分1
プログラムで使用する機器や定数を定義する部分です。
#include <Wire.h>
#include "Adafruit_MPRLS.h"
#include "Adafruit_MPR121.h"
// ブザー出力する端子
#define BUZZER_PIN 25
// ブザーを鳴らすときの圧力差
#define PRS_OFFSET 10.0
//音階名と周波数の対応
#define C4 261.6
#define Cs4 277.18
#define D4 293.665
#define Ds4 311.127
#define E4 329.63
#define F4 349.228
#define Fs4 369.994
#define G4 391.995
#define Gs4 415.305
#define A4 440
#define As4 466.164
#define B4 493.883
#define C5 523.251
// You can have up to 4 on one i2c bus but one is enough for testing!
Adafruit_MPR121 cap = Adafruit_MPR121();
// You dont *need* a reset and EOC pin for most uses, so we set to -1 and don't connect
#define RESET_PIN -1 // set to any GPIO pin # to hard-reset on begin()
#define EOC_PIN -1 // set to any GPIO pin to read end-of-conversion by pin
Adafruit_MPRLS mpr = Adafruit_MPRLS(RESET_PIN, EOC_PIN);
//プログラムで使用するグローバル変数の定義
char texts[50]; // 文字列出力用
float PrsInit; // 圧力センサ初期値
int LastOn = -1; // 最後に出した音の記憶用(-1は無音)
・1~3行 #include xxxで、使用する機能を取り込みますという意味です。
[Wire.h]:センサと通信するI2Cという機能
[Adafruit_MPRLS.h]:Adafruitの圧力センサ
[Adafruit_MPR121.h]:Adafruitタッチセンサ
・6行 #define BUZZER_PIN 25、PWMを使ってブザーを鳴らす端子に25番ピンを使用しますという定義です。
・8行 define PRS_OFFSET 10.0、息を吹き込んでいると判断する通常状態からの圧力差を決めておきます。
・10~23行 C4(ド)から1オクターブ上のC5(ド)の音の周波数を半音ずつ定義しています。
・25~26行 タッチセンサを使用するための記述です。タッチセンサのサンプルプログラムからそのまま引用しています。
・28~31行 圧力センサを使用するための記述です。圧力センサのサンプルプログラムからそのまま引用しています。
・33~36行 プログラムで使用するグローバル変数を定義しています。
char texts[50]:文字列出力用の汎用変数として使用します。
float PrsInit:プログラムを起動したときの圧力(息を吹き込んでいないときの圧力)を保存しておきます。
int LastOn:最後に出した音を記憶しておく変数です。内容が-1のときは無音とします。
②初期化部分2
起動時に最初に実行されるプログラムを記述する部分です。setup()の{}内に記述します。
void setup() {
Serial.begin(115200);
ledcAttachPin(BUZZER_PIN,1);
Serial.println("Wind Synthesizer");
// タッチセンサをアドレス0x05Aで初期化する
if (!cap.begin(0x5A)) {
Serial.println("MPR121 not found, check wiring?");
while (1);
}
Serial.println("MPR121 found!");
// 圧力センサを初期化する(デフォルトのアドレス0x18)
if (! mpr.begin()) {
Serial.println("Failed to communicate with MPRLS sensor, check wiring?");
while (1) {
delay(10);
}
}
Serial.println("Found MPRLS sensor");
// 何もない状態の圧力センサの値を取得する
float prsTotal = 0;
for (uint8_t ii = 0 ; ii < 10 ; ii++){
prsTotal += mpr.readPressure(); // 圧力センサから圧力取得
delay(10);
}
PrsInit = prsTotal / 10;
sprintf(texts, "Prs Init:%f\n", PrsInit);
Serial.print(texts);
}
・2行 Serial.begin(115200); モニターとして使用するシリアル通信を始めますという関数です。()内の115200は通信速度です。シリアルモニターを起動したときには、この115200設定とします。(第1回、第2回参照)
・3行 ledcAttachPin(BUZZER_PIN,1); ブザーを鳴らすために使用する端子とPWMチャンネルを指定します。①初期化部分①にてBUZZER_PINは25番ピンとしていますので、25番ピンでチャンネル1のPWMを使用すると宣言しています。(Arduino標準関数)
・5行 Serial.println(“Wind Synthesizer”); 起動確認用にシリアルに「Wind Synthesizer」の文字列を出力しているだけです。シリアルモニターを起動していると、この「Wind Synthesizer」の文字が表示されます。
・7~12行 タッチセンサの初期化
タッチセンサを初期化します。第1回で解説したタッチセンサのテストプログラムの初期化部分からそのまま引用しています。
・14~21行 圧力センサの初期化
圧力センサを初期化します。第2回で解説した圧力センサのテストプログラムの初期化部分からそのまま引用しています。
・23~31行 何もない状態の圧力センサの値を取得して、変数PrsInitに格納しておきます。
prsTotalという変数を定義して、10回圧力センサ値を読み込んで10で割って平均値を出します。この平均値を息を吹き込んでいない状態(大気圧)の圧力センサ値としてPrsInitに格納しています。
③プログラム本体
プログラム本体です。繰り返し実行されます。loop()の{}内に記述します。
void loop() {
float nowPrs = mpr.readPressure(); // 圧力センサから圧力取得
uint16_t nowTouch = cap.touched(); // タッチセンサの状態取得
// タッチセンサの値を0と1並びの文字に変換する
char bitptn[13];
int firstOn = -1;
for (uint8_t i=0; i<12; i++) {
if ((nowTouch & ( 1<<i))==0){
bitptn[i] = '0';
}else{
if (firstOn < 0) firstOn = i;
bitptn[i] = '1';
}
}
bitptn[12] = '\0';
if (nowPrs >= (PrsInit+PRS_OFFSET)){
// 圧力が高いときは音を出す
if (firstOn != LastOn) {
ringBuzzer(firstOn, 1);
LastOn = firstOn;
}
}else{
// 音を止める
ringBuzzer(-1, 1);
LastOn = -1;
}
sprintf(texts, "Prs:%f Tch:%s\n", nowPrs, bitptn);
Serial.print(texts);
delay(50);
}
・2行 圧力センサから圧力取得
mprライブラリのreadPressure();関数を使って、現在の圧力値をセンサから取得します。単位はhpa(ヘクトパスカル)です。変数nowPrsに格納しておきます。
・3行 タッチセンサの状態取得
capライブラリのtouched();関数を使って、現在のタッチ状態をセンサから取得します。タッチセンサには12個の端子がありタッチされている端子のビットは1、そうでない端子のビットは0となります。変数nowTouchに格納しておきます。
・5~16行 タッチセンサの状態を0と1並びの文字に変換
タッチセンサの状態を16進数表記ではわかりずらいので、2進数の0,1の文字列に変換します。また、0から11のタッチセンサの端子で触れている端子のうち一番小さい番号の端子番号をfirstOn変数に格納します。(複数端子に同時に触れていても1つに限定するため) 何も触れていない状態ではforstOn変数は-1となります。
・18~28行 圧力値によって音を鳴らす
変数nowPrsに格納されている圧力値が、初期化で取得したPrsInit(大気圧)にPRS_OFFSET(①で10と定義)加算した値以上の時には、タッチセンサの端子に触れている端子のうち一番小さい番号の端子に対応する音を出します。
圧力値がPrsInit+PRS_OFFSET未満の時には音を停止します。
音を鳴らす/止めるのは、別途作成するringBuzzer関数を呼び出します。
・30~32行 現在の状態をシリアル出力する
現在の圧力値とタッチセンサの端子に触れている状態をシリアル出力します。シリアルモニターを起動していれば、確認できます。delay(50)は50ミリ秒待ちます。(このウェイトは無くても良いです。)
④ブザーを鳴らす/止める関数
ブザーを鳴らす/止めるを関数として作ります。第1引数(argBit)はタッチセンサの触れている端子番号、第2引数(argRate)はドレミの周波数にかける倍率です。ですので、argRateを2にすれば周波数が2倍になりますので、C4~C5より1オクターブ上のC5~C6の音になります。
void ringBuzzer(int argBit, float argRate){
float freq;
switch(argBit){
case 0: freq = C4; break;
case 1: freq = D4; break;
case 2: freq = E4; break;
case 3: freq = F4; break;
case 4: freq = G4; break;
case 5: freq = A4; break;
case 6: freq = B4; break;
case 7: freq = C5; break;
default: freq = 0; break;
}
ledcWriteTone(1,freq*argRate);
}
・2~13行 タッチセンサの触れている端子番号をドレミの周波数に変換する
switch(argBit)は()内の変数argBitの状態によって、各case文の行を実行します。0番端子の時にはC4(ド)の周波数が変数freqに格納されます。case 0~7でドレミファソラシドに対応する周波数が格納します。最後のdefaultの行は0~7以外の時に実行します。freqにゼロを格納します。
・14行 ledcWriteTone(1,freq*argRate); 指定のチャンネルに指定の周波数を出力する関数です。(Arduino標準関数)
②初期化部分2でledcAttachPin(BUZZER_PIN,1);とチャンネル1を指定していますので、このledcWriteToneでも第引数に1を指定します。第2引数に周波数を指定します。freq*argRateとすることで、C4~C5の周波数を基本として、argRate倍した周波数を出力します。
第2引数に0が設定されると音は止まります。つまり、この関数の第1引数(argBit)が0~7以外の時には音が止まります。なので-1のときにも音が止まります。
電子リコーダのテスト版ができました
息を吹き込みながら、タッチセンサの端子に触れるとドレミの音が圧電スピーカから流れます。
★プログラム全体をダウンロード(ZIP圧縮ファイル)はこちらからダウンロードできます。→ここをクリック