ESP32マイコンと圧電スピーカーを利用してらぷり曲を流してみた


1.はじめに

この記事はらぷりアドベントカレンダー23日目の記事です。
みなさんこんにちは。ヤケノハラと言います。
普段は消費型オタクをやっています。
この機会で少しでも生産的なことをしようと今回の記事を書きました。
難しい内容もあるので、耐性がない方は下の目次の「結果」をクリックして結果をご覧ください。

2.目標

みなさんのらぷりのクリスマスソングとはなんですか?僕は
「Time Lapse Lights」です。

そこで今回、僕はESP32と呼ばれるマイコン(マイクロコントローラ)と圧電スピーカーを利用して、La prièreの「Time Lapse Lights」のサビ部分のメロディを再生しようと思います。

3.使用機材

3.1 ESP32

Wikipediaによると、ESP32とは

ESP32シリーズは Wi-FiとBluetoothを内蔵する低コスト、低消費電力なSoCのマイクロコントローラである。

ESP32 Wikipediaより

とされています。今回ESP32マイコンを使った理由として、Arduinoと同じ開発環境で設計することができたからです。また、最近の大学の講義で使っていたマイコンがたまたまESP32だったのも理由です。

3.2 圧電スピーカー

圧電スピーカーとは、逆圧電効果を利用することでピエゾ素子を振動させ、音を発する部品です。簡単に音を発することができるので僕みたいな初学者にはピッタリでした。

4.ドレミを鳴らす


4.1 周波数って何?

みなさんは音の高さは周波数によって決まるというのは聞いたことがあると思います。周波数は簡単に言うと、一秒間に何回振動したかを表すものです。単位は「Hz」を使います。
例えば、一秒間に10回振動した場合、周波数は10[Hz]となります。
余談として、人間の耳は20[Hz]から20,000[Hz]の音しか聞こえないらしいです。

4.2 ドレミの周波数を求める

オクターブとは、音の高さは違うが、音の名前が一緒になる音です。
ドレミファソラシドの最初と最後の「ド」の関係のことですね。
次に、音は1オクターブ上がるごとに周波数が2倍になります。逆に、1オクターブ下がると周波数が半分になります。

オクターブと周波数の関係 japaneseclass.jpより引用

したがって、
ド、ド#、レ、レ#、ミ、ファ、ファ#、ソ、ソ#、ラ、ラ#、シ、ド
の比が均等になるように分けます。
つまり、比は
1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : 11 : 12
となります。2倍を12等分するので、隣の音に行くときに、$${\sqrt[12]{2}}$$倍されることになります。つまり、公比が$${\sqrt[12]{2}}$$の公比数列となります。
したがって、基準の音を$${f_0}$$[Hz]とすると、$${n}$$番目隣の音の周波数$${f_n}$$は
$${f_n = \sqrt[12]{2}^{n} f_0}$$
で表すことができます。

$${\sqrt[12]{2}}$$ = 1.059463094 
であるので、音の周波数を求めるプログラムを作ります.

#include <stdio.h>
#include <math.h>

int main()
{
    int C[5];  // ド
    int Cs[5]; // ド#
    int D[5];  // レ
    int Ds[5]; // レ#
    int E[5];  // ミ
    int F[5];  // ファ
    int Fs[5]; // ファ#
    int G[5];  // ソ
    int Gs[5]; // ソ#
    int A[5];  // ラ
    int As[5]; // ラ#
    int B[5];  // シ

    int pitch = 440;        // 基準音
    double r = 1.059463094; // 12乗根2の値

    A[3] = pitch; //4オクターブ目のラの音の周波数を440Hzに設定

    //周波数を計算
    for (int i = 0; i <= 4; i++)
    {
        A[i] = round(A[3] * pow(2, i - 3));
        Gs[i] = round(A[i] * pow(r, -1));
        G[i] = round(A[i] * pow(r, -2));
        Fs[i] = round(A[i] * pow(r, -3));
        F[i] = round(A[i] * pow(r, -4));
        E[i] = round(A[i] * pow(r, -5));
        Ds[i] = round(A[i] * pow(r, -6));
        D[i] = round(A[i] * pow(r, -7));
        Cs[i] = round(A[i] * pow(r, -8));
        C[i] = round(A[i] * pow(r, -9));
        As[i] = round(A[i] * pow(r, 1));
        B[i] = round(A[i] * pow(r, 2));
    }

    //音の周波数を出力する
    for (int i = 0; i <= 4; i++)
    {
        printf("C%d = %d [Hz], ", i + 1, C[i]);
        printf("C#%d = %d [Hz], ", i + 1, Cs[i]);
        printf("D%d = %d [Hz], ", i + 1, D[i]);
        printf("D#%d = %d [Hz], ", i + 1, Ds[i]);
        printf("E%d = %d [Hz], ", i + 1, E[i]);
        printf("F%d = %d [Hz], ", i + 1, F[i]);
        printf("F#%d = %d [Hz], ", i + 1, Fs[i]);
        printf("G%d = %d [Hz], ", i + 1, G[i]);
        printf("G#%d = %d [Hz], ", i + 1, Gs[i]);
        printf("A%d = %d [Hz], ", i + 1, A[i]);
        printf("A#%d = %d [Hz], ", i + 1, As[i]);
        printf("B%d = %d [Hz]\n", i + 1, B[i]);
    }

    return 0;
}

出力結果は

音階の周波数を求めるプログラムの出力結果

となりました。

この音階の周波数が記載しているサイトを参考にすると、おおよそ一致しているのでこの計算結果は正しいと分かります。

4.3 ドレミを鳴らす

この周波数をもとにドレミを鳴らしていきたいと思います。

機材のセッティングをします。圧電スピーカーを27番の穴、GND(接地)に接続するだけです。
27番の穴から信号が送られてきます。

圧電スピーカーとマイコンの接続

ドレミを鳴らすプログラムを作成します。
このプログラムは1秒おきに音が変わるプログラムとなっています。

#include <math.h>

const int BUZZER = 27; // 圧電スピーカーのピン
const int BUZZER_CHANEL = 0;
const int pitch = 440; // 基準音
int BPM = 90;          // テンポ
int C[5];              // ド
int Cs[5];             // ド#
int D[5];              // レ
int Ds[5];             // レ#
int E[5];              // ミ
int F[5];              // ファ
int Fs[5];             // ファ#
int G[5];              // ソ
int Gs[5];             // ソ#
int A[5];              // ラ
int As[5];             // ラ#
int B[5];              // シ

double r = 1.059463094; // 12乗根2

void frequency() // 周波数計算
{
    A[3] = pitch; //4オクターブ目のラの音の周波数を440Hzに設定 
    for (int i = 0; i <= 4; i++)
    {
        A[i] = A[3] * pow(2, i - 3);
        Gs[i] = A[i] * pow(r, -1);
        G[i] = A[i] * pow(r, -2);
        Fs[i] = A[i] * pow(r, -3);
        F[i] = A[i] * pow(r, -4);
        E[i] = A[i] * pow(r, -5);
        Ds[i] = A[i] * pow(r, -6);
        D[i] = A[i] * pow(r, -7);
        Cs[i] = A[i] * pow(r, -8);
        C[i] = A[i] * pow(r, -9);
        As[i] = A[i] * pow(r, 1);
        B[i] = A[i] * pow(r, 2);
    }
}

void setup()
{
    ledcSetup(BUZZER_CHANEL, 12000, 8);
    ledcAttachPin(BUZZER, BUZZER_CHANEL); // 圧電スピーカーを使うセットアップ
    frequency();                          // 周波数を計算
}

void play(int note, int length) // 指定した音を指定した長さで鳴らす
{
    ledcWriteTone(BUZZER_CHANEL, note); // 音を鳴らす
    delay(length);                      // 音を延ばす
}

void loop()
{

    play(1000, C[3]); // ド
    play(1000, D[3]); // レ
    play(1000, E[3]); // ミ
    play(1000, F[3]); // ファ
    play(1000, G[3]); // ソ
    play(1000, A[3]); // ラ
    play(1000, B[3]); // シ
    play(1000, C[4]); // ド


}

これを実際に行ったのがこの動画です。

キレイにドレミが流せました!

5 リズムをつける

次に音楽の3大要素の一つであるリズムを構成していきましょう。
音楽のテンポを表す単位として BPM[Beats Per Minute]があります。
これは一分間に四分音符を何回鳴らすのかを表します。
例えば、 BPM = 60 とすると、一分間に60回。つまり、1秒に1回鳴ることになります。
他の例を見ていきましょう。
BPM = 120 とすると、一分間に120回。つまり、1秒に2回鳴ります。さきほどのちょうど2倍ですね。これを利用すれば四分音符は何秒間鳴らし続けるのかを表すことができます。
さきほどの BPM = 120 の例を考えます。
1秒に2回鳴るので、鳴っている時間は$${\frac 1 2 = 0.5}$$秒であることが求められます。このことから、BPMを
$${BPM = T}$$
とすると
四分音符が鳴っている時間$${t}$$は
$${t = \frac {60} {T}}$$
と分かります。
八分音符、十六分音符はそれぞれ四分音符を$${frac 1 2}$$倍、$${frac 1 4}$$したもの、
二分音符、全音符はそれぞれ2倍、4倍したもの
であるので、四分音符が鳴っている時間が分かれば全ての音符の鳴っている時間が分かります。
Time Lapse LightsのBPMは 176 であるので、これをもとにそれぞれの音符の鳴っている時間を求めていきましょう。
下が音符の鳴っている時間を求めるプログラムです。このマイコンの制御はミリ秒なので得た値を1000倍しています。

#include <stdio.h>

int main()
{
    int BPM = 176;
    int beat4 = 60 * 1000 / BPM; //四分音符
    int beat2 = beat4 * 2; //二分音符
    int beat1 = beat4 * 4; //全音符
    int beat8 = beat4 / 2; //八分音符
    int beat16 = beat4 /4; //十六分音符

    //結果の出力
    printf("四分音符 %d\n", beat4);
    printf("八分音符 %d\n", beat8);
    printf("十六分音符 %d\n", beat16);
    printf("二音符 %d\n", beat2);
    printf("全音符 %d\n", beat1);

    return 0;
}

結果はこうなりました。

四分音符 340[ms]
八分音符 170[ms]
十六分音符  85[ms]
二音符       680[ms]
全音符     1360[ms]

今までの説明したものをつかって、演奏していきましょう!

6 結果

ではTime Lapse Lightsを流すプログラムを書いていきます!メロディですが、僕は耳コピができないのでジャンクさん(Twitter(X):@jank3698)に耳コピをしていただきました。ありがとうございます!
プログラムは凄く長くなるので省きます。
結果はこちらになります。

7 最後に

周波数と音符の長さを逆にしていたという重大なミスをしたりしましたが、なんとか目標を達成できました!
明日はしゅんさん(Twitter(X):@shun09059)の記事です。
最後まで読んでくださりありがとうございました!


この記事が気に入ったらサポートをしてみませんか?