diff --git a/src/FrameMapper.cpp b/src/FrameMapper.cpp index 2374c82af..09c644e1a 100644 --- a/src/FrameMapper.cpp +++ b/src/FrameMapper.cpp @@ -558,12 +558,12 @@ std::shared_ptr FrameMapper::GetFrame(int64_t requested_frame) frame->AddImage(std::make_shared(*even_frame->GetImage()), false); } - // Determine if the wrapped reader actually supplied audio on this frame. - // Video-only readers can still be mapped onto an audio-enabled timeline, - // which yields frames with default audio metadata but no sample data. - const bool reader_has_audio = mapped_frame->SampleRate() > 0 && - mapped_frame->GetAudioChannelsCount() > 0 && - mapped_frame->GetAudioSamplesCount() > 0; + // Only treat the reader as audio-capable when the wrapped reader reports + // audio. Individual source frames can still be empty near boundaries, but + // those cases should pad with silence instead of dropping this mapped frame. + const bool reader_has_audio = reader->info.has_audio && + mapped_frame->SampleRate() > 0 && + mapped_frame->GetAudioChannelsCount() > 0; // Resample audio on frame (if needed) bool need_resampling = false; @@ -604,6 +604,10 @@ std::shared_ptr FrameMapper::GetFrame(int64_t requested_frame) } } + // Preserve the target frame duration even when a source frame has no samples. + if (reader_has_audio && samples_in_frame > 0) + frame->AddAudioSilence(samples_in_frame); + // Copy the samples int samples_copied = 0; int64_t starting_frame = copy_samples.frame_start; @@ -620,8 +624,28 @@ std::shared_ptr FrameMapper::GetFrame(int64_t requested_frame) } int original_samples = original_frame->GetAudioSamplesCount(); - if (original_samples <= 0) - break; + if (original_samples <= 0) { + if (starting_frame >= copy_samples.frame_end) + break; + starting_frame++; + continue; + } + + if (starting_frame == copy_samples.frame_start) + number_to_copy = original_samples - copy_samples.sample_start; + else if (starting_frame > copy_samples.frame_start && starting_frame < copy_samples.frame_end) + number_to_copy = original_samples; + else + number_to_copy = copy_samples.sample_end + 1; + + if (number_to_copy <= 0) { + if (starting_frame >= copy_samples.frame_end) + break; + starting_frame++; + continue; + } + if (number_to_copy > remaining_samples) + number_to_copy = remaining_samples; // Loop through each channel for (int channel = 0; channel < channels_in_frame; channel++) @@ -629,31 +653,16 @@ std::shared_ptr FrameMapper::GetFrame(int64_t requested_frame) if (starting_frame == copy_samples.frame_start) { // Starting frame (take the ending samples) - number_to_copy = original_samples - copy_samples.sample_start; - if (number_to_copy > remaining_samples) - number_to_copy = remaining_samples; - - // Add samples to new frame frame->AddAudio(true, channel, samples_copied, original_frame->GetAudioSamples(channel) + copy_samples.sample_start, number_to_copy, 1.0); } else if (starting_frame > copy_samples.frame_start && starting_frame < copy_samples.frame_end) { // Middle frame (take all samples) - number_to_copy = original_samples; - if (number_to_copy > remaining_samples) - number_to_copy = remaining_samples; - - // Add samples to new frame frame->AddAudio(true, channel, samples_copied, original_frame->GetAudioSamples(channel), number_to_copy, 1.0); } else { // Ending frame (take the beginning samples) - number_to_copy = copy_samples.sample_end + 1; - if (number_to_copy > remaining_samples) - number_to_copy = remaining_samples; - - // Add samples to new frame frame->AddAudio(false, channel, samples_copied, original_frame->GetAudioSamples(channel), number_to_copy, 1.0); } } diff --git a/tests/FrameMapper.cpp b/tests/FrameMapper.cpp index 9f92149d1..b2ff8e7e4 100644 --- a/tests/FrameMapper.cpp +++ b/tests/FrameMapper.cpp @@ -62,6 +62,47 @@ TEST_CASE( "Invalid_Frame_Too_Small", "[libopenshot][framemapper]" ) } +TEST_CASE( "zero_sample_source_frame_preserves_mapped_audio_duration", "[libopenshot][framemapper][audio]" ) +{ + CacheMemory cache; + const Fraction fps(24, 1); + const int sample_rate = 48000; + const int channels = 2; + const int samples_per_frame = Frame::GetSamplesPerFrame(1, fps, sample_rate, channels); + + auto empty_frame = std::make_shared(1, 1, 1, "#000000", 0, channels); + empty_frame->SampleRate(sample_rate); + empty_frame->ChannelsLayout(LAYOUT_STEREO); + cache.Add(empty_frame); + + auto audio_frame = std::make_shared(2, 1, 1, "#000000", samples_per_frame, channels); + audio_frame->SampleRate(sample_rate); + audio_frame->ChannelsLayout(LAYOUT_STEREO); + std::vector samples(samples_per_frame, 0.5f); + for (int channel = 0; channel < channels; ++channel) + audio_frame->AddAudio(true, channel, 0, samples.data(), samples_per_frame, 1.0f); + cache.Add(audio_frame); + + DummyReader reader(fps, 1, 1, sample_rate, channels, 2.0 / fps.ToDouble(), &cache); + reader.info.has_audio = true; + reader.Open(); + + FrameMapper map(&reader, fps, PULLDOWN_NONE, sample_rate, channels, LAYOUT_STEREO); + map.Open(); + + auto first = map.GetFrame(1); + CHECK(first->GetAudioSamplesCount() == samples_per_frame); + for (int i = 0; i < samples_per_frame; ++i) + CHECK(first->GetAudioSample(0, i, 1.0) == Approx(0.0f).margin(0.0001f)); + + auto second = map.GetFrame(2); + CHECK(second->GetAudioSamplesCount() == samples_per_frame); + CHECK(second->GetAudioSample(0, 0, 1.0) == Approx(0.5f).margin(0.0001f)); + + map.Close(); + reader.Close(); +} + TEST_CASE( "24_fps_to_30_fps_Pulldown_Classic", "[libopenshot][framemapper]" ) { // Create a reader @@ -746,4 +787,4 @@ TEST_CASE( "SampleRange", "[libopenshot][framemapper]") CHECK(Samples.frame_end == 10); CHECK(Samples.sample_end == 1469); CHECK(Samples.total == total_samples); -} \ No newline at end of file +}