💡 한 줄 요약
SDL_OpenAudioDevice가want/have두 스펙을 나눠서 다루는 이유를 협상(negotiation) 구조로 정리하고,have값을 기준으로 사인파 버퍼를 계산해SDL_QueueAudio로 재생하는 흐름을 정리한다.
🚨 1. 문제 상황
SDL2로 오디오를 재생하려고 SDL_OpenAudioDevice를 호출하면 인자로 want와 have(예제 변수명 기준) 두 개의 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로 넘기면 해당 항목에 한해 have가 want와 달라지는 것을 허용하게 된다.
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가 되는 원리를 그대로 적용한 것이다.format이AUDIO_F32SYS이기 때문에 sin 결과값(-1.0~1.0 범위)을 변환 없이 그대로buf에 채울 수 있다. 만약have.format이AUDIO_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_OpenAudioDevice는 want(요청)와 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
'잡(job)기술' 카테고리의 다른 글
| Asio strand 완전 정복: 멀티스레드 동시성 직렬화와 CUDA Stream 비교 분석 (0) | 2026.09.22 |
|---|---|
| C++20 코루틴 + Asio 비동기 기초: io_context부터 concurrent_channel, executor 표기까지 (0) | 2026.09.22 |
| XWayland: 두 세계를 잇는 다리, 그리고 종합 정리 (0) | 2026.09.21 |
| Wayland와 컴포지터: X11과는 다른 설계 (0) | 2026.09.21 |
| X11 기초 이해하기: Display, Screen, 그리고 $DISPLAY의 정체 (0) | 2026.09.21 |