무엇을 확인했고 무엇은 측정하지 않았나
2026년 11월 4일 나래 로컬 프로젝트의 pitch_detector.dart와 audio_dsp_specs.dart를 확인했습니다. 두 파일을 수정 없이 별도 검증 폴더에 복사하고 순수 Dart로 합성 신호를 입력했습니다. 앱이나 원본 파일은 바꾸지 않았습니다.
입력은 샘플 레이트 44,100 Hz, 길이 2,048샘플의 정현파였습니다. 실제 마이크, 스피커, 악기 연주나 휴대전화의 처리 지연을 측정한 테스트가 아닙니다. 아래 숫자는 이 조건에서 코드가 반환한 주파수이며 튜너의 실사용 정확도 보장이 아닙니다.
| 합성 입력 | 반환값 | 해석 |
|---|---|---|
| 440 Hz 정현파 | 440.101 Hz | 주파수 후보 반환 |
| 261.63 Hz 정현파 | 262.007 Hz | 주파수 후보 반환 |
| 82.41 Hz 정현파 | 83.012 Hz | 주파수 후보 반환 |
| 무음 / 256샘플 입력 / 고정 시드 잡음 | 각각 -1 | 검출 결과 없음 |
잡음에는 재현을 위해 난수 시드 42를 사용했습니다. 한 종류의 잡음을 거부했다는 결과를 모든 환경 소음을 구분한다는 뜻으로 확대할 수는 없습니다.
독립 Dart 검증 예제 ZIP 받기에는 이 계산에 필요한 검출기·상수·검증 스크립트와 한·영 README를 담았습니다. 마이크, Flutter, 네트워크·광고 SDK, 앱 자산은 포함하지 않습니다. 계이름 표시와 관련 없는 상수는 제외했고 검출·센트 변환 계산은 동일하게 유지했습니다.
- Dart SDK를 준비하고 ZIP을 새 폴더에 풉니다.
verify.dart와 README가 있는 폴더에서 아래 명령을 실행합니다.- 성공하면 JSON 한 줄이 출력됩니다. 표와 비교할 때 정현파 반환값을 소수 셋째 자리로 반올림합니다. 검사 실패는
StateError와 실패 종료로 표시됩니다.
dart run verify.dart
정현파 생성식은 0.5 * sin(2 * pi * frequency * n / 44100)이며 2,048샘플을 만듭니다. 각 반환값의 입력 주파수 대비 절대 차이가 1.5 Hz를 넘으면 실패하도록 했습니다. 무음은 2,048개의 0, 짧은 버퍼는 256개의 0.5입니다. 잡음은 Dart Random(42)에서 (nextDouble() * 2 - 1) * 0.1을 2,048번 생성합니다. 이 세 입력은 각각 -1을 반환해야 합니다.
센트 변환은 +19.56과 -7.85에서 각각 0.1센트 이내인지 검사합니다. 이 허용치는 예제의 회귀 검사 기준입니다. 실제 악기의 정확도 사양이 아닙니다. ZIP을 새 폴더에 풀어 Dart SDK 3.12.2·Windows x64에서 재실행해 같은 결과를 확인했습니다. SDK가 달라지면 난수 구현도 달라질 수 있으므로 dart --version 결과를 함께 기록합니다.
들어온 신호에서 주기를 찾는 순서
코드는 먼저 샘플 길이를 확인합니다. 1,024샘플보다 짧으면 분석하지 않습니다. 그다음 RMS로 입력 크기를 보고 작은 신호를 거릅니다. 평균값을 빼 DC 성분을 줄인 후 파형을 시간차를 두고 겹쳐 자기상관 값을 계산합니다.
비슷하게 반복되는 간격에서 상관값의 봉우리를 찾고, 신호 에너지 대비 봉우리의 비율로 후보를 거릅니다. 마지막에는 봉우리 앞뒤 값을 이용한 포물선 보간으로 정수 샘플 사이의 위치를 추정합니다. 설정 파일의 주파수 탐색 범위와 문턱값은 이 코드의 선택이지 모든 악기에 통용되는 물리적 보장은 아닙니다.
악기 소리에는 여러 배음이 있습니다. 가장 큰 FFT 봉우리 하나를 음정으로 읽는 접근과 자기상관 후보 비교는 같은 방법이 아닙니다. 자기상관을 쓰더라도 짧은 발음, 강한 배음, 다른 악기가 섞인 입력에서는 잘못된 후보가 나올 수 있어 실제 소리로 추가 확인해야 합니다.
주파수와 음이름·센트는 다른 단계
검출기가 주파수를 반환하면 convertHzToNote()가 기준음과의 비율을 로그로 바꿔 가장 가까운 음이름을 고릅니다. 선택한 음의 목표 주파수와 입력의 차이는 센트로 표시합니다. 같은 440 Hz도 기준음이 442 Hz라면 약간 낮은 음으로 표시됩니다.
아래는 센트 차이만 계산하는 독립 예제입니다. Node.js나 브라우저 콘솔에서 실행하면 19.56, -7.85가 나옵니다. 전체 피치 검출기를 대신하지 않으므로 마이크 권한이나 배음 문제를 해결하는 코드는 아닙니다.
function centsFrom(frequency, target) {
if (!Number.isFinite(frequency) || frequency <= 0 ||
!Number.isFinite(target) || target <= 0) {
throw new RangeError('Frequencies must be finite and positive.');
}
return 1200 * Math.log2(frequency / target);
}
console.log(centsFrom(445, 440).toFixed(2));
console.log(centsFrom(440, 442).toFixed(2));
// 19.56
// -7.85
실제 검출기 변환에서도 445 Hz는 A4·약 +19.56센트, 442 Hz 기준의 440 Hz는 A4·약 -7.85센트로 반환됐습니다. 기준음을 바꾸었을 때의 차이를 실제 악기의 이상으로 오해하지 않도록 화면 설정을 함께 확인하는 이유입니다.
음정이 안 잡히면 입력과 판정을 나눠 보기
- 마이크 권한을 확인하고, 앱이 입력을 받고 있는지 먼저 봅니다. 권한 실패와 검출 결과 없음은 원인이 다릅니다.
- 한 음을 조금 길게 내고 주변의 다른 소리를 줄여 비교합니다. 여러 음을 동시에 내면 단일 음정 검출에 불리합니다.
- 표시가 한 옥타브 높게 나오는 경우와 아무 값도 안 나오는 경우를 구분해 기록합니다.
- 기준음 설정과 목표 악기를 확인합니다. 같은 주파수라도 기준음이 다르면 센트 표시가 달라집니다.
확인한 코드에서는 무음·짧은 버퍼·신뢰도가 낮은 후보에 같은 “결과 없음” 반환값을 사용합니다. 따라서 화면에서 음이름이 비었다는 사실만으로 마이크가 고장 났다고 판단할 수 없습니다. 실제 장치에서 상태를 재현해 입력 문제인지 후보 거부인지 나누어야 합니다.
다음 검증에서 확인할 것
합성 정현파는 계산 경로를 확인하기 좋지만 실제 악기의 공격음, 잔향, 배음, 마이크 자동 처리까지 담지 못합니다. 실제 소리를 평가하려면 기기·출력 경로·마이크 거리·기준음·버퍼 조건을 기록하고 알려진 기준 신호와 비교해야 합니다. 처리 시간도 주파수 오차와 별도로 측정합니다.
마이크 분석 코드의 검토는 앱 전체 개인정보 흐름 검증과도 범위가 다릅니다. 오디오·광고 SDK의 처리 안내는 나래 개인정보처리방침에서 구분해 확인하고, 사용 순서는 나래 제품 안내에서 볼 수 있습니다.