DEVELOPER MONOGRAPH · INTERNAL
ANALOG SOUL DSP ENGINE
구현 논고
구현 코드를 축으로, 각 처리부가 왜 그 형태여야 하는지를 신호 처리 이론·실내 음향학·음향심리학의 언어로 정당화한다. 설명의 편의를 위한 비유는 쓰지 않는다.
전제 지식. 이 문서는 다음을 이미 아는 독자를 상정한다. 단시간 푸리에 변환과 위상 보코더 계열의 합성 이론, Volterra/체비쇼프 전개에 의한 무기억 비선형계 해석, Linkwitz-Riley 분할망의 전역통과 성질, Sabine–Eyring 잔향 이론과 Polack의 확산장 모형, Moore–Glasberg의 ERB 척도와 동시 차폐 모형, Blauert의 정위 이론과 선행음 효과, Bregman의 청각 장면 분석에서의 융합·분리 단서.
인용 규약. 코드 인용은 실제 구현에서 발췌한 것이며 의사코드가 아니다.
식별자·상수·제어 구조는 원문 그대로이며, 중간 생략은 …로 표시한다.
다만 지면 폭에 맞춘 줄바꿈 조정과, 논지를 가리키기 위한 주석 추가·교체가 있다.
주석은 원문의 것과 다를 수 있으므로 코드 본체만 신뢰할 것.
§1해석 틀과 표기
엔진은 두 개의 계산 모형을 병치한다. 하나는 실시간 유향 비순환 그래프이고, 다른 하나는 오프라인 블록 렌더링이다. 전자는 인과적·저지연 제약 아래 파라미터 시변성을 허용하고, 후자는 전역 통계량에 접근할 수 있다. 대역 확장이 후자에만 존재하는 이유가 여기서 갈린다. 이는 구현상의 편의가 아니라 추정 가능성의 문제다. §3.1에서 형식화한다.
정규화 파라미터는 대문자 한 글자 또는 식별자로 표기하며 모두 [0,1]에
사상된 값이다. getParams()가 DOM 값을 이 구간으로 옮긴다.
dB 단위 파라미터만 예외적으로 원값을 유지하고 각 사용처에서
10dB/20으로 선형화한다.
§2STFT 분석·합성 커널
대역 확장의 전 과정이 이 커널 위에서 일어난다. 커널의 선택이 이후 모든 산물의 상한을 결정하므로 먼저 다룬다.
const N = 4096; const half = N >> 1; const hop = N >> 2; // R = N/4, 75% 중첩 const binHz = sr / N; const win = new Float32Array(N); for (let i = 0; i < N; i++) win[i] = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / N);
COLA와 WOLA
창이 해석과 합성 양쪽에 곱해지므로 실효 창은 w²이다. 완전 재구성을 위해서는 w²가 홉 R에 대해 상수합 조건(COLA)을 만족해야 한다. Hann 창에서는 이것이 정확히 성립한다.
w[n] = ½ − ½cos(2πn/N) 일 때 Σm w²[n − mR] = 3/2 (R = N/4, 내부 영역).
그럼에도 구현은 상수 3/2로 나누지 않고 창 제곱을 실측 누적해 나눈다.
ifft(re, im); for(let i=0; i<N; i++){ const idx = frameStart + i; if(idx < len){ out [idx] += re[i] * win[i]; norm[idx] += win[i] * win[i]; // 창 제곱 누적 } } } for(let i=0; i<len; i++){ if(norm[i] > 0.01) data[i] = out[i] / norm[i]; }
상수 나눗셈을 쓰지 않는 이유는 경계다. 신호의 처음과 끝
N − R 표본 구간에서는 겹치는 프레임 수가 부족해 COLA가 깨진다.
실측 정규화는 이 구간에서 자동으로 올바른 이득을 회복한다.
norm[i] > 0.01 조건은 0으로 나누는 것을 막는 가드이며,
임계 아래에서는 표본을 0으로 남긴다.
시간–주파수 분해능
Hann 창의 주엽 폭은 4Δf, 최고 부엽은 −31.5 dB, 부엽 감쇠는 −18 dB/oct다. 부엽 특성이 중요한 이유는 §3.1의 컷오프 추정이 스펙트럼 누설에 직접 영향을 받기 때문이다. 직사각창(−13 dB, −6 dB/oct)이었다면 절벽 아래의 −30 dB 판정이 누설에 묻혔을 것이다.
| fs | Δf | 프레임 길이 | 홉 간격 | 주엽 폭 |
|---|---|---|---|---|
| 48 kHz | 11.7 Hz | 85.3 ms | 21.3 ms | 46.9 Hz |
| 96 kHz | 23.4 Hz | 42.7 ms | 10.7 ms | 93.8 Hz |
| 192 kHz | 46.9 Hz | 21.3 ms | 5.3 ms | 187.5 Hz |
고정 N에 대해 표본화 주파수가 오르면 시간 분해능이 개선되고 주파수 분해능이 악화된다. 이 교환이 지각적으로 무해한 이유는 ERB 척도에 있다. Moore–Glasberg의 ERBN = 24.7(4.37F + 1), F는 kHz 단위일 때 16 kHz에서 대역폭은 약 1752 Hz다. 192 kHz에서의 46.9 Hz 빈은 이 임계대역을 37개 빈으로 표본화하는 셈이므로, 대역 확장이 관여하는 고역에서는 분해능이 과잉이다.
역으로 저역에서는 192 kHz의 46.9 Hz 빈이 1 kHz의 ERB(133 Hz)를 3개 빈으로만 표본화한다. 대역 확장은 저역을 건드리지 않으므로 문제되지 않으나, 만약 이 커널을 저역 처리에 재사용한다면 다중 해상도 (Multi-resolution STFT) 또는 상수-Q 변환으로의 이행이 필요하다.
§3대역 확장
MPEG-4 SBR이 QMF 필터뱅크 위에서 패칭·포락선 조정·잡음 바닥 부가· 역필터링(tonality control)의 네 단계를 거치는 데 비해, 이 구현은 STFT 영역의 패칭만 수행한다. 나머지 세 단계의 부재가 어떤 결과를 낳는지가 이 절의 주제다.
컷오프 추정량
부호화기의 대역 제한 주파수를 추정하는 문제다. 단일 프레임 주기도표는 분산이 크므로 Bartlett 평균으로 분산을 줄인다.
for (let fs=0; fs<d0.length-N; fs+=scanHop) { for (let i=0;i<N;i++){ const idx=fs+i; tRe[i]=idx<d0.length?d0[idx]*win[i]:0; tIm[i]=0; } fft(tRe,tIm); for (let k=1;k<=half;k++) avgMag[k] += Math.sqrt(tRe[k]*tRe[k]+tIm[k]*tIm[k]); cnt++; } for (let k=1;k<=half;k++) avgMag[k] /= Math.max(1,cnt); // dB 스펙트럼 (9탭 이동평균) for (let k=4;k<=half-4;k++){ let s=0; for(let j=-4;j<=4;j++) s += avgMag[k+j]; s /= 9; dBs[k] = 20*Math.log10(Math.max(s,1e-12)); }
1e-12 하한 클램프는 로그 발산 방지이자 암묵적 잡음 바닥 설정이다. 이 값은 −240 dB에 해당하므로 float32의 표현 한계보다 아래이며, 실질적으로는 정확히 0인 빈만 걸린다. 진폭 평균 후 로그를 취하는 순서에 유의해야 한다. 로그 평균이 아니라 산술 평균의 로그이므로 에너지가 큰 프레임에 가중되며, 이는 무음 구간의 오판을 억제하는 방향으로 작동한다.
// 1~4kHz 기준 레벨 const refLo=Math.round(1000/binHz), refHi=Math.round(4000/binHz); … refDb /= Math.max(1,refN); const searchCeil = Math.round(half * 0.5); // = f_s/4 const winB = 12; for(let k=searchCeil-winB; k>=winB; k-=Math.max(1,winB>>1)){ let avg=0; for(let j=0;j<winB;j++) avg += dBs[k+j]; avg /= winB; if(avg > refDb-30){ cutBin = k+winB; break; } }
기준 대역을 1–4 kHz로 잡은 것은 임의가 아니다. 이 구간은 등청감 곡선상 감도가 가장 높고, 지각 부호화기가 비트 예산을 우선 배분하는 대역이며, 어떤 장르에서도 에너지가 존재한다. 즉 부호화 후에도 반드시 살아남는 기준점이다.
이 추정량은 대역 제한이 시불변이라고 가정한다. 실제 부호화기는 프레임별 비트 배분에 따라 대역폭을 변조하며, 특히 VBR에서는 구간마다 컷오프가 다르다. 전역 단일 추정은 이를 평균해버리므로, 컷오프가 낮은 구간에서는 원신호 위에 합성이 겹치고 높은 구간에서는 빈 자리가 남는다. 프레임별 또는 구간별 추정으로의 확장이 원리적 개선 경로다.
위상 배수 사상
전사의 핵심은 빈 j의 성분을 j·k로 옮기는 것이다. 진폭은 고정 비율로 감쇠시키고 위상은 k배한다.
for(let j=jLo; j<=jHi; j++){ const dk = j*order; if(dk<1||dk>half) continue; const m = oMag[j]*lev; const p = Math.atan2(oIm[j],oRe[j]) * order; // 위상 배수 보정 let w = 1.0; if(dk<cLo){ const t=(dk-fLo)/Math.max(1,cLo-fLo); w=0.5-0.5*Math.cos(Math.PI*t); } else if(dk>cHi){ const t=(dk-cHi)/Math.max(1,fHi-cHi); w=0.5+0.5*Math.cos(Math.PI*t); } re[dk] += m*Math.cos(p)*w; im[dk] += m*Math.sin(p)*w; }
원신호가 x(t) = A·cos(ωt + θ)일 때, 프레임 m의 STFT 위상은 φm ≈ ω·mR + θ이다. 목표 성분의 주파수가 kω이므로 그 위상은 kω·mR + φ₀로 진행해야 한다. k·φm = kω·mR + kθ이므로 진행률이 정확히 일치하며, 상수항 kθ는 임의의 초기 위상으로 흡수된다.
atan2는 φ를 (−π, π]로 감싸 반환한다.
k ∈ ℤ이면
k(φ + 2πn) = kφ + 2πkn ≡ kφ (mod 2π)이므로
감싼 값을 그대로 정수배해도 결과가 일의적이다.
k가 비정수였다면 위상 풀기(unwrapping)가 필수였을 것이다.
harmonics = [2,4,8,16,32,64,128]이 모두 정수라는 사실이
이 단순화를 정당화한다.
보정 이전 구현은 φ를 그대로 복사했다. 그 경우 목표 성분의 위상 진행률이 ω·mR로 남아 실제 주파수 kω와 어긋나며, 프레임 간 중첩 가산에서 상쇄 간섭이 발생해 진폭 변조로 나타난다. 홉이 R = N/4이므로 변조 주기는 R/fs의 정수배, 96 kHz에서 10.7 ms 단위다. 이는 약 93 Hz의 변조 성분으로 지각될 수 있는 영역이다.
빈 양자화와 비조화도
dk = j·order는 정수 사상이므로 분수 빈 보간이 없다. 대신 합성 성분의 주파수가 원천 파티셜의 실제 주파수가 아니라 원천 빈 중심의 정수배가 된다.
| 컷오프 | 최저 원천 | 상대 오차 | 센트 |
|---|---|---|---|
| 16 kHz | 8 kHz | 0.146 % | 2.5 ¢ |
| 8 kHz | 4 kHz | 0.293 % | 5.1 ¢ |
| 4 kHz | 2 kHz | 0.586 % | 10.1 ¢ |
상대 오차가 차수에 무관하다는 것이 이 설계의 다행한 성질이다. 128차에서도 비조화도는 2.5 센트에 머문다. 고역에서의 주파수 변별 역치(약 1–3 %)를 훨씬 밑돌므로 개별 파티셜의 음정 이탈로는 지각되지 않는다. 다만 §3.5에서 보듯 융합의 관점에서는 다른 평가가 필요하다.
교차 규칙
for(let k=xfStart; k<=cutBin&&k<=half; k++){ const t = (k-xfStart)/Math.max(1,cutBin-xfStart); const wO = 0.5+0.5*Math.cos(Math.PI*t); // 원음 1 → 0 const wH = 1.0-wO; // 합성 0 → 1 re[k] += oRe[k]*wO; im[k] += oIm[k]*wO; … }
상관 없는 두 신호를 섞을 때는 전력 보존 (wO² + wH² = 1)이 맞다. 여기서는 합성 성분이 원음에서 파생되어 ρ ≈ 1의 상관을 가지므로 진폭이 선형 가산된다. 전력 보존을 쓰면 교차 중앙 t = ½에서 wO = wH = 1/√2, 합이 √2가 되어 약 +3 dB의 융기가 생긴다. 구현은 wO + wH = 1을 택했다.
청각 장면 분석 관점
합성 파티셜이 원음과 하나의 음원으로 융합할지, 별개의 스트림으로 분리될지가 최종 음질을 지배한다. Bregman의 틀에서 융합 단서는 조화성, 공통 개시, 공통 변조다.
| 단서 | 구현의 상태 | 평가 |
|---|---|---|
| 조화성 | 정수배 사상, 2.5 ¢ 이탈 | 강한 융합 지지 |
| 공통 개시 (onset) | 진폭이 oMag[j]에서 파생 | 원음과 동일 포락선 — 강한 지지 |
| 공통 진폭 변조 | 동일 프레임 진폭을 상속 | 지지 |
| 스펙트럼 포락선 연속성 | 차수당 −6 dB/oct 고정 | 조건부 — 아래 참조 |
앞의 세 단서는 강하게 충족된다. 문제는 네 번째다. 구현은 차수당 정확히 −6 dB의 고정 기울기를 부과한다.
const harmonics = [2, 4, 8, 16, 32, 64, 128]; // 배음 레벨: 2x=1/2, 4x=1/4, 8x=1/8, … 128x=1/128 const lvlScale = 0.5 + hfStrength; // 0.5 ~ 1.5
실제 악기의 스펙트럼 기울기는 음원과 다이내믹에 따라 −6에서 −18 dB/oct 사이를 오간다. 관악기는 세게 불수록 기울기가 완만해지고, 현악기는 활 속도에 따라 변한다. 고정 −6 dB/oct는 모든 소스에 동일한 스펙트럼 포락선을 부과하는 것이므로, 원음의 포락선이 그보다 가파른 경우 합성 대역이 상대적으로 밝아진다. 이것이 SYNTH를 높였을 때 나타나는 금속성 착색의 물리적 실체다. 원음과의 포락선 불연속이 스펙트럼 단서를 제공해 분리 스트림을 형성하기 시작하는 것이다.
MPEG-4 SBR은 비트스트림에 포락선 데이터를 실어 보내
패칭 결과를 원신호의 실제 고역 포락선으로 재조정한다.
블라인드 대역 확장에는 그 정보가 없으므로,
차선책은 원음의 저역 포락선 기울기를 추정해
levels[]를 소스 적응적으로 결정하는 것이다.
컷오프 아래 2–3 옥타브 구간에서 dB 기울기를 최소제곱 적합하고
그 값을 외삽하면 고정 상수를 대체할 수 있다.
현 구현은 이를 수행하지 않는다.
또한 SBR의 잡음 바닥 부가와 역필터링이 없다는 점도 같은 맥락이다. 마찰음이나 심벌처럼 본래 잡음성인 고역을 정현파 패칭으로 채우면 tonality가 과도해져 금속성으로 들린다. 역으로 조성적 고역을 잡음으로 채우면 거칠어진다. 이 구현은 원음의 tonality를 상속하므로 잡음성 소스에서는 잡음성 패치가 되지만, 2차 이상의 배음 중첩이 스펙트럼을 국소적으로 평활화하는 방향으로 작용한다. 평활 파라미터가 이 경향을 조절하는 사실상의 tonality 제어다.
const smAmt = Math.max(0, Math.min(1, smooth || 0)); const smR = Math.max(1, Math.round(1 + smAmt * 3)); // 반경 1~4 const smSig = 0.6 + smAmt * 1.6; // σ const smK = new Float32Array(smR*2+1); for (let d=-smR; d<=smR; d++) smK[d+smR] = Math.exp(-(d*d)/(2*smSig*smSig)); … const mag = magO*(1-smAmt) + magS*smAmt; // 진폭만 평활, 위상 보존
위상을 평활하지 않는 것이 중요하다. 진폭 스펙트럼의 평활은 켑스트럼 영역에서 고차 계수를 감쇠시켜 스펙트럼 미세구조를 흐리는 조작이며 시간 영역에서는 무해하다. 반면 위상 평활은 군지연을 교란해 임펄스를 시간축으로 번지게 한다(스미어링). 트랜지언트가 뭉개지는 결과를 낳으므로 분리해야 한다.
§4무기억 비선형계
TUBE·EXCITER·SOUNDGOODIZER는 모두 WaveShaper를 쓴다.
메모리 없는 정적 전달 특성이므로 Volterra 급수가
단일 커널로 축약되고, 정현파 입력에 대한 응답은 체비쇼프 전개로 닫힌다.
체비쇼프 전개
const d = 1.0 + drive * 1.2; // 1.0 ~ 2.2 const b = (bias - 50) / 100 * 0.15; // −0.075 ~ +0.075 for (let i = 0; i < n; i++) { const x = (i * 2 / (n - 1)) - 1; const xb = x + b; let y; if (xb >= 0) { y = Math.tanh(xb * d); } else { // 음의 반주기는 살짝 더 강한 포화 → 비대칭 → 2nd harmonic y = Math.tanh(xb * d * 1.15); } curve[i] = y / Math.tanh(d); // 평균 이득 보상 }
f(−x) = −f(x)인 기함수는 체비쇼프 전개에서 홀수 차 항만 갖는다. 짝수 배음을 생성하려면 대칭을 깨야 한다. 구현은 두 방식으로 깬다. 하나는 바이어스 b에 의한 동작점 이동이고, 다른 하나는 음의 반주기에만 곱해지는 계수 1.15다. 전자는 bias = 50에서 소멸하지만 후자는 항상 작동하므로, 바이어스를 중앙에 두어도 2차 배음이 존재한다.
y/tanh(d) 정규화는 반드시 필요하다. tanh의 압축성 때문에 d가 커질수록 출력 RMS가 감소하며, 정규화가 없으면 DRIVE를 올릴 때 음량이 함께 떨어진다. 이는 A/B 비교를 체계적으로 편향시킨다. §9에서 같은 문제를 다시 다룬다.
앨리어싱 예산
oversample='4x'는 내부에서 4배 업샘플 → 곡선 적용 → 다운샘플을 수행한다.
내부 나이퀴스트는 2fs이며,
이를 넘은 성분은 4fs − f로 접힌다.
임계 차수가 표본화 주파수에 의존하지 않는다는 것이 핵심이다. 입력 상한이 나이퀴스트에 비례해 함께 오르기 때문이다. 따라서 고레이트로 옮기는 것만으로는 개선되지 않으며, 입력 대역을 나이퀴스트의 분수로 제한해야 한다.
const aaLPF = ac.createBiquadFilter(); aaLPF.type='lowpass'; aaLPF.frequency.value = ac.sampleRate / 3; aaLPF.Q.value = 0.707; … chain.connect(inHPF); inHPF.connect(aaLPF); aaLPF.connect(ws);
EXCITER와 SG 고역은 입력이 이미 고역 통과된 상태이므로 fs/3가 대역 하단보다 낮아질 수 있다. 그 경우 통과 대역이 소멸하므로 하한을 둔다.
excAA.frequency.value = Math.max(params.excFreq * 1.4, ac.sampleRate / 3);
잔류량은 정량화되어 있다. 음악의 16 kHz 성분이 중역 대비 −35 dB, 그 10.5차 배음이 −45 dB이므로 접힌 산물은 약 −80 dBFS다. 16 bit 양자화 바닥(−96 dBFS)보다는 위지만 실제 청취 환경의 암소음보다 10 dB 이상 아래다.
상호변조와 지각
고조파 왜곡률(THD)은 비선형계의 지각적 불쾌도와 상관이 낮다. Geddes와 Lee가 보였듯, 동일 THD에서도 왜곡 곡선의 고차 미분 특성에 따라 선호도가 극적으로 갈린다. 저차 짝수 배음이 지배적인 부드러운 압축은 선호되지만, 급격한 곡률 변화가 만드는 고차 상호변조는 강하게 기피된다.
tanh는 무한히 미분 가능하고 고차 미분이 빠르게 감소하므로 이 기준에서 유리한 곡선이다. 반면 SG의 3차 다항 성분은 3차 미분이 상수이고 그 위로 0이므로 대역이 제한적이다.
const k = 1 + saturation * 2.5; const d = 1 + drive * 1.5; … const soft = xd / (1 + Math.abs(xd) * k); // 짝수 우세 const cubic = xd - (xd * xd * xd) / (3 * k); // 홀수 우세 const blend = saturation; curve[i] = soft * (1 - blend * 0.3) + cubic * (blend * 0.3);
soft = x/(1+|x|k)와 cubic = x − x³/(3k)는 둘 다 기함수다. 기함수의 선형 결합 역시 기함수이므로 블렌딩만으로는 짝수 배음이 원리적으로 생성되지 않는다. 8192점 DFT로 실측한 결과가 이를 확인한다.
| 조건 | 2차 | 3차 | 4차 | DC |
|---|---|---|---|---|
| 비대칭 없음 (수정 전) | −68.6 dB | −17.7 dB | −75.0 dB | −65.8 dB |
| bias 0.08·sat (수정 후) | −28.9 dB | −17.8 dB | −35.3 dB | −26.0 dB |
수정 전의 −68.6 dB는 테이블 양자화와 WaveShaper 선형 보간이 남긴
잔차이지 의도된 배음이 아니다. 짝수 배음을 만들려면 대칭을 깨야 하며,
효과적인 기제는 이득 비대칭이 아니라 동작점 이동이다.
바이어스 b를 준 f(x+b)를 b 둘레로 전개하면 f(b) + f′(b)x + ½f″(b)x² + …이고, x² 항이 2차 배음과 DC를 만든다. 계수는 f″(b)에 비례한다. tanh와 soft는 모두 기함수라 f″(0) = 0이므로 바이어스가 0이면 2차 배음도 0이다. 반면 음의 반주기에만 이득을 곱하는 방식은 압축 영역에서 곡선을 미세하게만 바꾸므로 기여가 훨씬 작다.
const b = 0.08 * saturation; // 동작점 이동 → 짝수 배음 for (let i = 0; i < n; i++) { const x = (i * 2 / n) - 1; const xd = (x + b) * d; const soft = xd / (1 + Math.abs(xd) * k); const cubic = xd - (xd * xd * xd) / (3 * k); … }
TUBE에서도 같은 구조가 확인된다. 비대칭 계수 1.15는
bias = 50에서 2차를 −39.9 dB에 두는데 3차가 −23.1 dB이므로
3차가 17 dB 우세하다. 코드 주석의
2nd harmonic 위주는 성립하지 않는다.
bias를 0 또는 100으로 밀면 2차가 −30.7 / −26.3 dB까지 올라오며,
이때 비로소 짝수 배음이 음색을 지배하기 시작한다.
즉 짝수 배음의 실질적 제어자는 BIAS이고 1.15 계수는 보조다.
전개식의 f(b) 항이 DC다. 실측에서 SG는 −26 dB, TUBE는 bias 중앙에서도 −30 dB의 직류를 낸다. 직류는 헤드룸을 일방적으로 잠식하고 리미터 동작점을 한쪽으로 밀어 비대칭 클리핑을 유발한다.
문제는 포화 이후 경로에 고역 통과가 하나도 없었다는 점이다. MASTER 밴드 1은 저역 통과이므로 직류가 출력까지 도달한다. 12 Hz 2차 고역 통과를 포화 직후에 삽입해 차단했다. 중·고역 밴드는 크로스오버의 고역 통과가 이 역할을 겸하므로 불필요하다.
const tubeDC = ac.createBiquadFilter(); tubeDC.type='highpass'; tubeDC.frequency.value=12; tubeDC.Q.value=0.707; … ws.connect(tubeDC); tubeDC.connect(outLPF);
§5대역 분할
const _lrq = 0.707; function _mkLR(type) { const a = ac.createBiquadFilter(); a.type = type; a.Q.value = _lrq; const b = ac.createBiquadFilter(); b.type = type; b.Q.value = _lrq; a.connect(b); return { a: a, b: b, out: b }; } nodes.sgLoLP = _mkLR('lowpass'); // 저역 nodes.sgMidHP = _mkLR('highpass'); // 중역 하단 nodes.sgMidLP = _mkLR('lowpass'); // 중역 상단 nodes.sgHiHP = _mkLR('highpass'); // 고역
버터워스 2차를 종속 연결한 LR4에서 HLP(s) + HHP(s)는 전역통과 함수다. 정규화 s = jω/ωc, D(s) = s² + √2 s + 1에 대해
진폭은 평탄하지만 위상이 회전하므로 군지연에 fc 부근의 융기가 생긴다. LR4의 군지연 첨두는 약 1/(2fc) 수준이다. MASTER의 최저 분할점 120 Hz에서 이는 약 4 ms이며, 저역의 군지연 변별 역치(수십 ms)보다 충분히 작다.
정리 5.1은 각 대역이 선형 경로일 때만 유효하다.
SG는 각 대역에 파형 정형기를 두므로, 대역 출력은
입력의 필터링된 복사본이 아니다. 따라서 합산 시 평탄성이 보장되지 않는다.
보장되는 것은 드라이 경로뿐이며, 웨트 버스는
AMOUNT×0.6으로 가중되어 더해진다.
즉 LR4 도입의 이득은 대역 간 중첩 제거이지
합산 평탄성이 아니다. 이전 LPF+BPF+HPF 구성 대비
합이 0.906–1.568에서 1.000으로 개선된 것은 선형 항에 대한 값이다.
§6동적 처리
MASTER의 4대역 압축기는 어택 시정수를 대역별로 달리한다. 통상 저역은 느리게라는 경험칙으로 설명되지만, 정량적 근거는 게인 변조의 측파대 위치에 있다.
게인 g(t)가 시정수 τ로 변하면 y = g·x는 x의 스펙트럼을 ±1/(2πτ) 폭으로 번지게 한다. 이 측파대가 대역 내 최저 주파수 flo보다 안쪽에 들어오면 파형 자체가 변형되어 왜곡으로 지각된다. 조건은
| 대역 | flo | τ 하한 | 구현값 | 여유 |
|---|---|---|---|---|
| 1 | ~20 Hz | 8.0 ms | 30 ms | 3.8× |
| 2 | 120 Hz | 1.33 ms | 15 ms | 11× |
| 3 | 500 Hz | 0.32 ms | 5 ms | 16× |
| 4 | 5 kHz | 0.03 ms | 2 ms | 63× |
모든 대역이 하한을 충분히 상회한다. 밴드 1이 가장 여유가 적은데, 이는 설계가 타이트하다는 뜻이 아니라 저역에서 이 제약이 실제로 구속적임을 뜻한다. 30 ms보다 짧게 하면 킥 드럼의 기본파 파형이 변형되기 시작한다.
리미터와 브릭월
const t = 0.85; // 이 아래는 완전 선형 for (let i = 0; i < n; i++) { const x = (i * 2 / (n - 1)) - 1; const a = Math.abs(x); let y; if (a <= t) { y = a; // 선형 구간 } else { y = t + (1 - t) * Math.tanh((a - t) / (1 - t)); } c[i] = (x < 0 ? -y : y); }
DynamicsCompressor는 룩어헤드가 없고 사이드체인 입력도 없으므로
진정한 룩어헤드 리미팅을 구성할 수 없다.
대신 천장 기준으로 정규화한 정적 곡선을 최종단에 둔다.
tanh가 1에 점근하므로 출력은 어떤 입력에도
천장을 넘지 않는다. 0.85 아래가 완전 선형이라는 점이
통상 신호에 착색을 남기지 않는 근거다.
클리퍼는 M/S 폭 조절 뒤에 놓아야 한다.
폭을 1보다 크게 하면 사이드 성분이 증폭되어 피크가 다시 상승하므로,
앞에 두면 천장 보장이 깨진다. 구현은
limiter → masterWidth → ceilClip 순서다.
§7공간 합성
const earlyDelays = [0.007,0.013,0.017,0.023,0.031, 0.041,0.053,0.067,0.079,0.097]; const earlyGains = [0.85,0.72,0.68,0.61,0.54, 0.48,0.42,0.36,0.30,0.24];
지연값이 모두 소수(밀리초 단위)인 것은 의도적이다. 정수배 관계가 있으면 반사음이 특정 주파수에서 동상 가산되어 빗살 공진을 만든다. 소수 집합은 최소공배수를 최대화해 공진을 분산시킨다.
확산장으로 지각되려면 에코 밀도가 약 10³/s를 넘어야 한다. 초기 반사 10개가 97 ms 안에 분포하므로 평균 밀도는
Polack의 확산장 모형에서 에코 밀도는 t²에 비례해 증가하며, 혼합 시간 tmix ≈ √V (ms, V는 m³) 이후에 확산장이 성립한다. STADIUM 프리셋의 RT60 3.2 s에 대응하는 체적은 Sabine 식으로 수천 m³ 규모이므로 혼합 시간은 수십 ms다. 즉 97 ms까지 성긴 반사만 두는 현 구성은 물리적으로는 혼합 시간 이후에도 밀도가 부족한 상태다.
const onset = Math.floor(p.earlyMs / 1000 * sr); for (let i = 0; i < onset && i < len; i++) { data[i] *= i / onset; // 직접음 자리와 겹치지 않게 }
후기 잔향은 exp(−6.91 t / RT60) 포락선의 대역 제한 잡음이다. 3단 1차 저역 통과로 백색 잡음을 걸어 고역이 과한 인공적 꼬리를 피한다. 선형 페이드인은 초기 구간의 밀도 부족을 부분적으로 가린다.
초기 반사 밀도를 물리적으로 맞추려면 반사 개수를 시간의 제곱에 비례해 늘려야 한다. 이미지 소스법으로 직육면체 방의 2–3차 반사를 생성하면 수백 개가 되어 Schroeder 기준에 근접한다. 현재의 10개 고정 배열은 계산 비용을 위한 타협이다.
§8M/S와 양이 상관도
const midL = ac.createGain(); midL.gain.value = 0.5; const midR = ac.createGain(); midR.gain.value = 0.5; const sidL = ac.createGain(); sidL.gain.value = 0.5; const sidR = ac.createGain(); sidR.gain.value = -0.5; … // 같은 머저 입력에 들어온 연결은 합산된다 mid.connect(mg, 0, 0); wPos.connect(mg, 0, 0); mid.connect(mg, 0, 1); wNeg.connect(mg, 0, 1);
w = 1에서 항등 변환임을 확인할 수 있다.
M + S = (L+R)/2 + (L−R)/2 = L,
M − S = R.
구현은 ChannelMergerNode의 동일 입력에 대한 다중 연결이
합산된다는 명세 규정에 의존한다.
지각적으로 폭 조절은 양이 상관도(IACC)를 조작하는 것이다. IACC 감소는 겉보기 음원 폭(ASW)과 청자 포위감(LEV)을 증가시킨다. 다만 w > 1은 사이드 성분을 증폭할 뿐 탈상관시키지는 않는다. 진짜 탈상관은 주파수 의존 위상 회전이나 짧은 무작위 지연으로 얻어지며, 그쪽이 ASW 확장에 더 효과적이다. 단순 사이드 증폭은 모노 호환성만 악화시킨다.
선행음 경로
const haasR = ac.createDelay(0.05); haasR.delayTime.value = 0.001 + params.atmosObject * 0.004; // 1~5 ms
1–5 ms는 선행음 효과(precedence effect)가 작동하는 구간이다. 1 ms 미만은 가산 정위(summing localization)로 ITD처럼 작동하고, 약 30–50 ms를 넘으면 에코 역치를 넘어 별개 사건으로 분리된다. 구현은 정위 이동을 의도한 중간 구간을 쓴다.
OBJECT MIX를 올릴수록 딥이 저역으로 내려오며 촘촘해진다. 모노 호환성이 요구되는 용도에서는 이 파라미터를 억제해야 한다.
§9레벨 정합의 오류
for(let c=0;c<ch;c++){ const d = buffer.getChannelData(c); let rms=0; for(let i=0;i<d.length;i++) rms += d[i]*d[i]; rms = Math.sqrt(rms/d.length); if(rms > origRMS[c]*1.01){ const g = origRMS[c]/rms; … } }
단방향 보정이다. 커진 경우만 되돌리고 작아진 경우는 두므로 A/B 비교에서 처리본이 체계적으로 커지는 편향을 막는다. 의도는 옳지만 측도가 부적절하다.
ITU-R BS.1770의 K-가중은 1 kHz 대비 4–16 kHz를 약 +4 dB 강조한다. 대역 확장은 정확히 그 구간에 에너지를 더한다. 무가중 RMS로 정합하면 가중 라우드니스는 여전히 증가한 채 남는다. 즉 정합 후에도 처리본이 더 크게 들린다.
이 편향은 기능의 효과를 과대평가하게 만든다. SYNTH를 올렸을 때의 개선 인상 중 일부는 라우드니스 증가다. 올바른 절차는 BS.1770-4 게이트 라우드니스로 −0.1 LU 이내 정합 후 순서 무작위화한 ABX를 수행하는 것이다. 현 구현의 RMS 정합은 이 요건을 충족하지 않는다.
개선은 어렵지 않다. K-가중 필터는 고역 셸빙 1단과 고역 통과 1단의 종속이며 계수가 표준에 명시되어 있다. 400 ms 게이트 블록과 −10 LU 상대 게이트를 구현하면 정확한 통합 라우드니스를 얻을 수 있다.
§10그래프 수명주기
실시간 경로는 노드를 재사용하고 연결만 재구성한다. 복합 블록을 도입하면서 해제 규칙이 미묘해졌다.
// 복합 블록은 내부 배선이 이미 되어 있다. // 내부까지 끊으면 블록이 망가지므로 바깥으로 나가는 출력만 끊는다. Object.values(nodes).forEach(n => { if (!n) return; if (typeof n.disconnect === 'function') { try { n.disconnect(); } catch(e) {} } else if (typeof n === 'object' && n.out && typeof n.out.disconnect === 'function') { try { n.out.disconnect(); } catch(e) {} } });
disconnect()는 출력 연결만 제거한다.
따라서 블록의 out만 끊으면 블록 내부 배선과
상류에서 들어오는 연결은 보존된다. 상류 연결은
상류 노드의 disconnect()가 처리한다.
이 비대칭이 규칙을 성립시킨다.
LFO는 별도 처리가 필요하다. AudioParam으로 들어가는 연결은
대상 노드의 disconnect()로 제거되지 않으며,
OscillatorNode는 한 번 start()하면 재사용할 수 없다.
체인이 재생마다 재구성되므로 명시적 정지가 필요하다.
if (liveDSP.vinylWobble) { try { liveDSP.vinylWobble.wow.stop(); } catch(e){} try { liveDSP.vinylWobble.flt.stop(); } catch(e){} }
와우·플러터의 물리
// 지연 변조 진폭 A에서 피치 편차는 A·2πf. // 와우 0.556Hz에서 A=0.9ms면 약 ±0.31% 편차가 된다. wb.wowG.gain.value = amount * 0.0009; wb.fltG.gain.value = amount * 0.00006;
0.556 Hz는 33⅓ rpm의 회전 주파수다. 가변 지연선의 순간 주파수 편이는 Δf/f = −d(τ(t))/dt이며, τ(t) = A·sin(2πfmt)에서 |Δf/f|max = A·2πfm이다. 실측 LP 기기의 와우·플러터는 DIN 45507 가중으로 ±0.1–0.3 %이므로 최대 설정이 열화된 기기에 대응한다.
§11수치적 고려
OLA 누적의 오차
out과 norm은 Float32Array다.
각 표본에 최대 4개 프레임이 가산되므로 누적 항 수는 작고,
가수 24비트에서 상대 오차는 10⁻⁷ 규모로 무시할 만하다.
다만 norm은 항상 양수 항의 합이므로 상쇄 소거가 없어
더욱 안정적이다.
저주파 바이쿼드의 계수 민감도
192 kHz 컨텍스트에서 MASTER 밴드 1의 120 Hz LR4는 정규화 주파수 fc/fs = 6.25×10⁻⁴이다. 이 영역에서 직접형 II 전치 구조의 극점은 z = 1 근방에 밀집하며, 계수 양자화 민감도와 상태 변수의 동적 범위 문제가 나타난다. Web Audio 구현은 내부적으로 배정밀도를 쓰므로 실질적 위험은 낮으나, 이식 시에는 상태 변수 형식(state-variable) 또는 격자형(lattice) 구조로의 전환을 검토해야 한다.
비정규수
잔향 꼬리와 압축기 포락선 검출기는 지수적으로 0에 접근하므로 비정규수 영역에 진입한다. 플랫폼에 따라 수십 배의 성능 저하를 유발한다. Web Audio는 내부적으로 FTZ를 적용하는 것이 일반적이나 명세에 규정은 없다. 자체 구현으로 이식할 경우 미소 DC 오프셋 주입 또는 명시적 FTZ 설정이 필요하다.
분석기 표시의 양자화
nodes.analyser.minDecibels = SG_MIN_DB; // −100 nodes.analyser.maxDecibels = SG_MAX_DB; // −30
getByteFrequencyData는 이 70 dB 구간을 256 단계로 양자화하므로
해상도는 0.273 dB/step이다.
스펙트로그램의 컬러맵이 이 값을 직접 색인하므로
표시 가능한 최소 대비도 같은 값에 묶인다.
현재 컬러맵은 무지개 계열이다. Borland와 Taylor가 지적했듯 무지개 맵은 밝기가 단조롭지 않아 데이터에 없는 경계를 만들고 순서 지각을 왜곡한다. 청록–노랑 구간에서 인위적 등고선이 보이는 것이 그 증상이다. 정량 판독이 목적이라면 viridis 계열의 지각 균일 맵이 옳다. 현 선택은 가독성보다 관례적 친숙성을 택한 결과이며, 판독 정확도를 요구하는 용도에서는 교체를 권한다.
§12참고문헌
- Allen, J. B. (1977). Short-term spectral analysis, synthesis, and modification by discrete Fourier transform. IEEE Trans. ASSP, 25(3).
- Griffin, D. & Lim, J. (1984). Signal estimation from modified short-time Fourier transform. IEEE Trans. ASSP, 32(2).
- Laroche, J. & Dolson, M. (1999). Improved phase vocoder time-scale modification of audio. IEEE Trans. Speech and Audio Processing, 7(3).
- Dietz, M., Liljeryd, L., Kjörling, K., & Kunz, O. (2002). Spectral Band Replication, a novel approach in audio coding. AES 112th Convention.
- Moore, B. C. J. & Glasberg, B. R. (1990). Derivation of auditory filter shapes from notched-noise data. Hearing Research, 47.
- Zwicker, E. & Fastl, H. (1999). Psychoacoustics: Facts and Models. Springer.
- Bregman, A. S. (1990). Auditory Scene Analysis. MIT Press.
- Blauert, J. (1997). Spatial Hearing. MIT Press.
- Linkwitz, S. H. (1976). Active crossover networks for noncoincident drivers. JAES, 24(1).
- Schroeder, M. R. (1962). Natural sounding artificial reverberation. JAES, 10(3).
- Polack, J.-D. (1993). Playing billiards in the concert hall. Applied Acoustics, 38.
- Geddes, E. R. & Lee, L. W. (2003). Auditory perception of nonlinear distortion. AES 115th Convention.
- ITU-R BS.1770-4 (2015). Algorithms to measure audio programme loudness and true-peak audio level.
- Borland, D. & Taylor, R. M. (2007). Rainbow color map (still) considered harmful. IEEE CG&A, 27(2).
- Oohashi, T. et al. (2000). Inaudible high-frequency sounds affect brain activity. J. Neurophysiology, 83. — 후속 재현 실패 보고와 함께 비판적으로 읽을 것.