mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 19:47:46 +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
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue