Rework audio output, connecting AudioOut into coretiming to fix desync during heavy loads.
This commit is contained in:
		| @@ -194,6 +194,7 @@ add_library(audio_core STATIC | |||||||
|     sink/sink.h |     sink/sink.h | ||||||
|     sink/sink_details.cpp |     sink/sink_details.cpp | ||||||
|     sink/sink_details.h |     sink/sink_details.h | ||||||
|  |     sink/sink_stream.cpp | ||||||
|     sink/sink_stream.h |     sink/sink_stream.h | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -57,12 +57,4 @@ void AudioCore::PauseSinks(const bool pausing) const { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| u32 AudioCore::GetStreamQueue() const { |  | ||||||
|     return estimated_queue.load(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void AudioCore::SetStreamQueue(u32 size) { |  | ||||||
|     estimated_queue.store(size); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| } // namespace AudioCore | } // namespace AudioCore | ||||||
|   | |||||||
| @@ -65,20 +65,6 @@ public: | |||||||
|      */ |      */ | ||||||
|     void PauseSinks(bool pausing) const; |     void PauseSinks(bool pausing) const; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Get the size of the current stream queue. |  | ||||||
|      * |  | ||||||
|      * @return Current stream queue size. |  | ||||||
|      */ |  | ||||||
|     u32 GetStreamQueue() const; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Get the size of the current stream queue. |  | ||||||
|      * |  | ||||||
|      * @param size - New stream size. |  | ||||||
|      */ |  | ||||||
|     void SetStreamQueue(u32 size); |  | ||||||
|  |  | ||||||
| private: | private: | ||||||
|     /** |     /** | ||||||
|      * Create the sinks on startup. |      * Create the sinks on startup. | ||||||
| @@ -93,8 +79,6 @@ private: | |||||||
|     std::unique_ptr<Sink::Sink> input_sink; |     std::unique_ptr<Sink::Sink> input_sink; | ||||||
|     /// The ADSP in the sysmodule |     /// The ADSP in the sysmodule | ||||||
|     std::unique_ptr<AudioRenderer::ADSP::ADSP> adsp; |     std::unique_ptr<AudioRenderer::ADSP::ADSP> adsp; | ||||||
|     /// Current size of the stream queue |  | ||||||
|     std::atomic<u32> estimated_queue{0}; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| } // namespace AudioCore | } // namespace AudioCore | ||||||
|   | |||||||
| @@ -8,6 +8,10 @@ | |||||||
| namespace AudioCore { | namespace AudioCore { | ||||||
|  |  | ||||||
| struct AudioBuffer { | struct AudioBuffer { | ||||||
|  |     /// Timestamp this buffer started playing. | ||||||
|  |     u64 start_timestamp; | ||||||
|  |     /// Timestamp this buffer should finish playing. | ||||||
|  |     u64 end_timestamp; | ||||||
|     /// Timestamp this buffer completed playing. |     /// Timestamp this buffer completed playing. | ||||||
|     s64 played_timestamp; |     s64 played_timestamp; | ||||||
|     /// Game memory address for these samples. |     /// Game memory address for these samples. | ||||||
|   | |||||||
| @@ -58,6 +58,7 @@ public: | |||||||
|             if (index < 0) { |             if (index < 0) { | ||||||
|                 index += N; |                 index += N; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             out_buffers.push_back(buffers[index]); |             out_buffers.push_back(buffers[index]); | ||||||
|             registered_count++; |             registered_count++; | ||||||
|             registered_index = (registered_index + 1) % append_limit; |             registered_index = (registered_index + 1) % append_limit; | ||||||
| @@ -100,7 +101,7 @@ public: | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Check with the backend if this buffer can be released yet. |             // Check with the backend if this buffer can be released yet. | ||||||
|             if (!session.IsBufferConsumed(buffers[index].tag)) { |             if (!session.IsBufferConsumed(buffers[index])) { | ||||||
|                 break; |                 break; | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -280,6 +281,16 @@ public: | |||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     u64 GetNextTimestamp() const { | ||||||
|  |         // Iterate backwards through the buffer queue, and take the most recent buffer's end | ||||||
|  |         std::scoped_lock l{lock}; | ||||||
|  |         auto index{appended_index - 1}; | ||||||
|  |         if (index < 0) { | ||||||
|  |             index += append_limit; | ||||||
|  |         } | ||||||
|  |         return buffers[index].end_timestamp; | ||||||
|  |     } | ||||||
|  |  | ||||||
| private: | private: | ||||||
|     /// Buffer lock |     /// Buffer lock | ||||||
|     mutable std::recursive_mutex lock{}; |     mutable std::recursive_mutex lock{}; | ||||||
|   | |||||||
| @@ -7,11 +7,20 @@ | |||||||
| #include "audio_core/device/device_session.h" | #include "audio_core/device/device_session.h" | ||||||
| #include "audio_core/sink/sink_stream.h" | #include "audio_core/sink/sink_stream.h" | ||||||
| #include "core/core.h" | #include "core/core.h" | ||||||
|  | #include "core/core_timing.h" | ||||||
| #include "core/memory.h" | #include "core/memory.h" | ||||||
|  |  | ||||||
| namespace AudioCore { | namespace AudioCore { | ||||||
|  |  | ||||||
| DeviceSession::DeviceSession(Core::System& system_) : system{system_} {} | using namespace std::literals; | ||||||
|  | constexpr auto INCREMENT_TIME{5ms}; | ||||||
|  |  | ||||||
|  | DeviceSession::DeviceSession(Core::System& system_) | ||||||
|  |     : system{system_}, thread_event{Core::Timing::CreateEvent( | ||||||
|  |                            "AudioOutSampleTick", | ||||||
|  |                            [this](std::uintptr_t, s64 time, std::chrono::nanoseconds) { | ||||||
|  |                                return ThreadFunc(); | ||||||
|  |                            })} {} | ||||||
|  |  | ||||||
| DeviceSession::~DeviceSession() { | DeviceSession::~DeviceSession() { | ||||||
|     Finalize(); |     Finalize(); | ||||||
| @@ -50,20 +59,21 @@ void DeviceSession::Finalize() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void DeviceSession::Start() { | void DeviceSession::Start() { | ||||||
|     stream->SetPlayedSampleCount(played_sample_count); |     if (stream) { | ||||||
|     stream->Start(); |         stream->Start(); | ||||||
|  |         system.CoreTiming().ScheduleLoopingEvent(std::chrono::nanoseconds::zero(), INCREMENT_TIME, | ||||||
|  |                                                  thread_event); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| void DeviceSession::Stop() { | void DeviceSession::Stop() { | ||||||
|     if (stream) { |     if (stream) { | ||||||
|         played_sample_count = stream->GetPlayedSampleCount(); |  | ||||||
|         stream->Stop(); |         stream->Stop(); | ||||||
|  |         system.CoreTiming().UnscheduleEvent(thread_event, {}); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| void DeviceSession::AppendBuffers(std::span<AudioBuffer> buffers) const { | void DeviceSession::AppendBuffers(std::span<AudioBuffer> buffers) const { | ||||||
|     auto& memory{system.Memory()}; |  | ||||||
|  |  | ||||||
|     for (size_t i = 0; i < buffers.size(); i++) { |     for (size_t i = 0; i < buffers.size(); i++) { | ||||||
|         Sink::SinkBuffer new_buffer{ |         Sink::SinkBuffer new_buffer{ | ||||||
|             .frames = buffers[i].size / (channel_count * sizeof(s16)), |             .frames = buffers[i].size / (channel_count * sizeof(s16)), | ||||||
| @@ -77,7 +87,7 @@ void DeviceSession::AppendBuffers(std::span<AudioBuffer> buffers) const { | |||||||
|             stream->AppendBuffer(new_buffer, samples); |             stream->AppendBuffer(new_buffer, samples); | ||||||
|         } else { |         } else { | ||||||
|             std::vector<s16> samples(buffers[i].size / sizeof(s16)); |             std::vector<s16> samples(buffers[i].size / sizeof(s16)); | ||||||
|             memory.ReadBlockUnsafe(buffers[i].samples, samples.data(), buffers[i].size); |             system.Memory().ReadBlockUnsafe(buffers[i].samples, samples.data(), buffers[i].size); | ||||||
|             stream->AppendBuffer(new_buffer, samples); |             stream->AppendBuffer(new_buffer, samples); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -85,17 +95,13 @@ void DeviceSession::AppendBuffers(std::span<AudioBuffer> buffers) const { | |||||||
|  |  | ||||||
| void DeviceSession::ReleaseBuffer(AudioBuffer& buffer) const { | void DeviceSession::ReleaseBuffer(AudioBuffer& buffer) const { | ||||||
|     if (type == Sink::StreamType::In) { |     if (type == Sink::StreamType::In) { | ||||||
|         auto& memory{system.Memory()}; |  | ||||||
|         auto samples{stream->ReleaseBuffer(buffer.size / sizeof(s16))}; |         auto samples{stream->ReleaseBuffer(buffer.size / sizeof(s16))}; | ||||||
|         memory.WriteBlockUnsafe(buffer.samples, samples.data(), buffer.size); |         system.Memory().WriteBlockUnsafe(buffer.samples, samples.data(), buffer.size); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| bool DeviceSession::IsBufferConsumed(u64 tag) const { | bool DeviceSession::IsBufferConsumed(AudioBuffer& buffer) const { | ||||||
|     if (stream) { |     return played_sample_count >= buffer.end_timestamp; | ||||||
|         return stream->IsBufferConsumed(tag); |  | ||||||
|     } |  | ||||||
|     return true; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void DeviceSession::SetVolume(f32 volume) const { | void DeviceSession::SetVolume(f32 volume) const { | ||||||
| @@ -105,10 +111,22 @@ void DeviceSession::SetVolume(f32 volume) const { | |||||||
| } | } | ||||||
|  |  | ||||||
| u64 DeviceSession::GetPlayedSampleCount() const { | u64 DeviceSession::GetPlayedSampleCount() const { | ||||||
|     if (stream) { |     return played_sample_count; | ||||||
|         return stream->GetPlayedSampleCount(); | } | ||||||
|  |  | ||||||
|  | std::optional<std::chrono::nanoseconds> DeviceSession::ThreadFunc() { | ||||||
|  |     // Add 5ms of samples at a 48K sample rate. | ||||||
|  |     played_sample_count += 48'000 * INCREMENT_TIME / 1s; | ||||||
|  |     if (type == Sink::StreamType::Out) { | ||||||
|  |         system.AudioCore().GetAudioManager().SetEvent(Event::Type::AudioOutManager, true); | ||||||
|  |     } else { | ||||||
|  |         system.AudioCore().GetAudioManager().SetEvent(Event::Type::AudioInManager, true); | ||||||
|     } |     } | ||||||
|     return 0; |     return std::nullopt; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void DeviceSession::SetRingSize(u32 ring_size) { | ||||||
|  |     stream->SetRingSize(ring_size); | ||||||
| } | } | ||||||
|  |  | ||||||
| } // namespace AudioCore | } // namespace AudioCore | ||||||
|   | |||||||
| @@ -3,6 +3,9 @@ | |||||||
|  |  | ||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
|  | #include <chrono> | ||||||
|  | #include <memory> | ||||||
|  | #include <optional> | ||||||
| #include <span> | #include <span> | ||||||
|  |  | ||||||
| #include "audio_core/common/common.h" | #include "audio_core/common/common.h" | ||||||
| @@ -11,9 +14,13 @@ | |||||||
|  |  | ||||||
| namespace Core { | namespace Core { | ||||||
| class System; | class System; | ||||||
| } | namespace Timing { | ||||||
|  | struct EventType; | ||||||
|  | } // namespace Timing | ||||||
|  | } // namespace Core | ||||||
|  |  | ||||||
| namespace AudioCore { | namespace AudioCore { | ||||||
|  |  | ||||||
| namespace Sink { | namespace Sink { | ||||||
| class SinkStream; | class SinkStream; | ||||||
| struct SinkBuffer; | struct SinkBuffer; | ||||||
| @@ -70,7 +77,7 @@ public: | |||||||
|      * @param tag - Unqiue tag of the buffer to check. |      * @param tag - Unqiue tag of the buffer to check. | ||||||
|      * @return true if the buffer has been consumed, otherwise false. |      * @return true if the buffer has been consumed, otherwise false. | ||||||
|      */ |      */ | ||||||
|     bool IsBufferConsumed(u64 tag) const; |     bool IsBufferConsumed(AudioBuffer& buffer) const; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Start this device session, starting the backend stream. |      * Start this device session, starting the backend stream. | ||||||
| @@ -96,6 +103,16 @@ public: | |||||||
|      */ |      */ | ||||||
|     u64 GetPlayedSampleCount() const; |     u64 GetPlayedSampleCount() const; | ||||||
|  |  | ||||||
|  |     /* | ||||||
|  |      * CoreTiming callback to increment played_sample_count over time. | ||||||
|  |      */ | ||||||
|  |     std::optional<std::chrono::nanoseconds> ThreadFunc(); | ||||||
|  |  | ||||||
|  |     /* | ||||||
|  |      * Set the size of the ring buffer. | ||||||
|  |      */ | ||||||
|  |     void SetRingSize(u32 ring_size); | ||||||
|  |  | ||||||
| private: | private: | ||||||
|     /// System |     /// System | ||||||
|     Core::System& system; |     Core::System& system; | ||||||
| @@ -118,9 +135,13 @@ private: | |||||||
|     /// Applet resource user id of this device session |     /// Applet resource user id of this device session | ||||||
|     u64 applet_resource_user_id{}; |     u64 applet_resource_user_id{}; | ||||||
|     /// Total number of samples played by this device session |     /// Total number of samples played by this device session | ||||||
|     u64 played_sample_count{}; |     std::atomic<u64> played_sample_count{}; | ||||||
|  |     /// Event increasing the played sample count every 5ms | ||||||
|  |     std::shared_ptr<Core::Timing::EventType> thread_event; | ||||||
|     /// Is this session initialised? |     /// Is this session initialised? | ||||||
|     bool initialized{}; |     bool initialized{}; | ||||||
|  |     /// Buffer queue | ||||||
|  |     std::vector<AudioBuffer> buffer_queue{}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| } // namespace AudioCore | } // namespace AudioCore | ||||||
|   | |||||||
| @@ -93,6 +93,7 @@ Result System::Start() { | |||||||
|     std::vector<AudioBuffer> buffers_to_flush{}; |     std::vector<AudioBuffer> buffers_to_flush{}; | ||||||
|     buffers.RegisterBuffers(buffers_to_flush); |     buffers.RegisterBuffers(buffers_to_flush); | ||||||
|     session->AppendBuffers(buffers_to_flush); |     session->AppendBuffers(buffers_to_flush); | ||||||
|  |     session->SetRingSize(static_cast<u32>(buffers_to_flush.size())); | ||||||
|  |  | ||||||
|     return ResultSuccess; |     return ResultSuccess; | ||||||
| } | } | ||||||
| @@ -112,8 +113,13 @@ bool System::AppendBuffer(const AudioInBuffer& buffer, const u64 tag) { | |||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     AudioBuffer new_buffer{ |     const auto timestamp{buffers.GetNextTimestamp()}; | ||||||
|         .played_timestamp = 0, .samples = buffer.samples, .tag = tag, .size = buffer.size}; |     AudioBuffer new_buffer{.start_timestamp = timestamp, | ||||||
|  |                            .end_timestamp = timestamp + buffer.size / (channel_count * sizeof(s16)), | ||||||
|  |                            .played_timestamp = 0, | ||||||
|  |                            .samples = buffer.samples, | ||||||
|  |                            .tag = tag, | ||||||
|  |                            .size = buffer.size}; | ||||||
|  |  | ||||||
|     buffers.AppendBuffer(new_buffer); |     buffers.AppendBuffer(new_buffer); | ||||||
|     RegisterBuffers(); |     RegisterBuffers(); | ||||||
|   | |||||||
| @@ -92,6 +92,7 @@ Result System::Start() { | |||||||
|     std::vector<AudioBuffer> buffers_to_flush{}; |     std::vector<AudioBuffer> buffers_to_flush{}; | ||||||
|     buffers.RegisterBuffers(buffers_to_flush); |     buffers.RegisterBuffers(buffers_to_flush); | ||||||
|     session->AppendBuffers(buffers_to_flush); |     session->AppendBuffers(buffers_to_flush); | ||||||
|  |     session->SetRingSize(static_cast<u32>(buffers_to_flush.size())); | ||||||
|  |  | ||||||
|     return ResultSuccess; |     return ResultSuccess; | ||||||
| } | } | ||||||
| @@ -111,8 +112,13 @@ bool System::AppendBuffer(const AudioOutBuffer& buffer, u64 tag) { | |||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     AudioBuffer new_buffer{ |     const auto timestamp{buffers.GetNextTimestamp()}; | ||||||
|         .played_timestamp = 0, .samples = buffer.samples, .tag = tag, .size = buffer.size}; |     AudioBuffer new_buffer{.start_timestamp = timestamp, | ||||||
|  |                            .end_timestamp = timestamp + buffer.size / (channel_count * sizeof(s16)), | ||||||
|  |                            .played_timestamp = 0, | ||||||
|  |                            .samples = buffer.samples, | ||||||
|  |                            .tag = tag, | ||||||
|  |                            .size = buffer.size}; | ||||||
|  |  | ||||||
|     buffers.AppendBuffer(new_buffer); |     buffers.AppendBuffer(new_buffer); | ||||||
|     RegisterBuffers(); |     RegisterBuffers(); | ||||||
|   | |||||||
| @@ -106,9 +106,6 @@ void AudioRenderer::Start(AudioRenderer_Mailbox* mailbox_) { | |||||||
|  |  | ||||||
|     mailbox = mailbox_; |     mailbox = mailbox_; | ||||||
|     thread = std::thread(&AudioRenderer::ThreadFunc, this); |     thread = std::thread(&AudioRenderer::ThreadFunc, this); | ||||||
|     for (auto& stream : streams) { |  | ||||||
|         stream->Start(); |  | ||||||
|     } |  | ||||||
|     running = true; |     running = true; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -130,6 +127,7 @@ void AudioRenderer::CreateSinkStreams() { | |||||||
|         std::string name{fmt::format("ADSP_RenderStream-{}", i)}; |         std::string name{fmt::format("ADSP_RenderStream-{}", i)}; | ||||||
|         streams[i] = |         streams[i] = | ||||||
|             sink.AcquireSinkStream(system, channels, name, ::AudioCore::Sink::StreamType::Render); |             sink.AcquireSinkStream(system, channels, name, ::AudioCore::Sink::StreamType::Render); | ||||||
|  |         streams[i]->SetRingSize(4); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -198,11 +196,6 @@ void AudioRenderer::ThreadFunc() { | |||||||
|                             command_list_processor.Process(index) - start_time; |                             command_list_processor.Process(index) - start_time; | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     if (index == 0) { |  | ||||||
|                         auto stream{command_list_processor.GetOutputSinkStream()}; |  | ||||||
|                         system.AudioCore().SetStreamQueue(stream->GetQueueSize()); |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     const auto end_time{system.CoreTiming().GetClockTicks()}; |                     const auto end_time{system.CoreTiming().GetClockTicks()}; | ||||||
|  |  | ||||||
|                     command_buffer.remaining_command_count = |                     command_buffer.remaining_command_count = | ||||||
|   | |||||||
| @@ -43,13 +43,15 @@ void BehaviorInfo::AppendError(ErrorInfo& error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| void BehaviorInfo::CopyErrorInfo(std::span<ErrorInfo> out_errors, u32& out_count) { | void BehaviorInfo::CopyErrorInfo(std::span<ErrorInfo> out_errors, u32& out_count) { | ||||||
|     auto error_count_{std::min(error_count, MaxErrors)}; |     out_count = std::min(error_count, MaxErrors); | ||||||
|     std::memset(out_errors.data(), 0, MaxErrors * sizeof(ErrorInfo)); |  | ||||||
|  |  | ||||||
|     for (size_t i = 0; i < error_count_; i++) { |     for (size_t i = 0; i < MaxErrors; i++) { | ||||||
|         out_errors[i] = errors[i]; |         if (i < out_count) { | ||||||
|  |             out_errors[i] = errors[i]; | ||||||
|  |         } else { | ||||||
|  |             out_errors[i] = {}; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|     out_count = error_count_; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void BehaviorInfo::UpdateFlags(const Flags flags_) { | void BehaviorInfo::UpdateFlags(const Flags flags_) { | ||||||
|   | |||||||
| @@ -46,6 +46,10 @@ void DeviceSinkCommand::Process(const ADSP::CommandListProcessor& processor) { | |||||||
|  |  | ||||||
|     out_buffer.tag = reinterpret_cast<u64>(samples.data()); |     out_buffer.tag = reinterpret_cast<u64>(samples.data()); | ||||||
|     stream->AppendBuffer(out_buffer, samples); |     stream->AppendBuffer(out_buffer, samples); | ||||||
|  |  | ||||||
|  |     if (stream->IsPaused()) { | ||||||
|  |         stream->Start(); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| bool DeviceSinkCommand::Verify(const ADSP::CommandListProcessor& processor) { | bool DeviceSinkCommand::Verify(const ADSP::CommandListProcessor& processor) { | ||||||
|   | |||||||
| @@ -15,8 +15,7 @@ MICROPROFILE_DEFINE(Audio_RenderSystemManager, "Audio", "Render System Manager", | |||||||
|                     MP_RGB(60, 19, 97)); |                     MP_RGB(60, 19, 97)); | ||||||
|  |  | ||||||
| namespace AudioCore::AudioRenderer { | namespace AudioCore::AudioRenderer { | ||||||
| constexpr std::chrono::nanoseconds BaseRenderTime{5'000'000UL}; | constexpr std::chrono::nanoseconds RENDER_TIME{5'000'000UL}; | ||||||
| constexpr std::chrono::nanoseconds RenderTimeOffset{400'000UL}; |  | ||||||
|  |  | ||||||
| SystemManager::SystemManager(Core::System& core_) | SystemManager::SystemManager(Core::System& core_) | ||||||
|     : core{core_}, adsp{core.AudioCore().GetADSP()}, mailbox{adsp.GetRenderMailbox()}, |     : core{core_}, adsp{core.AudioCore().GetADSP()}, mailbox{adsp.GetRenderMailbox()}, | ||||||
| @@ -36,8 +35,8 @@ bool SystemManager::InitializeUnsafe() { | |||||||
|         if (adsp.Start()) { |         if (adsp.Start()) { | ||||||
|             active = true; |             active = true; | ||||||
|             thread = std::jthread([this](std::stop_token stop_token) { ThreadFunc(); }); |             thread = std::jthread([this](std::stop_token stop_token) { ThreadFunc(); }); | ||||||
|             core.CoreTiming().ScheduleLoopingEvent(std::chrono::nanoseconds(0), |             core.CoreTiming().ScheduleLoopingEvent(std::chrono::nanoseconds(0), RENDER_TIME, | ||||||
|                                                    BaseRenderTime - RenderTimeOffset, thread_event); |                                                    thread_event); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -121,35 +120,9 @@ void SystemManager::ThreadFunc() { | |||||||
| } | } | ||||||
|  |  | ||||||
| std::optional<std::chrono::nanoseconds> SystemManager::ThreadFunc2(s64 time) { | std::optional<std::chrono::nanoseconds> SystemManager::ThreadFunc2(s64 time) { | ||||||
|     std::optional<std::chrono::nanoseconds> new_schedule_time{std::nullopt}; |  | ||||||
|     const auto queue_size{core.AudioCore().GetStreamQueue()}; |  | ||||||
|     switch (state) { |  | ||||||
|     case StreamState::Filling: |  | ||||||
|         if (queue_size >= 5) { |  | ||||||
|             new_schedule_time = BaseRenderTime; |  | ||||||
|             state = StreamState::Steady; |  | ||||||
|         } |  | ||||||
|         break; |  | ||||||
|     case StreamState::Steady: |  | ||||||
|         if (queue_size <= 2) { |  | ||||||
|             new_schedule_time = BaseRenderTime - RenderTimeOffset; |  | ||||||
|             state = StreamState::Filling; |  | ||||||
|         } else if (queue_size > 5) { |  | ||||||
|             new_schedule_time = BaseRenderTime + RenderTimeOffset; |  | ||||||
|             state = StreamState::Draining; |  | ||||||
|         } |  | ||||||
|         break; |  | ||||||
|     case StreamState::Draining: |  | ||||||
|         if (queue_size <= 5) { |  | ||||||
|             new_schedule_time = BaseRenderTime; |  | ||||||
|             state = StreamState::Steady; |  | ||||||
|         } |  | ||||||
|         break; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     update.store(true); |     update.store(true); | ||||||
|     update.notify_all(); |     update.notify_all(); | ||||||
|     return new_schedule_time; |     return std::nullopt; | ||||||
| } | } | ||||||
|  |  | ||||||
| void SystemManager::PauseCallback(bool paused) { | void SystemManager::PauseCallback(bool paused) { | ||||||
|   | |||||||
| @@ -1,21 +1,13 @@ | |||||||
| // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | ||||||
| // SPDX-License-Identifier: GPL-2.0-or-later | // SPDX-License-Identifier: GPL-2.0-or-later | ||||||
|  |  | ||||||
| #include <algorithm> |  | ||||||
| #include <atomic> |  | ||||||
| #include <span> | #include <span> | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
| #include "audio_core/audio_core.h" | #include "audio_core/common/common.h" | ||||||
| #include "audio_core/audio_event.h" |  | ||||||
| #include "audio_core/audio_manager.h" |  | ||||||
| #include "audio_core/sink/cubeb_sink.h" | #include "audio_core/sink/cubeb_sink.h" | ||||||
| #include "audio_core/sink/sink_stream.h" | #include "audio_core/sink/sink_stream.h" | ||||||
| #include "common/assert.h" |  | ||||||
| #include "common/fixed_point.h" |  | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
| #include "common/reader_writer_queue.h" |  | ||||||
| #include "common/ring_buffer.h" |  | ||||||
| #include "common/settings.h" |  | ||||||
| #include "core/core.h" | #include "core/core.h" | ||||||
|  |  | ||||||
| #ifdef _WIN32 | #ifdef _WIN32 | ||||||
| @@ -42,10 +34,10 @@ public: | |||||||
|      * @param system_          - Core system. |      * @param system_          - Core system. | ||||||
|      * @param event            - Event used only for audio renderer, signalled on buffer consume. |      * @param event            - Event used only for audio renderer, signalled on buffer consume. | ||||||
|      */ |      */ | ||||||
|     CubebSinkStream(cubeb* ctx_, const u32 device_channels_, const u32 system_channels_, |     CubebSinkStream(cubeb* ctx_, u32 device_channels_, u32 system_channels_, | ||||||
|                     cubeb_devid output_device, cubeb_devid input_device, const std::string& name_, |                     cubeb_devid output_device, cubeb_devid input_device, const std::string& name_, | ||||||
|                     const StreamType type_, Core::System& system_) |                     StreamType type_, Core::System& system_) | ||||||
|         : ctx{ctx_}, type{type_}, system{system_} { |         : SinkStream(system_, type_), ctx{ctx_} { | ||||||
| #ifdef _WIN32 | #ifdef _WIN32 | ||||||
|         CoInitializeEx(nullptr, COINIT_MULTITHREADED); |         CoInitializeEx(nullptr, COINIT_MULTITHREADED); | ||||||
| #endif | #endif | ||||||
| @@ -79,12 +71,10 @@ public: | |||||||
|  |  | ||||||
|         minimum_latency = std::max(minimum_latency, 256u); |         minimum_latency = std::max(minimum_latency, 256u); | ||||||
|  |  | ||||||
|         playing_buffer.consumed = true; |         LOG_INFO(Service_Audio, | ||||||
|  |                  "Opening cubeb stream {} type {} with: rate {} channels {} (system channels {}) " | ||||||
|         LOG_DEBUG(Service_Audio, |                  "latency {}", | ||||||
|                   "Opening cubeb stream {} type {} with: rate {} channels {} (system channels {}) " |                  name, type, params.rate, params.channels, system_channels, minimum_latency); | ||||||
|                   "latency {}", |  | ||||||
|                   name, type, params.rate, params.channels, system_channels, minimum_latency); |  | ||||||
|  |  | ||||||
|         auto init_error{0}; |         auto init_error{0}; | ||||||
|         if (type == StreamType::In) { |         if (type == StreamType::In) { | ||||||
| @@ -111,6 +101,8 @@ public: | |||||||
|     ~CubebSinkStream() override { |     ~CubebSinkStream() override { | ||||||
|         LOG_DEBUG(Service_Audio, "Destructing cubeb stream {}", name); |         LOG_DEBUG(Service_Audio, "Destructing cubeb stream {}", name); | ||||||
|  |  | ||||||
|  |         Unstall(); | ||||||
|  |  | ||||||
|         if (!ctx) { |         if (!ctx) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @@ -136,7 +128,7 @@ public: | |||||||
|      * @param resume - Set to true if this is resuming the stream a previously-active stream. |      * @param resume - Set to true if this is resuming the stream a previously-active stream. | ||||||
|      *                 Default false. |      *                 Default false. | ||||||
|      */ |      */ | ||||||
|     void Start(const bool resume = false) override { |     void Start(bool resume = false) override { | ||||||
|         if (!ctx) { |         if (!ctx) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @@ -158,6 +150,7 @@ public: | |||||||
|      * Stop the sink stream. |      * Stop the sink stream. | ||||||
|      */ |      */ | ||||||
|     void Stop() override { |     void Stop() override { | ||||||
|  |         Unstall(); | ||||||
|         if (!ctx) { |         if (!ctx) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @@ -170,194 +163,7 @@ public: | |||||||
|         paused = true; |         paused = true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Append a new buffer and its samples to a waiting queue to play. |  | ||||||
|      * |  | ||||||
|      * @param buffer  - Audio buffer information to be queued. |  | ||||||
|      * @param samples - The s16 samples to be queue for playback. |  | ||||||
|      */ |  | ||||||
|     void AppendBuffer(::AudioCore::Sink::SinkBuffer& buffer, std::vector<s16>& samples) override { |  | ||||||
|         if (type == StreamType::In) { |  | ||||||
|             queue.enqueue(buffer); |  | ||||||
|             queued_buffers++; |  | ||||||
|         } else { |  | ||||||
|             constexpr s32 min{std::numeric_limits<s16>::min()}; |  | ||||||
|             constexpr s32 max{std::numeric_limits<s16>::max()}; |  | ||||||
|  |  | ||||||
|             auto yuzu_volume{Settings::Volume()}; |  | ||||||
|             if (yuzu_volume > 1.0f) { |  | ||||||
|                 yuzu_volume = 0.6f + 20 * std::log10(yuzu_volume); |  | ||||||
|             } |  | ||||||
|             auto volume{system_volume * device_volume * yuzu_volume}; |  | ||||||
|  |  | ||||||
|             if (system_channels == 6 && device_channels == 2) { |  | ||||||
|                 // We're given 6 channels, but our device only outputs 2, so downmix. |  | ||||||
|                 constexpr std::array<f32, 4> down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f}; |  | ||||||
|  |  | ||||||
|                 for (u32 read_index = 0, write_index = 0; read_index < samples.size(); |  | ||||||
|                      read_index += system_channels, write_index += device_channels) { |  | ||||||
|                     const auto left_sample{ |  | ||||||
|                         ((Common::FixedPoint<49, 15>( |  | ||||||
|                               samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * |  | ||||||
|                               down_mix_coeff[0] + |  | ||||||
|                           samples[read_index + static_cast<u32>(Channels::Center)] * |  | ||||||
|                               down_mix_coeff[1] + |  | ||||||
|                           samples[read_index + static_cast<u32>(Channels::LFE)] * |  | ||||||
|                               down_mix_coeff[2] + |  | ||||||
|                           samples[read_index + static_cast<u32>(Channels::BackLeft)] * |  | ||||||
|                               down_mix_coeff[3]) * |  | ||||||
|                          volume) |  | ||||||
|                             .to_int()}; |  | ||||||
|  |  | ||||||
|                     const auto right_sample{ |  | ||||||
|                         ((Common::FixedPoint<49, 15>( |  | ||||||
|                               samples[read_index + static_cast<u32>(Channels::FrontRight)]) * |  | ||||||
|                               down_mix_coeff[0] + |  | ||||||
|                           samples[read_index + static_cast<u32>(Channels::Center)] * |  | ||||||
|                               down_mix_coeff[1] + |  | ||||||
|                           samples[read_index + static_cast<u32>(Channels::LFE)] * |  | ||||||
|                               down_mix_coeff[2] + |  | ||||||
|                           samples[read_index + static_cast<u32>(Channels::BackRight)] * |  | ||||||
|                               down_mix_coeff[3]) * |  | ||||||
|                          volume) |  | ||||||
|                             .to_int()}; |  | ||||||
|  |  | ||||||
|                     samples[write_index + static_cast<u32>(Channels::FrontLeft)] = |  | ||||||
|                         static_cast<s16>(std::clamp(left_sample, min, max)); |  | ||||||
|                     samples[write_index + static_cast<u32>(Channels::FrontRight)] = |  | ||||||
|                         static_cast<s16>(std::clamp(right_sample, min, max)); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 samples.resize(samples.size() / system_channels * device_channels); |  | ||||||
|  |  | ||||||
|             } else if (system_channels == 2 && device_channels == 6) { |  | ||||||
|                 // We need moar samples! Not all games will provide 6 channel audio. |  | ||||||
|                 // TODO: Implement some upmixing here. Currently just passthrough, with other |  | ||||||
|                 // channels left as silence. |  | ||||||
|                 std::vector<s16> new_samples(samples.size() / system_channels * device_channels, 0); |  | ||||||
|  |  | ||||||
|                 for (u32 read_index = 0, write_index = 0; read_index < samples.size(); |  | ||||||
|                      read_index += system_channels, write_index += device_channels) { |  | ||||||
|                     const auto left_sample{static_cast<s16>(std::clamp( |  | ||||||
|                         static_cast<s32>( |  | ||||||
|                             static_cast<f32>( |  | ||||||
|                                 samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * |  | ||||||
|                             volume), |  | ||||||
|                         min, max))}; |  | ||||||
|  |  | ||||||
|                     new_samples[write_index + static_cast<u32>(Channels::FrontLeft)] = left_sample; |  | ||||||
|  |  | ||||||
|                     const auto right_sample{static_cast<s16>(std::clamp( |  | ||||||
|                         static_cast<s32>( |  | ||||||
|                             static_cast<f32>( |  | ||||||
|                                 samples[read_index + static_cast<u32>(Channels::FrontRight)]) * |  | ||||||
|                             volume), |  | ||||||
|                         min, max))}; |  | ||||||
|  |  | ||||||
|                     new_samples[write_index + static_cast<u32>(Channels::FrontRight)] = |  | ||||||
|                         right_sample; |  | ||||||
|                 } |  | ||||||
|                 samples = std::move(new_samples); |  | ||||||
|  |  | ||||||
|             } else if (volume != 1.0f) { |  | ||||||
|                 for (u32 i = 0; i < samples.size(); i++) { |  | ||||||
|                     samples[i] = static_cast<s16>(std::clamp( |  | ||||||
|                         static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             samples_buffer.Push(samples); |  | ||||||
|             queue.enqueue(buffer); |  | ||||||
|             queued_buffers++; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Release a buffer. Audio In only, will fill a buffer with recorded samples. |  | ||||||
|      * |  | ||||||
|      * @param num_samples - Maximum number of samples to receive. |  | ||||||
|      * @return Vector of recorded samples. May have fewer than num_samples. |  | ||||||
|      */ |  | ||||||
|     std::vector<s16> ReleaseBuffer(const u64 num_samples) override { |  | ||||||
|         static constexpr s32 min = std::numeric_limits<s16>::min(); |  | ||||||
|         static constexpr s32 max = std::numeric_limits<s16>::max(); |  | ||||||
|  |  | ||||||
|         auto samples{samples_buffer.Pop(num_samples)}; |  | ||||||
|  |  | ||||||
|         // TODO: Up-mix to 6 channels if the game expects it. |  | ||||||
|         // For audio input this is unlikely to ever be the case though. |  | ||||||
|  |  | ||||||
|         // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here. |  | ||||||
|         // TODO: Play with this and find something that works better. |  | ||||||
|         auto volume{system_volume * device_volume * 8}; |  | ||||||
|         for (u32 i = 0; i < samples.size(); i++) { |  | ||||||
|             samples[i] = static_cast<s16>( |  | ||||||
|                 std::clamp(static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (samples.size() < num_samples) { |  | ||||||
|             samples.resize(num_samples, 0); |  | ||||||
|         } |  | ||||||
|         return samples; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Check if a certain buffer has been consumed (fully played). |  | ||||||
|      * |  | ||||||
|      * @param tag - Unique tag of a buffer to check for. |  | ||||||
|      * @return True if the buffer has been played, otherwise false. |  | ||||||
|      */ |  | ||||||
|     bool IsBufferConsumed(const u64 tag) override { |  | ||||||
|         if (released_buffer.tag == 0) { |  | ||||||
|             if (!released_buffers.try_dequeue(released_buffer)) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (released_buffer.tag == tag) { |  | ||||||
|             released_buffer.tag = 0; |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Empty out the buffer queue. |  | ||||||
|      */ |  | ||||||
|     void ClearQueue() override { |  | ||||||
|         samples_buffer.Pop(); |  | ||||||
|         while (queue.pop()) { |  | ||||||
|         } |  | ||||||
|         while (released_buffers.pop()) { |  | ||||||
|         } |  | ||||||
|         queued_buffers = 0; |  | ||||||
|         released_buffer = {}; |  | ||||||
|         playing_buffer = {}; |  | ||||||
|         playing_buffer.consumed = true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| private: | private: | ||||||
|     /** |  | ||||||
|      * Signal events back to the audio system that a buffer was played/can be filled. |  | ||||||
|      * |  | ||||||
|      * @param buffer - Consumed audio buffer to be released. |  | ||||||
|      */ |  | ||||||
|     void SignalEvent(const ::AudioCore::Sink::SinkBuffer& buffer) { |  | ||||||
|         auto& manager{system.AudioCore().GetAudioManager()}; |  | ||||||
|         switch (type) { |  | ||||||
|         case StreamType::Out: |  | ||||||
|             released_buffers.enqueue(buffer); |  | ||||||
|             manager.SetEvent(Event::Type::AudioOutManager, true); |  | ||||||
|             break; |  | ||||||
|         case StreamType::In: |  | ||||||
|             released_buffers.enqueue(buffer); |  | ||||||
|             manager.SetEvent(Event::Type::AudioInManager, true); |  | ||||||
|             break; |  | ||||||
|         case StreamType::Render: |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Main callback from Cubeb. Either expects samples from us (audio render/audio out), or will |      * Main callback from Cubeb. Either expects samples from us (audio render/audio out), or will | ||||||
|      * provide samples to be copied (audio in). |      * provide samples to be copied (audio in). | ||||||
| @@ -378,106 +184,15 @@ private: | |||||||
|  |  | ||||||
|         const std::size_t num_channels = impl->GetDeviceChannels(); |         const std::size_t num_channels = impl->GetDeviceChannels(); | ||||||
|         const std::size_t frame_size = num_channels; |         const std::size_t frame_size = num_channels; | ||||||
|         const std::size_t frame_size_bytes = frame_size * sizeof(s16); |  | ||||||
|         const std::size_t num_frames{static_cast<size_t>(num_frames_)}; |         const std::size_t num_frames{static_cast<size_t>(num_frames_)}; | ||||||
|         size_t frames_written{0}; |  | ||||||
|         [[maybe_unused]] bool underrun{false}; |  | ||||||
|  |  | ||||||
|         if (impl->type == StreamType::In) { |         if (impl->type == StreamType::In) { | ||||||
|             // INPUT |  | ||||||
|             std::span<const s16> input_buffer{reinterpret_cast<const s16*>(in_buff), |             std::span<const s16> input_buffer{reinterpret_cast<const s16*>(in_buff), | ||||||
|                                               num_frames * frame_size}; |                                               num_frames * frame_size}; | ||||||
|  |             impl->ProcessAudioIn(input_buffer, num_frames); | ||||||
|             while (frames_written < num_frames) { |  | ||||||
|                 auto& playing_buffer{impl->playing_buffer}; |  | ||||||
|  |  | ||||||
|                 // If the playing buffer has been consumed or has no frames, we need a new one |  | ||||||
|                 if (playing_buffer.consumed || playing_buffer.frames == 0) { |  | ||||||
|                     if (!impl->queue.try_dequeue(impl->playing_buffer)) { |  | ||||||
|                         // If no buffer was available we've underrun, just push the samples and |  | ||||||
|                         // continue. |  | ||||||
|                         underrun = true; |  | ||||||
|                         impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], |  | ||||||
|                                                   (num_frames - frames_written) * frame_size); |  | ||||||
|                         frames_written = num_frames; |  | ||||||
|                         continue; |  | ||||||
|                     } else { |  | ||||||
|                         // Successfully got a new buffer, mark the old one as consumed and signal. |  | ||||||
|                         impl->queued_buffers--; |  | ||||||
|                         impl->SignalEvent(impl->playing_buffer); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 // Get the minimum frames available between the currently playing buffer, and the |  | ||||||
|                 // amount we have left to fill |  | ||||||
|                 size_t frames_available{ |  | ||||||
|                     std::min(playing_buffer.frames - playing_buffer.frames_played, |  | ||||||
|                              num_frames - frames_written)}; |  | ||||||
|  |  | ||||||
|                 impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], |  | ||||||
|                                           frames_available * frame_size); |  | ||||||
|  |  | ||||||
|                 frames_written += frames_available; |  | ||||||
|                 playing_buffer.frames_played += frames_available; |  | ||||||
|  |  | ||||||
|                 // If that's all the frames in the current buffer, add its samples and mark it as |  | ||||||
|                 // consumed |  | ||||||
|                 if (playing_buffer.frames_played >= playing_buffer.frames) { |  | ||||||
|                     impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); |  | ||||||
|                     impl->playing_buffer.consumed = true; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             std::memcpy(&impl->last_frame[0], &input_buffer[(frames_written - 1) * frame_size], |  | ||||||
|                         frame_size_bytes); |  | ||||||
|         } else { |         } else { | ||||||
|             // OUTPUT |  | ||||||
|             std::span<s16> output_buffer{reinterpret_cast<s16*>(out_buff), num_frames * frame_size}; |             std::span<s16> output_buffer{reinterpret_cast<s16*>(out_buff), num_frames * frame_size}; | ||||||
|  |             impl->ProcessAudioOutAndRender(output_buffer, num_frames); | ||||||
|             while (frames_written < num_frames) { |  | ||||||
|                 auto& playing_buffer{impl->playing_buffer}; |  | ||||||
|  |  | ||||||
|                 // If the playing buffer has been consumed or has no frames, we need a new one |  | ||||||
|                 if (playing_buffer.consumed || playing_buffer.frames == 0) { |  | ||||||
|                     if (!impl->queue.try_dequeue(impl->playing_buffer)) { |  | ||||||
|                         // If no buffer was available we've underrun, fill the remaining buffer with |  | ||||||
|                         // the last written frame and continue. |  | ||||||
|                         underrun = true; |  | ||||||
|                         for (size_t i = frames_written; i < num_frames; i++) { |  | ||||||
|                             std::memcpy(&output_buffer[i * frame_size], &impl->last_frame[0], |  | ||||||
|                                         frame_size_bytes); |  | ||||||
|                         } |  | ||||||
|                         frames_written = num_frames; |  | ||||||
|                         continue; |  | ||||||
|                     } else { |  | ||||||
|                         // Successfully got a new buffer, mark the old one as consumed and signal. |  | ||||||
|                         impl->queued_buffers--; |  | ||||||
|                         impl->SignalEvent(impl->playing_buffer); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 // Get the minimum frames available between the currently playing buffer, and the |  | ||||||
|                 // amount we have left to fill |  | ||||||
|                 size_t frames_available{ |  | ||||||
|                     std::min(playing_buffer.frames - playing_buffer.frames_played, |  | ||||||
|                              num_frames - frames_written)}; |  | ||||||
|  |  | ||||||
|                 impl->samples_buffer.Pop(&output_buffer[frames_written * frame_size], |  | ||||||
|                                          frames_available * frame_size); |  | ||||||
|  |  | ||||||
|                 frames_written += frames_available; |  | ||||||
|                 playing_buffer.frames_played += frames_available; |  | ||||||
|  |  | ||||||
|                 // If that's all the frames in the current buffer, add its samples and mark it as |  | ||||||
|                 // consumed |  | ||||||
|                 if (playing_buffer.frames_played >= playing_buffer.frames) { |  | ||||||
|                     impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); |  | ||||||
|                     impl->playing_buffer.consumed = true; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             std::memcpy(&impl->last_frame[0], &output_buffer[(frames_written - 1) * frame_size], |  | ||||||
|                         frame_size_bytes); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return num_frames_; |         return num_frames_; | ||||||
| @@ -490,32 +205,12 @@ private: | |||||||
|      * @param user_data   - Custom data pointer passed along, points to a CubebSinkStream. |      * @param user_data   - Custom data pointer passed along, points to a CubebSinkStream. | ||||||
|      * @param state       - New state of the device. |      * @param state       - New state of the device. | ||||||
|      */ |      */ | ||||||
|     static void StateCallback([[maybe_unused]] cubeb_stream* stream, |     static void StateCallback(cubeb_stream*, void*, cubeb_state) {} | ||||||
|                               [[maybe_unused]] void* user_data, |  | ||||||
|                               [[maybe_unused]] cubeb_state state) {} |  | ||||||
|  |  | ||||||
|     /// Main Cubeb context |     /// Main Cubeb context | ||||||
|     cubeb* ctx{}; |     cubeb* ctx{}; | ||||||
|     /// Cubeb stream backend |     /// Cubeb stream backend | ||||||
|     cubeb_stream* stream_backend{}; |     cubeb_stream* stream_backend{}; | ||||||
|     /// Name of this stream |  | ||||||
|     std::string name{}; |  | ||||||
|     /// Type of this stream |  | ||||||
|     StreamType type; |  | ||||||
|     /// Core system |  | ||||||
|     Core::System& system; |  | ||||||
|     /// Ring buffer of the samples waiting to be played or consumed |  | ||||||
|     Common::RingBuffer<s16, 0x10000> samples_buffer; |  | ||||||
|     /// Audio buffers queued and waiting to play |  | ||||||
|     Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> queue; |  | ||||||
|     /// The currently-playing audio buffer |  | ||||||
|     ::AudioCore::Sink::SinkBuffer playing_buffer{}; |  | ||||||
|     /// Audio buffers which have been played and are in queue to be released by the audio system |  | ||||||
|     Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> released_buffers{}; |  | ||||||
|     /// Currently released buffer waiting to be taken by the audio system |  | ||||||
|     ::AudioCore::Sink::SinkBuffer released_buffer{}; |  | ||||||
|     /// The last played (or received) frame of audio, used when the callback underruns |  | ||||||
|     std::array<s16, MaxChannels> last_frame{}; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| CubebSink::CubebSink(std::string_view target_device_name) { | CubebSink::CubebSink(std::string_view target_device_name) { | ||||||
| @@ -569,15 +264,15 @@ CubebSink::~CubebSink() { | |||||||
| #endif | #endif | ||||||
| } | } | ||||||
|  |  | ||||||
| SinkStream* CubebSink::AcquireSinkStream(Core::System& system, const u32 system_channels, | SinkStream* CubebSink::AcquireSinkStream(Core::System& system, u32 system_channels, | ||||||
|                                          const std::string& name, const StreamType type) { |                                          const std::string& name, StreamType type) { | ||||||
|     SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<CubebSinkStream>( |     SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<CubebSinkStream>( | ||||||
|         ctx, device_channels, system_channels, output_device, input_device, name, type, system)); |         ctx, device_channels, system_channels, output_device, input_device, name, type, system)); | ||||||
|  |  | ||||||
|     return stream.get(); |     return stream.get(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void CubebSink::CloseStream(const SinkStream* stream) { | void CubebSink::CloseStream(SinkStream* stream) { | ||||||
|     for (size_t i = 0; i < sink_streams.size(); i++) { |     for (size_t i = 0; i < sink_streams.size(); i++) { | ||||||
|         if (sink_streams[i].get() == stream) { |         if (sink_streams[i].get() == stream) { | ||||||
|             sink_streams[i].reset(); |             sink_streams[i].reset(); | ||||||
| @@ -611,19 +306,19 @@ f32 CubebSink::GetDeviceVolume() const { | |||||||
|     return sink_streams[0]->GetDeviceVolume(); |     return sink_streams[0]->GetDeviceVolume(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void CubebSink::SetDeviceVolume(const f32 volume) { | void CubebSink::SetDeviceVolume(f32 volume) { | ||||||
|     for (auto& stream : sink_streams) { |     for (auto& stream : sink_streams) { | ||||||
|         stream->SetDeviceVolume(volume); |         stream->SetDeviceVolume(volume); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| void CubebSink::SetSystemVolume(const f32 volume) { | void CubebSink::SetSystemVolume(f32 volume) { | ||||||
|     for (auto& stream : sink_streams) { |     for (auto& stream : sink_streams) { | ||||||
|         stream->SetSystemVolume(volume); |         stream->SetSystemVolume(volume); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| std::vector<std::string> ListCubebSinkDevices(const bool capture) { | std::vector<std::string> ListCubebSinkDevices(bool capture) { | ||||||
|     std::vector<std::string> device_list; |     std::vector<std::string> device_list; | ||||||
|     cubeb* ctx; |     cubeb* ctx; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -46,7 +46,7 @@ public: | |||||||
|      * |      * | ||||||
|      * @param stream - The stream to close. |      * @param stream - The stream to close. | ||||||
|      */ |      */ | ||||||
|     void CloseStream(const SinkStream* stream) override; |     void CloseStream(SinkStream* stream) override; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Close all streams. |      * Close all streams. | ||||||
|   | |||||||
| @@ -3,10 +3,29 @@ | |||||||
|  |  | ||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
|  | #include <string> | ||||||
|  | #include <string_view> | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
| #include "audio_core/sink/sink.h" | #include "audio_core/sink/sink.h" | ||||||
| #include "audio_core/sink/sink_stream.h" | #include "audio_core/sink/sink_stream.h" | ||||||
|  |  | ||||||
|  | namespace Core { | ||||||
|  | class System; | ||||||
|  | } // namespace Core | ||||||
|  |  | ||||||
| namespace AudioCore::Sink { | namespace AudioCore::Sink { | ||||||
|  | class NullSinkStreamImpl final : public SinkStream { | ||||||
|  | public: | ||||||
|  |     explicit NullSinkStreamImpl(Core::System& system_, StreamType type_) | ||||||
|  |         : SinkStream{system_, type_} {} | ||||||
|  |     ~NullSinkStreamImpl() override {} | ||||||
|  |     void AppendBuffer(SinkBuffer&, std::vector<s16>&) override {} | ||||||
|  |     std::vector<s16> ReleaseBuffer(u64) override { | ||||||
|  |         return {}; | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * A no-op sink for when no audio out is wanted. |  * A no-op sink for when no audio out is wanted. | ||||||
|  */ |  */ | ||||||
| @@ -15,14 +34,15 @@ public: | |||||||
|     explicit NullSink(std::string_view) {} |     explicit NullSink(std::string_view) {} | ||||||
|     ~NullSink() override = default; |     ~NullSink() override = default; | ||||||
|  |  | ||||||
|     SinkStream* AcquireSinkStream([[maybe_unused]] Core::System& system, |     SinkStream* AcquireSinkStream(Core::System& system, u32, const std::string&, | ||||||
|                                   [[maybe_unused]] u32 system_channels, |                                   StreamType type) override { | ||||||
|                                   [[maybe_unused]] const std::string& name, |         if (null_sink == nullptr) { | ||||||
|                                   [[maybe_unused]] StreamType type) override { |             null_sink = std::make_unique<NullSinkStreamImpl>(system, type); | ||||||
|         return &null_sink_stream; |         } | ||||||
|  |         return null_sink.get(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     void CloseStream([[maybe_unused]] const SinkStream* stream) override {} |     void CloseStream(SinkStream*) override {} | ||||||
|     void CloseStreams() override {} |     void CloseStreams() override {} | ||||||
|     void PauseStreams() override {} |     void PauseStreams() override {} | ||||||
|     void UnpauseStreams() override {} |     void UnpauseStreams() override {} | ||||||
| @@ -33,20 +53,7 @@ public: | |||||||
|     void SetSystemVolume(f32 volume) override {} |     void SetSystemVolume(f32 volume) override {} | ||||||
|  |  | ||||||
| private: | private: | ||||||
|     struct NullSinkStreamImpl final : SinkStream { |     SinkStreamPtr null_sink{}; | ||||||
|         void Finalize() override {} |  | ||||||
|         void Start(bool resume = false) override {} |  | ||||||
|         void Stop() override {} |  | ||||||
|         void AppendBuffer([[maybe_unused]] ::AudioCore::Sink::SinkBuffer& buffer, |  | ||||||
|                           [[maybe_unused]] std::vector<s16>& samples) override {} |  | ||||||
|         std::vector<s16> ReleaseBuffer([[maybe_unused]] u64 num_samples) override { |  | ||||||
|             return {}; |  | ||||||
|         } |  | ||||||
|         bool IsBufferConsumed([[maybe_unused]] const u64 tag) { |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|         void ClearQueue() override {} |  | ||||||
|     } null_sink_stream; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| } // namespace AudioCore::Sink | } // namespace AudioCore::Sink | ||||||
|   | |||||||
| @@ -1,20 +1,13 @@ | |||||||
| // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | ||||||
| // SPDX-License-Identifier: GPL-2.0-or-later | // SPDX-License-Identifier: GPL-2.0-or-later | ||||||
|  |  | ||||||
| #include <algorithm> | #include <span> | ||||||
| #include <atomic> | #include <vector> | ||||||
|  |  | ||||||
| #include "audio_core/audio_core.h" | #include "audio_core/common/common.h" | ||||||
| #include "audio_core/audio_event.h" |  | ||||||
| #include "audio_core/audio_manager.h" |  | ||||||
| #include "audio_core/sink/sdl2_sink.h" | #include "audio_core/sink/sdl2_sink.h" | ||||||
| #include "audio_core/sink/sink_stream.h" | #include "audio_core/sink/sink_stream.h" | ||||||
| #include "common/assert.h" |  | ||||||
| #include "common/fixed_point.h" |  | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
| #include "common/reader_writer_queue.h" |  | ||||||
| #include "common/ring_buffer.h" |  | ||||||
| #include "common/settings.h" |  | ||||||
| #include "core/core.h" | #include "core/core.h" | ||||||
|  |  | ||||||
| // Ignore -Wimplicit-fallthrough due to https://github.com/libsdl-org/SDL/issues/4307 | // Ignore -Wimplicit-fallthrough due to https://github.com/libsdl-org/SDL/issues/4307 | ||||||
| @@ -44,10 +37,9 @@ public: | |||||||
|      * @param system_          - Core system. |      * @param system_          - Core system. | ||||||
|      * @param event            - Event used only for audio renderer, signalled on buffer consume. |      * @param event            - Event used only for audio renderer, signalled on buffer consume. | ||||||
|      */ |      */ | ||||||
|     SDLSinkStream(u32 device_channels_, const u32 system_channels_, |     SDLSinkStream(u32 device_channels_, u32 system_channels_, const std::string& output_device, | ||||||
|                   const std::string& output_device, const std::string& input_device, |                   const std::string& input_device, StreamType type_, Core::System& system_) | ||||||
|                   const StreamType type_, Core::System& system_) |         : SinkStream{system_, type_} { | ||||||
|         : type{type_}, system{system_} { |  | ||||||
|         system_channels = system_channels_; |         system_channels = system_channels_; | ||||||
|         device_channels = device_channels_; |         device_channels = device_channels_; | ||||||
|  |  | ||||||
| @@ -63,8 +55,6 @@ public: | |||||||
|         spec.callback = &SDLSinkStream::DataCallback; |         spec.callback = &SDLSinkStream::DataCallback; | ||||||
|         spec.userdata = this; |         spec.userdata = this; | ||||||
|  |  | ||||||
|         playing_buffer.consumed = true; |  | ||||||
|  |  | ||||||
|         std::string device_name{output_device}; |         std::string device_name{output_device}; | ||||||
|         bool capture{false}; |         bool capture{false}; | ||||||
|         if (type == StreamType::In) { |         if (type == StreamType::In) { | ||||||
| @@ -84,31 +74,30 @@ public: | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         LOG_DEBUG(Service_Audio, |         LOG_INFO(Service_Audio, | ||||||
|                   "Opening sdl stream {} with: rate {} channels {} (system channels {}) " |                  "Opening SDL stream {} with: rate {} channels {} (system channels {}) " | ||||||
|                   " samples {}", |                  " samples {}", | ||||||
|                   device, obtained.freq, obtained.channels, system_channels, obtained.samples); |                  device, obtained.freq, obtained.channels, system_channels, obtained.samples); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Destroy the sink stream. |      * Destroy the sink stream. | ||||||
|      */ |      */ | ||||||
|     ~SDLSinkStream() override { |     ~SDLSinkStream() override { | ||||||
|         if (device == 0) { |         LOG_DEBUG(Service_Audio, "Destructing SDL stream {}", name); | ||||||
|             return; |         Finalize(); | ||||||
|         } |  | ||||||
|  |  | ||||||
|         SDL_CloseAudioDevice(device); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Finalize the sink stream. |      * Finalize the sink stream. | ||||||
|      */ |      */ | ||||||
|     void Finalize() override { |     void Finalize() override { | ||||||
|  |         Unstall(); | ||||||
|         if (device == 0) { |         if (device == 0) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         Stop(); | ||||||
|         SDL_CloseAudioDevice(device); |         SDL_CloseAudioDevice(device); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -118,7 +107,7 @@ public: | |||||||
|      * @param resume - Set to true if this is resuming the stream a previously-active stream. |      * @param resume - Set to true if this is resuming the stream a previously-active stream. | ||||||
|      *                 Default false. |      *                 Default false. | ||||||
|      */ |      */ | ||||||
|     void Start(const bool resume = false) override { |     void Start(bool resume = false) override { | ||||||
|         if (device == 0) { |         if (device == 0) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @@ -135,7 +124,8 @@ public: | |||||||
|     /** |     /** | ||||||
|      * Stop the sink stream. |      * Stop the sink stream. | ||||||
|      */ |      */ | ||||||
|     void Stop() { |     void Stop() override { | ||||||
|  |         Unstall(); | ||||||
|         if (device == 0) { |         if (device == 0) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @@ -143,191 +133,7 @@ public: | |||||||
|         paused = true; |         paused = true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Append a new buffer and its samples to a waiting queue to play. |  | ||||||
|      * |  | ||||||
|      * @param buffer  - Audio buffer information to be queued. |  | ||||||
|      * @param samples - The s16 samples to be queue for playback. |  | ||||||
|      */ |  | ||||||
|     void AppendBuffer(::AudioCore::Sink::SinkBuffer& buffer, std::vector<s16>& samples) override { |  | ||||||
|         if (type == StreamType::In) { |  | ||||||
|             queue.enqueue(buffer); |  | ||||||
|             queued_buffers++; |  | ||||||
|         } else { |  | ||||||
|             constexpr s32 min = std::numeric_limits<s16>::min(); |  | ||||||
|             constexpr s32 max = std::numeric_limits<s16>::max(); |  | ||||||
|  |  | ||||||
|             auto yuzu_volume{Settings::Volume()}; |  | ||||||
|             auto volume{system_volume * device_volume * yuzu_volume}; |  | ||||||
|  |  | ||||||
|             if (system_channels == 6 && device_channels == 2) { |  | ||||||
|                 // We're given 6 channels, but our device only outputs 2, so downmix. |  | ||||||
|                 constexpr std::array<f32, 4> down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f}; |  | ||||||
|  |  | ||||||
|                 for (u32 read_index = 0, write_index = 0; read_index < samples.size(); |  | ||||||
|                      read_index += system_channels, write_index += device_channels) { |  | ||||||
|                     const auto left_sample{ |  | ||||||
|                         ((Common::FixedPoint<49, 15>( |  | ||||||
|                               samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * |  | ||||||
|                               down_mix_coeff[0] + |  | ||||||
|                           samples[read_index + static_cast<u32>(Channels::Center)] * |  | ||||||
|                               down_mix_coeff[1] + |  | ||||||
|                           samples[read_index + static_cast<u32>(Channels::LFE)] * |  | ||||||
|                               down_mix_coeff[2] + |  | ||||||
|                           samples[read_index + static_cast<u32>(Channels::BackLeft)] * |  | ||||||
|                               down_mix_coeff[3]) * |  | ||||||
|                          volume) |  | ||||||
|                             .to_int()}; |  | ||||||
|  |  | ||||||
|                     const auto right_sample{ |  | ||||||
|                         ((Common::FixedPoint<49, 15>( |  | ||||||
|                               samples[read_index + static_cast<u32>(Channels::FrontRight)]) * |  | ||||||
|                               down_mix_coeff[0] + |  | ||||||
|                           samples[read_index + static_cast<u32>(Channels::Center)] * |  | ||||||
|                               down_mix_coeff[1] + |  | ||||||
|                           samples[read_index + static_cast<u32>(Channels::LFE)] * |  | ||||||
|                               down_mix_coeff[2] + |  | ||||||
|                           samples[read_index + static_cast<u32>(Channels::BackRight)] * |  | ||||||
|                               down_mix_coeff[3]) * |  | ||||||
|                          volume) |  | ||||||
|                             .to_int()}; |  | ||||||
|  |  | ||||||
|                     samples[write_index + static_cast<u32>(Channels::FrontLeft)] = |  | ||||||
|                         static_cast<s16>(std::clamp(left_sample, min, max)); |  | ||||||
|                     samples[write_index + static_cast<u32>(Channels::FrontRight)] = |  | ||||||
|                         static_cast<s16>(std::clamp(right_sample, min, max)); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 samples.resize(samples.size() / system_channels * device_channels); |  | ||||||
|  |  | ||||||
|             } else if (system_channels == 2 && device_channels == 6) { |  | ||||||
|                 // We need moar samples! Not all games will provide 6 channel audio. |  | ||||||
|                 // TODO: Implement some upmixing here. Currently just passthrough, with other |  | ||||||
|                 // channels left as silence. |  | ||||||
|                 std::vector<s16> new_samples(samples.size() / system_channels * device_channels, 0); |  | ||||||
|  |  | ||||||
|                 for (u32 read_index = 0, write_index = 0; read_index < samples.size(); |  | ||||||
|                      read_index += system_channels, write_index += device_channels) { |  | ||||||
|                     const auto left_sample{static_cast<s16>(std::clamp( |  | ||||||
|                         static_cast<s32>( |  | ||||||
|                             static_cast<f32>( |  | ||||||
|                                 samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * |  | ||||||
|                             volume), |  | ||||||
|                         min, max))}; |  | ||||||
|  |  | ||||||
|                     new_samples[write_index + static_cast<u32>(Channels::FrontLeft)] = left_sample; |  | ||||||
|  |  | ||||||
|                     const auto right_sample{static_cast<s16>(std::clamp( |  | ||||||
|                         static_cast<s32>( |  | ||||||
|                             static_cast<f32>( |  | ||||||
|                                 samples[read_index + static_cast<u32>(Channels::FrontRight)]) * |  | ||||||
|                             volume), |  | ||||||
|                         min, max))}; |  | ||||||
|  |  | ||||||
|                     new_samples[write_index + static_cast<u32>(Channels::FrontRight)] = |  | ||||||
|                         right_sample; |  | ||||||
|                 } |  | ||||||
|                 samples = std::move(new_samples); |  | ||||||
|  |  | ||||||
|             } else if (volume != 1.0f) { |  | ||||||
|                 for (u32 i = 0; i < samples.size(); i++) { |  | ||||||
|                     samples[i] = static_cast<s16>(std::clamp( |  | ||||||
|                         static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             samples_buffer.Push(samples); |  | ||||||
|             queue.enqueue(buffer); |  | ||||||
|             queued_buffers++; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Release a buffer. Audio In only, will fill a buffer with recorded samples. |  | ||||||
|      * |  | ||||||
|      * @param num_samples - Maximum number of samples to receive. |  | ||||||
|      * @return Vector of recorded samples. May have fewer than num_samples. |  | ||||||
|      */ |  | ||||||
|     std::vector<s16> ReleaseBuffer(const u64 num_samples) override { |  | ||||||
|         static constexpr s32 min = std::numeric_limits<s16>::min(); |  | ||||||
|         static constexpr s32 max = std::numeric_limits<s16>::max(); |  | ||||||
|  |  | ||||||
|         auto samples{samples_buffer.Pop(num_samples)}; |  | ||||||
|  |  | ||||||
|         // TODO: Up-mix to 6 channels if the game expects it. |  | ||||||
|         // For audio input this is unlikely to ever be the case though. |  | ||||||
|  |  | ||||||
|         // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here. |  | ||||||
|         // TODO: Play with this and find something that works better. |  | ||||||
|         auto volume{system_volume * device_volume * 8}; |  | ||||||
|         for (u32 i = 0; i < samples.size(); i++) { |  | ||||||
|             samples[i] = static_cast<s16>( |  | ||||||
|                 std::clamp(static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (samples.size() < num_samples) { |  | ||||||
|             samples.resize(num_samples, 0); |  | ||||||
|         } |  | ||||||
|         return samples; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Check if a certain buffer has been consumed (fully played). |  | ||||||
|      * |  | ||||||
|      * @param tag - Unique tag of a buffer to check for. |  | ||||||
|      * @return True if the buffer has been played, otherwise false. |  | ||||||
|      */ |  | ||||||
|     bool IsBufferConsumed(const u64 tag) override { |  | ||||||
|         if (released_buffer.tag == 0) { |  | ||||||
|             if (!released_buffers.try_dequeue(released_buffer)) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (released_buffer.tag == tag) { |  | ||||||
|             released_buffer.tag = 0; |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Empty out the buffer queue. |  | ||||||
|      */ |  | ||||||
|     void ClearQueue() override { |  | ||||||
|         samples_buffer.Pop(); |  | ||||||
|         while (queue.pop()) { |  | ||||||
|         } |  | ||||||
|         while (released_buffers.pop()) { |  | ||||||
|         } |  | ||||||
|         released_buffer = {}; |  | ||||||
|         playing_buffer = {}; |  | ||||||
|         playing_buffer.consumed = true; |  | ||||||
|         queued_buffers = 0; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| private: | private: | ||||||
|     /** |  | ||||||
|      * Signal events back to the audio system that a buffer was played/can be filled. |  | ||||||
|      * |  | ||||||
|      * @param buffer - Consumed audio buffer to be released. |  | ||||||
|      */ |  | ||||||
|     void SignalEvent(const ::AudioCore::Sink::SinkBuffer& buffer) { |  | ||||||
|         auto& manager{system.AudioCore().GetAudioManager()}; |  | ||||||
|         switch (type) { |  | ||||||
|         case StreamType::Out: |  | ||||||
|             released_buffers.enqueue(buffer); |  | ||||||
|             manager.SetEvent(Event::Type::AudioOutManager, true); |  | ||||||
|             break; |  | ||||||
|         case StreamType::In: |  | ||||||
|             released_buffers.enqueue(buffer); |  | ||||||
|             manager.SetEvent(Event::Type::AudioInManager, true); |  | ||||||
|             break; |  | ||||||
|         case StreamType::Render: |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Main callback from SDL. Either expects samples from us (audio render/audio out), or will |      * Main callback from SDL. Either expects samples from us (audio render/audio out), or will | ||||||
|      * provide samples to be copied (audio in). |      * provide samples to be copied (audio in). | ||||||
| @@ -345,122 +151,20 @@ private: | |||||||
|  |  | ||||||
|         const std::size_t num_channels = impl->GetDeviceChannels(); |         const std::size_t num_channels = impl->GetDeviceChannels(); | ||||||
|         const std::size_t frame_size = num_channels; |         const std::size_t frame_size = num_channels; | ||||||
|         const std::size_t frame_size_bytes = frame_size * sizeof(s16); |  | ||||||
|         const std::size_t num_frames{len / num_channels / sizeof(s16)}; |         const std::size_t num_frames{len / num_channels / sizeof(s16)}; | ||||||
|         size_t frames_written{0}; |  | ||||||
|         [[maybe_unused]] bool underrun{false}; |  | ||||||
|  |  | ||||||
|         if (impl->type == StreamType::In) { |         if (impl->type == StreamType::In) { | ||||||
|             std::span<s16> input_buffer{reinterpret_cast<s16*>(stream), num_frames * frame_size}; |             std::span<const s16> input_buffer{reinterpret_cast<const s16*>(stream), | ||||||
|  |                                               num_frames * frame_size}; | ||||||
|             while (frames_written < num_frames) { |             impl->ProcessAudioIn(input_buffer, num_frames); | ||||||
|                 auto& playing_buffer{impl->playing_buffer}; |  | ||||||
|  |  | ||||||
|                 // If the playing buffer has been consumed or has no frames, we need a new one |  | ||||||
|                 if (playing_buffer.consumed || playing_buffer.frames == 0) { |  | ||||||
|                     if (!impl->queue.try_dequeue(impl->playing_buffer)) { |  | ||||||
|                         // If no buffer was available we've underrun, just push the samples and |  | ||||||
|                         // continue. |  | ||||||
|                         underrun = true; |  | ||||||
|                         impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], |  | ||||||
|                                                   (num_frames - frames_written) * frame_size); |  | ||||||
|                         frames_written = num_frames; |  | ||||||
|                         continue; |  | ||||||
|                     } else { |  | ||||||
|                         impl->queued_buffers--; |  | ||||||
|                         impl->SignalEvent(impl->playing_buffer); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 // Get the minimum frames available between the currently playing buffer, and the |  | ||||||
|                 // amount we have left to fill |  | ||||||
|                 size_t frames_available{ |  | ||||||
|                     std::min(playing_buffer.frames - playing_buffer.frames_played, |  | ||||||
|                              num_frames - frames_written)}; |  | ||||||
|  |  | ||||||
|                 impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], |  | ||||||
|                                           frames_available * frame_size); |  | ||||||
|  |  | ||||||
|                 frames_written += frames_available; |  | ||||||
|                 playing_buffer.frames_played += frames_available; |  | ||||||
|  |  | ||||||
|                 // If that's all the frames in the current buffer, add its samples and mark it as |  | ||||||
|                 // consumed |  | ||||||
|                 if (playing_buffer.frames_played >= playing_buffer.frames) { |  | ||||||
|                     impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); |  | ||||||
|                     impl->playing_buffer.consumed = true; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             std::memcpy(&impl->last_frame[0], &input_buffer[(frames_written - 1) * frame_size], |  | ||||||
|                         frame_size_bytes); |  | ||||||
|         } else { |         } else { | ||||||
|             std::span<s16> output_buffer{reinterpret_cast<s16*>(stream), num_frames * frame_size}; |             std::span<s16> output_buffer{reinterpret_cast<s16*>(stream), num_frames * frame_size}; | ||||||
|  |             impl->ProcessAudioOutAndRender(output_buffer, num_frames); | ||||||
|             while (frames_written < num_frames) { |  | ||||||
|                 auto& playing_buffer{impl->playing_buffer}; |  | ||||||
|  |  | ||||||
|                 // If the playing buffer has been consumed or has no frames, we need a new one |  | ||||||
|                 if (playing_buffer.consumed || playing_buffer.frames == 0) { |  | ||||||
|                     if (!impl->queue.try_dequeue(impl->playing_buffer)) { |  | ||||||
|                         // If no buffer was available we've underrun, fill the remaining buffer with |  | ||||||
|                         // the last written frame and continue. |  | ||||||
|                         underrun = true; |  | ||||||
|                         for (size_t i = frames_written; i < num_frames; i++) { |  | ||||||
|                             std::memcpy(&output_buffer[i * frame_size], &impl->last_frame[0], |  | ||||||
|                                         frame_size_bytes); |  | ||||||
|                         } |  | ||||||
|                         frames_written = num_frames; |  | ||||||
|                         continue; |  | ||||||
|                     } else { |  | ||||||
|                         impl->queued_buffers--; |  | ||||||
|                         impl->SignalEvent(impl->playing_buffer); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 // Get the minimum frames available between the currently playing buffer, and the |  | ||||||
|                 // amount we have left to fill |  | ||||||
|                 size_t frames_available{ |  | ||||||
|                     std::min(playing_buffer.frames - playing_buffer.frames_played, |  | ||||||
|                              num_frames - frames_written)}; |  | ||||||
|  |  | ||||||
|                 impl->samples_buffer.Pop(&output_buffer[frames_written * frame_size], |  | ||||||
|                                          frames_available * frame_size); |  | ||||||
|  |  | ||||||
|                 frames_written += frames_available; |  | ||||||
|                 playing_buffer.frames_played += frames_available; |  | ||||||
|  |  | ||||||
|                 // If that's all the frames in the current buffer, add its samples and mark it as |  | ||||||
|                 // consumed |  | ||||||
|                 if (playing_buffer.frames_played >= playing_buffer.frames) { |  | ||||||
|                     impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); |  | ||||||
|                     impl->playing_buffer.consumed = true; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             std::memcpy(&impl->last_frame[0], &output_buffer[(frames_written - 1) * frame_size], |  | ||||||
|                         frame_size_bytes); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// SDL device id of the opened input/output device |     /// SDL device id of the opened input/output device | ||||||
|     SDL_AudioDeviceID device{}; |     SDL_AudioDeviceID device{}; | ||||||
|     /// Type of this stream |  | ||||||
|     StreamType type; |  | ||||||
|     /// Core system |  | ||||||
|     Core::System& system; |  | ||||||
|     /// Ring buffer of the samples waiting to be played or consumed |  | ||||||
|     Common::RingBuffer<s16, 0x10000> samples_buffer; |  | ||||||
|     /// Audio buffers queued and waiting to play |  | ||||||
|     Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> queue; |  | ||||||
|     /// The currently-playing audio buffer |  | ||||||
|     ::AudioCore::Sink::SinkBuffer playing_buffer{}; |  | ||||||
|     /// Audio buffers which have been played and are in queue to be released by the audio system |  | ||||||
|     Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> released_buffers{}; |  | ||||||
|     /// Currently released buffer waiting to be taken by the audio system |  | ||||||
|     ::AudioCore::Sink::SinkBuffer released_buffer{}; |  | ||||||
|     /// The last played (or received) frame of audio, used when the callback underruns |  | ||||||
|     std::array<s16, MaxChannels> last_frame{}; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| SDLSink::SDLSink(std::string_view target_device_name) { | SDLSink::SDLSink(std::string_view target_device_name) { | ||||||
| @@ -482,14 +186,14 @@ SDLSink::SDLSink(std::string_view target_device_name) { | |||||||
|  |  | ||||||
| SDLSink::~SDLSink() = default; | SDLSink::~SDLSink() = default; | ||||||
|  |  | ||||||
| SinkStream* SDLSink::AcquireSinkStream(Core::System& system, const u32 system_channels, | SinkStream* SDLSink::AcquireSinkStream(Core::System& system, u32 system_channels, | ||||||
|                                        const std::string&, const StreamType type) { |                                        const std::string&, StreamType type) { | ||||||
|     SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<SDLSinkStream>( |     SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<SDLSinkStream>( | ||||||
|         device_channels, system_channels, output_device, input_device, type, system)); |         device_channels, system_channels, output_device, input_device, type, system)); | ||||||
|     return stream.get(); |     return stream.get(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void SDLSink::CloseStream(const SinkStream* stream) { | void SDLSink::CloseStream(SinkStream* stream) { | ||||||
|     for (size_t i = 0; i < sink_streams.size(); i++) { |     for (size_t i = 0; i < sink_streams.size(); i++) { | ||||||
|         if (sink_streams[i].get() == stream) { |         if (sink_streams[i].get() == stream) { | ||||||
|             sink_streams[i].reset(); |             sink_streams[i].reset(); | ||||||
| @@ -523,19 +227,19 @@ f32 SDLSink::GetDeviceVolume() const { | |||||||
|     return sink_streams[0]->GetDeviceVolume(); |     return sink_streams[0]->GetDeviceVolume(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void SDLSink::SetDeviceVolume(const f32 volume) { | void SDLSink::SetDeviceVolume(f32 volume) { | ||||||
|     for (auto& stream : sink_streams) { |     for (auto& stream : sink_streams) { | ||||||
|         stream->SetDeviceVolume(volume); |         stream->SetDeviceVolume(volume); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| void SDLSink::SetSystemVolume(const f32 volume) { | void SDLSink::SetSystemVolume(f32 volume) { | ||||||
|     for (auto& stream : sink_streams) { |     for (auto& stream : sink_streams) { | ||||||
|         stream->SetSystemVolume(volume); |         stream->SetSystemVolume(volume); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| std::vector<std::string> ListSDLSinkDevices(const bool capture) { | std::vector<std::string> ListSDLSinkDevices(bool capture) { | ||||||
|     std::vector<std::string> device_list; |     std::vector<std::string> device_list; | ||||||
|  |  | ||||||
|     if (!SDL_WasInit(SDL_INIT_AUDIO)) { |     if (!SDL_WasInit(SDL_INIT_AUDIO)) { | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ public: | |||||||
|      * |      * | ||||||
|      * @param stream - The stream to close. |      * @param stream - The stream to close. | ||||||
|      */ |      */ | ||||||
|     void CloseStream(const SinkStream* stream) override; |     void CloseStream(SinkStream* stream) override; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Close all streams. |      * Close all streams. | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ public: | |||||||
|      * |      * | ||||||
|      * @param stream - The stream to close. |      * @param stream - The stream to close. | ||||||
|      */ |      */ | ||||||
|     virtual void CloseStream(const SinkStream* stream) = 0; |     virtual void CloseStream(SinkStream* stream) = 0; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Close all streams. |      * Close all streams. | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
| #include <memory> | #include <memory> | ||||||
| #include <string> | #include <string> | ||||||
| #include <vector> | #include <vector> | ||||||
| #include "audio_core/sink/null_sink.h" |  | ||||||
| #include "audio_core/sink/sink_details.h" | #include "audio_core/sink/sink_details.h" | ||||||
| #ifdef HAVE_CUBEB | #ifdef HAVE_CUBEB | ||||||
| #include "audio_core/sink/cubeb_sink.h" | #include "audio_core/sink/cubeb_sink.h" | ||||||
| @@ -13,6 +13,7 @@ | |||||||
| #ifdef HAVE_SDL2 | #ifdef HAVE_SDL2 | ||||||
| #include "audio_core/sink/sdl2_sink.h" | #include "audio_core/sink/sdl2_sink.h" | ||||||
| #endif | #endif | ||||||
|  | #include "audio_core/sink/null_sink.h" | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
|  |  | ||||||
| namespace AudioCore::Sink { | namespace AudioCore::Sink { | ||||||
| @@ -59,8 +60,7 @@ const SinkDetails& GetOutputSinkDetails(std::string_view sink_id) { | |||||||
|  |  | ||||||
|     if (sink_id == "auto" || iter == std::end(sink_details)) { |     if (sink_id == "auto" || iter == std::end(sink_details)) { | ||||||
|         if (sink_id != "auto") { |         if (sink_id != "auto") { | ||||||
|             LOG_ERROR(Audio, "AudioCore::Sink::GetOutputSinkDetails given invalid sink_id {}", |             LOG_ERROR(Audio, "Invalid sink_id {}", sink_id); | ||||||
|                       sink_id); |  | ||||||
|         } |         } | ||||||
|         // Auto-select. |         // Auto-select. | ||||||
|         // sink_details is ordered in terms of desirability, with the best choice at the front. |         // sink_details is ordered in terms of desirability, with the best choice at the front. | ||||||
|   | |||||||
							
								
								
									
										259
									
								
								src/audio_core/sink/sink_stream.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								src/audio_core/sink/sink_stream.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,259 @@ | |||||||
|  | // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | ||||||
|  | // SPDX-License-Identifier: GPL-2.0-or-later | ||||||
|  |  | ||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <array> | ||||||
|  | #include <atomic> | ||||||
|  | #include <memory> | ||||||
|  | #include <span> | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
|  | #include "audio_core/common/common.h" | ||||||
|  | #include "audio_core/sink/sink_stream.h" | ||||||
|  | #include "common/common_types.h" | ||||||
|  | #include "common/fixed_point.h" | ||||||
|  | #include "common/settings.h" | ||||||
|  | #include "core/core.h" | ||||||
|  |  | ||||||
|  | namespace AudioCore::Sink { | ||||||
|  |  | ||||||
|  | void SinkStream::AppendBuffer(SinkBuffer& buffer, std::vector<s16>& samples) { | ||||||
|  |     if (type == StreamType::In) { | ||||||
|  |         queue.enqueue(buffer); | ||||||
|  |         queued_buffers++; | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     constexpr s32 min{std::numeric_limits<s16>::min()}; | ||||||
|  |     constexpr s32 max{std::numeric_limits<s16>::max()}; | ||||||
|  |  | ||||||
|  |     auto yuzu_volume{Settings::Volume()}; | ||||||
|  |     if (yuzu_volume > 1.0f) { | ||||||
|  |         yuzu_volume = 0.6f + 20 * std::log10(yuzu_volume); | ||||||
|  |     } | ||||||
|  |     auto volume{system_volume * device_volume * yuzu_volume}; | ||||||
|  |  | ||||||
|  |     if (system_channels == 6 && device_channels == 2) { | ||||||
|  |         // We're given 6 channels, but our device only outputs 2, so downmix. | ||||||
|  |         constexpr std::array<f32, 4> down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f}; | ||||||
|  |  | ||||||
|  |         for (u32 read_index = 0, write_index = 0; read_index < samples.size(); | ||||||
|  |              read_index += system_channels, write_index += device_channels) { | ||||||
|  |             const auto left_sample{ | ||||||
|  |                 ((Common::FixedPoint<49, 15>( | ||||||
|  |                       samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * | ||||||
|  |                       down_mix_coeff[0] + | ||||||
|  |                   samples[read_index + static_cast<u32>(Channels::Center)] * down_mix_coeff[1] + | ||||||
|  |                   samples[read_index + static_cast<u32>(Channels::LFE)] * down_mix_coeff[2] + | ||||||
|  |                   samples[read_index + static_cast<u32>(Channels::BackLeft)] * down_mix_coeff[3]) * | ||||||
|  |                  volume) | ||||||
|  |                     .to_int()}; | ||||||
|  |  | ||||||
|  |             const auto right_sample{ | ||||||
|  |                 ((Common::FixedPoint<49, 15>( | ||||||
|  |                       samples[read_index + static_cast<u32>(Channels::FrontRight)]) * | ||||||
|  |                       down_mix_coeff[0] + | ||||||
|  |                   samples[read_index + static_cast<u32>(Channels::Center)] * down_mix_coeff[1] + | ||||||
|  |                   samples[read_index + static_cast<u32>(Channels::LFE)] * down_mix_coeff[2] + | ||||||
|  |                   samples[read_index + static_cast<u32>(Channels::BackRight)] * down_mix_coeff[3]) * | ||||||
|  |                  volume) | ||||||
|  |                     .to_int()}; | ||||||
|  |  | ||||||
|  |             samples[write_index + static_cast<u32>(Channels::FrontLeft)] = | ||||||
|  |                 static_cast<s16>(std::clamp(left_sample, min, max)); | ||||||
|  |             samples[write_index + static_cast<u32>(Channels::FrontRight)] = | ||||||
|  |                 static_cast<s16>(std::clamp(right_sample, min, max)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         samples.resize(samples.size() / system_channels * device_channels); | ||||||
|  |  | ||||||
|  |     } else if (system_channels == 2 && device_channels == 6) { | ||||||
|  |         // We need moar samples! Not all games will provide 6 channel audio. | ||||||
|  |         // TODO: Implement some upmixing here. Currently just passthrough, with other | ||||||
|  |         // channels left as silence. | ||||||
|  |         std::vector<s16> new_samples(samples.size() / system_channels * device_channels, 0); | ||||||
|  |  | ||||||
|  |         for (u32 read_index = 0, write_index = 0; read_index < samples.size(); | ||||||
|  |              read_index += system_channels, write_index += device_channels) { | ||||||
|  |             const auto left_sample{static_cast<s16>(std::clamp( | ||||||
|  |                 static_cast<s32>( | ||||||
|  |                     static_cast<f32>(samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * | ||||||
|  |                     volume), | ||||||
|  |                 min, max))}; | ||||||
|  |  | ||||||
|  |             new_samples[write_index + static_cast<u32>(Channels::FrontLeft)] = left_sample; | ||||||
|  |  | ||||||
|  |             const auto right_sample{static_cast<s16>(std::clamp( | ||||||
|  |                 static_cast<s32>( | ||||||
|  |                     static_cast<f32>(samples[read_index + static_cast<u32>(Channels::FrontRight)]) * | ||||||
|  |                     volume), | ||||||
|  |                 min, max))}; | ||||||
|  |  | ||||||
|  |             new_samples[write_index + static_cast<u32>(Channels::FrontRight)] = right_sample; | ||||||
|  |         } | ||||||
|  |         samples = std::move(new_samples); | ||||||
|  |  | ||||||
|  |     } else if (volume != 1.0f) { | ||||||
|  |         for (u32 i = 0; i < samples.size(); i++) { | ||||||
|  |             samples[i] = static_cast<s16>( | ||||||
|  |                 std::clamp(static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     samples_buffer.Push(samples); | ||||||
|  |     queue.enqueue(buffer); | ||||||
|  |     queued_buffers++; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | std::vector<s16> SinkStream::ReleaseBuffer(u64 num_samples) { | ||||||
|  |     constexpr s32 min = std::numeric_limits<s16>::min(); | ||||||
|  |     constexpr s32 max = std::numeric_limits<s16>::max(); | ||||||
|  |  | ||||||
|  |     auto samples{samples_buffer.Pop(num_samples)}; | ||||||
|  |  | ||||||
|  |     // TODO: Up-mix to 6 channels if the game expects it. | ||||||
|  |     // For audio input this is unlikely to ever be the case though. | ||||||
|  |  | ||||||
|  |     // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here. | ||||||
|  |     // TODO: Play with this and find something that works better. | ||||||
|  |     auto volume{system_volume * device_volume * 8}; | ||||||
|  |     for (u32 i = 0; i < samples.size(); i++) { | ||||||
|  |         samples[i] = static_cast<s16>( | ||||||
|  |             std::clamp(static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (samples.size() < num_samples) { | ||||||
|  |         samples.resize(num_samples, 0); | ||||||
|  |     } | ||||||
|  |     return samples; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SinkStream::ClearQueue() { | ||||||
|  |     samples_buffer.Pop(); | ||||||
|  |     while (queue.pop()) { | ||||||
|  |     } | ||||||
|  |     queued_buffers = 0; | ||||||
|  |     playing_buffer = {}; | ||||||
|  |     playing_buffer.consumed = true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SinkStream::ProcessAudioIn(std::span<const s16> input_buffer, std::size_t num_frames) { | ||||||
|  |     const std::size_t num_channels = GetDeviceChannels(); | ||||||
|  |     const std::size_t frame_size = num_channels; | ||||||
|  |     const std::size_t frame_size_bytes = frame_size * sizeof(s16); | ||||||
|  |     size_t frames_written{0}; | ||||||
|  |  | ||||||
|  |     if (queued_buffers > max_queue_size) { | ||||||
|  |         Stall(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     while (frames_written < num_frames) { | ||||||
|  |         // If the playing buffer has been consumed or has no frames, we need a new one | ||||||
|  |         if (playing_buffer.consumed || playing_buffer.frames == 0) { | ||||||
|  |             if (!queue.try_dequeue(playing_buffer)) { | ||||||
|  |                 // If no buffer was available we've underrun, just push the samples and | ||||||
|  |                 // continue. | ||||||
|  |                 samples_buffer.Push(&input_buffer[frames_written * frame_size], | ||||||
|  |                                     (num_frames - frames_written) * frame_size); | ||||||
|  |                 frames_written = num_frames; | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             // Successfully dequeued a new buffer. | ||||||
|  |             queued_buffers--; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Get the minimum frames available between the currently playing buffer, and the | ||||||
|  |         // amount we have left to fill | ||||||
|  |         size_t frames_available{std::min(playing_buffer.frames - playing_buffer.frames_played, | ||||||
|  |                                          num_frames - frames_written)}; | ||||||
|  |  | ||||||
|  |         samples_buffer.Push(&input_buffer[frames_written * frame_size], | ||||||
|  |                             frames_available * frame_size); | ||||||
|  |  | ||||||
|  |         frames_written += frames_available; | ||||||
|  |         playing_buffer.frames_played += frames_available; | ||||||
|  |  | ||||||
|  |         // If that's all the frames in the current buffer, add its samples and mark it as | ||||||
|  |         // consumed | ||||||
|  |         if (playing_buffer.frames_played >= playing_buffer.frames) { | ||||||
|  |             playing_buffer.consumed = true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     std::memcpy(&last_frame[0], &input_buffer[(frames_written - 1) * frame_size], frame_size_bytes); | ||||||
|  |  | ||||||
|  |     if (queued_buffers <= max_queue_size) { | ||||||
|  |         Unstall(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SinkStream::ProcessAudioOutAndRender(std::span<s16> output_buffer, std::size_t num_frames) { | ||||||
|  |     const std::size_t num_channels = GetDeviceChannels(); | ||||||
|  |     const std::size_t frame_size = num_channels; | ||||||
|  |     const std::size_t frame_size_bytes = frame_size * sizeof(s16); | ||||||
|  |     size_t frames_written{0}; | ||||||
|  |  | ||||||
|  |     if (queued_buffers > max_queue_size) { | ||||||
|  |         Stall(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     while (frames_written < num_frames) { | ||||||
|  |         // If the playing buffer has been consumed or has no frames, we need a new one | ||||||
|  |         if (playing_buffer.consumed || playing_buffer.frames == 0) { | ||||||
|  |             if (!queue.try_dequeue(playing_buffer)) { | ||||||
|  |                 // If no buffer was available we've underrun, fill the remaining buffer with | ||||||
|  |                 // the last written frame and continue. | ||||||
|  |                 for (size_t i = frames_written; i < num_frames; i++) { | ||||||
|  |                     std::memcpy(&output_buffer[i * frame_size], &last_frame[0], frame_size_bytes); | ||||||
|  |                 } | ||||||
|  |                 frames_written = num_frames; | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             // Successfully dequeued a new buffer. | ||||||
|  |             queued_buffers--; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Get the minimum frames available between the currently playing buffer, and the | ||||||
|  |         // amount we have left to fill | ||||||
|  |         size_t frames_available{std::min(playing_buffer.frames - playing_buffer.frames_played, | ||||||
|  |                                          num_frames - frames_written)}; | ||||||
|  |  | ||||||
|  |         samples_buffer.Pop(&output_buffer[frames_written * frame_size], | ||||||
|  |                            frames_available * frame_size); | ||||||
|  |  | ||||||
|  |         frames_written += frames_available; | ||||||
|  |         playing_buffer.frames_played += frames_available; | ||||||
|  |  | ||||||
|  |         // If that's all the frames in the current buffer, add its samples and mark it as | ||||||
|  |         // consumed | ||||||
|  |         if (playing_buffer.frames_played >= playing_buffer.frames) { | ||||||
|  |             playing_buffer.consumed = true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     std::memcpy(&last_frame[0], &output_buffer[(frames_written - 1) * frame_size], | ||||||
|  |                 frame_size_bytes); | ||||||
|  |  | ||||||
|  |     if (stalled && queued_buffers <= max_queue_size) { | ||||||
|  |         Unstall(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SinkStream::Stall() { | ||||||
|  |     if (stalled) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     stalled = true; | ||||||
|  |     system.StallProcesses(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SinkStream::Unstall() { | ||||||
|  |     if (!stalled) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     system.UnstallProcesses(); | ||||||
|  |     stalled = false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | } // namespace AudioCore::Sink | ||||||
| @@ -3,12 +3,20 @@ | |||||||
|  |  | ||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
|  | #include <array> | ||||||
| #include <atomic> | #include <atomic> | ||||||
| #include <memory> | #include <memory> | ||||||
|  | #include <span> | ||||||
| #include <vector> | #include <vector> | ||||||
|  |  | ||||||
| #include "audio_core/common/common.h" | #include "audio_core/common/common.h" | ||||||
| #include "common/common_types.h" | #include "common/common_types.h" | ||||||
|  | #include "common/reader_writer_queue.h" | ||||||
|  | #include "common/ring_buffer.h" | ||||||
|  |  | ||||||
|  | namespace Core { | ||||||
|  | class System; | ||||||
|  | } // namespace Core | ||||||
|  |  | ||||||
| namespace AudioCore::Sink { | namespace AudioCore::Sink { | ||||||
|  |  | ||||||
| @@ -34,20 +42,24 @@ struct SinkBuffer { | |||||||
|  * You should regularly call IsBufferConsumed with the unique SinkBuffer tag to check if the buffer |  * You should regularly call IsBufferConsumed with the unique SinkBuffer tag to check if the buffer | ||||||
|  * has been consumed. |  * has been consumed. | ||||||
|  * |  * | ||||||
|  * Since these are a FIFO queue, always check IsBufferConsumed in the same order you appended the |  * Since these are a FIFO queue, IsBufferConsumed must be checked in the same order buffers were | ||||||
|  * buffers, skipping a buffer will result in all following buffers to never release. |  * appended, skipping a buffer will result in the queue getting stuck, and all following buffers to | ||||||
|  |  * never release. | ||||||
|  * |  * | ||||||
|  * If the buffers appear to be stuck, you can stop and re-open an IAudioIn/IAudioOut service (this |  * If the buffers appear to be stuck, you can stop and re-open an IAudioIn/IAudioOut service (this | ||||||
|  * is what games do), or call ClearQueue to flush all of the buffers without a full restart. |  * is what games do), or call ClearQueue to flush all of the buffers without a full restart. | ||||||
|  */ |  */ | ||||||
| class SinkStream { | class SinkStream { | ||||||
| public: | public: | ||||||
|     virtual ~SinkStream() = default; |     explicit SinkStream(Core::System& system_, StreamType type_) : system{system_}, type{type_} {} | ||||||
|  |     virtual ~SinkStream() { | ||||||
|  |         Unstall(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Finalize the sink stream. |      * Finalize the sink stream. | ||||||
|      */ |      */ | ||||||
|     virtual void Finalize() = 0; |     virtual void Finalize() {} | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Start the sink stream. |      * Start the sink stream. | ||||||
| @@ -55,48 +67,19 @@ public: | |||||||
|      * @param resume - Set to true if this is resuming the stream a previously-active stream. |      * @param resume - Set to true if this is resuming the stream a previously-active stream. | ||||||
|      *                 Default false. |      *                 Default false. | ||||||
|      */ |      */ | ||||||
|     virtual void Start(bool resume = false) = 0; |     virtual void Start(bool resume = false) {} | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Stop the sink stream. |      * Stop the sink stream. | ||||||
|      */ |      */ | ||||||
|     virtual void Stop() = 0; |     virtual void Stop() {} | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Append a new buffer and its samples to a waiting queue to play. |  | ||||||
|      * |  | ||||||
|      * @param buffer  - Audio buffer information to be queued. |  | ||||||
|      * @param samples - The s16 samples to be queue for playback. |  | ||||||
|      */ |  | ||||||
|     virtual void AppendBuffer(SinkBuffer& buffer, std::vector<s16>& samples) = 0; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Release a buffer. Audio In only, will fill a buffer with recorded samples. |  | ||||||
|      * |  | ||||||
|      * @param num_samples - Maximum number of samples to receive. |  | ||||||
|      * @return Vector of recorded samples. May have fewer than num_samples. |  | ||||||
|      */ |  | ||||||
|     virtual std::vector<s16> ReleaseBuffer(u64 num_samples) = 0; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Check if a certain buffer has been consumed (fully played). |  | ||||||
|      * |  | ||||||
|      * @param tag - Unique tag of a buffer to check for. |  | ||||||
|      * @return True if the buffer has been played, otherwise false. |  | ||||||
|      */ |  | ||||||
|     virtual bool IsBufferConsumed(u64 tag) = 0; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Empty out the buffer queue. |  | ||||||
|      */ |  | ||||||
|     virtual void ClearQueue() = 0; |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Check if the stream is paused. |      * Check if the stream is paused. | ||||||
|      * |      * | ||||||
|      * @return True if paused, otherwise false. |      * @return True if paused, otherwise false. | ||||||
|      */ |      */ | ||||||
|     bool IsPaused() { |     bool IsPaused() const { | ||||||
|         return paused; |         return paused; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -127,34 +110,6 @@ public: | |||||||
|         return device_channels; |         return device_channels; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Get the total number of samples played by this stream. |  | ||||||
|      * |  | ||||||
|      * @return Number of samples played. |  | ||||||
|      */ |  | ||||||
|     u64 GetPlayedSampleCount() const { |  | ||||||
|         return played_sample_count; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Set the number of samples played. |  | ||||||
|      * This is started and stopped on system start/stop. |  | ||||||
|      * |  | ||||||
|      * @param played_sample_count_ - Number of samples to set. |  | ||||||
|      */ |  | ||||||
|     void SetPlayedSampleCount(u64 played_sample_count_) { |  | ||||||
|         played_sample_count = played_sample_count_; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Add to the played sample count. |  | ||||||
|      * |  | ||||||
|      * @param num_samples - Number of samples to add. |  | ||||||
|      */ |  | ||||||
|     void AddPlayedSampleCount(u64 num_samples) { |  | ||||||
|         played_sample_count += num_samples; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Get the system volume. |      * Get the system volume. | ||||||
|      * |      * | ||||||
| @@ -200,15 +155,65 @@ public: | |||||||
|         return queued_buffers.load(); |         return queued_buffers.load(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Set the maximum buffer queue size. | ||||||
|  |      */ | ||||||
|  |     void SetRingSize(u32 ring_size) { | ||||||
|  |         max_queue_size = ring_size; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Append a new buffer and its samples to a waiting queue to play. | ||||||
|  |      * | ||||||
|  |      * @param buffer  - Audio buffer information to be queued. | ||||||
|  |      * @param samples - The s16 samples to be queue for playback. | ||||||
|  |      */ | ||||||
|  |     virtual void AppendBuffer(SinkBuffer& buffer, std::vector<s16>& samples); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Release a buffer. Audio In only, will fill a buffer with recorded samples. | ||||||
|  |      * | ||||||
|  |      * @param num_samples - Maximum number of samples to receive. | ||||||
|  |      * @return Vector of recorded samples. May have fewer than num_samples. | ||||||
|  |      */ | ||||||
|  |     virtual std::vector<s16> ReleaseBuffer(u64 num_samples); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Empty out the buffer queue. | ||||||
|  |      */ | ||||||
|  |     void ClearQueue(); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Callback for AudioIn. | ||||||
|  |      * | ||||||
|  |      * @param input_buffer - Input buffer to be filled with samples. | ||||||
|  |      * @param num_frames - Number of frames to be filled. | ||||||
|  |      */ | ||||||
|  |     void ProcessAudioIn(std::span<const s16> input_buffer, std::size_t num_frames); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Callback for AudioOut and AudioRenderer. | ||||||
|  |      * | ||||||
|  |      * @param output_buffer - Output buffer to be filled with samples. | ||||||
|  |      * @param num_frames - Number of frames to be filled. | ||||||
|  |      */ | ||||||
|  |     void ProcessAudioOutAndRender(std::span<s16> output_buffer, std::size_t num_frames); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Stall core processes if the audio thread falls too far behind. | ||||||
|  |      */ | ||||||
|  |     void Stall(); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Unstall core processes. | ||||||
|  |      */ | ||||||
|  |     void Unstall(); | ||||||
|  |  | ||||||
| protected: | protected: | ||||||
|     /// Number of buffers waiting to be played |     /// Core system | ||||||
|     std::atomic<u32> queued_buffers{}; |     Core::System& system; | ||||||
|     /// Total samples played by this stream |     /// Type of this stream | ||||||
|     std::atomic<u64> played_sample_count{}; |     StreamType type; | ||||||
|     /// Set by the audio render/in/out system which uses this stream |  | ||||||
|     f32 system_volume{1.0f}; |  | ||||||
|     /// Set via IAudioDevice service calls |  | ||||||
|     f32 device_volume{1.0f}; |  | ||||||
|     /// Set by the audio render/in/out systen which uses this stream |     /// Set by the audio render/in/out systen which uses this stream | ||||||
|     u32 system_channels{2}; |     u32 system_channels{2}; | ||||||
|     /// Channels supported by hardware |     /// Channels supported by hardware | ||||||
| @@ -217,6 +222,28 @@ protected: | |||||||
|     std::atomic<bool> paused{true}; |     std::atomic<bool> paused{true}; | ||||||
|     /// Was this stream previously playing? |     /// Was this stream previously playing? | ||||||
|     std::atomic<bool> was_playing{false}; |     std::atomic<bool> was_playing{false}; | ||||||
|  |     /// Name of this stream | ||||||
|  |     std::string name{}; | ||||||
|  |  | ||||||
|  | private: | ||||||
|  |     /// Ring buffer of the samples waiting to be played or consumed | ||||||
|  |     Common::RingBuffer<s16, 0x10000> samples_buffer; | ||||||
|  |     /// Audio buffers queued and waiting to play | ||||||
|  |     Common::ReaderWriterQueue<SinkBuffer> queue; | ||||||
|  |     /// The currently-playing audio buffer | ||||||
|  |     SinkBuffer playing_buffer{}; | ||||||
|  |     /// The last played (or received) frame of audio, used when the callback underruns | ||||||
|  |     std::array<s16, MaxChannels> last_frame{}; | ||||||
|  |     /// Number of buffers waiting to be played | ||||||
|  |     std::atomic<u32> queued_buffers{}; | ||||||
|  |     /// The ring size for audio out buffers (usually 4, rarely 2 or 8) | ||||||
|  |     u32 max_queue_size{}; | ||||||
|  |     /// Set by the audio render/in/out system which uses this stream | ||||||
|  |     f32 system_volume{1.0f}; | ||||||
|  |     /// Set via IAudioDevice service calls | ||||||
|  |     f32 device_volume{1.0f}; | ||||||
|  |     /// True if coretiming has been stalled | ||||||
|  |     bool stalled{false}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| using SinkStreamPtr = std::unique_ptr<SinkStream>; | using SinkStreamPtr = std::unique_ptr<SinkStream>; | ||||||
|   | |||||||
| @@ -117,6 +117,7 @@ union Result { | |||||||
|     BitField<0, 9, ErrorModule> module; |     BitField<0, 9, ErrorModule> module; | ||||||
|     BitField<9, 13, u32> description; |     BitField<9, 13, u32> description; | ||||||
|  |  | ||||||
|  |     Result() = default; | ||||||
|     constexpr explicit Result(u32 raw_) : raw(raw_) {} |     constexpr explicit Result(u32 raw_) : raw(raw_) {} | ||||||
|  |  | ||||||
|     constexpr Result(ErrorModule module_, u32 description_) |     constexpr Result(ErrorModule module_, u32 description_) | ||||||
| @@ -130,6 +131,7 @@ union Result { | |||||||
|         return !IsSuccess(); |         return !IsSuccess(); | ||||||
|     } |     } | ||||||
| }; | }; | ||||||
|  | static_assert(std::is_trivial_v<Result>); | ||||||
|  |  | ||||||
| [[nodiscard]] constexpr bool operator==(const Result& a, const Result& b) { | [[nodiscard]] constexpr bool operator==(const Result& a, const Result& b) { | ||||||
|     return a.raw == b.raw; |     return a.raw == b.raw; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user