見出し画像

プログラムと電子工作・楽曲演奏(5)SpeakerHATへPWM出力2

楽曲演奏(2)では、五線譜の音符を一つひとつ鳴らして曲を奏でました。音階に対応する周波数を Pulse Width Modulation(PWM)出力機能を使用して出力しました。

楽曲演奏(3)では、WAVE形式の音楽ファイルのデータをデジタル・アナログ変換(DAC)して出力しました。

今回の楽曲演奏(5)では、WAVE形式の音楽ファイルのデータを Pulse Width Modulation(PWM)変換して出力します。M5StickC の PWM出力は、符号なし 8ビット整数を PWM のパルス幅デューティ比 0~100%に変換します。

楽曲演奏(2)と(3)の合せ技です。PWM出力は内部スピーカーに対しても有効(M5StickC Plus のみ、M5StickC は無効)ですが、めっちゃ音が小さいです(耳にくっつけてやっと聞こえるくらい)。

WAVE形式のファイルの構造は、「waveファイル 構造」でググって調べてください。ここでは音の波形データが一定の周期で並んでいる形式を入力データとして取り扱います。端的にいうと、モノラルで録音された、拡張子が .wav のファイルで M5StickC のフラッシュメモリに書き込めるサイズの楽曲です。データ部分だけを抜き出して使用します。

曲目はヴィヴァルディの「四季」より「春」の冒頭部分 8秒間です。


目標

  • 外部接続した SpeakerHAT で音を鳴らします。

  • ヴィヴァルディの「四季」より「春」の冒頭部分を演奏します。

部品・機材

使用する部品は次のとおりです。SpeakerHAT を使用します。

電子部品

開発用機材

  • PC(Windows10 または 11)、開発環境 Arduino-IDE 導入ずみ

  • USB-A・USB-C ケーブル

開発手順

  1. M5StickC Plus に SpeakerHAT を接続する。

  2. PC と M5StickC Plus を USBケーブルで接続する。

  3. Arduino-IDE でスケッチ pwm2.ino を開く。

  4. 同じフォルダに vivaldi_1ch_16000hz_uint8.h を保存する。

  5. 検証・コンパイルする。

  6. M5StickC Plus に書き込む。

  7. 上ボタン(押しボタンA)を押して、演奏を開始する。

  8. 曲が流れることを確認する。

スケッチ

pwm2.ino

#include <M5StickCPlus.h>
#include "vivaldi_1ch_16000hz_uint8.h"
#define SAMPLING_FREQ 16000
#define LEDCWRITE_DELAY 7  /*ledcWrite()処理時間(ms)*/
//  実験的に ledcWrite()の実行時間が 7マイクロ秒かかることが判明した。

#define SPEAKER_PIN GPIO_NUM_26  /*SpeakerHAT 26ピン*/

// PWM出力
const uint8_t PWM_CHANNEL = 0;  /*PWMチャンネル、0 or 1*/
const uint8_t PWM_RESOLUTION = 8;  /*PWM分解能(ビット)*/
const uint32_t PWM_FREQUENCY = getApbFrequency() / (1U << PWM_RESOLUTION);  /*PWM周波数*/

//------------------------------------------------------------------------------
//  setup()
void setup() {
    //  電源ON時に 1回だけ実行する処理をここに書く。
    M5.begin();               /*M5を初期化する*/
    M5.Axp.ScreenBreath(20);  /*画面の輝度を少し下げる*/
    M5.Lcd.setTextSize(3);    /*文字サイズはちょっと小さめ*/
    M5.Lcd.setRotation(3);    /*上スイッチが左になる向き*/
    M5.Lcd.println("pwm2");

    /*setCpuFrequencyMhz(80);*/
    /*240, 160, 80, 40, 20MHz と変えるとだんだん演奏速度が遅くなる。*/

    Serial.begin(115200);   /*デバッグ用のシリアル通信を初期化する*/

    M5.Lcd.println("BtnA to start");

    pinMode(SPEAKER_PIN, OUTPUT);

    // スピーカーの設定
    ledcSetup(PWM_CHANNEL, PWM_FREQUENCY, PWM_RESOLUTION);
    ledcAttachPin(SPEAKER_PIN, PWM_CHANNEL);
    ledcWrite(PWM_CHANNEL, 0);
}
//------------------------------------------------------------------------------
//  loop()
void loop() {
    //  自動的に繰り返し実行する処理をここに書く。
    uint32_t start_millis = 0;

    M5.update();

    if (M5.BtnA.wasPressed()) {
        //  ヴィヴァルディ「春」を演奏する。
        //  M5StickC Plus の場合、SPEAKER_PINが GPIO_NUM_2(内蔵スピーカ)でも動作する。
        //  GPIO_NUM_2には PWM機能がある。
        M5.Lcd.println("spring");
        //  ■  演奏が終了するまで playMusic()関数はブロックされる。
        Serial.println("*** playMusic()");
        start_millis = millis();  /*演奏開始時刻*/
        playMusic(wav, sizeof(wav) / sizeof(wav[0]), SAMPLING_FREQ);
        Serial.printf("*** elapsed time: %4.1f sec\n", (millis() - start_millis) / 1000.0);  /*演奏時間*/
    }
    delay(1);
}
//------------------------------------------------------------------------------
//  playMusic
//  楽曲データを演奏開始する。
//      in: const uint8_t* music_data 8ビット符号なし整数の音の振幅データ
//          const uint32_t length music_dataのバイト数
//          const uint32_t sample_rate 楽曲データのサンプリング周波数
void playMusic(const uint8_t* music_data, const uint32_t length, const uint32_t sample_rate)
{
    uint32_t delay_interval = (uint32_t)1000000 / sample_rate - LEDCWRITE_DELAY;  /*ledcWrite()処理時間を引く*/
    for (uint32_t i = 0; i < length; i++) {
        ledcWrite(PWM_CHANNEL, music_data[i]);
        delayMicroseconds(delay_interval);
    }
    //  フェードアウト
    for (int t = music_data[length - 1]; t >= 0; t--) {
        ledcWrite(PWM_CHANNEL, t);
        delay(2);
    }
    ledcWrite(PWM_CHANNEL, 0);
}

#include "vivaldi_1ch_16000hz_uint8.h" が、楽曲データです。const uint8_t wav[] PROGMEM = { 0x80, 0x80, 0x80, …}; という符号なし 8ビット整数の配列です。このデータは .wavファイルから python で生成しています(別の記事で紹介します)。

#define SAMPLING_FREQ 16000 は楽曲データのサンプリング周波数と一致させてください。

playMusic()関数

playMusic()が楽曲データを演奏する関数です。楽曲のサンプリング周波数から理論的な周期(マイクロ秒)を計算し、ledcWrite()の処理時間を引きます。

1番目の forループは、波形データを一定時間間隔で順番に出力する処理です。1音ずつのデータを ledcWrite()でデューティ比 0~100%に変換し、SPEAKER_PINへ出力します。delayMicroseconds()で実際的な周期だけ待ちます。

ledcWrite()の処理時間は処理系に依存するので、LEDCWRITE_DELAY は処理系ごとに求める必要があります。処理系依存のコードは不具合の原因になるので好ましくないです。プログラムで求めたいところです。
setup()内の setCpuFrequencyMhz(80); で CPUクロック周波数を変えてみれば処理系依存が体感できます。

2番めの forループは、最後の 1音を徐々に小さくするフェードアウト処理です。無音にする処理をしないと、演奏後も最後の 1音が鳴り続けます。フェードアウトなしでいきなり無音にする(ledcWrite(PWM_CHANNEL, 0);)とプチッというノイズが入ります。

楽曲演奏(その3)SpeakerHATへDAC出力のスケッチと見比べると、dacWrite() を ledcWrite() に書き換えていることが分かります。つまりスピーカー出力の一音の大きさを DACで変化させているか PWMで変化させているかの相違です。

内蔵スピーカー出力(2ピン)は PWM機能がありますので、このスケッチで演奏できます(耳を M5StickC本体に当てないと聞こえないくらい小さな音量ですが)。

結果

上ボタン(押しボタンA)を押すと、曲が流れます。大きな音は出ないです。

このスケッチでは、演奏中は他の処理ができないので、あまり実用性がないのは、楽曲演奏(その2)SpeakerHATへPWM出力楽曲演奏(その3)SpeakerHATへDAC出力と同じです。


写真1 pwm2.ino の実行結果

練習問題

  1. CPUクロックを 240, 160, 80, 40, 20MHzと変えてみて、演奏速度がどうなるか試してください。

  2. playMusic()内のフェードアウトも ledcWrite(SPEAKER_PIN, 0); も行わないとどうなるか試してください。

  3. playMusic()内のフェードアウトを行わないで、いきなり ledcWrite(SPEAKER_PIN, 0); を実行するとどうなるか試してください。

  4. 様々な WAVEファイルを演奏してみてください。

  5. ledcWrite()関数を割込み処理で実行するように、スケッチを変更してください。楽曲演奏(その4)割込み処理でDAC出力が参考になります。

参考

  • SpeakerHAT の回路図は、スイッチサイエンスの Webサイトに掲載されています。

ライセンス

このページのソースコードは、複製・改変・配布が自由です。営利目的にも使用してかまいませんが、何ら責任を負いません。


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