콘텐츠로 이동

오디오

This content is for v0.4. Switch to the latest version for up-to-date documentation.

엔진은 같은 문서로 그림을 렌더링하면서 프로젝트의 오디오도 믹싱합니다. 이 페이지에서는 클립에서 마스터 버스까지 신호를 따라가 보고, 목표 라우드니스를 맞추는 방법과 브라우저가 믹스를 공급하고 재생하는 방법을 설명합니다. 다 읽고 나면 JSON으로 대사와 음악의 균형을 잡을 수 있습니다.

  • audio 트랙의 audio 소스.
  • "has_audio": true인 video 소스. 비디오 트랙에 놓인 클립의 소리가 믹스에 들어갑니다. 이 플래그가 없으면 소리가 나지 않습니다.

클립의 time_map은 소리에도 적용됩니다. reverse는 소리를 거꾸로 재생하고, freeze는 무음이며, 속도가 1이 아닌 클립은 리샘플링되므로 속도에 따라 음높이도 바뀝니다. 컴포지션의 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
트랙 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입니다.

브라우저에서는 세 단계를 직접 실행합니다.

  1. engine.setLoudnessMetering(true)를 호출하고 renderAudio로 오디오를 한 번 렌더링합니다.
  2. JSON.parse(engine.loudnessReport())를 planLoudnessNormalization(wasm, report, targetLufs, ceilingDbtp)에 넘깁니다.
  3. 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으로 파형 요약을 계산할 수 있습니다.