ladybird/Libraries/LibMedia/Processors/AudioTimeStretchProcessor.cpp
Zaggy1024 28f670f09f LibMedia: Implement audio time stretching with WSOLA
The algorithm is ported over from Chromium's, which produces very good
results for speech, while being not objectionable for music, especially
in the background.

Other algorithms were tested.

Phase vocoders:
- Bungee
- Signalsmith Stretch
- pvdoneright
All three of these exhibited the usual phase shifting artifacts,
causing speech to sound slightly shifted into the high end. Speech is
the main thing we want to optimize for, so these aren't ideal.

Sonic (TD-PSOLA) performs better than WSOLA for speech, especially at
rates higher than 2x, but makes background sounds/music garbled and
unpleasant. It is still worth considering for speech clarity, and could
be added as an optional feature.
2026-06-06 19:58:17 -05:00

289 lines
8.9 KiB
C++

/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Math.h>
#include <AK/TypedTransfer.h>
#include <LibMedia/Audio/WSOLATimeStretcher.h>
#include <LibMedia/Processors/AudioTimeStretchProcessor.h>
namespace Media {
ErrorOr<NonnullRefPtr<AudioTimeStretchProcessor>> AudioTimeStretchProcessor::try_create()
{
return adopt_nonnull_ref_or_enomem(new (nothrow) AudioTimeStretchProcessor);
}
AudioTimeStretchProcessor::AudioTimeStretchProcessor() = default;
AudioTimeStretchProcessor::~AudioTimeStretchProcessor()
{
Sync::MutexLocker locker { m_mutex };
if (m_input != nullptr)
m_input->set_wake_handler(nullptr);
}
ErrorOr<void> AudioTimeStretchProcessor::connect_input(NonnullRefPtr<AudioProducer> const& input)
{
Sync::MutexLocker locker { m_mutex };
VERIFY(m_input == nullptr);
m_input = input;
input->set_wake_handler([this] {
bool should_wake_downstream;
{
Sync::MutexLocker locker { m_mutex };
auto status = PipelineStatus::HaveData;
if (m_pending_block.is_empty())
status = produce_block_while_locked(m_pending_block);
if (!m_pending_block.is_empty())
status = PipelineStatus::HaveData;
should_wake_downstream = m_downstream_needs_wake && resolves_seek(status);
}
if (should_wake_downstream)
dispatch_wake();
});
if (m_sample_specification.is_valid()) {
if (auto result = input->set_output_sample_specification(m_sample_specification); result.is_error()) {
input->set_wake_handler(nullptr);
m_input = nullptr;
return result.release_error();
}
if (m_started)
input->start();
}
return {};
}
void AudioTimeStretchProcessor::disconnect_input(NonnullRefPtr<AudioProducer> const& input)
{
Sync::MutexLocker locker { m_mutex };
VERIFY(m_input == input);
input->set_wake_handler(nullptr);
m_input = nullptr;
}
void AudioTimeStretchProcessor::seek(AK::Duration timestamp)
{
RefPtr<AudioProducer> input;
{
Sync::MutexLocker locker { m_mutex };
VERIFY(m_sample_specification.is_valid());
auto sample_rate = m_sample_specification.sample_rate();
auto target_frame = timestamp.to_time_units(1, sample_rate);
auto output_frame = target_frame;
ensure_stretcher_while_locked();
auto prerolled_target_frame = max(target_frame - m_stretcher->preroll_frame_count(), 0);
auto actual_preroll_delta = target_frame - prerolled_target_frame;
target_frame = prerolled_target_frame;
output_frame = output_frame - AK::round_to<i64>(static_cast<float>(actual_preroll_delta) / m_playback_rate);
m_next_emit_media_time = AK::Duration::from_time_units(target_frame, 1, sample_rate);
m_next_output_frame = output_frame;
m_stretcher->flush(m_next_emit_media_time, m_next_output_frame);
m_moved_position_pending = true;
m_pending_block.clear();
m_downstream_needs_wake = true;
m_stretcher_reached_eos = false;
input = m_input;
timestamp = m_next_emit_media_time;
}
if (input != nullptr) {
input->seek(timestamp);
return;
}
dispatch_wake();
}
ErrorOr<void> AudioTimeStretchProcessor::set_output_sample_specification(Audio::SampleSpecification sample_specification)
{
Sync::MutexLocker locker { m_mutex };
if (m_sample_specification == sample_specification)
return {};
m_sample_specification = sample_specification;
m_stretcher = nullptr;
m_pending_block.clear();
m_stretcher_reached_eos = false;
if (m_input != nullptr)
TRY(m_input->set_output_sample_specification(sample_specification));
return {};
}
void AudioTimeStretchProcessor::start()
{
Sync::MutexLocker locker { m_mutex };
m_started = true;
if (m_input != nullptr)
m_input->start();
}
void AudioTimeStretchProcessor::set_wake_handler(PipelineWakeHandler handler)
{
m_wake_handler = move(handler);
}
void AudioTimeStretchProcessor::dispatch_wake()
{
{
Sync::MutexLocker locker { m_mutex };
m_downstream_needs_wake = false;
}
if (m_wake_handler)
m_wake_handler();
}
void AudioTimeStretchProcessor::set_playback_rate(float rate)
{
VERIFY(isfinite(rate));
VERIFY(rate > 0.0f);
bool should_wake_downstream = false;
{
Sync::MutexLocker locker { m_mutex };
if (m_playback_rate == rate)
return;
m_playback_rate = rate;
should_wake_downstream = m_downstream_needs_wake;
}
if (should_wake_downstream)
dispatch_wake();
}
void AudioTimeStretchProcessor::ensure_stretcher_while_locked() const
{
if (m_stretcher) {
m_stretcher->set_rate(m_playback_rate);
return;
}
VERIFY(m_sample_specification.is_valid());
m_stretcher = MUST(Audio::WSOLATimeStretcher::create(m_sample_specification));
m_stretcher->set_rate(m_playback_rate);
m_stretcher->flush(m_next_emit_media_time, m_next_output_frame);
}
void AudioTimeStretchProcessor::maybe_recover_from_stale_upstream_eos_while_locked() const
{
if (!m_stretcher_reached_eos)
return;
auto status = m_input->status();
while (status == PipelineStatus::MovedPosition) {
m_input->pull(m_input_block);
VERIFY(m_input_block.is_empty());
status = m_input->status();
}
if (is_terminal(status))
return;
m_stretcher->flush(m_next_emit_media_time, m_next_output_frame);
m_input_block.clear();
m_stretcher_reached_eos = false;
}
PipelineStatus AudioTimeStretchProcessor::produce_block_while_locked(AudioBlock& into) const
{
if (m_input == nullptr || !m_sample_specification.is_valid())
return PipelineStatus::Pending;
VERIFY(m_playback_rate != 0.0f);
auto pull_input = [&](AudioBlock& input_block) -> PipelineStatus {
auto status = m_input->status();
while (status == PipelineStatus::MovedPosition) {
m_input->pull(input_block);
VERIFY(input_block.is_empty());
status = m_input->status();
}
if (status == PipelineStatus::HaveData)
m_input->pull(input_block);
else
input_block.clear();
return status;
};
ensure_stretcher_while_locked();
maybe_recover_from_stale_upstream_eos_while_locked();
while (true) {
auto result = m_stretcher->retrieve_block();
if (!result.is_error()) {
into = result.release_value();
m_next_output_frame = into.end_frame_index();
m_next_emit_media_time = into.media_time_end();
return PipelineStatus::HaveData;
}
if (result.error().category() == DecoderErrorCategory::EndOfStream) {
into.clear();
m_stretcher_reached_eos = true;
return PipelineStatus::EndOfStream;
}
if (result.error().category() != DecoderErrorCategory::NeedsMoreInput) {
into.clear();
return PipelineStatus::Error;
}
auto status = pull_input(m_input_block);
if (status == PipelineStatus::EndOfStream) {
VERIFY(m_input_block.is_empty());
m_stretcher->signal_end_of_stream();
m_stretcher_reached_eos = false;
continue;
}
if (m_input_block.is_empty()) {
into.clear();
return status;
}
VERIFY(status == PipelineStatus::HaveData);
VERIFY(m_input_block.sample_specification() == m_sample_specification);
m_stretcher->push_block(m_input_block);
}
}
PipelineStatus AudioTimeStretchProcessor::status() const
{
Sync::MutexLocker locker { m_mutex };
auto status = PipelineStatus::HaveData;
if (m_pending_block.is_empty())
status = produce_block_while_locked(m_pending_block);
if (!m_pending_block.is_empty())
status = PipelineStatus::HaveData;
if (m_moved_position_pending)
status = PipelineStatus::MovedPosition;
m_downstream_needs_wake = is_waiting_for_data(status);
return status;
}
void AudioTimeStretchProcessor::pull(AudioBlock& into)
{
Sync::MutexLocker locker { m_mutex };
if (m_moved_position_pending) {
m_moved_position_pending = false;
into.clear();
return;
}
if (!m_pending_block.is_empty()) {
into.initialize(m_pending_block.sample_specification(), m_pending_block.first_frame_index(), m_pending_block.frame_count());
for (size_t channel = 0; channel < into.channel_count(); channel++)
AK::TypedTransfer<float>::copy(into.channel_data(channel).data(), m_pending_block.channel_data(channel).data(), into.frame_count());
into.set_media_time_start(m_pending_block.media_time_start());
into.set_media_time_duration(m_pending_block.media_time_duration());
m_pending_block.clear();
return;
}
into.clear();
}
}