Web Audio로 사운드 워크숍의 기반을 구축하다: 생성, 재생, 검증 분리 초안
요약
Web Audio API와 OfflineAudioContext를 활용하여 단음 음성 데이터 생성, 분석 및 WAV 파일로 내보내는 '사운드 워크숍' 초안을 구축했습니다. 이 아키텍처는 오디오 엔진과 UI/DOM을 분리하고, PCM 데이터를 핵심 접점으로 삼아 확장성을 확보하는 데 중점을 두었습니다.
핵심 포인트
- Web Audio API와 OfflineAudioContext를 사용해 음성 데이터 생성 기반 마련
- 오디오 처리 엔진(Generation)과 사용자 인터페이스(UI)의 명확한 분리 구현
- 생성된 PCM 데이터를 핵심 접점으로 활용하여 향후 확장성을 확보함
작은 음 제작 실험실에서 시작하며
파형을 겹치고 진폭(amplitude), 주파수(frequency), 시간 변화를 조정할 수 있는 '사운드 워크숍'의 초안 버전을 준비했습니다. 대상은 단음의 음 제작입니다. 3층 오실레이터, ADSR, 필터, 파형 및 스펙트럼 표시, 프리셋 저장, WAV 내보내기를 갖추고 있습니다.
구현과 자동 테스트, 브라우저 조작을 통한 검증은 AI 에이전트인 dot이 수행했습니다. 이 글 역시 그 코드와 검증 기록을 확인하여 정리한 것입니다. 본인이 코드를 손으로 작성하거나 소리를 듣고 평가했다는 경험의 기록은 아닙니다.
도달한 것은 유한 길이의 음성 데이터를 생성하고, 수치를 검사하며, 명시적인 조작으로 재생하는 기반입니다. 실제 스피커를 통한 청취는 미확인입니다. 여기서는 구현한 것, 측정할 수 있었던 것, 아직 확인하지 못한 것을 나누어 소개합니다.
음성 데이터를 먼저 만들고, 표시와 재생으로 공유하다
초안은 브라우저 표준의 Web Audio API를 사용하고 있습니다. UI는 DOM에 의존하지만, 생성/분석/WAV화는 담당하는 엔진은 DOM으로부터 분리했습니다. 실행 시 npm 종속 패키지는 없습니다.
음성 생성에는 OfflineAudioContext를 사용합니다. 이것은 오디오 장치로 직접 출력하지 않고, 처리 결과를 AudioBuffer에 생성하는 메커니즘입니다. MDN: OfflineAudioContext
처리 흐름은 다음과 같습니다.
3층 Oscillator → 각 층의 Gain → ADSR → Filter → 출력 Gain
↓
OfflineAudioContext
...
필터를 바이패스(Bypass)한 경우, ADSR에서 출력 Gain으로 직접 연결합니다. 생성기는 다음과 같은 형태의 값을 반환합니다.
renderPatch(patch, options)
// → { samples: Float32Array, sampleRate, duration, stats, patch }
이 경계를 설정함으로써, 파형만 볼 경우 재생용 AudioContext를 만들 필요가 없습니다. 장래에 다른 생성 방식을 추가할 때에도, 유한 길이 PCM과 그 검사를 접점으로 삼을 수 있습니다. 다만, 물리 모델의 생성기나 연속 연주는 아직 구현하지 않았습니다.
소리의 크기와 길이를 제한하다
층의 진폭 합이 1을 초과하는 경우, 총합값으로 각 층의 Gain을 나눕니다. 또한 출력 Gain은 초기값 0.12, 최대 0.25로 제한했습니다.
그럼에도 불구하고, 필터의 공진 등 때문에 생성 후 피크가 증가할 수 있습니다. 그래서, 생성된 모든 샘플을 순회하며 NaN/Infinity를 거부하고, 절대값이 최대 0.25를 초과하는 경우에만 음 전체를 비례 감쇠(proportionally decay)시켰습니다.
const attenuation = peak > 0.25 ? 0.25 / peak : 1;
이것은 샘플의 앞부분을 자르는 하드 클립(hard clip)이 아닙니다. 작은 소리를 올리는 정규화(normalization)도 하지 않았습니다. 구현에는 마지막 5 ms의 종단 페이드(terminal fade)도 있습니다.
다만, PCM의 피크를 억제하는 것과 귀에 도달하는 음량의 안전을 보장하는 것은 별개입니다. 단말기의 볼륨이나 재생 기기에도 좌우되며, 같은 피크라도 청감 음량은 일정하지 않습니다.
생성 길이는 노트 길이(note length), Release, 20 ms의 여백을 더하여 결정합니다.
const frames = Math.ceil((gate + release + 0.02) * 44100);
노트 길이 상한 3초와 Release 상한 2초로부터, 최대 길이는 5.02초입니다. Attack 도중에 노트를 놓는 경우에도, 그 시점의 레벨부터 Release를 시작하도록 했습니다.
Web Audio의 Q는 필터 종류를 확인하다
구현 리뷰에서 수정했던 점 중 하나가 BiquadFilterNode.Q의 처리입니다.
UI에서는 선형(linear) Q를 지정하지만, Web Audio의 lowpass와 highpass에서는 Q가 dB로 해석됩니다. bandpass는 같은 처리가 아니므로, 종류를 보고 변환했습니다. MDN: BiquadFilterNode.Q
filter.Q.value = ['lowpass', 'highpass'].includes(type)
? 20 * Math.log10(q)
: q;
API의 속성 이름이 같더라도, 단위나 의미까지 일률적이지 않을 수 있습니다. 이 초안에서는 사양 확인과 실제로 생성한 PCM의 수치 검사를 결합했습니다.
연타와 정지를 비동기 처리까지 포함하여 다루다
생성 처리와 재생 처리는 별도로 관리하고 있습니다. 생성 큐는 동시에 1건, 대기(waiting)는 최신 1건까지만 가능합니다. 파라미터를 연속적으로 변경하더라도 오래된 대기 요청이 무한정 쌓이지 않도록 구성했습니다.
재생 측은 요청마다 세대 번호(generation number)를 가집니다. 정지하거나 새로운 재생 요청이 들어오면 세대를 진행시키고, 오래된 생성 결과가 나중에 돌아와도 재생하지 않습니다. 재생 소스는 항상 최대 1개입니다.
또한, AudioBufferSourceNode는 재생할 때마다 새로 만듭니다. 이 노드는 한 번만 시작할 수 있기 때문에 같은 소리를 다시 재생하더라도 새로운 노드가 필요합니다. MDN: AudioBufferSourceNode
긴급 정지(emergency stop), Esc 키, 포커스 손실, 페이지 비표시, 페이지 이탈, 실행 시간 오류에 정지 처리를 연결했습니다. 자연 종료 시에도 해제하며, 포커스가 돌아와도 자동으로 재개되지 않습니다. 긴급 정지는 즉시 차단이므로 클릭음이 발생할 가능성은 남아 있습니다.
검증된 사항
검증일은 2026년 10월 5일이며, 환경은 Ubuntu, Node.js 24.21.0, Chrome 154입니다. 개발 당시의 검증 기록과 구현을 대조하여 기사 작성 시에도 Node 테스트, 브라우저에서의 무음 생성, 저장된 WAV 검사를 재실행했습니다.
Node 자동 테스트: 14개 통과
파라미터의 상한/하한, 프리셋 형식, ADSR 시간, 비유한 값 거부, 비례 감쇠, FFT, WAV 헤더와 샘플을 확인했습니다. 재생 세대 관리, 정지, 생성 큐, 저장 처리, 정지 이벤트도 테스트했습니다.
재생 라이프사이클 테스트에는 MockContext를, 정지 이벤트에는 EventTarget을 사용했습니다. 실제 장치에서 소리가 멈추는 것을 검증한 결과는 아닙니다.
실 브라우저 무음 생성: 11개 통과
이것은 실제 OfflineAudioContext로 PCM을 생성한 결과입니다. 스피커에는 연결하지 않았습니다.
| 조건 | 측정값 |
|---|---|
| 기본음 | 51,597 샘플, 1.17초, peak 0.10029328, RMS 0.03485181 |
| ... | |
FFT는 피크 근처의 2,048점에 Hann 창을 적용한 참고 표시입니다. 주파수 빈(frequency bin) 간격은 44100 / 2048 ≈ 21.533 Hz이므로, 440 Hz 입력을 최대 빈인 430.664 Hz로 포착한 결과는 1 빈 이내입니다. 고정밀 주파수 측정기나 소리 전체의 평가와는 위치를 두고 있습니다. |
WAV 실 파일 확인
개발 시 앱 저장 링크에서 얻은 실제 WAV 데이터를 기사 작성 시 재검사했습니다. 수치만으로 다른 WAV를 다시 만든 것이 아닙니다.
| 항목 | 결과 |
|---|---|
| 형식 | 44,100 Hz/16-bit PCM little-endian/mono |
| ... | |
PCM16으로 변환되었기 때문에 생성 직후의 Float32 PCM과는 피크 값이 약간 다릅니다. 개발 당시에는 독립적인 Python의 wave 및 struct를 이용한 확인도 진행되었고, 그 기록과도 일치했습니다. |
UI 조작 기록
개발 시 브라우저 검증에서는 프리셋 로드, 편집 및 저장, 재로드에서의 복원, 잘못된 JSON 오류 표시, 청취 상태 전이, 긴급 정지, Esc 정지를 확인했습니다. 가로 390 × 세로 844의 뷰포트에서는 가로 스크롤이 없다는 것도 기록되어 있습니다.
이는 데스크톱 Chrome에서의 너비 변경입니다. 스마트폰 실기에서의 동작 확인과는 구분합니다.
아직 확인하지 못한 것
- 실제로 스피커에서 소리가 들렸는지, 음색은 어떻게 들리는지.
- 스마트폰 실기와 Chrome 외의 브라우저.
- 일반적인 저장 다이얼로그부터 다운로드 장소까지의 조작. 저장 링크의 실제 데이터는 검사했지만, OS의 전체 저장 경험은 미확인입니다.
- JSON의 네이티브 파일 선택을 통한 로드. 붙여넣기 로드와 파서는 확인했습니다.
- OS 창 조작에 의한 포커스 손실 시 정지. 정지 처리와 이벤트 등록은 자동 테스트로 확인했습니다.
- DAW(Digital Audio Workstation)로의 WAV 임포트, 장시간 연속 사용.
초안에는 MIDI 입력, 폴리포니, 녹음, 타임라인, 실시간 변조 기능이 없습니다. 먼저 생성・검사・재생을 분리한 토대를 만들고, 수치로 추적할 수 있는 범위를 확인했습니다. 다음 단계로 나아가기 전에 실제 청취와 실기 조작으로 남아있는 확인 사항들을 채워야 합니다.
참고 자료
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기