Opus 오디오 코덱 | SILK·CELT 구조, WebRTC 설정, FFmpeg 인코딩

이 글의 핵심

화상회의나 보이스챗에서는 음질만큼 지연 시간이 중요한데, Opus는 로열티 없이 저지연과 높은 압축 효율을 함께 얻을 수 있는 IETF 표준입니다. 다만 일부 구형 기기와 브라우저에서 재생되지 않는 호환성 문제가 있어, 어떤 상황에서 AAC나 MP3를 폴백으로 남겨야 하는지까지 판단할 수 있게 구성했습니다.

들어가며

Opus는 IETF RFC 6716으로 표준화된 오디오 코덱으로, 음성(SILK 계열)과 음악(CELT 계열)을 하나의 코덱에서 다루도록 설계되었습니다. 6~510 kbps 같은 넓은 비트레이트 범위와 프레임 길이 조절 덕분에 실시간 화상·음성·게임 보이스처럼 지연 예산이 빡빡한 환경에서 강합니다. Opus가 실시간 통신에서 사실상 기본값이 된 이유는 음질보다 설계 목표 쪽에 있습니다. MP3와 AAC는 “파일을 저장했다가 재생하는” 용도를 전제로 만들어져, 앞뒤 프레임을 넉넉히 보고 압축 효율을 높이는 대신 수십 ms 이상의 지연을 받아들였습니다. 반면 Opus는 처음부터 “말하는 사람과 듣는 사람 사이의 왕복 지연”을 줄이는 것을 목표로, 프레임 길이를 2.5ms까지 줄일 수 있고 비트레이트·대역폭·모드를 패킷 단위로 바꿀 수 있게 만들어졌습니다. 이 글은 그 구조를 먼저 설명한 뒤, FFmpeg와 WebRTC에서 실제로 어떤 값을 골라야 하는지와 호환성 문제를 다룹니다.


코덱 개요

역사 및 개발 배경

Opus는 Xiph.org가 주도한 CELT와 Skype가 발전시킨 SILK를 결합한 하이브리드 코덱으로, IETF codec working group에서 표준화되었습니다.

주요 마일스톤은 다음과 같습니다.

연도내용
2007Xiph.org에서 CELT 프로젝트 시작
2009Skype가 SILK 공개
2010IETF codec WG에서 CELT와 SILK를 하나의 코덱으로 통합하기로 결정
2012RFC 6716 표준화
2016RFC 7874에서 G.711과 함께 WebRTC 필수 구현(MTI) 오디오 코덱으로 지정

현재는 주요 브라우저의 WebRTC 구현, Discord 같은 음성 채팅 서비스, 메신저 통화 등에 널리 쓰입니다. 참조 구현 libopus는 BSD 라이선스이고, 관련 특허 보유사들이 로열티 없는 라이선스를 제공하므로 상용 제품에도 비용 없이 넣을 수 있습니다.

기술적 특징

항목설명
압축 방식SILK (음성) + CELT (음악) 하이브리드
샘플레이트8/12/16/24/48 kHz (내부 자동 처리)
비트레이트6~510 kbps
지연5~66.5ms (프레임 길이 조절 가능)
채널모노, 스테레오 (최대 255 채널 이론상 가능)
패킷 손실 복구FEC (Forward Error Correction) 내장

주요 모드

모드비트레이트용도특징
SILK6-40 kbps음성 통화LPC 기반, 음성 최적화
Hybrid12-64 kbps음성+음악 혼합자동 전환
CELT48-510 kbps음악, 고품질MDCT 기반

표의 비트레이트 구간은 대략적인 경향이고 경계가 딱 나뉘지는 않습니다. libopus 인코더는 목표 비트레이트, application 설정, 입력 신호의 성격(음성인지 음악인지 판별한 결과)을 보고 패킷마다 모드와 오디오 대역폭(협대역 4kHz부터 전대역 20kHz까지)을 스스로 고릅니다. 그래서 같은 32kbps라도 voip로 두면 SILK나 Hybrid 쪽으로, audio로 두면 CELT 쪽으로 기울어집니다. 한 스트림 안에서도 모드가 바뀔 수 있고, 디코더는 패킷 첫 바이트(TOC)의 정보만 보고 알아서 따라가므로 수신 측에서 따로 설정할 것은 없습니다.


압축 원리

SILK는 음성, CELT는 음악과 일반 소리를 맡고 하이브리드 모드는 8kHz 아래를 SILK가 그 위를 CELT가 부호화하는 구조

그림: SILK는 음성 전용, CELT는 음악과 일반 소리용이다. 하이브리드 모드는 두 코덱을 섞는 것이 아니라 8kHz 아래는 SILK, 그 위는 CELT가 나눠 맡는다.

SILK (Speech) 모드

SILK는 선형 예측 부호화(LPC, Linear Predictive Coding)에 기반한 음성 전용 코덱입니다. 사람 목소리를 성대 진동(피치)과 성도의 공명(포만트)이라는 모델로 근사하고, 모델이 예측하지 못한 나머지(잔차)만 양자화하므로 낮은 비트레이트에서도 말소리를 알아듣기 쉽게 유지합니다. 대신 음악처럼 모델과 맞지 않는 신호에는 효율이 떨어집니다.

PCM 입력
  ↓
음성 분석 (피치, 포만트)
  ↓
LPC 계수 추출
  ↓
잔차 신호 양자화
  ↓
비트스트림

CELT (Music) 모드

CELT는 MDCT로 신호를 주파수 영역으로 바꾼 뒤 대역별 에너지를 보존하면서 양자화하는 변환 코덱입니다. 음성 모델을 가정하지 않으므로 음악과 일반 소리에 두루 맞고, 처음부터 짧은 프레임(최소 2.5ms)으로 동작하도록 설계되어 지연이 작습니다.

PCM 입력
  ↓
MDCT 변환
  ↓
심리음향 모델
  ↓
양자화·비트 할당
  ↓
비트스트림

하이브리드 모드

Hybrid 모드는 두 코덱의 비중을 섞는 방식이 아니라 주파수 대역을 나눠 맡는 방식입니다. 8kHz 아래 저대역은 음성에 강한 SILK가 부호화하고, 그 위 고대역은 CELT가 부호화해 두 결과를 합칩니다. 사람 목소리의 핵심 정보는 저대역에 몰려 있으므로 이 대역은 음성 모델로 효율적으로 압축하고, 치찰음이나 배경음 같은 고대역 성분은 범용 변환 코덱에 맡기는 구조입니다. 이 설계 덕분에 20~32kbps 정도의 낮은 비트레이트에서도 전대역(fullband) 음성을 보낼 수 있습니다.


실전 인코딩

FFmpeg 기본 예제

음성 인코딩 (VoIP)

# 모노, 32 kbps (음성 최적화)
ffmpeg -i voice.wav \
  -c:a libopus \
  -b:a 32k \
  -ac 1 \
  -ar 48000 \
  -application voip \
  voice.opus

-b:a 32k는 목표 비트레이트, -ac 1은 모노, -application voip는 음성 명료도를 우선하는 인코더 튜닝입니다. -ar 48000을 명시하는 이유는 libopus가 받아들이는 입력 샘플레이트가 8/12/16/24/48kHz뿐이기 때문입니다. 44.1kHz WAV를 넣으면 FFmpeg가 자동으로 48kHz로 리샘플링하는데, 이 동작을 명시해 두면 파이프라인을 읽는 사람이 의도를 알 수 있습니다. 또 Ogg Opus 파일의 헤더에는 “원본 샘플레이트” 필드가 따로 있어 디코더가 44.1kHz로 되돌려 출력할 수도 있지만, Opus 내부는 항상 48kHz 기준으로 동작합니다.

음악 인코딩

# 스테레오, 128 kbps (음악)
ffmpeg -i music.wav \
  -c:a libopus \
  -b:a 128k \
  -ac 2 \
  -ar 48000 \
  -application audio \
  music.opus

저지연 인코딩

# 저지연 모드 (5ms 프레임)
ffmpeg -i input.wav \
  -c:a libopus \
  -b:a 64k \
  -frame_duration 5 \
  -application lowdelay \
  lowdelay.opus

-frame_duration에 줄 수 있는 값은 2.5, 5, 10, 20(기본값), 40, 60(ms)입니다. 짧을수록 지연이 줄고, 길수록 패킷당 헤더 오버헤드가 줄어 같은 비트레이트에서 효율이 좋아집니다.

주의할 점은 10ms보다 짧은 프레임(2.5ms, 5ms)에서는 SILK를 쓸 수 없다는 것입니다. SILK는 10ms 단위로 동작하므로, 이 프레임 길이에서는 CELT만 쓰이고 저비트레이트 음성 효율이 떨어집니다. 위 예제에서 -application lowdelay를 함께 준 것도 이 때문으로, lowdelay는 SILK를 아예 끄고 CELT 전용으로 동작시켜 알고리즘 지연을 최소화합니다. 기본값은 20ms이며, WebRTC도 대부분 20ms 패킷(ptime=20)을 씁니다. 네트워크 패킷 수와 헤더 오버헤드를 생각하면 20ms가 지연과 효율 사이의 무난한 균형점입니다.

고급 옵션

VBR vs CBR

# VBR (기본, 권장)
ffmpeg -i input.wav -c:a libopus -b:a 128k -vbr on output.opus
# CBR (스트리밍 대역폭 예측)
ffmpeg -i input.wav -c:a libopus -b:a 128k -vbr off output.opus
# Constrained VBR (중간)
ffmpeg -i input.wav -c:a libopus -b:a 128k -vbr constrained output.opus

패킷 손실 대응 (FEC)

# FEC 활성화 (음성 신호 + voip 모드에서 효과적)
ffmpeg -i input.wav \
  -c:a libopus \
  -b:a 32k \
  -application voip \
  -packet_loss 10 \
  -fec 1 \
  output.opus

Opus의 인밴드 FEC는 SILK 계층의 기능이라, 인코더가 CELT 모드를 고르는 설정(높은 비트레이트의 audio, lowdelay)에서는 켜 두어도 효과가 없습니다. 또 FEC는 패킷 N에 패킷 N-1의 저품질 사본을 덧붙여, 수신 측이 N-1을 잃었을 때 N을 받아 복원하는 방식입니다. 따라서 파일로 저장해 재생하는 .opus에는 의미가 없고, RTP처럼 패킷을 잃을 수 있는 실시간 전송에서만 가치가 있습니다. 파일 인코딩 예제에서 이 옵션을 쓰는 것은 설정을 미리 들어 보는 테스트 용도라고 보면 됩니다. FEC는 여분 정보를 싣는 만큼 같은 비트레이트에서 본래 음성에 쓸 비트가 줄어들므로, -packet_loss(예상 손실률 %)를 실제 환경보다 과하게 잡지 않는 것이 좋습니다.

WebM 컨테이너

# 비디오 + Opus 오디오
ffmpeg -i input.mp4 \
  -c:v libvpx-vp9 \
  -c:a libopus \
  -b:a 128k \
  output.webm

배치 처리

Bash 스크립트

#!/bin/bash
# 모든 WAV 파일을 Opus로 변환
for f in *.wav; do
  echo "Processing: $f"
  ffmpeg -y -i "$f" \
    -c:a libopus \
    -b:a 128k \
    -application audio \
    "${f%.wav}.opus"
done
echo "완료!"

Python 스크립트

import subprocess
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
def convert_to_opus(input_file, output_file, bitrate='128k', application='audio'):
    """
    WAV → Opus 변환
    """
    cmd = [
        'ffmpeg', '-y',
        '-i', str(input_file),
        '-c:a', 'libopus',
        '-b:a', bitrate,
        '-application', application,
        str(output_file)
    ]
    
    subprocess.run(cmd, check=True)
    return output_file
def batch_convert(input_dir, output_dir, max_workers=4):
    """
    병렬 배치 변환
    """
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    
    wav_files = list(Path(input_dir).glob('*.wav'))
    
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = []
        
        for wav_file in wav_files:
            output_file = Path(output_dir) / f"{wav_file.stem}.opus"
            future = executor.submit(convert_to_opus, wav_file, output_file)
            futures.append((wav_file.name, future))
        
        for name, future in futures:
            try:
                result = future.result()
                print(f"✓ {name} → {result.name}")
            except Exception as e:
                print(f"✗ {name}: {e}")
# 사용
batch_convert('input_wavs', 'output_opus', max_workers=4)

성능 비교

비트레이트별 음질 비교

아래 표는 공개된 청취 테스트들(예: HydrogenAudio 커뮤니티의 다중 포맷 청취 테스트)과 코덱 설계에서 나오는 일반적인 경향을 정리한 것으로, 특정 측정 결과가 아닙니다. 실제 체감 품질은 원본 소스, 인코더 버전·설정, 청취 환경에 따라 달라지므로 서비스에 적용할 비트레이트는 자기 콘텐츠로 ABX 청취 테스트를 해 보고 정하는 것이 좋습니다.

음성 (Speech)

대상: 모노, 음성 전용

비트레이트MP3AAC-HEOpus평가
12 kbps표준 비트레이트 아님나쁨보통Opus만 실용적
16 kbps매우 나쁨나쁨좋음Opus 실용
24 kbps나쁨보통매우 좋음Opus 최적
32 kbps보통좋음우수Opus 유리
64 kbps좋음매우 좋음우수모두 충분

음악 (Music)

대상: 스테레오 음악

비트레이트MP3AAC-LCOpus평가
64 kbps나쁨보통좋음Opus 유리
96 kbps보통좋음매우 좋음Opus/AAC 우수
128 kbps좋음매우 좋음매우 좋음일반 청취 충분
192 kbps매우 좋음우수우수구분 어려움

지연 시간 비교

코덱프레임 크기알고리즘 지연총 지연용도
Opus (2.5ms, lowdelay)120 샘플5ms매우 낮음실시간 악기 합주·게임
Opus (10ms)480 샘플12.5~16.5ms낮음VoIP
Opus (20ms, 기본)960 샘플22.5~26.5ms낮음일반 통화
AAC-LC1024 샘플수십 ms중간스트리밍
MP31152 샘플수십 ms높음파일 재생

Opus의 알고리즘 지연은 “프레임 길이 + 미리 보기(look-ahead)“입니다. CELT 전용(lowdelay)은 미리 보기가 2.5ms라 2.5ms 프레임에서 5ms가 되고, SILK·Hybrid를 쓰는 기본 설정은 미리 보기가 6.5ms라 20ms 프레임에서 26.5ms가 됩니다. 사용자가 느끼는 입에서 귀까지의 지연은 여기에 오디오 장치 버퍼, 네트워크 전송, 수신 측 지터 버퍼가 더해진 값이라, 실제 통화에서는 코덱 지연보다 지터 버퍼와 네트워크가 더 큰 비중을 차지하는 경우가 많습니다. 코덱 프레임을 2.5ms로 줄여도 지터 버퍼가 60ms라면 체감 차이는 크지 않다는 뜻입니다.

인코딩 속도는 크게 걱정할 부분이 아닙니다. libopus는 현대 CPU에서 음성 한 채널을 실시간보다 훨씬 빠르게 인코딩하므로, 서버 한 대에서 많은 채널을 동시에 처리하는 경우가 아니면 병목이 되지 않습니다. 대량 인코딩 비용이 걱정된다면 -compression_level(0~10, 기본 10)을 낮춰 CPU 사용량과 품질을 맞바꿀 수 있습니다.


실무 활용 사례

사례 1: WebRTC 화상회의

화상회의는 대화가 자연스럽게 이어질 만큼 지연이 작아야 하고, 음성 명료도가 음악적 품질보다 중요하며, 무선 환경의 패킷 손실을 견뎌야 합니다.

JavaScript WebRTC 설정

class OpusWebRTCConfig {
  static getAudioConfig(mode = 'voip') {
    const configs = {
      voip: {
        codec: 'opus',
        bitrate: 32000,
        sampleRate: 48000,
        channels: 1,
        fec: true,
        dtx: true,
        maxptime: 60
      },
      music: {
        codec: 'opus',
        bitrate: 128000,
        sampleRate: 48000,
        channels: 2,
        fec: false,
        dtx: false,
        maxptime: 20
      }
    };
    
    return configs[mode];
  }
  
  static createSDP(config) {
    return `
m=audio 9 UDP/TLS/RTP/SAVPF 111
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=${config.fec ? 1 : 0};usedtx=${config.dtx ? 1 : 0}
a=maxptime:${config.maxptime}
a=ptime:20
    `.trim();
  }
}
// 사용
const voipConfig = OpusWebRTCConfig.getAudioConfig('voip');
const sdp = OpusWebRTCConfig.createSDP(voipConfig);
console.log(sdp);

이 코드는 SDP의 어느 줄이 어떤 설정에 대응하는지 보여 주기 위한 예시이고, 브라우저에 codec, bitrate 같은 필드를 가진 설정 객체를 넘기는 API가 있는 것은 아닙니다. 실제 브라우저 WebRTC에서는 대부분 SDP의 a=fmtp 줄을 협상 전에 고쳐 쓰거나(SDP munging), 비트레이트는 RTCRtpSender.setParameters()의 encodings[0].maxBitrate로 조정합니다. 몇 가지 헷갈리는 점이 있습니다. a=rtpmap:111 opus/48000/2의 /2는 모노로 보낼 때도 항상 2로 적는 것이 RFC 7587의 규칙이고, 실제 스테레오 수신 여부는 fmtp의 stereo=1로 정합니다. useinbandfec=1은 “나는 FEC가 담긴 패킷을 받을 수 있다”는 수신 측 선언이라, 상대방 인코더가 FEC를 켜게 만드는 것이지 내 인코더 설정이 아닙니다. usedtx=1(무음 구간 전송 중단)은 대역폭을 크게 아끼지만, 일부 서버 측 믹서나 녹음 파이프라인이 끊긴 패킷을 손실로 오인해 잡음을 넣는 경우가 있어 켜기 전에 전체 경로를 확인해야 합니다.

FFmpeg 테스트 인코딩

# 음성 최적화 (16 kbps)
ffmpeg -i voice.wav \
  -c:a libopus \
  -b:a 16k \
  -ac 1 \
  -application voip \
  -frame_duration 20 \
  voice_16k.opus
# 음성 최적화 (32 kbps)
ffmpeg -i voice.wav \
  -c:a libopus \
  -b:a 32k \
  -ac 1 \
  -application voip \
  voice_32k.opus

사례 2: 게임 보이스챗

게임 보이스챗은 게임 트래픽과 대역폭을 나눠 써야 하고, 여러 플레이어의 스트림을 동시에 주고받으므로 채널당 비트레이트를 낮게 잡아야 합니다. 아래는 Unity에서 네이티브 libopus를 직접 호출하는 골격입니다.

Unity C# 예제

using System;
using System.Runtime.InteropServices;
public class OpusVoiceChat
{
    [DllImport("opus")]
    private static extern IntPtr opus_encoder_create(
        int Fs, 
        int channels, 
        int application, 
        out int error
    );
    
    [DllImport("opus")]
    private static extern int opus_encode(
        IntPtr st, 
        short[] pcm, 
        int frame_size, 
        byte[] data, 
        int max_data_bytes
    );
    
    private IntPtr encoder;
    private const int SAMPLE_RATE = 48000;
    private const int CHANNELS = 1;
    private const int BITRATE = 24000;
    private const int FRAME_SIZE = 960;  // 20ms at 48kHz
    
    public void Initialize()
    {
        int error;
        encoder = opus_encoder_create(
            SAMPLE_RATE, 
            CHANNELS, 
            2048,  // OPUS_APPLICATION_VOIP (opus_defines.h)
            out error
        );
        
        if (error != 0)
        {
            throw new Exception($"Opus encoder error: {error}");
        }
    }
    
    public byte[] Encode(short[] pcm)
    {
        byte[] output = new byte[4000];
        int encoded_bytes = opus_encode(
            encoder, 
            pcm, 
            FRAME_SIZE, 
            output, 
            output.Length
        );
        
        if (encoded_bytes < 0)
        {
            throw new Exception($"Encode error: {encoded_bytes}");
        }
        
        byte[] result = new byte[encoded_bytes];
        Array.Copy(output, result, encoded_bytes);
        return result;
    }
}

이 예제는 인코더 생성과 인코딩만 보여 주는 최소 골격이라 실제로 쓰려면 몇 가지를 채워야 합니다. 먼저 application 인자는 작은 정수가 아니라 opus_defines.h에 정의된 상수(OPUS_APPLICATION_VOIP = 2048, OPUS_APPLICATION_AUDIO = 2049, OPUS_APPLICATION_RESTRICTED_LOWDELAY = 2051)여야 하며, 잘못된 값을 넘기면 OPUS_BAD_ARG 에러가 납니다. BITRATE 상수를 선언해 두고도 적용하지 않았는데, 비트레이트는 opus_encoder_ctl(enc, OPUS_SET_BITRATE(24000))으로 설정합니다. 이 함수는 C의 가변 인자 함수라 C#의 DllImport로 직접 부르면 플랫폼(특히 ARM64 iOS·Android)에 따라 인자가 잘못 전달될 수 있으므로, 고정 인자를 받는 작은 C 래퍼 함수를 만들어 노출하는 방식이 흔히 쓰입니다. 또 인코더를 해제하는 opus_encoder_destroy를 호출하지 않으면 씬을 오갈 때마다 네이티브 메모리가 새므로 IDisposable로 감싸야 합니다. frame_size는 채널당 샘플 수라서, 48kHz에서 20ms면 960이고 입력 pcm 배열은 정확히 그만큼의 샘플을 담고 있어야 합니다. Unity 마이크 입력은 보통 다른 크기의 버퍼로 들어오므로 링 버퍼에 모아 960개씩 잘라 넘기는 처리가 필요합니다. 출력 버퍼 4000바이트는 libopus 문서가 권장하는 안전한 크기입니다.

FFmpeg 테스트

# 게임 보이스 (24 kbps, 저지연)
ffmpeg -i game_voice.wav \
  -c:a libopus \
  -b:a 24k \
  -ac 1 \
  -application voip \
  -frame_duration 10 \
  game_voice.opus

사례 3: 팟캐스트 배포

팟캐스트는 대부분 말소리라 모노·낮은 비트레이트로도 충분하고, 파일이 작을수록 청취자의 다운로드 부담이 줄어듭니다. 다만 팟캐스트 앱과 RSS 생태계는 MP3·AAC가 기본이라, Opus만 제공하면 일부 앱이나 기기에서 재생되지 않을 수 있습니다.

Opus 설정 (최소 크기)

# 모노, 48 kbps (팟캐스트)
ffmpeg -i podcast.wav \
  -c:a libopus \
  -b:a 48k \
  -ac 1 \
  -ar 48000 \
  -application voip \
  -metadata title="에피소드 제목" \
  -metadata artist="팟캐스트 이름" \
  podcast.opus

파일 크기는 비트레이트 × 길이로 계산됩니다. 1시간 기준으로 MP3 64 kbps는 64,000 × 3,600 / 8 ≈ 28.8MB, Opus 48 kbps는 약 21.6MB입니다(컨테이너 오버헤드 제외, VBR이면 평균 비트레이트 기준).

Python 자동화

import subprocess
from pathlib import Path
def create_podcast_opus(input_file, metadata):
    """
    팟캐스트 Opus 생성
    """
    output_file = Path(metadata['filename'])
    
    cmd = [
        'ffmpeg', '-y',
        '-i', str(input_file),
        '-c:a', 'libopus',
        '-b:a', '48k',
        '-ac', '1',
        '-ar', '48000',
        '-application', 'voip',
        '-metadata', f"title={metadata['title']}",
        '-metadata', f"artist={metadata['artist']}",
        str(output_file)
    ]
    
    subprocess.run(cmd, check=True)
    
    size_mb = output_file.stat().st_size / (1024 * 1024)
    print(f"생성 완료: {output_file.name} ({size_mb:.2f} MB)")
# 사용
metadata = {
    'filename': 'episode_01.opus',
    'title': '첫 번째 에피소드',
    'artist': '내 팟캐스트'
}
create_podcast_opus('recording.wav', metadata)

사례 4: 웹 오디오 플레이어

웹에서는 브라우저마다 지원하는 컨테이너가 다르므로, Opus를 우선 제공하되 재생하지 못하는 브라우저를 위해 AAC나 MP3 폴백을 함께 둡니다.

HTML5 Audio

<audio controls>
  <source src="audio.opus" type="audio/ogg; codecs=opus">
  <source src="audio.webm" type="audio/webm; codecs=opus">
  <source src="audio.m4a" type="audio/mp4">
  <source src="audio.mp3" type="audio/mpeg">
  브라우저가 audio 태그를 지원하지 않습니다.
</audio>

JavaScript 적응형 로딩

class AdaptiveOpusPlayer {
  constructor() {
    this.audio = new Audio();
    this.formats = [
      { src: 'audio.opus', type: 'audio/ogg; codecs=opus' },
      { src: 'audio.webm', type: 'audio/webm; codecs=opus' },
      { src: 'audio.m4a', type: 'audio/mp4' },
      { src: 'audio.mp3', type: 'audio/mpeg' }
    ];
  }
  
  selectFormat() {
    for (const format of this.formats) {
      if (this.audio.canPlayType(format.type)) {
        return format.src;
      }
    }
    return this.formats[this.formats.length - 1].src;
  }
  
  play() {
    this.audio.src = this.selectFormat();
    this.audio.play();
  }
}
// 사용
const player = new AdaptiveOpusPlayer();
player.play();

canPlayType()은 "probably", "maybe", "" 중 하나를 돌려주는데, "maybe"도 참으로 평가되므로 이 코드는 “재생할 수도 있다”는 형식을 선택합니다. 대부분의 경우 문제없지만, 실제 재생 실패에 대비하려면 audio의 error 이벤트에서 다음 형식으로 넘어가는 처리를 더하는 것이 안전합니다. 또 play()는 프로미스를 돌려주고, 사용자 클릭 같은 제스처 없이 호출하면 브라우저의 자동 재생 정책 때문에 NotAllowedError로 거부될 수 있으므로 버튼 클릭 핸들러 안에서 부르고 반환된 프로미스의 실패를 처리해야 합니다.


최적화 팁

비트레이트 최적화

음성 비트레이트 가이드

용도비트레이트품질
저품질 통화12-16 kbps이해 가능
일반 통화24-32 kbps좋음
고품질 통화40-64 kbps매우 좋음

음악 비트레이트 가이드

용도비트레이트품질
저품질64-80 kbps보통
일반96-128 kbps좋음
고품질160-192 kbps매우 좋음
최고256+ kbps원본과 거의 동일

프레임 길이 최적화

# 저지연 우선 (5ms, CELT 전용이 됨)
ffmpeg -i input.wav -c:a libopus -b:a 64k -frame_duration 5 lowdelay.opus
# 효율 우선 (60ms)
ffmpeg -i input.wav -c:a libopus -b:a 64k -frame_duration 60 efficient.opus

짧은 프레임은 지연이 낮은 대신 패킷 수가 늘어 RTP/UDP/IP 헤더 오버헤드가 커지고, 긴 프레임은 효율이 좋은 대신 지연이 늘고 패킷 하나를 잃을 때 사라지는 소리도 길어집니다. 실시간 통화라면 20ms, 저장용 파일이라면 기본값을 그대로 두는 것이 무난합니다.

이 글의 FFmpeg 명령은 Ubuntu 24.04 컨테이너(FFmpeg 6.1.1, libopus)에서 사인파와 핑크 노이즈를 섞은 10초짜리 WAV(44.1kHz)로 실행해 확인했습니다. voip·audio 예제, -frame_duration 2.5·5·10·20·40·60, lowdelay 5ms, -vbr on/off/constrained, -fec 1 -packet_loss 10, VP9와 Opus를 묶는 WebM 인코딩이 모두 오류 없이 끝났고, 출력은 입력이 44.1kHz여도 48000Hz로 나왔습니다. 목록에 없는 값(-frame_duration 3)은 Invalid frame duration: 3.으로 거부됐습니다. 같은 64kbps에서 파일 크기는 5ms 프레임이 77,431바이트, 20ms가 75,636바이트, 60ms가 75,592바이트였습니다. 짧은 프레임일수록 헤더 때문에 조금 커진다는 방향은 맞지만, 합성 신호로 한 한 번의 측정이라 차이의 크기는 음원에 따라 달라질 수 있습니다. 귀로 듣는 음질과 SILK·CELT 전환은 확인하지 않았습니다.


트러블슈팅

문제 1: 브라우저 재생 안 됨

증상: 일부 브라우저(특히 Safari·iOS)에서 .opus 파일이 재생되지 않습니다.

<audio src="audio.opus" controls></audio>
<!-- 브라우저에 따라 재생 안 됨 -->

원인: FFmpeg로 만든 .opus 파일은 이미 Ogg 컨테이너이므로 Chrome·Firefox에서는 보통 재생됩니다. 문제는 대개 두 가지입니다. 첫째, Safari는 오랫동안 Ogg 컨테이너를 지원하지 않았고 Opus를 담을 수 있는 컨테이너 지원도 버전에 따라 다릅니다. 둘째, 웹 서버가 .opus 확장자에 audio/ogg 대신 application/octet-stream 같은 MIME 타입을 붙여 보내면 브라우저가 재생을 거부하기도 합니다. 서버 MIME 설정을 먼저 확인하고, 폭넓은 호환이 필요하면 WebM과 AAC(m4a) 폴백을 함께 제공합니다. 해결:

# Ogg 컨테이너
ffmpeg -i input.wav -c:a libopus -b:a 128k output.ogg
# WebM 컨테이너
ffmpeg -i input.wav -c:a libopus -b:a 128k output.webm
<audio controls>
  <source src="audio.ogg" type="audio/ogg; codecs=opus">
  <source src="audio.webm" type="audio/webm; codecs=opus">
</audio>

문제 2: 구형 기기 미지원

차량 오디오의 USB 재생이나 구형 스마트폰·MP3 플레이어는 Opus 디코더 자체가 없는 경우가 많습니다. 이런 기기를 대상으로 배포한다면 같은 원본에서 AAC나 MP3 파일을 함께 만들어 제공합니다.

# 다중 포맷 생성
ffmpeg -i input.wav -c:a libopus -b:a 96k output.opus
ffmpeg -i input.wav -c:a aac -b:a 128k output.m4a
ffmpeg -i input.wav -c:a libmp3lame -b:a 192k output.mp3

문제 3: 음악 품질 저하

64 kbps 스테레오 음악에서 고음이 뭉개지거나 스테레오 이미지가 좁아졌다면 대개 비트레이트가 부족한 것입니다. 이 구간에서 인코더는 대역폭을 줄이거나 스테레오 정보를 단순화해 비트를 아낍니다. 음악은 96 kbps 정도에서 시작해 청취 테스트로 올리는 것이 무난하고, 저장된 파일을 다시 인코딩한 경우라면 원본(무손실)에서 다시 인코딩합니다.

# 음악 최소 권장 (96 kbps)
ffmpeg -i music.wav \
  -c:a libopus \
  -b:a 96k \
  -application audio \
  music.opus
# 고품질 (128 kbps)
ffmpeg -i music.wav \
  -c:a libopus \
  -b:a 128k \
  -application audio \
  music_hq.opus

문제 4: 지연 시간 높음

실시간 통화의 지연이 크게 느껴질 때 프레임 길이가 40·60ms로 설정되어 있다면 줄여 볼 만합니다. 다만 앞의 지연 비교에서 본 것처럼 체감 지연은 지터 버퍼와 네트워크가 더 큰 경우가 많으므로, 코덱 설정을 바꾸기 전에 각 구간의 지연을 먼저 확인합니다.

# 저지연 (10ms 프레임)
ffmpeg -i input.wav \
  -c:a libopus \
  -b:a 64k \
  -frame_duration 10 \
  -application lowdelay \
  lowdelay.opus

문제 5: 파일 크기 예측 불가 (VBR)

VBR(기본값)은 신호의 복잡도에 따라 비트를 배분하므로 결과 파일 크기가 목표 비트레이트 계산값과 조금씩 다릅니다. 크기나 순간 대역폭을 정확히 맞춰야 한다면 CBR(-vbr off)을 쓰고, 같은 크기에서의 품질은 VBR보다 다소 손해를 봅니다.

# CBR (크기 예측 가능)
ffmpeg -i input.wav \
  -c:a libopus \
  -b:a 128k \
  -vbr off \
  output.opus
# 파일 크기 계산
# 크기 = (비트레이트 * 시간) / 8
# 128 kbps * 300s / 8 = 4.8 MB

용도별 설정 요약

상황비트레이트설정
VoIP/화상회의16-32 kbps모노, -application voip
게임 보이스챗24-48 kbps모노, -frame_duration 10
팟캐스트48-64 kbps모노, -application voip
음악 스트리밍96-128 kbps스테레오, -application audio
고품질 음악160-192 kbps스테레오, VBR

차량이나 구형 기기처럼 Opus 디코더가 없는 환경까지 지원해야 한다면 MP3나 AAC를 함께 제공합니다.

참고 자료