기타를 치는 사람이라면 카포를 압니다. 기타 넥의 특정 프렛에 끼워 여섯 줄을 한꺼번에 눌러 주는 도구로, 손모양(코드 폼)은 그대로 두고 소리만 반음 단위로 올립니다. 곡이 내 목소리에 비해 낮게 느껴질 때, 코드를 새로 외우지 않고 카포만 끼워서 키를 맞추는 데 씁니다.
Guitar Auto-Strum에도 카포가 있습니다. 그런데 이 앱의 카포는 −5부터 +7까지 움직입니다. 현실의 카포에는 없는 “마이너스”가 있는 것입니다. 이 글은 그 결정을 정리합니다.
카포는 숫자 하나로 모델링된다
카포가 하는 일은 “모든 줄의 프렛 번호에 N을 더하는 것”입니다. 코드 폼은 그대로이고 소리만 N반음 올라갑니다. 코드에서는 이 계산이 딱 한 곳에만 있습니다.
fret + capo— 프렛 번호 대신 이것을 쓴다. (chords.js의frequencyForString)
화면과 저장 데이터가 카포를 알 필요 없이, 소리를 만드는 곳에서만 더하면 됩니다.
왜 마이너스를 넣었나
현실의 카포는 줄을 짧게 만들기 때문에 소리를 올리기만 합니다. 이 앱의 기타는 실제 기타가 아니라서 그 제약이 없습니다. 코드의 주석은 이렇게 설명합니다(번역).
이 기타는 진짜가 아니고, 카포가 거부하는 방향이야말로 낮은 목소리에 필요한 방향이다. 노래를 가수에게 맞추는 일은 올리는 만큼 내리는 경우도 많다.
마이너스 값은 카포가 아니라 악기 전체를 낮게 튜닝한 것입니다. 반음 내림 튜닝, 온음 내림 튜닝은 기타에서 흔한 일입니다. 그래서 카포와 튜닝을 별도의 두 조절 장치로 두지 않고, 하나의 축으로 합쳤습니다. 대신 화면의 표기는 부호를 그대로 드러냅니다.
// capoLabel: 부호를 언제나 적는다
// 'Capo +2', 'Capo -3', 'Capo —'
export function capoLabel(capo) {
const n = clampCapo(capo);
if (n === 0) return 'Capo —';
return `Capo ${n > 0 ? '+' : '-'}${Math.abs(n)}`;
}
두 가지 표기 결정이 눈에 띕니다.
- 올릴 때도
+를 적는다. 목록에Capo 2와Capo -3이 함께 있으면-3만 “의도된 방향”처럼 읽히기 때문입니다. - 카포 없음은
0이 아니라—. “카포가 없다”는 것은 “0프렛 카포”가 아니라 카포의 부재이기 때문입니다.
범위를 정한 기준: 올리는 쪽은 손, 내리는 쪽은 귀
web-app/js/music/capo.js 주석은 +7과 −5의 선정 이유를 각각 설명합니다.
올리는 쪽(+7): 손이 닿는 곳까지
코드 폼 중 높은 것은 이미 14프렛까지 올라간다. 14 + 7은 21프렛이고, 어쿠스틱의 넥이 몸통과 만나는 지점을 넘어 손이 닿지 않는 곳이다. (번역)
이는 앱의 범위를 정한 주석의 설명입니다. 실제 기타에서 손이 닿는 프렛은 악기 구조에 따라 다릅니다.
내리는 쪽(−5): 귀가 다른 악기로 느끼기 전까지
내리는 쪽은 계산이나 손의 문제가 아닙니다. 코드의 주석에 따르면 더 내려도 깨지는 것이 없습니다. 오히려 낮은 음일수록 줄을 흉내 내는 지연선이 길어져서 안정적이고, 튜닝 계산도 −12까지 정확히 풀립니다. 문제는 소리가 다른 악기가 된다는 것입니다.
| 카포 | 개방 6번줄(E)의 주파수 | 들리는 느낌 |
|---|---|---|
| 0 | 약 82 Hz | 보통의 기타 |
| −5 | 61.7 Hz | 바리톤 기타처럼 들린다 |
| −12 | 41.2 Hz | 정확히 베이스 기타의 가장 낮은 줄 |
주석은 −5를 바리톤 기타처럼 들리는 하한으로 설명합니다. 음색을 어떤 방법으로 비교했는지는 코드만으로 확인할 수 없습니다.
코드의 주석은 하나를 더 확인했다고 합니다. 저음에서는 작은 스피커가 기본음을 재생하지 못할 수 있는데, 배음이 음을 실어 준다는 것입니다. 각 음높이에서 에너지의 82~97%가 200Hz 위에 있어서, 기본음을 재생하지 못하는 스피커에서도 배음이 음높이를 전달한다고 적혀 있습니다.
카포는 키(Key)와 다르다
이 앱에는 이미 키 선택이 있습니다. 카포와 무엇이 다를까요? 키는 어떤 코드가 코드 판에 올라오는지와 코드 이름을 어떻게 표기하는지를 정합니다. 곡의 코드 진행은 절대 음(루트)으로 저장되기 때문에, 키를 바꿔도 C 마디는 여전히 C입니다.
그래서 카포가 생기기 전에는 곡 전체를 다른 사람의 목소리에 맞게 옮기려면 마디를 하나하나 다시 쓰는 수밖에 없었다. (번역)
카포는 곡을 옮기는 도구이고, 키는 곡을 읽는 방법입니다. 둘을 한 컨트롤로 합치지 않은 이유입니다.
두 이름을 함께 보여 준다: C (→D)
카포를 쓰면 이름이 두 가지가 됩니다. 손이 잡는 모양은 C인데, 실제로 나는 소리는 D입니다. 노래를 부르는 사람은 D가 필요하고, 기타를 치는 사람은 C를 봐야 합니다. 그래서 화면에는 둘을 함께 보여 줍니다.
export function soundingLabel(key, chord, capo) {
const shape = chord?.displayName ?? '—';
if (clampCapo(capo) === 0) return shape;
const sounding = soundingName(key, chord, capo);
if (!sounding || sounding === shape) return shape;
return `${shape} (→${sounding})`; // 예: C (→D)
}
카포가 없거나 이름이 같으면 모양만 보여 주어 화면을 어지럽히지 않습니다.
한 가지 작은 함정도 코드에 남아 있습니다. 마이너스 카포가 들어오면서 루트 계산이 (root + capo) % 12인데, 자바스크립트의 %는 음수에서 음수를 돌려줍니다. 그래서 ((x % 12) + 12) % 12처럼 두 번 나누어 항상 0~11로 만듭니다.
// Twice-modulo, because `%` keeps the sign in JS and the capo can be negative.
const root = (((chord.root + n) % 12) + 12) % 12;
지판에서는 어떻게 보이나
현실의 카포는 넥 위의 굵은 바(bar)로 보입니다. 이 앱의 지판은 카포의 위치에서 바라본 모습으로 그립니다. 바가 너트(0프렛)의 자리를 대신하고, 코드 폼은 움직이지 않습니다. 이것이 카포의 목적이기도 합니다. 마이너스에서는 아무것도 물리지 않으므로 바가 없고, 대신 개방현 글자(E, A, D…)가 바뀌며, 어느 쪽이든 배지가 얼마만큼 옮겼는지 알려 줍니다. (docs/architecture.md)
잘못된 값이 들어와도 곡은 지킨다
저장된 곡이나 세션에 이상한 카포 값이 들어 있으면 어떻게 할까요? 코드의 규칙은 한 줄입니다.
알아볼 수 없는 값은 “카포 없음”이다. 나쁜 값은 사용자에게서 곡이 아니라 카포를 잃게 해야 한다.
export function clampCapo(n) {
if (typeof n !== 'number' || !Number.isFinite(n)) return DEFAULT_CAPO;
return Math.min(MAX_CAPO, Math.max(MIN_CAPO, Math.round(n)));
}
capo-assert.mjs는 범위를 벗어난 값, 음수 값, 잘못된 입력의 정규화와 주파수 이동을 검사합니다. 검토 중 npm run test:capo가 통과했습니다.