Arduino UNOでボタンを押したら音(wav)を鳴らす仕組みを作ります
SDカードは使わずArduino UNOのフラッシュメモリ(32KB)に載せられる程度の短めの音源です
1. (必要に応じて) FFmpegで音源の加工
鳴らしたい音がse.mp3として手元にあるとします
ffmpeg -i se.mp3 -af "afade=t=out:st=0.8:d=0.4,volume=7.9dB" -to 0:01.2 se_short.mp3
この例では0.8秒地点から0.4秒かけフェードアウトさせ1.2秒以降をカットしています
このようにフェードアウトなどでごまかしつつ秒数をできる限り削って容量削減します
またボリュームを7.9dB上げています
ボリュームについては
ffmpeg -i se.mp3 -vn -af volumedetect -f null -
というコマンドで「max_volume: -7.9 dB」と出たので0dBに合わせるため+7.9dBとしました
2. FFmpegで音源をUnsigned 8bit PCMに変換
ffmpeg -i se_short.mp3 -ar 20000 -ac 1 -f u8 se.wav
-ar 20000←サンプリング20kHz
-ac 1←モノラル
-f u8←Unsigned 8bit PCM
音質を上げたかったので容量と相談しつつ20kHzにしています
拡張子wavですが、RIFFヘッダーのないrawデータですので、PCで再生したい場合は-f u8を外すか一つ前の工程で確認しておきます
後に使うため出力ファイルサイズを確認しメモります
20kHz・1.2秒・8bit・モノラル=24000バイト と計算通りでした
3. ソースコード埋め込み用に配列形式でバイナリデータを列挙
with open("se.wav", "rb") as f:
while chunk := f.read(16):
print(", ".join(f"0x{b:02x}" for b in chunk) + ",")
Pythonでバイナリデータを16進数で列挙させるスクリプトです
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f, 0x7f, 0x80, 0x7f, 0x80, 0x80, 0x81, 0x81, 0x81, 0x81,
・・・
のように配列で記述したような形でバイナリデータが列挙されます
今回手元にPython環境があったのでPythonを使用しましたが、参考サイトではxxdコマンドを使用していたり、「PROGMEM作蔵さん」というHTML版のツールも公開されているようです
4. Arduino・スピーカー・ボタン接続
D3とGNDをスピーカー(圧電スピーカー)へ、
D7とGNDをボタン(スイッチ)へ繋いでいます

5. ソースコード
const unsigned char sound_raw[] PROGMEM = {
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f, 0x7f, 0x80, 0x7f, 0x80, 0x80, 0x81, 0x81, 0x81, 0x81,
// 中略 前工程で出力したバイナリデータ24KB分を張り付ける
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
};
const unsigned int sound_raw_len = 24000; // 前工程で確認したファイルサイズ
const int BUTTON_PIN = 7;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP); // プルアップ抵抗有効化
pinMode(3, OUTPUT); // OCR2Bに対応
TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20); // 8bit高速PWM
TCCR2B = _BV(CS20); // 分周なし
}
void play() {
for (int i = 0; i < sound_raw_len; i++) {
OCR2B = pgm_read_byte_near(&sound_raw[i]);
delayMicroseconds(50); // 1秒÷20kHz=50us
}
}
void loop() {
int value = digitalRead(BUTTON_PIN);
if (value == LOW) {
play();
}
}
音声データ部分はPROGMEMと定義することで本来変数が格納される2KBのRAMではなく、32KBあるフラッシュメモリ側に保存されるようになります
ボタンで使うD7ピンはプルアップ抵抗を有効化し抵抗部品を省略しています
loop()ではボタン状態を読み取り押下を検知したらplay()を呼び出しているだけです
残りの再生部分が特殊で、タイマー割り込みとPWMの設定だけして実際の出力はマイコンに任せています
Arduino UNOに載っているATmega328Pでは、Timer0/1/2といったいくつかタイマー(カウンタ)があり今回はTimer2を利用しています
TCCR2AやTCCR2BはTimer2の動作設定レジスタで、OCR2BはD3ピンのPWM出力を制御するためのレジスタになっています
delayMicroseconds(50);の部分はサンプリング周波数によって調整します
参考記事
https://qiita.com/mml/items/c28d69ab889eb36dd522
https://blog.revetronique.com/arduino_dayon1/
https://stemship.com/arduino-wav2code/
https://nn-hokuson.hatenablog.com/entry/2017/09/01/092945
https://usicolog.nomaki.jp/engineering/avr/avrPWM.html
感想
最初は部屋に転がっていたダイナミックスピーカーを使ったのですが、音量が全然出なかったので後に圧電スピーカーへ変更しましたがそれでも音は小さめでした
また今回「ボタンを押したら音声流したい」という要件で、雰囲気でArduinoを使いましたが、後からChatGPTに聞いてみたら「マイコンを使わずDFPlayer Mini単体で鳴らす方が安くて簡単で高音質」と指摘されたことを書き添えておきます
追記 DFPlayer Mini で同じことを実施
DFPlayer Miniの互換品のMP3-TF-16P(270円)を入手して同じことをしてみました。

ピン接続は以下の通りです。
・ADKEY1 と GND をスイッチに接続
・SPK1 と SPK2 をスピーカーに接続(SPK1が+)
・VCC と GND でUSBから5V電源供給

SDカードはAmazonで見つけた2GBのもの(600円)に以下のようにmp3ファイルを格納しただけです

SDカードの探し方が下手なのかSDカードの方が高くついてしまいました。
アンプが内蔵されているためArduino単体より大きい音量が出てくれました。
再生までにちょっとラグがあるのと、長押しした時に連続再生する機能が不要なのが気になりましたが、
「ボタンを押すと音を鳴らす」という仕組みは確かに簡単に実現できました。