libretro-common: rmp3_stream f32 output; audio_transfer MP3 arm moved onto the stream - #19318
Merged
Merged
Conversation
The stream decoded to s16 only, which left a caller serving a float pipeline - audio_transfer's f32 read, and through it the mixer's float voices - unable to move off the resident pull API without a quantisation it never had before. The decoder core already runs both pipelines; what the stream lacked was a way to select one. rmp3_stream_set_out_f32 is that, shaped like its s16 partner, and selecting either converts the decoder's persistent filter state and any undrained frame in place - the same operation the pull API's reads perform, now factored into shared helpers (rmp3d_convert_state, rmp3d_convert_frames) rather than restated. The frame buffer becomes the same s16/f32 union the pull API holds, named (rmp3_frame_buf) so both sides convert through one function with union-member access and no aliasing games. A parse-only call selects nothing: a walk in the middle of a decode must not disturb the pipeline the decode is in. And rmp3_stream_info's doc now says what the code always did - a located frame is what fills the channel count and rate in, so a parse-only walk answers it without anything having been decoded. A mid-stream channel change now adapts instead of erroring: mono duplicated, stereo averaged into the stream's own layout, the same arithmetic the pull API's reads apply, and a rate change decodes through at the stream's stated rate, which is all the pull API ever did with one. The stream refusing what the pull API played would have turned concatenated-rip joins from a wrong-but-playing tail into a truncation, and the caller this API exists to migrate plays such files today. Verified bit-exact against the pull API as oracle on seven fixtures (LAME CBR 128/320, VBR -q0, MPEG-2 mono 22.05k, Layer II, and stereo+mono / 44.1k+22.05k concatenations): f32 and s16 each across 25 combinations of input window (512 bytes to whole file) and output chunk (4 frames to 256K), and 32-segment alternating s16/f32 sequences at frame counts that land the switches mid-frame with samples pending. ASan, UBSan and LeakSan clean; TSan clean with eight concurrent streams in mixed and switching modes.
The last arm holding a whole-file decoder over its buffer. rmp3's pull API borrows the pointer and cursors over all of it, so the frontier buffer_tell reported (readPos) named bytes merely read, not bytes releasable, and windowing an MP3 the way the Ogg arm windows a Vorbis file had nothing to stand on. The stream consumes its input - what is behind the cursor is genuinely done with, a frame possibly straddling the boundary held in the stream's own 4 KB reassembly hold - so the cursor kept here is a real frontier, leading the decode position by at most that hold, the safe side for a feeder. Open walks the stream parse-only to the first frame, which is where the channel count and rate come from and decodes nothing, then rewinds for the reads; the Xing/LAME/VBRI parse and the header-walk length fallback are untouched. Reads drain through the stream in either pipeline, freely mixed as before. Seeking adopts the Opus arm's shape: recorded at seek(), performed at the following read as a reset and decode forward in that read's own pipeline, and refused up front where it lies past a known length. That closes two seams the resident decoder carried. Its rewind kept the synthesis filter's state - QMF and overlap history from wherever playback had been - so the first frames after any seek, and after every loop back to the start, deviated from a linear decode; and it walked forward in f32 whatever the caller's format, converting the filter state through a float round-trip on the way. Measured against linear ground truth, the old arm deviated on every fixture at every seek target and on every loop pass (1-342 bytes per 16 K window, the worst on a rate-change join); the new arm is byte-identical to the playthrough everywhere, in both formats. Behaviour otherwise intended to be identical, and verified so against the old arm linked beside the new one: linear decode byte-exact in s16 and f32 across chunk sizes 1 to 64 K frames, mixed-format segment sequences byte-exact, info() identical, on seven fixtures including stereo+mono and 44.1k+22.05k concatenations (which the stream now adapts as the pull API did). What is not identical, beyond the seek correctness above: buffer_tell reports the consumption frontier rather than the pull cursor's scan position; a seek moves nothing until the next read, so a tell between the two still names the old position; and a stream the length walk could not measure (free format) fails a past-the-end seek at the read that performs it rather than at seek() itself. ASan, UBSan and LeakSan clean over open/decode/seek/loop/teardown, short and truncated files, and a garbage buffer whose open is refused without leaking the stream; TSan clean with eight concurrent contexts mixing formats and seeking.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #19316 and #19317. Two commits: the rmp3 streaming API gains the float pipeline, and audio_transfer's plain-MP3 arm — the last arm holding a whole-file pull decoder over its buffer — moves onto the stream.
rmp3: add f32 output to the streaming API
The stream decoded to s16 only, which left a caller serving a float pipeline (audio_transfer's f32 read, and through it the mixer's float voices) unable to move off the resident pull API without a quantisation it never had before. The decoder core already runs both pipelines; what the stream lacked was a way to select one.
rmp3_stream_set_out_f32, shaped like its s16 partner. Selecting either converts the decoder's persistent filter state and any undrained frame in place — the same operation the pull API's reads perform, now factored into shared helpers (rmp3d_convert_state,rmp3d_convert_frames) rather than restated. The frame buffer becomes the same s16/f32 union the pull API holds, named (rmp3_frame_buf) so both sides convert through one function with union-member access and no aliasing games.rmp3_stream_info's doc now says what the code always did — a located frame fills the channel count and rate in, so a parse-only walk answers it without anything having been decoded.audio_transfer: move the plain-MP3 arm onto rmp3_stream
rmp3's pull API borrows the buffer pointer and cursors over all of it, so the frontier
buffer_tellreported (readPos) named bytes merely read, not bytes releasable, and windowing an MP3 the way the Ogg arm windows a Vorbis file had nothing to stand on. The stream consumes its input — what is behind the cursor is genuinely done with, a frame straddling the boundary held in the stream's own 4 KB reassembly hold — so the cursor kept here is a real frontier, leading the decode position by at most that hold, the safe side for a feeder.seek(), performed at the following read as a reset and decode forward in that read's own pipeline, refused up front where it lies past a known length.That closes two seams the resident decoder carried. Its rewind kept the synthesis filter's state — QMF and overlap history from wherever playback had been — so the first frames after any seek, and after every loop back to the start, deviated from a linear decode; and it walked forward in f32 whatever the caller's format, converting the filter state through a float round-trip on the way. Measured against linear ground truth, the old arm deviated on every fixture at every seek target and on every loop pass (1–342 bytes per 16 K comparison window, the worst on a rate-change join); the new arm is byte-identical to the playthrough everywhere, in both formats.
Behavioural divergences
Beyond the seek correctness above, three, all deliberate:
buffer_tellreports the consumption frontier rather than the pull cursor's scan position.seek()itself.Verification
Seven fixtures: LAME CBR 128/320, VBR -q0, MPEG-2 mono 22.05 kHz, Layer II, and stereo+mono / 44.1k+22.05k concatenations.
info().-std=c89 -pedantic) and gnu99-Wall -Wextraclean.