이상값 필터가 이상값에 속을 때
피치 추출이 음절 하나를 통째로 두 배 높이로 잡으면, 중앙값 기반 이상값 필터는 전부 정상이라고 판정합니다. 음절 사이를 비교하는 접기 단계를 추가해 고친 기록입니다.
발음 교정 서비스의 피드백 카드는 학습자의 녹음에서 피치(목소리의 높낮이)를 추출해 곡선으로 그립니다. 추출에는 YIN이라는 알고리즘을 쓰는데, 어느 날 학생 카드에서 첫 음절 "머"만 위로 튀고 나머지 음절이 전부 바닥에 뭉쳐 있었습니다.
원인은 옥타브 에러였습니다. YIN 계열 알고리즘은 성대 진동이 불안정한 구간에서 실제 음의 배음(두 배 주파수)을 기본 주파수로 잘못 집을 때가 있습니다. 발화의 첫 음절은 성대가 아직 안정되지 않아 특히 자주 걸립니다. 카드의 세로 범위는 전체 음절의 최고·최저값으로 잡히므로, 두 배로 잡힌 음절 하나가 들어오면 나머지 정상 음절이 전부 세로로 압축됩니다.
이상값 필터는 이미 있었습니다. 각 음절 안에서 점들의 중앙값을 구하고, 중앙값에서 ±40% 벗어난 점을 버리는 방식입니다. 점 하나가 튀는 경우는 이걸로 충분합니다.
문제는 음절의 점들이 통째로 두 배가 된 경우입니다. 그 음절의 중앙값 자체가 두 배값이 되므로, 모든 점이 중앙값 근처에 얌전히 모여 있는 것으로 보입니다. 필터 입장에서는 전부 정상입니다. 이상값 판정의 기준 자체가 이상값에 오염되면, 그 필터는 원리적으로 아무것도 잡을 수 없습니다.
그래서 음절 안이 아니라 음절 사이를 비교하는 단계를 추가했습니다.
| 단계 | 하는 일 |
|---|---|
| 1 | 음절별 대표 피치를 구한다 (유성 점들의 중앙값) |
| 2 | 대표값들의 중앙값을 전체 기준으로 삼는다 (평균은 에러에 끌려간다) |
| 3 | 기준 대비 1.6배 이상 또는 1/1.6 이하인 음절만 ÷2 / ×2를 시도한다 |
| 4 | 접은 값이 기준에 로그 거리상 더 가까울 때만 채택한다 |
4단계가 핵심 안전장치입니다. "기준보다 많이 높다"는 이유만으로 접으면 정상적인 억양 봉우리(1.5배까지 올라갑니다)를 깎아버립니다. 접었을 때 기준에 실제로 가까워지는 경우, 즉 옥타브 에러일 때만 접습니다.
export function chooseFoldFactor(rep: number, reference: number): number {
const ratio = rep / reference
const candidate = (() => {
if (ratio >= FOLD_HIGH_TRIGGER) return 0.5
if (ratio <= FOLD_LOW_TRIGGER) return 2
return 1
})()
if (candidate === 1) return 1
// 접은 결과가 기준에 더 가까워질 때만 채택 (억양 봉우리를 깎지 않기 위한 조건)
if (logDistance(rep * candidate, reference) < logDistance(rep, reference)) {
return candidate
}
return 1
}문턱값 1.6에도 이유가 있습니다. 두 배 에러는 이론상 기준의 2.0배로 나타나지만, 발화 억양이 겹치면 1.6~2.6배 사이에서 흔들립니다. 1.6 미만은 첫 음절 상승이나 강세 같은 정상 억양 범위와 겹치므로 건드리지 않습니다. 거리를 log2로 재는 것도 같은 맥락입니다. 두 배와 절반을 대칭으로 다루려면 선형 거리가 아니라 로그 거리가 필요합니다.
이 접기가 못 잡는 경우도 코드 주석에 그대로 적어뒀습니다.
음절의 절반만 배음으로 잡힌 경우는 대표값이 애매해져 접기가 걸리지 않습니다. 그건 기존 ±40% 점 단위 필터의 몫입니다. 그리고 녹음의 과반이 틀린 경우는 기준 자체가 오염되므로 교정할 수 없습니다. 이 화자의 목소리가 원래 어느 높이인지에 대한 사전 지식(성별, 연령) 없이는 원리적으로 판별이 불가능합니다.
그래서 필터를 더 쌓는 대신, 각 필터가 어떤 오염까지 견디는지를 주석에 적어뒀습니다. 다음에 카드가 또 이상해지면 저부터 이 주석을 볼 겁니다.