/* * Copyright (c) 2025, Gregory Bertilson * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include "AudioPlaybackSink.h" namespace Media { static constexpr size_t OUTPUT_BLOCK_QUEUE_CAPACITY = 4; class AudioPlaybackSink::OutputThreadData : public AtomicRefCounted { public: OutputThreadData(PipelineStateChangeHandler on_state_changed) : m_main_thread_event_loop(Core::EventLoop::current()) , m_on_state_changed(move(on_state_changed)) { } ReadonlySpan move_output_to_playback_stream_buffer(Span); void dispatch_state_if_changed(PipelineStatus, u32 seek_id); RefPtr m_playback_stream; RefPtr m_input; Core::EventLoop& m_main_thread_event_loop; mutable Sync::Mutex m_output_mutex; mutable Sync::ConditionVariable m_output_condition { m_output_mutex }; AK::Array m_blocks; size_t m_block_head { 0 }; size_t m_block_tail { 0 }; size_t m_block_count { 0 }; i64 m_next_frame_to_play { 0 }; PipelineStateChangeHandler m_on_state_changed; PipelineStatus m_last_pull_status { PipelineStatus::Pending }; PipelineStatus m_last_dispatched_status { PipelineStatus::Pending }; i64 m_last_real_data_end_in_frames { 0 }; Atomic m_seek_id { 0 }; bool m_audio_processor_should_exit { false }; bool m_waiting_for_upstream_data { false }; }; ErrorOr> AudioPlaybackSink::try_create(PipelineStateChangeHandler on_state_changed) { auto output_thread_data = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) OutputThreadData(move(on_state_changed)))); auto sink = TRY(try_make_ref_counted(output_thread_data)); auto thread = TRY(Threading::Thread::try_create("Audio Processor"sv, [output_thread_data]() -> intptr_t { while (!output_thread_data->m_audio_processor_should_exit) { RefPtr input; { Sync::MutexLocker locker { output_thread_data->m_output_mutex }; input = output_thread_data->m_input; } auto& output_block = output_thread_data->m_blocks[output_thread_data->m_block_tail]; if (input != nullptr) { auto status = input->status(); if (status == PipelineStatus::MovedPosition) { input->pull(output_block); VERIFY(output_block.is_empty()); Sync::MutexLocker locker { output_thread_data->m_output_mutex }; output_thread_data->m_block_head = 0; output_thread_data->m_block_tail = 0; output_thread_data->m_block_count = 0; output_thread_data->m_last_real_data_end_in_frames = output_thread_data->m_next_frame_to_play; output_thread_data->m_waiting_for_upstream_data = false; output_thread_data->m_output_condition.broadcast(); continue; } } u32 seek_id_at_pull; { Sync::MutexLocker locker { output_thread_data->m_output_mutex }; if (output_thread_data->m_audio_processor_should_exit) break; if (output_thread_data->m_seek_id == 0) { output_thread_data->m_output_condition.wait(); continue; } if (output_thread_data->m_input == nullptr) { output_thread_data->m_output_condition.wait(); continue; } if (output_thread_data->m_block_count == OUTPUT_BLOCK_QUEUE_CAPACITY) { output_thread_data->m_output_condition.wait(); continue; } if (output_thread_data->m_waiting_for_upstream_data) { output_thread_data->m_output_condition.wait(); continue; } if (output_thread_data->m_audio_processor_should_exit) continue; output_thread_data->m_waiting_for_upstream_data = true; seek_id_at_pull = output_thread_data->m_seek_id; input = output_thread_data->m_input; } auto status = input->status(); if (status == PipelineStatus::MovedPosition) continue; input->pull(output_block); { Sync::MutexLocker locker { output_thread_data->m_output_mutex }; if (output_thread_data->m_seek_id != seek_id_at_pull) continue; output_thread_data->m_last_pull_status = status; if (!output_block.is_empty()) { VERIFY(can_carry_data(status)); output_thread_data->m_block_tail = (output_thread_data->m_block_tail + 1) % OUTPUT_BLOCK_QUEUE_CAPACITY; output_thread_data->m_block_count++; if (output_thread_data->m_playback_stream) output_thread_data->m_playback_stream->notify_data_available(); if (status == PipelineStatus::HaveData) output_thread_data->m_last_real_data_end_in_frames = output_block.end_frame_index(); } output_thread_data->m_waiting_for_upstream_data = !can_carry_data(status); if (!can_carry_data(output_thread_data->m_last_dispatched_status) && can_carry_data(status)) output_thread_data->dispatch_state_if_changed(status, seek_id_at_pull); } } return 0; })); thread->start(); thread->detach(); return sink; } AudioPlaybackSink::AudioPlaybackSink(NonnullRefPtr output_thread_data) : m_main_thread_event_loop(Core::EventLoop::current()) , m_output_thread_data(move(output_thread_data)) { m_main_thread_event_loop.deferred_invoke([self = NonnullRefPtr(*this)] { self->create_playback_stream(); }); } AudioPlaybackSink::~AudioPlaybackSink() { Sync::MutexLocker locker { m_output_thread_data->m_output_mutex }; if (m_output_thread_data->m_input != nullptr) m_output_thread_data->m_input->set_wake_handler(nullptr); m_output_thread_data->m_input = nullptr; m_output_thread_data->m_on_state_changed = nullptr; m_output_thread_data->m_audio_processor_should_exit = true; m_output_thread_data->m_output_condition.broadcast(); } ErrorOr AudioPlaybackSink::connect_input(NonnullRefPtr const& input) { input->set_wake_handler([input, &output_thread_data = *m_output_thread_data] { auto status = input->status(); if (status == PipelineStatus::Pending) return; Sync::MutexLocker locker { output_thread_data.m_output_mutex }; output_thread_data.m_last_pull_status = status; output_thread_data.m_waiting_for_upstream_data = false; output_thread_data.m_output_condition.broadcast(); }); if (m_sample_specification.is_valid()) { if (auto result = input->set_output_sample_specification(m_sample_specification); result.is_error()) { Sync::MutexLocker locker { m_output_thread_data->m_output_mutex }; disconnect_input_while_locked(input); return result.release_error(); } input->seek(current_time()); input->start(); } Sync::MutexLocker locker { m_output_thread_data->m_output_mutex }; VERIFY(m_output_thread_data->m_input == nullptr); m_output_thread_data->m_input = input; m_output_thread_data->m_output_condition.broadcast(); return {}; } void AudioPlaybackSink::disconnect_input_while_locked(NonnullRefPtr const& input) { input->set_wake_handler(nullptr); m_output_thread_data->m_input = nullptr; } void AudioPlaybackSink::disconnect_input(NonnullRefPtr const& input) { Sync::MutexLocker locker { m_output_thread_data->m_output_mutex }; VERIFY(m_output_thread_data->m_input == input); disconnect_input_while_locked(input); } void AudioPlaybackSink::create_playback_stream() { if (m_started_creating_playback_stream) return; m_started_creating_playback_stream = true; auto data_callback = [output_thread_data = m_output_thread_data](Span buffer) -> ReadonlySpan { return output_thread_data->move_output_to_playback_stream_buffer(buffer); }; constexpr u32 target_latency_ms = 100; auto promise = Audio::PlaybackStream::create(Audio::OutputState::Suspended, target_latency_ms, move(data_callback)); promise->when_resolved([self = NonnullRefPtr(*this)](auto& stream) { self->m_sample_specification = stream->sample_specification(); auto const& input = self->m_output_thread_data->m_input; if (input != nullptr) { if (auto result = input->set_output_sample_specification(self->m_sample_specification); result.is_error()) { if (self->on_audio_output_error) self->on_audio_output_error(result.release_error()); return; } } self->m_output_thread_data->m_playback_stream = stream; self->set_volume(self->m_volume); if (self->m_temporary_time.has_value()) { self->seek(self->m_temporary_time.release_value()); if (input != nullptr) input->start(); return; } if (input != nullptr) { input->seek(self->current_time()); input->start(); } { Sync::MutexLocker locker { self->m_output_thread_data->m_output_mutex }; self->m_output_thread_data->m_seek_id++; self->m_output_thread_data->m_output_condition.broadcast(); } if (self->m_playing) self->resume(); }); promise->when_rejected([self = NonnullRefPtr(*this)](auto& error) { if (self->on_audio_output_error) self->on_audio_output_error(move(error)); }); } ReadonlySpan AudioPlaybackSink::OutputThreadData::move_output_to_playback_stream_buffer(Span buffer) { VERIFY(buffer.size() > 0); Sync::MutexLocker locker { m_output_mutex }; size_t samples_written = 0; while (samples_written < buffer.size() && m_block_count > 0) { auto const& head_block = m_blocks[m_block_head]; auto channel_count = head_block.channel_count(); auto block_start_frame = head_block.first_frame_index(); auto block_end_frame = block_start_frame + static_cast(head_block.frame_count()); if (m_next_frame_to_play >= block_end_frame) { m_block_head = (m_block_head + 1) % OUTPUT_BLOCK_QUEUE_CAPACITY; m_block_count--; continue; } if (m_next_frame_to_play < block_start_frame) { auto silence_samples = static_cast(block_start_frame - m_next_frame_to_play) * channel_count; auto samples_to_silence = min(silence_samples, buffer.size() - samples_written); for (size_t i = 0; i < samples_to_silence; i++) buffer[samples_written + i] = 0.0f; samples_written += samples_to_silence; m_next_frame_to_play += static_cast(samples_to_silence / channel_count); continue; } auto offset_in_head_frames = static_cast(m_next_frame_to_play - block_start_frame); auto samples_to_copy = head_block.copy_to_interleaved(buffer.slice(samples_written), offset_in_head_frames); samples_written += samples_to_copy; m_next_frame_to_play += static_cast(samples_to_copy / channel_count); if ((offset_in_head_frames * channel_count) + samples_to_copy == head_block.sample_count()) { m_block_head = (m_block_head + 1) % OUTPUT_BLOCK_QUEUE_CAPACITY; m_block_count--; } } if (samples_written < buffer.size()) { buffer = buffer.trim(samples_written); if (m_last_pull_status == PipelineStatus::Blocked || m_last_pull_status == PipelineStatus::Error) dispatch_state_if_changed(m_last_pull_status, m_seek_id); } if (m_last_pull_status == PipelineStatus::EndOfStream && m_next_frame_to_play >= m_last_real_data_end_in_frames) dispatch_state_if_changed(PipelineStatus::EndOfStream, m_seek_id); m_output_condition.broadcast(); return buffer; } void AudioPlaybackSink::OutputThreadData::dispatch_state_if_changed(PipelineStatus status, u32 seek_id) { if (status == m_last_dispatched_status) return; m_last_dispatched_status = status; m_main_thread_event_loop.deferred_invoke([self = NonnullRefPtr(*this), status, seek_id] { Sync::MutexLocker locker { self->m_output_mutex }; if (self->m_seek_id != seek_id) return; if (self->m_on_state_changed) self->m_on_state_changed(status); }); } AK::Duration AudioPlaybackSink::current_time() const { if (m_temporary_time.has_value()) return m_temporary_time.value(); if (!m_output_thread_data->m_playback_stream) return m_last_media_time; auto stream_time = m_output_thread_data->m_playback_stream->total_time_played(); return m_last_media_time + (stream_time - m_last_stream_time); } void AudioPlaybackSink::resume() { m_playing = true; // If we're in the middle of the seek() callbacks, let those take care of resuming. if (m_temporary_time.has_value()) return; if (!m_output_thread_data->m_playback_stream) return; m_output_thread_data->m_playback_stream->resume() ->when_resolved([self = NonnullRefPtr(*this)](auto new_stream_time) { self->m_main_thread_event_loop.deferred_invoke([self, new_stream_time]() { auto new_media_time = self->m_last_media_time + (new_stream_time - self->m_last_stream_time); self->m_last_stream_time = new_stream_time; self->m_last_media_time = new_media_time; }); }) .when_rejected([](auto&& error) { warnln("Unexpected error while resuming AudioPlaybackSink: {}", error.string_literal()); }); } void AudioPlaybackSink::pause() { m_playing = false; if (!m_output_thread_data->m_playback_stream) return; m_output_thread_data->m_playback_stream->drain_buffer_and_suspend() ->when_resolved([]() { }) .when_rejected([](auto&& error) { warnln("Unexpected error while pausing AudioPlaybackSink: {}", error.string_literal()); }); } void AudioPlaybackSink::seek(AK::Duration time) { bool already_draining_for_seek = m_temporary_time.has_value(); m_temporary_time = time; if (!m_output_thread_data->m_playback_stream) return; auto seek_target_in_frames = time.to_time_units(1, m_sample_specification.sample_rate()); { Sync::MutexLocker locker { m_output_thread_data->m_output_mutex }; m_output_thread_data->m_seek_id++; m_output_thread_data->m_next_frame_to_play = seek_target_in_frames; m_output_thread_data->m_last_pull_status = PipelineStatus::Pending; m_output_thread_data->m_last_dispatched_status = PipelineStatus::Pending; m_output_thread_data->m_waiting_for_upstream_data = true; } if (m_output_thread_data->m_input != nullptr) m_output_thread_data->m_input->seek(time); if (already_draining_for_seek) return; m_output_thread_data->m_playback_stream->drain_buffer_and_suspend() ->when_resolved([self = NonnullRefPtr(*this)]() { auto new_stream_time = self->m_output_thread_data->m_playback_stream->total_time_played(); self->m_main_thread_event_loop.deferred_invoke([self, new_stream_time]() { self->m_last_stream_time = new_stream_time; self->m_last_media_time = self->m_temporary_time.release_value(); if (self->m_playing) self->resume(); }); }) .when_rejected([](auto&& error) { warnln("Unexpected error while seeking AudioPlaybackSink: {}", error.string_literal()); }); } void AudioPlaybackSink::set_volume(double volume) { m_volume = volume; if (m_output_thread_data->m_playback_stream) { m_output_thread_data->m_playback_stream->set_volume(m_volume) ->when_rejected([](Error&&) { // FIXME: Do we even need this function to return a promise? }); } } }