WavStreamInstance::seek() selects the decoder path by testing the codec union pointer instead of the parsed filetype:
// src/audiosource/wav/soloud_wavstream.cpp
result WavStreamInstance::seek(double aSeconds, float* mScratch, unsigned int mScratchSize)
{
if (mCodec.mOgg) // <-- mCodec is a UNION: non-null for drwav*/drflac*/drmp3* too
{
int pos = (int)floor(mBaseSamplerate * aSeconds);
stb_vorbis_seek(mCodec.mOgg, pos);
...
mCodec is a union of stb_vorbis* / drwav* / drflac* / drmp3*, so for any non-ogg WavStream with a live decoder this condition is true and stb_vorbis_seek() runs on a foreign decoder object. getAudio() and rewind() correctly switch (mParent->mFiletype); seek() is the one branch that doesn't.
Impact (observed in production, macOS): a looping WAV-typed WavStream reaches end-of-stream, mixBus_internal's loop path calls voice->seek(...) on the audio thread, and stb_vorbis_seek walks the drwav as if it were a vorbis handle. In our crashes it ended in stb_vorbis_stream_length_in_samples()'s error path writing f->total_samples = 0xffffffff into the foreign allocation — corrupting whatever object the heap had placed at that offset (we chased a "gesture recognizer" crash for days before tracing it back here). Any looping .wav/.flac/.mp3 stream triggers it on the first loop; sanitizers don't flag it because both objects are live.
Repro sketch:
SoLoud::Soloud soloud; soloud.init();
SoLoud::WavStream ws;
ws.load("anything.wav"); // filetype = WAVSTREAM_WAV
int h = soloud.play(ws);
soloud.setLooping(h, true);
// ...let it play to the end of the file; the loop-seek runs
// stb_vorbis_seek() on the live drwav* -> heap corruption / UB.
Fix (matches getAudio/rewind):
result WavStreamInstance::seek(double aSeconds, float* mScratch, unsigned int mScratchSize)
{
- if (mCodec.mOgg)
+ if (mParent->mFiletype == WAVSTREAM_OGG && mCodec.mOgg)
{
Non-ogg streams then fall through to the generic AudioSourceInstance::seek() (rewind + skip), which also makes looping WAV streams actually loop — with the current code the vorbis mis-seek fails, while (seek == SO_NO_ERROR) breaks, and the looping voice silently dies at its first loop point.
Happy to send this as a PR if useful.
WavStreamInstance::seek()selects the decoder path by testing the codec union pointer instead of the parsed filetype:mCodecis a union ofstb_vorbis*/drwav*/drflac*/drmp3*, so for any non-ogg WavStream with a live decoder this condition is true andstb_vorbis_seek()runs on a foreign decoder object.getAudio()andrewind()correctlyswitch (mParent->mFiletype);seek()is the one branch that doesn't.Impact (observed in production, macOS): a looping WAV-typed
WavStreamreaches end-of-stream,mixBus_internal's loop path callsvoice->seek(...)on the audio thread, andstb_vorbis_seekwalks thedrwavas if it were a vorbis handle. In our crashes it ended instb_vorbis_stream_length_in_samples()'s error path writingf->total_samples = 0xffffffffinto the foreign allocation — corrupting whatever object the heap had placed at that offset (we chased a "gesture recognizer" crash for days before tracing it back here). Any looping.wav/.flac/.mp3stream triggers it on the first loop; sanitizers don't flag it because both objects are live.Repro sketch:
Fix (matches getAudio/rewind):
result WavStreamInstance::seek(double aSeconds, float* mScratch, unsigned int mScratchSize) { - if (mCodec.mOgg) + if (mParent->mFiletype == WAVSTREAM_OGG && mCodec.mOgg) {Non-ogg streams then fall through to the generic
AudioSourceInstance::seek()(rewind + skip), which also makes looping WAV streams actually loop — with the current code the vorbis mis-seek fails,while (seek == SO_NO_ERROR)breaks, and the looping voice silently dies at its first loop point.Happy to send this as a PR if useful.