This is an intermediate step towards unifying the pipeline around new Producer/Sink interfaces. Producers now have a pull() method that gets the next piece of data from them. The pull() method returns a status that can indicate whether it has current data, and if not, why it's unavailable. This signal will be passed down the pipeline to the final sink, which can expose the signal to its user, which in the normal playback pipeline is PlaybackManager. The signal can be used to transition between playback states. Currently, this is only hooked up to the buffering state, but should be used later for ending playback as well as decoding error propagation. Buffering is now determined solely based on whether the pipeline is blocked on incomplete data, so the ready state for video now progresses past HAVE_METADATA immediately after playback manager initializes. This will change when files have buffered ranges.
139 lines
4.6 KiB
C++
139 lines
4.6 KiB
C++
/*
|
|
* Copyright (c) 2026, The Ladybird developers
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/ByteBuffer.h>
|
|
#include <AK/NumericLimits.h>
|
|
#include <AK/StringView.h>
|
|
#include <AK/Time.h>
|
|
#include <LibCore/EventLoop.h>
|
|
#include <LibMedia/FFmpeg/FFmpegDemuxer.h>
|
|
#include <LibMedia/IncrementallyPopulatedStream.h>
|
|
#include <LibMedia/PipelineStatus.h>
|
|
#include <LibMedia/Producers/DecodedAudioProducer.h>
|
|
#include <LibTest/TestCase.h>
|
|
|
|
template<typename Integer>
|
|
requires(IsIntegral<Integer>)
|
|
static void append_little_endian(ByteBuffer& buffer, Integer value)
|
|
{
|
|
using Unsigned = MakeUnsigned<Integer>;
|
|
Unsigned unsigned_value = static_cast<Unsigned>(value);
|
|
auto bytes = buffer.must_get_bytes_for_writing(sizeof(Integer));
|
|
for (size_t byte_index = 0; byte_index < sizeof(Integer); ++byte_index)
|
|
bytes[byte_index] = static_cast<u8>(unsigned_value >> (byte_index * 8));
|
|
}
|
|
|
|
template<typename Sample>
|
|
requires(IsSigned<Sample>)
|
|
static ByteBuffer make_square_wav(size_t sample_count)
|
|
{
|
|
constexpr u16 channel_count = 1;
|
|
constexpr u32 sample_rate = 8'000;
|
|
constexpr u16 bits_per_sample = sizeof(Sample) * 8;
|
|
constexpr u16 block_align = channel_count * sizeof(Sample);
|
|
constexpr u32 byte_rate = sample_rate * block_align;
|
|
|
|
u32 data_size = static_cast<u32>(sample_count * sizeof(Sample));
|
|
u32 riff_chunk_size = 36 + data_size;
|
|
|
|
ByteBuffer buffer;
|
|
buffer.ensure_capacity(44 + data_size);
|
|
|
|
buffer.append("RIFF"sv.bytes());
|
|
append_little_endian(buffer, riff_chunk_size);
|
|
buffer.append("WAVE"sv.bytes());
|
|
|
|
buffer.append("fmt "sv.bytes());
|
|
append_little_endian(buffer, 16u);
|
|
append_little_endian(buffer, static_cast<u16>(1));
|
|
append_little_endian(buffer, channel_count);
|
|
append_little_endian(buffer, sample_rate);
|
|
append_little_endian(buffer, byte_rate);
|
|
append_little_endian(buffer, block_align);
|
|
append_little_endian(buffer, bits_per_sample);
|
|
|
|
buffer.append("data"sv.bytes());
|
|
append_little_endian(buffer, data_size);
|
|
|
|
for (size_t sample_index = 0; sample_index < sample_count; ++sample_index) {
|
|
Sample sample = (sample_index % 2 == 0) ? NumericLimits<Sample>::min() : NumericLimits<Sample>::max();
|
|
append_little_endian(buffer, sample);
|
|
}
|
|
|
|
return buffer;
|
|
}
|
|
|
|
template<typename Sample>
|
|
requires(IsSigned<Sample>)
|
|
static void decode_and_expect()
|
|
{
|
|
Core::EventLoop loop;
|
|
|
|
constexpr size_t sample_count = 2048;
|
|
auto wav_data = make_square_wav<Sample>(sample_count);
|
|
auto stream = Media::IncrementallyPopulatedStream::create_from_buffer(wav_data);
|
|
auto demuxer = MUST(Media::FFmpeg::FFmpegDemuxer::from_stream(stream));
|
|
|
|
auto tracks = TRY_OR_FAIL(demuxer->get_tracks_for_type(Media::TrackType::Audio));
|
|
VERIFY(!tracks.is_empty());
|
|
auto producer = TRY_OR_FAIL(Media::DecodedAudioProducer::try_create(Core::EventLoop::current_weak(), demuxer, tracks[0]));
|
|
|
|
producer->set_error_handler([&](Media::DecoderError&&) {
|
|
FAIL("An error occurred while decoding generated WAV data.");
|
|
});
|
|
producer->start();
|
|
|
|
bool saw_negative_full_scale_sample = false;
|
|
bool saw_positive_peak_sample = false;
|
|
size_t decoded_sample_count = 0;
|
|
bool reached_end_of_stream = false;
|
|
|
|
MonotonicTime deadline = MonotonicTime::now_coarse() + AK::Duration::from_seconds(1);
|
|
while (MonotonicTime::now_coarse() < deadline) {
|
|
Media::AudioBlock block;
|
|
auto status = producer->pull(block);
|
|
if (status == Media::PipelineStatus::HaveData) {
|
|
EXPECT(!block.is_empty());
|
|
for (float sample : block.data()) {
|
|
EXPECT(sample >= -1.0f);
|
|
if constexpr (sizeof(Sample) >= sizeof(i32))
|
|
EXPECT(sample <= 1.0f);
|
|
else
|
|
EXPECT(sample < 1.0f);
|
|
if (sample == -1.0f)
|
|
saw_negative_full_scale_sample = true;
|
|
if (sample > 0.0f)
|
|
saw_positive_peak_sample = true;
|
|
}
|
|
decoded_sample_count += block.sample_count();
|
|
} else if (status == Media::PipelineStatus::EndOfStream) {
|
|
reached_end_of_stream = true;
|
|
break;
|
|
}
|
|
|
|
loop.pump(Core::EventLoop::WaitMode::PollForEvents);
|
|
}
|
|
|
|
EXPECT(reached_end_of_stream);
|
|
EXPECT_EQ(decoded_sample_count, sample_count);
|
|
EXPECT(saw_negative_full_scale_sample);
|
|
EXPECT(saw_positive_peak_sample);
|
|
}
|
|
|
|
TEST_CASE(signed_16bit_pcm_to_float)
|
|
{
|
|
decode_and_expect<i16>();
|
|
}
|
|
|
|
TEST_CASE(signed_32bit_pcm_to_float)
|
|
{
|
|
decode_and_expect<i32>();
|
|
}
|
|
|
|
TEST_CASE(signed_64bit_pcm_to_float)
|
|
{
|
|
decode_and_expect<i64>();
|
|
}
|