Partial implementation

free-direct implements a read/write PCM buffer layer over SDL3. Hardware acceleration, 3D audio, EAX effects, DSBPLAY_LOOPING, and stereo panning are not functional. The implementation is designed to port legacy 2D game audio that uses simple mono/stereo PCM sounds played once.

Contents

Design and internal architecture

  DirectSoundCreate()
        │
        ▼
  DirectSoundImpl ──── SharedAudioDevice (singleton)
        │                     │
        │              SDL_AudioDeviceID (opened once on first Play)
        │
        ▼
  DirectSoundBufferImpl
    ├── std::vector<uint8_t>  ← raw PCM storage (dwBufferBytes)
    ├── SDL_AudioStream*       ← per-buffer stream (created on Play)
    ├── PCMWAVEFORMAT          ← format snapshot from DSBUFFERDESC
    ├── long  volume_cb        ← centibels, default 0 (full volume)
    ├── long  pan_cb           ← centibels, stored but NOT applied
    └── bool  playing          ← set by Play(), cleared by Stop()
  

Key design points:

  • One SharedAudioDevice singleton is opened on the first Play() call; all subsequent buffers share the same SDL audio device ID.
  • Each buffer owns a separate SDL_AudioStream created at play time. This stream is destroyed on Stop() or Release().
  • Volume is converted from centibels to a linear gain: gain = 10^(cb / 2000), applied with SDL_SetAudioStreamGain.
  • Stereo panning computes left/right gains using a constant-power curve but the result is (void)-discarded — pan is stored but not sent to SDL.
  • The PCM format is read from DSBUFFERDESC.lpwfxFormat cast to PCMWAVEFORMAT* to avoid LP64 struct-padding issues — see the PCMWAVEFORMAT section.
  • Multiple simultaneous buffers each have their own SDL_AudioStream; SDL mixes them at the device level.

Global functions

DirectSoundCreate Implemented

Creates a DirectSound object. Entry point for all DirectSound use.

HRESULT DirectSoundCreate(
    LPGUID          lpGuid,
    LPDIRECTSOUND*  ppDS,
    IUnknown*       pUnkOuter
);
ParameterDirTypeDescription
lpGuidinLPGUID Device GUID. Pass nullptr to select the default audio device. Any non-null value is accepted but ignored — there is always one audio device.
ppDSoutLPDIRECTSOUND* Receives the created IDirectSound pointer. Set to nullptr on failure. Must not be nullptr itself.
pUnkOuterinIUnknown* COM aggregation outer object. Must be nullptr; any other value is silently ignored.
No audio device opened yet

DirectSoundCreate only allocates the IDirectSound object. The SDL audio device is not opened until the first Play() call on any buffer.

Return valueCondition
DS_OKObject created successfully.
DSERR_INVALIDPARAMppDS is nullptr.

IDirectSound interface

Obtained from DirectSoundCreate. Pointer typedef: LPDIRECTSOUND.

IDirectSound::AddRef / Release Implemented

COM reference counting implemented with std::atomic<ULONG>. Release() deletes the object when the count reaches zero. Initial reference count after DirectSoundCreate is 1.

ULONG AddRef();
ULONG Release();

Both return the new reference count after the operation.

IDirectSound::SetCooperativeLevel Partial — stored only

Sets the cooperative level for the DirectSound object. In free-direct this call is accepted for compatibility but does not change any SDL audio behavior. Audio is always operated at the equivalent of DSSCL_NORMAL.

HRESULT SetCooperativeLevel(
    HWND    hwnd,
    DWORD   dwLevel
);
ParameterDirTypeDescription
hwndinHWND Application window handle. Stored but not used — SDL audio does not require an HWND.
dwLevelinDWORD Cooperative level. See DSSCL_* constants. Only DSSCL_NORMAL (0x00000001) is meaningful; all values are accepted without error.
Return valueCondition
DS_OKAlways returned.
IDirectSound::CreateSoundBuffer Implemented

Creates a sound buffer. free-direct creates a software PCM buffer backed by std::vector<uint8_t>. Hardware buffers are not supported — DSBCAPS_LOCHARDWARE is silently treated as software.

HRESULT CreateSoundBuffer(
    LPCDSBUFFERDESC      pcDSBufferDesc,
    LPDIRECTSOUNDBUFFER* ppDSBuffer,
    IUnknown*            pUnkOuter
);
ParameterDirTypeDescription
pcDSBufferDescinLPCDSBUFFERDESC Pointer to a DSBUFFERDESC describing the buffer. Must not be nullptr. dwSize, dwBufferBytes, and lpwfxFormat must all be valid.
ppDSBufferoutLPDIRECTSOUNDBUFFER* Receives the created buffer object on success. Set to nullptr on failure. Must not be nullptr itself.
pUnkOuterinIUnknown* COM aggregation — must be nullptr; silently ignored.

Accepted DSBUFFERDESC.dwFlags combinations and their effect:

FlagSupportNotes
DSBCAPS_CTRLVOLUMEFunctionalSetVolume applies gain via SDL.
DSBCAPS_CTRLPANStored onlySetPan stores the value; pan is not applied.
DSBCAPS_CTRLFREQUENCYNo-opFlag accepted; no frequency shifting.
DSBCAPS_LOCSOFTWAREFunctionalAll buffers are software.
DSBCAPS_LOCHARDWARETreated as softwareNo hardware mixing available.
DSBCAPS_STATICAcceptedNo semantic difference from non-static.
Return valueCondition
DS_OKBuffer created successfully.
DSERR_INVALIDPARAMpcDSBufferDesc or ppDSBuffer is nullptr, or dwBufferBytes is 0, or lpwfxFormat is nullptr.
DSERR_OUTOFMEMORYAllocation of the buffer failed.

IDirectSoundBuffer interface

Obtained from IDirectSound::CreateSoundBuffer. Pointer typedef: LPDIRECTSOUNDBUFFER.

IDirectSoundBuffer::AddRef / Release Implemented

COM reference counting with std::atomic<ULONG>. Release() when count reaches zero: calls Stop(), destroys the SDL_AudioStream, frees the PCM vector. Initial reference count is 1.

ULONG AddRef();
ULONG Release();

Both return the new reference count.

IDirectSoundBuffer::Lock Implemented

Obtains write access to a region of the PCM buffer. Returns one or two pointers directly into the internal std::vector<uint8_t>. No mutex or synchronization is used — caller must not call Play() while locked.

HRESULT Lock(
    DWORD   dwWriteCursor,
    DWORD   dwWriteBytes,
    LPVOID* ppvAudioPtr1,
    LPDWORD pdwAudioBytes1,
    LPVOID* ppvAudioPtr2,
    LPDWORD pdwAudioBytes2,
    DWORD   dwFlags
);
ParameterDirTypeDescription
dwWriteCursorinDWORD Byte offset from the start of the buffer at which to begin the lock. Ignored when DSBLOCK_FROMWRITECURSOR is set in dwFlags.
dwWriteBytesinDWORD Number of bytes to lock. Must be ≤ dwBufferBytes. When DSBLOCK_ENTIREBUFFER is set this value is ignored and the entire buffer is locked.
ppvAudioPtr1outLPVOID* Receives a pointer to the first (and possibly only) segment of locked memory. Points into the internal vector. Must not be nullptr.
pdwAudioBytes1outLPDWORD Receives the byte count of the first segment. Must not be nullptr.
ppvAudioPtr2outLPVOID* Receives a pointer to the second (wrap-around) segment, or nullptr if no wrap occurred. May be nullptr (caller ignores wrap).
pdwAudioBytes2outLPDWORD Receives the byte count of the second segment, or 0 if none. May be nullptr.
dwFlagsinDWORD Lock flags. See DSBLOCK_* constants.

Wrap-around behaviour: The buffer is treated as circular. If dwWriteCursor + dwWriteBytes > dwBufferBytes:

  • Segment 1: from dwWriteCursor to end-of-buffer → size = dwBufferBytes − dwWriteCursor.
  • Segment 2: from start of buffer → size = dwWriteBytes − size1.

If no wrap, segment 2 pointer is nullptr and size is 0.

Return valueCondition
DS_OKLock succeeded. Pointers valid for writing.
DSERR_INVALIDPARAMppvAudioPtr1 or pdwAudioBytes1 is nullptr.
DSERR_INVALIDCALLRequested region exceeds buffer size.
IDirectSoundBuffer::Unlock Implemented — no-op

Signals write completion. In free-direct this is a no-op — the vector is already in-place, no flush or copy is needed. Parameters are accepted for API compatibility and then ignored.

HRESULT Unlock(
    LPVOID  pvAudioPtr1,
    DWORD   dwAudioBytes1,
    LPVOID  pvAudioPtr2,
    DWORD   dwAudioBytes2
);
ParameterDirTypeDescription
pvAudioPtr1inLPVOIDFirst pointer from Lock. Ignored.
dwAudioBytes1inDWORDBytes written to first segment. Ignored.
pvAudioPtr2inLPVOIDSecond pointer from Lock. Ignored.
dwAudioBytes2inDWORDBytes written to second segment. Ignored.
Return valueCondition
DS_OKAlways returned.
IDirectSoundBuffer::Play Partial — looping not functional

Starts playback. The entire PCM buffer is put into a new SDL_AudioStream and the stream is bound to the shared device. The buffer plays once; DSBPLAY_LOOPING is accepted but does not cause the buffer to repeat.

HRESULT Play(
    DWORD  dwReserved1,
    DWORD  dwPriority,
    DWORD  dwFlags
);
ParameterDirTypeDescription
dwReserved1inDWORD Reserved — must be 0. Ignored by free-direct.
dwPriorityinDWORD Buffer priority for voice stealing. Accepted but not used — all buffers have equal priority under SDL.
dwFlagsinDWORD Play flags. 0 = play once. DSBPLAY_LOOPING (0x00000001) = accepted but looping does not occur.

Internal steps on Play():

  1. Open SharedAudioDevice singleton if not yet open (SDL_OpenAudioDevice).
  2. Create a new SDL_AudioStream for the buffer's PCM format (sample rate, channels, bit depth).
  3. Apply current volume: compute gain = 10^(volume_cb / 2000), call SDL_SetAudioStreamGain(stream, gain).
  4. Put all PCM data into the stream: SDL_PutAudioStreamData(stream, data.data(), data.size()).
  5. Bind the stream to the audio device: SDL_BindAudioStream(device, stream).
  6. Set playing = true.
DSBPLAY_LOOPING not functional

The flag is accepted without error, but the audio plays once and stops. To simulate looping, poll GetStatus and re-call Play() when the buffer finishes — or wrap it in a helper class. See Developer Notes for an implementation sketch.

Return valueCondition
DS_OKPlayback started successfully.
DSERR_INVALIDCALLSDL audio device could not be opened.
DSERR_GENERICSDL_AudioStream creation or binding failed.
IDirectSoundBuffer::Stop Implemented

Stops playback. Unbinds the SDL_AudioStream from the device and destroys it. The PCM vector contents are preserved. The buffer can be played again with Play(). Calling Stop on an already-stopped buffer is harmless (idempotent).

HRESULT Stop();
Return valueCondition
DS_OKAlways returned.
IDirectSoundBuffer::GetStatus Partial

Returns the current playback status. free-direct tracks a simple playing boolean. It does not detect when SDL finishes consuming the stream, so DSBSTATUS_PLAYING may remain set after audio has finished.

HRESULT GetStatus(
    LPDWORD pdwStatus
);
ParameterDirTypeDescription
pdwStatusoutLPDWORD Receives a bitmask of DSBSTATUS_* flags. Set to 0 if the buffer is stopped.
Flag returnedValueMeaning
DSBSTATUS_PLAYING0x00000001Set after Play(); cleared only by Stop() or Release().
DSBSTATUS_LOOPING0x00000004Never set — looping is not implemented.
DSBSTATUS_BUFFERLOST0x00000002Never set — CPU buffers are never lost.
Status does not auto-clear on completion

free-direct does not poll SDL to detect end-of-stream. DSBSTATUS_PLAYING remains set until Stop() is called explicitly. Do not rely on this flag alone to detect when audio playback ends.

Return valueCondition
DS_OKStatus written successfully.
DSERR_INVALIDPARAMpdwStatus is nullptr.
IDirectSoundBuffer::SetVolume Implemented

Sets the playback volume. Volume is specified in centibels (hundredths of a decibel). Converted to a linear gain and applied to the SDL_AudioStream immediately if one is open; stored for future Play() calls otherwise.

HRESULT SetVolume(
    LONG lVolume
);
ParameterDirTypeDescription
lVolumeinLONG Volume in centibels. Range: DSBVOLUME_MIN (−10000, ≈ silence) to DSBVOLUME_MAX (0, full volume). Values above 0 are not defined by the original API.
ValuedBLinear gainMeaning
00 dB1.000Full volume (default)
−500−5 dB≈ 0.562About half perceived loudness
−1000−10 dB≈ 0.316Roughly one-third amplitude
−2000−20 dB≈ 0.100One-tenth amplitude
−10000−100 dB≈ 0.00001Essentially silent

Conversion formula: float gain = powf(10.0f, lVolume / 2000.0f);

Applied with SDL_SetAudioStreamGain(stream, gain) if a stream is open.

Return valueCondition
DS_OKAlways returned.
IDirectSoundBuffer::SetPan Stored only — not applied

Sets the stereo pan position. The value and the computed left/right gains are stored but the result is (void)-discarded before it reaches SDL. Pan has no audible effect.

HRESULT SetPan(
    LONG lPan
);
ParameterDirTypeDescription
lPaninLONG Pan in centibels. −10000 = full left, 0 = center, +10000 = full right. Stored in the buffer but not applied to SDL. All audio is mixed at center.
Pan not applied to SDL

The implementation computes constant-power left/right gains but passes them to (void)left; (void)right; — the computed values are discarded. This is a known limitation; see Developer Notes for the recommended path to add proper panning.

Return valueCondition
DS_OKAlways returned.
IDirectSoundBuffer::SetCurrentPosition Stored — not applied to active stream

Sets the playback start position. Stored in the buffer but not used to seek within an active SDL stream (SDL_AudioStream does not support seeking). Effective only before Play().

HRESULT SetCurrentPosition(
    DWORD dwNewPosition
);
ParameterDirTypeDescription
dwNewPositioninDWORD Byte offset from start of buffer. Values beyond the buffer size are clamped. When Play() is called, the PCM data is fed from this offset if it has been set before Play.
Return valueCondition
DS_OKAlways returned.
IDirectSoundBuffer::GetCurrentPosition Stub — always returns 0

Returns the current play and write cursor positions. free-direct always returns 0 for both — SDL_AudioStream does not expose a consumption position.

HRESULT GetCurrentPosition(
    LPDWORD pdwCurrentPlayCursor,
    LPDWORD pdwCurrentWriteCursor
);
ParameterDirTypeDescription
pdwCurrentPlayCursoroutLPDWORD Receives the play cursor position. Always set to 0. May be nullptr (skip this output).
pdwCurrentWriteCursoroutLPDWORD Receives the safe write cursor position. Always set to 0. May be nullptr.
Return valueCondition
DS_OKAlways returned.

Structs

DSBUFFERDESC

Describes a sound buffer. Passed to IDirectSound::CreateSoundBuffer.

typedef struct {
    DWORD          dwSize;        // sizeof(DSBUFFERDESC)
    DWORD          dwFlags;       // DSBCAPS_* capability flags
    DWORD          dwBufferBytes; // PCM buffer size in bytes
    DWORD          dwReserved;    // must be 0
    LPWAVEFORMATEX lpwfxFormat;   // pointer to PCMWAVEFORMAT (see note)
} DSBUFFERDESC;
FieldDirTypeDescription
dwSizeinDWORD Structure size in bytes. Must be sizeof(DSBUFFERDESC) before passing to CreateSoundBuffer.
dwFlagsinDWORD Combination of DSBCAPS_* flags. Controls which features are enabled. DSBCAPS_CTRLVOLUME is the most useful flag in free-direct.
dwBufferBytesinDWORD Total byte size of the PCM data area. A std::vector<uint8_t> of exactly this size is allocated. Must be > 0.
dwReservedinDWORD Reserved — set to 0. Ignored by free-direct.
lpwfxFormatinLPWAVEFORMATEX Pointer to a PCMWAVEFORMAT (typed as LPWAVEFORMATEX for API compatibility). free-direct casts this to PCMWAVEFORMAT* — always fill a PCMWAVEFORMAT to avoid LP64 padding issues. Must not be nullptr.
WAVEFORMAT

Base wave format structure. Embedded as the first member of PCMWAVEFORMAT.

typedef struct {
    WORD  wFormatTag;       // audio format type
    WORD  nChannels;        // number of channels
    DWORD nSamplesPerSec;   // sample rate in Hz
    DWORD nAvgBytesPerSec;  // nSamplesPerSec * nBlockAlign
    WORD  nBlockAlign;      // bytes per sample frame
} WAVEFORMAT;
// sizeof(WAVEFORMAT) = 14 bytes on LP64 (no internal padding)
FieldByte offsetTypeDescription
wFormatTag0WORDFormat type. 1 = WAVE_FORMAT_PCM (only supported value).
nChannels2WORDChannel count. 1 = mono, 2 = stereo. Both are passed through to SDL.
nSamplesPerSec4DWORDSamples per second per channel. Common values: 8000, 11025, 22050, 44100.
nAvgBytesPerSec8DWORDAverage byte transfer rate = nSamplesPerSec × nBlockAlign. Not used by free-direct for processing.
nBlockAlign12WORDBytes per sample frame = nChannels × (wBitsPerSample / 8). E.g. stereo 16-bit = 4.
PCMWAVEFORMAT

PCM wave format descriptor. This is the correct struct to pass as lpwfxFormat in free-direct.

typedef struct {
    WAVEFORMAT wf;            // base format (14 bytes)
    WORD       wBitsPerSample; // bits per sample (at byte offset 14)
} PCMWAVEFORMAT;
// sizeof(PCMWAVEFORMAT) = 16 bytes
FieldByte offsetTypeDescription
wf0WAVEFORMATBase format fields (see WAVEFORMAT).
wBitsPerSample14WORDBits per sample per channel. 8 = unsigned 8-bit PCM, 16 = signed 16-bit PCM LE. free-direct reads this field at offset 14.
Critical: use PCMWAVEFORMAT, not WAVEFORMATEX

On LP64 systems (Linux 64-bit, macOS 64-bit):

  • PCMWAVEFORMAT.wBitsPerSample is at byte offset 14.
  • WAVEFORMATEX.wBitsPerSample is at byte offset 16 (due to DWORD alignment padding after the WORD nBlockAlign in some compiler layouts).

free-direct always reads wBitsPerSample at offset 14. If you fill a WAVEFORMATEX and cast it, free-direct will read padding bytes as the bit depth and open the SDL stream with incorrect parameters, causing silence or noise.

// CORRECT: use PCMWAVEFORMAT
PCMWAVEFORMAT pcm = {};
pcm.wf.wFormatTag      = 1;      // WAVE_FORMAT_PCM
pcm.wf.nChannels       = 1;      // mono
pcm.wf.nSamplesPerSec  = 22050;
pcm.wf.nBlockAlign     = 2;      // 1 ch * 2 bytes
pcm.wf.nAvgBytesPerSec = 44100;
pcm.wBitsPerSample     = 16;
bufDesc.lpwfxFormat = (LPWAVEFORMATEX)&pcm;
WAVEFORMATEX

Extended wave format. Defined in dsound.h for compile compatibility. Do not use this struct directly as lpwfxFormat in free-direct — use PCMWAVEFORMAT instead.

typedef struct {
    WORD  wFormatTag;
    WORD  nChannels;
    DWORD nSamplesPerSec;
    DWORD nAvgBytesPerSec;
    WORD  nBlockAlign;
    WORD  wBitsPerSample;  // at offset 16 on LP64 (NOT offset 14)
    WORD  cbSize;          // extra bytes beyond this struct
} WAVEFORMATEX;
FieldTypeDescription
wFormatTagWORD1 = WAVE_FORMAT_PCM.
nChannelsWORD1 = mono, 2 = stereo.
nSamplesPerSecDWORDSample rate in Hz.
nAvgBytesPerSecDWORDnSamplesPerSec × nBlockAlign.
nBlockAlignWORDnChannels × (wBitsPerSample / 8).
wBitsPerSampleWORDBits per sample. At offset 16 on LP64, not offset 14. Do not use this field path with free-direct.
cbSizeWORDExtra data size appended after this struct. 0 for PCM.

Flag constants

DSBCAPS_* — buffer capability flags

Passed in DSBUFFERDESC.dwFlags to CreateSoundBuffer.

ConstantValueSupportDescription
DSBCAPS_PRIMARYBUFFER0x00000001NoPrimary mixer output buffer. Not implemented.
DSBCAPS_STATIC0x00000002YesBuffer loaded once, played many times. Accepted — all buffers are effectively static.
DSBCAPS_LOCHARDWARE0x00000004Treated as softwareForce hardware mixing. No hardware mixing; silently treated as software.
DSBCAPS_LOCSOFTWARE0x00000008YesForce software mixing. Default for all buffers.
DSBCAPS_CTRL3D0x00000010No3D positional audio. Not implemented.
DSBCAPS_CTRLFREQUENCY0x00000020No-opFrequency (pitch) control. Accepted but SetFrequency not implemented.
DSBCAPS_CTRLPAN0x00000040Stored onlyStereo pan control. SetPan stores the value but does not apply it.
DSBCAPS_CTRLVOLUME0x00000080YesVolume control. SetVolume is fully functional via SDL_SetAudioStreamGain.
DSBCAPS_CTRLDEFAULT0x000000E0PartialCombined CTRLFREQUENCY|CTRLPAN|CTRLVOLUME. Volume works; pan stored only; frequency no-op.
DSBCAPS_CTRLALL0x000001F0PartialAll controls. Only volume is functional.
DSBCAPS_STICKYFOCUS0x00004000NoContinue when app loses focus. SDL audio is always running; no special handling.
DSBCAPS_GLOBALFOCUS0x00008000NoGlobal audio focus. Not implemented.

DSBLOCK_* — lock flags

Passed as dwFlags to IDirectSoundBuffer::Lock.

ConstantValueSupportDescription
DSBLOCK_FROMWRITECURSOR0x00000001 Yes Ignore dwWriteCursor and lock from the current write cursor (always 0 in free-direct — the buffer is always fully lockable from offset 0).
DSBLOCK_ENTIREBUFFER0x00000002 Yes Lock the entire buffer. Overrides dwWriteBytes; equivalent to passing dwBufferBytes as the size.

DSBSTATUS_* — status flags

Returned by IDirectSoundBuffer::GetStatus.

ConstantValueSupportDescription
DSBSTATUS_PLAYING0x00000001 Partial Buffer is playing. Set by Play(); cleared by Stop(). Does not auto-clear on stream end.
DSBSTATUS_BUFFERLOST0x00000002 Never set Buffer memory lost. CPU-side buffers never become lost.
DSBSTATUS_LOOPING0x00000004 Never set Buffer looping. Looping not implemented; never returned.

DSSCL_* — cooperative level values

Passed to IDirectSound::SetCooperativeLevel.

ConstantValueSupportDescription
DSSCL_NORMAL0x00000001YesNormal shared access. The only level that has semantic meaning in free-direct (SDL audio is always shared).
DSSCL_PRIORITY0x00000002AcceptedPriority access. Accepted without error; treated as DSSCL_NORMAL.
DSSCL_EXCLUSIVE0x00000003AcceptedExclusive access. Accepted; no exclusive device ownership occurs.
DSSCL_WRITEPRIMARY0x00000004NoWrite to primary buffer. Primary buffers not implemented.

DSBPLAY_* — play flags

Passed as dwFlags to IDirectSoundBuffer::Play.

ConstantValueSupportDescription
DSBPLAY_LOOPING0x00000001 Accepted, not functional Loop playback continuously. Accepted without error but buffer plays only once. Re-call Play() to simulate looping.

Result codes

Success

ConstantValueDescription
DS_OK0x00000000Operation succeeded. Equivalent to S_OK.

Error codes actively returned

ConstantValueReturned byDescription
DSERR_INVALIDPARAM0x80070057 DirectSoundCreate, CreateSoundBuffer, Lock, GetStatus A required pointer is nullptr, or a required field is zero or invalid.
DSERR_OUTOFMEMORY0x80070008 CreateSoundBuffer Memory allocation for the buffer failed.
DSERR_INVALIDCALL0x88780032 Lock, Play Lock: region exceeds buffer size. Play: SDL audio device could not be opened.
DSERR_GENERIC0x88780081 Play SDL_AudioStream creation or binding to device failed.

Compile-compatibility constants (defined, not returned)

These constants are defined in dsound.h to allow legacy code to compile. free-direct never returns them.

ConstantValue
DSERR_ALLOCATED0x8878000A
DSERR_CONTROLUNAVAIL0x8878001E
DSERR_BADFORMAT0x88780064
DSERR_NODRIVER0x88780078
DSERR_OTHERAPP0x88780082
DSERR_BUFFERLOST0x88780096
DSERR_UNINITIALIZED0x888700AA
DSERR_UNSUPPORTED0x80004001
DSERR_PRIOLEVELNEEDED0x88780046

Known limitations

FeatureStatusNotes
DSBPLAY_LOOPINGNot functionalFlag accepted; plays once. Re-call Play() to loop.
Stereo panning (SetPan)Stored onlyGains computed but voided; all audio plays at center.
Frequency shiftingNot implementedNo SetFrequency; DSBCAPS_CTRLFREQUENCY is a no-op.
3D positional audioNot implementedNo IDirectSound3DBuffer or IDirectSound3DListener.
Primary bufferNot implementedDSBCAPS_PRIMARYBUFFER is not supported.
GetStatus completion detectionIncompleteDSBSTATUS_PLAYING does not auto-clear on stream end.
GetCurrentPositionStubAlways returns 0 for both cursors.
SetCurrentPositionStored onlyValue stored; not applied to an active SDL stream.
Multiple simultaneous buffersSupportedEach buffer has its own SDL_AudioStream; SDL mixes at device level.
Mixed sample ratesSupportedSDL resamples each stream independently to the device rate.
8-bit PCMSupportedSDL_AUDIO_U8 format is passed through correctly.
16-bit PCMSupportedSDL_AUDIO_S16LE format is passed through correctly.
Hardware accelerationNot availableAll mixing is in SDL software; no hardware voice management.
EAX / DSP effectsNot implementedNo DirectSound effects processing of any kind.
WAVEFORMATEX as lpwfxFormatDo not useLP64 padding places wBitsPerSample at offset 16 instead of 14; free-direct reads offset 14. Always use PCMWAVEFORMAT.
How to implement looping

Since DSBPLAY_LOOPING is not functional, the recommended workaround is to monitor playback from within the game loop and re-play the buffer. A simple wrapper class that tracks elapsed time vs. buffer duration and calls Play() again when needed is the cleanest approach. See Developer Notes for an implementation sketch.