diff --git a/Userland/Libraries/LibAudio/FlacTypes.h b/Userland/Libraries/LibAudio/FlacTypes.h index 2cc97d2663..64026a5756 100644 --- a/Userland/Libraries/LibAudio/FlacTypes.h +++ b/Userland/Libraries/LibAudio/FlacTypes.h @@ -37,6 +37,8 @@ using FlacFrameHeaderCRC = Crypto::Checksum::CRC8; static constexpr u16 ibm_polynomial = 0xA001; using IBMCRC = Crypto::Checksum::CRC16; +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 diff --git a/Userland/Libraries/LibAudio/FlacWriter.cpp b/Userland/Libraries/LibAudio/FlacWriter.cpp index 257c96362b..18c66efb8c 100644 --- a/Userland/Libraries/LibAudio/FlacWriter.cpp +++ b/Userland/Libraries/LibAudio/FlacWriter.cpp @@ -61,6 +61,8 @@ ErrorOr 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 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(static_cast(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(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 FlacWriter::write_header() { ByteBuffer data; @@ -158,6 +207,19 @@ ErrorOr 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 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 FlacWriter::add_metadata_block(FlacRawMetadataBlock block, Optiona return {}; } -ErrorOr FlacWriter::write_metadata_block(FlacRawMetadataBlock const& block) +ErrorOr 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(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 FlacRawMetadataBlock::write_to_stream(Stream& stream) const return {}; } +ErrorOr 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(m_cached_seektable.size()) / static_cast(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>(seekpoint.sample_index)); + TRY(seektable_stream.write_value>(seekpoint.byte_offset)); + // This is probably wrong for the last frame, but it doesn't seem to matter. + TRY(seektable_stream.write_value>(block_size)); + } + + FlacRawMetadataBlock seektable { + .is_last_block = false, + .type = FlacMetadataBlockType::SEEKTABLE, + .length = static_cast(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 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(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 FlacWriter::write_frame_for(ReadonlySpan> subblock, FlacFrameChannelType channel_type) +ErrorOr FlacWriter::write_frame_for(ReadonlySpan> subblock, FlacFrameChannelType channel_type) { auto sample_count = subblock.first().size(); @@ -406,7 +577,6 @@ ErrorOr FlacWriter::write_frame_for(ReadonlySpan> .sample_count = static_cast(sample_count), .sample_or_frame_index = static_cast(m_current_frame), .blocking_strategy = BlockingStrategy::Fixed, - // FIXME: We should brute-force channel coupling for stereo. .channels = channel_type, .bit_depth = static_cast(m_bits_per_sample), // Calculated for us during header write. @@ -444,7 +614,7 @@ ErrorOr FlacWriter::write_frame_for(ReadonlySpan> m_current_frame++; m_sample_count += sample_count; - return {}; + return frame_start_offset; } ErrorOr FlacWriter::write_subframe(ReadonlySpan subframe, BigEndianOutputBitStream& bit_stream, u8 bits_per_sample) diff --git a/Userland/Libraries/LibAudio/FlacWriter.h b/Userland/Libraries/LibAudio/FlacWriter.h index fbb05492a6..21e2631dd9 100644 --- a/Userland/Libraries/LibAudio/FlacWriter.h +++ b/Userland/Libraries/LibAudio/FlacWriter.h @@ -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 set_sample_rate(u32 sample_rate); ErrorOr 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 set_metadata(Metadata const& metadata) override; ErrorOr finalize_header_format(); @@ -92,7 +102,8 @@ private: ErrorOr write_header(); ErrorOr write_frame(); - ErrorOr write_frame_for(ReadonlySpan> subblock, FlacFrameChannelType channel_type); + // Returns the frame start byte offset, to be used for creating a seektable. + ErrorOr write_frame_for(ReadonlySpan> subblock, FlacFrameChannelType channel_type); ErrorOr write_subframe(ReadonlySpan subframe, BigEndianOutputBitStream& bit_stream, u8 bits_per_sample); ErrorOr write_lpc_subframe(FlacLPCEncodedSubframe lpc_subframe, BigEndianOutputBitStream& bit_stream, u8 bits_per_sample); ErrorOr write_verbatim_subframe(ReadonlySpan subframe, BigEndianOutputBitStream& bit_stream, u8 bits_per_sample); @@ -104,7 +115,12 @@ private: ErrorOr> encode_fixed_lpc(FlacFixedLPC order, ReadonlySpan subframe, size_t current_min_cost, u8 bits_per_sample); ErrorOr add_metadata_block(FlacRawMetadataBlock block, Optional insertion_index = {}); - ErrorOr 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 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 flush_seektable(); NonnullOwnPtr 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 m_last_padding; // Raw metadata blocks that will be written out before header finalization. Vector m_cached_metadata_blocks; + + // The full seektable, may be fully or partially written. + SeekTable m_cached_seektable {}; }; } diff --git a/Userland/Libraries/LibAudio/GenericTypes.cpp b/Userland/Libraries/LibAudio/GenericTypes.cpp index 6575624a67..407de22703 100644 --- a/Userland/Libraries/LibAudio/GenericTypes.cpp +++ b/Userland/Libraries/LibAudio/GenericTypes.cpp @@ -21,6 +21,11 @@ ReadonlySpan SeekTable::seek_points() const return m_seek_points.span(); } +Vector& SeekTable::seek_points() +{ + return m_seek_points; +} + Optional SeekTable::seek_point_before(u64 sample_index) const { if (m_seek_points.is_empty()) diff --git a/Userland/Libraries/LibAudio/GenericTypes.h b/Userland/Libraries/LibAudio/GenericTypes.h index ba4a870311..33a344b7aa 100644 --- a/Userland/Libraries/LibAudio/GenericTypes.h +++ b/Userland/Libraries/LibAudio/GenericTypes.h @@ -66,6 +66,7 @@ public: size_t size() const; ReadonlySpan seek_points() const; + Vector& seek_points(); ErrorOr insert_seek_point(SeekPoint);