본문 바로가기
잡(job)기술

SDL2 오디오 재생 기초: want/have 협상 구조와 SDL_QueueAudio로 사인파 큐잉하기

by 무니이구나 2026. 9. 21.

💡 한 줄 요약

  • SDL_OpenAudioDevicewant/have 두 스펙을 나눠서 다루는 이유를 협상(negotiation) 구조로 정리하고, have 값을 기준으로 사인파 버퍼를 계산해 SDL_QueueAudio로 재생하는 흐름을 정리한다.

🚨 1. 문제 상황

SDL2로 오디오를 재생하려고 SDL_OpenAudioDevice를 호출하면 인자로 wanthave(예제 변수명 기준) 두 개의 SDL_AudioSpec을 넘기게 된다.

SDL_AudioSpec want{}, have{};
want.freq = 48000; want.format = AUDIO_F32SYS; want.channels = 2; want.samples = 1024;
SDL_AudioDeviceID dev = SDL_OpenAudioDevice(nullptr, 0, &want, &have, 0);

⚠️ 공통적인 Pain Point
"내가 설정한 want가 있는데 왜 have라는 게 따로 나오지?", "버퍼는 want 기준으로 만들어야 하나 have 기준으로 만들어야 하나?", "큐에 넣을 데이터 크기는 어떻게 계산하지?" — 처음 SDL 오디오 API를 접하면 이 세 가지 지점에서 막히기 쉽다.

 


🛠️ 2. 개발 환경

분류 기술 스택 / 도구 버전
OS Ubuntu 24.04 (WSL2)
언어/기반 C++ g++ 13.3.0
라이브러리 SDL2 2.30.0

 


🔍 3. 핵심 개념: want vs have

3.1 왜 want/have 두 개가 필요한가

SDL_OpenAudioDevice는 요청한 스펙(want)을 하드웨어가 그대로 지원한다고 보장하지 않는다. 실제 디바이스가 지원하는(혹은 SDL이 내부적으로 맞춰준) 최종 스펙이 have에 채워진다.

  • want: "이렇게 열어줬으면 좋겠다"는 요청값
  • have: 실제로 열린 디바이스의 확정값

마지막 인자 allowed_changes(예제에서는 0)가 이 협상의 범위를 결정한다. 0이면 SDL이 want를 임의로 바꾸지 않고, 하드웨어가 정확히 그 스펙을 못 맞추면 SDL이 내부적으로 변환(리샘플링 등)을 수행해서라도 want와 동일한 have를 만들어준다. SDL_AUDIO_ALLOW_*_CHANGE 플래그를 OR로 넘기면 해당 항목에 한해 havewant와 달라지는 것을 허용하게 된다.

3.2 SDL_AudioSpec 필드 정리

필드 의미 예제 값 비고
freq 샘플링 레이트 (초당 프레임 수, Hz) 48000 CD 음질은 44100, 방송/영상 쪽은 48000이 표준
format 샘플 하나의 데이터 표현 방식 AUDIO_F32SYS 32비트 float, 시스템 엔디안 기준
channels 채널 수 2 모노(1) vs 스테레오(2)
samples 콜백/버퍼 한 조각당 프레임 수 1024 레이턴시와 직결 — 작을수록 지연은 줄지만 언더런 위험 증가

 


✅ 4. 해결 방법: want/have로 오디오 큐잉하기

have가 결정된 뒤, 아래 코드로 약 0.5초 분량의 440Hz 사인파를 만들어 큐에 넣고 재생한다.

std::vector<float> buf(size_t(have.samples) * have.channels * 24); // ~0.5s
for (size_t i = 0; i < buf.size(); i += have.channels) {
    float s = 0.2f * std::sinf(2 * 3.14159265f * 440.f
                               * float(i / have.channels) / have.freq);
    for (int c = 0; c < have.channels; ++c) buf[i + c] = s;
}
SDL_QueueAudio(dev, buf.data(), buf.size() * sizeof(float));
SDL_PauseAudioDevice(dev, 0);

4.1 버퍼 크기: 왜 have를 기준으로 계산하는가

std::vector<float> buf(size_t(have.samples) * have.channels * 24);
  • have.samples: 실제로 협상된 "버퍼 한 조각(프레임 수)". want가 아니라 have를 써야 하는 이유가 여기서 명확해진다 — 하드웨어가 실제로 처리하는 단위가 기준이 되어야 한다.
  • have.channels: 프레임 하나당 필요한 float 개수. 스테레오면 프레임당 2개.
  • 전체 버퍼 길이 = 프레임 수 × 채널 수, 이것이 인터리브(interleaved) 오디오 데이터의 기본 레이아웃이다.

4.2 왜 × 24가 대략 0.5초가 되는가

freq는 "초당 프레임 수"이므로, 역으로 재생 시간은 다음과 같이 구할 수 있다.

재생 시간(초) = 전체 프레임 수 / have.freq
             = (have.samples × 24) / have.freq

 

have.samples = 1024, have.freq = 48000을 대입하면:

(1024 × 24) / 48000 = 24576 / 48000 ≈ 0.512초

 

24는 일반 공식이 아니라, samples=1024, freq=48000 조합에서 0.5초가 나오도록 역산해서 고른 상수다. 이 값이 달라지면 24도 같이 바뀌어야 정확한 0.5초가 된다.

4.3 샘플 생성 루프: channels와 freq의 역할

for (size_t i = 0; i < buf.size(); i += have.channels) {
    float s = 0.2f * std::sinf(2 * 3.14159265f * 440.f
                               * float(i / have.channels) / have.freq);
    for (int c = 0; c < have.channels; ++c) buf[i + c] = s;
}
  • i += have.channels: 루프가 "프레임" 단위로 전진한다. channels가 2면 한 번에 2칸씩 건너뛰며, 그 프레임 안의 모든 채널에 같은 값을 채운다 (모노 신호를 스테레오로 복제하는 방식).
  • i / have.channels: 인터리브 인덱스 i를 다시 "몇 번째 프레임인가"로 환산한다. 채널 개수를 알아야만 이 역변환이 가능하다.
  • / have.freq: 프레임 인덱스를 초 단위 시간으로 변환한다. sin(2π × 440Hz × t)에서 t = frame_index / freq가 되는 원리를 그대로 적용한 것이다.
  • formatAUDIO_F32SYS이기 때문에 sin 결과값(-1.0~1.0 범위)을 변환 없이 그대로 buf에 채울 수 있다. 만약 have.formatAUDIO_S16 같은 정수형이었다면 별도의 스케일링/변환 코드가 필요하다.

4.4 큐에 넣고 재생 시작

SDL_QueueAudio(dev, buf.data(), buf.size() * sizeof(float));
SDL_PauseAudioDevice(dev, 0);
  • SDL_QueueAudio는 바이트 크기를 요구하므로 buf.size() * sizeof(float)로 변환한다. format이 float라는 걸 알아야 이 변환이 맞는지 검증할 수 있다.
  • SDL로 연 오디오 디바이스는 기본적으로 일시정지 상태에서 시작하므로, SDL_PauseAudioDevice(dev, 0)으로 재생을 시작시켜야 한다. 큐에 데이터가 있어도 이 호출이 없으면 소리가 나지 않는다.

💡 5. 적용 범위 및 흔한 실수

  • 핵심 원칙: want는 협상 요청용이고, 버퍼 계산·인터리브 처리·바이트 크기 변환 등 이후의 모든 오디오 처리 로직은 반드시 have 기준으로 해야 한다.
  • ❌ 흔한 실수 1: want 값을 기준으로 버퍼 크기나 시간 계산을 하는 경우. 하드웨어가 요청과 다르게 열렸다면(allowed_changes를 0이 아닌 값으로 넘긴 경우 등) 재생 시간이나 채널 레이아웃이 어긋난다.
  • ❌ 흔한 실수 2: × 24 같은 매직 넘버를 다른 samples/freq 조합에 그대로 재사용하는 경우. 이 상수는 samples=1024, freq=48000에서만 0.5초를 보장한다. 환경에 따라 달라질 수 있는 값을 정확히 맞추려면 아래처럼 목표 시간에서 역산하는 게 안전하다.
  • float target_seconds = 0.5f; int chunks = std::ceil(target_seconds * have.freq / have.samples); std::vector<float> buf(size_t(have.samples) * have.channels * chunks);

🏁 6. 마무리

6.1 결론

SDL_OpenAudioDevicewant(요청)와 have(확정) 두 스펙을 분리해 하드웨어와의 협상을 명시적으로 드러낸다. 버퍼 크기 계산, 인터리브 샘플 배치, 재생 시간 환산 등 실제 오디오 처리는 전부 have 값을 기준으로 이루어져야 하며, × 24 같은 상수는 특정 samples/freq 조합에서만 유효한 값이라는 점을 기억해두자.

6.2 참고 자료

6.3 예제 코드

 

 

decode-render/tools/sdl_check.cpp at main · moony211/decode-render

디코딩과 렌더링을 연구한다. Contribute to moony211/decode-render development by creating an account on GitHub.

github.com