蜂鳴器、ledcWriteTone()進階練習:結合按鈕開關、蜂鳴器、LED指示燈的音樂播放器

使用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完成上週的音樂播放作業。

  1. 開機後,音樂停止播放、LED指示燈亮。
  2. 每按一下按鈕開關,可開始或停止播放音樂。
  3. 長按按鈕開關,重頭播放音樂。
  4. 當音樂正在播放時,LED指示燈會閃爍;當音樂停止播放時,LED指示燈會停止閃爍。
  5. 請在Wokwi網站模擬後,完成實體接線。
  6. 請拍照、錄影,並完成實習報告(PDF格式),上傳Google Classroom作業。
  7. 實習報告須包含下列內容:(1)摘要 (2)動機 (3)主題 (4)實作步驟 (5)實作結果 (6)心得與討論 (7)參考資料
  8. 加入Google Classroom課程代碼:e4mofe7z

作業範例:

控制116乙陳翊涵
控制116乙林澄鈺
控制116乙王博玄
控制116乙林澄鈺