Conversation
Generates its own media with the vendored ffmpeg at run time, nothing checked in: tone and click tracks across the containers and codecs the reader supports, at 44.1, 48 and 96 kHz, with priming trimmed, delivered and undeclared, with and without video, plus transport and program streams. Decodes forty frames of each through the reader sequentially, shuffled and in reverse, and requires the runs to agree sample for sample with no allowance, every frame to be a clean tone, and every click to land on the sample the source put it on, following the audio stream's own clock where a container starts it late. The media is left behind after the run, to be played and checked by hand. Cases the reader is known to get wrong are listed with their reason and run only by a disabled test, which gtest reports on every run and runs on request. Fixing one moves it out of the list. FFMpegStream gains first_packet_pts(), the pts of a stream's first packet, which the click test needs to follow a stream that starts late. Signed-off-by: Ben de Luca <bdeluca@gmail.com>
Buffer::set_buf_data swapped the underlying BufferData shared_ptr without touching Buffer::size_. typed_resample is the only caller and always swaps in a different-sized (resampled) buffer. When audio is resampled down the new buffer is smaller, so Buffer::~Buffer parked it in the recycler under the pre-swap (larger) size key. A later Buffer::allocate at that key handed the smaller block back believing it was the bigger size; the next write overran and smashed adjacent heap metadata, eventually crashing on free of an unrelated adjacent block. Diagnosed from two Windows crash dumps with matching FAILURE_ID_HASH bucket in ucrtbase free_base. Bug is platform-independent C++ UB; the same overrun triggers under AddressSanitizer on Linux against the actual Buffer and recycler. Post-fix the same scenario runs clean. Make set_buf_data require the new size and update both the buffer pointer and size_ together. Single call site in typed_resample updated. Signed-off-by: Ben de Luca <bdeluca@gmail.com>
Three faults in how decoded packets fill the per-frame audio buffers. A buffer half filled before a seek was kept and finished with samples from the new position, a splice inside the frame. The merge of filled regions only joined two when an endpoint of one fell inside the other, so a region wholly inside another was never absorbed; a sequential decode never produces one, but a packet re-delivering samples already written did, and the buffer was then never reported complete. The sample arithmetic was unsigned, so a packet with a negative timestamp, which is how matroska hands AAC priming over, was skipped. Half-filled buffers are cleared on a seek, regions merge when they overlap or touch, and the arithmetic is signed. Signed-off-by: Ben de Luca <bdeluca@gmail.com>
Two things the reader did on a seek that fftools do not. It flushed the decoder, and a flush does not reset every decoder: AC-3, E-AC-3, FLAC and ALAC have no flush callback, so after a seek back to the head AC-3 overlap-added its first frame with the tail of the last one decoded, and AAC keeps its noise-substitution state across a flush. The reader now opens a fresh codec context, which is what the head of the stream always had. And it threw on a packet the decoder rejects, the partial packet a seek into a program stream lands on, so the request failed and the next one landed on the same packet. Such a packet is now skipped. Measured on the harness: frame 0 of the avi AC-3 and m4a AAC cases read after a seek, and every frame of both program streams. Signed-off-by: Ben de Luca <bdeluca@gmail.com>
A decoded frame was placed by its pts where the stream's time_base is at least its sample rate, and otherwise by a pts rounded to the time_base after a seek and a count of samples from there. On matroska and webm, which count milliseconds, a frame reached by a seek landed up to a millisecond from where playing through put it, and the frames xSTUDIO cuts from the two did not meet. Real vorbis is worse than rounding. Around a change of vorbis block size the pts in Wikimedia's webm transcodes and in a matroska film release are tens of milliseconds from the samples before them. Read through the reader with a seek before each frame, 1193 of 1198 frames of one webm landed elsewhere than playback put them, 740 of 740 of the film. Ogg vorbis, placed by pts throughout, broke in plain playback, 18 times in 50 s of one file. How a position is found is now chosen per stream from what the library says of it. A codec with no frames, whose samples come straight from the packet's bytes (av_get_exact_bits_per_sample), with a time_base unit of one sample or less: the pts is the position. A codec the library gives a frame size for (av_get_audio_frame_duration2): frame k starts at start + k * frame_size, and k is the pts in samples over the frame size, rounded, since a pts is never out by anything near half a frame. That also puts back a pts a sample out in an mp4 whose packet durations do not tile. Otherwise the format lets the size change from packet to packet, as vorbis picks one of three from the sound, and every packet is a region of its own size. The stream keeps a table of them: where each packet is in the file and the samples it decodes to, sized by the library without decoding. A region starts at the sum of the sizes before it. A seek finds the region that holds the wanted sample and goes to its block's pts, the one the file stores. A packet is named by AVPacket.pos and which of the packets there it is, and the decoder hands that to the frame (AV_CODEC_FLAG_COPY_OPAQUE). No pts places anything: inside a matroska block only the block's time is stored, the demuxer adds durations for the rest, and after a seek onto the block it sizes the first packet without the one before it, so the same packets come out with other pts. The table is filled only as far into the file as has been needed. Playing on adds each region from the packet being decoded, and a seek beyond it reads forward from where it stops: 64 ms, once, to reach 625 s into an 830 MB film read from the page cache. Where the audio starts is taken once, from the stream's first frames decoded with a codec context of their own: at the declared priming's distance before zero, or at the first packet where the container declares none. The decoder is opened with AV_CODEC_FLAG2_SKIP_MANUAL so frames arrive whole and the priming the side data names is dropped here. A seek goes to the first packet's pts, not INT64_MIN, which not every demuxer reads as the start, and a demuxer that lands with less than half the pre-roll to spare is asked again from twice as far back. A packet the library cannot size or that has no file position, a packet the table does not hold and a frame tied to no packet all throw. Pre-roll is dropped by where a frame is, not by its pts. The unit test covers each method and holds one block of the film with the two sets of pts it is read with. The same real files now land at one constant offset from a plain decode, in order and with a seek before every frame. Signed-off-by: Ben de Luca <bdeluca@gmail.com>
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.
Symptom
Audio read out of order was wrong: frames popped, went silent, or carried samples from elsewhere, in plain playback as much as when scrubbing, because the cache fills from several readers and every reader seeks. On a webm the whole track sat 3 ms late against picture. Seven defects in the ffmpeg reader and one latent fault, each below with its cause, fix, commit and the test that shows it.
Issues
1. A frame spliced across a seek
FFMpegDecoder::do_seek).DecoderInternals.SeekClearsHalfAssembledAudioBuffers; the pop measured at up to 2925 against a 2900 peak in the harness's shuffled runs is gone.2. Region merge could not absorb a nested region (latent)
PartiallyFilledAudioBuf::copy_samples_from_other_buffer). With issue 6 fixed the case cannot arise; the merge is now correct regardless.PartialAudioBuf.ContainedRegionStillCompletesTheFrame,PartialAudioBuf.AdjacentRegionsCompleteTheFrame. No harness case depends on it.3. Priming packets skipped
AudioIntegrity.*onmkv_aac_48k,mkv_mp3_44k1,mkv_ac3_48k(first packets at negative timestamps).4. Decoder state survived a seek
FFMpegStream::open_audio_decoder), which is what the head of the stream always had.AudioIntegrity.RandomAccessIsRepeatableonavi_ac3_48kandclicks_m4a_aac_48k, whose frame 0 read after a seek differed from the sequential decode before, every later frame matching.5. Program streams failed on every seek
FFMpegDecoder::decode_next_frame,decode_and_store_next_frame).AudioIntegrity.*onmpg_mp2_48k,mpg_mp2_44k1,mpg_ac3_48k, which threw on every frame before.6. Random access disagreed with sequential decode in millisecond containers
AudioPtsRescaler. How a frame's position is found is chosen per stream, inFFMpegStream::probe_audio_start, from what libavcodec states about it. A codec with no frames, whose samples come straight from the packet's bytes (av_get_exact_bits_per_sample), with a time_base unit of one sample or less: the pts is the position, which is right for a raw PCM demuxer that cuts packets anywhere after a seek. A codec the library gives a frame size for (av_get_audio_frame_duration2): frame k starts at start + k * frame_size, k being the pts in samples over the frame size, rounded, since a pts is never out by anything near half a frame. Anything else is issue 8. The decoder runs withAV_CODEC_FLAG2_SKIP_MANUALso frames arrive whole and the reader drops the priming the side data names; the first frames, decoded once at open, fix where the audio begins. Replaces the lead-in probe, the counting rule andav_rescale_delta.audio_pts_rescaler_test, which takes pts sequences recorded from thirteen of the harness's streams and requires any frame, from its own pts alone, to land where a count from the start of the stream puts it;AudioIntegrity.*on every matroska, webm and avi AC-3 case, all failing before;AudioIntegrity.ClicksLandOnTheirSampleonclicks_webm_vorbis_48kfor the 3 ms.7. Pre-roll dropped audio that was needed
FFMpegDecoder::decode_audio_frame).AudioIntegrity.EveryFrameIsStillAPureToneandRandomAccessAgreesWithSequentialonmpg_mp2_44k1, the last case to pass.8. A pts cannot place a packet whose size changes
AudioPtsRescaler::use_region_table: each packet's file position (AVPacket.pos) and the samples it decodes to, sized by the library without decoding (av_get_audio_frame_duration2on the packet size, elseAVCodecParserContext::duration, else the packet duration where the time_base is one sample). A region starts at the sum of the sizes before it. A seek finds the region that holds the wanted sample and goes to its block's stored pts (FFMpegStream::audio_seek_pts). A packet is named by its pos and which of the packets there it is, and the decoder hands that to the frame (AV_CODEC_FLAG_COPY_OPAQUE). No pts places anything. The table is filled only as far into the file as has been needed: playing on adds each region from the packet being decoded, a seek beyond it reads forward from where it stops. A packet that cannot be sized or has no pos, a packet the table does not hold and a frame tied to no packet throw.audio_pts_rescaler_testholds that block of the film with both sets of pts, and checks the table both ways, packet to start and sample to region, on the thirteen recorded streams.AudioIntegrity.*on every vorbis, Opus, FLAC, E-AC-3 and ALAC case and PCM in matroska, which take this path. Outside the tree, real files decoded through the reader, 1200 frames in order and again with a seek before every frame, each buffer located in a plain ffmpeg decode: before commit 5 a seek placed 1193 of 1198 frames of one Wikimedia webm elsewhere than playback did, 740 of 740 of an 830 MB matroska film, and ogg vorbis broke 18 times in plain playback; after it, thirteen files each sit at one constant offset with no breaks. A seek 625 s into the film costs 64 ms once, from the page cache. Only vorbis was found in real files; the pts and frame size paths have been run on the harness's media only.Testing
Buffer::size_in step withset_buf_data, which upstream lacks. The harness commit before it shows why: resampling audio corrupts the heap and the harness aborts in its first decoder test; the fix turns it green. It should go upstream on its own ahead of this.ffmpeg_audio_integrity_testgenerates its own media with the vendored ffmpeg intotone_mediaand leaves it there to be played and checked by hand; it allows no tolerance, and compares sequential, shuffled and reverse decodes sample for sample: 8 of 8 pass. Containers: wav, aiff, mov, mp4, m4a, mkv, webm, avi, mxf, MPEG-TS, MPEG-PS, ogg, opus, raw mp3. Codecs: PCM, AAC, ALAC, FLAC, mp2, mp3, AC-3, E-AC-3, vorbis, opus. Rates: 44.1, 48 and 96 kHz.audio_pts_rescaler_test: 8 of 8 pass. Needs no media.avi_ac3_48kand the mp4 AC-3, E-AC-3 and mp3 click cases now passing. Commit 5: 8 of 8, none. The harness commit addsFFMpegStream::first_packet_pts(), which its click test needs.scripts/av_sync_clock.py(not in this PR).Known broken
Listed by name and reason in
kKnownBrokenin the harness, run only byAudioIntegrity.DISABLED_KnownBroken. gtest reports the disabled test on every run;--gtest_also_run_disabled_testsruns it and it fails on all twelve. A case that starts passing there is fixed and moves out of the list.Commits