Skip to content

rmp3: add a streaming API - #19317

Merged
LibretroAdmin merged 1 commit into
masterfrom
neocd-backport
Jul 31, 2026
Merged

rmp3: add a streaming API#19317
LibretroAdmin merged 1 commit into
masterfrom
neocd-backport

Conversation

@LibretroAdmin

Copy link
Copy Markdown
Contributor

rmp3_init_memory takes a borrowed pointer to the whole file and seeks by re-scanning it from the front, so a caller must hold the entire MP3 to decode any of it. A caller reading from storage has no such buffer, and building one costs the file in memory to play a few kilobytes at a time.

MPEG audio needs far less. A frame is self-contained apart from the bit reservoir, which the decoder already carries between frames, so what a decode needs resident is one frame - a couple of kilobytes at the extremes of the format. What was missing was a way to hand frames over as they arrive rather than pointing at all of them at once.

Nothing here decodes. rmp3dec_decode_frame already took a buffer and a length and told the caller what it consumed; this wraps it with the window bookkeeping that turns that into a stream. Same shape as rvorbis and rflac: set_in, set_out, process. Input is consumed rather than borrowed, so a window may slide freely, and a frame split across two windows is reassembled internally rather than asking the caller for an alignment it has no way to guarantee.

Three things the format forces.

Handed fewer bytes than a frame occupies, the frame finder reports what it scanned as bytes to skip: it cannot tell the head of a frame that has not all arrived from junk, and acting on that discards the frame. So a partly-filled hold is never decoded - which leaves the tail of a stream waiting for a window that will never fill, and rmp3_stream_set_eof is what says the short tail is all there is.

A stream states no length. Short of a Xing header the only way to know is to walk the frames, so setting no output does exactly that: frames are located and counted, nothing is decoded. In that mode process() returns per frame, so rmp3_stream_frame_offset names the frame just counted and a caller can build a seek index as it walks.

And not every frame decodes to samples. Layer III puts part of a frame's data in the frames before it, so a decoder that joined the stream partway meets frames whose data begins before where it started, and those produce nothing. How many is a property of how the encoder spent its bits - one frame in some places, three in others - so a caller resuming at a known frame cannot assume its position advances one frame per emission. rmp3_stream_frames_in counts frames consumed whether they emitted or not, which is what states where the stream actually stands. A located frame fills in the header fields whether or not it went on to produce samples, and junk merely scanned past leaves them clear; that is what separates the two.

Verified against the resident decoder as oracle on 19 real CD audio tracks: bit-exact at input windows from 512 bytes up, and at output chunk sizes from 4 bytes to 256KB. Stream length agrees with an independent frame walk of the same files. ASan, UBSan and LeakSan clean.

Guidelines

  1. Rebase before opening a pull request
  2. If you are sending several unrelated fixes or features, use a branch and a separate pull request for each
  3. If possible try squashing everything in a single commit. This is particularly beneficial in the case of feature merges since it allows easy bisecting when a problem arises
  4. RetroArch codebase follows C89 coding rules for portability across many old platforms check using C89_BUILD=1

Description

[Description of the pull request, detail any issues you are fixing or any features you are implementing]

Related Issues

[Any issues this pull request may be addressing]

Related Pull Requests

[Any other PRs from related repositories that might be needed for this pull request to work]

Reviewers

[If possible @mention all the people that should review your pull request]

rmp3_init_memory takes a borrowed pointer to the whole file and seeks by
re-scanning it from the front, so a caller must hold the entire MP3 to
decode any of it. A caller reading from storage has no such buffer, and
building one costs the file in memory to play a few kilobytes at a time.

MPEG audio needs far less. A frame is self-contained apart from the bit
reservoir, which the decoder already carries between frames, so what a
decode needs resident is one frame - a couple of kilobytes at the
extremes of the format. What was missing was a way to hand frames over
as they arrive rather than pointing at all of them at once.

Nothing here decodes. rmp3dec_decode_frame already took a buffer and a
length and told the caller what it consumed; this wraps it with the
window bookkeeping that turns that into a stream. Same shape as rvorbis
and rflac: set_in, set_out, process. Input is consumed rather than
borrowed, so a window may slide freely, and a frame split across two
windows is reassembled internally rather than asking the caller for an
alignment it has no way to guarantee.

Three things the format forces.

Handed fewer bytes than a frame occupies, the frame finder reports what
it scanned as bytes to skip: it cannot tell the head of a frame that has
not all arrived from junk, and acting on that discards the frame. So a
partly-filled hold is never decoded - which leaves the tail of a stream
waiting for a window that will never fill, and rmp3_stream_set_eof is
what says the short tail is all there is.

A stream states no length. Short of a Xing header the only way to know
is to walk the frames, so setting no output does exactly that: frames
are located and counted, nothing is decoded. In that mode process()
returns per frame, so rmp3_stream_frame_offset names the frame just
counted and a caller can build a seek index as it walks.

And not every frame decodes to samples. Layer III puts part of a frame's
data in the frames before it, so a decoder that joined the stream partway
meets frames whose data begins before where it started, and those
produce nothing. How many is a property of how the encoder spent its
bits - one frame in some places, three in others - so a caller resuming
at a known frame cannot assume its position advances one frame per
emission. rmp3_stream_frames_in counts frames consumed whether they
emitted or not, which is what states where the stream actually stands.
A located frame fills in the header fields whether or not it went on to
produce samples, and junk merely scanned past leaves them clear; that is
what separates the two.

Verified against the resident decoder as oracle on 19 real CD audio
tracks: bit-exact at input windows from 512 bytes up, and at output
chunk sizes from 4 bytes to 256KB. Stream length agrees with an
independent frame walk of the same files. ASan, UBSan and LeakSan clean.
@LibretroAdmin
LibretroAdmin merged commit 82635b2 into master Jul 31, 2026
86 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant