diff options
Diffstat (limited to 'src/audio_core/sink')
-rw-r--r-- | src/audio_core/sink/cubeb_sink.cpp | 383 | ||||
-rw-r--r-- | src/audio_core/sink/cubeb_sink.h | 17 | ||||
-rw-r--r-- | src/audio_core/sink/null_sink.h | 49 | ||||
-rw-r--r-- | src/audio_core/sink/sdl2_sink.cpp | 377 | ||||
-rw-r--r-- | src/audio_core/sink/sdl2_sink.h | 17 | ||||
-rw-r--r-- | src/audio_core/sink/sink.h | 15 | ||||
-rw-r--r-- | src/audio_core/sink/sink_details.cpp | 6 | ||||
-rw-r--r-- | src/audio_core/sink/sink_stream.cpp | 279 | ||||
-rw-r--r-- | src/audio_core/sink/sink_stream.h | 173 |
9 files changed, 477 insertions, 839 deletions
diff --git a/src/audio_core/sink/cubeb_sink.cpp b/src/audio_core/sink/cubeb_sink.cpp index 90d049e8e..36b115ad6 100644 --- a/src/audio_core/sink/cubeb_sink.cpp +++ b/src/audio_core/sink/cubeb_sink.cpp @@ -1,21 +1,13 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include <algorithm> -#include <atomic> #include <span> +#include <vector> -#include "audio_core/audio_core.h" -#include "audio_core/audio_event.h" -#include "audio_core/audio_manager.h" +#include "audio_core/common/common.h" #include "audio_core/sink/cubeb_sink.h" #include "audio_core/sink/sink_stream.h" -#include "common/assert.h" -#include "common/fixed_point.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" #ifdef _WIN32 @@ -42,10 +34,10 @@ public: * @param system_ - Core system. * @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_, - const StreamType type_, Core::System& system_) - : ctx{ctx_}, type{type_}, system{system_} { + StreamType type_, Core::System& system_) + : SinkStream(system_, type_), ctx{ctx_} { #ifdef _WIN32 CoInitializeEx(nullptr, COINIT_MULTITHREADED); #endif @@ -79,12 +71,10 @@ public: minimum_latency = std::max(minimum_latency, 256u); - playing_buffer.consumed = true; - - LOG_DEBUG(Service_Audio, - "Opening cubeb stream {} type {} with: rate {} channels {} (system channels {}) " - "latency {}", - name, type, params.rate, params.channels, system_channels, minimum_latency); + LOG_INFO(Service_Audio, + "Opening cubeb stream {} type {} with: rate {} channels {} (system channels {}) " + "latency {}", + name, type, params.rate, params.channels, system_channels, minimum_latency); auto init_error{0}; if (type == StreamType::In) { @@ -111,6 +101,8 @@ public: ~CubebSinkStream() override { LOG_DEBUG(Service_Audio, "Destructing cubeb stream {}", name); + Unstall(); + if (!ctx) { return; } @@ -136,21 +128,14 @@ public: * @param resume - Set to true if this is resuming the stream a previously-active stream. * Default false. */ - void Start(const bool resume = false) override { - if (!ctx) { + void Start(bool resume = false) override { + if (!ctx || !paused) { return; } - if (resume && was_playing) { - if (cubeb_stream_start(stream_backend) != CUBEB_OK) { - LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); - } - paused = false; - } else if (!resume) { - if (cubeb_stream_start(stream_backend) != CUBEB_OK) { - LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); - } - paused = false; + paused = false; + if (cubeb_stream_start(stream_backend) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); } } @@ -158,207 +143,20 @@ public: * Stop the sink stream. */ void Stop() override { - if (!ctx) { + Unstall(); + + if (!ctx || paused) { return; } + paused = true; if (cubeb_stream_stop(stream_backend) != CUBEB_OK) { LOG_CRITICAL(Audio_Sink, "Error stopping cubeb stream"); } - - was_playing.store(!paused); - 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: /** - * 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 * provide samples to be copied (audio in). * @@ -378,106 +176,15 @@ private: const std::size_t num_channels = impl->GetDeviceChannels(); 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_)}; - size_t frames_written{0}; - [[maybe_unused]] bool underrun{false}; if (impl->type == StreamType::In) { - // INPUT std::span<const s16> input_buffer{reinterpret_cast<const s16*>(in_buff), num_frames * frame_size}; - - 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); + impl->ProcessAudioIn(input_buffer, num_frames); } else { - // OUTPUT std::span<s16> output_buffer{reinterpret_cast<s16*>(out_buff), num_frames * frame_size}; - - 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); + impl->ProcessAudioOutAndRender(output_buffer, num_frames); } return num_frames_; @@ -490,32 +197,12 @@ private: * @param user_data - Custom data pointer passed along, points to a CubebSinkStream. * @param state - New state of the device. */ - static void StateCallback([[maybe_unused]] cubeb_stream* stream, - [[maybe_unused]] void* user_data, - [[maybe_unused]] cubeb_state state) {} + static void StateCallback(cubeb_stream*, void*, cubeb_state) {} /// Main Cubeb context cubeb* ctx{}; /// Cubeb 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) { @@ -569,15 +256,15 @@ CubebSink::~CubebSink() { #endif } -SinkStream* CubebSink::AcquireSinkStream(Core::System& system, const u32 system_channels, - const std::string& name, const StreamType type) { +SinkStream* CubebSink::AcquireSinkStream(Core::System& system, u32 system_channels, + const std::string& name, StreamType type) { SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<CubebSinkStream>( ctx, device_channels, system_channels, output_device, input_device, name, type, system)); return stream.get(); } -void CubebSink::CloseStream(const SinkStream* stream) { +void CubebSink::CloseStream(SinkStream* stream) { for (size_t i = 0; i < sink_streams.size(); i++) { if (sink_streams[i].get() == stream) { sink_streams[i].reset(); @@ -591,18 +278,6 @@ void CubebSink::CloseStreams() { sink_streams.clear(); } -void CubebSink::PauseStreams() { - for (auto& stream : sink_streams) { - stream->Stop(); - } -} - -void CubebSink::UnpauseStreams() { - for (auto& stream : sink_streams) { - stream->Start(true); - } -} - f32 CubebSink::GetDeviceVolume() const { if (sink_streams.empty()) { return 1.0f; @@ -611,19 +286,19 @@ f32 CubebSink::GetDeviceVolume() const { return sink_streams[0]->GetDeviceVolume(); } -void CubebSink::SetDeviceVolume(const f32 volume) { +void CubebSink::SetDeviceVolume(f32 volume) { for (auto& stream : sink_streams) { stream->SetDeviceVolume(volume); } } -void CubebSink::SetSystemVolume(const f32 volume) { +void CubebSink::SetSystemVolume(f32 volume) { for (auto& stream : sink_streams) { stream->SetSystemVolume(volume); } } -std::vector<std::string> ListCubebSinkDevices(const bool capture) { +std::vector<std::string> ListCubebSinkDevices(bool capture) { std::vector<std::string> device_list; cubeb* ctx; diff --git a/src/audio_core/sink/cubeb_sink.h b/src/audio_core/sink/cubeb_sink.h index f0f43dfa1..4b0cb160d 100644 --- a/src/audio_core/sink/cubeb_sink.h +++ b/src/audio_core/sink/cubeb_sink.h @@ -34,8 +34,7 @@ public: * May differ from the device's channel count. * @param name - Name of this stream. * @param type - Type of this stream, render/in/out. - * @param event - Audio render only, a signal used to prevent the renderer running too - * fast. + * * @return A pointer to the created SinkStream */ SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, @@ -46,7 +45,7 @@ public: * * @param stream - The stream to close. */ - void CloseStream(const SinkStream* stream) override; + void CloseStream(SinkStream* stream) override; /** * Close all streams. @@ -54,16 +53,6 @@ public: void CloseStreams() override; /** - * Pause all streams. - */ - void PauseStreams() override; - - /** - * Unpause all streams. - */ - void UnpauseStreams() override; - - /** * Get the device volume. Set from calls to the IAudioDevice service. * * @return Volume of the device. @@ -101,7 +90,7 @@ private: }; /** - * Get a list of conencted devices from Cubeb. + * Get a list of connected devices from Cubeb. * * @param capture - Return input (capture) devices if true, otherwise output devices. */ diff --git a/src/audio_core/sink/null_sink.h b/src/audio_core/sink/null_sink.h index 47a342171..1215d3cd2 100644 --- a/src/audio_core/sink/null_sink.h +++ b/src/audio_core/sink/null_sink.h @@ -3,10 +3,29 @@ #pragma once +#include <string> +#include <string_view> +#include <vector> + #include "audio_core/sink/sink.h" #include "audio_core/sink/sink_stream.h" +namespace Core { +class System; +} // namespace Core + 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. */ @@ -15,17 +34,16 @@ public: explicit NullSink(std::string_view) {} ~NullSink() override = default; - SinkStream* AcquireSinkStream([[maybe_unused]] Core::System& system, - [[maybe_unused]] u32 system_channels, - [[maybe_unused]] const std::string& name, - [[maybe_unused]] StreamType type) override { - return &null_sink_stream; + SinkStream* AcquireSinkStream(Core::System& system, u32, const std::string&, + StreamType type) override { + if (null_sink == nullptr) { + null_sink = std::make_unique<NullSinkStreamImpl>(system, type); + } + return null_sink.get(); } - void CloseStream([[maybe_unused]] const SinkStream* stream) override {} + void CloseStream(SinkStream*) override {} void CloseStreams() override {} - void PauseStreams() override {} - void UnpauseStreams() override {} f32 GetDeviceVolume() const override { return 1.0f; } @@ -33,20 +51,7 @@ public: void SetSystemVolume(f32 volume) override {} private: - struct NullSinkStreamImpl final : SinkStream { - 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; + SinkStreamPtr null_sink{}; }; } // namespace AudioCore::Sink diff --git a/src/audio_core/sink/sdl2_sink.cpp b/src/audio_core/sink/sdl2_sink.cpp index d6c9ec90d..1bd001b94 100644 --- a/src/audio_core/sink/sdl2_sink.cpp +++ b/src/audio_core/sink/sdl2_sink.cpp @@ -1,20 +1,13 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include <algorithm> -#include <atomic> +#include <span> +#include <vector> -#include "audio_core/audio_core.h" -#include "audio_core/audio_event.h" -#include "audio_core/audio_manager.h" +#include "audio_core/common/common.h" #include "audio_core/sink/sdl2_sink.h" #include "audio_core/sink/sink_stream.h" -#include "common/assert.h" -#include "common/fixed_point.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" // Ignore -Wimplicit-fallthrough due to https://github.com/libsdl-org/SDL/issues/4307 @@ -44,10 +37,9 @@ public: * @param system_ - Core system. * @param event - Event used only for audio renderer, signalled on buffer consume. */ - SDLSinkStream(u32 device_channels_, const u32 system_channels_, - const std::string& output_device, const std::string& input_device, - const StreamType type_, Core::System& system_) - : type{type_}, system{system_} { + SDLSinkStream(u32 device_channels_, u32 system_channels_, const std::string& output_device, + const std::string& input_device, StreamType type_, Core::System& system_) + : SinkStream{system_, type_} { system_channels = system_channels_; device_channels = device_channels_; @@ -63,8 +55,6 @@ public: spec.callback = &SDLSinkStream::DataCallback; spec.userdata = this; - playing_buffer.consumed = true; - std::string device_name{output_device}; bool capture{false}; if (type == StreamType::In) { @@ -84,31 +74,30 @@ public: return; } - LOG_DEBUG(Service_Audio, - "Opening sdl stream {} with: rate {} channels {} (system channels {}) " - " samples {}", - device, obtained.freq, obtained.channels, system_channels, obtained.samples); + LOG_INFO(Service_Audio, + "Opening SDL stream {} with: rate {} channels {} (system channels {}) " + " samples {}", + device, obtained.freq, obtained.channels, system_channels, obtained.samples); } /** * Destroy the sink stream. */ ~SDLSinkStream() override { - if (device == 0) { - return; - } - - SDL_CloseAudioDevice(device); + LOG_DEBUG(Service_Audio, "Destructing SDL stream {}", name); + Finalize(); } /** * Finalize the sink stream. */ void Finalize() override { + Unstall(); if (device == 0) { return; } + Stop(); SDL_CloseAudioDevice(device); } @@ -118,217 +107,29 @@ public: * @param resume - Set to true if this is resuming the stream a previously-active stream. * Default false. */ - void Start(const bool resume = false) override { - if (device == 0) { + void Start(bool resume = false) override { + if (device == 0 || !paused) { return; } - if (resume && was_playing) { - SDL_PauseAudioDevice(device, 0); - paused = false; - } else if (!resume) { - SDL_PauseAudioDevice(device, 0); - paused = false; - } + paused = false; + SDL_PauseAudioDevice(device, 0); } /** * Stop the sink stream. */ - void Stop() { - if (device == 0) { + void Stop() override { + Unstall(); + if (device == 0 || paused) { return; } - SDL_PauseAudioDevice(device, 1); 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; + SDL_PauseAudioDevice(device, 1); } 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 * provide samples to be copied (audio in). * @@ -345,122 +146,20 @@ private: const std::size_t num_channels = impl->GetDeviceChannels(); 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)}; - size_t frames_written{0}; - [[maybe_unused]] bool underrun{false}; if (impl->type == StreamType::In) { - std::span<s16> input_buffer{reinterpret_cast<s16*>(stream), num_frames * frame_size}; - - 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 { - 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); + std::span<const s16> input_buffer{reinterpret_cast<const s16*>(stream), + num_frames * frame_size}; + impl->ProcessAudioIn(input_buffer, num_frames); } else { std::span<s16> output_buffer{reinterpret_cast<s16*>(stream), num_frames * frame_size}; - - 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); + impl->ProcessAudioOutAndRender(output_buffer, num_frames); } } /// SDL device id of the opened input/output 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) { @@ -482,14 +181,14 @@ SDLSink::SDLSink(std::string_view target_device_name) { SDLSink::~SDLSink() = default; -SinkStream* SDLSink::AcquireSinkStream(Core::System& system, const u32 system_channels, - const std::string&, const StreamType type) { +SinkStream* SDLSink::AcquireSinkStream(Core::System& system, u32 system_channels, + const std::string&, StreamType type) { SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<SDLSinkStream>( device_channels, system_channels, output_device, input_device, type, system)); return stream.get(); } -void SDLSink::CloseStream(const SinkStream* stream) { +void SDLSink::CloseStream(SinkStream* stream) { for (size_t i = 0; i < sink_streams.size(); i++) { if (sink_streams[i].get() == stream) { sink_streams[i].reset(); @@ -503,18 +202,6 @@ void SDLSink::CloseStreams() { sink_streams.clear(); } -void SDLSink::PauseStreams() { - for (auto& stream : sink_streams) { - stream->Stop(); - } -} - -void SDLSink::UnpauseStreams() { - for (auto& stream : sink_streams) { - stream->Start(); - } -} - f32 SDLSink::GetDeviceVolume() const { if (sink_streams.empty()) { return 1.0f; @@ -523,19 +210,19 @@ f32 SDLSink::GetDeviceVolume() const { return sink_streams[0]->GetDeviceVolume(); } -void SDLSink::SetDeviceVolume(const f32 volume) { +void SDLSink::SetDeviceVolume(f32 volume) { for (auto& stream : sink_streams) { stream->SetDeviceVolume(volume); } } -void SDLSink::SetSystemVolume(const f32 volume) { +void SDLSink::SetSystemVolume(f32 volume) { for (auto& stream : sink_streams) { stream->SetSystemVolume(volume); } } -std::vector<std::string> ListSDLSinkDevices(const bool capture) { +std::vector<std::string> ListSDLSinkDevices(bool capture) { std::vector<std::string> device_list; if (!SDL_WasInit(SDL_INIT_AUDIO)) { diff --git a/src/audio_core/sink/sdl2_sink.h b/src/audio_core/sink/sdl2_sink.h index 186bc2fa3..f01eddc1b 100644 --- a/src/audio_core/sink/sdl2_sink.h +++ b/src/audio_core/sink/sdl2_sink.h @@ -32,8 +32,7 @@ public: * May differ from the device's channel count. * @param name - Name of this stream. * @param type - Type of this stream, render/in/out. - * @param event - Audio render only, a signal used to prevent the renderer running too - * fast. + * * @return A pointer to the created SinkStream */ SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, @@ -44,7 +43,7 @@ public: * * @param stream - The stream to close. */ - void CloseStream(const SinkStream* stream) override; + void CloseStream(SinkStream* stream) override; /** * Close all streams. @@ -52,16 +51,6 @@ public: void CloseStreams() override; /** - * Pause all streams. - */ - void PauseStreams() override; - - /** - * Unpause all streams. - */ - void UnpauseStreams() override; - - /** * Get the device volume. Set from calls to the IAudioDevice service. * * @return Volume of the device. @@ -92,7 +81,7 @@ private: }; /** - * Get a list of conencted devices from Cubeb. + * Get a list of connected devices from SDL. * * @param capture - Return input (capture) devices if true, otherwise output devices. */ diff --git a/src/audio_core/sink/sink.h b/src/audio_core/sink/sink.h index 91fe455e4..f28c6d126 100644 --- a/src/audio_core/sink/sink.h +++ b/src/audio_core/sink/sink.h @@ -32,7 +32,7 @@ public: * * @param stream - The stream to close. */ - virtual void CloseStream(const SinkStream* stream) = 0; + virtual void CloseStream(SinkStream* stream) = 0; /** * Close all streams. @@ -40,16 +40,6 @@ public: virtual void CloseStreams() = 0; /** - * Pause all streams. - */ - virtual void PauseStreams() = 0; - - /** - * Unpause all streams. - */ - virtual void UnpauseStreams() = 0; - - /** * Create a new sink stream, kept within this sink, with a pointer returned for use. * Do not free the returned pointer. When done with the stream, call CloseStream on the sink. * @@ -58,8 +48,7 @@ public: * May differ from the device's channel count. * @param name - Name of this stream. * @param type - Type of this stream, render/in/out. - * @param event - Audio render only, a signal used to prevent the renderer running too - * fast. + * * @return A pointer to the created SinkStream */ virtual SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, diff --git a/src/audio_core/sink/sink_details.cpp b/src/audio_core/sink/sink_details.cpp index 253c0fd1e..67bdab779 100644 --- a/src/audio_core/sink/sink_details.cpp +++ b/src/audio_core/sink/sink_details.cpp @@ -5,7 +5,7 @@ #include <memory> #include <string> #include <vector> -#include "audio_core/sink/null_sink.h" + #include "audio_core/sink/sink_details.h" #ifdef HAVE_CUBEB #include "audio_core/sink/cubeb_sink.h" @@ -13,6 +13,7 @@ #ifdef HAVE_SDL2 #include "audio_core/sink/sdl2_sink.h" #endif +#include "audio_core/sink/null_sink.h" #include "common/logging/log.h" 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") { - LOG_ERROR(Audio, "AudioCore::Sink::GetOutputSinkDetails given invalid sink_id {}", - sink_id); + LOG_ERROR(Audio, "Invalid sink_id {}", sink_id); } // Auto-select. // sink_details is ordered in terms of desirability, with the best choice at the front. diff --git a/src/audio_core/sink/sink_stream.cpp b/src/audio_core/sink/sink_stream.cpp new file mode 100644 index 000000000..37fe725e4 --- /dev/null +++ b/src/audio_core/sink/sink_stream.cpp @@ -0,0 +1,279 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <array> +#include <atomic> +#include <memory> +#include <span> +#include <vector> + +#include "audio_core/audio_core.h" +#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 we're paused or going to shut down, we don't want to consume buffers as coretiming is + // paused and we'll desync, so just return. + if (system.IsPaused() || system.IsShuttingDown()) { + return; + } + + 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 we're paused or going to shut down, we don't want to consume buffers as coretiming is + // paused and we'll desync, so just play silence. + if (system.IsPaused() || system.IsShuttingDown()) { + constexpr std::array<s16, 6> silence{}; + for (size_t i = frames_written; i < num_frames; i++) { + std::memcpy(&output_buffer[i * frame_size], &silence[0], frame_size_bytes); + } + return; + } + + // Due to many frames being queued up with nvdec (5 frames or so?), a lot of buffers also get + // queued up (30+) but not all at once, which causes constant stalling here, so just let the + // video play out without attempting to stall. + // Can hopefully remove this later with a more complete NVDEC implementation. + const auto nvdec_active{system.AudioCore().IsNVDECActive()}; + if (!nvdec_active && 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 diff --git a/src/audio_core/sink/sink_stream.h b/src/audio_core/sink/sink_stream.h index 17ed6593f..9366ebbd3 100644 --- a/src/audio_core/sink/sink_stream.h +++ b/src/audio_core/sink/sink_stream.h @@ -3,12 +3,20 @@ #pragma once +#include <array> #include <atomic> #include <memory> +#include <span> #include <vector> #include "audio_core/common/common.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 { @@ -34,20 +42,24 @@ struct SinkBuffer { * You should regularly call IsBufferConsumed with the unique SinkBuffer tag to check if the buffer * has been consumed. * - * Since these are a FIFO queue, always check IsBufferConsumed in the same order you appended the - * buffers, skipping a buffer will result in all following buffers to never release. + * Since these are a FIFO queue, IsBufferConsumed must be checked in the same order buffers were + * 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 * is what games do), or call ClearQueue to flush all of the buffers without a full restart. */ class SinkStream { public: - virtual ~SinkStream() = default; + explicit SinkStream(Core::System& system_, StreamType type_) : system{system_}, type{type_} {} + virtual ~SinkStream() { + Unstall(); + } /** * Finalize the sink stream. */ - virtual void Finalize() = 0; + virtual void Finalize() {} /** * Start the sink stream. @@ -55,48 +67,19 @@ public: * @param resume - Set to true if this is resuming the stream a previously-active stream. * Default false. */ - virtual void Start(bool resume = false) = 0; + virtual void Start(bool resume = false) {} /** * Stop the sink stream. */ - virtual void Stop() = 0; - - /** - * 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; + virtual void Stop() {} /** * Check if the stream is paused. * * @return True if paused, otherwise false. */ - bool IsPaused() { + bool IsPaused() const { return paused; } @@ -128,34 +111,6 @@ public: } /** - * 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. * * @return The current system volume. @@ -200,23 +155,93 @@ public: 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: - /// Number of buffers waiting to be played - std::atomic<u32> queued_buffers{}; - /// Total samples played by this stream - std::atomic<u64> played_sample_count{}; + /// Core system + Core::System& system; + /// Type of this stream + 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 u32 system_channels{2}; /// Channels supported by hardware u32 device_channels{2}; /// Is this stream currently paused? std::atomic<bool> paused{true}; - /// Was this stream previously playing? - 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>; |