ladybird/Tests/LibMedia/TestDataProviders.cpp
Jonathan Gamble 0dea87110e LibMedia: Fix multi-channel decode for dumb containers (like WAV)
For web audio, I reckon an occasional misjudged channel layout is
better than more frequent exceptions.

Signed PCM is normalized with unsigned max divided by 2, not
signed max. If you divide by the signed max (32767), you get headroom
that can exceed the threshold below -1.0. It's not audible, this mostly
matters for tests that assume correct normalization. But it turns out
there's no shortage of "golden ears" jackholes out there who swear they
can hear the difference.
2026-02-13 17:57:19 -06:00

140 lines
4.9 KiB
C++

/*
* Copyright (c) 2026, Gregory Bertilson <gregory@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/EventLoop.h>
#include <LibCore/File.h>
#include <LibCore/System.h>
#include <LibMedia/Audio/ChannelMap.h>
#include <LibMedia/Containers/Matroska/MatroskaDemuxer.h>
#include <LibMedia/FFmpeg/FFmpegDemuxer.h>
#include <LibMedia/IncrementallyPopulatedStream.h>
#include <LibMedia/Providers/AudioDataProvider.h>
#include <LibMedia/Providers/MediaTimeProvider.h>
#include <LibMedia/Providers/VideoDataProvider.h>
#include <LibTest/TestCase.h>
// The following tests attempt to reproduce a race condition in AudioDataProvider and VideoDataProvider
// where rapidly transitioning through states None -> Suspended -> Exit can cause the decoder thread to
// continue with a null decoder.
static NonnullRefPtr<Media::IncrementallyPopulatedStream> load_test_file(StringView path)
{
auto file = MUST(Core::File::open(path, Core::File::OpenMode::Read));
return Media::IncrementallyPopulatedStream::create_from_buffer(MUST(file->read_until_eof()));
}
static NonnullRefPtr<Media::Demuxer> create_demuxer(NonnullRefPtr<Media::IncrementallyPopulatedStream> const& stream)
{
auto matroska_result = Media::Matroska::MatroskaDemuxer::from_stream(stream);
if (!matroska_result.is_error())
return matroska_result.release_value();
return MUST(Media::FFmpeg::FFmpegDemuxer::from_stream(stream));
}
static constexpr size_t iterations = 100;
TEST_CASE(audio_provider_suspend_then_exit)
{
Core::EventLoop loop;
for (size_t i = 0; i < iterations; i++) {
auto stream = load_test_file("test-webm-xiph-lacing.mka"sv);
auto demuxer = create_demuxer(stream);
auto track = TRY_OR_FAIL(demuxer->get_preferred_track_for_type(Media::TrackType::Audio));
VERIFY(track.has_value());
auto provider = TRY_OR_FAIL(Media::AudioDataProvider::try_create(Core::EventLoop::current_weak(), demuxer, track.release_value()));
provider->suspend();
MUST(Core::System::sleep_ms(1));
}
}
TEST_CASE(video_provider_suspend_then_exit)
{
Core::EventLoop loop;
for (size_t i = 0; i < iterations; i++) {
auto stream = load_test_file("vp9_in_webm.webm"sv);
auto demuxer = create_demuxer(stream);
auto track = TRY_OR_FAIL(demuxer->get_preferred_track_for_type(Media::TrackType::Video));
VERIFY(track.has_value());
auto provider = TRY_OR_FAIL(Media::VideoDataProvider::try_create(Core::EventLoop::current_weak(), demuxer, track.release_value()));
provider->suspend();
MUST(Core::System::sleep_ms(1));
}
}
TEST_CASE(audio_provider_start_suspend_then_exit)
{
Core::EventLoop loop;
for (size_t i = 0; i < iterations; i++) {
auto stream = load_test_file("test-webm-xiph-lacing.mka"sv);
auto demuxer = create_demuxer(stream);
auto track = TRY_OR_FAIL(demuxer->get_preferred_track_for_type(Media::TrackType::Audio));
VERIFY(track.has_value());
auto provider = TRY_OR_FAIL(Media::AudioDataProvider::try_create(Core::EventLoop::current_weak(), demuxer, track.release_value()));
provider->start();
MUST(Core::System::sleep_ms(1));
provider->suspend();
MUST(Core::System::sleep_ms(1));
}
}
TEST_CASE(video_provider_start_suspend_then_exit)
{
Core::EventLoop loop;
for (size_t i = 0; i < iterations; i++) {
auto stream = load_test_file("vp9_in_webm.webm"sv);
auto demuxer = create_demuxer(stream);
auto track = TRY_OR_FAIL(demuxer->get_preferred_track_for_type(Media::TrackType::Video));
VERIFY(track.has_value());
auto provider = TRY_OR_FAIL(Media::VideoDataProvider::try_create(Core::EventLoop::current_weak(), demuxer, track.release_value()));
provider->start();
MUST(Core::System::sleep_ms(1));
provider->suspend();
MUST(Core::System::sleep_ms(1));
}
}
TEST_CASE(audio_provider_underspecified_5_1_channel_map)
{
Core::EventLoop loop;
auto stream = load_test_file("WAV/tone_44100_5_1_underspecified.wav"sv);
auto demuxer = create_demuxer(stream);
auto track = TRY_OR_FAIL(demuxer->get_preferred_track_for_type(Media::TrackType::Audio));
VERIFY(track.has_value());
auto provider = TRY_OR_FAIL(Media::AudioDataProvider::try_create(Core::EventLoop::current_weak(), demuxer, track.release_value()));
provider->start();
auto time_limit = AK::Duration::from_seconds(1);
auto start_time = MonotonicTime::now_coarse();
while (true) {
auto block = provider->retrieve_block();
if (!block.is_empty()) {
EXPECT_EQ(block.channel_count(), 6);
EXPECT_EQ(block.sample_specification().channel_map(), Audio::ChannelMap::surround_5_1());
return;
}
if (MonotonicTime::now_coarse() - start_time >= time_limit)
break;
loop.pump(Core::EventLoop::WaitMode::PollForEvents);
}
FAIL("Decoding timed out.");
}