LibMedia+Utilities: Remove encoders and aconv

We don't use these in Ladybird, so let's get rid of them.
This commit is contained in:
Jelle Raaijmakers 2024-08-28 12:40:06 +02:00 committed by Andreas Kling
parent 85fd2e281b
commit 7728633906
Notes: github-actions[bot] 2024-09-12 08:02:33 +00:00
13 changed files with 0 additions and 1521 deletions

View file

@ -449,7 +449,6 @@ endif()
# Lagom Utilities
lagom_utility(abench SOURCES ../../Userland/Utilities/abench.cpp LIBS LibMain LibFileSystem LibMedia)
lagom_utility(aconv SOURCES ../../Userland/Utilities/aconv.cpp LIBS LibMain LibFileSystem LibMedia)
if (ENABLE_GUI_TARGETS)
lagom_utility(animation SOURCES ../../Userland/Utilities/animation.cpp LIBS LibGfx LibMain)

View file

@ -15,7 +15,6 @@ shared_library("LibMedia") {
"Audio/UserSampleQueue.cpp",
"Audio/VorbisComment.cpp",
"Audio/WavLoader.cpp",
"Audio/WavWriter.cpp",
]
if (enable_pulseaudio) {
sources += [

View file

@ -5,40 +5,14 @@
*/
#include <AK/ByteString.h>
#include <AK/LexicalPath.h>
#include <LibCore/Directory.h>
#include <LibCore/File.h>
#include <LibFileSystem/FileSystem.h>
#include <LibFileSystem/TempFile.h>
#include <LibMedia/Audio/WavLoader.h>
#include <LibMedia/Audio/WavWriter.h>
#include <LibTest/TestCase.h>
static void compare_files(StringView const& in_path, StringView const& out_path)
{
Array<u8, 4096> buffer1;
Array<u8, 4096> buffer2;
auto original_file = MUST(Core::File::open(in_path, Core::File::OpenMode::Read));
auto copied_file = MUST(Core::File::open(out_path, Core::File::OpenMode::Read));
while (!original_file->is_eof() && !copied_file->is_eof()) {
auto original_bytes = TRY_OR_FAIL(original_file->read_some(buffer1));
auto copied_bytes = TRY_OR_FAIL(copied_file->read_some(buffer2));
EXPECT_EQ(original_bytes, copied_bytes);
}
}
static void run_test(StringView file_name, int const num_samples, int const channels, u32 const rate)
{
constexpr auto format = "RIFF WAVE (.wav)";
constexpr int bits = 16;
auto out_file = TRY_OR_FAIL(FileSystem::TempFile::create_temp_file());
auto out_path = out_file->path();
ByteString in_path = ByteString::formatted("WAV/{}", file_name);
auto loader = TRY_OR_FAIL(Audio::Loader::create(in_path));
@ -48,26 +22,6 @@ static void run_test(StringView file_name, int const num_samples, int const chan
EXPECT_EQ(loader->num_channels(), channels);
EXPECT_EQ(loader->bits_per_sample(), bits);
EXPECT_EQ(loader->total_samples(), num_samples);
auto writer = TRY_OR_FAIL(Audio::WavWriter::create_from_file(out_path, rate, channels));
int samples_read = 0;
int size = 0;
do {
auto samples = TRY_OR_FAIL(loader->get_more_samples());
TRY_OR_FAIL(writer->write_samples(samples.span()));
size = samples.size();
samples_read += size;
} while (size);
TRY_OR_FAIL(writer->finalize());
EXPECT_EQ(samples_read, num_samples);
compare_files(in_path, out_path);
}
// 5 seconds, 16-bit audio samples

View file

@ -1,38 +0,0 @@
/*
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Forward.h"
#include "Sample.h"
#include <AK/Error.h>
#include <AK/Span.h>
namespace Audio {
class Encoder {
public:
virtual ~Encoder() = default;
// Encodes the given samples and writes them to the output stream.
// Note that due to format restrictions, not all samples might be written immediately, this is only guaranteed after a call to finalize().
virtual ErrorOr<void> write_samples(ReadonlySpan<Sample> samples) = 0;
// Finalizes the stream, future calls to write_samples() will cause an error.
// This method makes sure that all samples are encoded and written out.
// This method is called in the destructor, but since this can error, you should call this function yourself before disposing of the decoder.
virtual ErrorOr<void> finalize() = 0;
// Sets the metadata for this audio file.
// Not all encoders support this, and metadata may not be writeable after starting to write samples.
virtual ErrorOr<void> set_metadata([[maybe_unused]] Metadata const& metadata) { return {}; }
// Provides a hint about the total number of samples to the encoder, improving some encoder's performance in various aspects.
// Note that the hint does not have to be fully correct; wrong hints never cause errors, not even indirectly.
virtual void sample_count_hint([[maybe_unused]] size_t sample_count) { }
};
}

View file

@ -1,889 +0,0 @@
/*
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "FlacWriter.h"
#include "Metadata.h"
#include "VorbisComment.h"
#include <AK/BitStream.h>
#include <AK/DisjointChunks.h>
#include <AK/Endian.h>
#include <AK/IntegralMath.h>
#include <AK/MemoryStream.h>
#include <AK/Statistics.h>
#include <LibCrypto/Checksum/ChecksummingStream.h>
namespace Audio {
ErrorOr<NonnullOwnPtr<FlacWriter>> FlacWriter::create(NonnullOwnPtr<SeekableStream> stream, u32 sample_rate, u8 num_channels, u16 bits_per_sample)
{
auto writer = TRY(AK::adopt_nonnull_own_or_enomem(new (nothrow) FlacWriter(move(stream))));
TRY(writer->set_bits_per_sample(bits_per_sample));
TRY(writer->set_sample_rate(sample_rate));
TRY(writer->set_num_channels(num_channels));
return writer;
}
FlacWriter::FlacWriter(NonnullOwnPtr<SeekableStream> stream)
: m_stream(move(stream))
{
}
FlacWriter::~FlacWriter()
{
if (m_state != WriteState::FullyFinalized)
(void)finalize();
}
ErrorOr<void> FlacWriter::finalize()
{
if (m_state == WriteState::FullyFinalized)
return Error::from_string_literal("File is already finalized");
if (m_state == WriteState::HeaderUnwritten)
TRY(finalize_header_format());
if (!m_sample_buffer.is_empty())
TRY(write_frame());
{
// 1 byte metadata block header + 3 bytes size + 2*2 bytes min/max block size
TRY(m_stream->seek(m_streaminfo_start_index + 8, AK::SeekMode::SetPosition));
BigEndianOutputBitStream bit_stream { MaybeOwned<Stream> { *m_stream } };
TRY(bit_stream.write_bits(m_min_frame_size, 24));
TRY(bit_stream.write_bits(m_max_frame_size, 24));
TRY(bit_stream.write_bits(m_sample_rate, 20));
TRY(bit_stream.write_bits(m_num_channels - 1u, 3));
TRY(bit_stream.write_bits(m_bits_per_sample - 1u, 5));
TRY(bit_stream.write_bits(m_sample_count, 36));
TRY(bit_stream.align_to_byte_boundary());
}
TRY(flush_seektable());
// TODO: Write the audio data MD5 to the header.
m_stream->close();
m_state = WriteState::FullyFinalized;
return {};
}
ErrorOr<void> FlacWriter::finalize_header_format()
{
if (m_state != WriteState::HeaderUnwritten)
return Error::from_string_literal("Header format is already finalized");
TRY(write_header());
m_state = WriteState::FormatFinalized;
return {};
}
ErrorOr<void> FlacWriter::set_num_channels(u8 num_channels)
{
if (m_state != WriteState::HeaderUnwritten)
return Error::from_string_literal("Header format is already finalized");
if (num_channels > 8)
return Error::from_string_literal("FLAC doesn't support more than 8 channels");
m_num_channels = num_channels;
return {};
}
ErrorOr<void> FlacWriter::set_sample_rate(u32 sample_rate)
{
if (m_state != WriteState::HeaderUnwritten)
return Error::from_string_literal("Header format is already finalized");
m_sample_rate = sample_rate;
return {};
}
ErrorOr<void> FlacWriter::set_bits_per_sample(u16 bits_per_sample)
{
if (m_state != WriteState::HeaderUnwritten)
return Error::from_string_literal("Header format is already finalized");
if (bits_per_sample < 8 || bits_per_sample > 32)
return Error::from_string_literal("FLAC only supports bits per sample between 8 and 32");
m_bits_per_sample = bits_per_sample;
return {};
}
ErrorOr<void> FlacWriter::set_metadata(Metadata const& metadata)
{
AllocatingMemoryStream vorbis_stream;
TRY(write_vorbis_comment(metadata, vorbis_stream));
auto vorbis_data = TRY(vorbis_stream.read_until_eof());
FlacRawMetadataBlock vorbis_block {
.is_last_block = false,
.type = FlacMetadataBlockType::VORBIS_COMMENT,
.length = static_cast<u32>(vorbis_data.size()),
.data = move(vorbis_data),
};
return add_metadata_block(move(vorbis_block), 0);
}
size_t FlacWriter::max_number_of_seekpoints() const
{
if (m_last_padding.has_value())
return m_last_padding->size / flac_seekpoint_size;
if (!m_cached_metadata_blocks.is_empty() && m_cached_metadata_blocks.last().type == FlacMetadataBlockType::PADDING)
return m_cached_metadata_blocks.last().length / flac_seekpoint_size;
return 0;
}
void FlacWriter::sample_count_hint(size_t sample_count)
{
constexpr StringView oom_warning = "FLAC Warning: Couldn't use sample hint to reserve {} bytes padding; ignoring hint."sv;
auto const samples_per_seekpoint = m_sample_rate * seekpoint_period_seconds;
auto seekpoint_count = round_to<size_t>(static_cast<double>(sample_count) / samples_per_seekpoint);
// Round seekpoint count down to an even number, so that the seektable byte size is divisible by 4.
// One seekpoint is 18 bytes, which isn't divisible by 4.
seekpoint_count &= ~1;
auto const seektable_size = seekpoint_count * flac_seekpoint_size;
// Only modify the trailing padding block; other padding blocks are intentionally untouched.
if (!m_cached_metadata_blocks.is_empty() && m_cached_metadata_blocks.last().type == FlacMetadataBlockType::PADDING) {
auto padding_block = m_cached_metadata_blocks.last();
auto result = padding_block.data.try_resize(seektable_size);
padding_block.length = padding_block.data.size();
// Fuzzers and inputs with wrong large sample counts often hit this.
if (result.is_error())
dbgln(oom_warning, seektable_size);
} else {
auto empty_buffer = ByteBuffer::create_zeroed(seektable_size);
if (empty_buffer.is_error()) {
dbgln(oom_warning, seektable_size);
return;
}
FlacRawMetadataBlock padding {
.is_last_block = true,
.type = FlacMetadataBlockType::PADDING,
.length = static_cast<u32>(empty_buffer.value().size()),
.data = empty_buffer.release_value(),
};
// If we can't add padding, we're out of luck.
(void)add_metadata_block(move(padding));
}
}
ErrorOr<void> FlacWriter::write_header()
{
ByteBuffer data;
// STREAMINFO is always exactly 34 bytes long.
TRY(data.try_resize(34));
BigEndianOutputBitStream header_stream { TRY(try_make<FixedMemoryStream>(data.bytes())) };
// Duplication on purpose:
// Minimum frame size.
TRY(header_stream.write_bits(block_size, 16));
// Maximum frame size.
TRY(header_stream.write_bits(block_size, 16));
// Leave the frame sizes as unknown for now.
TRY(header_stream.write_bits(0u, 24));
TRY(header_stream.write_bits(0u, 24));
TRY(header_stream.write_bits(m_sample_rate, 20));
TRY(header_stream.write_bits(m_num_channels - 1u, 3));
TRY(header_stream.write_bits(m_bits_per_sample - 1u, 5));
// Leave the sample count as unknown for now.
TRY(header_stream.write_bits(0u, 36));
// TODO: Calculate the MD5 signature of all of the audio data.
auto md5 = TRY(ByteBuffer::create_zeroed(128u / 8u));
TRY(header_stream.write_until_depleted(md5));
FlacRawMetadataBlock streaminfo_block = {
.is_last_block = true,
.type = FlacMetadataBlockType::STREAMINFO,
.length = static_cast<u32>(data.size()),
.data = move(data),
};
TRY(add_metadata_block(move(streaminfo_block), 0));
// Add default padding if necessary.
if (m_cached_metadata_blocks.last().type != FlacMetadataBlockType::PADDING) {
auto padding_data = ByteBuffer::create_zeroed(default_padding);
if (!padding_data.is_error()) {
TRY(add_metadata_block({
.is_last_block = true,
.type = FlacMetadataBlockType::PADDING,
.length = default_padding,
.data = padding_data.release_value(),
}));
}
}
TRY(m_stream->write_until_depleted(flac_magic.bytes()));
m_streaminfo_start_index = TRY(m_stream->tell());
for (size_t i = 0; i < m_cached_metadata_blocks.size(); ++i) {
auto& block = m_cached_metadata_blocks[i];
// Correct is_last_block flag here to avoid index shenanigans in add_metadata_block.
auto const is_last_block = i == m_cached_metadata_blocks.size() - 1;
block.is_last_block = is_last_block;
if (is_last_block) {
m_last_padding = LastPadding {
.start = TRY(m_stream->tell()),
.size = block.length,
};
}
TRY(write_metadata_block(block));
}
m_cached_metadata_blocks.clear();
m_frames_start_index = TRY(m_stream->tell());
return {};
}
ErrorOr<void> FlacWriter::add_metadata_block(FlacRawMetadataBlock block, Optional<size_t> insertion_index)
{
if (m_state != WriteState::HeaderUnwritten)
return Error::from_string_literal("Metadata blocks can only be added before the header is finalized");
if (insertion_index.has_value())
TRY(m_cached_metadata_blocks.try_insert(insertion_index.value(), move(block)));
else
TRY(m_cached_metadata_blocks.try_append(move(block)));
return {};
}
ErrorOr<void> FlacWriter::write_metadata_block(FlacRawMetadataBlock& block)
{
if (m_state == WriteState::FormatFinalized) {
if (!m_last_padding.has_value())
return Error::from_string_literal("No (more) padding available to write block into");
auto const last_padding = m_last_padding.release_value();
if (block.length > last_padding.size)
return Error::from_string_literal("Late metadata block doesn't fit in available padding");
auto const current_position = TRY(m_stream->tell());
ScopeGuard guard = [&] { (void)m_stream->seek(current_position, SeekMode::SetPosition); };
TRY(m_stream->seek(last_padding.start, SeekMode::SetPosition));
// No more padding after this: the new block is the last.
auto new_size = last_padding.size - block.length;
if (new_size == 0)
block.is_last_block = true;
TRY(m_stream->write_value(block));
// If the size is zero, we don't need to write a new padding block.
// If the size is between 1 and 3, we have empty space that cannot be marked with an empty padding block, so we must abort.
// Other code should make sure that this never happens; e.g. our seektable only has sizes divisible by 4 anyways.
// If the size is 4, we have no padding, but the padding block header can be written without any subsequent payload.
if (new_size >= 4) {
FlacRawMetadataBlock new_padding_block {
.is_last_block = true,
.type = FlacMetadataBlockType::PADDING,
.length = static_cast<u32>(new_size),
.data = TRY(ByteBuffer::create_zeroed(new_size)),
};
m_last_padding = LastPadding {
.start = TRY(m_stream->tell()),
.size = new_size,
};
TRY(m_stream->write_value(new_padding_block));
} else if (new_size != 0) {
return Error::from_string_literal("Remaining padding is not divisible by 4, there will be some stray zero bytes!");
}
return {};
}
return m_stream->write_value(block);
}
ErrorOr<void> FlacRawMetadataBlock::write_to_stream(Stream& stream) const
{
BigEndianOutputBitStream bit_stream { MaybeOwned<Stream> { stream } };
TRY(bit_stream.write_bits(static_cast<u8>(is_last_block), 1));
TRY(bit_stream.write_bits(to_underlying(type), 7));
TRY(bit_stream.write_bits(length, 24));
VERIFY(data.size() == length);
TRY(bit_stream.write_until_depleted(data));
return {};
}
ErrorOr<void> FlacWriter::flush_seektable()
{
if (m_cached_seektable.size() == 0)
return {};
auto max_seekpoints = max_number_of_seekpoints();
if (max_seekpoints < m_cached_seektable.size()) {
dbgln("FLAC Warning: There are {} seekpoints, but we only have space for {}. Some seekpoints will be dropped.", m_cached_seektable.size(), max_seekpoints);
// Drop seekpoints in regular intervals to space out the loss of seek precision.
auto const points_to_drop = m_cached_seektable.size() - max_seekpoints;
auto const drop_interval = static_cast<double>(m_cached_seektable.size()) / static_cast<double>(points_to_drop);
double ratio = 0.;
for (size_t i = 0; i < m_cached_seektable.size(); ++i) {
// Avoid dropping the first seekpoint.
if (ratio > drop_interval) {
m_cached_seektable.seek_points().remove(i);
--i;
ratio -= drop_interval;
}
++ratio;
}
// Account for integer division imprecisions.
if (max_seekpoints < m_cached_seektable.size())
m_cached_seektable.seek_points().shrink(max_seekpoints);
}
auto seektable_data = TRY(ByteBuffer::create_zeroed(m_cached_seektable.size() * flac_seekpoint_size));
FixedMemoryStream seektable_stream { seektable_data.bytes() };
for (auto const& seekpoint : m_cached_seektable.seek_points()) {
// https://www.ietf.org/archive/id/draft-ietf-cellar-flac-08.html#name-seekpoint
TRY(seektable_stream.write_value<BigEndian<u64>>(seekpoint.sample_index));
TRY(seektable_stream.write_value<BigEndian<u64>>(seekpoint.byte_offset));
// This is probably wrong for the last frame, but it doesn't seem to matter.
TRY(seektable_stream.write_value<BigEndian<u16>>(block_size));
}
FlacRawMetadataBlock seektable {
.is_last_block = false,
.type = FlacMetadataBlockType::SEEKTABLE,
.length = static_cast<u32>(seektable_data.size()),
.data = move(seektable_data),
};
return write_metadata_block(seektable);
}
// If the given sample count is uncommon, this function will return one of the uncommon marker block sizes.
// The caller has to handle and add these later manually.
static BlockSizeCategory to_common_block_size(u16 sample_count)
{
switch (sample_count) {
case 192:
return BlockSizeCategory::S192;
case 576:
return BlockSizeCategory::S576;
case 1152:
return BlockSizeCategory::S1152;
case 2304:
return BlockSizeCategory::S2304;
case 4608:
return BlockSizeCategory::S4608;
case 256:
return BlockSizeCategory::S256;
case 512:
return BlockSizeCategory::S512;
case 1024:
return BlockSizeCategory::S1024;
case 2048:
return BlockSizeCategory::S2048;
case 4096:
return BlockSizeCategory::S4096;
case 8192:
return BlockSizeCategory::S8192;
case 16384:
return BlockSizeCategory::S16384;
case 32768:
return BlockSizeCategory::S32768;
}
if (sample_count - 1 <= 0xff)
return BlockSizeCategory::Uncommon8Bits;
// Data type guarantees that 16-bit storage is possible.
return BlockSizeCategory::Uncommon16Bits;
}
static ByteBuffer to_utf8(u64 value)
{
ByteBuffer buffer;
if (value < 0x7f) {
buffer.append(static_cast<u8>(value));
} else if (value < 0x7ff) {
buffer.append(static_cast<u8>(0b110'00000 | (value >> 6)));
buffer.append(static_cast<u8>(0b10'000000 | (value & 0b111111)));
} else if (value < 0xffff) {
buffer.append(static_cast<u8>(0b1110'0000 | (value >> 12)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 6) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 0) & 0b111111)));
} else if (value < 0x1f'ffff) {
buffer.append(static_cast<u8>(0b11110'000 | (value >> 18)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 12) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 6) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 0) & 0b111111)));
} else if (value < 0x3ff'ffff) {
buffer.append(static_cast<u8>(0b111110'00 | (value >> 24)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 18) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 12) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 6) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 0) & 0b111111)));
} else if (value < 0x7fff'ffff) {
buffer.append(static_cast<u8>(0b1111110'0 | (value >> 30)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 24) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 18) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 12) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 6) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 0) & 0b111111)));
} else if (value < 0xf'ffff'ffff) {
buffer.append(static_cast<u8>(0b11111110));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 30) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 24) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 18) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 12) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 6) & 0b111111)));
buffer.append(static_cast<u8>(0b10'000000 | ((value >> 0) & 0b111111)));
} else {
// Anything larger is illegal even in expanded UTF-8, but FLAC only passes 32-bit values anyways.
VERIFY_NOT_REACHED();
}
return buffer;
}
ErrorOr<void> FlacFrameHeader::write_to_stream(Stream& stream) const
{
Crypto::Checksum::ChecksummingStream<FlacFrameHeaderCRC> checksumming_stream { MaybeOwned<Stream> { stream } };
BigEndianOutputBitStream bit_stream { MaybeOwned<Stream> { checksumming_stream } };
TRY(bit_stream.write_bits(0b11111111111110u, 14));
TRY(bit_stream.write_bits(0u, 1));
TRY(bit_stream.write_bits(to_underlying(blocking_strategy), 1));
auto common_block_size = to_common_block_size(sample_count);
TRY(bit_stream.write_bits(to_underlying(common_block_size), 4));
// We always store sample rate in the file header.
TRY(bit_stream.write_bits(0u, 4));
TRY(bit_stream.write_bits(to_underlying(channels), 4));
// We always store bit depth in the file header.
TRY(bit_stream.write_bits(0u, 3));
// Reserved zero bit.
TRY(bit_stream.write_bits(0u, 1));
auto coded_number = to_utf8(sample_or_frame_index);
TRY(bit_stream.write_until_depleted(coded_number));
if (common_block_size == BlockSizeCategory::Uncommon8Bits)
TRY(bit_stream.write_value(static_cast<u8>(sample_count - 1)));
if (common_block_size == BlockSizeCategory::Uncommon16Bits)
TRY(bit_stream.write_value(BigEndian<u16>(static_cast<u16>(sample_count - 1))));
// Ensure that the checksum is calculated correctly.
TRY(bit_stream.align_to_byte_boundary());
auto checksum = checksumming_stream.digest();
TRY(bit_stream.write_value(checksum));
return {};
}
ErrorOr<void> FlacWriter::write_samples(ReadonlySpan<Sample> samples)
{
if (m_state == WriteState::FullyFinalized)
return Error::from_string_literal("File is already finalized");
auto remaining_samples = samples;
while (remaining_samples.size() > 0) {
if (m_sample_buffer.size() == block_size) {
TRY(write_frame());
m_sample_buffer.clear();
}
auto amount_to_copy = min(remaining_samples.size(), m_sample_buffer.capacity() - m_sample_buffer.size());
auto current_buffer_size = m_sample_buffer.size();
TRY(m_sample_buffer.try_resize_and_keep_capacity(current_buffer_size + amount_to_copy));
remaining_samples.copy_trimmed_to(m_sample_buffer.span().slice(current_buffer_size));
remaining_samples = remaining_samples.slice(amount_to_copy);
}
// Ensure that the buffer is flushed if possible.
if (m_sample_buffer.size() == block_size) {
TRY(write_frame());
m_sample_buffer.clear();
}
return {};
}
ErrorOr<void> FlacWriter::write_frame()
{
auto frame_samples = move(m_sample_buffer);
// De-interleave and integer-quantize subframes.
float sample_rescale = static_cast<float>(1 << (m_bits_per_sample - 1));
auto subframe_samples = Vector<Vector<i64, block_size>>();
TRY(subframe_samples.try_resize_and_keep_capacity(m_num_channels));
for (auto const& sample : frame_samples) {
TRY(subframe_samples[0].try_append(static_cast<i64>(sample.left * sample_rescale)));
// FIXME: We don't have proper data for any channels past 2.
for (auto i = 1; i < m_num_channels; ++i)
TRY(subframe_samples[i].try_append(static_cast<i64>(sample.right * sample_rescale)));
}
auto channel_type = static_cast<FlacFrameChannelType>(m_num_channels - 1);
if (channel_type == FlacFrameChannelType::Stereo) {
auto const& left_channel = subframe_samples[0];
auto const& right_channel = subframe_samples[1];
Vector<i64, block_size> mid_channel;
Vector<i64, block_size> side_channel;
TRY(mid_channel.try_ensure_capacity(left_channel.size()));
TRY(side_channel.try_ensure_capacity(left_channel.size()));
for (auto i = 0u; i < left_channel.size(); ++i) {
auto mid = (left_channel[i] + right_channel[i]) / 2;
auto side = left_channel[i] - right_channel[i];
mid_channel.unchecked_append(mid);
side_channel.unchecked_append(side);
}
AK::Statistics<i64, AK::DisjointSpans<i64>> normal_costs {
AK::DisjointSpans<i64> { { subframe_samples[0], subframe_samples[1] } }
};
AK::Statistics<i64, AK::DisjointSpans<i64>> correlated_costs {
AK::DisjointSpans<i64> { { mid_channel, side_channel } }
};
if (correlated_costs.standard_deviation() < normal_costs.standard_deviation()) {
dbgln_if(FLAC_ENCODER_DEBUG, "Using channel coupling since sample stddev {} is better than {}", correlated_costs.standard_deviation(), normal_costs.standard_deviation());
channel_type = FlacFrameChannelType::MidSideStereo;
subframe_samples[0] = move(mid_channel);
subframe_samples[1] = move(side_channel);
}
}
auto const sample_index = m_sample_count;
auto const frame_start_byte = TRY(write_frame_for(subframe_samples, channel_type));
// Insert a seekpoint if necessary.
auto const seekpoint_period_samples = m_sample_rate * seekpoint_period_seconds;
auto const last_seekpoint = m_cached_seektable.seek_point_before(sample_index);
if (!last_seekpoint.has_value() || static_cast<double>(sample_index - last_seekpoint->sample_index) >= seekpoint_period_samples) {
dbgln_if(FLAC_ENCODER_DEBUG, "Inserting seekpoint at sample index {} frame start {}", sample_index, frame_start_byte);
TRY(m_cached_seektable.insert_seek_point({
.sample_index = sample_index,
.byte_offset = frame_start_byte - m_frames_start_index,
}));
}
return {};
}
ErrorOr<size_t> FlacWriter::write_frame_for(ReadonlySpan<Vector<i64, block_size>> subblock, FlacFrameChannelType channel_type)
{
auto sample_count = subblock.first().size();
FlacFrameHeader header {
.sample_rate = m_sample_rate,
.sample_count = static_cast<u16>(sample_count),
.sample_or_frame_index = static_cast<u32>(m_current_frame),
.blocking_strategy = BlockingStrategy::Fixed,
.channels = channel_type,
.bit_depth = static_cast<u8>(m_bits_per_sample),
// Calculated for us during header write.
.checksum = 0,
};
auto frame_stream = Crypto::Checksum::ChecksummingStream<IBMCRC> { MaybeOwned<Stream> { *m_stream } };
auto frame_start_offset = TRY(m_stream->tell());
TRY(frame_stream.write_value(header));
BigEndianOutputBitStream bit_stream { MaybeOwned<Stream> { frame_stream } };
for (auto i = 0u; i < subblock.size(); ++i) {
auto const& subframe = subblock[i];
auto bits_per_sample = m_bits_per_sample;
// Side channels need an extra bit per sample.
if ((i == 1 && (channel_type == FlacFrameChannelType::LeftSideStereo || channel_type == FlacFrameChannelType::MidSideStereo))
|| (i == 0 && channel_type == FlacFrameChannelType::RightSideStereo)) {
bits_per_sample++;
}
TRY(write_subframe(subframe.span(), bit_stream, bits_per_sample));
}
TRY(bit_stream.align_to_byte_boundary());
auto frame_crc = frame_stream.digest();
dbgln_if(FLAC_ENCODER_DEBUG, "Frame {:4} CRC: {:04x}", m_current_frame, frame_crc);
TRY(frame_stream.write_value<AK::BigEndian<u16>>(frame_crc));
auto frame_end_offset = TRY(m_stream->tell());
auto frame_size = frame_end_offset - frame_start_offset;
m_max_frame_size = max(m_max_frame_size, frame_size);
m_min_frame_size = min(m_min_frame_size, frame_size);
m_current_frame++;
m_sample_count += sample_count;
return frame_start_offset;
}
ErrorOr<void> FlacWriter::write_subframe(ReadonlySpan<i64> subframe, BigEndianOutputBitStream& bit_stream, u8 bits_per_sample)
{
// The current subframe encoding strategy is as follows:
// - Check if the subframe is constant; use constant encoding in this case.
// - Try all fixed predictors and record the resulting residuals.
// - Estimate their encoding cost by taking the sum of all absolute logarithmic residuals,
// which is an accurate estimate of the final encoded size of the residuals.
// - Accurately estimate the encoding cost of a verbatim subframe.
// - Select the encoding strategy with the lowest cost out of this selection.
auto constant_value = subframe[0];
auto is_constant = true;
for (auto const sample : subframe) {
if (sample != constant_value) {
is_constant = false;
break;
}
}
if (is_constant) {
dbgln_if(FLAC_ENCODER_DEBUG, "Encoding constant frame with value {}", constant_value);
TRY(bit_stream.write_bits(1u, 0));
TRY(bit_stream.write_bits(to_underlying(FlacSubframeType::Constant), 6));
TRY(bit_stream.write_bits(1u, 0));
TRY(bit_stream.write_bits(bit_cast<u64>(constant_value), bits_per_sample));
return {};
}
auto verbatim_cost_bits = subframe.size() * bits_per_sample;
Optional<FlacLPCEncodedSubframe> best_lpc_subframe;
auto current_min_cost = verbatim_cost_bits;
for (auto order : { FlacFixedLPC::Zero, FlacFixedLPC::One, FlacFixedLPC::Two, FlacFixedLPC::Three, FlacFixedLPC::Four }) {
// Too many warm-up samples would be required; the lower-level encoding procedures assume that this was checked.
if (to_underlying(order) > subframe.size())
continue;
auto encode_result = TRY(encode_fixed_lpc(order, subframe, current_min_cost, bits_per_sample));
if (encode_result.has_value() && encode_result.value().residual_cost_bits < current_min_cost) {
current_min_cost = encode_result.value().residual_cost_bits;
best_lpc_subframe = encode_result.release_value();
}
}
// No LPC encoding was better than verbatim.
if (!best_lpc_subframe.has_value()) {
dbgln_if(FLAC_ENCODER_DEBUG, "Best subframe type was Verbatim; encoding {} samples at {} bps = {} bits", subframe.size(), m_bits_per_sample, verbatim_cost_bits);
TRY(write_verbatim_subframe(subframe, bit_stream, bits_per_sample));
} else {
dbgln_if(FLAC_ENCODER_DEBUG, "Best subframe type was Fixed LPC order {} (estimated cost {} bits); encoding {} samples", to_underlying(best_lpc_subframe->coefficients.get<FlacFixedLPC>()), best_lpc_subframe->residual_cost_bits, subframe.size());
TRY(write_lpc_subframe(best_lpc_subframe.release_value(), bit_stream, bits_per_sample));
}
return {};
}
ErrorOr<Optional<FlacLPCEncodedSubframe>> FlacWriter::encode_fixed_lpc(FlacFixedLPC order, ReadonlySpan<i64> subframe, size_t current_min_cost, u8 bits_per_sample)
{
FlacLPCEncodedSubframe lpc {
.warm_up_samples = Vector<i64> { subframe.trim(to_underlying(order)) },
.coefficients = order,
.residuals {},
// Warm-up sample cost.
.residual_cost_bits = to_underlying(order) * bits_per_sample,
.single_partition_optimal_order {},
};
TRY(lpc.residuals.try_ensure_capacity(subframe.size() - to_underlying(order)));
Vector<i64> predicted;
TRY(predicted.try_resize_and_keep_capacity(subframe.size()));
lpc.warm_up_samples.span().copy_trimmed_to(predicted);
// NOTE: Although we can't interrupt the prediction if the corresponding residuals would become too bad,
// we don't need to branch on the order in every loop during prediction, meaning this shouldn't cost us much.
predict_fixed_lpc(order, subframe, predicted);
// There isnt really a way of computing an LPCs cost without performing most of the calculations, including a Rice parameter search.
// This is nevertheless optimized in multiple ways, so that we always bail out once we are sure no improvements can be made.
auto extra_residual_cost = NumericLimits<size_t>::max();
// Keep track of when we want to estimate costs again. We don't do this for every new residual since it's an expensive procedure.
// The likelihood for misprediction is pretty high for large orders; start with a later index for them.
auto next_cost_estimation_index = min(subframe.size() - 1, first_residual_estimation * (to_underlying(order) + 1));
for (auto i = to_underlying(order); i < subframe.size(); ++i) {
auto residual = subframe[i] - predicted[i];
if (!AK::is_within_range<i32>(residual)) {
dbgln_if(FLAC_ENCODER_DEBUG, "Bailing from Fixed LPC order {} due to residual overflow ({} is outside the 32-bit range)", to_underlying(order), residual);
return Optional<FlacLPCEncodedSubframe> {};
}
lpc.residuals.append(residual);
if (i >= next_cost_estimation_index) {
// Find best exponential Golomb order.
// Storing this in the LPC data allows us to automatically reuse the computation during LPC encoding.
// FIXME: Use more than one partition to improve compression.
// FIXME: Investigate whether this can be estimated “good enough” to improve performance at the cost of compression strength.
// Especially at larger sample counts, it is unlikely that we will find a different optimal order.
// Therefore, use a zig-zag search around the previous optimal order.
extra_residual_cost = NumericLimits<size_t>::max();
auto start_order = lpc.single_partition_optimal_order;
size_t useless_parameters = 0;
size_t steps = 0;
constexpr auto max_rice_parameter = AK::exp2(4) - 1;
for (auto offset = 0; start_order + offset < max_rice_parameter || start_order - offset >= 0; ++offset) {
for (auto factor : { -1, 1 }) {
auto k = start_order + factor * offset;
if (k >= max_rice_parameter || k < 0)
continue;
auto order_cost = count_exp_golomb_bits_in(k, lpc.residuals);
if (order_cost < extra_residual_cost) {
extra_residual_cost = order_cost;
lpc.single_partition_optimal_order = k;
} else {
useless_parameters++;
}
steps++;
// Dont do 0 twice.
if (offset == 0)
break;
}
// If we found enough useless parameters, we probably won't find useful ones anymore.
// The only exception is the first ever parameter search, where we search everything.
if (useless_parameters >= useless_parameter_threshold && start_order != 0)
break;
}
// Min cost exceeded; bail out.
if (lpc.residual_cost_bits + extra_residual_cost > current_min_cost) {
dbgln_if(FLAC_ENCODER_DEBUG, " Bailing from Fixed LPC order {} at sample index {} and cost {} (best {})", to_underlying(order), i, lpc.residual_cost_bits + extra_residual_cost, current_min_cost);
return Optional<FlacLPCEncodedSubframe> {};
}
// Figure out when to next estimate costs.
auto estimated_bits_per_residual = static_cast<double>(extra_residual_cost) / static_cast<double>(i);
auto estimated_residuals_for_min_cost = static_cast<double>(current_min_cost) / estimated_bits_per_residual;
auto unchecked_next_cost_estimation_index = AK::round_to<size_t>(estimated_residuals_for_min_cost * (1 - residual_cost_margin));
// Check either at the estimated residual, or the next residual if that is in the past, or the last residual.
next_cost_estimation_index = min(subframe.size() - 1, max(unchecked_next_cost_estimation_index, i + min_residual_estimation_step));
dbgln_if(FLAC_ENCODER_DEBUG, " {} {:4} Estimate cost/residual {:.1f} (param {:2} after {:2} steps), will hit at {:6.1f}, jumping to {:4} (sanitized to {:4})", to_underlying(order), i, estimated_bits_per_residual, lpc.single_partition_optimal_order, steps, estimated_residuals_for_min_cost, unchecked_next_cost_estimation_index, next_cost_estimation_index);
}
}
lpc.residual_cost_bits += extra_residual_cost;
return lpc;
}
void predict_fixed_lpc(FlacFixedLPC order, ReadonlySpan<i64> samples, Span<i64> predicted_output)
{
switch (order) {
case FlacFixedLPC::Zero:
// s_0(t) = 0
for (auto i = to_underlying(order); i < predicted_output.size(); ++i)
predicted_output[i] += 0;
break;
case FlacFixedLPC::One:
// s_1(t) = s(t-1)
for (auto i = to_underlying(order); i < predicted_output.size(); ++i)
predicted_output[i] += samples[i - 1];
break;
case FlacFixedLPC::Two:
// s_2(t) = 2s(t-1) - s(t-2)
for (auto i = to_underlying(order); i < predicted_output.size(); ++i)
predicted_output[i] += 2 * samples[i - 1] - samples[i - 2];
break;
case FlacFixedLPC::Three:
// s_3(t) = 3s(t-1) - 3s(t-2) + s(t-3)
for (auto i = to_underlying(order); i < predicted_output.size(); ++i)
predicted_output[i] += 3 * samples[i - 1] - 3 * samples[i - 2] + samples[i - 3];
break;
case FlacFixedLPC::Four:
// s_4(t) = 4s(t-1) - 6s(t-2) + 4s(t-3) - s(t-4)
for (auto i = to_underlying(order); i < predicted_output.size(); ++i)
predicted_output[i] += 4 * samples[i - 1] - 6 * samples[i - 2] + 4 * samples[i - 3] - samples[i - 4];
break;
default:
VERIFY_NOT_REACHED();
}
}
// https://www.ietf.org/archive/id/draft-ietf-cellar-flac-08.html#name-verbatim-subframe
ErrorOr<void> FlacWriter::write_verbatim_subframe(ReadonlySpan<i64> subframe, BigEndianOutputBitStream& bit_stream, u8 bits_per_sample)
{
TRY(bit_stream.write_bits(0u, 1));
TRY(bit_stream.write_bits(to_underlying(FlacSubframeType::Verbatim), 6));
TRY(bit_stream.write_bits(0u, 1));
for (auto const& sample : subframe)
TRY(bit_stream.write_bits(bit_cast<u64>(sample), bits_per_sample));
return {};
}
// https://www.ietf.org/archive/id/draft-ietf-cellar-flac-08.html#name-fixed-predictor-subframe
ErrorOr<void> FlacWriter::write_lpc_subframe(FlacLPCEncodedSubframe lpc_subframe, BigEndianOutputBitStream& bit_stream, u8 bits_per_sample)
{
// Reserved.
TRY(bit_stream.write_bits(0u, 1));
// 9.2.1 Subframe header (https://www.ietf.org/archive/id/draft-ietf-cellar-flac-08.html#name-subframe-header)
u8 encoded_type;
if (lpc_subframe.coefficients.has<FlacFixedLPC>())
encoded_type = to_underlying(lpc_subframe.coefficients.get<FlacFixedLPC>()) + to_underlying(FlacSubframeType::Fixed);
else
encoded_type = lpc_subframe.coefficients.get<Vector<i64>>().size() - 1 + to_underlying(FlacSubframeType::LPC);
TRY(bit_stream.write_bits(encoded_type, 6));
// No wasted bits per sample (unnecessary for the vast majority of data).
TRY(bit_stream.write_bits(0u, 1));
for (auto const& warm_up_sample : lpc_subframe.warm_up_samples)
TRY(bit_stream.write_bits(bit_cast<u64>(warm_up_sample), bits_per_sample));
// 4-bit Rice parameters.
TRY(bit_stream.write_bits(0b00u, 2));
// Only one partition (2^0 = 1).
TRY(bit_stream.write_bits(0b0000u, 4));
TRY(write_rice_partition(lpc_subframe.single_partition_optimal_order, lpc_subframe.residuals, bit_stream));
return {};
}
ErrorOr<void> FlacWriter::write_rice_partition(u8 k, ReadonlySpan<i64> residuals, BigEndianOutputBitStream& bit_stream)
{
TRY(bit_stream.write_bits(k, 4));
for (auto const& residual : residuals)
TRY(encode_unsigned_exp_golomb(k, static_cast<i32>(residual), bit_stream));
return {};
}
u32 signed_to_rice(i32 x)
{
// Implements (x < 0 ? -1 : 0) + 2 * abs(x) in about half as many instructions.
// The reference encoders implementation is known to be the fastest on -O2/3 clang and gcc:
// x << 1 = multiply by 2.
// For negative numbers, x >> 31 will create an all-ones XOR mask, meaning that the number will be inverted.
// In two's complement this is -value - 1, exactly what we need.
// For positive numbers, x >> 31 == 0.
return static_cast<u32>((x << 1) ^ (x >> 31));
}
// Adopted from https://github.com/xiph/flac/blob/28e4f0528c76b296c561e922ba67d43751990599/src/libFLAC/bitwriter.c#L727
ErrorOr<void> encode_unsigned_exp_golomb(u8 k, i32 value, BigEndianOutputBitStream& bit_stream)
{
auto zigzag_encoded = signed_to_rice(value);
auto msbs = zigzag_encoded >> k;
auto pattern = 1u << k;
pattern |= zigzag_encoded & ((1 << k) - 1);
TRY(bit_stream.write_bits(0u, msbs));
TRY(bit_stream.write_bits(pattern, k + 1));
return {};
}
// Adopted from count_rice_bits_in_partition():
// https://github.com/xiph/flac/blob/28e4f0528c76b296c561e922ba67d43751990599/src/libFLAC/stream_encoder.c#L4299
size_t count_exp_golomb_bits_in(u8 k, ReadonlySpan<i64> residuals)
{
// Exponential Golomb order size (4).
// One unary stop bit and the entire exponential Golomb parameter for every residual.
size_t partition_bits = 4 + (1 + k) * residuals.size();
// Bit magic to compute the amount of leading unary bits.
for (auto const& residual : residuals)
partition_bits += (static_cast<u32>((residual << 1) ^ (residual >> 31)) >> k);
return partition_bits;
}
}

View file

@ -1,158 +0,0 @@
/*
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Encoder.h"
#include "FlacTypes.h"
#include "Forward.h"
#include "GenericTypes.h"
#include "Sample.h"
#include "SampleFormats.h"
#include <AK/MaybeOwned.h>
#include <AK/Noncopyable.h>
#include <AK/RefPtr.h>
#include <AK/Stream.h>
#include <AK/StringView.h>
#include <LibCore/Forward.h>
namespace Audio {
// Encodes the sign representation method used in Rice coding.
// Numbers alternate between positive and negative: 0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5, ...
ALWAYS_INLINE u32 signed_to_rice(i32 x);
// Encode a single number encoded with exponential golomb encoding of the specified order (k).
ALWAYS_INLINE ErrorOr<void> encode_unsigned_exp_golomb(u8 k, i32 value, BigEndianOutputBitStream& bit_stream);
size_t count_exp_golomb_bits_in(u8 k, ReadonlySpan<i64> residuals);
void predict_fixed_lpc(FlacFixedLPC order, ReadonlySpan<i64> samples, Span<i64> predicted_output);
// A simple FLAC encoder that writes FLAC files compatible with the streamable subset.
// The encoder currently has the following simple output properties:
// FIXME: All frames have a fixed sample size, see below.
// FIXME: All frames are encoded with the best fixed LPC predictor.
// FIXME: All residuals are encoded in one Rice partition.
class FlacWriter : public Encoder {
AK_MAKE_NONCOPYABLE(FlacWriter);
AK_MAKE_NONMOVABLE(FlacWriter);
/// Tunable static parameters. Please try to improve these; only some have already been well-tuned!
// Constant block size.
static constexpr size_t block_size = 1024;
// Used as a percentage to check residual costs before the estimated "necessary" estimation point.
// We usually over-estimate residual costs, so this prevents us from overshooting the actual bail point.
static constexpr double residual_cost_margin = 0.07;
// At what sample index to first estimate residuals, so that the residual parameter can "stabilize" through more encoded values.
static constexpr size_t first_residual_estimation = 16;
// How many samples to advance at minimum before estimating residuals again.
static constexpr size_t min_residual_estimation_step = 20;
// After how many useless (i.e. worse than current optimal) Rice parameters to abort parameter search.
// Note that due to the zig-zag search, we start with searching the parameters that are most likely to be good.
static constexpr size_t useless_parameter_threshold = 2;
// How often a seek point is inserted.
static constexpr double seekpoint_period_seconds = 2.0;
// Default padding reserved for seek points; enough for almost 4 minutes of audio.
static constexpr size_t default_padding = 2048;
enum class WriteState {
// Header has not been written at all, audio data cannot be written.
HeaderUnwritten,
// Header was written, i.e. sample format is finalized,
// but audio data has not been finalized and therefore some header information is still missing.
FormatFinalized,
// File is fully finalized, no more sample data can be written.
FullyFinalized,
};
public:
static ErrorOr<NonnullOwnPtr<FlacWriter>> create(NonnullOwnPtr<SeekableStream> stream, u32 sample_rate = 44100, u8 num_channels = 2, u16 bits_per_sample = 16);
virtual ~FlacWriter();
virtual ErrorOr<void> write_samples(ReadonlySpan<Sample> samples) override;
virtual ErrorOr<void> finalize() override;
u32 sample_rate() const { return m_sample_rate; }
u8 num_channels() const { return m_num_channels; }
PcmSampleFormat sample_format() const { return integer_sample_format_for(m_bits_per_sample).value(); }
Stream const& output_stream() const { return *m_stream; }
ErrorOr<void> set_num_channels(u8 num_channels);
ErrorOr<void> set_sample_rate(u32 sample_rate);
ErrorOr<void> set_bits_per_sample(u16 bits_per_sample);
// The FLAC encoder by default tries to reserve some space for seek points,
// but that may not be enough if more than approximately four minutes of audio are stored.
// The sample count hint can be used to instruct the FLAC encoder on how much space to reserve for seek points,
// which will both reduce the padding for small files and allow the FLAC encoder to write seek points at the end of large files.
virtual void sample_count_hint(size_t sample_count) override;
virtual ErrorOr<void> set_metadata(Metadata const& metadata) override;
ErrorOr<void> finalize_header_format();
private:
FlacWriter(NonnullOwnPtr<SeekableStream>);
ErrorOr<void> write_header();
ErrorOr<void> write_frame();
// Returns the frame start byte offset, to be used for creating a seektable.
ErrorOr<size_t> write_frame_for(ReadonlySpan<Vector<i64, block_size>> subblock, FlacFrameChannelType channel_type);
ErrorOr<void> write_subframe(ReadonlySpan<i64> subframe, BigEndianOutputBitStream& bit_stream, u8 bits_per_sample);
ErrorOr<void> write_lpc_subframe(FlacLPCEncodedSubframe lpc_subframe, BigEndianOutputBitStream& bit_stream, u8 bits_per_sample);
ErrorOr<void> write_verbatim_subframe(ReadonlySpan<i64> subframe, BigEndianOutputBitStream& bit_stream, u8 bits_per_sample);
// Assumes 4-bit k for now.
ErrorOr<void> write_rice_partition(u8 k, ReadonlySpan<i64> residuals, BigEndianOutputBitStream& bit_stream);
// Aborts encoding once the costs exceed the previous minimum, thereby speeding up the encoder's parameter search.
// In this case, an empty Optional is returned.
ErrorOr<Optional<FlacLPCEncodedSubframe>> encode_fixed_lpc(FlacFixedLPC order, ReadonlySpan<i64> subframe, size_t current_min_cost, u8 bits_per_sample);
ErrorOr<void> add_metadata_block(FlacRawMetadataBlock block, Optional<size_t> insertion_index = {});
// Depending on whether the header is finished or not, we either write to the current position for an unfinished header,
// or we write to the start of the last padding and adjust that padding block.
ErrorOr<void> write_metadata_block(FlacRawMetadataBlock& block);
// Determine how many seekpoints we can write depending on the size of our final padding.
size_t max_number_of_seekpoints() const;
ErrorOr<void> flush_seektable();
NonnullOwnPtr<SeekableStream> m_stream;
WriteState m_state { WriteState::HeaderUnwritten };
Vector<Sample, block_size> m_sample_buffer {};
size_t m_current_frame { 0 };
u32 m_sample_rate;
u8 m_num_channels;
u16 m_bits_per_sample;
// Data updated during encoding; written to the header at the end.
u32 m_max_frame_size { 0 };
u32 m_min_frame_size { NumericLimits<u32>::max() };
size_t m_sample_count { 0 };
// Remember where the STREAMINFO block was written in the stream.
size_t m_streaminfo_start_index;
// Start of the first frame, used for calculating seektable byte offsets.
size_t m_frames_start_index;
struct LastPadding {
size_t start;
size_t size;
};
// Remember last PADDING block data, since we overwrite part of it with "late" metadata blocks.
Optional<LastPadding> m_last_padding;
// Raw metadata blocks that will be written out before header finalization.
Vector<FlacRawMetadataBlock> m_cached_metadata_blocks;
// The full seektable, may be fully or partially written.
SeekTable m_cached_seektable {};
};
}

View file

@ -10,7 +10,6 @@ namespace Audio {
class ConnectionToServer;
class Loader;
class Encoder;
struct Person;
struct Metadata;
class PlaybackStream;

View file

@ -39,18 +39,6 @@ Optional<StringView> Person::name_for_role() const
VERIFY_NOT_REACHED();
}
void Metadata::replace_encoder_with_serenity()
{
auto version_or_error = Core::Version::read_long_version_string();
// Unset the encoder field in this case; we definitely want to replace the existing encoder field.
if (version_or_error.is_error())
encoder = {};
auto encoder_string = String::formatted("SerenityOS LibMedia {}", version_or_error.release_value());
if (encoder_string.is_error())
encoder = {};
encoder = encoder_string.release_value();
}
Optional<String> Metadata::first_artist() const
{
auto artist = people.find_if([](auto const& person) { return person.is_artist(); });

View file

@ -42,7 +42,6 @@ struct Person {
struct Metadata {
using Year = unsigned;
void replace_encoder_with_serenity();
ErrorOr<void> add_miscellaneous(String const& field, String value);
ErrorOr<void> add_person(Person::Role role, String name);
Optional<String> first_artist() const;

View file

@ -1,141 +0,0 @@
/*
* Copyright (c) 2020, William McPherson <willmcpherson2@gmail.com>
* Copyright (c) 2023, Cameron Youell <cameronyouell@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "WavLoader.h"
#include "WavTypes.h"
#include "WavWriter.h"
#include <AK/Endian.h>
namespace Audio {
ErrorOr<NonnullOwnPtr<WavWriter>> WavWriter::create_from_file(StringView path, u32 sample_rate, u16 num_channels, PcmSampleFormat sample_format)
{
auto wav_writer = TRY(adopt_nonnull_own_or_enomem(new (nothrow) WavWriter(sample_rate, num_channels, sample_format)));
TRY(wav_writer->set_file(path));
return wav_writer;
}
WavWriter::WavWriter(u32 sample_rate, u16 num_channels, PcmSampleFormat sample_format)
: m_sample_rate(sample_rate)
, m_num_channels(num_channels)
, m_sample_format(sample_format)
{
}
WavWriter::~WavWriter()
{
if (!m_finalized)
(void)finalize();
}
ErrorOr<void> WavWriter::set_file(StringView path)
{
auto file = TRY(Core::File::open(path, Core::File::OpenMode::Write));
m_file = TRY(Core::OutputBufferedFile::create(move(file)));
TRY(m_file->seek(44, SeekMode::SetPosition));
m_finalized = false;
return {};
}
ErrorOr<void> WavWriter::write_samples(ReadonlySpan<Sample> samples)
{
switch (m_sample_format) {
// FIXME: For non-float formats, we don't add good quantization noise, leading to possibly unpleasant quantization artifacts.
case PcmSampleFormat::Uint8: {
constexpr float scale = static_cast<float>(NumericLimits<u8>::max()) * .5f;
for (auto const& sample : samples) {
u8 left = static_cast<u8>((sample.left + 1) * scale);
u8 right = static_cast<u8>((sample.right + 1) * scale);
TRY(m_file->write_value(left));
if (m_num_channels >= 2)
TRY(m_file->write_value(right));
}
m_data_sz += samples.size() * m_num_channels * sizeof(u8);
break;
}
case PcmSampleFormat::Int16: {
constexpr float scale = static_cast<float>(NumericLimits<i16>::max());
for (auto const& sample : samples) {
u16 left = AK::convert_between_host_and_little_endian(static_cast<i16>(sample.left * scale));
u16 right = AK::convert_between_host_and_little_endian(static_cast<i16>(sample.right * scale));
TRY(m_file->write_value(left));
if (m_num_channels >= 2)
TRY(m_file->write_value(right));
}
m_data_sz += samples.size() * m_num_channels * sizeof(u16);
break;
}
default:
VERIFY_NOT_REACHED();
}
return {};
}
ErrorOr<void> WavWriter::finalize()
{
VERIFY(!m_finalized);
m_finalized = true;
if (m_file && m_file->is_open()) {
TRY(m_file->seek(0, SeekMode::SetPosition));
TRY(write_header());
m_file->close();
}
m_data_sz = 0;
return {};
}
ErrorOr<void> WavWriter::write_header()
{
// "RIFF"
static u32 riff = 0x46464952;
TRY(m_file->write_value(riff));
// Size of data + (size of header - previous field - this field)
u32 sz = m_data_sz + (44 - 4 - 4);
TRY(m_file->write_value(sz));
// "WAVE"
static u32 wave = 0x45564157;
TRY(m_file->write_value(wave));
// "fmt "
static u32 fmt_id = 0x20746D66;
TRY(m_file->write_value(fmt_id));
// Size of the next 6 fields
static u32 fmt_size = 16;
TRY(m_file->write_value(fmt_size));
static u16 audio_format = to_underlying(Wav::WaveFormat::Pcm);
TRY(m_file->write_value(audio_format));
TRY(m_file->write_value(m_num_channels));
TRY(m_file->write_value(m_sample_rate));
VERIFY(m_sample_format == PcmSampleFormat::Int16 || m_sample_format == PcmSampleFormat::Uint8);
u16 bits_per_sample = pcm_bits_per_sample(m_sample_format);
u32 byte_rate = m_sample_rate * m_num_channels * (bits_per_sample / 8);
TRY(m_file->write_value(byte_rate));
u16 block_align = m_num_channels * (bits_per_sample / 8);
TRY(m_file->write_value(block_align));
TRY(m_file->write_value(bits_per_sample));
// "data"
static u32 chunk_id = 0x61746164;
TRY(m_file->write_value(chunk_id));
TRY(m_file->write_value(m_data_sz));
return {};
}
}

View file

@ -1,46 +0,0 @@
/*
* Copyright (c) 2020, William McPherson <willmcpherson2@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Encoder.h"
#include "Sample.h"
#include "SampleFormats.h"
#include <AK/ByteString.h>
#include <AK/Noncopyable.h>
#include <AK/RefPtr.h>
#include <AK/StringView.h>
#include <LibCore/File.h>
#include <LibCore/Forward.h>
namespace Audio {
class WavWriter : public Encoder {
AK_MAKE_NONCOPYABLE(WavWriter);
AK_MAKE_NONMOVABLE(WavWriter);
public:
static ErrorOr<NonnullOwnPtr<WavWriter>> create_from_file(StringView path, u32 sample_rate = 44100, u16 num_channels = 2, PcmSampleFormat sample_format = PcmSampleFormat::Int16);
WavWriter(u32 sample_rate = 44100, u16 num_channels = 2, PcmSampleFormat sample_format = PcmSampleFormat::Int16);
~WavWriter();
virtual ErrorOr<void> write_samples(ReadonlySpan<Sample> samples) override;
virtual ErrorOr<void> finalize() override;
ErrorOr<void> set_file(StringView path);
private:
ErrorOr<void> write_header();
OwnPtr<Core::OutputBufferedFile> m_file;
bool m_finalized { false };
u32 m_sample_rate;
u16 m_num_channels;
PcmSampleFormat m_sample_format;
u32 m_data_sz { 0 };
};
}

View file

@ -4,8 +4,6 @@ set(SOURCES
Audio/Loader.cpp
Audio/WavLoader.cpp
Audio/FlacLoader.cpp
Audio/FlacWriter.cpp
Audio/WavWriter.cpp
Audio/Metadata.cpp
Audio/MP3Loader.cpp
Audio/PlaybackStream.cpp

View file

@ -1,185 +0,0 @@
/*
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/LexicalPath.h>
#include <AK/Types.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/System.h>
#include <LibFileSystem/FileSystem.h>
#include <LibMain/Main.h>
#include <LibMedia/Audio/Encoder.h>
#include <LibMedia/Audio/FlacWriter.h>
#include <LibMedia/Audio/Loader.h>
#include <LibMedia/Audio/WavWriter.h>
#include <stdio.h>
static ErrorOr<StringView> guess_format_from_extension(StringView path)
{
if (path == "-"sv)
return Error::from_string_literal("Cannot guess format for standard stream, please specify format manually");
LexicalPath lexical_path { path };
auto extension = lexical_path.extension();
if (extension.is_empty())
return Error::from_string_literal("Cannot guess format for file without file extension");
// Note: Do not return the `extension` StringView in any case, since that will possibly lead to UAF.
if (extension == "wav"sv || extension == "wave"sv)
return "wav"sv;
if (extension == "flac"sv)
return "flac"sv;
if (extension == "mp3"sv || extension == "mpeg3"sv)
return "mp3"sv;
if (extension == "qoa"sv)
return "qoa"sv;
return Error::from_string_literal("Cannot guess format for the given file extension");
}
static ErrorOr<Audio::PcmSampleFormat> parse_sample_format(StringView textual_format)
{
if (textual_format == "u8"sv)
return Audio::PcmSampleFormat::Uint8;
if (textual_format == "s16le"sv)
return Audio::PcmSampleFormat::Int16;
if (textual_format == "s24le"sv)
return Audio::PcmSampleFormat::Int24;
if (textual_format == "s32le"sv)
return Audio::PcmSampleFormat::Int32;
if (textual_format == "f32le"sv)
return Audio::PcmSampleFormat::Float32;
if (textual_format == "f64le"sv)
return Audio::PcmSampleFormat::Float64;
return Error::from_string_literal("Unknown sample format");
}
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
TRY(Core::System::pledge("stdio rpath wpath cpath"));
StringView input {};
StringView output {};
StringView input_format {};
StringView output_format {};
StringView output_sample_format;
Core::ArgsParser args_parser;
args_parser.set_general_help("Convert between audio formats");
args_parser.add_option(input, "Audio file to convert (or '-' for standard input)", "input", 'i', "input");
args_parser.add_option(input_format, "Force input codec and container (see manual for supported codecs and containers)", "input-audio-codec", 0, "input-codec");
args_parser.add_option(output_format, "Set output codec", "audio-codec", 0, "output-codec");
args_parser.add_option(output_sample_format, "Set output sample format (see manual for supported formats)", "audio-format", 0, "sample-format");
args_parser.add_option(output, "Target file (or '-' for standard output)", "output", 'o', "output");
args_parser.parse(arguments);
if (input.is_empty())
return Error::from_string_literal("Input file is required, use '-' to read from standard input");
if (output_format.is_empty() && output == "-"sv)
return Error::from_string_literal("Output format must be specified manually when writing to standard output");
if (input != "-"sv)
TRY(Core::System::unveil(TRY(FileSystem::absolute_path(input)), "r"sv));
if (output != "-"sv)
TRY(Core::System::unveil(TRY(FileSystem::absolute_path(output)), "rwc"sv));
TRY(Core::System::unveil(nullptr, nullptr));
RefPtr<Audio::Loader> input_loader;
// Use normal loader infrastructure to guess input format.
if (input_format.is_empty()) {
auto loader_or_error = Audio::Loader::create(input);
if (loader_or_error.is_error()) {
warnln("Could not guess codec for input file '{}'. Try forcing a codec with '--input-audio-codec'", input);
return 1;
}
input_loader = loader_or_error.release_value();
} else {
warnln("Forcing input codec is not supported");
return 1;
}
VERIFY(input_loader);
if (output_format.is_empty())
output_format = TRY(guess_format_from_extension(output));
VERIFY(!output_format.is_empty());
Optional<NonnullOwnPtr<Audio::Encoder>> writer;
if (!output.is_empty()) {
if (output_format == "wav"sv) {
auto parsed_output_sample_format = input_loader->pcm_format();
if (!output_sample_format.is_empty())
parsed_output_sample_format = TRY(parse_sample_format(output_sample_format));
writer.emplace(TRY(Audio::WavWriter::create_from_file(
output,
static_cast<int>(input_loader->sample_rate()),
input_loader->num_channels(),
parsed_output_sample_format)));
} else if (output_format == "flac"sv) {
auto parsed_output_sample_format = input_loader->pcm_format();
if (!output_sample_format.is_empty())
parsed_output_sample_format = TRY(parse_sample_format(output_sample_format));
if (!Audio::is_integer_format(parsed_output_sample_format)) {
warnln("FLAC does not support sample format {}", Audio::sample_format_name(parsed_output_sample_format));
return 1;
}
auto output_stream = TRY(Core::OutputBufferedFile::create(TRY(Core::File::open(output, Core::File::OpenMode::Write | Core::File::OpenMode::Truncate))));
auto flac_writer = TRY(Audio::FlacWriter::create(
move(output_stream),
static_cast<int>(input_loader->sample_rate()),
input_loader->num_channels(),
Audio::pcm_bits_per_sample(parsed_output_sample_format)));
writer.emplace(move(flac_writer));
} else {
warnln("Codec {} is not supported for encoding", output_format);
return 1;
}
if (writer.has_value()) {
(*writer)->sample_count_hint(input_loader->total_samples());
auto metadata = input_loader->metadata();
metadata.replace_encoder_with_serenity();
TRY((*writer)->set_metadata(metadata));
}
// FIXME: Maybe use a generalized interface for this as well if the need arises.
if (output_format == "flac"sv)
TRY(static_cast<Audio::FlacWriter*>(writer->ptr())->finalize_header_format());
if (output != "-"sv)
out("Writing: \033[s");
auto start = MonotonicTime::now();
while (input_loader->loaded_samples() < input_loader->total_samples()) {
auto samples_or_error = input_loader->get_more_samples();
if (samples_or_error.is_error()) {
warnln("Error while loading samples: {} (at {})", samples_or_error.error().description, samples_or_error.error().index);
return 1;
}
auto samples = samples_or_error.release_value();
if (writer.has_value())
TRY((*writer)->write_samples(samples.span()));
// TODO: Show progress updates like aplay by moving the progress calculation into a common utility function.
if (output != "-"sv) {
out("\033[u{}/{}", input_loader->loaded_samples(), input_loader->total_samples());
fflush(stdout);
}
}
auto end = MonotonicTime::now();
auto seconds_to_write = (end - start).to_milliseconds() / 1000.0;
dbgln("Wrote {} samples in {:.3f}s, {:3.2f}% realtime", input_loader->loaded_samples(), seconds_to_write, input_loader->loaded_samples() / static_cast<double>(input_loader->sample_rate()) / seconds_to_write * 100.0);
if (writer.has_value())
TRY((*writer)->finalize());
if (output != "-"sv)
outln();
}
return 0;
}