아두이노 tone()이나 Windows의 Beep API로 멜로디를 낼 때는 "도"가 몇 Hz인지 숫자로 넣어야 합니다. 매번 계산하기 번거로워서 계이름별 주파수를 표로 만들어 두었습니다.

소리의 높낮이는 주파수로 정해진다

소리는 공기의 진동이고, 1초에 몇 번 떨리느냐(주파수, Hz)에 따라 음의 높이(피치)가 달라집니다. 사람 귀로 들을 수 있는 범위는 대략 20Hz에서 20,000Hz 사이로 알려져 있습니다. 실제 부저나 작은 스피커는 이 범위 전체를 고르게 내지 못하니, 너무 낮거나 높은 음은 소리가 작게 나거나 거의 안 들릴 수 있습니다.

광고

음계의 기준은 피아노 "라(A)" 음 440Hz입니다. 한 옥타브를 올라가면 주파수가 정확히 두 배가 되고, 그 사이를 12개의 반음(A, A#, B, C, C#, D, D#, E, F, F#, G, G#)으로 나누는데, 이때 같은 간격으로 더하는 게 아니라 같은 비율로 곱해 나갑니다(등비수열). 반음 하나 올라갈 때마다 2의 12제곱근, 약 1.0595배씩 커지는 구조입니다.

12음계 건반과 음 이름 배치 그림

그래서 표가 없어도 기준음에서 몇 반음 떨어졌는지만 알면 직접 구할 수 있습니다.

주파수 = 440 × 2^(n / 12)      ' n = A4(440Hz)에서 떨어진 반음 수 (위로 +, 아래로 -)

예) C5 (A4 에서 위로 3반음)  = 440 × 2^(3/12)  ≈ 523.25
    C4 (A4 에서 아래로 9반음) = 440 × 2^(-9/12) ≈ 261.63

MIDI를 다뤄 봤다면 음 번호로 계산하는 쪽이 더 편할 수 있습니다. MIDI 음 번호에서 A4는 69, 가운데 도(C4)는 60이라서 440 × 2^((번호 - 69) / 12)로 바로 주파수가 나옵니다. 코드로 옮기면 C#에서는 440 * Math.Pow(2, (note - 69) / 12.0) 한 줄이고, 여기서 12.0처럼 실수로 나눠야 합니다. 정수끼리 나누면 소수점이 버려져서 반음 단위가 다 무너집니다.

옥타브별 주파수 표 (단위 Hz)

빈칸 없이 반음까지 모두 넣었습니다. 도(C)·레(D)·미(E)·파(F)·솔(G)·라(A)·시(B)가 흰 건반, #이 붙은 음이 검은 건반입니다.

옥타브 C 도 C# D 레 D# E 미 F 파 F# G 솔 G# A 라 A# B 시
1 32.7032 34.64783 36.7081 38.89087 41.20344 43.65353 46.2493 48.99943 51.91309 55 58.27047 61.73541
2 65.40639 69.29566 73.41619 77.78175 82.40689 87.30706 92.49861 97.99886 103.8262 110 116.5409 123.4708
3 130.8128 138.5913 146.8324 155.5635 164.8138 174.6141 184.9972 195.9977 207.6523 220 233.0819 246.9417
4 261.6256 277.1826 293.6648 311.127 329.6276 349.2282 369.9944 391.9954 415.3047 440 466.1638 493.8833
5 523.2511 554.3653 587.3295 622.254 659.2551 698.4565 739.9888 783.9909 830.6094 880 932.3275 987.7666
6 1046.502 1108.731 1174.659 1244.508 1318.51 1396.913 1479.978 1567.982 1661.219 1760 1864.655 1975.533
7 2093.005 2217.461 2349.318 2489.016 2637.02 2793.826 2959.955 3135.963 3322.438 3520 3729.31 3951.066
8 4186.009 4434.922 4698.636 4978.032 5274.041 5587.652 5919.911 6271.927 6644.875 7040 7458.62 7902.133

같은 열에서 한 칸 내려갈 때마다 값이 두 배가 되는 걸 보면 표가 맞는지 쉽게 확인할 수 있습니다. A 열이 55, 110, 220, 440, 880으로 딱 떨어지는 것도 그 때문이고요. 4옥타브 C(261.6Hz)가 흔히 말하는 "가운데 도"입니다.

코드에 넣을 때

tone()이나 Beep는 주파수를 정수로 받는 경우가 많아서, 표의 값을 반올림해 넣으면 됩니다. 소수점 아래 차이는 귀로 구분하기 어렵습니다. 멜로디를 만들 때는 음 이름별 상수를 배열로 정의해 두고, 박자 길이 배열을 따로 두어 둘을 같이 돌리는 구조가 관리하기 편합니다. 음과 음 사이에 아주 짧은 쉼을 넣지 않으면 같은 음이 연속될 때 하나로 이어져 들리니, 그 부분도 같이 신경 쓰면 훨씬 자연스럽게 들립니다.

아래는 4옥타브 도부터 5옥타브 도까지 한 번 올라가는 아두이노 예제입니다. 8번 핀에 수동 부저(피에조)를 연결했다고 보고, 음 길이의 30% 정도를 쉼으로 둬서 음끼리 붙지 않게 했습니다.

const int BUZZER = 8;
int melody[]    = {262, 294, 330, 349, 392, 440, 494, 523};
int durations[] = {300, 300, 300, 300, 300, 300, 300, 600};

void setup() {
  for (int i = 0; i < 8; i++) {
    tone(BUZZER, melody[i], durations[i]);
    delay(durations[i] * 1.3);
    noTone(BUZZER);
  }
}

void loop() {}

부저를 살 때 "능동(액티브)" 부저인지 "수동(패시브)" 부저인지 확인하세요. 능동 부저는 전원만 주면 정해진 한 가지 소리만 내기 때문에 tone()으로 주파수를 바꿔도 음정이 거의 안 바뀝니다. 멜로디를 내려면 수동 부저가 필요합니다.

낼 수 있는 주파수에도 하한이 있습니다. 아두이노 우노 같은 AVR 보드의 tone()은 31Hz가 최저이고, .NET의 Console.Beep은 37Hz부터 받습니다. 그래서 표의 1옥타브 C·C#(32.7Hz, 34.6Hz)은 Console.Beep으로는 넣을 수 없고, 넣으면 범위 밖이라 예외가 납니다. 또 우노에서 tone()을 쓰는 동안은 3번·11번 핀의 PWM 출력이 영향을 받으니, LED 밝기 조절 같은 걸 같이 돌린다면 핀 배치를 먼저 확인해 두는 게 좋습니다.