mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-01-23 01:32:14 -05:00
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:
parent
85fd2e281b
commit
7728633906
Notes:
github-actions[bot]
2024-09-12 08:02:33 +00:00
Author: https://github.com/gmta Commit: https://github.com/LadybirdBrowser/ladybird/commit/77286339067 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/1369
13 changed files with 0 additions and 1521 deletions
|
@ -449,7 +449,6 @@ endif()
|
||||||
|
|
||||||
# Lagom Utilities
|
# Lagom Utilities
|
||||||
lagom_utility(abench SOURCES ../../Userland/Utilities/abench.cpp LIBS LibMain LibFileSystem LibMedia)
|
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)
|
if (ENABLE_GUI_TARGETS)
|
||||||
lagom_utility(animation SOURCES ../../Userland/Utilities/animation.cpp LIBS LibGfx LibMain)
|
lagom_utility(animation SOURCES ../../Userland/Utilities/animation.cpp LIBS LibGfx LibMain)
|
||||||
|
|
|
@ -15,7 +15,6 @@ shared_library("LibMedia") {
|
||||||
"Audio/UserSampleQueue.cpp",
|
"Audio/UserSampleQueue.cpp",
|
||||||
"Audio/VorbisComment.cpp",
|
"Audio/VorbisComment.cpp",
|
||||||
"Audio/WavLoader.cpp",
|
"Audio/WavLoader.cpp",
|
||||||
"Audio/WavWriter.cpp",
|
|
||||||
]
|
]
|
||||||
if (enable_pulseaudio) {
|
if (enable_pulseaudio) {
|
||||||
sources += [
|
sources += [
|
||||||
|
|
|
@ -5,40 +5,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <AK/ByteString.h>
|
#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/WavLoader.h>
|
||||||
#include <LibMedia/Audio/WavWriter.h>
|
|
||||||
#include <LibTest/TestCase.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)
|
static void run_test(StringView file_name, int const num_samples, int const channels, u32 const rate)
|
||||||
{
|
{
|
||||||
|
|
||||||
constexpr auto format = "RIFF WAVE (.wav)";
|
constexpr auto format = "RIFF WAVE (.wav)";
|
||||||
constexpr int bits = 16;
|
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);
|
ByteString in_path = ByteString::formatted("WAV/{}", file_name);
|
||||||
|
|
||||||
auto loader = TRY_OR_FAIL(Audio::Loader::create(in_path));
|
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->num_channels(), channels);
|
||||||
EXPECT_EQ(loader->bits_per_sample(), bits);
|
EXPECT_EQ(loader->bits_per_sample(), bits);
|
||||||
EXPECT_EQ(loader->total_samples(), num_samples);
|
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
|
// 5 seconds, 16-bit audio samples
|
||||||
|
|
|
@ -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) { }
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 isn’t really a way of computing an LPC’s 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++;
|
|
||||||
// Don’t 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 encoder’s 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 {};
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -10,7 +10,6 @@ namespace Audio {
|
||||||
|
|
||||||
class ConnectionToServer;
|
class ConnectionToServer;
|
||||||
class Loader;
|
class Loader;
|
||||||
class Encoder;
|
|
||||||
struct Person;
|
struct Person;
|
||||||
struct Metadata;
|
struct Metadata;
|
||||||
class PlaybackStream;
|
class PlaybackStream;
|
||||||
|
|
|
@ -39,18 +39,6 @@ Optional<StringView> Person::name_for_role() const
|
||||||
VERIFY_NOT_REACHED();
|
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
|
Optional<String> Metadata::first_artist() const
|
||||||
{
|
{
|
||||||
auto artist = people.find_if([](auto const& person) { return person.is_artist(); });
|
auto artist = people.find_if([](auto const& person) { return person.is_artist(); });
|
||||||
|
|
|
@ -42,7 +42,6 @@ struct Person {
|
||||||
struct Metadata {
|
struct Metadata {
|
||||||
using Year = unsigned;
|
using Year = unsigned;
|
||||||
|
|
||||||
void replace_encoder_with_serenity();
|
|
||||||
ErrorOr<void> add_miscellaneous(String const& field, String value);
|
ErrorOr<void> add_miscellaneous(String const& field, String value);
|
||||||
ErrorOr<void> add_person(Person::Role role, String name);
|
ErrorOr<void> add_person(Person::Role role, String name);
|
||||||
Optional<String> first_artist() const;
|
Optional<String> first_artist() const;
|
||||||
|
|
|
@ -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 {};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 };
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -4,8 +4,6 @@ set(SOURCES
|
||||||
Audio/Loader.cpp
|
Audio/Loader.cpp
|
||||||
Audio/WavLoader.cpp
|
Audio/WavLoader.cpp
|
||||||
Audio/FlacLoader.cpp
|
Audio/FlacLoader.cpp
|
||||||
Audio/FlacWriter.cpp
|
|
||||||
Audio/WavWriter.cpp
|
|
||||||
Audio/Metadata.cpp
|
Audio/Metadata.cpp
|
||||||
Audio/MP3Loader.cpp
|
Audio/MP3Loader.cpp
|
||||||
Audio/PlaybackStream.cpp
|
Audio/PlaybackStream.cpp
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
Loading…
Reference in a new issue