
使用delay()函式的音樂播放程式,會造成程式阻塞,等待暫停的時間過去後才會繼續執行下一行指令,無法同時執行其他任務,例如:按鍵輸入、螢幕顯示、指示燈顯示等。若需要多工處理其他任務,必須改用millis()函式,參考程式如下:
原使用delay()程式
// ---------------------------------------------------------------------
// 定義音符頻率 (Hz)
#define C4 262 // Do
#define D4 294 // Re
#define E4 330 // Mi
#define F4 349 // Fa
#define G4 393 // So
#define A4 440 // La
#define B4 494 // Si
#define REST 0 // 休止符 (頻率 0 即無聲)
// ---------------------------------------------------------------------
// 定義音符結構(Struct),名稱為Note
struct Note {
int frequency; // 音頻 (Hz)
int length; // 音長 (為 base_duration 的倍數)
};
// ---------------------------------------------------------------------
// 建立一個 Note 物件陣列,名稱為 song
Note song[] = {
{G4, 1}, {E4, 1}, {E4, 2}, {F4, 1}, {D4, 1}, {D4, 2}, {C4, 1}, {D4, 1}, {E4, 1}, {F4, 1}, {G4, 1}, {G4, 1}, {G4, 2},
{G4, 1}, {E4, 1}, {E4, 2}, {F4, 1}, {D4, 1}, {D4, 2}, {C4, 1}, {E4, 1}, {G4, 1}, {G4, 1}, {E4, 2},
{D4, 1}, {D4, 1}, {D4, 1}, {D4, 1}, {D4, 1}, {E4, 1}, {F4, 2}, {E4, 1}, {E4, 1}, {E4, 1}, {E4, 1}, {E4, 1}, {F4, 1}, {G4, 2},
{G4, 1}, {E4, 1}, {E4, 2}, {F4, 1}, {D4, 1}, {D4, 2}, {C4, 1}, {E4, 1}, {G4, 1}, {G4, 1}, {C4, 4}
};
// ---------------------------------------------------------------------
// 音符總數 = melody 陣列總位元組數 / 單一整數位元組數
int total = sizeof(song) / sizeof(Note);
// ---------------------------------------------------------------------
int pin = 15; // 蜂鳴器連接腳位 (GPIO 15)
int base_duration = 200; // 音符基準時間 (單位: ms,決定節奏快慢)
int note_interval = 100; // 音符間的短暫間隔 (單位: ms,避免聲音黏在一起)
// ---------------------------------------------------------------------
int i = 0; // 當前播放的音符索引
int state = 1; // 狀態機控制旗標:
// 1:播放音符
// 2:音符間隔
// 3:切換至下個音符
// 4:播放結束
// ---------------------------------------------------------------------
void setup() {
// ESP32 LEDC 設定
ledcAttach(pin, 2000, 10); // 設定腳位, 初始頻率 2000Hz, 解析度 10 bits
ledcWriteTone(pin, 0); // 初始狀態為靜音
}
void loop() {
switch(state)
{
case 1: // 播放音符
if (i < total) // 還有音符沒播完
{
int note_duration = base_duration * song[i].length; // 計算當前音符持續時間
ledcWriteTone(pin, song[i].frequency); // 發出聲音
delay(note_duration); // 音符播放時間
state = 2; // 進入狀態2
}
else // 所有音符已經播完
{
state = 4; // 進入狀態4
}
break;
case 2: // 音符間隔
ledcWriteTone(pin, 0); // 靜音
delay(note_interval); // 間隔時間
state = 3; // 進入狀態3
break;
case 3: // 切換至下一個音符
i++; // 索引值+1
state = 1; // 進入狀態1
break;
case 4: // 播放結束
ledcWriteTone(pin, 0); // 靜音
delay(2000); // 播放結束後暫停2秒
i = 0; // 索引值歸零(重新開始)
state = 1; // 進入狀態1(重新開始)
break;
}
}
改用millis()程式
// ---------------------------------------------------------------------
// 定義音符頻率 (Hz)
#define C4 262 // Do
#define D4 294 // Re
#define E4 330 // Mi
#define F4 349 // Fa
#define G4 393 // So
#define A4 440 // La
#define B4 494 // Si
#define REST 0 // 休止符 (頻率 0 即無聲)
// ---------------------------------------------------------------------
// 定義音符結構(Struct),名稱為Note
struct Note {
int frequency; // 音頻 (Hz)
int length; // 音長 (為 base_duration 的倍數)
};
// ---------------------------------------------------------------------
// 建立一個 Note 物件陣列,名稱為 song
Note song[] = {
{G4, 1}, {E4, 1}, {E4, 2}, {F4, 1}, {D4, 1}, {D4, 2}, {C4, 1}, {D4, 1}, {E4, 1}, {F4, 1}, {G4, 1}, {G4, 1}, {G4, 2},
{G4, 1}, {E4, 1}, {E4, 2}, {F4, 1}, {D4, 1}, {D4, 2}, {C4, 1}, {E4, 1}, {G4, 1}, {G4, 1}, {E4, 2},
{D4, 1}, {D4, 1}, {D4, 1}, {D4, 1}, {D4, 1}, {E4, 1}, {F4, 2}, {E4, 1}, {E4, 1}, {E4, 1}, {E4, 1}, {E4, 1}, {F4, 1}, {G4, 2},
{G4, 1}, {E4, 1}, {E4, 2}, {F4, 1}, {D4, 1}, {D4, 2}, {C4, 1}, {E4, 1}, {G4, 1}, {G4, 1}, {C4, 4}
};
// ---------------------------------------------------------------------
// 音符總數 = melody 陣列總位元組數 / 單一整數位元組數
int total = sizeof(song) / sizeof(Note);
// ---------------------------------------------------------------------
int pin = 15; // 蜂鳴器連接腳位 (GPIO 15)
int base_duration = 200; // 音符基準時間 (單位: ms,決定節奏快慢)
int note_interval = 100; // 音符間的短暫間隔 (單位: ms,避免聲音黏在一起)
// ---------------------------------------------------------------------
int i = 0; // 當前播放的音符索引
int state = 1; // 狀態機控制旗標:
// 1:播放音符
// 2:音符間隔
// 3:切換至下個音符
// 4:播放結束
// ---------------------------------------------------------------------
unsigned long previousMillis = 0; // 上一次狀態開始的時間
int note_duration = 0; // 當前音符應該持續的時間
// ---------------------------------------------------------------------
void setup() {
// ESP32 LEDC 設定
ledcAttach(pin, 2000, 10); // 設定腳位, 初始頻率 2000Hz, 解析度 10 bits
ledcWriteTone(pin, 0); // 初始狀態為靜音
}
void loop() {
unsigned long currentMillis = millis(); // 當前的millis()時間
switch(state)
{
case 1: // 播放音符
if (i < total) // 還有音符沒播完
{
note_duration = base_duration * song[i].length; // 計算當前音符持續時間
ledcWriteTone(pin, song[i].frequency); // 發出聲音
previousMillis = currentMillis; // 更新前一次的millis()時間
state = 2; // 進入狀態2
}
else // 所有音符已經播完
{
previousMillis = currentMillis; // 更新前一次的millis()時間
state = 4; // 進入狀態4
}
break;
case 2: // 音符間隔
if (currentMillis - previousMillis >= note_duration) // 音符播放時間到
{
ledcWriteTone(pin, 0); // 靜音
previousMillis = currentMillis; // 更新前一次的millis()時間
state = 3; // 進入狀態3
}
break;
case 3: // 切換至下一個音符
if (currentMillis - previousMillis >= note_interval) // 間隔時間到
{
i++; // 索引值+1
state = 1; // 進入狀態1 (State 1 會自己重置計時器,所以這裡不用重置)
}
break;
case 4: // 播放結束
if (currentMillis - previousMillis >= 2000) // 暫停2秒時間到
{
i = 0; // 索引值歸零(重新開始)
state = 1; // 進入狀態1(重新開始)
}
break;
}
}
加入按鈕開關(使用OneButton函式庫),按一下開始播放,再按一下停止播放,長按後從頭開始播放,參考程式如下:(參考說明頁面)
// ---------------------------------------------------------------------
// 定義音符頻率 (Hz)
#define C4 262 // Do
#define D4 294 // Re
#define E4 330 // Mi
#define F4 349 // Fa
#define G4 393 // So
#define A4 440 // La
#define B4 494 // Si
#define REST 0 // 休止符 (頻率 0 即無聲)
// ---------------------------------------------------------------------
// 定義音符結構(Struct),名稱為Note
struct Note {
int frequency; // 音頻 (Hz)
int length; // 音長 (為 base_duration 的倍數)
};
// ---------------------------------------------------------------------
// 建立一個 Note 物件陣列,名稱為 song
Note song[] = {
{G4, 1}, {E4, 1}, {E4, 2}, {F4, 1}, {D4, 1}, {D4, 2}, {C4, 1}, {D4, 1}, {E4, 1}, {F4, 1}, {G4, 1}, {G4, 1}, {G4, 2},
{G4, 1}, {E4, 1}, {E4, 2}, {F4, 1}, {D4, 1}, {D4, 2}, {C4, 1}, {E4, 1}, {G4, 1}, {G4, 1}, {E4, 2},
{D4, 1}, {D4, 1}, {D4, 1}, {D4, 1}, {D4, 1}, {E4, 1}, {F4, 2}, {E4, 1}, {E4, 1}, {E4, 1}, {E4, 1}, {E4, 1}, {F4, 1}, {G4, 2},
{G4, 1}, {E4, 1}, {E4, 2}, {F4, 1}, {D4, 1}, {D4, 2}, {C4, 1}, {E4, 1}, {G4, 1}, {G4, 1}, {C4, 4}
};
// ---------------------------------------------------------------------
// 音符總數 = melody 陣列總位元組數 / 單一整數位元組數
int total = sizeof(song) / sizeof(Note);
// ---------------------------------------------------------------------
int pin = 15; // 蜂鳴器連接腳位 (GPIO 15)
int base_duration = 200; // 音符基準時間 (單位: ms,決定節奏快慢)
int note_interval = 100; // 音符間的短暫間隔 (單位: ms,避免聲音黏在一起)
// ---------------------------------------------------------------------
int i = 0; // 當前播放的音符索引
int state = 1; // 狀態機控制旗標:
// 1:播放音符
// 2:音符間隔
// 3:切換至下個音符
// 4:播放結束
// ---------------------------------------------------------------------
unsigned long previousMillis = 0; // 上一次狀態開始的時間
int note_duration = 0; // 當前音符應該持續的時間
// ---------------------------------------------------------------------
#include <OneButton.h> // 引用OneButton函式庫
#define pb 34 // 定義按鈕引腳
OneButton button(pb, true); // 建立OneButton物件,名稱為button,按鈕為低態動作
boolean startPlay = 0; // 0:停止播放,1:開始播放
//----------------------------------------------------------------------
// 單擊處理
void singleClick() {
Serial.println("Button Single Click.");
startPlay = !startPlay; // 開始播放或停止播放
if(startPlay == 0) // 停止播放時
{
ledcWriteTone(pin, 0); // 靜音
}
}
// 雙擊處理
void doubleClick() {
Serial.println("Button Double Click.");
}
// 長按開始
void longPressStart() {
Serial.println("Button Long Press Start.");
ledcWriteTone(pin, 0); // 靜音
}
// 長按過程中
void longPress() {
Serial.println("Button Long Press ......");
startPlay = 0; // 停止播放
}
// 長按結束
void longPressStop() {
Serial.println("Button Long Press Stop.");
i = 0; // 索引值歸零(重新開始)
state = 1; // 進入狀態1(重新開始)
}
//----------------------------------------------------------------------
void setup() {
//----------------------------------------------------------------------
// ESP32 LEDC 設定
ledcAttach(pin, 2000, 10); // 設定腳位, 初始頻率 2000Hz, 解析度 10 bits
ledcWriteTone(pin, 0); // 初始狀態為靜音
//----------------------------------------------------------------------
Serial.begin(9600); // 啟用串列埠監看視窗
//----------------------------------------------------------------------
button.attachClick(singleClick); // 單擊
button.attachDoubleClick(doubleClick); // 雙擊
button.attachLongPressStart(longPressStart); // 長按開始
button.attachDuringLongPress(longPress); // 長按過程中
button.attachLongPressStop(longPressStop); // 長按結束
//----------------------------------------------------------------------
}
void loop() {
//----------------------------------------------------------
button.tick(); // 定期偵測按鈕狀態
//----------------------------------------------------------
if (startPlay == 1) // 當startPlay為1時,狀態機才運作
{
unsigned long currentMillis = millis(); // 當前的millis()時間
switch(state)
{
case 1: // 播放音符
if (i < total) // 還有音符沒播完
{
note_duration = base_duration * song[i].length; // 計算當前音符持續時間
ledcWriteTone(pin, song[i].frequency); // 發出聲音
previousMillis = currentMillis; // 更新前一次的millis()時間
state = 2; // 進入狀態2
}
else // 所有音符已經播完
{
previousMillis = currentMillis; // 更新前一次的millis()時間
state = 4; // 進入狀態4
}
break;
case 2: // 音符間隔
if (currentMillis - previousMillis >= note_duration) // 音符播放時間到
{
ledcWriteTone(pin, 0); // 靜音
previousMillis = currentMillis; // 更新前一次的millis()時間
state = 3; // 進入狀態3
}
break;
case 3: // 切換至下一個音符
if (currentMillis - previousMillis >= note_interval) // 間隔時間到
{
i++; // 索引值+1
state = 1; // 進入狀態1 (State 1 會自己重置計時器,所以這裡不用重置)
}
break;
case 4: // 播放結束
if (currentMillis - previousMillis >= 2000) // 暫停2秒時間到
{
i = 0; // 索引值歸零(重新開始)
state = 1; // 進入狀態1(重新開始)
}
break;
}
}
}
作業練習:請使用按鈕開關、蜂鳴器、LED指示燈,利用OneButton Library完成上週的音樂播放作業。
- 開機後,音樂停止播放、LED指示燈亮。
- 每按一下按鈕開關,可開始或停止播放音樂。
- 長按按鈕開關,重頭播放音樂。
- 當音樂正在播放時,LED指示燈會閃爍;當音樂停止播放時,LED指示燈會停止閃爍。
- 請在Wokwi網站模擬後,完成實體接線。
- 請拍照、錄影,並完成實習報告(PDF格式),上傳Google Classroom作業。
- 實習報告須包含下列內容:(1)摘要 (2)動機 (3)主題 (4)實作步驟 (5)實作結果 (6)心得與討論 (7)參考資料
- 加入Google Classroom課程代碼:e4mofe7z
作業範例: