아두이노를 사용하신다면 간단한 비프(beep)음을 내는데 사용하는 버저 또는 피에조 스피커를 가지고 계실겁니다. 이걸 이용해서 음악을 연주하는 방법을 소개합니다. 특히 슈퍼마리오 배경음악으로 사용되었던 곡을 연주해 볼겁니다.
버저를 사용하는 기본적인 방법은 아두이노 디지털 핀에 연결해서 digitalWrite() 함수로 5V 전압을 on/off 하는 방식입니다. 하지만 이런 방법으로는 간단한 비프(beep)음만 만들 수 있습니다. 아두이노의 PWM 기능을 이용해서 analogWrite()를 해주면 되지 않을까 생각할 수도 있는데, 실제로 analogWrite()를 사용하면 볼륨 조절의 기능이 될 뿐 음계를 표현할 수는 없습니다.
음악을 만들기 위해서는 각 음계의 주파수에 맞는 사각파(square wave)를 만들어줘야 합니다. 아두이노에서 tone() 함수를 통해 이 기능을 제공합니다. tone() 함수는 analogWrite() 없이 자체적으로 square wave 를 만들어줍니다. 따라서 일반 디지털 핀 아무거나 하나 사용하면 됩니다.
tone() 함수는 주파수에 맞는 square wave 를 만들기 위해 자체적으로 timer를 사용합니다.(타이머 인터럽트로 동작) 따라서 아두이노 setup(), loop() 함수에서 delay()를 사용해 아두이노를 멈추더라도 tone() 함수는 동작합니다. 만약 음악을 중간에 멈추고 싶다면 noTone() 함수를 사용하면 됩니다.
tone() 함수에서 사용하는 타이머는 아두이노 디지털 3번과 11번 PWM 핀과 충돌을 일으킬 수 있으므로 주의해야 합니다.
연결 방법
버저의 사용법은 엄청 간단합니다. 단순히 디지털 핀 하나를 사용하면 됩니다. 볼륨 조절이 필요한 경우 333~1K 옴 저항을 추가로 연결해주면 됩니다.
스케치
음악이 연주되도록 스케치를 작성해 보겠습니다. 먼저 tone() 함수의 사용법 부터 봐야겠습니다.
- tone(pin, frequency)
- tone(pin, frequency, duration)
두 가지 방법이 있는데 여기서는 두 번째 방법을 사용합니다. 음을 재생하는데 필요한 값은 [디지털 핀 번호], [주파수], [재생 시간] 입니다. 따라서 음악을 만들기 위해서는 각 음계에 맞는 주파수 값과 재생 시간을 미리 만들어 둬야 합니다. 아래 스케치에서는 melody[] 배열과 tempo[] 배열로 미리 만들어 뒀습니다.
melody[] 배열을 보시면 NOTE_E7 형식으로 표현되어 있습니다. 7옥타브 미(E) 음을 출력하라는 의미입니다. 그런데 tone() 함수에는 주파수 값을 넣어야한다고 했습니다. 따라서 NOTE_E7 에 해당하는 주파수 값이 어딘가에 정의되어 있어야 합니다.
주파수 값들은 아두이노 공식 사이트에서 배포되고 있으니 링크에서 텍스트를 복사하세요. 그리고 아두이노 개발환경 우측 상단에서 [화살표 아이콘 – 새 탭] 선택하신 뒤 pitches.h 라는 파일을 만들어 여기에 붙여넣으면 됩니다. (Ctrl+Shift+N 키를 눌러 생성해도 됨)
pitches.h 파일이 저장되었으면 준비는 다 됐습니다. 아래 스케치를 복사해서 넣으시고, 업로드 후 음악을 감상하시면 됩니다.
#include "pitches.h" #define melodyPin 6 unsigned long prevPlayTime = 0; unsigned long playDuration = 0; int currentMelody = 0; //Mario main theme melody int melodySize = 75; int melody[] = { NOTE_E7, NOTE_E7, 0, NOTE_E7, 0, NOTE_C7, NOTE_E7, 0, NOTE_G7, 0, 0, 0, NOTE_G6, 0, 0, 0, NOTE_C7, 0, 0, NOTE_G6, 0, 0, NOTE_E6, 0, 0, NOTE_A6, 0, NOTE_B6, 0, NOTE_AS6, NOTE_A6, 0, NOTE_G6, NOTE_E7, NOTE_G7, NOTE_A7, 0, NOTE_F7, NOTE_G7, 0, NOTE_E7, 0,NOTE_C7, NOTE_D7, NOTE_B6, 0, 0, NOTE_C7, 0, 0, NOTE_G6, 0, 0, NOTE_E6, 0, 0, NOTE_A6, 0, NOTE_B6, 0, NOTE_AS6, NOTE_A6, 0, NOTE_G6, NOTE_E7, NOTE_G7, NOTE_A7, 0, NOTE_F7, NOTE_G7, 0, NOTE_E7, 0,NOTE_C7, NOTE_D7, NOTE_B6, 0, 0 }; //Mario main them tempo int tempo[] = { 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 9, 9, 9, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 9, 9, 9, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, }; void sing() { if(millis() < prevPlayTime + playDuration) { // music is playing. do nothing return; } // stop the tone playing: noTone(8); if(currentMelody >= melodySize) currentMelody = 0; // to calculate the note duration, take one second // divided by the note type. //e.g. quarter note = 1000 / 4, eighth note = 1000/8, etc. int noteDuration = 1000/tempo[currentMelody]; tone(melodyPin, melody[currentMelody], noteDuration); prevPlayTime = millis(); playDuration = noteDuration * 1.30; currentMelody++; } void setup() { pinMode(melodyPin, OUTPUT); } void loop() { // Do what you want // play music sing(); }
실제 연주를 하는 부분은 sing() 함수입니다. 소스코드를 조금 더 자세히 살펴보겠습니다.
sing() 함수 첫 부분에서 현재 시간을 체크합니다. loop() 함수가 굉장히 빠르게 반복되면서 계속 sing() 함수를 호출하는데 이미 음을 재생하는 중이라면 음 재생이 끝날 때까지는 다른 음을 재생해서는 안되기 때문입니다.
if(millis() < prevPlayTime + playDuration) { // music is playing. do nothing return; }
일단 음 재생이 끝났으면 다음 음을 재생하기 위해 기존 음 재생을 멈춥니다.
// stop the tone playing: noTone(8);
음악을 끝까지 연주했으면 다시 처음부터 재생하도록 배열 index 를 체크하고
if(currentMelody >= melodySize) currentMelody = 0;
재생할 음의 재생 시간을 구합니다. tempo[] 배열에 각 음의 재생 시간이 기록되어 있는데, 8분 음표 4분 음표… 이런 단위로 기록되어 있습니다. 그래서 1초(1000 밀리초)를 tempo 값으로 나누면 재생 시간이 나옵니다.
// to calculate the note duration, take one second // divided by the note type. //e.g. quarter note = 1000 / 4, eighth note = 1000/8, etc. int noteDuration = 1000/tempo[currentMelody];
이제 tone() 함수를 이용해서 음을 재생합니다.
tone(melodyPin, melody[currentMelody], noteDuration); prevPlayTime = millis(); playDuration = noteDuration * 1.30; currentMelody++;
음 재생이 끝날 때까지 sing() 함수가 다시 실행되지 않도록 prevPlayTime 값을 현재 시간으로 설정해줍니다. 그리고 playDuration 값을 재생시간의 1.3 배로 설정해줍니다. playDuration 값은 음 재생이 끝날 때까지 기다려야 하는 시간을 의미합니다. 실제 음 재생시간의 1.3배가 적당하다고 합니다.
마지막으로 배열의 index를 증가시켜 다음 음을 재생할 준비를 합니다.
이상의 스케치를 실행하면 슈퍼 마리오 배경음악을 버저로 재생할 수 있습니다. 위 소스코드는 loop() 함수안에서 다른 작업을 하더라도 영향을 받지 않도록 작성한 코드입니다. 따라서 소스코드를 복사해서 다른 프로젝트에 간단히 적용할 수 있습니다.
이 예제에는 하나의 음악만 사용했는데 배경음악으로 사용 가능한 데이터가 더 있습니다. 아래 코드를 추가해서 사용해보세요.
//Underworld melody int underworld_melody[] = { NOTE_C4, NOTE_C5, NOTE_A3, NOTE_A4, NOTE_AS3, NOTE_AS4, 0, 0, NOTE_C4, NOTE_C5, NOTE_A3, NOTE_A4, NOTE_AS3, NOTE_AS4, 0, 0, NOTE_F3, NOTE_F4, NOTE_D3, NOTE_D4, NOTE_DS3, NOTE_DS4, 0, 0, NOTE_F3, NOTE_F4, NOTE_D3, NOTE_D4, NOTE_DS3, NOTE_DS4, 0, 0, NOTE_DS4, NOTE_CS4, NOTE_D4, NOTE_CS4, NOTE_DS4, NOTE_DS4, NOTE_GS3, NOTE_G3, NOTE_CS4, NOTE_C4, NOTE_FS4, NOTE_F4, NOTE_E3, NOTE_AS4, NOTE_A4, NOTE_GS4, NOTE_DS4, NOTE_B3, NOTE_AS3, NOTE_A3, NOTE_GS3, 0, 0, 0 }; //Underwolrd tempo int underworld_tempo[] = { 12, 12, 12, 12, 12, 12, 6, 3, 12, 12, 12, 12, 12, 12, 6, 3, 12, 12, 12, 12, 12, 12, 6, 3, 12, 12, 12, 12, 12, 12, 6, 6, 18, 18, 18, 6, 6, 6, 6, 6, 6, 18, 18, 18, 18, 18, 18, 10, 10, 10, 10, 10, 10, 3, 3, 3 };
참고자료