fix: preserve audio samples across Miruro timestamp jitter

This commit is contained in:
2026-10-13 16:13:33 +02:00
parent 51382bc63a
commit 4ef8ceee24
6 changed files with 11 additions and 5 deletions
+1 -1
View File
@@ -132,7 +132,7 @@ int main(int argc, char **argv) {
require(argc == 2, "fixture directory required"); require(argc == 2, "fixture directory required");
require(!player_set_volume(nullptr, .5f) && player_get_volume(nullptr) == 0, "null volume handle"); require(!player_set_volume(nullptr, .5f) && player_get_volume(nullptr) == 0, "null volume handle");
std::string directory = argv[1]; std::string directory = argv[1];
for (const char *name : {"tone441.wav", "tone480.wav", "tone441.mka", "tone-aac.m4a"}) { for (const char *name : {"tone441.wav", "tone480.wav", "tone441.mka", "tone-aac.m4a", "tone-jitter.ts"}) {
std::string path = directory + "/" + name; std::string path = directory + "/" + name;
std::ifstream input(path + ".f32", std::ios::binary | std::ios::ate); std::ifstream input(path + ".f32", std::ios::binary | std::ios::ate);
require(bool(input), "PCM reference exists"); require(bool(input), "PCM reference exists");
+2
View File
@@ -129,11 +129,13 @@ check-audio: $(BUILD_DIR)/audio-continuity-check
ffmpeg -hide_banner -loglevel error -y -f lavfi -i testsrc2=size=160x90:rate=24:duration=2 -i $(BUILD_DIR)/fixtures/tone480.wav -c:v mpeg4 -g 48 -bf 2 -c:a copy $(BUILD_DIR)/fixtures/tone-step.mov ffmpeg -hide_banner -loglevel error -y -f lavfi -i testsrc2=size=160x90:rate=24:duration=2 -i $(BUILD_DIR)/fixtures/tone480.wav -c:v mpeg4 -g 48 -bf 2 -c:a copy $(BUILD_DIR)/fixtures/tone-step.mov
ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone441.wav -c:a copy $(BUILD_DIR)/fixtures/tone441.mka ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone441.wav -c:a copy $(BUILD_DIR)/fixtures/tone441.mka
ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone441.wav -c:a aac $(BUILD_DIR)/fixtures/tone-aac.m4a ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone441.wav -c:a aac $(BUILD_DIR)/fixtures/tone-aac.m4a
ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone480.wav -af "asetnsamples=n=1024:p=0,asetpts='round(PTS*TB*1000)/1000/TB'" -c:a aac -f mpegts $(BUILD_DIR)/fixtures/tone-jitter.ts
ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone441.wav -af "asetpts='PTS+gte(T,1)*0.25/TB'" -c:a pcm_s16le $(BUILD_DIR)/fixtures/tone-gap.mka ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone441.wav -af "asetpts='PTS+gte(T,1)*0.25/TB'" -c:a pcm_s16le $(BUILD_DIR)/fixtures/tone-gap.mka
ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone441.wav -ar 48000 -ac 2 -f f32le $(BUILD_DIR)/fixtures/tone441.wav.f32 ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone441.wav -ar 48000 -ac 2 -f f32le $(BUILD_DIR)/fixtures/tone441.wav.f32
ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone480.wav -ar 48000 -ac 2 -f f32le $(BUILD_DIR)/fixtures/tone480.wav.f32 ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone480.wav -ar 48000 -ac 2 -f f32le $(BUILD_DIR)/fixtures/tone480.wav.f32
ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone441.mka -ar 48000 -ac 2 -f f32le $(BUILD_DIR)/fixtures/tone441.mka.f32 ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone441.mka -ar 48000 -ac 2 -f f32le $(BUILD_DIR)/fixtures/tone441.mka.f32
ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone-aac.m4a -ar 48000 -ac 2 -f f32le $(BUILD_DIR)/fixtures/tone-aac.m4a.f32 ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone-aac.m4a -ar 48000 -ac 2 -f f32le $(BUILD_DIR)/fixtures/tone-aac.m4a.f32
ffmpeg -hide_banner -loglevel error -y -i $(BUILD_DIR)/fixtures/tone-jitter.ts -ar 48000 -ac 2 -f f32le $(BUILD_DIR)/fixtures/tone-jitter.ts.f32
$(BUILD_DIR)/audio-continuity-check $(BUILD_DIR)/fixtures $(BUILD_DIR)/audio-continuity-check $(BUILD_DIR)/fixtures
$(BUILD_DIR)/compact-window-check: Checks/compact-window.swift Sources/App/CompactWindow.swift Makefile | $(BUILD_DIR) check-dependencies $(BUILD_DIR)/compact-window-check: Checks/compact-window.swift Sources/App/CompactWindow.swift Makefile | $(BUILD_DIR) check-dependencies
+1 -1
View File
@@ -315,7 +315,7 @@ make check
Generates short fixtures and exercises the real playback interface and native outputs: audio/video, video-only, audio-only, non-square-pixel display aspect, pause, paused/playing and queued seek acknowledgement, exact bidirectional VFR/B-frame stepping across keyframes, repeated-step bursts beyond the ordinary command capacity, mixed seek/direction ordering, replacement media superseding a long pending run, first/last-frame boundaries, stepping pause/resume, completion, replay, close, HDR rejection, failed-open recovery, and destruction while playing. It initializes the actual Metal shaders and uses an offscreen native window. It requires a working audio device and graphical macOS session, and plays short sine-wave audio. It is a runtime smoke check, not a mocked clock/device test. Generates short fixtures and exercises the real playback interface and native outputs: audio/video, video-only, audio-only, non-square-pixel display aspect, pause, paused/playing and queued seek acknowledgement, exact bidirectional VFR/B-frame stepping across keyframes, repeated-step bursts beyond the ordinary command capacity, mixed seek/direction ordering, replacement media superseding a long pending run, first/last-frame boundaries, stepping pause/resume, completion, replay, close, HDR rejection, failed-open recovery, and destruction while playing. It initializes the actual Metal shaders and uses an offscreen native window. It requires a working audio device and graphical macOS session, and plays short sine-wave audio. It is a runtime smoke check, not a mocked clock/device test.
`make check-audio` runs just the audio-quality check. A check-only hook captures actual Core Audio callback PCM into preallocated storage and compares every sample against FFmpeg's reference output for PCM/AAC, 44.1/48 kHz, and coarse container timestamps. It verifies that repeated forward/backward stepping stays silent and that resumed audio matches the exact selected-frame PCM suffix. It also checks fractional gain, silent muted startup, smooth live volume changes, and unchanged playback completion. A separate gapped fixture verifies that real timestamp gaps remain silent. Release builds contain no capture hook. `make check-audio` runs just the audio-quality check. A check-only hook captures actual Core Audio callback PCM into preallocated storage and compares every sample against FFmpeg's reference output for PCM/AAC, 44.1/48 kHz, coarse container timestamps, and AAC in MPEG-TS with millisecond-rounded timestamps. Audio continuity allows up to two milliseconds of timestamp jitter (or a larger container tick) without inserting or dropping samples; this covers Miruro streams whose advertised 90 kHz timebase overstates their timestamp precision. It verifies that repeated forward/backward stepping stays silent and that resumed audio matches the exact selected-frame PCM suffix. It also checks fractional gain, silent muted startup, smooth live volume changes, and unchanged playback completion. A separate gapped fixture verifies that real timestamp gaps remain silent. Release builds contain no capture hook.
`make check-window` runs the compact-mode check, also included in `make check`. It exercises magnetic thresholds/corners, negative-origin screens, portrait/extreme aspect bounds, configured/session-only snapping, actual hidden-window entry/resizing/snapping/restoration, remembered manual compact width across reentry/controller recreation with a different aspect, isolation from programmatic/normal-window resizing, view-host identity, keyboard eligibility, and a replay of AppKit's fullscreen-failure lifecycle. It requires a graphical macOS session but does not display windows or play audio. Visible fullscreen transitions, cross-Space/Stage Manager behavior, and hover/drag usability still require a check in the app. `make check-window` runs the compact-mode check, also included in `make check`. It exercises magnetic thresholds/corners, negative-origin screens, portrait/extreme aspect bounds, configured/session-only snapping, actual hidden-window entry/resizing/snapping/restoration, remembered manual compact width across reentry/controller recreation with a different aspect, isolation from programmatic/normal-window resizing, view-host identity, keyboard eligibility, and a replay of AppKit's fullscreen-failure lifecycle. It requires a graphical macOS session but does not display windows or play audio. Visible fullscreen transitions, cross-Space/Stage Manager behavior, and hover/drag usability still require a check in the app.
+4 -2
View File
@@ -1032,8 +1032,10 @@ struct Player {
} }
const double timestamp = framePts(track, decoded); const double timestamp = framePts(track, decoded);
const double tick = av_q2d(track.stream()->time_base); const double tick = av_q2d(track.stream()->time_base);
const bool contiguous = continuing && std::abs(timestamp - track.nextPts) <= tick + 1.0 / rate; // Some TS muxers round timestamps to milliseconds despite a 90 kHz timebase.
// Quantized packet PTS and rounded resampler delay must not insert/drop PCM samples. // Allow two rounded endpoints of jitter; real gaps still insert silence.
const bool contiguous = continuing &&
std::abs(timestamp - track.nextPts) <= std::max(tick, 0.002) + 1.0 / rate;
const int64_t delayBase = static_cast<int64_t>(rate) * sampleRate; const int64_t delayBase = static_cast<int64_t>(rate) * sampleRate;
pcmPts = contiguous ? resampleNextPts : pcmPts = contiguous ? resampleNextPts :
timestamp - static_cast<double>(swr_get_delay(resampler, delayBase)) / delayBase; timestamp - static_cast<double>(swr_get_delay(resampler, delayBase)) / delayBase;
+1 -1
View File
@@ -1,5 +1,5 @@
- [x] add miruro streaming support - [x] add miruro streaming support
- [ ] investigate and fix audio when streaming from miruro - audio seems to crackle (not for youtube i think; only for miruros) - [x] investigate and fix audio when streaming from miruro - audio seems to crackle (not for youtube i think; only for miruros)
- [ ] in the "choose what to play" screen, we should change the youtube button general streaming - [ ] in the "choose what to play" screen, we should change the youtube button general streaming
- we can figure out the stream and how to handle it from the url (i.e. if youtube url, handle as youtube stream; if miruro url, handle accordingly) - we can figure out the stream and how to handle it from the url (i.e. if youtube url, handle as youtube stream; if miruro url, handle accordingly)
- [ ] add chrome cast support - [ ] add chrome cast support
+2
View File
@@ -117,6 +117,8 @@ Handle `null` duration as ineligible. This measures position, not viewing covera
| Scenario | Observed result | | Scenario | Observed result |
| --- | --- | | --- | --- |
| Explicit fresh direct Sun native smoke | Video/audio decode and advancing playback; default English VTT; expected cue text after forward/backward acknowledged seeks | | Explicit fresh direct Sun native smoke | Video/audio decode and advancing playback; default English VTT; expected cue text after forward/backward acknowledged seeks |
| Live Sun audio continuity | Dragon Ball episode 1: actual Core Audio PCM diverged from FFmpeg after about 0.6 seconds before the fix; after tolerating millisecond-scale timestamp jitter, all 960,008 stereo frames compared across twenty seconds and multiple HLS segments matched exactly |
| Audio regression | Deterministic AAC/MPEG-TS timestamp-jitter fixture failed before the shared continuity fix and matched FFmpeg exactly afterward; existing authored-gap, resampling, gain and stepped-resume checks passed |
| Local resolved playback model | Independent stream/subtitle headers, nested playlist tokens, video/audio playback, cue timing and stable v2 progress | | Local resolved playback model | Independent stream/subtitle headers, nested playlist tokens, video/audio playback, cue timing and stable v2 progress |
| Local native network checks | Encrypted MPEG-TS and fMP4; image-suffixed resources; headers on master/variant/segment/key/init; seeks/frame steps; redirect/foreign-origin isolation; diagnostic redaction; existing YouTube redirects | | Local native network checks | Encrypted MPEG-TS and fMP4; image-suffixed resources; headers on master/variant/segment/key/init; seeks/frame steps; redirect/foreign-origin isolation; diagnostic redaction; existing YouTube redirects |
| Actual CLI-to-GUI handoff | Two independent Miruro episode windows; authenticated encrypted HLS; resource-specific default VTT; v2 progress and quit draining | | Actual CLI-to-GUI handoff | Two independent Miruro episode windows; authenticated encrypted HLS; resource-specific default VTT; v2 progress and quit draining |