audioplayer/src/audio/decoders.jai
2026-05-04 19:50:59 +03:00

79 lines
2.9 KiB
Plaintext

//
// OGG Vorbis decoder. Jellyfin always serves us /universal as OGG, so this
// is the single decode path. Returns Sound_Data with type LINEAR_SAMPLE_ARRAY
// — same buffer the visualizer reads via sound_data.samples (no parallel
// decoder, no cursor desync).
//
// We do NOT use stb_vorbis_decode_memory: that malloc()s the output buffer
// with libc's allocator, and Sound_Player's release_asset path frees it
// with Jai's allocator → SEGV at song end. Instead we open the stream,
// allocate the PCM buffer ourselves, and decode into it. Same allocator on
// both ends.
//
decode_ogg :: (bytes: string, name: string) -> Sound.Sound_Data, bool {
err: s32;
v := Stb_Vorbis.stb_vorbis_open_memory(bytes.data, cast(s32) bytes.count, *err, null);
if !v {
log_error("decode_ogg: open failed for '%' (err=%)", name, err);
return .{}, false;
}
defer Stb_Vorbis.stb_vorbis_close(v);
info := Stb_Vorbis.stb_vorbis_get_info(v);
nch := cast(s64) info.channels;
if nch <= 0 || info.sample_rate == 0 {
log_error("decode_ogg: bad info for '%' (ch=%, rate=%)", name, info.channels, info.sample_rate);
return .{}, false;
}
total_frames := cast(s64) Stb_Vorbis.stb_vorbis_stream_length_in_samples(v);
if total_frames <= 0 {
log_error("decode_ogg: zero-length stream for '%'", name);
return .{}, false;
}
total_samples := total_frames * nch;
samples := cast(*s16) alloc(total_samples * size_of(s16));
if !samples {
log_error("decode_ogg: alloc failed for '%'", name);
return .{}, false;
}
// stb_vorbis sometimes hands back fewer frames than the header advertised
// (chained / corrupt streams), so loop until it returns 0 and use what
// we got.
decoded_frames: s64 = 0;
while decoded_frames < total_frames {
remaining_shorts := cast(s32) ((total_frames - decoded_frames) * nch);
n := Stb_Vorbis.stb_vorbis_get_samples_short_interleaved(
v, cast(s32) nch,
samples + decoded_frames * nch,
remaining_shorts);
if n <= 0 break;
decoded_frames += n;
}
if decoded_frames == 0 {
log_error("decode_ogg: no frames decoded for '%'", name);
free(samples);
return .{}, false;
}
actual_samples := decoded_frames * nch;
sd: Sound.Sound_Data;
sd.name = copy_string(name);
sd.loaded = true;
sd.type = .LINEAR_SAMPLE_ARRAY;
sd.nchannels = cast(u16) nch;
sd.sampling_rate = cast(u32) info.sample_rate;
sd.nsamples_times_nchannels = actual_samples;
sd.samples = samples;
sd.buffer.count = actual_samples * size_of(s16);
sd.buffer.data = cast(*u8) samples;
log_info("decode_ogg: '%' ch=%, rate=%, frames=%", name, nch, info.sample_rate, decoded_frames);
return sd, true;
}