오디오
엔진은 같은 문서로 그림을 렌더링하면서 프로젝트의 오디오도 믹싱합니다. 이 페이지에서는 클립에서 마스터 버스까지 신호를 따라가 보고, 목표 라우드니스를 맞추는 방법과 브라우저가 믹스를 공급하고 재생하는 방법을 설명합니다. 다 읽고 나면 JSON으로 대사와 음악의 균형을 잡을 수 있습니다.
소리의 출처
섹션 제목: “소리의 출처”audio트랙의audio소스."has_audio": true인video소스. 비디오 트랙에 놓인 클립의 소리가 믹스에 들어갑니다. 이 플래그가 없으면 소리가 나지 않습니다.
클립의 time_map은 소리에도 적용됩니다. reverse는 소리를 거꾸로 재생하고, freeze는 무음이며, 속도가 1이 아닌 클립은 리샘플링되거나 audio.preserve_pitch가 true이면 타임 스트레칭됩니다. 컴포지션의 media 노드는 소리를 더하지 않습니다.
신호 경로
섹션 제목: “신호 경로”clip: gain, pan, fades, effects → track bus: gain, pan, effects, mute, solo, ducking → master: gain, effects, limiter| 단계 | 필드 | 설정 |
|---|---|---|
| 클립 | audio |
gain, pan, fade_in, fade_out, fade_shape, effects, muted, preserve_pitch |
| 트랙 | audio |
gain, pan, effects, muted, solo, ducking |
| 프로젝트 | audio |
gain, effects, limiter |
- 게인은 0부터 16까지의 선형 진폭이며 애니메이션할 수 있습니다.
1은 원래 레벨,0.5는 절반입니다. - 팬은
-1(왼쪽)부터1(오른쪽)까지의 스테레오 위치이며 애니메이션할 수 있습니다. - 페이드는 틱 단위의 길이입니다.
fade_in은 클립 시작부터,fade_out은 클립 끝에서 거꾸로 잽니다.fade_shape는linear(선형 페이드 두 개가 겹치면 6dB 꺼짐) 또는equal_power(사인 4분의 1 곡선. 두 개가 겹치면 일정한 파워로 크로스페이드)입니다. - 솔로를 어느 트랙에서든 켜면 믹스가 솔로인 트랙으로만 제한됩니다.
클립이나 트랙의 게인과 팬 키프레임은 다른 값과 똑같이 씁니다. 애니메이션을 참고하세요.
이펙트
섹션 제목: “이펙트”체인 하나에는 이펙트를 최대 16개까지 넣을 수 있고, 순서대로 적용됩니다.
eq: 바이쿼드 밴드 1~16개. 각 밴드는peaking,low_pass,high_pass중 하나이며frequency_hz,q, (peaking의 경우)gain_db를 가집니다.compressor:threshold_db,ratio,attack_ms,release_ms,lookahead_ms를 갖는 룩어헤드 컴프레서.reverb:room_size,damping,mix를 갖는 Schroeder 리버브.
더킹은 다른 트랙이 재생되는 동안 한 트랙의 소리를 줄입니다. 줄어드는 쪽 트랙에 작성합니다. sidechain_track은 엔벨로프를 움직이는 믹스 출력을 가진 트랙이고, threshold(선형)는 감쇠가 시작되는 레벨, depth_db는 최대 감쇠량, attack_ms와 release_ms는 반응 속도입니다. 트랙이 자기 자신을 더킹할 수는 없고, 더킹 연결이 순환을 이루어서도 안 됩니다.
리미터
섹션 제목: “리미터”마스터 버스의 끝에는 피크 리미터가 있습니다. limiter를 생략하면 { "threshold": 1, "release_ms": 50 }(풀 스케일 상한)입니다. "limiter": null을 명시하면 리미터 없이 믹싱합니다.
믹스 예제
섹션 제목: “믹스 예제”인터뷰 영상의 대사, 그 아래로 더킹되는 배경 음악, 마스터의 컴프레서와 리미터입니다.
{ "schema_version": 1, "id": "audio-mix", "name": "Audio mix", "settings": { "width": 1280, "height": 720, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000 }, "sources": [ { "id": "interview", "kind": "video", "locator": "file:///media/interview.mp4", "has_audio": true }, { "id": "music", "kind": "audio", "locator": "file:///media/music.m4a" } ], "timeline": { "tracks": [ { "id": "dialogue", "name": "Dialogue", "kind": "video", "clips": [ { "id": "interview-clip", "source_id": "interview", "timeline_range": { "start": 0, "end": 2400000 }, "source_range": { "start": 600000, "end": 3000000 }, "audio": { "gain": { "mode": "constant", "value": 1.2 }, "fade_in": 30000, "effects": [ { "type": "eq", "bands": [{ "kind": "high_pass", "frequency_hz": 80, "q": 0.7 }] } ] } } ] }, { "id": "music-bed", "name": "Music", "kind": "audio", "clips": [ { "id": "music-clip", "source_id": "music", "timeline_range": { "start": 0, "end": 2400000 }, "source_range": { "start": 0, "end": 2400000 }, "audio": { "fade_in": 120000, "fade_out": 240000, "fade_shape": "equal_power", "pan": { "mode": "constant", "value": -0.2 } } } ], "audio": { "gain": { "mode": "constant", "value": 0.5 }, "ducking": { "sidechain_track": "dialogue", "threshold": 0.05, "depth_db": -12, "attack_ms": 20, "release_ms": 300 } } } ] }, "audio": { "gain": { "mode": "constant", "value": 1 }, "effects": [ { "type": "compressor", "threshold_db": -18, "ratio": 3, "attack_ms": 10, "release_ms": 150 } ], "limiter": { "threshold": 0.9, "release_ms": 50 } }}- 인터뷰 클립은 파일의 처음 5초를 건너뛰고, 게인 1.2를 주고, 0.25초 동안 페이드인하며, 80Hz 하이패스 필터를 거칩니다.
- 음악은 equal-power 곡선으로 1초 동안 페이드인, 2초 동안 페이드아웃하고, 약간 왼쪽에 놓입니다. 음악 트랙은 절반 게인으로 재생되며, 대사 트랙이 0.05를 넘으면 최대 12dB까지 줄어듭니다.
- 마스터는 가볍게 압축한 뒤 0.9에서 리미팅합니다.
라우드니스
섹션 제목: “라우드니스”alpha2/loudness는 ITU-R BS.1770-4와 EBU R128이 정의하는 방식으로 라우드니스를 측정합니다. 통합 라우드니스(LUFS), 라우드니스 레인지, 트루 피크, 샘플 피크, 모멘터리·숏텀 최댓값을 구하고, 믹스를 목표에 맞추는 고정 게인도 계획합니다.
네이티브 export는 목표 라우드니스를 받아 두 단계를 모두 처리합니다. @alpha2-sh/node에서는 loudness: { targetLufs, truePeakCeilingDbtp }, Python에서는 loudness={"target_lufs": -14, ...}, 명령줄에서는 --loudness-target <LUFS>와 --true-peak-ceiling <dBTP>입니다. 목표는 -600 LUFS, 상한은 -200 dBTP입니다.
브라우저에서는 세 단계를 직접 실행합니다.
engine.setLoudnessMetering(true)를 호출하고renderAudio로 오디오를 한 번 렌더링합니다.JSON.parse(engine.loudnessReport())를planLoudnessNormalization(wasm, report, targetLufs, ceilingDbtp)에 넘깁니다.engine.setNormalizationGainDb(plan.gainDb)를 적용해 export를 렌더링합니다.
계획은 믹스를 마스터 리미터의 threshold 아래로 유지하므로, 리미터가 계획한 라우드니스를 바꾸지 않습니다.
브라우저의 오디오
섹션 제목: “브라우저의 오디오”브라우저에서는 앱이 오디오를 디코딩해 업로드하고, 엔진이 믹싱합니다.
// 한 소스의 디코딩된 샘플. 프로젝트의 샘플 레이트로 리샘플링하고 플레인을 이어 붙인 것.engine.uploadAudioBlock("music", 0n, 48000, 2, samples, true /* 이 업로드가 소스 끝까지 닿음 */);
const block = engine.renderAudio(0n, 4800, true); // 채널당 최대 engine.audioMaxBlock 샘플const missing = JSON.parse(engine.audioMissingSources()); // 이 블록에 아직 필요한 소스uploadAudioBlock(sourceId, startSample, sampleRate, layout, samples, endsSource)는 프로젝트 샘플 레이트의 planar 오디오를 받습니다.startSample은 첫 샘플의 소스 샘플 위치이고,layout은 채널 수 또는 플레인별 스피커입니다.- 빠진 샘플은 무음이 아니라 아직 도착하지 않은 오디오로 취급되므로, 블록을 믹싱하려면 그 블록의 모든 샘플이 있어야 합니다. 소스 끝 이후로는 0을 업로드하거나
endsSource를 넘기세요. audioNeeds(startSample, frames)는 블록에 필요한 소스 구간을 알려 줍니다. 일부가 빠진 블록은 공백을 둔 채 믹싱하지 않고audioMissingSources로 보고합니다.renderAudio는 믹싱된 채널들을 이어 붙여 돌려줍니다.
프리뷰에서는 alpha2/audio-timeline이 공유 메모리의 링으로 믹싱된 블록을 오디오 스레드에 보내고, alpha2/audio-worklet이 AudioWorklet에서 재생합니다. 공유 메모리에는 cross-origin isolated 페이지가 필요하며, canShareAudioMemory()로 확인할 수 있습니다. export에서는 alpha2/audio가 오디오를 오프라인으로 싱크에 렌더링합니다. 타임라인 화면에는 alpha2/waveform으로 파형 요약을 계산할 수 있습니다.
