プログラムと電子工作・楽曲演奏(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 を使用します。
電子部品
M5StickC Plus 1台
SpeakerHAT 1台[例:スイッチサイエンス]
開発用機材
PC(Windows10 または 11)、開発環境 Arduino-IDE 導入ずみ
USB-A・USB-C ケーブル
開発手順
M5StickC Plus に SpeakerHAT を接続する。
PC と M5StickC Plus を USBケーブルで接続する。
Arduino-IDE でスケッチ pwm2.ino を開く。
同じフォルダに vivaldi_1ch_16000hz_uint8.h を保存する。
検証・コンパイルする。
M5StickC Plus に書き込む。
上ボタン(押しボタンA)を押して、演奏を開始する。
曲が流れることを確認する。
スケッチ
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出力と同じです。
練習問題
CPUクロックを 240, 160, 80, 40, 20MHzと変えてみて、演奏速度がどうなるか試してください。
playMusic()内のフェードアウトも ledcWrite(SPEAKER_PIN, 0); も行わないとどうなるか試してください。
playMusic()内のフェードアウトを行わないで、いきなり ledcWrite(SPEAKER_PIN, 0); を実行するとどうなるか試してください。
様々な WAVEファイルを演奏してみてください。
ledcWrite()関数を割込み処理で実行するように、スケッチを変更してください。楽曲演奏(その4)割込み処理でDAC出力が参考になります。
参考
SpeakerHAT の回路図は、スイッチサイエンスの Webサイトに掲載されています。
ライセンス
このページのソースコードは、複製・改変・配布が自由です。営利目的にも使用してかまいませんが、何ら責任を負いません。