mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-12-22 10:57:18 +00:00
LibMedia: Remove the now-unused audio loader plugins
This commit is contained in:
committed by
Gregory Bertilson
parent
07b5ac5db0
commit
9d6bc89ed7
Notes:
github-actions[bot]
2025-12-16 00:04:01 +00:00
Author: https://github.com/Zaggy1024 Commit: https://github.com/LadybirdBrowser/ladybird/commit/9d6bc89ed7a Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/7149
@@ -1,293 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "FFmpegLoader.h"
|
||||
#include <AK/NumericLimits.h>
|
||||
#include <LibCore/System.h>
|
||||
|
||||
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 24, 100)
|
||||
# define USE_FFMPEG_CH_LAYOUT
|
||||
#endif
|
||||
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(59, 0, 100)
|
||||
# define USE_CONSTIFIED_POINTERS
|
||||
#endif
|
||||
|
||||
namespace Audio {
|
||||
|
||||
static constexpr int BUFFER_MAX_PROBE_SIZE = 64 * KiB;
|
||||
|
||||
FFmpegLoaderPlugin::FFmpegLoaderPlugin(NonnullOwnPtr<SeekableStream> stream, NonnullOwnPtr<Media::FFmpeg::FFmpegIOContext> io_context)
|
||||
: LoaderPlugin(move(stream))
|
||||
, m_io_context(move(io_context))
|
||||
{
|
||||
}
|
||||
|
||||
FFmpegLoaderPlugin::~FFmpegLoaderPlugin()
|
||||
{
|
||||
if (m_frame != nullptr)
|
||||
av_frame_free(&m_frame);
|
||||
if (m_packet != nullptr)
|
||||
av_packet_free(&m_packet);
|
||||
if (m_codec_context != nullptr)
|
||||
avcodec_free_context(&m_codec_context);
|
||||
if (m_format_context != nullptr)
|
||||
avformat_close_input(&m_format_context);
|
||||
}
|
||||
|
||||
ErrorOr<NonnullOwnPtr<LoaderPlugin>> FFmpegLoaderPlugin::create(NonnullOwnPtr<SeekableStream> stream)
|
||||
{
|
||||
auto io_context = TRY(Media::FFmpeg::FFmpegIOContext::create(*stream));
|
||||
auto loader = make<FFmpegLoaderPlugin>(move(stream), move(io_context));
|
||||
TRY(loader->initialize());
|
||||
return loader;
|
||||
}
|
||||
|
||||
ErrorOr<void> FFmpegLoaderPlugin::initialize()
|
||||
{
|
||||
// Open the container
|
||||
m_format_context = avformat_alloc_context();
|
||||
if (m_format_context == nullptr)
|
||||
return Error::from_string_literal("Failed to allocate format context");
|
||||
m_format_context->pb = m_io_context->avio_context();
|
||||
if (avformat_open_input(&m_format_context, nullptr, nullptr, nullptr) < 0)
|
||||
return Error::from_string_literal("Failed to open input for format parsing");
|
||||
|
||||
// Read stream info; doing this is required for headerless formats like MPEG
|
||||
if (avformat_find_stream_info(m_format_context, nullptr) < 0)
|
||||
return Error::from_string_literal("Failed to find stream info");
|
||||
|
||||
#ifdef USE_CONSTIFIED_POINTERS
|
||||
AVCodec const* codec {};
|
||||
#else
|
||||
AVCodec* codec {};
|
||||
#endif
|
||||
// Find the best stream to play within the container
|
||||
int best_stream_index = av_find_best_stream(m_format_context, AVMediaType::AVMEDIA_TYPE_AUDIO, -1, -1, &codec, 0);
|
||||
if (best_stream_index == AVERROR_STREAM_NOT_FOUND)
|
||||
return Error::from_string_literal("No audio stream found in container");
|
||||
if (best_stream_index == AVERROR_DECODER_NOT_FOUND)
|
||||
return Error::from_string_literal("No suitable decoder found for stream");
|
||||
if (best_stream_index < 0)
|
||||
return Error::from_string_literal("Failed to find an audio stream");
|
||||
m_audio_stream = m_format_context->streams[best_stream_index];
|
||||
|
||||
// Set up the context to decode the audio stream
|
||||
m_codec_context = avcodec_alloc_context3(codec);
|
||||
if (m_codec_context == nullptr)
|
||||
return Error::from_string_literal("Failed to allocate the codec context");
|
||||
|
||||
if (avcodec_parameters_to_context(m_codec_context, m_audio_stream->codecpar) < 0)
|
||||
return Error::from_string_literal("Failed to copy codec parameters");
|
||||
|
||||
m_codec_context->pkt_timebase = m_audio_stream->time_base;
|
||||
m_codec_context->thread_count = AK::min(static_cast<int>(Core::System::hardware_concurrency()), 4);
|
||||
|
||||
if (avcodec_open2(m_codec_context, codec, nullptr) < 0)
|
||||
return Error::from_string_literal("Failed to open input for decoding");
|
||||
|
||||
// This is an initial estimate of the total number of samples in the stream.
|
||||
// During decoding, we might need to increase the number as more frames come in.
|
||||
auto duration_in_seconds = TRY([this] -> ErrorOr<double> {
|
||||
if (m_audio_stream->duration >= 0) {
|
||||
auto time_base = av_q2d(m_audio_stream->time_base);
|
||||
return static_cast<double>(m_audio_stream->duration) * time_base;
|
||||
}
|
||||
|
||||
// If the stream doesn't specify the duration, fallback to what the container says the duration is.
|
||||
// If the container doesn't know the duration, then we're out of luck. Return an error.
|
||||
if (m_format_context->duration < 0)
|
||||
return Error::from_string_literal("Negative stream duration");
|
||||
|
||||
return static_cast<double>(m_format_context->duration) / AV_TIME_BASE;
|
||||
}());
|
||||
|
||||
m_total_samples = AK::round_to<decltype(m_total_samples)>(sample_rate() * duration_in_seconds);
|
||||
|
||||
// Allocate packet (logical chunk of data) and frame (video / audio frame) buffers
|
||||
m_packet = av_packet_alloc();
|
||||
if (m_packet == nullptr)
|
||||
return Error::from_string_literal("Failed to allocate packet");
|
||||
|
||||
m_frame = av_frame_alloc();
|
||||
if (m_frame == nullptr)
|
||||
return Error::from_string_literal("Failed to allocate frame");
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
double FFmpegLoaderPlugin::time_base() const
|
||||
{
|
||||
return av_q2d(m_audio_stream->time_base);
|
||||
}
|
||||
|
||||
bool FFmpegLoaderPlugin::sniff(SeekableStream& stream)
|
||||
{
|
||||
auto io_context = MUST(Media::FFmpeg::FFmpegIOContext::create(stream));
|
||||
#ifdef USE_CONSTIFIED_POINTERS
|
||||
AVInputFormat const* detected_format {};
|
||||
#else
|
||||
AVInputFormat* detected_format {};
|
||||
#endif
|
||||
auto score = av_probe_input_buffer2(io_context->avio_context(), &detected_format, nullptr, nullptr, 0, BUFFER_MAX_PROBE_SIZE);
|
||||
return score > 0;
|
||||
}
|
||||
|
||||
static ErrorOr<FixedArray<Sample>> extract_samples_from_frame(AVFrame& frame)
|
||||
{
|
||||
size_t number_of_samples = frame.nb_samples;
|
||||
VERIFY(number_of_samples > 0);
|
||||
|
||||
#ifdef USE_FFMPEG_CH_LAYOUT
|
||||
size_t number_of_channels = frame.ch_layout.nb_channels;
|
||||
#else
|
||||
size_t number_of_channels = frame.channels;
|
||||
#endif
|
||||
auto format = static_cast<AVSampleFormat>(frame.format);
|
||||
auto packed_format = av_get_packed_sample_fmt(format);
|
||||
auto is_planar = av_sample_fmt_is_planar(format) == 1;
|
||||
|
||||
// FIXME: handle number_of_channels > 2
|
||||
if (number_of_channels != 1 && number_of_channels != 2)
|
||||
return Error::from_string_view("Unsupported number of channels"sv);
|
||||
|
||||
switch (format) {
|
||||
case AV_SAMPLE_FMT_FLTP:
|
||||
case AV_SAMPLE_FMT_S16:
|
||||
case AV_SAMPLE_FMT_S32:
|
||||
break;
|
||||
default:
|
||||
// FIXME: handle other formats
|
||||
return Error::from_string_view("Unsupported sample format"sv);
|
||||
}
|
||||
|
||||
auto get_plane_pointer = [&](size_t channel_index) -> uint8_t* {
|
||||
return is_planar ? frame.extended_data[channel_index] : frame.extended_data[0];
|
||||
};
|
||||
auto index_in_plane = [&](size_t sample_index, size_t channel_index) {
|
||||
if (is_planar)
|
||||
return sample_index;
|
||||
return sample_index * number_of_channels + channel_index;
|
||||
};
|
||||
auto read_sample = [&](uint8_t* data, size_t index) -> float {
|
||||
switch (packed_format) {
|
||||
case AV_SAMPLE_FMT_FLT:
|
||||
return reinterpret_cast<float*>(data)[index];
|
||||
case AV_SAMPLE_FMT_S16:
|
||||
return reinterpret_cast<i16*>(data)[index] / static_cast<float>(NumericLimits<i16>::max());
|
||||
case AV_SAMPLE_FMT_S32:
|
||||
return reinterpret_cast<i32*>(data)[index] / static_cast<float>(NumericLimits<i32>::max());
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
};
|
||||
|
||||
auto samples = TRY(FixedArray<Sample>::create(number_of_samples));
|
||||
for (size_t sample = 0; sample < number_of_samples; ++sample) {
|
||||
if (number_of_channels == 1) {
|
||||
samples.unchecked_at(sample) = Sample { read_sample(get_plane_pointer(0), index_in_plane(sample, 0)) };
|
||||
} else {
|
||||
samples.unchecked_at(sample) = Sample {
|
||||
read_sample(get_plane_pointer(0), index_in_plane(sample, 0)),
|
||||
read_sample(get_plane_pointer(1), index_in_plane(sample, 1)),
|
||||
};
|
||||
}
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
ErrorOr<Vector<FixedArray<Sample>>> FFmpegLoaderPlugin::load_chunks(size_t samples_to_read_from_input)
|
||||
{
|
||||
Vector<FixedArray<Sample>> chunks {};
|
||||
|
||||
do {
|
||||
// Obtain a packet
|
||||
auto read_frame_error = av_read_frame(m_format_context, m_packet);
|
||||
if (read_frame_error < 0) {
|
||||
if (read_frame_error == AVERROR_EOF)
|
||||
break;
|
||||
return Error::from_string_literal("Failed to read frame");
|
||||
}
|
||||
if (m_packet->stream_index != m_audio_stream->index) {
|
||||
av_packet_unref(m_packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Send the packet to the decoder
|
||||
if (avcodec_send_packet(m_codec_context, m_packet) < 0)
|
||||
return Error::from_string_literal("Failed to send packet");
|
||||
av_packet_unref(m_packet);
|
||||
|
||||
// Ask the decoder for a new frame. We might not have sent enough data yet
|
||||
auto receive_frame_error = avcodec_receive_frame(m_codec_context, m_frame);
|
||||
if (receive_frame_error != 0) {
|
||||
if (receive_frame_error == AVERROR(EAGAIN))
|
||||
continue;
|
||||
if (receive_frame_error == AVERROR_EOF)
|
||||
break;
|
||||
return Error::from_string_literal("Failed to receive frame");
|
||||
}
|
||||
|
||||
chunks.append(TRY(extract_samples_from_frame(*m_frame)));
|
||||
|
||||
// Use the frame's presentation timestamp to set the number of loaded samples
|
||||
m_loaded_samples = static_cast<int>(m_frame->pts * sample_rate() * time_base());
|
||||
if (m_loaded_samples > m_total_samples) [[unlikely]]
|
||||
m_total_samples = m_loaded_samples;
|
||||
|
||||
samples_to_read_from_input -= AK::min(samples_to_read_from_input, m_frame->nb_samples);
|
||||
} while (samples_to_read_from_input > 0);
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
ErrorOr<void> FFmpegLoaderPlugin::reset()
|
||||
{
|
||||
return seek(0);
|
||||
}
|
||||
|
||||
ErrorOr<void> FFmpegLoaderPlugin::seek(int sample_index)
|
||||
{
|
||||
auto sample_position_in_seconds = static_cast<double>(sample_index) / sample_rate();
|
||||
auto sample_timestamp = AK::round_to<int64_t>(sample_position_in_seconds / time_base());
|
||||
|
||||
if (av_seek_frame(m_format_context, m_audio_stream->index, sample_timestamp, AVSEEK_FLAG_ANY) < 0)
|
||||
return Error::from_string_literal("Failed to seek");
|
||||
avcodec_flush_buffers(m_codec_context);
|
||||
|
||||
m_loaded_samples = sample_index;
|
||||
return {};
|
||||
}
|
||||
|
||||
u32 FFmpegLoaderPlugin::sample_rate()
|
||||
{
|
||||
VERIFY(m_codec_context != nullptr);
|
||||
return m_codec_context->sample_rate;
|
||||
}
|
||||
|
||||
u16 FFmpegLoaderPlugin::num_channels()
|
||||
{
|
||||
VERIFY(m_codec_context != nullptr);
|
||||
#ifdef USE_FFMPEG_CH_LAYOUT
|
||||
return m_codec_context->ch_layout.nb_channels;
|
||||
#else
|
||||
return m_codec_context->channels;
|
||||
#endif
|
||||
}
|
||||
|
||||
PcmSampleFormat FFmpegLoaderPlugin::pcm_format()
|
||||
{
|
||||
// FIXME: pcm_format() is unused, always return Float for now
|
||||
return PcmSampleFormat::Float32;
|
||||
}
|
||||
|
||||
ByteString FFmpegLoaderPlugin::format_name()
|
||||
{
|
||||
if (!m_format_context)
|
||||
return "unknown";
|
||||
return m_format_context->iformat->name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Loader.h"
|
||||
#include <AK/Error.h>
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <LibMedia/FFmpeg/FFmpegIOContext.h>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
}
|
||||
|
||||
namespace Audio {
|
||||
|
||||
class FFmpegLoaderPlugin : public LoaderPlugin {
|
||||
public:
|
||||
explicit FFmpegLoaderPlugin(NonnullOwnPtr<SeekableStream>, NonnullOwnPtr<Media::FFmpeg::FFmpegIOContext>);
|
||||
virtual ~FFmpegLoaderPlugin();
|
||||
|
||||
static bool sniff(SeekableStream& stream);
|
||||
static ErrorOr<NonnullOwnPtr<LoaderPlugin>> create(NonnullOwnPtr<SeekableStream>);
|
||||
|
||||
virtual ErrorOr<Vector<FixedArray<Sample>>> load_chunks(size_t samples_to_read_from_input) override;
|
||||
|
||||
virtual ErrorOr<void> reset() override;
|
||||
virtual ErrorOr<void> seek(int sample_index) override;
|
||||
|
||||
virtual int loaded_samples() override { return m_loaded_samples; }
|
||||
virtual int total_samples() override { return m_total_samples; }
|
||||
virtual u32 sample_rate() override;
|
||||
virtual u16 num_channels() override;
|
||||
virtual PcmSampleFormat pcm_format() override;
|
||||
virtual ByteString format_name() override;
|
||||
|
||||
private:
|
||||
ErrorOr<void> initialize();
|
||||
double time_base() const;
|
||||
|
||||
AVStream* m_audio_stream;
|
||||
AVCodecContext* m_codec_context { nullptr };
|
||||
AVFormatContext* m_format_context { nullptr };
|
||||
AVFrame* m_frame { nullptr };
|
||||
NonnullOwnPtr<Media::FFmpeg::FFmpegIOContext> m_io_context;
|
||||
int m_loaded_samples { 0 };
|
||||
AVPacket* m_packet { nullptr };
|
||||
int m_total_samples { 0 };
|
||||
};
|
||||
|
||||
}
|
||||
@@ -9,8 +9,6 @@
|
||||
namespace Audio {
|
||||
|
||||
class AudioConverter;
|
||||
class Loader;
|
||||
class PlaybackStream;
|
||||
struct Sample;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2023, the SerenityOS developers.
|
||||
* Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "Loader.h"
|
||||
#include "FFmpegLoader.h"
|
||||
#include <AK/TypedTransfer.h>
|
||||
#include <LibCore/MappedFile.h>
|
||||
|
||||
namespace Audio {
|
||||
|
||||
LoaderPlugin::LoaderPlugin(NonnullOwnPtr<SeekableStream> stream)
|
||||
: m_stream(move(stream))
|
||||
{
|
||||
}
|
||||
|
||||
Loader::Loader(NonnullOwnPtr<LoaderPlugin> plugin)
|
||||
: m_plugin(move(plugin))
|
||||
{
|
||||
}
|
||||
|
||||
struct LoaderPluginInitializer {
|
||||
bool (*sniff)(SeekableStream&);
|
||||
ErrorOr<NonnullOwnPtr<LoaderPlugin>> (*create)(NonnullOwnPtr<SeekableStream>);
|
||||
};
|
||||
|
||||
static constexpr LoaderPluginInitializer s_initializers[] = {
|
||||
{ FFmpegLoaderPlugin::sniff, FFmpegLoaderPlugin::create },
|
||||
};
|
||||
|
||||
ErrorOr<NonnullRefPtr<Loader>> Loader::create(StringView path)
|
||||
{
|
||||
auto stream = TRY(Core::MappedFile::map(path, Core::MappedFile::Mode::ReadOnly));
|
||||
auto plugin = TRY(Loader::create_plugin(move(stream)));
|
||||
return adopt_ref(*new (nothrow) Loader(move(plugin)));
|
||||
}
|
||||
|
||||
ErrorOr<NonnullRefPtr<Loader>> Loader::create(ReadonlyBytes buffer)
|
||||
{
|
||||
auto stream = TRY(try_make<FixedMemoryStream>(buffer));
|
||||
auto plugin = TRY(Loader::create_plugin(move(stream)));
|
||||
return adopt_ref(*new (nothrow) Loader(move(plugin)));
|
||||
}
|
||||
|
||||
ErrorOr<NonnullOwnPtr<LoaderPlugin>> Loader::create_plugin(NonnullOwnPtr<SeekableStream> stream)
|
||||
{
|
||||
for (auto const& loader : s_initializers) {
|
||||
if (loader.sniff(*stream)) {
|
||||
TRY(stream->seek(0, SeekMode::SetPosition));
|
||||
return loader.create(move(stream));
|
||||
}
|
||||
TRY(stream->seek(0, SeekMode::SetPosition));
|
||||
}
|
||||
|
||||
return Error::from_string_literal("No loader plugin available");
|
||||
}
|
||||
|
||||
ErrorOr<Samples> Loader::get_more_samples(size_t samples_to_read_from_input)
|
||||
{
|
||||
if (m_plugin_at_end_of_stream && m_buffer.is_empty())
|
||||
return Samples {};
|
||||
|
||||
size_t remaining_samples = total_samples() - loaded_samples();
|
||||
size_t samples_to_read = min(remaining_samples, samples_to_read_from_input);
|
||||
auto samples = TRY(Samples::create(samples_to_read));
|
||||
|
||||
size_t sample_index = 0;
|
||||
|
||||
if (m_buffer.size() > 0) {
|
||||
size_t to_transfer = min(m_buffer.size(), samples_to_read);
|
||||
AK::TypedTransfer<Sample>::move(samples.data(), m_buffer.data(), to_transfer);
|
||||
if (to_transfer < m_buffer.size())
|
||||
m_buffer.remove(0, to_transfer);
|
||||
else
|
||||
m_buffer.clear_with_capacity();
|
||||
|
||||
sample_index += to_transfer;
|
||||
}
|
||||
|
||||
while (sample_index < samples_to_read) {
|
||||
auto chunk_data = TRY(m_plugin->load_chunks(samples_to_read - sample_index));
|
||||
chunk_data.remove_all_matching([](auto& chunk) { return chunk.is_empty(); });
|
||||
if (chunk_data.is_empty()) {
|
||||
m_plugin_at_end_of_stream = true;
|
||||
break;
|
||||
}
|
||||
for (auto& chunk : chunk_data) {
|
||||
if (sample_index < samples_to_read) {
|
||||
auto count = min(samples_to_read - sample_index, chunk.size());
|
||||
AK::TypedTransfer<Sample>::move(samples.span().offset(sample_index), chunk.data(), count);
|
||||
// We didn't read all of the chunk; transfer the rest into the buffer.
|
||||
if (count < chunk.size()) {
|
||||
auto remaining_samples_count = chunk.size() - count;
|
||||
// We will always have an empty buffer at this point!
|
||||
TRY(m_buffer.try_append(chunk.span().offset(count), remaining_samples_count));
|
||||
}
|
||||
} else {
|
||||
// We're now past what the user requested. Transfer the entirety of the data into the buffer.
|
||||
TRY(m_buffer.try_append(chunk.data(), chunk.size()));
|
||||
}
|
||||
sample_index += chunk.size();
|
||||
}
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2022, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Sample.h"
|
||||
#include "SampleFormats.h"
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/Error.h>
|
||||
#include <AK/FixedArray.h>
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <AK/NonnullRefPtr.h>
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/Stream.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibMedia/Export.h>
|
||||
|
||||
namespace Audio {
|
||||
|
||||
// Experimentally determined to be a decent buffer size on i686:
|
||||
// 4K (the default) is slightly worse, and 64K is much worse.
|
||||
// At sufficiently large buffer sizes, the advantage of infrequent read() calls is outweighed by the memmove() overhead.
|
||||
// There was no intensive fine-tuning done to determine this value, so improvements may definitely be possible.
|
||||
constexpr size_t const loader_buffer_size = 8 * KiB;
|
||||
|
||||
// Two seek points should ideally not be farther apart than this.
|
||||
// This variable is a heuristic for seek table-constructing loaders.
|
||||
constexpr u64 const maximum_seekpoint_distance_ms = 1000;
|
||||
// Seeking should be at least as precise as this.
|
||||
// That means: The actual achieved seek position must not be more than this amount of time before the requested seek position.
|
||||
constexpr u64 const seek_tolerance_ms = 5000;
|
||||
|
||||
using Samples = FixedArray<Sample>;
|
||||
|
||||
class LoaderPlugin {
|
||||
public:
|
||||
explicit LoaderPlugin(NonnullOwnPtr<SeekableStream> stream);
|
||||
virtual ~LoaderPlugin() = default;
|
||||
|
||||
// Load as many audio chunks as necessary to get up to the required samples.
|
||||
// A chunk can be anything that is convenient for the plugin to load in one go without requiring to move samples around different buffers.
|
||||
// For example: A FLAC, MP3 or QOA frame.
|
||||
// The chunks are returned in a vector, so the loader can simply add chunks until the requested sample amount is reached.
|
||||
// The sample count MAY be surpassed, but only as little as possible. It CAN be undershot when the end of the stream is reached.
|
||||
// If the loader has no chunking limitations (e.g. WAV), it may return a single exact-sized chunk.
|
||||
virtual ErrorOr<Vector<FixedArray<Sample>>> load_chunks(size_t samples_to_read_from_input) = 0;
|
||||
|
||||
virtual ErrorOr<void> reset() = 0;
|
||||
|
||||
virtual ErrorOr<void> seek(int const sample_index) = 0;
|
||||
|
||||
// total_samples() and loaded_samples() should be independent
|
||||
// of the number of channels.
|
||||
//
|
||||
// For example, with a three-second-long, stereo, 44.1KHz audio file:
|
||||
// num_channels() should return 2
|
||||
// sample_rate() should return 44100 (each channel is sampled at this rate)
|
||||
// total_samples() should return 132300 (sample_rate * three seconds)
|
||||
virtual int loaded_samples() = 0;
|
||||
virtual int total_samples() = 0;
|
||||
virtual u32 sample_rate() = 0;
|
||||
virtual u16 num_channels() = 0;
|
||||
|
||||
// Human-readable name of the file format, of the form <full abbreviation> (.<ending>)
|
||||
virtual ByteString format_name() = 0;
|
||||
virtual PcmSampleFormat pcm_format() = 0;
|
||||
|
||||
protected:
|
||||
NonnullOwnPtr<SeekableStream> m_stream;
|
||||
};
|
||||
|
||||
class MEDIA_API Loader : public RefCounted<Loader> {
|
||||
public:
|
||||
static ErrorOr<NonnullRefPtr<Loader>> create(StringView path);
|
||||
static ErrorOr<NonnullRefPtr<Loader>> create(ReadonlyBytes buffer);
|
||||
|
||||
// Will only read less samples if we're at the end of the stream.
|
||||
ErrorOr<Samples> get_more_samples(size_t samples_to_read_from_input = 128 * KiB);
|
||||
|
||||
ErrorOr<void> reset() const
|
||||
{
|
||||
m_plugin_at_end_of_stream = false;
|
||||
return m_plugin->reset();
|
||||
}
|
||||
ErrorOr<void> seek(int const position) const
|
||||
{
|
||||
m_buffer.clear_with_capacity();
|
||||
m_plugin_at_end_of_stream = false;
|
||||
return m_plugin->seek(position);
|
||||
}
|
||||
|
||||
int loaded_samples() const { return m_plugin->loaded_samples() - (int)m_buffer.size(); }
|
||||
int total_samples() const { return m_plugin->total_samples(); }
|
||||
u32 sample_rate() const { return m_plugin->sample_rate(); }
|
||||
u16 num_channels() const { return m_plugin->num_channels(); }
|
||||
ByteString format_name() const { return m_plugin->format_name(); }
|
||||
u16 bits_per_sample() const { return pcm_bits_per_sample(m_plugin->pcm_format()); }
|
||||
PcmSampleFormat pcm_format() const { return m_plugin->pcm_format(); }
|
||||
|
||||
private:
|
||||
static ErrorOr<NonnullOwnPtr<LoaderPlugin>> create_plugin(NonnullOwnPtr<SeekableStream> stream);
|
||||
|
||||
explicit Loader(NonnullOwnPtr<LoaderPlugin>);
|
||||
|
||||
mutable NonnullOwnPtr<LoaderPlugin> m_plugin;
|
||||
// The plugin can signal an end of stream by returning no (or only empty) chunks.
|
||||
mutable bool m_plugin_at_end_of_stream { false };
|
||||
mutable Vector<Sample, loader_buffer_size> m_buffer;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "SampleFormats.h"
|
||||
#include <AK/AtomicRefCounted.h>
|
||||
#include <AK/Function.h>
|
||||
#include <AK/Time.h>
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2021, kleines Filmröllchen <filmroellchen@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Format.h>
|
||||
#include <AK/Math.h>
|
||||
|
||||
namespace Audio {
|
||||
|
||||
using AK::Exponentials::exp;
|
||||
using AK::Exponentials::log;
|
||||
// Constants for logarithmic volume. See Sample::linear_to_log
|
||||
// Corresponds to 60dB
|
||||
constexpr float DYNAMIC_RANGE = 1000;
|
||||
constexpr float VOLUME_A = 1 / DYNAMIC_RANGE;
|
||||
float const VOLUME_B = log(DYNAMIC_RANGE);
|
||||
|
||||
// A single sample in an audio buffer.
|
||||
// Values are floating point, and should range from -1.0 to +1.0
|
||||
struct Sample {
|
||||
constexpr Sample() = default;
|
||||
|
||||
// For mono
|
||||
constexpr explicit Sample(float left)
|
||||
: left(left)
|
||||
, right(left)
|
||||
{
|
||||
}
|
||||
|
||||
// For stereo
|
||||
constexpr Sample(float left, float right)
|
||||
: left(left)
|
||||
, right(right)
|
||||
{
|
||||
}
|
||||
|
||||
// Returns the absolute maximum range (separate per channel) of the given sample buffer.
|
||||
// For example { 0.8, 0 } means that samples on the left channel occupy the range { -0.8, 0.8 },
|
||||
// while all samples on the right channel are 0.
|
||||
static Sample max_range(ReadonlySpan<Sample> span)
|
||||
{
|
||||
Sample result { NumericLimits<float>::min_normal(), NumericLimits<float>::min_normal() };
|
||||
for (Sample sample : span) {
|
||||
result.left = max(result.left, AK::fabs(sample.left));
|
||||
result.right = max(result.right, AK::fabs(sample.right));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void clip()
|
||||
{
|
||||
if (left > 1)
|
||||
left = 1;
|
||||
else if (left < -1)
|
||||
left = -1;
|
||||
|
||||
if (right > 1)
|
||||
right = 1;
|
||||
else if (right < -1)
|
||||
right = -1;
|
||||
}
|
||||
|
||||
// Logarithmic scaling, as audio should ALWAYS do.
|
||||
// Reference: https://www.dr-lex.be/info-stuff/volumecontrols.html
|
||||
// We use the curve `factor = a * exp(b * change)`,
|
||||
// where change is the input fraction we want to change by,
|
||||
// a = 1/1000, b = ln(1000) = 6.908 and factor is the multiplier used.
|
||||
// The value 1000 represents the dynamic range in sound pressure, which corresponds to 60 dB(A).
|
||||
// This is a good dynamic range because it can represent all loudness values from
|
||||
// 30 dB(A) (barely hearable with background noise)
|
||||
// to 90 dB(A) (almost too loud to hear and about the reasonable limit of actual sound equipment).
|
||||
//
|
||||
// Format ranges:
|
||||
// - Linear: 0.0 to 1.0
|
||||
// - Logarithmic: 0.0 to 1.0
|
||||
|
||||
ALWAYS_INLINE float linear_to_log(float const change) const
|
||||
{
|
||||
// TODO: Add linear slope around 0
|
||||
return VOLUME_A * exp(VOLUME_B * change);
|
||||
}
|
||||
|
||||
ALWAYS_INLINE float log_to_linear(float const val) const
|
||||
{
|
||||
// TODO: Add linear slope around 0
|
||||
return log(val / VOLUME_A) / VOLUME_B;
|
||||
}
|
||||
|
||||
ALWAYS_INLINE Sample& log_multiply(float const change)
|
||||
{
|
||||
float factor = linear_to_log(change);
|
||||
left *= factor;
|
||||
right *= factor;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ALWAYS_INLINE Sample log_multiplied(float const volume_change) const
|
||||
{
|
||||
Sample new_frame { left, right };
|
||||
new_frame.log_multiply(volume_change);
|
||||
return new_frame;
|
||||
}
|
||||
|
||||
// Constant power panning
|
||||
ALWAYS_INLINE Sample& pan(float const position)
|
||||
{
|
||||
float const pi_over_2 = AK::Pi<float> * 0.5f;
|
||||
float const root_over_2 = AK::sqrt<float>(2.0) * 0.5f;
|
||||
float const angle = position * pi_over_2 * 0.5f;
|
||||
float s, c;
|
||||
AK::sincos<float>(angle, s, c);
|
||||
left *= root_over_2 * (c - s);
|
||||
right *= root_over_2 * (c + s);
|
||||
return *this;
|
||||
}
|
||||
|
||||
ALWAYS_INLINE Sample panned(float const position) const
|
||||
{
|
||||
Sample new_sample { left, right };
|
||||
new_sample.pan(position);
|
||||
return new_sample;
|
||||
}
|
||||
|
||||
constexpr Sample& operator*=(float const mult)
|
||||
{
|
||||
left *= mult;
|
||||
right *= mult;
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr Sample operator*(float const mult) const
|
||||
{
|
||||
return { left * mult, right * mult };
|
||||
}
|
||||
|
||||
constexpr Sample& operator+=(Sample const& other)
|
||||
{
|
||||
left += other.left;
|
||||
right += other.right;
|
||||
return *this;
|
||||
}
|
||||
constexpr Sample& operator+=(float other)
|
||||
{
|
||||
left += other;
|
||||
right += other;
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr Sample operator+(Sample const& other) const
|
||||
{
|
||||
return { left + other.left, right + other.right };
|
||||
}
|
||||
|
||||
float left { 0 };
|
||||
float right { 0 };
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace AK {
|
||||
|
||||
template<>
|
||||
struct Formatter<Audio::Sample> : Formatter<FormatString> {
|
||||
ErrorOr<void> format(FormatBuilder& builder, Audio::Sample const& value)
|
||||
{
|
||||
return Formatter<FormatString>::format(builder, "[{}, {}]"sv, value.left, value.right);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "SampleFormats.h"
|
||||
#include <AK/Assertions.h>
|
||||
|
||||
namespace Audio {
|
||||
|
||||
u16 pcm_bits_per_sample(PcmSampleFormat format)
|
||||
{
|
||||
switch (format) {
|
||||
case PcmSampleFormat::Uint8:
|
||||
return 8;
|
||||
case PcmSampleFormat::Int16:
|
||||
return 16;
|
||||
case PcmSampleFormat::Int24:
|
||||
return 24;
|
||||
case PcmSampleFormat::Int32:
|
||||
case PcmSampleFormat::Float32:
|
||||
return 32;
|
||||
case PcmSampleFormat::Float64:
|
||||
return 64;
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Types.h>
|
||||
#include <LibMedia/Export.h>
|
||||
|
||||
namespace Audio {
|
||||
|
||||
// Supported PCM sample formats.
|
||||
enum class PcmSampleFormat : u8 {
|
||||
Uint8,
|
||||
Int16,
|
||||
Int24,
|
||||
Int32,
|
||||
Float32,
|
||||
Float64,
|
||||
};
|
||||
|
||||
// Most of the read code only cares about how many bits to read or write
|
||||
MEDIA_API u16 pcm_bits_per_sample(PcmSampleFormat format);
|
||||
|
||||
}
|
||||
@@ -3,8 +3,6 @@ include(audio)
|
||||
include(ffmpeg)
|
||||
|
||||
set(SOURCES
|
||||
Audio/Loader.cpp
|
||||
Audio/SampleFormats.cpp
|
||||
Color/ColorConverter.cpp
|
||||
Color/ColorPrimaries.cpp
|
||||
Color/TransferCharacteristics.cpp
|
||||
@@ -24,7 +22,6 @@ ladybird_lib(LibMedia media EXPLICIT_SYMBOL_EXPORT)
|
||||
target_link_libraries(LibMedia PRIVATE LibCore LibCrypto LibIPC LibGfx LibThreading LibUnicode)
|
||||
|
||||
target_sources(LibMedia PRIVATE
|
||||
Audio/FFmpegLoader.cpp
|
||||
FFmpeg/FFmpegAudioConverter.cpp
|
||||
FFmpeg/FFmpegAudioDecoder.cpp
|
||||
FFmpeg/FFmpegDemuxer.cpp
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <AK/RefPtr.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibMedia/Audio/Forward.h>
|
||||
#include <LibMedia/Audio/SampleFormats.h>
|
||||
#include <LibMedia/Export.h>
|
||||
#include <LibMedia/Forward.h>
|
||||
#include <LibMedia/Providers/MediaTimeProvider.h>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022, Luke Wilde <lukew@serenityos.org>
|
||||
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
|
||||
* Copyright (c) 2021-2023, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Concepts.h>
|
||||
#include <AK/MemoryStream.h>
|
||||
#include <LibMedia/Audio/Loader.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
template<typename LoaderPluginType>
|
||||
requires(IsBaseOf<Audio::LoaderPlugin, LoaderPluginType>)
|
||||
int fuzz_audio_loader(uint8_t const* data, size_t size)
|
||||
{
|
||||
auto const bytes = ReadonlyBytes { data, size };
|
||||
auto stream = try_make<FixedMemoryStream>(bytes).release_value();
|
||||
auto audio_or_error = LoaderPluginType::create(move(stream));
|
||||
|
||||
if (audio_or_error.is_error())
|
||||
return 0;
|
||||
|
||||
auto audio = audio_or_error.release_value();
|
||||
|
||||
for (;;) {
|
||||
auto samples = audio->load_chunks(4 * KiB);
|
||||
if (samples.is_error())
|
||||
return 0;
|
||||
if (samples.value().size() == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import("//Meta/gn/build/libs/pulse/enable.gni")
|
||||
shared_library("LibMedia") {
|
||||
include_dirs = [ "//Userland/Libraries" ]
|
||||
sources = [
|
||||
"Audio/Loader.cpp",
|
||||
"Audio/PlaybackStream.cpp",
|
||||
"Audio/SampleFormats.cpp",
|
||||
"Color/ColorConverter.cpp",
|
||||
@@ -29,7 +28,6 @@ shared_library("LibMedia") {
|
||||
}
|
||||
if (enable_ffmpeg) {
|
||||
sources += [
|
||||
"Audio/FFmpegLoader.cpp",
|
||||
"FFmpeg/FFmpegAudioConverter.cpp",
|
||||
"FFmpeg/FFmpegAudioDecoder.cpp",
|
||||
"FFmpeg/FFmpegHelpers.cpp",
|
||||
|
||||
@@ -8,7 +8,6 @@ else()
|
||||
endif()
|
||||
|
||||
lagom_utility(xml SOURCES xml.cpp LIBS LibFileSystem LibMain LibXML LibURL)
|
||||
lagom_utility(abench SOURCES abench.cpp LIBS LibMain LibFileSystem LibMedia)
|
||||
lagom_utility(dns SOURCES dns.cpp LIBS LibDNS LibMain LibTLS LibCrypto)
|
||||
|
||||
if (ENABLE_GUI_TARGETS)
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/NumericLimits.h>
|
||||
#include <AK/Types.h>
|
||||
#include <LibCore/ArgsParser.h>
|
||||
#include <LibCore/ElapsedTimer.h>
|
||||
#include <LibFileSystem/FileSystem.h>
|
||||
#include <LibMain/Main.h>
|
||||
#include <LibMedia/Audio/Loader.h>
|
||||
#include <stdio.h>
|
||||
|
||||
// The Kernel has problems with large anonymous buffers, so let's limit sample reads ourselves.
|
||||
static constexpr size_t MAX_CHUNK_SIZE = 1 * MiB / 2;
|
||||
|
||||
ErrorOr<int> ladybird_main(Main::Arguments arguments)
|
||||
{
|
||||
StringView path {};
|
||||
int sample_count = -1;
|
||||
|
||||
Core::ArgsParser args_parser;
|
||||
args_parser.set_general_help("Benchmark audio loading");
|
||||
args_parser.add_positional_argument(path, "Path to audio file", "path");
|
||||
args_parser.add_option(sample_count, "How many samples to load at maximum", "sample-count", 's', "samples");
|
||||
args_parser.parse(arguments);
|
||||
|
||||
auto maybe_loader = Audio::Loader::create(path);
|
||||
if (maybe_loader.is_error()) {
|
||||
warnln("Failed to load audio file: {}", maybe_loader.error());
|
||||
return 1;
|
||||
}
|
||||
auto loader = maybe_loader.release_value();
|
||||
|
||||
Core::ElapsedTimer sample_timer { Core::TimerType::Precise };
|
||||
i64 total_loader_time = 0;
|
||||
int remaining_samples = sample_count > 0 ? sample_count : NumericLimits<int>::max();
|
||||
unsigned total_loaded_samples = 0;
|
||||
|
||||
for (;;) {
|
||||
if (remaining_samples > 0) {
|
||||
sample_timer = sample_timer.start_new();
|
||||
auto samples = loader->get_more_samples(min(MAX_CHUNK_SIZE, remaining_samples));
|
||||
total_loader_time += sample_timer.elapsed_milliseconds();
|
||||
if (!samples.is_error()) {
|
||||
remaining_samples -= samples.value().size();
|
||||
total_loaded_samples += samples.value().size();
|
||||
if (samples.value().size() == 0)
|
||||
break;
|
||||
} else {
|
||||
warnln("Error while loading audio: {}", samples.error());
|
||||
return 1;
|
||||
}
|
||||
} else
|
||||
break;
|
||||
}
|
||||
|
||||
auto time_per_sample = static_cast<double>(total_loader_time) / static_cast<double>(total_loaded_samples) * 1000.;
|
||||
auto playback_time_per_sample = (1. / static_cast<double>(loader->sample_rate())) * 1000'000.;
|
||||
|
||||
outln("Loaded {:10d} samples in {:06.3f} s, {:9.3f} µs/sample, {:6.1f}% speed (realtime {:9.3f} µs/sample)", total_loaded_samples, static_cast<double>(total_loader_time) / 1000., time_per_sample, playback_time_per_sample / time_per_sample * 100., playback_time_per_sample);
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user