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.
This commit is contained in:
Jonathan Gamble
2026-02-07 02:43:15 -06:00
committed by Gregory Bertilson
parent 9418981398
commit 0dea87110e
Notes: github-actions[bot] 2026-02-13 23:58:32 +00:00
7 changed files with 59 additions and 3 deletions

View File

@@ -140,7 +140,8 @@ requires(IsSigned<T>)
static float float_sample_from_frame_data(u8** data, size_t plane, size_t index)
{
auto* pointer = reinterpret_cast<T*>(data[plane]);
return pointer[index] / static_cast<float>(NumericLimits<T>::max());
constexpr float full_scale = NumericLimits<MakeUnsigned<T>>::max() / 2;
return static_cast<float>(pointer[index]) / static_cast<float>(full_scale);
}
template<typename T>

View File

@@ -42,7 +42,16 @@ ErrorOr<Audio::ChannelMap> av_channel_layout_to_channel_map(AVChannelLayout cons
return Audio::ChannelMap::mono();
if (layout.nb_channels == 2)
return Audio::ChannelMap::stereo();
return Error::from_string_literal("Unspecified channel order was neither mono nor stereo");
if (layout.nb_channels == 4)
return Audio::ChannelMap::quadrophonic();
if (layout.nb_channels == 6)
return Audio::ChannelMap::surround_5_1();
if (layout.nb_channels == 8)
return Audio::ChannelMap::surround_7_1();
for (int i = 0; i < layout.nb_channels; ++i)
channels[i] = Audio::Channel::Unknown;
return Audio::ChannelMap(channels);
}
#define AV_CHANNEL_TO_AUDIO_CHANNEL(audio_channel, av_channel) \

View File

@@ -7,6 +7,7 @@
#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>
@@ -106,3 +107,34 @@ TEST_CASE(video_provider_start_suspend_then_exit)
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.");
}

View File

@@ -7,8 +7,10 @@
#pragma once
#include <AK/Function.h>
#include <AK/Optional.h>
#include <LibCore/EventLoop.h>
#include <LibCore/File.h>
#include <LibMedia/Audio/ChannelMap.h>
#include <LibMedia/Containers/Matroska/MatroskaDemuxer.h>
#include <LibMedia/Containers/Matroska/Reader.h>
#include <LibMedia/Demuxer.h>
@@ -65,7 +67,7 @@ static inline void decode_video(StringView path, size_t expected_frame_count, T
VERIFY_NOT_REACHED();
}
static inline void decode_audio(StringView path, u32 sample_rate, u8 channel_count, size_t expected_sample_count)
static inline void decode_audio(StringView path, u32 sample_rate, u8 channel_count, size_t expected_sample_count, Optional<Audio::ChannelMap> expected_channel_map = {})
{
Core::EventLoop loop;
@@ -105,6 +107,8 @@ static inline void decode_audio(StringView path, u32 sample_rate, u8 channel_cou
} else {
EXPECT_EQ(block.sample_rate(), sample_rate);
EXPECT_EQ(block.channel_count(), channel_count);
if (expected_channel_map.has_value())
EXPECT_EQ(block.sample_specification().channel_map(), expected_channel_map.value());
VERIFY(sample_count == 0 || last_sample <= block.timestamp_in_samples());
last_sample = block.timestamp_in_samples() + static_cast<i64>(block.sample_count());

View File

@@ -58,3 +58,13 @@ TEST_CASE(stereo_44khz)
{
decode_audio("WAV/tone_44100_stereo.wav"sv, 44100, 2, 220500);
}
TEST_CASE(stereo_8khz_24bit)
{
decode_audio("WAV/voices_8000_stereo_int24.wav"sv, 8000, 2, 23493);
}
TEST_CASE(underspecified_5_1_44khz)
{
decode_audio("WAV/tone_44100_5_1_underspecified.wav"sv, 44100, 6, 44100, Audio::ChannelMap::surround_5_1());
}

Binary file not shown.

Binary file not shown.