プログラムと電子工作・楽曲演奏(3)SpeakerHATへDAC出力
楽曲演奏(2)では、五線譜の音符を一つひとつ鳴らして曲を奏でました。音階に対応する周波数を Pulse Width Modulation(PWM)出力機能を使用して出力しました。
今回の楽曲演奏(3)では、WAVE形式の音楽ファイルのデータをデジタル・アナログ変換(DAC)して出力します。M5StickC の DAC出力は、符号なし 8ビット整数を 0~3.3Vの電圧に変換します。
WAVE形式のファイルの構造は、「waveファイル 構造」でググって調べてください。ここでは音の波形データが一定の周期で並んでいる形式を入力データとして取り扱います。端的にいうと、モノラルで録音された、拡張子が .wav のファイルで M5StickC のフラッシュメモリに書き込めるサイズの楽曲です。データ部分だけを抜き出して使用します。
曲目はヴィヴァルディの「四季」より「春」の冒頭部分 8秒間です。
目標
外部接続した SpeakerHAT で音を鳴らします。
ヴィヴァルディの「四季」より「春」の冒頭部分を演奏します。
部品・機材
使用する部品は次のとおりです。SpeakerHAT を使用します。
電子部品
M5StickC Plus 1台
SpeakerHAT 1台[例:スイッチサイエンス]
開発用機材
PC(Windows10 または 11)、開発環境 Arduino-IDE 導入ずみ
USB-A・USB-C ケーブル
開発手順
M5StickC Plus に SpeakerHAT を接続する。
PC と M5StickC Plus を USBケーブルで接続する。
Arduino-IDE でスケッチ dacwrite.ino を開く。
同じフォルダに vivaldi_1ch_16000hz_uint8.h を保存する。
検証・コンパイルする。
M5StickC Plus に書き込む。
上ボタン(押しボタンA)を押して、演奏を開始する。
曲が流れることを確認する。
スケッチ
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出力と同じです。
練習問題
CPUクロックを 240, 160, 80, 40, 20MHzと変えてみて、演奏速度がどうなるか試してください。
playMusic()内のフェードアウトを行わない(forループを削除する)とどうなるか試してください。
playMusic()内のフェードアウトを行わないで、いきなり dacWrite(SPEAKER_PIN, 0); を実行するとどうなるか試してください。
様々な WAVEファイルを演奏してみてください。
参考
SpeakerHAT の回路図は、スイッチサイエンスの Webサイトに掲載されています。
参考にした関数 playMusic() は、次のファイルで定義されています。
C:\Users\<ユーザ>\Documents\Arduino\libraries\M5StickCPlus\src\utility\Speaker.h
C:\Users\<ユーザ>\Documents\Arduino\libraries\M5StickCPlus\src\utility\Speaker.cpp
ライセンス
このページのソースコードは、複製・改変・配布が自由です。営利目的にも使用してかまいませんが、何ら責任を負いません。