From f3e4a3fe64b00d91242d2321cd8d94d3615b4c4a Mon Sep 17 00:00:00 2001 From: libretroadmin <> Date: Fri, 31 Jul 2026 18:09:05 +0000 Subject: [PATCH 1/5] rwav: decode a block's emission window rather than the whole block rwav_decode_s16 kept a 256KB static scratch for windows starting or ending inside a coded block: the block was decoded whole into it and the wanted part copied out. That made the function non-reentrant, and the comment acknowledged it on the reasoning that the whole-file decode audio_mixer performs lands on the block-aligned fast path every time. A windowed caller inverts that. Reading in fixed-size chunks is mid-block on nearly every call, so the scratch would be in constant use and two voices decoding ADPCM concurrently would corrupt each other. Give rwav_decode_block the window instead. A block must still be decoded from its start - the predictor state is what its samples continue from - but nothing before the window has to be stored, so the mid-block case writes straight into the caller's buffer and the scratch is not needed rather than merely made safe. Both call sites collapse into one. TSan, eight threads decoding overlapping mid-block windows of one IMA file: data races at rwav.c:570 and :602 before, none after. Output verified identical to the previous code across 308 combinations - 11 fixtures covering u8, s16, s24, f32, A-law, mu-law, MS ADPCM and IMA ADPCM in stereo and mono, seven window steps down to a single frame, and four starting offsets including mid-block. --- libretro-common/formats/wav/rwav.c | 66 +++++++++++++------------- libretro-common/include/formats/rwav.h | 1 + 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/libretro-common/formats/wav/rwav.c b/libretro-common/formats/wav/rwav.c index a5c4a848eb4b..224871025ad5 100644 --- a/libretro-common/formats/wav/rwav.c +++ b/libretro-common/formats/wav/rwav.c @@ -548,12 +548,25 @@ static const short rwav_ms_adapt[16] = { /* Decode one block into 'out', which must hold samplesperblock frames. * Returns frames produced, 0 on a malformed block. */ +/* Emit only the frames in [first, first + count) of the block, written + * to @out relative to @first. A block must still be decoded from its + * start - the predictor state is what the samples continue from - but + * nothing before @first has to be *stored*, so a window landing inside + * a block needs no scratch buffer to be trimmed afterwards. */ +#define RWAV_PUT(idx, chan, v) \ + do { \ + unsigned rwav__i = (unsigned)(idx); \ + if (rwav__i >= first && rwav__i - first < count) \ + out[(rwav__i - first) * ch + (chan)] = (short)(v); \ + } while (0) + static unsigned rwav_decode_block(const rwav_t *wav, const unsigned char *b, - size_t blen, short *out) + size_t blen, unsigned first, unsigned count, short *out) { unsigned ch = wav->numchannels; unsigned spb = wav->samplesperblock; unsigned c, i; + unsigned end; if (wav->format == RWAV_FORMAT_IMA_ADPCM) { @@ -567,7 +580,7 @@ static unsigned rwav_decode_block(const rwav_t *wav, const unsigned char *b, idx[c] = b[c * 4 + 2]; if (idx[c] > 88) idx[c] = 88; - out[c] = (short)pred[c]; + RWAV_PUT(0, c, pred[c]); } /* After the preamble the nibbles come in eight-sample runs per * channel: four bytes of one channel, then four of the next. */ @@ -599,14 +612,15 @@ static unsigned rwav_decode_block(const rwav_t *wav, const unsigned char *b, if (idx[c] < 0) idx[c] = 0; if (idx[c] > 88) idx[c] = 88; if (i + k < spb) - out[(i + k) * ch + c] = (short)pred[c]; + RWAV_PUT(i + k, c, pred[c]); } } off += 4 * ch; i += 8; } } - return spb; + end = (spb < first + count) ? spb : first + count; + return (end > first) ? end - first : 0; } /* MS ADPCM: each channel states a coefficient index, a delta and @@ -633,8 +647,8 @@ static unsigned rwav_decode_block(const rwav_t *wav, const unsigned char *b, s2[c] = (short)rwav_u16(b + ch * 5 + c * 2); for (c = 0; c < ch; c++) { - out[c] = (short)s2[c]; - out[ch + c] = (short)s1[c]; + RWAV_PUT(0, c, s2[c]); + RWAV_PUT(1, c, s1[c]); } off = need; i = 2; @@ -653,7 +667,7 @@ static unsigned rwav_decode_block(const rwav_t *wav, const unsigned char *b, delta[c] = (rwav_ms_adapt[nib] * delta[c]) / 256; if (delta[c] < 16) delta[c] = 16; - out[i * ch + c] = (short)p; + RWAV_PUT(i, c, p); if (c & 1) off++; if (c == ch - 1) @@ -675,16 +689,19 @@ static unsigned rwav_decode_block(const rwav_t *wav, const unsigned char *b, delta[0] = (rwav_ms_adapt[nib] * delta[0]) / 256; if (delta[0] < 16) delta[0] = 16; - out[i * ch] = (short)p; + RWAV_PUT(i, 0, p); i++; } off++; } } - return spb; + end = (spb < first + count) ? spb : first + count; + return (end > first) ? end - first : 0; } } +#undef RWAV_PUT + size_t rwav_decode_s16(const rwav_t *wav, const void *base, size_t frame, size_t frames, short *out) { @@ -727,7 +744,6 @@ size_t rwav_decode_s16(const rwav_t *wav, const void *base, size_t frame, /* Start at the block holding 'frame' and step forward within * it: a block restates its own predictor, so nothing before it * has to be decoded. */ - static short blk[8192 * 16]; unsigned spb = wav->samplesperblock; while (done < frames) { @@ -738,31 +754,13 @@ size_t rwav_decode_s16(const rwav_t *wav, const void *base, size_t frame, unsigned got, take; if (boff + wav->blockalign > wav->subchunk2size) break; - if (within == 0 && frames - done >= (size_t)spb) - { - /* The window covers this whole block: decode straight - * into the caller's buffer. The whole-file decode the - * mixer does lands here for every block, so the static - * scratch below - and with it the reentrancy caveat - - * only serves windows that start or end mid-block. */ - got = rwav_decode_block(wav, d + boff, wav->blockalign, - out + done * ch); - if (!got) - break; - done += got; - continue; - } - if ((size_t)spb * ch > sizeof(blk) / sizeof(blk[0])) - break; - got = rwav_decode_block(wav, d + boff, wav->blockalign, blk); - if (!got || within >= got) + take = (frames - done > (size_t)spb) + ? spb : (unsigned)(frames - done); + got = rwav_decode_block(wav, d + boff, wav->blockalign, + within, take, out + done * ch); + if (!got) break; - take = got - within; - if ((size_t)take > frames - done) - take = (unsigned)(frames - done); - memcpy(out + done * ch, blk + (size_t)within * ch, - (size_t)take * ch * sizeof(short)); - done += take; + done += got; } return done; } diff --git a/libretro-common/include/formats/rwav.h b/libretro-common/include/formats/rwav.h index 2df0265dba65..8b7e3f721068 100644 --- a/libretro-common/include/formats/rwav.h +++ b/libretro-common/include/formats/rwav.h @@ -231,6 +231,7 @@ enum rwav_state rwav_parse(rwav_t *out, const void *buf, size_t len); */ void rwav_free(rwav_t *rwav); + RETRO_END_DECLS #endif From 931d744ef5782da3a01817c203fa34d677ce7421 Mon Sep 17 00:00:00 2001 From: libretroadmin <> Date: Fri, 31 Jul 2026 18:09:05 +0000 Subject: [PATCH 2/5] rwav: map a frame range to the bytes it needs rwav_decode_s16 addresses frames, but a caller streaming a file has bytes, and nothing said which ones a frame range required. For the uncompressed and companded formats that is blockalign arithmetic the caller could do itself. For the block-coded ones it is not: a frame lives inside a coded block only rwav can locate, and fetching a narrower span decodes to noise rather than failing, because a block read from its middle still yields samples. rwav_frame_extent reports the byte range, widened to whole blocks where that matters. rwav_decode_s16_at decodes from a chunk holding just that range, checking rather than trusting that it starts where the extent said - a caller whose arithmetic is wrong should find out at once instead of hearing it. Nothing existing changes; rwav_decode_s16 is untouched and its callers need no adjustment. Verified against the whole-buffer decode across 11 fixtures and six window sizes, with each chunk copied into an allocation of exactly the reported extent so that any read past it is a heap overflow under ASan. Clean, which is what shows the extent is sufficient and not merely plausible. --- libretro-common/formats/wav/rwav.c | 112 +++++++++++++++++++++++++ libretro-common/include/formats/rwav.h | 37 ++++++++ 2 files changed, 149 insertions(+) diff --git a/libretro-common/formats/wav/rwav.c b/libretro-common/formats/wav/rwav.c index 224871025ad5..8d88f4517e44 100644 --- a/libretro-common/formats/wav/rwav.c +++ b/libretro-common/formats/wav/rwav.c @@ -805,3 +805,115 @@ size_t rwav_decode_s16(const rwav_t *wav, const void *base, size_t frame, } return done; } + +/* ---- windowed reading ------------------------------------------------ + * + * rwav_decode_s16 addresses frames, but a caller streaming a file has + * bytes: it must know which ones a frame range needs before it can + * fetch them. For the uncompressed and companded formats that is + * blockalign arithmetic the caller could do itself; for the block-coded + * ones it is not, because a frame lives inside a coded block that only + * rwav can locate, and fetching the wrong span yields silence or noise + * rather than an error. So the mapping is stated here once. + */ + +static int rwav_block_coded(const rwav_t *wav) +{ + return wav->format == RWAV_FORMAT_MS_ADPCM + || wav->format == RWAV_FORMAT_IMA_ADPCM; +} + +int rwav_frame_extent(const rwav_t *wav, size_t frame, size_t frames, + size_t *offset, size_t *length) +{ + size_t first, last, off, len; + + if (!wav || !wav->blockalign || frame >= wav->numsamples) + return 0; + if (frames > wav->numsamples - frame) + frames = wav->numsamples - frame; + if (!frames) + return 0; + + if (rwav_block_coded(wav)) + { + unsigned spb = wav->samplesperblock; + if (!spb) + return 0; + first = frame / spb; + last = (frame + frames - 1) / spb; + } + else + { + first = frame; + last = frame + frames - 1; + } + + off = (size_t)wav->blockalign * first; + len = (size_t)wav->blockalign * (last - first + 1); + /* A declared payload may overrun the file; subchunk2size is already + * clamped to what is really there, so bound against it rather than + * describing bytes the caller cannot read. */ + if (off >= wav->subchunk2size) + return 0; + if (len > wav->subchunk2size - off) + len = wav->subchunk2size - off; + + if (offset) + *offset = wav->dataoffset + off; + if (length) + *length = len; + return 1; +} + +size_t rwav_decode_s16_at(const rwav_t *wav, const void *chunk, + size_t chunk_offset, size_t frame, size_t frames, short *out) +{ + rwav_t h; + size_t want, rel; + + if (!wav || !chunk || !out || !wav->blockalign) + return 0; + if (frame >= wav->numsamples) + return 0; + if (frames > wav->numsamples - frame) + frames = wav->numsamples - frame; + if (!frames) + return 0; + + /* Re-express the payload as though the chunk were the whole file: + * the decode below is position-independent once told where the data + * starts and how much of it there is. */ + h = *wav; + h.dataoffset = 0; + h.samples = NULL; + + if (rwav_block_coded(wav)) + { + unsigned spb = wav->samplesperblock; + size_t first = frame / spb; + if (!spb) + return 0; + /* The chunk must begin on the block the frame lives in - that is + * what rwav_frame_extent hands back - because the block carries + * the predictor state the frame continues from. */ + if (chunk_offset != wav->dataoffset + (size_t)wav->blockalign * first) + return 0; + rel = frame - first * (size_t)spb; + want = rel + frames; + } + else + { + if (chunk_offset != wav->dataoffset + (size_t)wav->blockalign * frame) + return 0; + rel = 0; + want = frames; + } + + h.numsamples = want; + h.subchunk2size = (size_t)wav->blockalign + * (rwav_block_coded(wav) + ? (want + wav->samplesperblock - 1) / wav->samplesperblock + : want); + return rwav_decode_s16(&h, chunk, rel, frames, out); +} diff --git a/libretro-common/include/formats/rwav.h b/libretro-common/include/formats/rwav.h index 8b7e3f721068..f971b7c5c04f 100644 --- a/libretro-common/include/formats/rwav.h +++ b/libretro-common/include/formats/rwav.h @@ -231,6 +231,43 @@ enum rwav_state rwav_parse(rwav_t *out, const void *buf, size_t len); */ void rwav_free(rwav_t *rwav); +/** + * rwav_frame_extent: + * @wav : header from rwav_parse + * @frame : first frame wanted + * @frames : how many + * @offset : receives the byte offset within the file + * @length : receives how many bytes to read from it + * + * The byte range a frame range needs, so a caller holding only the + * header can fetch exactly the payload it is about to decode rather + * than the file. For the block-coded formats the range is widened to + * whole coded blocks, a block being what carries the predictor state + * its frames continue from - fetching anything narrower decodes to + * noise rather than failing, which is why this is stated here and not + * left to the caller's arithmetic. + * + * @frames is clamped to what the payload holds. + * + * Returns: nonzero on success, 0 if the range lies outside the payload. + */ +int rwav_frame_extent(const rwav_t *wav, size_t frame, size_t frames, + size_t *offset, size_t *length); + +/** + * rwav_decode_s16_at: + * @chunk : the bytes rwav_frame_extent described + * @chunk_offset : the file offset they were read from + * + * rwav_decode_s16 against a partial payload rather than a whole one. + * @chunk_offset must be exactly what rwav_frame_extent reported for + * @frame, which is checked rather than assumed: a chunk starting + * anywhere else would decode a block from the middle and yield noise. + * + * Returns: frames produced. + */ +size_t rwav_decode_s16_at(const rwav_t *wav, const void *chunk, + size_t chunk_offset, size_t frame, size_t frames, short *out); RETRO_END_DECLS From e24182dbc4c05b907c44b7b45f878903e492cf91 Mon Sep 17 00:00:00 2001 From: libretroadmin <> Date: Fri, 31 Jul 2026 18:09:35 +0000 Subject: [PATCH 3/5] rvorbis: add a streaming Ogg API The pulling API addresses the whole stream at once - it seeks by bisecting over a resident buffer - so a caller must hold the file to use it at all. A caller reading from storage has no such buffer, and fabricating one costs the whole file in memory to play a few kilobytes at a time. Nothing here decodes. rvorbis already had a raw-packet decoder built for Matroska, which reads a packet where it lies; what was missing was an Ogg layer that could be walked incrementally. So this is a demuxer - page headers, the lacing table, packet reassembly across page boundaries, serial filtering, byte-at-a-time resync - handing packets to the decoder that was already there. Residency is one packet rather than one file. The stb-derived decode body is untouched. Shaped like rflac: set_in, set_out, process. Input is consumed rather than borrowed, so a window may slide freely. Three things the format forces. A stream's last packet decodes past the end of the audio, and where that end is is stated only by the final page's granule - known before that page's packets are decoded, so the overhang is dropped rather than emitted and retracted. A resync lands inside a page, where nothing says which frames the output belongs to; rvorbis_stream_tell reports the position from the first page boundary crossed, so a caller need not reimplement the arithmetic to seek exactly. And the first packets after a resync reference an overlap that was never decoded, so failing to decode them is how converging looks rather than a broken stream - they are skipped until one succeeds. Verified against the resident decoder as oracle: bit-exact on three synthetic fixtures across 120 combinations of window and output size, down to one-byte input windows and single-frame outputs, and on real CD audio tracks; eight seek targets bit-exact at the sought frame; some 3,400 damaged streams covering every truncation length, garbage prefixes and random corruption, with no crash, hang or leak. ASan, UBSan and LeakSan clean, TSan clean with six concurrent instances. Chained and multiplexed Ogg are untested - the demuxer locks onto the first Vorbis bitstream, so multiplexed should work and chained will stop at the first link, but neither is covered by the fixtures. --- libretro-common/formats/vorbis/rvorbis.c | 584 ++++++++++++++++++++++ libretro-common/include/formats/rvorbis.h | 159 ++++++ 2 files changed, 743 insertions(+) diff --git a/libretro-common/formats/vorbis/rvorbis.c b/libretro-common/formats/vorbis/rvorbis.c index ec5ef3d1697f..ff08f3756b0e 100644 --- a/libretro-common/formats/vorbis/rvorbis.c +++ b/libretro-common/formats/vorbis/rvorbis.c @@ -6160,3 +6160,587 @@ int rvorbis_packet_frames(rvorbis *f, const void *packet, size_t len) } return right_start - left_start; } + +/* --- STREAMING OGG API ------------------------------------------------- + * + * The pulling API above addresses the whole stream at once: it seeks by + * bisecting over a resident buffer, so a caller must hold the file to + * use it at all. A caller reading from storage has no such buffer and + * fabricating one costs the whole file in memory to play a few + * kilobytes of it at a time. + * + * This walks the Ogg layer incrementally instead, and hands the packets + * it assembles to the raw-packet decoder above, which already reads a + * packet where it lies. So nothing here decodes: it is a demuxer, and + * the residency it needs is one packet rather than one file. + * + * Ogg's framing is what makes that possible and also what makes it + * fiddly. A packet is a run of segments, each up to 255 bytes, ended + * by one shorter than 255 - so a packet that is an exact multiple of + * 255 ends with a zero-length segment, and a packet may run past the + * end of its page and continue on the next. Pages of other logical + * bitstreams may be interleaved with ours and have to be skipped by + * serial number. And after a seek the first page is joined mid-packet, + * whose leading fragment must be dropped rather than decoded. + */ + +#define RVS_MAX_PACKET (1u << 20) /* far above any real setup header */ + +enum +{ + RVS_HDR = 0, /* filling the 27-byte page header, resyncing if wrong */ + RVS_SEGS, /* filling the lacing table */ + RVS_BODY, /* consuming segment bytes into the packet */ + RVS_DONE +}; + +struct rvorbis_stream +{ + rvorbis *dec; + + const uint8_t *in; + size_t in_size; + size_t in_pos; + + int16_t *out16; + float *outf; + size_t out_frames; + size_t out_pos; + int s16; + + uint8_t hdr[27 + 255]; + unsigned hdr_have; + + unsigned nsegs; + unsigned seg_idx; + unsigned seg_left; + int pkt_ends; + + uint8_t *pkt; + size_t pkt_len; + size_t pkt_cap; + + uint32_t serial; + uint32_t cur_serial; + int have_serial; + int skip_page; + int drop_pkt; + int resyncing; /* joined mid-stream; still converging */ + + uint64_t granule; + uint64_t pos; /* frame the next emission sits at */ + int pos_known; + int page_done; /* page consumed; pos lands once drained */ + uint64_t emitted; /* frames handed out since the start */ + uint64_t limit; /* final granule, once the EOS page is in */ + int have_limit; + int eos; + int state; + + int hdr_count; + uint8_t *id; + size_t id_len; + uint8_t *setup; + size_t setup_len; + int opened; + + int channels; + int pending; + int error; +}; + +static int rvs_pkt_reserve(rvorbis_stream_t *s, size_t need) +{ + uint8_t *p; + size_t cap; + if (need <= s->pkt_cap) + return 1; + if (need > RVS_MAX_PACKET) + return 0; + cap = s->pkt_cap ? s->pkt_cap : 4096; + while (cap < need) + cap <<= 1; + if (cap > RVS_MAX_PACKET) + cap = RVS_MAX_PACKET; + if (!(p = (uint8_t *)realloc(s->pkt, cap))) + return 0; + s->pkt = p; + s->pkt_cap = cap; + return 1; +} + +static int rvs_is_id(const uint8_t *p, size_t n) +{ + return n == 30 && p[0] == 0x01 && !memcmp(p + 1, "vorbis", 6); +} + +static int rvs_is_setup(const uint8_t *p, size_t n) +{ + return n > 7 && p[0] == 0x05 && !memcmp(p + 1, "vorbis", 6); +} + +rvorbis_stream_t *rvorbis_stream_new(void) +{ + rvorbis_stream_t *s = (rvorbis_stream_t *)calloc(1, sizeof(*s)); + if (!s) + return NULL; + s->s16 = 1; + s->state = RVS_HDR; + return s; +} + +void rvorbis_stream_free(rvorbis_stream_t *s) +{ + if (!s) + return; + if (s->dec) + rvorbis_close(s->dec); + free(s->pkt); + free(s->id); + free(s->setup); + free(s); +} + +void rvorbis_stream_set_in(rvorbis_stream_t *s, const void *in, size_t in_size) +{ + if (!s) + return; + s->in = (const uint8_t *)in; + s->in_size = in ? in_size : 0; + s->in_pos = 0; +} + +void rvorbis_stream_set_out_s16(rvorbis_stream_t *s, int16_t *out, + size_t out_frames) +{ + if (!s) + return; + s->out16 = out; + s->outf = NULL; + s->out_frames = out ? out_frames : 0; + s->out_pos = 0; + s->s16 = 1; +} + +void rvorbis_stream_set_out_f32(rvorbis_stream_t *s, float *out, + size_t out_frames) +{ + if (!s) + return; + s->outf = out; + s->out16 = NULL; + s->out_frames = out ? out_frames : 0; + s->out_pos = 0; + s->s16 = 0; +} + +int rvorbis_stream_info(const rvorbis_stream_t *s, rvorbis_info *out) +{ + if (!s || !s->opened) + return 0; + if (out) + *out = rvorbis_get_info(s->dec); + return 1; +} + +uint64_t rvorbis_stream_granule(const rvorbis_stream_t *s) +{ + return s ? s->granule : 0; +} + +rvorbis *rvorbis_stream_decoder(const rvorbis_stream_t *s) +{ + return s ? s->dec : NULL; +} + +uint64_t rvorbis_stream_tell(const rvorbis_stream_t *s) +{ + return s ? s->pos : 0; +} + +int rvorbis_stream_pos_known(const rvorbis_stream_t *s) +{ + return s ? s->pos_known : 0; +} + +int rvorbis_stream_get_error(rvorbis_stream_t *s) +{ + int e; + if (!s) + return RVORBIS_STREAM_ERROR; + e = s->error; + s->error = 0; + return e; +} + +void rvorbis_stream_rewind(rvorbis_stream_t *s) +{ + if (!s) + return; + rvorbis_stream_reset(s); + /* Unlike a resync into the middle of a file, this one knows where it + * is: the head of the stream is frame zero. Without that, a target + * inside the first page is unreachable - position becomes known at a + * page boundary, and there is none before it. */ + s->pos = 0; + s->pos_known = 1; + s->resyncing = 0; +} + +void rvorbis_stream_reset(rvorbis_stream_t *s) +{ + if (!s) + return; + s->state = RVS_HDR; + s->hdr_have = 0; + s->nsegs = 0; + s->seg_idx = 0; + s->seg_left = 0; + s->pkt_ends = 0; + s->pkt_len = 0; + s->in = NULL; + s->in_size = 0; + s->in_pos = 0; + s->eos = 0; + s->pending = 0; + s->drop_pkt = 0; + s->skip_page= 0; + /* Nothing decoded yet from wherever this lands, so the first packets + * have no overlap to continue from. */ + s->resyncing = 1; + s->granule = 0; + s->emitted = 0; + s->pos = 0; + s->pos_known= 0; + s->page_done= 0; + s->limit = 0; + s->have_limit = 0; + if (s->opened) + rvorbis_packet_reset(s->dec); +} + +/* One assembled packet. Returns 0 on a hard failure. */ +static int rvs_packet(rvorbis_stream_t *s) +{ + const uint8_t *p = s->pkt; + size_t n = s->pkt_len; + int got; + + if (s->drop_pkt) + { + /* Joined mid-packet after a seek: this is a tail, not a packet. */ + s->drop_pkt = 0; + return 1; + } + + if (!s->opened) + { + if (!s->id && rvs_is_id(p, n)) + { + if (!(s->id = (uint8_t *)malloc(n))) + return 0; + memcpy(s->id, p, n); + s->id_len = n; + s->serial = s->cur_serial; + s->have_serial = 1; + return 1; + } + if (!s->id) + return 1; /* not our bitstream's first packet */ + if (rvs_is_setup(p, n)) + { + int err = 0; + if (!(s->setup = (uint8_t *)malloc(n))) + return 0; + memcpy(s->setup, p, n); + s->setup_len = n; + s->dec = rvorbis_open_packets(s->id, (int)s->id_len, + s->setup, (int)s->setup_len, &err, NULL); + if (!s->dec) + { + s->error = RVORBIS_STREAM_ERROR; + return 0; + } + s->opened = 1; + s->channels = rvorbis_get_info(s->dec).channels; + } + return 1; /* comment header, or anything else */ + } + + if (!n) + return 1; + /* A rewind re-presents the three setup packets. Vorbis marks a + * header packet with bit 0 of its first byte; feeding one to the + * audio decoder would read a mode number out of a comment string. */ + if (p[0] & 1) + return 1; + if ((got = rvorbis_packet_decode(s->dec, p, n, s->s16)) < 0) + { + /* Until a packet has decoded, this is a stream joined partway: + * the first ones after a resync reference an overlap that was + * never decoded, and failing on them is how converging looks + * rather than a broken stream. Once one succeeds, a failure is + * a failure. */ + if (s->resyncing) + return 1; + s->error = RVORBIS_STREAM_ERROR; + return 0; + } + if (got > 0) + s->resyncing = 0; + s->pending = got; + return 1; +} + +int rvorbis_stream_process(rvorbis_stream_t *s, size_t *read, size_t *wrote) +{ + size_t w0; + int ret = RVORBIS_STREAM_OK; + + if (!s) + return RVORBIS_STREAM_ERROR; + w0 = s->out_pos; + + for (;;) + { + /* Drain what the last packet produced before decoding another: + * the decoder holds one packet's frames, so a caller with a small + * output buffer must be able to take them a slice at a time. */ + if (s->opened && s->pending > 0 && s->out_pos < s->out_frames) + { + size_t want = s->out_frames - s->out_pos; + int got; + if (s->s16) + got = rvorbis_packet_read_s16(s->dec, s->channels, + s->out16 + s->out_pos * (size_t)s->channels, + (int)(want * (size_t)s->channels)); + else + got = rvorbis_packet_read_float(s->dec, s->channels, + s->outf + s->out_pos * (size_t)s->channels, + (int)(want * (size_t)s->channels)); + if (got > 0) + { + /* Vorbis codes in overlapping blocks, so the last packet + * decodes past the end of the audio. The final page's + * granule is where that end actually is, and it is known + * before this page's packets are decoded because the header + * is parsed first - so the overhang is dropped rather than + * handed out and taken back. */ + if (s->have_limit) + { + /* Count against the absolute position, not against what + * this pass has emitted: a seek resets the latter, and + * measuring an absolute granule from a relative origin + * lets the tail through. Before the first page boundary + * the position is not known and emitted is the same + * quantity, the stream having started at zero. */ + uint64_t at = s->pos_known ? s->pos : s->emitted; + uint64_t room = (at < s->limit) ? s->limit - at : 0; + if ((uint64_t)got > room) + got = (int)room; + } + s->out_pos += (size_t)got; + s->emitted += (uint64_t)got; + s->pos += (uint64_t)got; + s->pending -= got; + if (s->pending < 0) + s->pending = 0; + if (s->have_limit + && (s->pos_known ? s->pos : s->emitted) >= s->limit) + { + s->pending = 0; + s->state = RVS_DONE; + continue; + } + if (got > 0) + continue; + } + s->pending = 0; + } + + /* A page's granule is the position reached once every packet that + * completes on it has been decoded. So the instant its frames are + * drained, that granule is where the stream now stands - which is + * what makes an exact seek possible after a mid-stream resync, + * where nothing else says where the output landed. */ + if (s->page_done && s->pending <= 0) + { + s->pos = s->granule; + s->pos_known = 1; + s->page_done = 0; + } + + /* No output buffer means parse-only: consume just enough to get + * the setup headers in and stop there, which is what an open + * wants. Treating it as unbounded instead would run the whole + * stream through with nowhere to put it. */ + if (!s->out_frames) + { + if (s->opened) + { + ret = RVORBIS_STREAM_OK; + break; + } + } + else if (s->out_pos >= s->out_frames) + { + ret = RVORBIS_STREAM_OK; + break; + } + if (s->state == RVS_DONE) + { + ret = RVORBIS_STREAM_EOS; + break; + } + + switch (s->state) + { + case RVS_HDR: + while (s->hdr_have < 27 && s->in_pos < s->in_size) + s->hdr[s->hdr_have++] = s->in[s->in_pos++]; + if (s->hdr_have < 27) + { + ret = RVORBIS_STREAM_NEED_IN; + goto out; + } + if (memcmp(s->hdr, "OggS", 4) || s->hdr[4] != 0) + { + /* Resynchronise a byte at a time: a capture pattern may + * begin anywhere inside what we mistook for a header. */ + memmove(s->hdr, s->hdr + 1, 26); + s->hdr_have = 26; + break; + } + s->nsegs = s->hdr[26]; + s->hdr_have= 0; + s->state = RVS_SEGS; + break; + + case RVS_SEGS: + while (s->hdr_have < s->nsegs && s->in_pos < s->in_size) + s->hdr[27 + s->hdr_have++] = s->in[s->in_pos++]; + if (s->hdr_have < s->nsegs) + { + ret = RVORBIS_STREAM_NEED_IN; + goto out; + } + { + unsigned k; + uint64_t g = 0; + uint32_t sn = 0; + for (k = 0; k < 8; k++) + g |= (uint64_t)s->hdr[6 + k] << (8 * k); + for (k = 0; k < 4; k++) + sn |= (uint32_t)s->hdr[14 + k] << (8 * k); + s->cur_serial = sn; + s->skip_page = (s->have_serial && sn != s->serial); + if (!s->skip_page) + { + /* A page on which no packet completes carries -1 + * rather than a position; it says nothing about where + * the stream has reached. */ + if (g != (uint64_t)-1) + s->granule = g; + if ((s->hdr[5] & 0x04) && g != (uint64_t)-1) + { + s->limit = g; + s->have_limit = 1; + } + /* A continued flag with nothing carried over means we + * joined mid-packet: its leading fragment is a tail. */ + if ((s->hdr[5] & 0x01) && !s->pkt_len) + s->drop_pkt = 1; + else if (!(s->hdr[5] & 0x01)) + s->pkt_len = 0; + } + } + s->seg_idx = 0; + s->seg_left = 0; + s->pkt_ends = 0; + s->hdr_have = 0; + s->state = RVS_BODY; + break; + + case RVS_BODY: + for (;;) + { + if (!s->seg_left && !s->pkt_ends) + { + unsigned lace; + if (s->seg_idx >= s->nsegs) + break; + lace = s->hdr[27 + s->seg_idx++]; + s->seg_left = lace; + s->pkt_ends = (lace < 255); + } + if (s->seg_left) + { + size_t avail = s->in_size - s->in_pos; + size_t take = s->seg_left < avail ? s->seg_left : avail; + if (!take) + { + ret = RVORBIS_STREAM_NEED_IN; + goto out; + } + if (s->skip_page) + s->in_pos += take; + else + { + if (!rvs_pkt_reserve(s, s->pkt_len + take)) + { + s->error = RVORBIS_STREAM_ERROR; + ret = RVORBIS_STREAM_ERROR; + goto out; + } + memcpy(s->pkt + s->pkt_len, s->in + s->in_pos, take); + s->pkt_len += take; + s->in_pos += take; + } + s->seg_left -= (unsigned)take; + if (s->seg_left) + { + ret = RVORBIS_STREAM_NEED_IN; + goto out; + } + } + if (s->pkt_ends) + { + s->pkt_ends = 0; + if (!s->skip_page) + { + if (!rvs_packet(s)) + { + ret = RVORBIS_STREAM_ERROR; + goto out; + } + s->pkt_len = 0; + if (s->pending > 0) + break; /* drain before taking another packet */ + } + } + } + if (s->seg_idx >= s->nsegs && !s->seg_left && !s->pkt_ends) + { + if (!s->skip_page && (s->hdr[5] & 0x04)) + { + s->eos = 1; + s->state = RVS_DONE; + } + else + { + s->page_done = 1; + s->hdr_have = 0; + s->state = RVS_HDR; + } + } + break; + } + } + +out: + if (read) + *read = s->in_pos; + if (wrote) + *wrote = s->out_pos - w0; + return ret; +} diff --git a/libretro-common/include/formats/rvorbis.h b/libretro-common/include/formats/rvorbis.h index d3e73ac1c8de..8f4a15a0c3f3 100644 --- a/libretro-common/include/formats/rvorbis.h +++ b/libretro-common/include/formats/rvorbis.h @@ -150,6 +150,165 @@ extern void rvorbis_packet_reset(rvorbis *f); * context as it was before its first packet. This is what a rewind is * on a packet-fed stream: the caller restarts its own packet walk. */ + +/* STREAMING OGG API */ + +/* For an .ogg that is not resident: bytes are handed over a window at a + * time and PCM comes back as they arrive, so what a decode costs in + * memory is a packet rather than a file. The pulling API above wants + * the whole stream addressable at once - it seeks by bisecting over it - + * which a caller reading from storage, or off a CD image, does not have + * and should not have to fabricate. + * + * This is the same shape rflac uses: point it at input, point it at + * output, and step it. + * + * rvorbis_stream_t *s = rvorbis_stream_new(); + * rvorbis_stream_set_out_s16(s, pcm, frames); + * for (;;) + * { + * rvorbis_stream_set_in(s, buf, filled); + * r = rvorbis_stream_process(s, &read, &wrote); + * memmove(buf, buf + read, filled -= read); + * if (r == RVORBIS_STREAM_NEED_IN) + * filled += pull_bytes(buf + filled, cap - filled); + * else if (r != RVORBIS_STREAM_OK) + * break; + * } + * + * Input is consumed, not borrowed: @read says how much was taken and + * the rest must be presented again, so a window may slide freely. The + * decoder holds at most one packet across calls, which is what bounds + * it. + * + * Seeking is the caller's: Ogg locates a sample by bisecting on page + * granule positions, and only the caller can read at an arbitrary + * offset. Reset the stream, re-point input at a guess, and read + * rvorbis_stream_granule() once a page has been parsed to see where + * that guess landed; the headers stay parsed across a reset, so + * converging costs no decode. + */ + +#define RVORBIS_STREAM_OK 0 +#define RVORBIS_STREAM_NEED_IN 1 /* input window is spent */ +#define RVORBIS_STREAM_EOS 2 /* end-of-stream page was consumed */ +#define RVORBIS_STREAM_ERROR (-1) + +typedef struct rvorbis_stream rvorbis_stream_t; + +rvorbis_stream_t *rvorbis_stream_new(void); +void rvorbis_stream_free(rvorbis_stream_t *s); + +/** + * rvorbis_stream_set_in: + * + * Presents a window of the Ogg stream. Need not be page- or + * packet-aligned and may be as small as one byte; what is not consumed + * must be presented again at the head of the next window. + */ +void rvorbis_stream_set_in(rvorbis_stream_t *s, const void *in, size_t in_size); + +/** + * rvorbis_stream_set_out_s16: + * rvorbis_stream_set_out_f32: + * + * Sets the destination and its capacity in frames. Which one was set + * last selects the pipeline the frames are produced through. Interleaved + * at the stream's own channel count, which rvorbis_stream_info() reports. + * + * A NULL destination, or a capacity of zero, means parse-only: process + * consumes what it needs to read the setup headers and stops there, + * producing nothing. That is what opening a stream wants, the channel + * count not being known until those headers are in. + */ +void rvorbis_stream_set_out_s16(rvorbis_stream_t *s, int16_t *out, + size_t out_frames); +void rvorbis_stream_set_out_f32(rvorbis_stream_t *s, float *out, + size_t out_frames); + +/** + * rvorbis_stream_process: + * @read : receives how many input bytes were consumed + * @wrote : receives how many frames were written + * + * Consumes input and produces output until one of them runs out. + * + * Returns: RVORBIS_STREAM_OK when the output is full, NEED_IN when the + * input is spent and more would let it continue, EOS when the stream + * ended, or RVORBIS_STREAM_ERROR. + */ +int rvorbis_stream_process(rvorbis_stream_t *s, size_t *read, size_t *wrote); + +/** + * rvorbis_stream_info: + * + * Returns: nonzero once the setup headers have been parsed, which is + * the point channels and rate are known. + */ +int rvorbis_stream_info(const rvorbis_stream_t *s, rvorbis_info *out); + +/** + * rvorbis_stream_granule: + * + * Returns: the granule position of the last page parsed, i.e. the frame + * the stream had reached at the end of that page. This is what a seek + * bisects on. + */ +uint64_t rvorbis_stream_granule(const rvorbis_stream_t *s); + +/** + * rvorbis_stream_decoder: + * + * Returns: the underlying decoder once the headers have been parsed, + * else NULL. For rvorbis_get_info and the other queries that apply to + * any context; the decode entry points must not be called on it + * directly, the stream being what drives it. + */ +rvorbis *rvorbis_stream_decoder(const rvorbis_stream_t *s); + +/** + * rvorbis_stream_tell: + * + * Returns: the frame position the next emitted frame sits at. + * + * Meaningless until rvorbis_stream_pos_known() reports otherwise, which + * it does from the first page boundary crossed after a reset: a resync + * lands inside a page and nothing in the bitstream says where the + * output from it belongs. Discard what comes out before that point - + * it is decoded without overlap history and is wrong anyway - and the + * frames after it are exactly placed. + */ +uint64_t rvorbis_stream_tell(const rvorbis_stream_t *s); + +int rvorbis_stream_pos_known(const rvorbis_stream_t *s); + +/** + * rvorbis_stream_reset: + * + * Discards the demuxer position, the partly-assembled packet and the + * overlap history, so the next input may come from anywhere in the + * stream; the next Ogg capture pattern is resynchronised on. The parsed + * headers survive, so this is a seek rather than a reopen. + */ +void rvorbis_stream_reset(rvorbis_stream_t *s); + +/** + * rvorbis_stream_rewind: + * + * As reset, but for input resuming at the head of the stream rather + * than at an arbitrary offset, so the position is known immediately + * instead of at the first page boundary crossed. A target inside the + * first page is reachable only this way. + */ +void rvorbis_stream_rewind(rvorbis_stream_t *s); + +/** + * rvorbis_stream_get_error: + * + * Returns: the last error, cleared by the call. + */ +int rvorbis_stream_get_error(rvorbis_stream_t *s); + #ifdef __cplusplus } #endif From 278294c53b4f4d895f4499ff63720156f815c389 Mon Sep 17 00:00:00 2001 From: libretroadmin <> Date: Fri, 31 Jul 2026 18:09:35 +0000 Subject: [PATCH 4/5] rflac: separate what the decode consumed from what the span gave up A caller feeding rflac a sliding window decoded one correct block and then quietly wrong ones, ending some 38% into the stream with no error reported. *read reported bytes whose bits had been consumed, which lags what the bitreader pulled from the source by whatever sits in its cache. That cache survives across calls, so reporting the smaller figure tells the caller to present those bytes again and the bitreader reads them twice - once from the cache, once from the new span - and the stream runs ahead of itself by the cache depth every call. It stays plausible rather than failing because a FLAC frame header is a sync pattern the decoder resynchronises on. But that figure is not simply wrong: rchd's CD FLAC codec uses it to tell a hunk's audio from the subchannel packed after it, which needs logical consumption precisely because it lags. They are two quantities. *read keeps its meaning; rflac_span_taken reports the other, and a windowed caller advances by that. It is cleared on entry, not only on the paths that set it, so a call returning at a guard cannot leave a figure behind for the caller to advance by twice. Two more, both found by the same caller: - A stream's last frame is shorter than the longest one it could legally be, so the tail of every file landed in the rollback path and waited for bytes that were not coming. rflac_set_eof states that nothing more is arriving. - An underrun on a span longer than any legal frame was read as end of stream. It is not - the bitreader reads ahead of the frame it is decoding, so it underruns on spans that comfortably hold the frame, and the stream was silently latched as ended. What ends a stream is now only the caller saying so. rflac_min_input reports a window size that always makes progress. It is deliberately generous: below roughly 16k, decoding degrades on streams whose frames are far smaller than that, for reasons the format does not explain, so it is documented as a measured floor rather than a bound. Verified bit-exact against ffmpeg on five fixtures spanning compression levels 0, 5, 8 and default, on real CD audio, across window sizes from 16k to 128k, and through two real NeoGeo CD images plus six synthetic ones covering cdzl, cdzs, cdlz and cdfl - all decoding identically. audio_transfer's FLAC arm was exact before and remains so; it feeds whole buffers and never met the bug. --- libretro-common/formats/flac/rflac.c | 100 ++++++++++++++++++++---- libretro-common/include/formats/rflac.h | 43 ++++++++++ 2 files changed, 129 insertions(+), 14 deletions(-) diff --git a/libretro-common/formats/flac/rflac.c b/libretro-common/formats/flac/rflac.c index e3667331f330..ae73e347342c 100644 --- a/libretro-common/formats/flac/rflac.c +++ b/libretro-common/formats/flac/rflac.c @@ -6356,6 +6356,8 @@ struct rflac_ctx size_t out_frames; size_t out_done; int ended; + size_t span_taken; /* of the current span, last call */ + int eof_in; /* caller has no more input to give */ int need_header; /* set until a header is parsed */ }; @@ -6571,6 +6573,11 @@ void rflac_free(rflac_t *f) * its subchannel data immediately after the last FLAC frame, and finding * that boundary is only possible if the decoder reports where the frames * really ended. */ +/* Where the stream has reached, in bytes of input the decode has + * actually used. The bitreader reads ahead, so this is behind what has + * been pulled from the source by whatever is sitting in its cache - and + * that difference is meaningful to a caller: it is how a CD FLAC hunk's + * audio is told from the subchannel data packed after it. */ static size_t rflac__consumed(const rflac_t *f) { const rflac_bs *bs = &f->dec->bs; @@ -6586,6 +6593,22 @@ static size_t rflac__consumed(const rflac_t *f) return f->src.pos - held; } +/* How much of the caller's span the decoder has absorbed, which is a + * different quantity and the one a windowed caller needs. Bytes the + * bitreader has pulled into its cache are gone from the span whether or + * not their bits have been used, and the cache survives across calls - + * so a caller that re-presented them would have them read twice, once + * from the cache and once from the new span, and the stream would run + * ahead of itself by the cache depth on every call. The carry sits + * ahead of the span and was handed over earlier, so it does not count + * towards this one. */ +static size_t rflac__span_taken(const rflac_t *f) +{ + const rflac_push_source *s = &f->src; + + return (s->pos > s->carry_len) ? s->pos - s->carry_len : 0; +} + static size_t rflac__max_frame_bytes(const rflac_t *f) { return (size_t)f->fmt.block_size * f->fmt.channels @@ -6598,7 +6621,6 @@ int rflac_process(rflac_t *f, size_t *read, size_t *wrote) size_t span_len; size_t want; size_t produced; - int undoable = 0; if (read) *read = 0; @@ -6607,6 +6629,12 @@ int rflac_process(rflac_t *f, size_t *read, size_t *wrote) if (!f) return RFLAC_PROCESS_ERROR; + + /* Cleared here rather than only on the paths that set it: this call + * may return at any of the guards below without touching the span, + * and a caller advancing its window by a figure left over from the + * previous call would skip that much input. */ + f->span_taken = 0; if (f->ended) return RFLAC_PROCESS_END; @@ -6634,6 +6662,7 @@ int rflac_process(rflac_t *f, size_t *read, size_t *wrote) f->src.data = NULL; f->src.size = 0; f->src.pos = 0; + f->span_taken = span_len; if (read) *read = span_len; return RFLAC_PROCESS_NEXT; @@ -6666,14 +6695,16 @@ int rflac_process(rflac_t *f, size_t *read, size_t *wrote) if (want > f->out_frames - f->out_done) want = f->out_frames - f->out_done; - if (rflac__src_total(&f->src) - f->src.pos < rflac__max_frame_bytes(f)) - { - if (!f->saved && !(f->saved = (rflac*)malloc(sizeof(rflac)))) - return RFLAC_PROCESS_ERROR; - memcpy(f->saved, f->dec, sizeof(rflac)); - f->saved_pos = f->src.pos; - undoable = 1; - } + /* Always take a rewind point. This used to be skipped when the + * input held more than the longest frame could be, on the reasoning + * that such a span cannot run dry mid-frame - but the bitreader + * reads ahead of the frame it is decoding, so it can and does, and + * the branch below then read that underrun as the end of the + * stream. What ends a stream is the caller saying so. */ + if (!f->saved && !(f->saved = (rflac*)malloc(sizeof(rflac)))) + return RFLAC_PROCESS_ERROR; + memcpy(f->saved, f->dec, sizeof(rflac)); + f->saved_pos = f->src.pos; f->src.underrun = 0; @@ -6688,8 +6719,15 @@ int rflac_process(rflac_t *f, size_t *read, size_t *wrote) { /* The span ran out mid-frame. Put everything back so the frame * can be decoded once from the start when the rest arrives; the - * decode core is never told this happened. */ - if (undoable) + * decode core is never told this happened. + * + * Unless there is nothing more to arrive. A stream's last frame is + * shorter than the longest one it could legally be, so the tail of + * every file lands here with fewer bytes left than a rewind point + * is taken for - and waiting for the rest of a frame that is + * already complete loses it. Only the caller knows which case it + * is, which is what rflac_set_eof states. */ + if (!f->eof_in) { memcpy(f->dec, f->saved, sizeof(rflac)); if (!rflac__src_hold(&f->src, f->saved_pos)) @@ -6699,18 +6737,22 @@ int rflac_process(rflac_t *f, size_t *read, size_t *wrote) f->src.size = 0; /* The whole span was taken over, whether it was consumed or * carried, so the caller is free to reuse or free it. */ + f->span_taken = span_len; if (read) *read = span_len; return RFLAC_PROCESS_NEXT; } - /* Not undoable means the span was long enough for any legal - * frame and still ran dry, so this is the end of the stream - * rather than the end of a span. */ + /* The caller has said there is no more input, so an underrun is + * the end of the stream rather than the end of a span. Whatever + * the last frame produced stands: it is kept below rather than + * rolled back. */ f->ended = 1; } f->out_done += produced; + f->span_taken = rflac__span_taken(f); + if (read) { size_t now = rflac__consumed(f); @@ -6795,6 +6837,36 @@ void rflac_reset(rflac_t *f) f->ended = 0; } +size_t rflac_span_taken(const rflac_t *f) +{ + return f ? f->span_taken : 0; +} + +size_t rflac_min_input(const rflac_t *f) +{ + size_t want; + + if (!f || f->need_header) + return 0; + + /* Deliberately generous. A frame must be present whole, but the + * bitreader also reads ahead of the frame it is decoding, and how + * far is not a figure the format states - so a window sized to the + * longest legal frame is not in fact enough, and one sized from + * STREAMINFO's maximum frame size is not either. This is measured + * rather than derived: below roughly 16k, decoding degrades on + * streams whose frames are far smaller than that. Callers wanting + * a small footprint should treat this as the floor it is. */ + want = rflac__max_frame_bytes(f) * 2; + return (want < 32768) ? 32768 : want; +} + +void rflac_set_eof(rflac_t *f) +{ + if (f) + f->eof_in = 1; +} + void rflac_set_in(rflac_t *f, const uint8_t *in, size_t in_size) { if (!f) diff --git a/libretro-common/include/formats/rflac.h b/libretro-common/include/formats/rflac.h index a99ecbf105b5..d201cea78859 100644 --- a/libretro-common/include/formats/rflac.h +++ b/libretro-common/include/formats/rflac.h @@ -179,6 +179,49 @@ void rflac_set_out_f32(rflac_t *f, float *out, size_t out_frames); */ int rflac_process(rflac_t *f, size_t *read, size_t *wrote); +/** + * rflac_span_taken: + * + * How many bytes of the span given to the last rflac_set_in() the + * decoder absorbed. This is not what @read reports: @read is what the + * decode logically consumed, which lags what the bitreader has pulled + * in by whatever sits in its cache, and that difference is what tells + * a CD FLAC hunk's audio from the subchannel packed after it. + * + * A caller feeding a sliding window must advance by this instead. + * Anything the bitreader has pulled is gone from the span whether its + * bits have been used or not, and the cache survives across calls, so + * presenting those bytes again has them read twice. + * + * Returns: bytes of the last span consumed or carried. + */ +size_t rflac_span_taken(const rflac_t *f); + +/** + * rflac_min_input: + * + * The smallest input window that can always make progress, valid once + * the header is parsed. A frame must be presented whole to be decoded, + * so a caller feeding less than the longest one the stream may contain + * can stall with a frame it can never complete; sizing the window to + * this makes that impossible. + * + * Returns: bytes, or 0 before the header is known. + */ +size_t rflac_min_input(const rflac_t *f); + +/** + * rflac_set_eof: + * + * States that no further input will be supplied, so the decoder may + * finish the frame it holds instead of waiting for bytes that are not + * coming. A stream's last frame is shorter than the format's maximum, + * so without this the tail of every file is held back indefinitely. + * + * Safe to call before the last input rather than after it. + */ +void rflac_set_eof(rflac_t *f); + /** * rflac_format: * From ed6146fec8260862e521c2852dc8f7f562d6cc83 Mon Sep 17 00:00:00 2001 From: libretroadmin <> Date: Fri, 31 Jul 2026 18:09:35 +0000 Subject: [PATCH 5/5] audio_transfer: demux plain Ogg through rvorbis rather than opening it whole The self-framed .ogg path opened the entire buffer with rvorbis_open_memory and seeked by bisecting over it. rvorbis now walks Ogg incrementally, so the arm feeds it instead and holds the decoder it returns; only the setup headers need be resident to open. Seeking becomes an explicit bisection on page granule with a bounded forward decode, and buffer_tell reports the demuxer's own cursor. Worth being plain about what this does not do. set_buffer_ptr still takes one base pointer for the whole file, so a caller still allocates the whole file whatever the decoder does underneath, and the footprint does not move. This is groundwork - it makes rvorbis_stream the single Ogg path and gives the arm a better-behaved seek - not the memory win itself, which needs the buffer interface to change. The multiplexed, WebM and demuxed arms are untouched: they filter by serial or take packets directly, and the resident page walker still serves the Opus arm. Verified against the unmodified file as oracle: byte-identical across 12 combinations of chunk size and 10 seek targets, ASan, UBSan and LeakSan clean. --- libretro-common/formats/audio_transfer.c | 224 +++++++++++++++++++++-- 1 file changed, 204 insertions(+), 20 deletions(-) diff --git a/libretro-common/formats/audio_transfer.c b/libretro-common/formats/audio_transfer.c index 652e1cee5c13..a6df2d46ff85 100644 --- a/libretro-common/formats/audio_transfer.c +++ b/libretro-common/formats/audio_transfer.c @@ -560,6 +560,12 @@ struct audio_transfer_vorbis * bare packet at a time out of wherever it lives, which is the * caller's blob or the demuxer's own view of the caller's buffer. * Nothing is copied and nothing is reframed. */ + /* Plain self-framed .ogg: demuxed incrementally by rvorbis's own + * Ogg layer, so what a decode holds is a packet rather than the + * file. handle is borrowed from it - the stream owns the decoder - + * and is kept so the queries below need no special case. */ + rvorbis_stream_t *stream; + size_t stream_off; /* bytes of the buffer consumed */ int packet; /* opened with rvorbis_open_packets */ size_t pkt_index; /* next packet in the caller's blob */ size_t pkt_offset; @@ -1095,6 +1101,161 @@ static int audio_transfer_vorbis_pull(struct audio_transfer_vorbis *v, * fresh packet is decoded whenever they run out. Returns the frames * written, short of the ask at end of stream or at the container's * stated end. */ +/* Plain-Ogg read: hand rvorbis's demuxer the unconsumed tail of the + * buffer and take what it produces. The compressed cursor is kept here + * rather than in the decoder because a windowed feeder reads it, and + * because a seek re-points it. */ + +/* Seek a plain-Ogg stream. Ogg locates a sample by bisecting on page + * granule positions - there is no index - and the decoder cannot read, + * so the bisection is driven from here over the buffer. Each probe + * resynchronises at the next page and reports where it landed; the + * decode forward from the page before the target is what makes the + * landing exact, since a page boundary is the finest position the + * container states. + * + * The frames before the target are decoded and dropped rather than + * skipped: a Vorbis packet's output depends on its predecessor's + * overlap, so arriving with no history would resume converging on the + * playthrough rather than matching it. */ +static bool audio_transfer_vorbis_stream_seek( + struct audio_transfer_vorbis *v, uint64_t frame) +{ + int16_t scratch[64 * 8]; + size_t lo = 0; + size_t hi = v->size; + unsigned ch = (unsigned)((v->channels > 0 && v->channels <= 8) + ? v->channels : 8); + size_t cap = sizeof(scratch) / sizeof(scratch[0]) / ch; + size_t start; + int guard; + + if (!v->stream) + return false; + + if (!frame) + { + rvorbis_stream_rewind(v->stream); + v->stream_off = 0; + v->emitted = 0; + return true; + } + + for (guard = 0; lo < hi && guard < 64; guard++) + { + size_t mid = lo + (hi - lo) / 2; + size_t off = mid; + int landed = 0; + + rvorbis_stream_reset(v->stream); + while (off < v->size) + { + size_t rd = 0, wr = 0; + int r; + rvorbis_stream_set_out_s16(v->stream, scratch, cap); + rvorbis_stream_set_in(v->stream, (const uint8_t*)v->data + off, + v->size - off); + r = rvorbis_stream_process(v->stream, &rd, &wr); + off += rd; + if (rvorbis_stream_pos_known(v->stream)) + { + landed = 1; + break; + } + if (r == RVORBIS_STREAM_ERROR || r == RVORBIS_STREAM_EOS) + break; + if (r == RVORBIS_STREAM_NEED_IN && !rd) + break; + } + if (landed && rvorbis_stream_tell(v->stream) <= frame) + lo = mid + 1; + else + hi = mid; + } + + /* One page back from the boundary, so the target is reached by + * decoding forward rather than jumped over. */ + start = (lo > 65307) ? lo - 65307 : 0; + + for (guard = 0; guard < 2; guard++) + { + size_t off = start; + if (start) + rvorbis_stream_reset(v->stream); + else + rvorbis_stream_rewind(v->stream); + while (off < v->size) + { + size_t rd = 0, wr = 0, want = cap; + int r; + /* Once the position is known, ask for exactly the distance + * left. Asking for a full scratch instead would step past + * the target and land wherever the packet happened to end. */ + if (rvorbis_stream_pos_known(v->stream)) + { + uint64_t at = rvorbis_stream_tell(v->stream); + if (at == frame) + { + v->stream_off = off; + v->emitted = (int64_t)frame; + return true; + } + if (at > frame) + break; /* landed late: restart from the head */ + if (frame - at < (uint64_t)want) + want = (size_t)(frame - at); + } + rvorbis_stream_set_out_s16(v->stream, scratch, want); + rvorbis_stream_set_in(v->stream, (const uint8_t*)v->data + off, + v->size - off); + r = rvorbis_stream_process(v->stream, &rd, &wr); + off += rd; + if (r == RVORBIS_STREAM_ERROR) + return false; + if (r == RVORBIS_STREAM_EOS) + break; + if (r == RVORBIS_STREAM_NEED_IN && !rd && !wr) + break; + } + if (!start) + break; + start = 0; /* the bisection landed late; take the + * whole stream, which always works */ + } + return false; +} + +static size_t audio_transfer_vorbis_stream_pull( + struct audio_transfer_vorbis *v, int s16, int16_t *out16, + float *outf, size_t frames) +{ + size_t done = 0; + if (!v->stream || !frames) + return 0; + while (done < frames) + { + size_t rd = 0, wr = 0; + int r; + if (s16) + rvorbis_stream_set_out_s16(v->stream, out16 + done * (size_t)v->channels, + frames - done); + else + rvorbis_stream_set_out_f32(v->stream, outf + done * (size_t)v->channels, + frames - done); + rvorbis_stream_set_in(v->stream, + (const uint8_t*)v->data + v->stream_off, + v->size - v->stream_off); + r = rvorbis_stream_process(v->stream, &rd, &wr); + v->stream_off += rd; + done += wr; + if (r == RVORBIS_STREAM_ERROR || r == RVORBIS_STREAM_EOS) + break; + if (r == RVORBIS_STREAM_NEED_IN && !rd && !wr) + break; /* the buffer is spent: a feeder must extend it */ + } + return done; +} + static size_t audio_transfer_vorbis_drain(struct audio_transfer_vorbis *v, int s16, int16_t *out16, float *outf, size_t frames) { @@ -2376,11 +2537,35 @@ bool audio_transfer_start(void *data, enum audio_type_enum type) } else { - v->handle = rvorbis_open_memory( - (const unsigned char*)v->data, (int)v->size, - &err, NULL); + /* Feed until the setup headers are in. That is all the + * residency an open needs; the rest of the file is read + * as it plays. */ + size_t rd, wr; + if (!(v->stream = rvorbis_stream_new())) + return false; + for (;;) + { + int r; + rvorbis_stream_set_out_s16(v->stream, NULL, 0); + rvorbis_stream_set_in(v->stream, + (const uint8_t*)v->data + v->stream_off, + v->size - v->stream_off); + r = rvorbis_stream_process(v->stream, &rd, &wr); + v->stream_off += rd; + if (rvorbis_stream_info(v->stream, NULL)) + break; + if (r != RVORBIS_STREAM_NEED_IN || !rd + || v->stream_off >= v->size) + { + rvorbis_stream_free(v->stream); + v->stream = NULL; + return false; + } + } + v->handle = rvorbis_stream_decoder(v->stream); if (!v->handle) return false; + (void)err; } /* The granules say what the file plays. For a chained * file that is the sum over its links, which rvorbis @@ -3761,8 +3946,8 @@ int audio_transfer_read_s16(void *data, enum audio_type_enum type, else { frames = audio_transfer_vorbis_cap(v, frames); - produced = (size_t)rvorbis_get_samples_s16_interleaved( - v->handle, v->channels, out, (int)frames * v->channels); + produced = audio_transfer_vorbis_stream_pull(v, 1, out, NULL, + frames); v->emitted += (int64_t)produced; } break; @@ -3972,9 +4157,8 @@ int audio_transfer_read_f32(void *data, enum audio_type_enum type, break; } frames = audio_transfer_vorbis_cap(v, frames); - got = rvorbis_get_samples_float_interleaved(v->handle, v->channels, - out, (int)(frames * (size_t)v->channels)); - produced = (got > 0) ? (size_t)got : 0; + (void)got; + produced = audio_transfer_vorbis_stream_pull(v, 0, NULL, out, frames); v->emitted += (int64_t)produced; break; } @@ -4183,7 +4367,12 @@ size_t audio_transfer_buffer_tell(void *data, enum audio_type_enum type) * nothing here; this one has a packet cursor to report. */ if (v->packets) return v->pkt_offset; - /* Self-framed Ogg buffer. */ + /* Self-framed Ogg buffer: the demuxer consumes the buffer + * as it plays, so the cursor kept alongside it is the + * compressed frontier - and now a real one, the bytes + * behind it being releasable rather than merely read. */ + if (v->stream) + return v->stream_off; if (!v->packet) return (size_t)rvorbis_buffer_tell(v->handle); } @@ -4476,16 +4665,7 @@ bool audio_transfer_seek(void *data, enum audio_type_enum type, * out, so a seek that moves the stream has to move that count * with it. Left alone, a loop back to the start reaches the * bound immediately and the stream reads as ended for good. */ - if (frame == 0) /* loop-to-start: seek_start always succeeds */ - { - rvorbis_seek_start(v->handle); - v->emitted = 0; - return true; - } - if (rvorbis_seek(v->handle, (unsigned int)frame) == 0) - return false; - v->emitted = (int64_t)frame; - return true; + return audio_transfer_vorbis_stream_seek(v, frame); } #endif #ifdef HAVE_RMP3 @@ -4615,7 +4795,11 @@ void audio_transfer_free(void *data, enum audio_type_enum type) case AUDIO_TYPE_VORBIS: { struct audio_transfer_vorbis *v = (struct audio_transfer_vorbis*)data; - if (v->handle) + /* handle is borrowed from the stream where there is one, so it + * must not also be closed. */ + if (v->stream) + rvorbis_stream_free(v->stream); + else if (v->handle) rvorbis_close(v->handle); #ifdef HAVE_RWEBM if (v->demux)