/* * Copyright 2012 The Chromium Authors * Copyright (c) 2026-present, the Ladybird developers. * * SPDX-License-Identifier: BSD-3-Clause */ // Source: chromium/media/filters/audio_renderer_algorithm.{h,cc}. #include #include #include #include namespace Audio { namespace { constexpr u64 OLA_WINDOW_SIZE_MS = 20; constexpr u64 WSOLA_SEARCH_INTERVAL_MS = 30; size_t window_size(u32 sample_rate) { auto const frame_count = (static_cast(sample_rate) * OLA_WINDOW_SIZE_MS + 500) / 1000; return max(frame_count + (frame_count & 1), 2); } size_t search_interval(u32 sample_rate) { auto const frame_count = (static_cast(sample_rate) * WSOLA_SEARCH_INTERVAL_MS + 500) / 1000; return max(frame_count, 1); } void zero_block(Media::AudioBlock& block) { for (size_t channel = 0; channel < block.channel_count(); channel++) { auto channel_data = block.channel_data(channel); for (auto& sample : channel_data) sample = 0.0f; } } void zero_frames(Media::AudioBlock& block, size_t starting_frame, size_t frame_count) { VERIFY(starting_frame <= block.frame_count()); VERIFY(frame_count <= block.frame_count() - starting_frame); for (size_t channel = 0; channel < block.channel_count(); channel++) { auto channel_data = block.channel_data(channel); for (size_t frame = 0; frame < frame_count; frame++) channel_data[starting_frame + frame] = 0.0f; } } void copy_partial_frames(Media::AudioBlock const& source, size_t source_offset, size_t frame_count, size_t destination_offset, Media::AudioBlock& destination) { VERIFY(source.sample_specification() == destination.sample_specification()); VERIFY(source_offset <= source.frame_count()); VERIFY(frame_count <= source.frame_count() - source_offset); VERIFY(destination_offset <= destination.frame_count()); VERIFY(frame_count <= destination.frame_count() - destination_offset); for (size_t channel = 0; channel < source.channel_count(); channel++) { auto source_channel = source.channel_data(channel).slice(source_offset, frame_count); auto destination_channel = destination.channel_data(channel).slice(destination_offset, frame_count); AK::TypedTransfer::copy(destination_channel.data(), source_channel.data(), frame_count); } } } WSOLAAlgorithm::WSOLAAlgorithm(SampleSpecification sample_specification) : m_sample_specification(sample_specification) , m_audio_buffer(sample_specification) , m_ola_window_size(window_size(sample_rate())) , m_ola_hop_size(m_ola_window_size / 2) , m_num_candidate_blocks(search_interval(sample_rate())) , m_search_block_center_offset((m_num_candidate_blocks / 2) + ((m_ola_window_size / 2) - 1)) { VERIFY(m_sample_specification.is_valid()); m_ola_window.resize(m_ola_window_size); WSOLAInternals::get_periodic_hanning_window(m_ola_window.span()); m_transition_window.resize(m_ola_window_size * 2); WSOLAInternals::get_periodic_hanning_window(m_transition_window.span()); m_wsola_output = create_block(m_ola_window_size + m_ola_hop_size); zero_block(m_wsola_output); m_optimal_block = create_block(m_ola_window_size); m_search_block = create_block(m_num_candidate_blocks + (m_ola_window_size - 1)); m_target_block = create_block(m_ola_window_size); } WSOLAAlgorithm::~WSOLAAlgorithm() = default; Media::AudioBlock WSOLAAlgorithm::create_block(size_t frame_count) const { Media::AudioBlock block; block.initialize(m_sample_specification, 0, frame_count); return block; } void WSOLAAlgorithm::flush_buffers() { m_audio_buffer.clear(); m_input_position_remainder = 0.0f; m_search_block_center_index = 0; m_search_block_index = 0; m_target_block_index = 0; m_num_complete_frames = 0; zero_block(m_wsola_output); } void WSOLAAlgorithm::enqueue_buffer(Media::AudioBlock const& buffer_in) { VERIFY(buffer_in.sample_specification() == m_sample_specification); m_audio_buffer.append(buffer_in); } void WSOLAAlgorithm::mark_end_of_stream(float playback_rate) { VERIFY(playback_rate > 0.0f); auto const search_block_size = m_num_candidate_blocks + (m_ola_window_size - 1); auto const padding = search_block_size + m_ola_window_size + m_ola_hop_size; m_audio_buffer.append_silence(padding); } size_t WSOLAAlgorithm::fill_buffer(Media::AudioBlock& destination, size_t destination_offset, size_t requested_frames, float playback_rate) { if (playback_rate == 0.0f) return 0; VERIFY(playback_rate > 0.0f); VERIFY(destination.sample_specification() == m_sample_specification); size_t rendered_frames = 0; do { rendered_frames += write_completed_frames_to( requested_frames - rendered_frames, destination_offset + rendered_frames, destination); } while (rendered_frames < requested_frames && run_one_wsola_iteration(playback_rate)); return rendered_frames; } bool WSOLAAlgorithm::can_perform_wsola() const { auto const search_block_size = m_num_candidate_blocks + (m_ola_window_size - 1); auto const frames = m_audio_buffer.frame_count(); return m_target_block_index + static_cast(m_ola_window_size) <= static_cast(frames) && m_search_block_index + static_cast(search_block_size) <= static_cast(frames); } bool WSOLAAlgorithm::target_is_within_search_region() const { auto const search_block_size = m_num_candidate_blocks + (m_ola_window_size - 1); return m_target_block_index >= m_search_block_index && m_target_block_index + static_cast(m_ola_window_size) <= m_search_block_index + static_cast(search_block_size); } void WSOLAAlgorithm::peek_audio_with_zero_prepend(i64 read_offset_frames, Media::AudioBlock& destination) { auto const destination_frame_count = destination.frame_count(); VERIFY(read_offset_frames <= static_cast(m_audio_buffer.frame_count()) - static_cast(destination_frame_count)); size_t write_offset = 0; auto frames_to_read = destination_frame_count; if (read_offset_frames < 0) { auto num_zero_frames_appended = min(-static_cast(read_offset_frames), frames_to_read); read_offset_frames = 0; frames_to_read -= num_zero_frames_appended; write_offset = num_zero_frames_appended; zero_frames(destination, 0, num_zero_frames_appended); } if (frames_to_read > 0) m_audio_buffer.copy_frames_to(static_cast(read_offset_frames), frames_to_read, write_offset, destination); } void WSOLAAlgorithm::get_optimal_block() { i64 optimal_index = 0; constexpr i64 exclude_interval_length_frames = 160; if (target_is_within_search_region()) { optimal_index = m_target_block_index; peek_audio_with_zero_prepend(optimal_index, m_optimal_block); } else { peek_audio_with_zero_prepend(m_target_block_index, m_target_block); peek_audio_with_zero_prepend(m_search_block_index, m_search_block); auto last_optimal = m_target_block_index - static_cast(m_ola_hop_size) - m_search_block_index; auto exclude_interval_low = last_optimal - (exclude_interval_length_frames / 2); auto exclude_interval_high = last_optimal + (exclude_interval_length_frames / 2); WSOLAInternals::Interval exclude_interval { exclude_interval_low <= 0 ? 0 : static_cast(exclude_interval_low), exclude_interval_high <= 0 ? 0 : static_cast(exclude_interval_high), }; optimal_index = static_cast(WSOLAInternals::optimal_index(m_search_block, m_target_block, exclude_interval)); optimal_index += m_search_block_index; peek_audio_with_zero_prepend(optimal_index, m_optimal_block); for (size_t channel_index = 0; channel_index < m_sample_specification.channel_count(); channel_index++) { auto optimal_channel = m_optimal_block.channel_data(channel_index); auto target_channel = m_target_block.channel_data(channel_index); for (size_t n = 0; n < m_ola_window_size; n++) { optimal_channel[n] = (optimal_channel[n] * m_transition_window[n]) + (target_channel[n] * m_transition_window[m_ola_window_size + n]); } } } m_target_block_index = optimal_index + static_cast(m_ola_hop_size); } bool WSOLAAlgorithm::run_one_wsola_iteration(float playback_rate) { if (!can_perform_wsola()) return false; get_optimal_block(); for (size_t channel_index = 0; channel_index < m_sample_specification.channel_count(); channel_index++) { auto optimal_channel = m_optimal_block.channel_data(channel_index); auto output_channel = m_wsola_output.channel_data(channel_index).slice(m_num_complete_frames); for (size_t n = 0; n < m_ola_hop_size; n++) { output_channel[n] = output_channel[n] * m_ola_window[m_ola_hop_size + n]; output_channel[n] += optimal_channel[n] * m_ola_window[n]; } for (size_t n = 0; n < m_ola_hop_size; n++) output_channel[m_ola_hop_size + n] = optimal_channel[m_ola_hop_size + n]; } m_num_complete_frames += m_ola_hop_size; auto input_position_advance = (static_cast(m_ola_hop_size) * playback_rate) + m_input_position_remainder; auto whole_input_frames = AK::round_to(input_position_advance); m_input_position_remainder = input_position_advance - static_cast(whole_input_frames); m_search_block_center_index += whole_input_frames; m_search_block_index = m_search_block_center_index - static_cast(m_search_block_center_offset); remove_old_input_frames(); return true; } void WSOLAAlgorithm::remove_old_input_frames() { auto const earliest_used_index = min(m_target_block_index, m_search_block_index); if (earliest_used_index <= 0) return; m_audio_buffer.drop_front(static_cast(earliest_used_index)); m_target_block_index -= earliest_used_index; m_search_block_center_index -= earliest_used_index; m_search_block_index -= earliest_used_index; } size_t WSOLAAlgorithm::write_completed_frames_to(size_t requested_frames, size_t destination_offset, Media::AudioBlock& destination) { auto const rendered_frames = min(m_num_complete_frames, requested_frames); if (rendered_frames == 0) return 0; copy_partial_frames(m_wsola_output, 0, rendered_frames, destination_offset, destination); auto const frames_to_move = m_wsola_output.frame_count() - rendered_frames; for (size_t channel_index = 0; channel_index < m_sample_specification.channel_count(); channel_index++) { auto channel_data = m_wsola_output.channel_data(channel_index); for (size_t i = 0; i < frames_to_move; i++) channel_data[i] = channel_data[i + rendered_frames]; } m_num_complete_frames -= rendered_frames; return rendered_frames; } }