mirror of
https://github.com/RGBCube/serenity
synced 2025-05-14 05:54:58 +00:00
LibAudio: Automatically write a FLAC seektable
This contains a seekpoint every 2 seconds, allowing our own players to work better.
This commit is contained in:
parent
c0d7749654
commit
5054f34b4a
5 changed files with 213 additions and 7 deletions
|
@ -37,6 +37,8 @@ using FlacFrameHeaderCRC = Crypto::Checksum::CRC8<flac_polynomial>;
|
|||
static constexpr u16 ibm_polynomial = 0xA001;
|
||||
using IBMCRC = Crypto::Checksum::CRC16<ibm_polynomial>;
|
||||
|
||||
static constexpr size_t flac_seekpoint_size = (64 + 64 + 16) / 8;
|
||||
|
||||
// 11.8 BLOCK_TYPE (7 bits)
|
||||
enum class FlacMetadataBlockType : u8 {
|
||||
STREAMINFO = 0, // Important data about the audio format
|
||||
|
|
|
@ -61,6 +61,8 @@ ErrorOr<void> FlacWriter::finalize()
|
|||
TRY(bit_stream.align_to_byte_boundary());
|
||||
}
|
||||
|
||||
TRY(flush_seektable());
|
||||
|
||||
// TODO: Write the audio data MD5 to the header.
|
||||
|
||||
m_stream->close();
|
||||
|
@ -124,6 +126,53 @@ ErrorOr<void> FlacWriter::set_metadata(Metadata const& metadata)
|
|||
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;
|
||||
|
@ -158,6 +207,19 @@ ErrorOr<void> FlacWriter::write_header()
|
|||
};
|
||||
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());
|
||||
|
||||
|
@ -166,11 +228,18 @@ ErrorOr<void> FlacWriter::write_header()
|
|||
// 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 {};
|
||||
}
|
||||
|
||||
|
@ -187,8 +256,50 @@ ErrorOr<void> FlacWriter::add_metadata_block(FlacRawMetadataBlock block, Optiona
|
|||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> FlacWriter::write_metadata_block(FlacRawMetadataBlock const& block)
|
||||
ErrorOr<void> FlacWriter::write_metadata_block(FlacRawMetadataBlock& block)
|
||||
{
|
||||
if (m_state == WriteState::FormatFinalized) {
|
||||
if (!m_last_padding.has_value())
|
||||
return Error::from_string_view("No (more) padding available to write block into"sv);
|
||||
|
||||
auto const last_padding = m_last_padding.release_value();
|
||||
if (block.length > last_padding.size)
|
||||
return Error::from_string_view("Late metadata block doesn't fit in available padding"sv);
|
||||
|
||||
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_view("Remaining padding is not divisible by 4, there will be some stray zero bytes!"sv);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
return m_stream->write_value(block);
|
||||
}
|
||||
|
||||
|
@ -204,6 +315,52 @@ ErrorOr<void> FlacRawMetadataBlock::write_to_stream(Stream& stream) const
|
|||
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)
|
||||
|
@ -394,10 +551,24 @@ ErrorOr<void> FlacWriter::write_frame()
|
|||
}
|
||||
}
|
||||
|
||||
return write_frame_for(subframe_samples, channel_type);
|
||||
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<void> FlacWriter::write_frame_for(ReadonlySpan<Vector<i64, block_size>> subblock, FlacFrameChannelType channel_type)
|
||||
ErrorOr<size_t> FlacWriter::write_frame_for(ReadonlySpan<Vector<i64, block_size>> subblock, FlacFrameChannelType channel_type)
|
||||
{
|
||||
auto sample_count = subblock.first().size();
|
||||
|
||||
|
@ -406,7 +577,6 @@ ErrorOr<void> FlacWriter::write_frame_for(ReadonlySpan<Vector<i64, block_size>>
|
|||
.sample_count = static_cast<u16>(sample_count),
|
||||
.sample_or_frame_index = static_cast<u32>(m_current_frame),
|
||||
.blocking_strategy = BlockingStrategy::Fixed,
|
||||
// FIXME: We should brute-force channel coupling for stereo.
|
||||
.channels = channel_type,
|
||||
.bit_depth = static_cast<u8>(m_bits_per_sample),
|
||||
// Calculated for us during header write.
|
||||
|
@ -444,7 +614,7 @@ ErrorOr<void> FlacWriter::write_frame_for(ReadonlySpan<Vector<i64, block_size>>
|
|||
m_current_frame++;
|
||||
m_sample_count += sample_count;
|
||||
|
||||
return {};
|
||||
return frame_start_offset;
|
||||
}
|
||||
|
||||
ErrorOr<void> FlacWriter::write_subframe(ReadonlySpan<i64> subframe, BigEndianOutputBitStream& bit_stream, u8 bits_per_sample)
|
||||
|
|
|
@ -55,6 +55,10 @@ class FlacWriter : public Encoder {
|
|||
// 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.
|
||||
|
@ -83,6 +87,12 @@ public:
|
|||
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();
|
||||
|
@ -92,7 +102,8 @@ private:
|
|||
ErrorOr<void> write_header();
|
||||
|
||||
ErrorOr<void> write_frame();
|
||||
ErrorOr<void> write_frame_for(ReadonlySpan<Vector<i64, block_size>> subblock, FlacFrameChannelType channel_type);
|
||||
// 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);
|
||||
|
@ -104,7 +115,12 @@ private:
|
|||
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 = {});
|
||||
ErrorOr<void> write_metadata_block(FlacRawMetadataBlock const& block);
|
||||
// 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 };
|
||||
|
@ -122,9 +138,21 @@ private:
|
|||
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 {};
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -21,6 +21,11 @@ ReadonlySpan<SeekPoint> SeekTable::seek_points() const
|
|||
return m_seek_points.span();
|
||||
}
|
||||
|
||||
Vector<SeekPoint>& SeekTable::seek_points()
|
||||
{
|
||||
return m_seek_points;
|
||||
}
|
||||
|
||||
Optional<SeekPoint const&> SeekTable::seek_point_before(u64 sample_index) const
|
||||
{
|
||||
if (m_seek_points.is_empty())
|
||||
|
|
|
@ -66,6 +66,7 @@ public:
|
|||
|
||||
size_t size() const;
|
||||
ReadonlySpan<SeekPoint> seek_points() const;
|
||||
Vector<SeekPoint>& seek_points();
|
||||
|
||||
ErrorOr<void> insert_seek_point(SeekPoint);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue