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
+}