圧電ブザーはとても便利ですが、普通に音楽を流そうとするとその間他の処理ができませんよね。
ここではRTOSなどを使わずに簡易的に非同期での再生を実現する方法について紹介します。
この記事は Tuton Advent Calender 2025 の20日目の記事です。
前提
Arduinoで圧電ブザーをPWMで再生させることを考えます。
Arduino以外でもArduino言語で書けるマイコンなら同じようにできると思います。
普通に音楽を流す
多くの記事で紹介されていますが、以下のようにしてブザーで音を鳴らすことができます。
#include <Aruduino.h>
#define BuzzrerPin 9
void setup() {
pinMode(BuzzrerPin, OUTPUT);
}
void playNote(int freq)
{
analogWriteFrequency(freq);
analogWrite(BuzzrerPin, 64);
}
void mute()
{
analogWrite(BuzzrerPin, 0);
}
void loop() {
// ドレミファソラシド
playNote(262); // ド
delay(500);
playNote(294); // レ
delay(500);
playNote(330); // ミ
delay(500);
playNote(349); // ファ
delay(500);
playNote(392); // ソ
delay(500);
playNote(440); // ラ
delay(500);
playNote(494); // シ
delay(500);
playNote(523); // ド
delay(500);
mute();
delay(1000);
}
しかし、これではvoid loop()の中でdelay()をしているので音楽を流しながら他の処理ができません。
そこで、以下のように配列に保存しておいてmillis()で時間を見ながら演奏することで疑似的に非同期で動作できます。
#include <Arduino.h>
#define BuzzerPin 9
#define QUEUE_SIZE 16
/* ===== キュー操作 ===== */
struct NoteEvent {
int freq; // 周波数 (0 = mute)
unsigned long dur; // 再生時間 [ms]
};
NoteEvent queue[QUEUE_SIZE];
int qHead = 0;
int qTail = 0;
bool enqueue(int freq, unsigned long dur) {
int next = (qTail + 1) % QUEUE_SIZE;
if (next == qHead) return false; // full
queue[qTail] = {freq, dur};
qTail = next;
return true;
}
bool dequeue(NoteEvent &ev) {
if (qHead == qTail) return false; // empty
ev = queue[qHead];
qHead = (qHead + 1) % QUEUE_SIZE;
return true;
}
/* ===== 再生制御 ===== */
bool playing = false;
unsigned long noteEndTime = 0;
void playNote(int freq) {
if (freq == 0) {
analogWrite(BuzzerPin, 0);
} else {
analogWriteFrequency(freq);
analogWrite(BuzzerPin, 64);
}
}
void updateBuzzer() {
unsigned long now = millis();
if (playing && now < noteEndTime) {
return;
}
playing = false;
// 次のイベントを取得
NoteEvent ev;
if (dequeue(ev)) {
playNote(ev.freq);
noteEndTime = now + ev.dur;
playing = true;
}
}
void setup() {
pinMode(BuzzerPin, OUTPUT);
// ドレミファソラシド をキューに積む
enqueue(262, 500); // ド
enqueue(294, 500); // レ
enqueue(330, 500); // ミ
enqueue(349, 500); // ファ
enqueue(392, 500); // ソ
enqueue(440, 500); // ラ
enqueue(494, 500); // シ
enqueue(523, 500); // ド
enqueue(0, 1000); // 休符
}
void loop() {
updateBuzzer();
// ここで他の処理
}
queueを使うと楽に書くこともできますが、Arduinoなどのメモリの小さいボードでqueueを使うとヒープ領域の断片化などの問題があるそうなので避けた方がいいかもしれません。
#include <Arduino.h>
#include <queue>
#define BuzzerPin 9
struct NoteEvent {
int freq; // 周波数(0 = ミュート)
unsigned long duration; // 再生時間 [ms]
};
std::queue<NoteEvent> noteQueue;
bool isPlaying = false;
unsigned long noteEndTime = 0;
void playNote(int freq) {
if (freq == 0) {
analogWrite(BuzzerPin, 0);
} else {
analogWriteFrequency(freq);
analogWrite(BuzzerPin, 64);
}
}
void updateBuzzer() {
unsigned long now = millis();
// 再生中で、まだ終了していない
if (isPlaying && now < noteEndTime) {
return;
}
// 現在の音が終了
isPlaying = false;
// 次の音符があれば再生
if (!noteQueue.empty()) {
NoteEvent ev = noteQueue.front();
noteQueue.pop();
playNote(ev.freq);
noteEndTime = now + ev.duration;
isPlaying = true;
}
}
void setup() {
pinMode(BuzzerPin, OUTPUT);
noteQueue.push({262, 500}); // ド
noteQueue.push({294, 500}); // レ
noteQueue.push({330, 500}); // ミ
noteQueue.push({349, 500}); // ファ
noteQueue.push({392, 500}); // ソ
noteQueue.push({440, 500}); // ラ
noteQueue.push({494, 500}); // シ
noteQueue.push({523, 500}); // ド
noteQueue.push({0, 1000}); // 休符
}
void loop() {
updateBuzzer();
// ここに他の処理
}
さいごに
時間を配列に保存しておいてmillis()で読んで実行する非同期処理の方法は電子工作で便利なのでぜひ他の用途でも使ってみてください。