2015/03/13

Arduino練習:使用tone()播放RTTTL

之前使用tone()透過小喇叭播放周杰倫的青花瓷,現在要使用tone()來播放RTTTL(Ring Tone Transfer Language),這是一種用來儲存鈴聲的格式,譬如底下這一串是不可能的任務主題曲:

"MissionImp:d=16,o=6,b=95:32d,32d#,32d,32d#,32d,32d#,32d,32d#,32d,32d,32d#,32e,32f,32f#,32g,g,8p,g,8p,a#,p,c7,p,g,8p,g,8p,f,p,f#,p,g,8p,g,8p,a#,p,c7,p,g,8p,g,8p,f,p,f#,p,a#,g,2d,32p,a#,g,2c#,32p,a#,g,2c,a#5,8c,2p,32p,a#5,g5,2f#,32p,a#5,g5,2f,32p,a#5,g5,2e,d#,8d"

電路圖(Fritzing格式)與程式原始碼,可到此下載。其中播放RTTTL的函式,來自於Brett Hagman。 

Arduino板子腳位3,接到100 ohm電阻,電阻另一端接到揚聲器的正極。

Arduino板子的GND,接到揚聲器的負極。

程式碼:
#define BAUDRATE 38400
#define BUZZER_PIN 3


#define OCTAVE_OFFSET 0

int notes[] = { 0,
262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494,
523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988,
1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976,
2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951
};


// 底下是鈴聲的RTTTL資料
const char *song1 = "Indiana..." // 印第安那瓊斯,省略RTTTL資料內容
const char *song5 = "Xfiles..." // X檔案
const char *song10 = "StarWars..." // 星際大戰
const char *song13 = "A-Team..." // 天龍特攻隊
const char *song20 = "MissionImp..." // 不可能的任務
// ...省略其他鈴聲

// 把想播放的鈴聲,放進來
#define SONGS_NUM 6
const char *songs[SONGS_NUM] = {
  song1, song10, song13,
};


#define isdigit(n) (n >= '0' && n <= '9')

// 底下是播放RTTTL的函式
void play_rtttl(const char *p)
{ // 並沒有做任何錯誤偵測
  byte default_dur = 4;
  byte default_oct = 6;
  int bpm = 63;
  int num;
  long wholenote;
  long duration;
  byte note;
  byte scale;

  // 格式:d=N,o=N,b=NNN:
  // 跳過name,找到開頭處
  while(*p != ':') // 跳過name
    p++;
  p++;                     // 跳過':'

  // 取得預設duration
  if(*p == 'd')
  {
    p++; p++;              // 跳過"d="
    num = 0;
    while(isdigit(*p)){
      num = (num * 10) + (*p++ - '0');
    }
    if(num > 0)
      default_dur = num;
    p++;                   // 跳過','
  }
  Serial.print("default_dur: ");
  Serial.println(default_dur, 10);

  // 取得預設octave
  if(*p == 'o')
  {
    p++; p++;              // 跳過"o="
    num = *p++ - '0';
    if(num >= 3 && num <=7)
      default_oct = num;
    p++;                   // 跳過','
  }
  Serial.print("default_oct: ");
  Serial.println(default_oct, 10);

  // 取得BPM(beats per minute),也就是tempo
  if(*p == 'b')
  {
    p++; p++;              // 跳過"b="
    num = 0;
    while(isdigit(*p)){
      num = (num * 10) + (*p++ - '0');
    }
    bpm = num;
    p++;                   // 跳過':'
  }
  Serial.print("bpm: ");
  Serial.println(bpm, 10);

  // BPM通常表示為每分鐘幾個quarter notes
  wholenote = (60 * 1000L / bpm) * 4;  // 這是全音符的時間(milliseconds)
  Serial.print("wholenote: ");
  Serial.println(wholenote, 10);

  while(*p) // 開始播放每個音符
  {
    // 首先取得音符的uration,如果有的話
    num = 0;
    while(isdigit(*p)){
      num = (num * 10) + (*p++ - '0');
    }
  
    if(num)
      duration = wholenote / num;
    else
      duration = wholenote / default_dur;  // 之後須檢查是否有附點'.'

    // 取得音符
    note = 0;

    switch(*p){
      case 'c':
        note = 1;
        break;
      case 'd':
        note = 3;
        break;
      case 'e':
        note = 5;
        break;
      case 'f':
        note = 6;
        break;
      case 'g':
        note = 8;
        break;
      case 'a':
        note = 10;
        break;
      case 'b':
        note = 12;
        break;
      case 'p':
      default:
        note = 0;
    }
    p++;

    // 取得選用性的'#' sharp
    if(*p == '#'){
      note++;
      p++;
    }

    // 取得選用性的'.' dotted note
    if(*p == '.'){
      duration += duration/2;
      p++;
    }

    // now, get scale
    if(isdigit(*p)){
      scale = *p - '0';
      p++;
    }
    else{
      scale = default_oct;
    }
    scale += OCTAVE_OFFSET;

    if(*p == ',')
      p++;       // 跳過',',處理下個音符,或到尾巴了

    // 播放音符
    if(note){
      Serial.print("Playing: ");
      Serial.print(scale, 10); 

      Serial.print(' ');
      Serial.print(note, 10); 

      Serial.print(" (");
      Serial.print(notes[(scale - 4) * 12 + note], 10);
      Serial.print(") ");
      Serial.println(duration, 10);
      tone(BUZZER_PIN, notes[(scale - 4) * 12 + note]);
      delay(duration);
      noTone(BUZZER_PIN);
    }
    else{
      Serial.print("Pausing: ");
      Serial.println(duration, 10);
      delay(duration);
    }
  }
}


// setup與loop,沒什麼,就是把鈴聲資料丟給play_rtttl就對了
void setup(void)
{
  Serial.begin(BAUDRATE);
}
void loop(void)
{
  int i;
  Serial.println("Start.");
  for(i = 0; i < SONGS_NUM; i++){
    play_rtttl(songs[i]);
    delay(1000);
  }
  Serial.println("Done.");
  delay(3000);
}


若想聽聽印第安那瓊斯、星際大戰、天龍特攻隊的鈴聲效果,請到YouTube聆聽,音量很小,請自行放大音量。

9 comments:

  1. 大大妳好,我是個對Arduino有興趣的大二生,我是想要自學,請問要入門的話買哪一塊主板比較好呢?我未來是有想考AMA的認證,麻煩大大指點一條明路

    ReplyDelete
  2. 入門的話建議買Arduino UNO,最廣為使用的一塊板子,很多範例與文件,也都是以UNO為準。

    雖然有更新的板子,譬如Leonardo,但有著諸多小差異,可能會對初學者造成困擾。
    關於UNO與Leonardo的差異,可參考http://yehnan.blogspot.tw/2013/09/arduinoleonardouno.html。

    關於AMA認證,您學校教授應能給出更好的建議。

    ReplyDelete
  3. 葉先生,您好
    不好意思,我也是個arduio的新手,我有買個UNO的板子,我想試著使用arduino做個網路轉Rs232的轉換器,硬體:enc28j60+arduino uno
    我收尋了很久但都沒有什麼收穫,想請教葉先生
    謝謝

    ReplyDelete
  4. 網路轉Rs232的轉換器?
    這是什麼意思?

    ReplyDelete
  5. 像似
    http://en.usr.cn/RS232-serial-to-ethernet-converter-tcp-ip-module

    ReplyDelete
    Replies
    1. 嗯,了解,就是從RS232序列介面、轉到Ethernet。
      硬體部分,Ethernet由enc28j60負責,然後你需要RS232轉一般Serial(3.3V or 5V)的轉接器,負責電腦-RS232-轉接器-Serial-Arduino微控制器晶片之間的溝通。

      那麼,從RS232要怎麼下網路相關指令呢?知道這部份的話,接下來就是撰寫Arduino程式,從RS232接收指令,呼叫enc28j60程式庫,傳出或接收網路封包,

      關於指令的部份,或可參考ESP8266的作法:
      http://www.instructables.com/id/Using-the-ESP8266-module/?ALLSTEPS

      以上只是從理論上猜想,我沒有相關經驗,僅供參考。

      Delete
  6. 您的RS232端的轉接器意思是用MAX232?

    ReplyDelete
    Replies
    1. 恩 謝謝您
      我再慢慢摸索如何ethernet端封包聽取轉換封包對serial端輸出的程式碼
      這部分,第一次接觸有點頭疼,要找個管道學一下

      Delete