見出し画像

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

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

今回の楽曲演奏(3)では、WAVE形式の音楽ファイルのデータをデジタル・アナログ変換(DAC)して出力します。M5StickC の DAC出力は、符号なし 8ビット整数を 0~3.3Vの電圧に変換します。

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 でスケッチ dacwrite.ino を開く。

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

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

  6. M5StickC Plus に書き込む。

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

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

スケッチ

dacwrite.ino

#include <M5StickCPlus.h>
#include "vivaldi_1ch_16000hz_uint8.h"
#define SAMPLING_FREQ 16000
#define VOLUME 11  /*音量、0~11 最大*/
#define DACWRITE_DELAY 20  /*dacWrite()処理時間(ms)*/
//  実験的に dacWrite()の実行時間が 20マイクロ秒かかることが判明した。

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

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

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

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

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

    pinMode(SPEAKER_PIN, OUTPUT);
}
//------------------------------------------------------------------------------
//  loop()
void loop() {
    //  自動的に繰り返し実行する処理をここに書く。
    uint32_t start_millis = 0;

    M5.update();

    if (M5.BtnA.wasPressed()) {
        //  ヴィヴァルディ「春」を演奏する。
        //  M5StickC Plus の場合、SPEAKER_PINが GPIO_NUM_2では動作しない。
        //  GPIO_NUM_2には DA変換機能がない。
        M5.Lcd.println("spring");
        //  ■  演奏が終了するまで playMusic()関数はブロックされる。
        Serial.println("*** playMusic()");
        start_millis = millis();  /*演奏開始時刻*/
        playMusic(wav, sizeof(wav) / sizeof(wav[0]), SAMPLING_FREQ, VOLUME);
        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 楽曲データのサンプリング周波数
//          const uint8_t volume 音量、最大 11、最小 0
//  ・  wavファイルの場合は、dataチャンクを 8ビット符号なし整数に変換する。
//  ・  M5StickC Plus の場合、SPEAKER_PIN は GPIO_NUM_25、GPIO_NUM_26 の 2ピンでのみ動作する。
//  ・  delay_interval は理論値から DACWRITE_DELAYを減じる。なぜこれほど処理時間がかかるのか不明。
void playMusic(const uint8_t* music_data, const uint32_t length, const uint32_t sample_rate, const uint8_t volume)
{
    uint32_t delay_interval = (uint32_t)1000000 / sample_rate - DACWRITE_DELAY;  /*dacWrite()処理時間を引く*/
    uint8_t vol = (volume >= 11) ? 1 : 11 - volume;
    for (uint32_t i = 0; i < length; i++) {
        dacWrite(SPEAKER_PIN, music_data[i] / vol);
        delayMicroseconds(delay_interval);
    }
    //  フェードアウト
    for (int t = music_data[length - 1] / vol; t >= 0; t--) {
        dacWrite(SPEAKER_PIN, t);
        delay(2);
    }
}

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

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

playMusic()関数

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

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

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

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

結果

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

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

写真1 dacwrite.ino の実行結果

練習問題

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

  2. playMusic()内のフェードアウトを行わない(forループを削除する)とどうなるか試してください。

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

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

参考

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

  • 参考にした関数 playMusic() は、次のファイルで定義されています。
    C:\Users\<ユーザ>\Documents\Arduino\libraries\M5StickCPlus\src\utility\Speaker.h
    C:\Users\<ユーザ>\Documents\Arduino\libraries\M5StickCPlus\src\utility\Speaker.cpp

ライセンス

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


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