diff --git a/Checks/audio-continuity.cpp b/Checks/audio-continuity.cpp index d5b1de3..5107340 100644 --- a/Checks/audio-continuity.cpp +++ b/Checks/audio-continuity.cpp @@ -132,7 +132,7 @@ int main(int argc, char **argv) { require(argc == 2, "fixture directory required"); require(!player_set_volume(nullptr, .5f) && player_get_volume(nullptr) == 0, "null volume handle"); 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::ifstream input(path + ".f32", std::ios::binary | std::ios::ate); require(bool(input), "PCM reference exists"); diff --git a/Makefile b/Makefile index 29cf43d..de3ebe4 100644 --- a/Makefile +++ b/Makefile @@ -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 -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/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 -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/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-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)/compact-window-check: Checks/compact-window.swift Sources/App/CompactWindow.swift Makefile | $(BUILD_DIR) check-dependencies diff --git a/README.md b/README.md index 8483b92..47bca45 100644 --- a/README.md +++ b/README.md @@ -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. -`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. diff --git a/Sources/Core/player.cpp b/Sources/Core/player.cpp index 0c4ad56..c56ace1 100644 --- a/Sources/Core/player.cpp +++ b/Sources/Core/player.cpp @@ -1032,8 +1032,10 @@ struct Player { } const double timestamp = framePts(track, decoded); const double tick = av_q2d(track.stream()->time_base); - const bool contiguous = continuing && std::abs(timestamp - track.nextPts) <= tick + 1.0 / rate; - // Quantized packet PTS and rounded resampler delay must not insert/drop PCM samples. + // Some TS muxers round timestamps to milliseconds despite a 90 kHz timebase. + // 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(rate) * sampleRate; pcmPts = contiguous ? resampleNextPts : timestamp - static_cast(swr_get_delay(resampler, delayBase)) / delayBase; diff --git a/TODO.md b/TODO.md index 68f5cf0..d5b2765 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,5 @@ - [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 - 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 \ No newline at end of file diff --git a/docs/miruro-playback-handoff.md b/docs/miruro-playback-handoff.md index 8ccacd2..59c7009 100644 --- a/docs/miruro-playback-handoff.md +++ b/docs/miruro-playback-handoff.md @@ -117,6 +117,8 @@ Handle `null` duration as ineligible. This measures position, not viewing covera | 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 | +| 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 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 |