Opus 오디오 코덱 차세대 표준 | WebRTC·저지연·FFmpeg 실전 가이드

이 글의 핵심

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

들어가며

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


코덱 개요

역사 및 개발 배경

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

주요 마일스톤:

  • 2007: CELT 프로젝트 시작 (Xiph.org)
  • 2009: SILK 개발 (Skype)
  • 2010: IETF에서 CELT + SILK 통합 결정
  • 2012: RFC 6716 표준화
  • 2016: RFC 7874에서 G.711과 함께 WebRTC 필수 구현(MTI) 오디오 코덱으로 지정
  • 현재: 모든 주요 브라우저의 WebRTC 구현, Discord 등 음성 채팅 서비스, WhatsApp 같은 메신저 통화에 널리 쓰임

기술적 특징

항목설명
압축 방식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 (Speech) 모드

특징:

  • LPC (Linear Predictive Coding) 기반
  • 음성 포만트 보존
  • 저비트레이트 최적화 처리 과정:
PCM 입력
  ↓
음성 분석 (피치, 포만트)
  ↓
LPC 계수 추출
  ↓
잔차 신호 양자화
  ↓
비트스트림

CELT (Music) 모드

특징:

  • MDCT 기반 주파수 변환
  • 저지연 설계 (짧은 프레임)
  • 음악 품질 우선 처리 과정:
PCM 입력
  ↓
MDCT 변환
  ↓
심리음향 모델
  ↓
양자화·비트 할당
  ↓
비트스트림

하이브리드 모드

자동 전환:

  • 입력 신호 분석

  • 음성 비율 높음 → SILK 비중 증가

  • 음악 비율 높음 → CELT 비중 증가 장점:

  • 혼합 콘텐츠 (음성+배경음악) 최적화

  • 비트레이트 효율 향상

정확히 말하면 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: 비트레이트 32 kbps
  • -ac 1: 모노
  • -ar 48000: 48kHz 샘플레이트
  • -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

프레임 길이 옵션:

  • 2.5: 2.5ms (최저 지연)
  • 5: 5ms
  • 10: 10ms (기본)
  • 20: 20ms
  • 40: 40ms
  • 60: 60ms (최고 효율)

주의할 점은 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처럼 패킷을 잃을 수 있는 실시간 전송에서만 가치가 있습니다. 파일 인코딩 예제에서 이 옵션을 쓰는 것은 설정을 미리 들어 보는 테스트 용도라고 보면 됩니다.

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 샘플수십~100ms 이상높음파일 재생

결론: Opus는 알고리즘 지연을 수 ms~수십 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 화상회의

요구사항:

  • 초저지연 (<50ms)
  • 음성 명료도
  • 패킷 손실 대응

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: 게임 보이스챗

요구사항:

  • 저지연 (<30ms)
  • 대역폭 절약
  • 다중 플레이어 동시 송수신

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, 
            2,  // OPUS_APPLICATION_VOIP
            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;
    }
}

이 예제는 인코더 생성과 인코딩만 보여 주는 최소 골격이라 실제로 쓰려면 몇 가지를 채워야 합니다. 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 피드 호환

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

파일 크기 비교:

  • MP3 64k: 1시간 = 28MB
  • Opus 48k: 1시간 = 21MB

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: 웹 오디오 플레이어

요구사항:

  • 브라우저 호환성
  • 적응형 품질
  • 폴백 지원

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)
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

트레이드오프:

  • 짧은 프레임: 낮은 지연, 높은 오버헤드
  • 긴 프레임: 높은 효율, 높은 지연

패킷 손실 대응

# FEC 활성화 (10% 손실 예상, 음성 모드)
ffmpeg -i input.wav \
  -c:a libopus \
  -b:a 32k \
  -application voip \
  -packet_loss 10 \
  -fec 1 \
  output.opus

트러블슈팅

문제 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/AAC 폴백 제공

# 다중 포맷 생성
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 Opus에서 음악이 뭉개짐 원인: 비트레이트 부족 해결: 음악은 최소 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: 지연 시간 높음

증상: 실시간 통화에서 지연 체감 원인: 프레임 길이가 너무 김 해결: 짧은 프레임 사용

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

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

증상: VBR 모드에서 파일 크기를 미리 알 수 없음 해결: CBR 모드 사용

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

마무리

Opus는 음성+음악 하이브리드와 프레임 길이 조절로 저지연 실시간에 강합니다.

핵심 요약

  1. 하이브리드 구조
    • SILK: 음성 최적화 (6-40 kbps)
    • CELT: 음악 최적화 (48-510 kbps)
    • 자동 모드 전환
  2. 저지연
    • 프레임 길이: 2.5~60ms
    • 총 지연: 5~66ms
    • WebRTC 표준
  3. 로열티 프리
    • 참조 구현 libopus는 BSD 라이선스
    • 상용 배포 자유
    • 관련 특허 보유사가 로열티 없는 라이선스를 제공

선택 가이드

상황비트레이트설정
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

FFmpeg 명령 치트시트

# 음성 (32 kbps)
ffmpeg -i voice.wav -c:a libopus -b:a 32k -ac 1 -application voip voice.opus
# 음악 (128 kbps)
ffmpeg -i music.wav -c:a libopus -b:a 128k -ac 2 -application audio music.opus
# 저지연 (10ms)
ffmpeg -i input.wav -c:a libopus -b:a 64k -frame_duration 10 lowdelay.opus
# WebM 컨테이너
ffmpeg -i input.mp4 -c:v copy -c:a libopus -b:a 128k output.webm
# 배치 변환
for f in *.wav; do
  ffmpeg -y -i "$f" -c:a libopus -b:a 128k "${f%.wav}.opus"
done

추천 사용 시나리오

  • 실시간 음성·화상·게임: Opus 우선
  • 음악 스트리밍 (신규): Opus 검토
  • 팟캐스트 (파일 크기 중요): Opus 모노 48k
  • 레거시 호환 필요: MP3/AAC 병행

다음 단계

참고 자료


자주 묻는 질문 (FAQ)

Q. 네트워크 패킷 손실이 있는 환경에서 Opus 음질을 지키려면 어떻게 설정하나요?

A. Opus는 인밴드 FEC(Forward Error Correction)를 지원하므로, FFmpeg libopus에서 -fec 1과 예상 손실률을 뜻하는 -packet_loss 값을 함께 지정하면(FEC는 SILK·Hybrid 모드에서만 동작하므로 -application voip처럼 음성 모드가 쓰이는 설정이어야 합니다) 손실된 프레임을 일부 복원할 수 있습니다. FEC는 여분 정보를 싣는 만큼 같은 비트레이트에서 원래 음성에 쓸 비트가 줄어들기 때문에, 손실률을 실제 환경보다 과하게 잡지 않는 것이 좋습니다. 손실 대응이 중요하다면 짧은 프레임 길이와 함께 조정해 보는 것도 방법입니다.


같이 보면 좋은 글