diff options
Diffstat (limited to 'src/audio_core/sink')
| -rw-r--r-- | src/audio_core/sink/cubeb_sink.cpp | 651 | ||||
| -rw-r--r-- | src/audio_core/sink/cubeb_sink.h | 110 | ||||
| -rw-r--r-- | src/audio_core/sink/null_sink.h | 52 | ||||
| -rw-r--r-- | src/audio_core/sink/sdl2_sink.cpp | 556 | ||||
| -rw-r--r-- | src/audio_core/sink/sdl2_sink.h | 101 | ||||
| -rw-r--r-- | src/audio_core/sink/sink.h | 106 | ||||
| -rw-r--r-- | src/audio_core/sink/sink_details.cpp | 91 | ||||
| -rw-r--r-- | src/audio_core/sink/sink_details.h | 43 | ||||
| -rw-r--r-- | src/audio_core/sink/sink_stream.h | 224 |
9 files changed, 1934 insertions, 0 deletions
diff --git a/src/audio_core/sink/cubeb_sink.cpp b/src/audio_core/sink/cubeb_sink.cpp new file mode 100644 index 000000000..a4e28de6d --- /dev/null +++ b/src/audio_core/sink/cubeb_sink.cpp | |||
| @@ -0,0 +1,651 @@ | |||
| 1 | // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | #include <algorithm> | ||
| 5 | #include <atomic> | ||
| 6 | #include <span> | ||
| 7 | |||
| 8 | #include "audio_core/audio_core.h" | ||
| 9 | #include "audio_core/audio_event.h" | ||
| 10 | #include "audio_core/audio_manager.h" | ||
| 11 | #include "audio_core/sink/cubeb_sink.h" | ||
| 12 | #include "audio_core/sink/sink_stream.h" | ||
| 13 | #include "common/assert.h" | ||
| 14 | #include "common/fixed_point.h" | ||
| 15 | #include "common/logging/log.h" | ||
| 16 | #include "common/reader_writer_queue.h" | ||
| 17 | #include "common/ring_buffer.h" | ||
| 18 | #include "common/settings.h" | ||
| 19 | #include "core/core.h" | ||
| 20 | |||
| 21 | #ifdef _WIN32 | ||
| 22 | #include <objbase.h> | ||
| 23 | #undef CreateEvent | ||
| 24 | #endif | ||
| 25 | |||
| 26 | namespace AudioCore::Sink { | ||
| 27 | /** | ||
| 28 | * Cubeb sink stream, responsible for sinking samples to hardware. | ||
| 29 | */ | ||
| 30 | class CubebSinkStream final : public SinkStream { | ||
| 31 | public: | ||
| 32 | /** | ||
| 33 | * Create a new sink stream. | ||
| 34 | * | ||
| 35 | * @param ctx_ - Cubeb context to create this stream with. | ||
| 36 | * @param device_channels_ - Number of channels supported by the hardware. | ||
| 37 | * @param system_channels_ - Number of channels the audio systems expect. | ||
| 38 | * @param output_device - Cubeb output device id. | ||
| 39 | * @param input_device - Cubeb input device id. | ||
| 40 | * @param name_ - Name of this stream. | ||
| 41 | * @param type_ - Type of this stream. | ||
| 42 | * @param system_ - Core system. | ||
| 43 | * @param event - Event used only for audio renderer, signalled on buffer consume. | ||
| 44 | */ | ||
| 45 | CubebSinkStream(cubeb* ctx_, const u32 device_channels_, const u32 system_channels_, | ||
| 46 | cubeb_devid output_device, cubeb_devid input_device, const std::string& name_, | ||
| 47 | const StreamType type_, Core::System& system_) | ||
| 48 | : ctx{ctx_}, type{type_}, system{system_} { | ||
| 49 | #ifdef _WIN32 | ||
| 50 | CoInitializeEx(nullptr, COINIT_MULTITHREADED); | ||
| 51 | #endif | ||
| 52 | name = name_; | ||
| 53 | device_channels = device_channels_; | ||
| 54 | system_channels = system_channels_; | ||
| 55 | |||
| 56 | cubeb_stream_params params{}; | ||
| 57 | params.rate = TargetSampleRate; | ||
| 58 | params.channels = device_channels; | ||
| 59 | params.format = CUBEB_SAMPLE_S16LE; | ||
| 60 | params.prefs = CUBEB_STREAM_PREF_NONE; | ||
| 61 | switch (params.channels) { | ||
| 62 | case 1: | ||
| 63 | params.layout = CUBEB_LAYOUT_MONO; | ||
| 64 | break; | ||
| 65 | case 2: | ||
| 66 | params.layout = CUBEB_LAYOUT_STEREO; | ||
| 67 | break; | ||
| 68 | case 6: | ||
| 69 | params.layout = CUBEB_LAYOUT_3F2_LFE; | ||
| 70 | break; | ||
| 71 | } | ||
| 72 | |||
| 73 | u32 minimum_latency{0}; | ||
| 74 | const auto latency_error = cubeb_get_min_latency(ctx, ¶ms, &minimum_latency); | ||
| 75 | if (latency_error != CUBEB_OK) { | ||
| 76 | LOG_CRITICAL(Audio_Sink, "Error getting minimum latency, error: {}", latency_error); | ||
| 77 | minimum_latency = 256U; | ||
| 78 | } | ||
| 79 | |||
| 80 | minimum_latency = std::max(minimum_latency, 256u); | ||
| 81 | |||
| 82 | playing_buffer.consumed = true; | ||
| 83 | |||
| 84 | LOG_DEBUG(Service_Audio, | ||
| 85 | "Opening cubeb stream {} type {} with: rate {} channels {} (system channels {}) " | ||
| 86 | "latency {}", | ||
| 87 | name, type, params.rate, params.channels, system_channels, minimum_latency); | ||
| 88 | |||
| 89 | auto init_error{0}; | ||
| 90 | if (type == StreamType::In) { | ||
| 91 | init_error = cubeb_stream_init(ctx, &stream_backend, name.c_str(), input_device, | ||
| 92 | ¶ms, output_device, nullptr, minimum_latency, | ||
| 93 | &CubebSinkStream::DataCallback, | ||
| 94 | &CubebSinkStream::StateCallback, this); | ||
| 95 | } else { | ||
| 96 | init_error = cubeb_stream_init(ctx, &stream_backend, name.c_str(), input_device, | ||
| 97 | nullptr, output_device, ¶ms, minimum_latency, | ||
| 98 | &CubebSinkStream::DataCallback, | ||
| 99 | &CubebSinkStream::StateCallback, this); | ||
| 100 | } | ||
| 101 | |||
| 102 | if (init_error != CUBEB_OK) { | ||
| 103 | LOG_CRITICAL(Audio_Sink, "Error initializing cubeb stream, error: {}", init_error); | ||
| 104 | return; | ||
| 105 | } | ||
| 106 | } | ||
| 107 | |||
| 108 | /** | ||
| 109 | * Destroy the sink stream. | ||
| 110 | */ | ||
| 111 | ~CubebSinkStream() override { | ||
| 112 | LOG_DEBUG(Service_Audio, "Destructing cubeb stream {}", name); | ||
| 113 | |||
| 114 | if (!ctx) { | ||
| 115 | return; | ||
| 116 | } | ||
| 117 | |||
| 118 | Finalize(); | ||
| 119 | |||
| 120 | #ifdef _WIN32 | ||
| 121 | CoUninitialize(); | ||
| 122 | #endif | ||
| 123 | } | ||
| 124 | |||
| 125 | /** | ||
| 126 | * Finalize the sink stream. | ||
| 127 | */ | ||
| 128 | void Finalize() override { | ||
| 129 | Stop(); | ||
| 130 | cubeb_stream_destroy(stream_backend); | ||
| 131 | } | ||
| 132 | |||
| 133 | /** | ||
| 134 | * Start the sink stream. | ||
| 135 | * | ||
| 136 | * @param resume - Set to true if this is resuming the stream a previously-active stream. | ||
| 137 | * Default false. | ||
| 138 | */ | ||
| 139 | void Start(const bool resume = false) override { | ||
| 140 | if (!ctx) { | ||
| 141 | return; | ||
| 142 | } | ||
| 143 | |||
| 144 | if (resume && was_playing) { | ||
| 145 | if (cubeb_stream_start(stream_backend) != CUBEB_OK) { | ||
| 146 | LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); | ||
| 147 | } | ||
| 148 | paused = false; | ||
| 149 | } else if (!resume) { | ||
| 150 | if (cubeb_stream_start(stream_backend) != CUBEB_OK) { | ||
| 151 | LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); | ||
| 152 | } | ||
| 153 | paused = false; | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 157 | /** | ||
| 158 | * Stop the sink stream. | ||
| 159 | */ | ||
| 160 | void Stop() override { | ||
| 161 | if (!ctx) { | ||
| 162 | return; | ||
| 163 | } | ||
| 164 | |||
| 165 | if (cubeb_stream_stop(stream_backend) != CUBEB_OK) { | ||
| 166 | LOG_CRITICAL(Audio_Sink, "Error stopping cubeb stream"); | ||
| 167 | } | ||
| 168 | |||
| 169 | was_playing.store(!paused); | ||
| 170 | paused = true; | ||
| 171 | } | ||
| 172 | |||
| 173 | /** | ||
| 174 | * Append a new buffer and its samples to a waiting queue to play. | ||
| 175 | * | ||
| 176 | * @param buffer - Audio buffer information to be queued. | ||
| 177 | * @param samples - The s16 samples to be queue for playback. | ||
| 178 | */ | ||
| 179 | void AppendBuffer(::AudioCore::Sink::SinkBuffer& buffer, std::vector<s16>& samples) override { | ||
| 180 | if (type == StreamType::In) { | ||
| 181 | queue.enqueue(buffer); | ||
| 182 | queued_buffers++; | ||
| 183 | } else { | ||
| 184 | constexpr s32 min{std::numeric_limits<s16>::min()}; | ||
| 185 | constexpr s32 max{std::numeric_limits<s16>::max()}; | ||
| 186 | |||
| 187 | auto yuzu_volume{Settings::Volume()}; | ||
| 188 | auto volume{system_volume * device_volume * yuzu_volume}; | ||
| 189 | |||
| 190 | if (system_channels == 6 && device_channels == 2) { | ||
| 191 | // We're given 6 channels, but our device only outputs 2, so downmix. | ||
| 192 | constexpr std::array<f32, 4> down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f}; | ||
| 193 | |||
| 194 | for (u32 read_index = 0, write_index = 0; read_index < samples.size(); | ||
| 195 | read_index += system_channels, write_index += device_channels) { | ||
| 196 | const auto left_sample{ | ||
| 197 | ((Common::FixedPoint<49, 15>( | ||
| 198 | samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * | ||
| 199 | down_mix_coeff[0] + | ||
| 200 | samples[read_index + static_cast<u32>(Channels::Center)] * | ||
| 201 | down_mix_coeff[1] + | ||
| 202 | samples[read_index + static_cast<u32>(Channels::LFE)] * | ||
| 203 | down_mix_coeff[2] + | ||
| 204 | samples[read_index + static_cast<u32>(Channels::BackLeft)] * | ||
| 205 | down_mix_coeff[3]) * | ||
| 206 | volume) | ||
| 207 | .to_int()}; | ||
| 208 | |||
| 209 | const auto right_sample{ | ||
| 210 | ((Common::FixedPoint<49, 15>( | ||
| 211 | samples[read_index + static_cast<u32>(Channels::FrontRight)]) * | ||
| 212 | down_mix_coeff[0] + | ||
| 213 | samples[read_index + static_cast<u32>(Channels::Center)] * | ||
| 214 | down_mix_coeff[1] + | ||
| 215 | samples[read_index + static_cast<u32>(Channels::LFE)] * | ||
| 216 | down_mix_coeff[2] + | ||
| 217 | samples[read_index + static_cast<u32>(Channels::BackRight)] * | ||
| 218 | down_mix_coeff[3]) * | ||
| 219 | volume) | ||
| 220 | .to_int()}; | ||
| 221 | |||
| 222 | samples[write_index + static_cast<u32>(Channels::FrontLeft)] = | ||
| 223 | static_cast<s16>(std::clamp(left_sample, min, max)); | ||
| 224 | samples[write_index + static_cast<u32>(Channels::FrontRight)] = | ||
| 225 | static_cast<s16>(std::clamp(right_sample, min, max)); | ||
| 226 | } | ||
| 227 | |||
| 228 | samples.resize(samples.size() / system_channels * device_channels); | ||
| 229 | |||
| 230 | } else if (system_channels == 2 && device_channels == 6) { | ||
| 231 | // We need moar samples! Not all games will provide 6 channel audio. | ||
| 232 | // TODO: Implement some upmixing here. Currently just passthrough, with other | ||
| 233 | // channels left as silence. | ||
| 234 | std::vector<s16> new_samples(samples.size() / system_channels * device_channels, 0); | ||
| 235 | |||
| 236 | for (u32 read_index = 0, write_index = 0; read_index < samples.size(); | ||
| 237 | read_index += system_channels, write_index += device_channels) { | ||
| 238 | const auto left_sample{static_cast<s16>(std::clamp( | ||
| 239 | static_cast<s32>( | ||
| 240 | static_cast<f32>( | ||
| 241 | samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * | ||
| 242 | volume), | ||
| 243 | min, max))}; | ||
| 244 | |||
| 245 | new_samples[write_index + static_cast<u32>(Channels::FrontLeft)] = left_sample; | ||
| 246 | |||
| 247 | const auto right_sample{static_cast<s16>(std::clamp( | ||
| 248 | static_cast<s32>( | ||
| 249 | static_cast<f32>( | ||
| 250 | samples[read_index + static_cast<u32>(Channels::FrontRight)]) * | ||
| 251 | volume), | ||
| 252 | min, max))}; | ||
| 253 | |||
| 254 | new_samples[write_index + static_cast<u32>(Channels::FrontRight)] = | ||
| 255 | right_sample; | ||
| 256 | } | ||
| 257 | samples = std::move(new_samples); | ||
| 258 | |||
| 259 | } else if (volume != 1.0f) { | ||
| 260 | for (u32 i = 0; i < samples.size(); i++) { | ||
| 261 | samples[i] = static_cast<s16>(std::clamp( | ||
| 262 | static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); | ||
| 263 | } | ||
| 264 | } | ||
| 265 | |||
| 266 | samples_buffer.Push(samples); | ||
| 267 | queue.enqueue(buffer); | ||
| 268 | queued_buffers++; | ||
| 269 | } | ||
| 270 | } | ||
| 271 | |||
| 272 | /** | ||
| 273 | * Release a buffer. Audio In only, will fill a buffer with recorded samples. | ||
| 274 | * | ||
| 275 | * @param num_samples - Maximum number of samples to receive. | ||
| 276 | * @return Vector of recorded samples. May have fewer than num_samples. | ||
| 277 | */ | ||
| 278 | std::vector<s16> ReleaseBuffer(const u64 num_samples) override { | ||
| 279 | static constexpr s32 min = std::numeric_limits<s16>::min(); | ||
| 280 | static constexpr s32 max = std::numeric_limits<s16>::max(); | ||
| 281 | |||
| 282 | auto samples{samples_buffer.Pop(num_samples)}; | ||
| 283 | |||
| 284 | // TODO: Up-mix to 6 channels if the game expects it. | ||
| 285 | // For audio input this is unlikely to ever be the case though. | ||
| 286 | |||
| 287 | // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here. | ||
| 288 | // TODO: Play with this and find something that works better. | ||
| 289 | auto volume{system_volume * device_volume * 8}; | ||
| 290 | for (u32 i = 0; i < samples.size(); i++) { | ||
| 291 | samples[i] = static_cast<s16>( | ||
| 292 | std::clamp(static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); | ||
| 293 | } | ||
| 294 | |||
| 295 | if (samples.size() < num_samples) { | ||
| 296 | samples.resize(num_samples, 0); | ||
| 297 | } | ||
| 298 | return samples; | ||
| 299 | } | ||
| 300 | |||
| 301 | /** | ||
| 302 | * Check if a certain buffer has been consumed (fully played). | ||
| 303 | * | ||
| 304 | * @param tag - Unique tag of a buffer to check for. | ||
| 305 | * @return True if the buffer has been played, otherwise false. | ||
| 306 | */ | ||
| 307 | bool IsBufferConsumed(const u64 tag) override { | ||
| 308 | if (released_buffer.tag == 0) { | ||
| 309 | if (!released_buffers.try_dequeue(released_buffer)) { | ||
| 310 | return false; | ||
| 311 | } | ||
| 312 | } | ||
| 313 | |||
| 314 | if (released_buffer.tag == tag) { | ||
| 315 | released_buffer.tag = 0; | ||
| 316 | return true; | ||
| 317 | } | ||
| 318 | return false; | ||
| 319 | } | ||
| 320 | |||
| 321 | /** | ||
| 322 | * Empty out the buffer queue. | ||
| 323 | */ | ||
| 324 | void ClearQueue() override { | ||
| 325 | samples_buffer.Pop(); | ||
| 326 | while (queue.pop()) { | ||
| 327 | } | ||
| 328 | while (released_buffers.pop()) { | ||
| 329 | } | ||
| 330 | queued_buffers = 0; | ||
| 331 | released_buffer = {}; | ||
| 332 | playing_buffer = {}; | ||
| 333 | playing_buffer.consumed = true; | ||
| 334 | } | ||
| 335 | |||
| 336 | private: | ||
| 337 | /** | ||
| 338 | * Signal events back to the audio system that a buffer was played/can be filled. | ||
| 339 | * | ||
| 340 | * @param buffer - Consumed audio buffer to be released. | ||
| 341 | */ | ||
| 342 | void SignalEvent(const ::AudioCore::Sink::SinkBuffer& buffer) { | ||
| 343 | auto& manager{system.AudioCore().GetAudioManager()}; | ||
| 344 | switch (type) { | ||
| 345 | case StreamType::Out: | ||
| 346 | released_buffers.enqueue(buffer); | ||
| 347 | manager.SetEvent(Event::Type::AudioOutManager, true); | ||
| 348 | break; | ||
| 349 | case StreamType::In: | ||
| 350 | released_buffers.enqueue(buffer); | ||
| 351 | manager.SetEvent(Event::Type::AudioInManager, true); | ||
| 352 | break; | ||
| 353 | case StreamType::Render: | ||
| 354 | break; | ||
| 355 | } | ||
| 356 | } | ||
| 357 | |||
| 358 | /** | ||
| 359 | * Main callback from Cubeb. Either expects samples from us (audio render/audio out), or will | ||
| 360 | * provide samples to be copied (audio in). | ||
| 361 | * | ||
| 362 | * @param stream - Cubeb-specific data about the stream. | ||
| 363 | * @param user_data - Custom data pointer passed along, points to a CubebSinkStream. | ||
| 364 | * @param in_buff - Input buffer to be used if the stream is an input type. | ||
| 365 | * @param out_buff - Output buffer to be used if the stream is an output type. | ||
| 366 | * @param num_frames_ - Number of frames of audio in the buffers. Note: Not number of samples. | ||
| 367 | */ | ||
| 368 | static long DataCallback([[maybe_unused]] cubeb_stream* stream, void* user_data, | ||
| 369 | [[maybe_unused]] const void* in_buff, void* out_buff, | ||
| 370 | long num_frames_) { | ||
| 371 | auto* impl = static_cast<CubebSinkStream*>(user_data); | ||
| 372 | if (!impl) { | ||
| 373 | return -1; | ||
| 374 | } | ||
| 375 | |||
| 376 | const std::size_t num_channels = impl->GetDeviceChannels(); | ||
| 377 | const std::size_t frame_size = num_channels; | ||
| 378 | const std::size_t frame_size_bytes = frame_size * sizeof(s16); | ||
| 379 | const std::size_t num_frames{static_cast<size_t>(num_frames_)}; | ||
| 380 | size_t frames_written{0}; | ||
| 381 | [[maybe_unused]] bool underrun{false}; | ||
| 382 | |||
| 383 | if (impl->type == StreamType::In) { | ||
| 384 | // INPUT | ||
| 385 | std::span<const s16> input_buffer{reinterpret_cast<const s16*>(in_buff), | ||
| 386 | num_frames * frame_size}; | ||
| 387 | |||
| 388 | while (frames_written < num_frames) { | ||
| 389 | auto& playing_buffer{impl->playing_buffer}; | ||
| 390 | |||
| 391 | // If the playing buffer has been consumed or has no frames, we need a new one | ||
| 392 | if (playing_buffer.consumed || playing_buffer.frames == 0) { | ||
| 393 | if (!impl->queue.try_dequeue(impl->playing_buffer)) { | ||
| 394 | // If no buffer was available we've underrun, just push the samples and | ||
| 395 | // continue. | ||
| 396 | underrun = true; | ||
| 397 | impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], | ||
| 398 | (num_frames - frames_written) * frame_size); | ||
| 399 | frames_written = num_frames; | ||
| 400 | continue; | ||
| 401 | } else { | ||
| 402 | // Successfully got a new buffer, mark the old one as consumed and signal. | ||
| 403 | impl->queued_buffers--; | ||
| 404 | impl->SignalEvent(impl->playing_buffer); | ||
| 405 | } | ||
| 406 | } | ||
| 407 | |||
| 408 | // Get the minimum frames available between the currently playing buffer, and the | ||
| 409 | // amount we have left to fill | ||
| 410 | size_t frames_available{ | ||
| 411 | std::min(playing_buffer.frames - playing_buffer.frames_played, | ||
| 412 | num_frames - frames_written)}; | ||
| 413 | |||
| 414 | impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], | ||
| 415 | frames_available * frame_size); | ||
| 416 | |||
| 417 | frames_written += frames_available; | ||
| 418 | playing_buffer.frames_played += frames_available; | ||
| 419 | |||
| 420 | // If that's all the frames in the current buffer, add its samples and mark it as | ||
| 421 | // consumed | ||
| 422 | if (playing_buffer.frames_played >= playing_buffer.frames) { | ||
| 423 | impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); | ||
| 424 | impl->playing_buffer.consumed = true; | ||
| 425 | } | ||
| 426 | } | ||
| 427 | |||
| 428 | std::memcpy(&impl->last_frame[0], &input_buffer[(frames_written - 1) * frame_size], | ||
| 429 | frame_size_bytes); | ||
| 430 | } else { | ||
| 431 | // OUTPUT | ||
| 432 | std::span<s16> output_buffer{reinterpret_cast<s16*>(out_buff), num_frames * frame_size}; | ||
| 433 | |||
| 434 | while (frames_written < num_frames) { | ||
| 435 | auto& playing_buffer{impl->playing_buffer}; | ||
| 436 | |||
| 437 | // If the playing buffer has been consumed or has no frames, we need a new one | ||
| 438 | if (playing_buffer.consumed || playing_buffer.frames == 0) { | ||
| 439 | if (!impl->queue.try_dequeue(impl->playing_buffer)) { | ||
| 440 | // If no buffer was available we've underrun, fill the remaining buffer with | ||
| 441 | // the last written frame and continue. | ||
| 442 | underrun = true; | ||
| 443 | for (size_t i = frames_written; i < num_frames; i++) { | ||
| 444 | std::memcpy(&output_buffer[i * frame_size], &impl->last_frame[0], | ||
| 445 | frame_size_bytes); | ||
| 446 | } | ||
| 447 | frames_written = num_frames; | ||
| 448 | continue; | ||
| 449 | } else { | ||
| 450 | // Successfully got a new buffer, mark the old one as consumed and signal. | ||
| 451 | impl->queued_buffers--; | ||
| 452 | impl->SignalEvent(impl->playing_buffer); | ||
| 453 | } | ||
| 454 | } | ||
| 455 | |||
| 456 | // Get the minimum frames available between the currently playing buffer, and the | ||
| 457 | // amount we have left to fill | ||
| 458 | size_t frames_available{ | ||
| 459 | std::min(playing_buffer.frames - playing_buffer.frames_played, | ||
| 460 | num_frames - frames_written)}; | ||
| 461 | |||
| 462 | impl->samples_buffer.Pop(&output_buffer[frames_written * frame_size], | ||
| 463 | frames_available * frame_size); | ||
| 464 | |||
| 465 | frames_written += frames_available; | ||
| 466 | playing_buffer.frames_played += frames_available; | ||
| 467 | |||
| 468 | // If that's all the frames in the current buffer, add its samples and mark it as | ||
| 469 | // consumed | ||
| 470 | if (playing_buffer.frames_played >= playing_buffer.frames) { | ||
| 471 | impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); | ||
| 472 | impl->playing_buffer.consumed = true; | ||
| 473 | } | ||
| 474 | } | ||
| 475 | |||
| 476 | std::memcpy(&impl->last_frame[0], &output_buffer[(frames_written - 1) * frame_size], | ||
| 477 | frame_size_bytes); | ||
| 478 | } | ||
| 479 | |||
| 480 | return num_frames_; | ||
| 481 | } | ||
| 482 | |||
| 483 | /** | ||
| 484 | * Cubeb callback for if a device state changes. Unused currently. | ||
| 485 | * | ||
| 486 | * @param stream - Cubeb-specific data about the stream. | ||
| 487 | * @param user_data - Custom data pointer passed along, points to a CubebSinkStream. | ||
| 488 | * @param state - New state of the device. | ||
| 489 | */ | ||
| 490 | static void StateCallback([[maybe_unused]] cubeb_stream* stream, | ||
| 491 | [[maybe_unused]] void* user_data, | ||
| 492 | [[maybe_unused]] cubeb_state state) {} | ||
| 493 | |||
| 494 | /// Main Cubeb context | ||
| 495 | cubeb* ctx{}; | ||
| 496 | /// Cubeb stream backend | ||
| 497 | cubeb_stream* stream_backend{}; | ||
| 498 | /// Name of this stream | ||
| 499 | std::string name{}; | ||
| 500 | /// Type of this stream | ||
| 501 | StreamType type; | ||
| 502 | /// Core system | ||
| 503 | Core::System& system; | ||
| 504 | /// Ring buffer of the samples waiting to be played or consumed | ||
| 505 | Common::RingBuffer<s16, 0x10000> samples_buffer; | ||
| 506 | /// Audio buffers queued and waiting to play | ||
| 507 | Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> queue; | ||
| 508 | /// The currently-playing audio buffer | ||
| 509 | ::AudioCore::Sink::SinkBuffer playing_buffer{}; | ||
| 510 | /// Audio buffers which have been played and are in queue to be released by the audio system | ||
| 511 | Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> released_buffers{}; | ||
| 512 | /// Currently released buffer waiting to be taken by the audio system | ||
| 513 | ::AudioCore::Sink::SinkBuffer released_buffer{}; | ||
| 514 | /// The last played (or received) frame of audio, used when the callback underruns | ||
| 515 | std::array<s16, MaxChannels> last_frame{}; | ||
| 516 | }; | ||
| 517 | |||
| 518 | CubebSink::CubebSink(std::string_view target_device_name) { | ||
| 519 | // Cubeb requires COM to be initialized on the thread calling cubeb_init on Windows | ||
| 520 | #ifdef _WIN32 | ||
| 521 | com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); | ||
| 522 | #endif | ||
| 523 | |||
| 524 | if (cubeb_init(&ctx, "yuzu", nullptr) != CUBEB_OK) { | ||
| 525 | LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); | ||
| 526 | return; | ||
| 527 | } | ||
| 528 | |||
| 529 | if (target_device_name != auto_device_name && !target_device_name.empty()) { | ||
| 530 | cubeb_device_collection collection; | ||
| 531 | if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) { | ||
| 532 | LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); | ||
| 533 | } else { | ||
| 534 | const auto collection_end{collection.device + collection.count}; | ||
| 535 | const auto device{ | ||
| 536 | std::find_if(collection.device, collection_end, [&](const cubeb_device_info& info) { | ||
| 537 | return info.friendly_name != nullptr && | ||
| 538 | target_device_name == std::string(info.friendly_name); | ||
| 539 | })}; | ||
| 540 | if (device != collection_end) { | ||
| 541 | output_device = device->devid; | ||
| 542 | } | ||
| 543 | cubeb_device_collection_destroy(ctx, &collection); | ||
| 544 | } | ||
| 545 | } | ||
| 546 | |||
| 547 | cubeb_get_max_channel_count(ctx, &device_channels); | ||
| 548 | device_channels = device_channels >= 6U ? 6U : 2U; | ||
| 549 | } | ||
| 550 | |||
| 551 | CubebSink::~CubebSink() { | ||
| 552 | if (!ctx) { | ||
| 553 | return; | ||
| 554 | } | ||
| 555 | |||
| 556 | for (auto& sink_stream : sink_streams) { | ||
| 557 | sink_stream.reset(); | ||
| 558 | } | ||
| 559 | |||
| 560 | cubeb_destroy(ctx); | ||
| 561 | |||
| 562 | #ifdef _WIN32 | ||
| 563 | if (SUCCEEDED(com_init_result)) { | ||
| 564 | CoUninitialize(); | ||
| 565 | } | ||
| 566 | #endif | ||
| 567 | } | ||
| 568 | |||
| 569 | SinkStream* CubebSink::AcquireSinkStream(Core::System& system, const u32 system_channels, | ||
| 570 | const std::string& name, const StreamType type) { | ||
| 571 | SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<CubebSinkStream>( | ||
| 572 | ctx, device_channels, system_channels, output_device, input_device, name, type, system)); | ||
| 573 | |||
| 574 | return stream.get(); | ||
| 575 | } | ||
| 576 | |||
| 577 | void CubebSink::CloseStream(const SinkStream* stream) { | ||
| 578 | for (size_t i = 0; i < sink_streams.size(); i++) { | ||
| 579 | if (sink_streams[i].get() == stream) { | ||
| 580 | sink_streams[i].reset(); | ||
| 581 | sink_streams.erase(sink_streams.begin() + i); | ||
| 582 | break; | ||
| 583 | } | ||
| 584 | } | ||
| 585 | } | ||
| 586 | |||
| 587 | void CubebSink::CloseStreams() { | ||
| 588 | sink_streams.clear(); | ||
| 589 | } | ||
| 590 | |||
| 591 | void CubebSink::PauseStreams() { | ||
| 592 | for (auto& stream : sink_streams) { | ||
| 593 | stream->Stop(); | ||
| 594 | } | ||
| 595 | } | ||
| 596 | |||
| 597 | void CubebSink::UnpauseStreams() { | ||
| 598 | for (auto& stream : sink_streams) { | ||
| 599 | stream->Start(true); | ||
| 600 | } | ||
| 601 | } | ||
| 602 | |||
| 603 | f32 CubebSink::GetDeviceVolume() const { | ||
| 604 | if (sink_streams.empty()) { | ||
| 605 | return 1.0f; | ||
| 606 | } | ||
| 607 | |||
| 608 | return sink_streams[0]->GetDeviceVolume(); | ||
| 609 | } | ||
| 610 | |||
| 611 | void CubebSink::SetDeviceVolume(const f32 volume) { | ||
| 612 | for (auto& stream : sink_streams) { | ||
| 613 | stream->SetDeviceVolume(volume); | ||
| 614 | } | ||
| 615 | } | ||
| 616 | |||
| 617 | void CubebSink::SetSystemVolume(const f32 volume) { | ||
| 618 | for (auto& stream : sink_streams) { | ||
| 619 | stream->SetSystemVolume(volume); | ||
| 620 | } | ||
| 621 | } | ||
| 622 | |||
| 623 | std::vector<std::string> ListCubebSinkDevices(const bool capture) { | ||
| 624 | std::vector<std::string> device_list; | ||
| 625 | cubeb* ctx; | ||
| 626 | |||
| 627 | if (cubeb_init(&ctx, "yuzu Device Enumerator", nullptr) != CUBEB_OK) { | ||
| 628 | LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); | ||
| 629 | return {}; | ||
| 630 | } | ||
| 631 | |||
| 632 | auto type{capture ? CUBEB_DEVICE_TYPE_INPUT : CUBEB_DEVICE_TYPE_OUTPUT}; | ||
| 633 | cubeb_device_collection collection; | ||
| 634 | if (cubeb_enumerate_devices(ctx, type, &collection) != CUBEB_OK) { | ||
| 635 | LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); | ||
| 636 | } else { | ||
| 637 | for (std::size_t i = 0; i < collection.count; i++) { | ||
| 638 | const cubeb_device_info& device = collection.device[i]; | ||
| 639 | if (device.friendly_name && device.friendly_name[0] != '\0' && | ||
| 640 | device.state == CUBEB_DEVICE_STATE_ENABLED) { | ||
| 641 | device_list.emplace_back(device.friendly_name); | ||
| 642 | } | ||
| 643 | } | ||
| 644 | cubeb_device_collection_destroy(ctx, &collection); | ||
| 645 | } | ||
| 646 | |||
| 647 | cubeb_destroy(ctx); | ||
| 648 | return device_list; | ||
| 649 | } | ||
| 650 | |||
| 651 | } // namespace AudioCore::Sink | ||
diff --git a/src/audio_core/sink/cubeb_sink.h b/src/audio_core/sink/cubeb_sink.h new file mode 100644 index 000000000..f0f43dfa1 --- /dev/null +++ b/src/audio_core/sink/cubeb_sink.h | |||
| @@ -0,0 +1,110 @@ | |||
| 1 | // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | #pragma once | ||
| 5 | |||
| 6 | #include <string> | ||
| 7 | #include <vector> | ||
| 8 | |||
| 9 | #include <cubeb/cubeb.h> | ||
| 10 | |||
| 11 | #include "audio_core/sink/sink.h" | ||
| 12 | |||
| 13 | namespace Core { | ||
| 14 | class System; | ||
| 15 | } | ||
| 16 | |||
| 17 | namespace AudioCore::Sink { | ||
| 18 | class SinkStream; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * Cubeb backend sink, holds multiple output streams and is responsible for sinking samples to | ||
| 22 | * hardware. Used by Audio Render, Audio In and Audio Out. | ||
| 23 | */ | ||
| 24 | class CubebSink final : public Sink { | ||
| 25 | public: | ||
| 26 | explicit CubebSink(std::string_view device_id); | ||
| 27 | ~CubebSink() override; | ||
| 28 | |||
| 29 | /** | ||
| 30 | * Create a new sink stream. | ||
| 31 | * | ||
| 32 | * @param system - Core system. | ||
| 33 | * @param system_channels - Number of channels the audio system expects. | ||
| 34 | * May differ from the device's channel count. | ||
| 35 | * @param name - Name of this stream. | ||
| 36 | * @param type - Type of this stream, render/in/out. | ||
| 37 | * @param event - Audio render only, a signal used to prevent the renderer running too | ||
| 38 | * fast. | ||
| 39 | * @return A pointer to the created SinkStream | ||
| 40 | */ | ||
| 41 | SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, | ||
| 42 | const std::string& name, StreamType type) override; | ||
| 43 | |||
| 44 | /** | ||
| 45 | * Close a given stream. | ||
| 46 | * | ||
| 47 | * @param stream - The stream to close. | ||
| 48 | */ | ||
| 49 | void CloseStream(const SinkStream* stream) override; | ||
| 50 | |||
| 51 | /** | ||
| 52 | * Close all streams. | ||
| 53 | */ | ||
| 54 | void CloseStreams() override; | ||
| 55 | |||
| 56 | /** | ||
| 57 | * Pause all streams. | ||
| 58 | */ | ||
| 59 | void PauseStreams() override; | ||
| 60 | |||
| 61 | /** | ||
| 62 | * Unpause all streams. | ||
| 63 | */ | ||
| 64 | void UnpauseStreams() override; | ||
| 65 | |||
| 66 | /** | ||
| 67 | * Get the device volume. Set from calls to the IAudioDevice service. | ||
| 68 | * | ||
| 69 | * @return Volume of the device. | ||
| 70 | */ | ||
| 71 | f32 GetDeviceVolume() const override; | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Set the device volume. Set from calls to the IAudioDevice service. | ||
| 75 | * | ||
| 76 | * @param volume - New volume of the device. | ||
| 77 | */ | ||
| 78 | void SetDeviceVolume(f32 volume) override; | ||
| 79 | |||
| 80 | /** | ||
| 81 | * Set the system volume. Comes from the audio system using this stream. | ||
| 82 | * | ||
| 83 | * @param volume - New volume of the system. | ||
| 84 | */ | ||
| 85 | void SetSystemVolume(f32 volume) override; | ||
| 86 | |||
| 87 | private: | ||
| 88 | /// Backend Cubeb context | ||
| 89 | cubeb* ctx{}; | ||
| 90 | /// Cubeb id of the actual hardware output device | ||
| 91 | cubeb_devid output_device{}; | ||
| 92 | /// Cubeb id of the actual hardware input device | ||
| 93 | cubeb_devid input_device{}; | ||
| 94 | /// Vector of streams managed by this sink | ||
| 95 | std::vector<SinkStreamPtr> sink_streams{}; | ||
| 96 | |||
| 97 | #ifdef _WIN32 | ||
| 98 | /// Cubeb required COM to be initialized multi-threaded on Windows | ||
| 99 | u32 com_init_result = 0; | ||
| 100 | #endif | ||
| 101 | }; | ||
| 102 | |||
| 103 | /** | ||
| 104 | * Get a list of conencted devices from Cubeb. | ||
| 105 | * | ||
| 106 | * @param capture - Return input (capture) devices if true, otherwise output devices. | ||
| 107 | */ | ||
| 108 | std::vector<std::string> ListCubebSinkDevices(bool capture); | ||
| 109 | |||
| 110 | } // namespace AudioCore::Sink | ||
diff --git a/src/audio_core/sink/null_sink.h b/src/audio_core/sink/null_sink.h new file mode 100644 index 000000000..47a342171 --- /dev/null +++ b/src/audio_core/sink/null_sink.h | |||
| @@ -0,0 +1,52 @@ | |||
| 1 | // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | #pragma once | ||
| 5 | |||
| 6 | #include "audio_core/sink/sink.h" | ||
| 7 | #include "audio_core/sink/sink_stream.h" | ||
| 8 | |||
| 9 | namespace AudioCore::Sink { | ||
| 10 | /** | ||
| 11 | * A no-op sink for when no audio out is wanted. | ||
| 12 | */ | ||
| 13 | class NullSink final : public Sink { | ||
| 14 | public: | ||
| 15 | explicit NullSink(std::string_view) {} | ||
| 16 | ~NullSink() override = default; | ||
| 17 | |||
| 18 | SinkStream* AcquireSinkStream([[maybe_unused]] Core::System& system, | ||
| 19 | [[maybe_unused]] u32 system_channels, | ||
| 20 | [[maybe_unused]] const std::string& name, | ||
| 21 | [[maybe_unused]] StreamType type) override { | ||
| 22 | return &null_sink_stream; | ||
| 23 | } | ||
| 24 | |||
| 25 | void CloseStream([[maybe_unused]] const SinkStream* stream) override {} | ||
| 26 | void CloseStreams() override {} | ||
| 27 | void PauseStreams() override {} | ||
| 28 | void UnpauseStreams() override {} | ||
| 29 | f32 GetDeviceVolume() const override { | ||
| 30 | return 1.0f; | ||
| 31 | } | ||
| 32 | void SetDeviceVolume(f32 volume) override {} | ||
| 33 | void SetSystemVolume(f32 volume) override {} | ||
| 34 | |||
| 35 | private: | ||
| 36 | struct NullSinkStreamImpl final : SinkStream { | ||
| 37 | void Finalize() override {} | ||
| 38 | void Start(bool resume = false) override {} | ||
| 39 | void Stop() override {} | ||
| 40 | void AppendBuffer([[maybe_unused]] ::AudioCore::Sink::SinkBuffer& buffer, | ||
| 41 | [[maybe_unused]] std::vector<s16>& samples) override {} | ||
| 42 | std::vector<s16> ReleaseBuffer([[maybe_unused]] u64 num_samples) override { | ||
| 43 | return {}; | ||
| 44 | } | ||
| 45 | bool IsBufferConsumed([[maybe_unused]] const u64 tag) { | ||
| 46 | return true; | ||
| 47 | } | ||
| 48 | void ClearQueue() override {} | ||
| 49 | } null_sink_stream; | ||
| 50 | }; | ||
| 51 | |||
| 52 | } // namespace AudioCore::Sink | ||
diff --git a/src/audio_core/sink/sdl2_sink.cpp b/src/audio_core/sink/sdl2_sink.cpp new file mode 100644 index 000000000..d6c9ec90d --- /dev/null +++ b/src/audio_core/sink/sdl2_sink.cpp | |||
| @@ -0,0 +1,556 @@ | |||
| 1 | // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | #include <algorithm> | ||
| 5 | #include <atomic> | ||
| 6 | |||
| 7 | #include "audio_core/audio_core.h" | ||
| 8 | #include "audio_core/audio_event.h" | ||
| 9 | #include "audio_core/audio_manager.h" | ||
| 10 | #include "audio_core/sink/sdl2_sink.h" | ||
| 11 | #include "audio_core/sink/sink_stream.h" | ||
| 12 | #include "common/assert.h" | ||
| 13 | #include "common/fixed_point.h" | ||
| 14 | #include "common/logging/log.h" | ||
| 15 | #include "common/reader_writer_queue.h" | ||
| 16 | #include "common/ring_buffer.h" | ||
| 17 | #include "common/settings.h" | ||
| 18 | #include "core/core.h" | ||
| 19 | |||
| 20 | // Ignore -Wimplicit-fallthrough due to https://github.com/libsdl-org/SDL/issues/4307 | ||
| 21 | #ifdef __clang__ | ||
| 22 | #pragma clang diagnostic push | ||
| 23 | #pragma clang diagnostic ignored "-Wimplicit-fallthrough" | ||
| 24 | #endif | ||
| 25 | #include <SDL.h> | ||
| 26 | #ifdef __clang__ | ||
| 27 | #pragma clang diagnostic pop | ||
| 28 | #endif | ||
| 29 | |||
| 30 | namespace AudioCore::Sink { | ||
| 31 | /** | ||
| 32 | * SDL sink stream, responsible for sinking samples to hardware. | ||
| 33 | */ | ||
| 34 | class SDLSinkStream final : public SinkStream { | ||
| 35 | public: | ||
| 36 | /** | ||
| 37 | * Create a new sink stream. | ||
| 38 | * | ||
| 39 | * @param device_channels_ - Number of channels supported by the hardware. | ||
| 40 | * @param system_channels_ - Number of channels the audio systems expect. | ||
| 41 | * @param output_device - Name of the output device to use for this stream. | ||
| 42 | * @param input_device - Name of the input device to use for this stream. | ||
| 43 | * @param type_ - Type of this stream. | ||
| 44 | * @param system_ - Core system. | ||
| 45 | * @param event - Event used only for audio renderer, signalled on buffer consume. | ||
| 46 | */ | ||
| 47 | SDLSinkStream(u32 device_channels_, const u32 system_channels_, | ||
| 48 | const std::string& output_device, const std::string& input_device, | ||
| 49 | const StreamType type_, Core::System& system_) | ||
| 50 | : type{type_}, system{system_} { | ||
| 51 | system_channels = system_channels_; | ||
| 52 | device_channels = device_channels_; | ||
| 53 | |||
| 54 | SDL_AudioSpec spec; | ||
| 55 | spec.freq = TargetSampleRate; | ||
| 56 | spec.channels = static_cast<u8>(device_channels); | ||
| 57 | spec.format = AUDIO_S16SYS; | ||
| 58 | if (type == StreamType::Render) { | ||
| 59 | spec.samples = TargetSampleCount; | ||
| 60 | } else { | ||
| 61 | spec.samples = 1024; | ||
| 62 | } | ||
| 63 | spec.callback = &SDLSinkStream::DataCallback; | ||
| 64 | spec.userdata = this; | ||
| 65 | |||
| 66 | playing_buffer.consumed = true; | ||
| 67 | |||
| 68 | std::string device_name{output_device}; | ||
| 69 | bool capture{false}; | ||
| 70 | if (type == StreamType::In) { | ||
| 71 | device_name = input_device; | ||
| 72 | capture = true; | ||
| 73 | } | ||
| 74 | |||
| 75 | SDL_AudioSpec obtained; | ||
| 76 | if (device_name.empty()) { | ||
| 77 | device = SDL_OpenAudioDevice(nullptr, capture, &spec, &obtained, false); | ||
| 78 | } else { | ||
| 79 | device = SDL_OpenAudioDevice(device_name.c_str(), capture, &spec, &obtained, false); | ||
| 80 | } | ||
| 81 | |||
| 82 | if (device == 0) { | ||
| 83 | LOG_CRITICAL(Audio_Sink, "Error opening SDL audio device: {}", SDL_GetError()); | ||
| 84 | return; | ||
| 85 | } | ||
| 86 | |||
| 87 | LOG_DEBUG(Service_Audio, | ||
| 88 | "Opening sdl stream {} with: rate {} channels {} (system channels {}) " | ||
| 89 | " samples {}", | ||
| 90 | device, obtained.freq, obtained.channels, system_channels, obtained.samples); | ||
| 91 | } | ||
| 92 | |||
| 93 | /** | ||
| 94 | * Destroy the sink stream. | ||
| 95 | */ | ||
| 96 | ~SDLSinkStream() override { | ||
| 97 | if (device == 0) { | ||
| 98 | return; | ||
| 99 | } | ||
| 100 | |||
| 101 | SDL_CloseAudioDevice(device); | ||
| 102 | } | ||
| 103 | |||
| 104 | /** | ||
| 105 | * Finalize the sink stream. | ||
| 106 | */ | ||
| 107 | void Finalize() override { | ||
| 108 | if (device == 0) { | ||
| 109 | return; | ||
| 110 | } | ||
| 111 | |||
| 112 | SDL_CloseAudioDevice(device); | ||
| 113 | } | ||
| 114 | |||
| 115 | /** | ||
| 116 | * Start the sink stream. | ||
| 117 | * | ||
| 118 | * @param resume - Set to true if this is resuming the stream a previously-active stream. | ||
| 119 | * Default false. | ||
| 120 | */ | ||
| 121 | void Start(const bool resume = false) override { | ||
| 122 | if (device == 0) { | ||
| 123 | return; | ||
| 124 | } | ||
| 125 | |||
| 126 | if (resume && was_playing) { | ||
| 127 | SDL_PauseAudioDevice(device, 0); | ||
| 128 | paused = false; | ||
| 129 | } else if (!resume) { | ||
| 130 | SDL_PauseAudioDevice(device, 0); | ||
| 131 | paused = false; | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | /** | ||
| 136 | * Stop the sink stream. | ||
| 137 | */ | ||
| 138 | void Stop() { | ||
| 139 | if (device == 0) { | ||
| 140 | return; | ||
| 141 | } | ||
| 142 | SDL_PauseAudioDevice(device, 1); | ||
| 143 | paused = true; | ||
| 144 | } | ||
| 145 | |||
| 146 | /** | ||
| 147 | * Append a new buffer and its samples to a waiting queue to play. | ||
| 148 | * | ||
| 149 | * @param buffer - Audio buffer information to be queued. | ||
| 150 | * @param samples - The s16 samples to be queue for playback. | ||
| 151 | */ | ||
| 152 | void AppendBuffer(::AudioCore::Sink::SinkBuffer& buffer, std::vector<s16>& samples) override { | ||
| 153 | if (type == StreamType::In) { | ||
| 154 | queue.enqueue(buffer); | ||
| 155 | queued_buffers++; | ||
| 156 | } else { | ||
| 157 | constexpr s32 min = std::numeric_limits<s16>::min(); | ||
| 158 | constexpr s32 max = std::numeric_limits<s16>::max(); | ||
| 159 | |||
| 160 | auto yuzu_volume{Settings::Volume()}; | ||
| 161 | auto volume{system_volume * device_volume * yuzu_volume}; | ||
| 162 | |||
| 163 | if (system_channels == 6 && device_channels == 2) { | ||
| 164 | // We're given 6 channels, but our device only outputs 2, so downmix. | ||
| 165 | constexpr std::array<f32, 4> down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f}; | ||
| 166 | |||
| 167 | for (u32 read_index = 0, write_index = 0; read_index < samples.size(); | ||
| 168 | read_index += system_channels, write_index += device_channels) { | ||
| 169 | const auto left_sample{ | ||
| 170 | ((Common::FixedPoint<49, 15>( | ||
| 171 | samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * | ||
| 172 | down_mix_coeff[0] + | ||
| 173 | samples[read_index + static_cast<u32>(Channels::Center)] * | ||
| 174 | down_mix_coeff[1] + | ||
| 175 | samples[read_index + static_cast<u32>(Channels::LFE)] * | ||
| 176 | down_mix_coeff[2] + | ||
| 177 | samples[read_index + static_cast<u32>(Channels::BackLeft)] * | ||
| 178 | down_mix_coeff[3]) * | ||
| 179 | volume) | ||
| 180 | .to_int()}; | ||
| 181 | |||
| 182 | const auto right_sample{ | ||
| 183 | ((Common::FixedPoint<49, 15>( | ||
| 184 | samples[read_index + static_cast<u32>(Channels::FrontRight)]) * | ||
| 185 | down_mix_coeff[0] + | ||
| 186 | samples[read_index + static_cast<u32>(Channels::Center)] * | ||
| 187 | down_mix_coeff[1] + | ||
| 188 | samples[read_index + static_cast<u32>(Channels::LFE)] * | ||
| 189 | down_mix_coeff[2] + | ||
| 190 | samples[read_index + static_cast<u32>(Channels::BackRight)] * | ||
| 191 | down_mix_coeff[3]) * | ||
| 192 | volume) | ||
| 193 | .to_int()}; | ||
| 194 | |||
| 195 | samples[write_index + static_cast<u32>(Channels::FrontLeft)] = | ||
| 196 | static_cast<s16>(std::clamp(left_sample, min, max)); | ||
| 197 | samples[write_index + static_cast<u32>(Channels::FrontRight)] = | ||
| 198 | static_cast<s16>(std::clamp(right_sample, min, max)); | ||
| 199 | } | ||
| 200 | |||
| 201 | samples.resize(samples.size() / system_channels * device_channels); | ||
| 202 | |||
| 203 | } else if (system_channels == 2 && device_channels == 6) { | ||
| 204 | // We need moar samples! Not all games will provide 6 channel audio. | ||
| 205 | // TODO: Implement some upmixing here. Currently just passthrough, with other | ||
| 206 | // channels left as silence. | ||
| 207 | std::vector<s16> new_samples(samples.size() / system_channels * device_channels, 0); | ||
| 208 | |||
| 209 | for (u32 read_index = 0, write_index = 0; read_index < samples.size(); | ||
| 210 | read_index += system_channels, write_index += device_channels) { | ||
| 211 | const auto left_sample{static_cast<s16>(std::clamp( | ||
| 212 | static_cast<s32>( | ||
| 213 | static_cast<f32>( | ||
| 214 | samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * | ||
| 215 | volume), | ||
| 216 | min, max))}; | ||
| 217 | |||
| 218 | new_samples[write_index + static_cast<u32>(Channels::FrontLeft)] = left_sample; | ||
| 219 | |||
| 220 | const auto right_sample{static_cast<s16>(std::clamp( | ||
| 221 | static_cast<s32>( | ||
| 222 | static_cast<f32>( | ||
| 223 | samples[read_index + static_cast<u32>(Channels::FrontRight)]) * | ||
| 224 | volume), | ||
| 225 | min, max))}; | ||
| 226 | |||
| 227 | new_samples[write_index + static_cast<u32>(Channels::FrontRight)] = | ||
| 228 | right_sample; | ||
| 229 | } | ||
| 230 | samples = std::move(new_samples); | ||
| 231 | |||
| 232 | } else if (volume != 1.0f) { | ||
| 233 | for (u32 i = 0; i < samples.size(); i++) { | ||
| 234 | samples[i] = static_cast<s16>(std::clamp( | ||
| 235 | static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); | ||
| 236 | } | ||
| 237 | } | ||
| 238 | |||
| 239 | samples_buffer.Push(samples); | ||
| 240 | queue.enqueue(buffer); | ||
| 241 | queued_buffers++; | ||
| 242 | } | ||
| 243 | } | ||
| 244 | |||
| 245 | /** | ||
| 246 | * Release a buffer. Audio In only, will fill a buffer with recorded samples. | ||
| 247 | * | ||
| 248 | * @param num_samples - Maximum number of samples to receive. | ||
| 249 | * @return Vector of recorded samples. May have fewer than num_samples. | ||
| 250 | */ | ||
| 251 | std::vector<s16> ReleaseBuffer(const u64 num_samples) override { | ||
| 252 | static constexpr s32 min = std::numeric_limits<s16>::min(); | ||
| 253 | static constexpr s32 max = std::numeric_limits<s16>::max(); | ||
| 254 | |||
| 255 | auto samples{samples_buffer.Pop(num_samples)}; | ||
| 256 | |||
| 257 | // TODO: Up-mix to 6 channels if the game expects it. | ||
| 258 | // For audio input this is unlikely to ever be the case though. | ||
| 259 | |||
| 260 | // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here. | ||
| 261 | // TODO: Play with this and find something that works better. | ||
| 262 | auto volume{system_volume * device_volume * 8}; | ||
| 263 | for (u32 i = 0; i < samples.size(); i++) { | ||
| 264 | samples[i] = static_cast<s16>( | ||
| 265 | std::clamp(static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); | ||
| 266 | } | ||
| 267 | |||
| 268 | if (samples.size() < num_samples) { | ||
| 269 | samples.resize(num_samples, 0); | ||
| 270 | } | ||
| 271 | return samples; | ||
| 272 | } | ||
| 273 | |||
| 274 | /** | ||
| 275 | * Check if a certain buffer has been consumed (fully played). | ||
| 276 | * | ||
| 277 | * @param tag - Unique tag of a buffer to check for. | ||
| 278 | * @return True if the buffer has been played, otherwise false. | ||
| 279 | */ | ||
| 280 | bool IsBufferConsumed(const u64 tag) override { | ||
| 281 | if (released_buffer.tag == 0) { | ||
| 282 | if (!released_buffers.try_dequeue(released_buffer)) { | ||
| 283 | return false; | ||
| 284 | } | ||
| 285 | } | ||
| 286 | |||
| 287 | if (released_buffer.tag == tag) { | ||
| 288 | released_buffer.tag = 0; | ||
| 289 | return true; | ||
| 290 | } | ||
| 291 | return false; | ||
| 292 | } | ||
| 293 | |||
| 294 | /** | ||
| 295 | * Empty out the buffer queue. | ||
| 296 | */ | ||
| 297 | void ClearQueue() override { | ||
| 298 | samples_buffer.Pop(); | ||
| 299 | while (queue.pop()) { | ||
| 300 | } | ||
| 301 | while (released_buffers.pop()) { | ||
| 302 | } | ||
| 303 | released_buffer = {}; | ||
| 304 | playing_buffer = {}; | ||
| 305 | playing_buffer.consumed = true; | ||
| 306 | queued_buffers = 0; | ||
| 307 | } | ||
| 308 | |||
| 309 | private: | ||
| 310 | /** | ||
| 311 | * Signal events back to the audio system that a buffer was played/can be filled. | ||
| 312 | * | ||
| 313 | * @param buffer - Consumed audio buffer to be released. | ||
| 314 | */ | ||
| 315 | void SignalEvent(const ::AudioCore::Sink::SinkBuffer& buffer) { | ||
| 316 | auto& manager{system.AudioCore().GetAudioManager()}; | ||
| 317 | switch (type) { | ||
| 318 | case StreamType::Out: | ||
| 319 | released_buffers.enqueue(buffer); | ||
| 320 | manager.SetEvent(Event::Type::AudioOutManager, true); | ||
| 321 | break; | ||
| 322 | case StreamType::In: | ||
| 323 | released_buffers.enqueue(buffer); | ||
| 324 | manager.SetEvent(Event::Type::AudioInManager, true); | ||
| 325 | break; | ||
| 326 | case StreamType::Render: | ||
| 327 | break; | ||
| 328 | } | ||
| 329 | } | ||
| 330 | |||
| 331 | /** | ||
| 332 | * Main callback from SDL. Either expects samples from us (audio render/audio out), or will | ||
| 333 | * provide samples to be copied (audio in). | ||
| 334 | * | ||
| 335 | * @param userdata - Custom data pointer passed along, points to a SDLSinkStream. | ||
| 336 | * @param stream - Buffer of samples to be filled or read. | ||
| 337 | * @param len - Length of the stream in bytes. | ||
| 338 | */ | ||
| 339 | static void DataCallback(void* userdata, Uint8* stream, int len) { | ||
| 340 | auto* impl = static_cast<SDLSinkStream*>(userdata); | ||
| 341 | |||
| 342 | if (!impl) { | ||
| 343 | return; | ||
| 344 | } | ||
| 345 | |||
| 346 | const std::size_t num_channels = impl->GetDeviceChannels(); | ||
| 347 | const std::size_t frame_size = num_channels; | ||
| 348 | const std::size_t frame_size_bytes = frame_size * sizeof(s16); | ||
| 349 | const std::size_t num_frames{len / num_channels / sizeof(s16)}; | ||
| 350 | size_t frames_written{0}; | ||
| 351 | [[maybe_unused]] bool underrun{false}; | ||
| 352 | |||
| 353 | if (impl->type == StreamType::In) { | ||
| 354 | std::span<s16> input_buffer{reinterpret_cast<s16*>(stream), num_frames * frame_size}; | ||
| 355 | |||
| 356 | while (frames_written < num_frames) { | ||
| 357 | auto& playing_buffer{impl->playing_buffer}; | ||
| 358 | |||
| 359 | // If the playing buffer has been consumed or has no frames, we need a new one | ||
| 360 | if (playing_buffer.consumed || playing_buffer.frames == 0) { | ||
| 361 | if (!impl->queue.try_dequeue(impl->playing_buffer)) { | ||
| 362 | // If no buffer was available we've underrun, just push the samples and | ||
| 363 | // continue. | ||
| 364 | underrun = true; | ||
| 365 | impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], | ||
| 366 | (num_frames - frames_written) * frame_size); | ||
| 367 | frames_written = num_frames; | ||
| 368 | continue; | ||
| 369 | } else { | ||
| 370 | impl->queued_buffers--; | ||
| 371 | impl->SignalEvent(impl->playing_buffer); | ||
| 372 | } | ||
| 373 | } | ||
| 374 | |||
| 375 | // Get the minimum frames available between the currently playing buffer, and the | ||
| 376 | // amount we have left to fill | ||
| 377 | size_t frames_available{ | ||
| 378 | std::min(playing_buffer.frames - playing_buffer.frames_played, | ||
| 379 | num_frames - frames_written)}; | ||
| 380 | |||
| 381 | impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], | ||
| 382 | frames_available * frame_size); | ||
| 383 | |||
| 384 | frames_written += frames_available; | ||
| 385 | playing_buffer.frames_played += frames_available; | ||
| 386 | |||
| 387 | // If that's all the frames in the current buffer, add its samples and mark it as | ||
| 388 | // consumed | ||
| 389 | if (playing_buffer.frames_played >= playing_buffer.frames) { | ||
| 390 | impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); | ||
| 391 | impl->playing_buffer.consumed = true; | ||
| 392 | } | ||
| 393 | } | ||
| 394 | |||
| 395 | std::memcpy(&impl->last_frame[0], &input_buffer[(frames_written - 1) * frame_size], | ||
| 396 | frame_size_bytes); | ||
| 397 | } else { | ||
| 398 | std::span<s16> output_buffer{reinterpret_cast<s16*>(stream), num_frames * frame_size}; | ||
| 399 | |||
| 400 | while (frames_written < num_frames) { | ||
| 401 | auto& playing_buffer{impl->playing_buffer}; | ||
| 402 | |||
| 403 | // If the playing buffer has been consumed or has no frames, we need a new one | ||
| 404 | if (playing_buffer.consumed || playing_buffer.frames == 0) { | ||
| 405 | if (!impl->queue.try_dequeue(impl->playing_buffer)) { | ||
| 406 | // If no buffer was available we've underrun, fill the remaining buffer with | ||
| 407 | // the last written frame and continue. | ||
| 408 | underrun = true; | ||
| 409 | for (size_t i = frames_written; i < num_frames; i++) { | ||
| 410 | std::memcpy(&output_buffer[i * frame_size], &impl->last_frame[0], | ||
| 411 | frame_size_bytes); | ||
| 412 | } | ||
| 413 | frames_written = num_frames; | ||
| 414 | continue; | ||
| 415 | } else { | ||
| 416 | impl->queued_buffers--; | ||
| 417 | impl->SignalEvent(impl->playing_buffer); | ||
| 418 | } | ||
| 419 | } | ||
| 420 | |||
| 421 | // Get the minimum frames available between the currently playing buffer, and the | ||
| 422 | // amount we have left to fill | ||
| 423 | size_t frames_available{ | ||
| 424 | std::min(playing_buffer.frames - playing_buffer.frames_played, | ||
| 425 | num_frames - frames_written)}; | ||
| 426 | |||
| 427 | impl->samples_buffer.Pop(&output_buffer[frames_written * frame_size], | ||
| 428 | frames_available * frame_size); | ||
| 429 | |||
| 430 | frames_written += frames_available; | ||
| 431 | playing_buffer.frames_played += frames_available; | ||
| 432 | |||
| 433 | // If that's all the frames in the current buffer, add its samples and mark it as | ||
| 434 | // consumed | ||
| 435 | if (playing_buffer.frames_played >= playing_buffer.frames) { | ||
| 436 | impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); | ||
| 437 | impl->playing_buffer.consumed = true; | ||
| 438 | } | ||
| 439 | } | ||
| 440 | |||
| 441 | std::memcpy(&impl->last_frame[0], &output_buffer[(frames_written - 1) * frame_size], | ||
| 442 | frame_size_bytes); | ||
| 443 | } | ||
| 444 | } | ||
| 445 | |||
| 446 | /// SDL device id of the opened input/output device | ||
| 447 | SDL_AudioDeviceID device{}; | ||
| 448 | /// Type of this stream | ||
| 449 | StreamType type; | ||
| 450 | /// Core system | ||
| 451 | Core::System& system; | ||
| 452 | /// Ring buffer of the samples waiting to be played or consumed | ||
| 453 | Common::RingBuffer<s16, 0x10000> samples_buffer; | ||
| 454 | /// Audio buffers queued and waiting to play | ||
| 455 | Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> queue; | ||
| 456 | /// The currently-playing audio buffer | ||
| 457 | ::AudioCore::Sink::SinkBuffer playing_buffer{}; | ||
| 458 | /// Audio buffers which have been played and are in queue to be released by the audio system | ||
| 459 | Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> released_buffers{}; | ||
| 460 | /// Currently released buffer waiting to be taken by the audio system | ||
| 461 | ::AudioCore::Sink::SinkBuffer released_buffer{}; | ||
| 462 | /// The last played (or received) frame of audio, used when the callback underruns | ||
| 463 | std::array<s16, MaxChannels> last_frame{}; | ||
| 464 | }; | ||
| 465 | |||
| 466 | SDLSink::SDLSink(std::string_view target_device_name) { | ||
| 467 | if (!SDL_WasInit(SDL_INIT_AUDIO)) { | ||
| 468 | if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { | ||
| 469 | LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError()); | ||
| 470 | return; | ||
| 471 | } | ||
| 472 | } | ||
| 473 | |||
| 474 | if (target_device_name != auto_device_name && !target_device_name.empty()) { | ||
| 475 | output_device = target_device_name; | ||
| 476 | } else { | ||
| 477 | output_device.clear(); | ||
| 478 | } | ||
| 479 | |||
| 480 | device_channels = 2; | ||
| 481 | } | ||
| 482 | |||
| 483 | SDLSink::~SDLSink() = default; | ||
| 484 | |||
| 485 | SinkStream* SDLSink::AcquireSinkStream(Core::System& system, const u32 system_channels, | ||
| 486 | const std::string&, const StreamType type) { | ||
| 487 | SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<SDLSinkStream>( | ||
| 488 | device_channels, system_channels, output_device, input_device, type, system)); | ||
| 489 | return stream.get(); | ||
| 490 | } | ||
| 491 | |||
| 492 | void SDLSink::CloseStream(const SinkStream* stream) { | ||
| 493 | for (size_t i = 0; i < sink_streams.size(); i++) { | ||
| 494 | if (sink_streams[i].get() == stream) { | ||
| 495 | sink_streams[i].reset(); | ||
| 496 | sink_streams.erase(sink_streams.begin() + i); | ||
| 497 | break; | ||
| 498 | } | ||
| 499 | } | ||
| 500 | } | ||
| 501 | |||
| 502 | void SDLSink::CloseStreams() { | ||
| 503 | sink_streams.clear(); | ||
| 504 | } | ||
| 505 | |||
| 506 | void SDLSink::PauseStreams() { | ||
| 507 | for (auto& stream : sink_streams) { | ||
| 508 | stream->Stop(); | ||
| 509 | } | ||
| 510 | } | ||
| 511 | |||
| 512 | void SDLSink::UnpauseStreams() { | ||
| 513 | for (auto& stream : sink_streams) { | ||
| 514 | stream->Start(); | ||
| 515 | } | ||
| 516 | } | ||
| 517 | |||
| 518 | f32 SDLSink::GetDeviceVolume() const { | ||
| 519 | if (sink_streams.empty()) { | ||
| 520 | return 1.0f; | ||
| 521 | } | ||
| 522 | |||
| 523 | return sink_streams[0]->GetDeviceVolume(); | ||
| 524 | } | ||
| 525 | |||
| 526 | void SDLSink::SetDeviceVolume(const f32 volume) { | ||
| 527 | for (auto& stream : sink_streams) { | ||
| 528 | stream->SetDeviceVolume(volume); | ||
| 529 | } | ||
| 530 | } | ||
| 531 | |||
| 532 | void SDLSink::SetSystemVolume(const f32 volume) { | ||
| 533 | for (auto& stream : sink_streams) { | ||
| 534 | stream->SetSystemVolume(volume); | ||
| 535 | } | ||
| 536 | } | ||
| 537 | |||
| 538 | std::vector<std::string> ListSDLSinkDevices(const bool capture) { | ||
| 539 | std::vector<std::string> device_list; | ||
| 540 | |||
| 541 | if (!SDL_WasInit(SDL_INIT_AUDIO)) { | ||
| 542 | if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { | ||
| 543 | LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError()); | ||
| 544 | return {}; | ||
| 545 | } | ||
| 546 | } | ||
| 547 | |||
| 548 | const int device_count = SDL_GetNumAudioDevices(capture); | ||
| 549 | for (int i = 0; i < device_count; ++i) { | ||
| 550 | device_list.emplace_back(SDL_GetAudioDeviceName(i, 0)); | ||
| 551 | } | ||
| 552 | |||
| 553 | return device_list; | ||
| 554 | } | ||
| 555 | |||
| 556 | } // namespace AudioCore::Sink | ||
diff --git a/src/audio_core/sink/sdl2_sink.h b/src/audio_core/sink/sdl2_sink.h new file mode 100644 index 000000000..186bc2fa3 --- /dev/null +++ b/src/audio_core/sink/sdl2_sink.h | |||
| @@ -0,0 +1,101 @@ | |||
| 1 | // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | #pragma once | ||
| 5 | |||
| 6 | #include <string> | ||
| 7 | #include <vector> | ||
| 8 | |||
| 9 | #include "audio_core/sink/sink.h" | ||
| 10 | |||
| 11 | namespace Core { | ||
| 12 | class System; | ||
| 13 | } | ||
| 14 | |||
| 15 | namespace AudioCore::Sink { | ||
| 16 | class SinkStream; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * SDL backend sink, holds multiple output streams and is responsible for sinking samples to | ||
| 20 | * hardware. Used by Audio Render, Audio In and Audio Out. | ||
| 21 | */ | ||
| 22 | class SDLSink final : public Sink { | ||
| 23 | public: | ||
| 24 | explicit SDLSink(std::string_view device_id); | ||
| 25 | ~SDLSink() override; | ||
| 26 | |||
| 27 | /** | ||
| 28 | * Create a new sink stream. | ||
| 29 | * | ||
| 30 | * @param system - Core system. | ||
| 31 | * @param system_channels - Number of channels the audio system expects. | ||
| 32 | * May differ from the device's channel count. | ||
| 33 | * @param name - Name of this stream. | ||
| 34 | * @param type - Type of this stream, render/in/out. | ||
| 35 | * @param event - Audio render only, a signal used to prevent the renderer running too | ||
| 36 | * fast. | ||
| 37 | * @return A pointer to the created SinkStream | ||
| 38 | */ | ||
| 39 | SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, | ||
| 40 | const std::string& name, StreamType type) override; | ||
| 41 | |||
| 42 | /** | ||
| 43 | * Close a given stream. | ||
| 44 | * | ||
| 45 | * @param stream - The stream to close. | ||
| 46 | */ | ||
| 47 | void CloseStream(const SinkStream* stream) override; | ||
| 48 | |||
| 49 | /** | ||
| 50 | * Close all streams. | ||
| 51 | */ | ||
| 52 | void CloseStreams() override; | ||
| 53 | |||
| 54 | /** | ||
| 55 | * Pause all streams. | ||
| 56 | */ | ||
| 57 | void PauseStreams() override; | ||
| 58 | |||
| 59 | /** | ||
| 60 | * Unpause all streams. | ||
| 61 | */ | ||
| 62 | void UnpauseStreams() override; | ||
| 63 | |||
| 64 | /** | ||
| 65 | * Get the device volume. Set from calls to the IAudioDevice service. | ||
| 66 | * | ||
| 67 | * @return Volume of the device. | ||
| 68 | */ | ||
| 69 | f32 GetDeviceVolume() const override; | ||
| 70 | |||
| 71 | /** | ||
| 72 | * Set the device volume. Set from calls to the IAudioDevice service. | ||
| 73 | * | ||
| 74 | * @param volume - New volume of the device. | ||
| 75 | */ | ||
| 76 | void SetDeviceVolume(f32 volume) override; | ||
| 77 | |||
| 78 | /** | ||
| 79 | * Set the system volume. Comes from the audio system using this stream. | ||
| 80 | * | ||
| 81 | * @param volume - New volume of the system. | ||
| 82 | */ | ||
| 83 | void SetSystemVolume(f32 volume) override; | ||
| 84 | |||
| 85 | private: | ||
| 86 | /// Name of the output device used by streams | ||
| 87 | std::string output_device; | ||
| 88 | /// Name of the input device used by streams | ||
| 89 | std::string input_device; | ||
| 90 | /// Vector of streams managed by this sink | ||
| 91 | std::vector<SinkStreamPtr> sink_streams; | ||
| 92 | }; | ||
| 93 | |||
| 94 | /** | ||
| 95 | * Get a list of conencted devices from Cubeb. | ||
| 96 | * | ||
| 97 | * @param capture - Return input (capture) devices if true, otherwise output devices. | ||
| 98 | */ | ||
| 99 | std::vector<std::string> ListSDLSinkDevices(bool capture); | ||
| 100 | |||
| 101 | } // namespace AudioCore::Sink | ||
diff --git a/src/audio_core/sink/sink.h b/src/audio_core/sink/sink.h new file mode 100644 index 000000000..91fe455e4 --- /dev/null +++ b/src/audio_core/sink/sink.h | |||
| @@ -0,0 +1,106 @@ | |||
| 1 | // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | #pragma once | ||
| 5 | |||
| 6 | #include <memory> | ||
| 7 | #include <string> | ||
| 8 | |||
| 9 | #include "audio_core/sink/sink_stream.h" | ||
| 10 | #include "common/common_types.h" | ||
| 11 | |||
| 12 | namespace Common { | ||
| 13 | class Event; | ||
| 14 | } | ||
| 15 | namespace Core { | ||
| 16 | class System; | ||
| 17 | } | ||
| 18 | |||
| 19 | namespace AudioCore::Sink { | ||
| 20 | |||
| 21 | constexpr char auto_device_name[] = "auto"; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * This class is an interface for an audio sink, holds multiple output streams and is responsible | ||
| 25 | * for sinking samples to hardware. Used by Audio Render, Audio In and Audio Out. | ||
| 26 | */ | ||
| 27 | class Sink { | ||
| 28 | public: | ||
| 29 | virtual ~Sink() = default; | ||
| 30 | /** | ||
| 31 | * Close a given stream. | ||
| 32 | * | ||
| 33 | * @param stream - The stream to close. | ||
| 34 | */ | ||
| 35 | virtual void CloseStream(const SinkStream* stream) = 0; | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Close all streams. | ||
| 39 | */ | ||
| 40 | virtual void CloseStreams() = 0; | ||
| 41 | |||
| 42 | /** | ||
| 43 | * Pause all streams. | ||
| 44 | */ | ||
| 45 | virtual void PauseStreams() = 0; | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Unpause all streams. | ||
| 49 | */ | ||
| 50 | virtual void UnpauseStreams() = 0; | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Create a new sink stream, kept within this sink, with a pointer returned for use. | ||
| 54 | * Do not free the returned pointer. When done with the stream, call CloseStream on the sink. | ||
| 55 | * | ||
| 56 | * @param system - Core system. | ||
| 57 | * @param system_channels - Number of channels the audio system expects. | ||
| 58 | * May differ from the device's channel count. | ||
| 59 | * @param name - Name of this stream. | ||
| 60 | * @param type - Type of this stream, render/in/out. | ||
| 61 | * @param event - Audio render only, a signal used to prevent the renderer running too | ||
| 62 | * fast. | ||
| 63 | * @return A pointer to the created SinkStream | ||
| 64 | */ | ||
| 65 | virtual SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, | ||
| 66 | const std::string& name, StreamType type) = 0; | ||
| 67 | |||
| 68 | /** | ||
| 69 | * Get the number of channels the hardware device supports. | ||
| 70 | * Either 2 or 6. | ||
| 71 | * | ||
| 72 | * @return Number of device channels. | ||
| 73 | */ | ||
| 74 | u32 GetDeviceChannels() const { | ||
| 75 | return device_channels; | ||
| 76 | } | ||
| 77 | |||
| 78 | /** | ||
| 79 | * Get the device volume. Set from calls to the IAudioDevice service. | ||
| 80 | * | ||
| 81 | * @return Volume of the device. | ||
| 82 | */ | ||
| 83 | virtual f32 GetDeviceVolume() const = 0; | ||
| 84 | |||
| 85 | /** | ||
| 86 | * Set the device volume. Set from calls to the IAudioDevice service. | ||
| 87 | * | ||
| 88 | * @param volume - New volume of the device. | ||
| 89 | */ | ||
| 90 | virtual void SetDeviceVolume(f32 volume) = 0; | ||
| 91 | |||
| 92 | /** | ||
| 93 | * Set the system volume. Comes from the audio system using this stream. | ||
| 94 | * | ||
| 95 | * @param volume - New volume of the system. | ||
| 96 | */ | ||
| 97 | virtual void SetSystemVolume(f32 volume) = 0; | ||
| 98 | |||
| 99 | protected: | ||
| 100 | /// Number of device channels supported by the hardware | ||
| 101 | u32 device_channels{2}; | ||
| 102 | }; | ||
| 103 | |||
| 104 | using SinkPtr = std::unique_ptr<Sink>; | ||
| 105 | |||
| 106 | } // namespace AudioCore::Sink | ||
diff --git a/src/audio_core/sink/sink_details.cpp b/src/audio_core/sink/sink_details.cpp new file mode 100644 index 000000000..253c0fd1e --- /dev/null +++ b/src/audio_core/sink/sink_details.cpp | |||
| @@ -0,0 +1,91 @@ | |||
| 1 | // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | #include <algorithm> | ||
| 5 | #include <memory> | ||
| 6 | #include <string> | ||
| 7 | #include <vector> | ||
| 8 | #include "audio_core/sink/null_sink.h" | ||
| 9 | #include "audio_core/sink/sink_details.h" | ||
| 10 | #ifdef HAVE_CUBEB | ||
| 11 | #include "audio_core/sink/cubeb_sink.h" | ||
| 12 | #endif | ||
| 13 | #ifdef HAVE_SDL2 | ||
| 14 | #include "audio_core/sink/sdl2_sink.h" | ||
| 15 | #endif | ||
| 16 | #include "common/logging/log.h" | ||
| 17 | |||
| 18 | namespace AudioCore::Sink { | ||
| 19 | namespace { | ||
| 20 | struct SinkDetails { | ||
| 21 | using FactoryFn = std::unique_ptr<Sink> (*)(std::string_view); | ||
| 22 | using ListDevicesFn = std::vector<std::string> (*)(bool); | ||
| 23 | |||
| 24 | /// Name for this sink. | ||
| 25 | const char* id; | ||
| 26 | /// A method to call to construct an instance of this type of sink. | ||
| 27 | FactoryFn factory; | ||
| 28 | /// A method to call to list available devices. | ||
| 29 | ListDevicesFn list_devices; | ||
| 30 | }; | ||
| 31 | |||
| 32 | // sink_details is ordered in terms of desirability, with the best choice at the top. | ||
| 33 | constexpr SinkDetails sink_details[] = { | ||
| 34 | #ifdef HAVE_CUBEB | ||
| 35 | SinkDetails{"cubeb", | ||
| 36 | [](std::string_view device_id) -> std::unique_ptr<Sink> { | ||
| 37 | return std::make_unique<CubebSink>(device_id); | ||
| 38 | }, | ||
| 39 | &ListCubebSinkDevices}, | ||
| 40 | #endif | ||
| 41 | #ifdef HAVE_SDL2 | ||
| 42 | SinkDetails{"sdl2", | ||
| 43 | [](std::string_view device_id) -> std::unique_ptr<Sink> { | ||
| 44 | return std::make_unique<SDLSink>(device_id); | ||
| 45 | }, | ||
| 46 | &ListSDLSinkDevices}, | ||
| 47 | #endif | ||
| 48 | SinkDetails{"null", | ||
| 49 | [](std::string_view device_id) -> std::unique_ptr<Sink> { | ||
| 50 | return std::make_unique<NullSink>(device_id); | ||
| 51 | }, | ||
| 52 | [](bool capture) { return std::vector<std::string>{"null"}; }}, | ||
| 53 | }; | ||
| 54 | |||
| 55 | const SinkDetails& GetOutputSinkDetails(std::string_view sink_id) { | ||
| 56 | auto iter = | ||
| 57 | std::find_if(std::begin(sink_details), std::end(sink_details), | ||
| 58 | [sink_id](const auto& sink_detail) { return sink_detail.id == sink_id; }); | ||
| 59 | |||
| 60 | if (sink_id == "auto" || iter == std::end(sink_details)) { | ||
| 61 | if (sink_id != "auto") { | ||
| 62 | LOG_ERROR(Audio, "AudioCore::Sink::GetOutputSinkDetails given invalid sink_id {}", | ||
| 63 | sink_id); | ||
| 64 | } | ||
| 65 | // Auto-select. | ||
| 66 | // sink_details is ordered in terms of desirability, with the best choice at the front. | ||
| 67 | iter = std::begin(sink_details); | ||
| 68 | } | ||
| 69 | |||
| 70 | return *iter; | ||
| 71 | } | ||
| 72 | } // Anonymous namespace | ||
| 73 | |||
| 74 | std::vector<const char*> GetSinkIDs() { | ||
| 75 | std::vector<const char*> sink_ids(std::size(sink_details)); | ||
| 76 | |||
| 77 | std::transform(std::begin(sink_details), std::end(sink_details), std::begin(sink_ids), | ||
| 78 | [](const auto& sink) { return sink.id; }); | ||
| 79 | |||
| 80 | return sink_ids; | ||
| 81 | } | ||
| 82 | |||
| 83 | std::vector<std::string> GetDeviceListForSink(std::string_view sink_id, bool capture) { | ||
| 84 | return GetOutputSinkDetails(sink_id).list_devices(capture); | ||
| 85 | } | ||
| 86 | |||
| 87 | std::unique_ptr<Sink> CreateSinkFromID(std::string_view sink_id, std::string_view device_id) { | ||
| 88 | return GetOutputSinkDetails(sink_id).factory(device_id); | ||
| 89 | } | ||
| 90 | |||
| 91 | } // namespace AudioCore::Sink | ||
diff --git a/src/audio_core/sink/sink_details.h b/src/audio_core/sink/sink_details.h new file mode 100644 index 000000000..3ebdb1e30 --- /dev/null +++ b/src/audio_core/sink/sink_details.h | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | #pragma once | ||
| 5 | |||
| 6 | #include <string> | ||
| 7 | #include <string_view> | ||
| 8 | #include <vector> | ||
| 9 | |||
| 10 | namespace AudioCore { | ||
| 11 | class AudioManager; | ||
| 12 | |||
| 13 | namespace Sink { | ||
| 14 | |||
| 15 | class Sink; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Retrieves the IDs for all available audio sinks. | ||
| 19 | * | ||
| 20 | * @return Vector of available sink names. | ||
| 21 | */ | ||
| 22 | std::vector<const char*> GetSinkIDs(); | ||
| 23 | |||
| 24 | /** | ||
| 25 | * Gets the list of devices for a particular sink identified by the given ID. | ||
| 26 | * | ||
| 27 | * @param sink_id - Id of the sink to get devices from. | ||
| 28 | * @param capture - Get capture (input) devices, or output devices? | ||
| 29 | * @return Vector of device names. | ||
| 30 | */ | ||
| 31 | std::vector<std::string> GetDeviceListForSink(std::string_view sink_id, bool capture); | ||
| 32 | |||
| 33 | /** | ||
| 34 | * Creates an audio sink identified by the given device ID. | ||
| 35 | * | ||
| 36 | * @param sink_id - Id of the sink to create. | ||
| 37 | * @param device_id - Name of the device to create. | ||
| 38 | * @return Pointer to the created sink. | ||
| 39 | */ | ||
| 40 | std::unique_ptr<Sink> CreateSinkFromID(std::string_view sink_id, std::string_view device_id); | ||
| 41 | |||
| 42 | } // namespace Sink | ||
| 43 | } // namespace AudioCore | ||
diff --git a/src/audio_core/sink/sink_stream.h b/src/audio_core/sink/sink_stream.h new file mode 100644 index 000000000..17ed6593f --- /dev/null +++ b/src/audio_core/sink/sink_stream.h | |||
| @@ -0,0 +1,224 @@ | |||
| 1 | // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | #pragma once | ||
| 5 | |||
| 6 | #include <atomic> | ||
| 7 | #include <memory> | ||
| 8 | #include <vector> | ||
| 9 | |||
| 10 | #include "audio_core/common/common.h" | ||
| 11 | #include "common/common_types.h" | ||
| 12 | |||
| 13 | namespace AudioCore::Sink { | ||
| 14 | |||
| 15 | enum class StreamType { | ||
| 16 | Render, | ||
| 17 | Out, | ||
| 18 | In, | ||
| 19 | }; | ||
| 20 | |||
| 21 | struct SinkBuffer { | ||
| 22 | u64 frames; | ||
| 23 | u64 frames_played; | ||
| 24 | u64 tag; | ||
| 25 | bool consumed; | ||
| 26 | }; | ||
| 27 | |||
| 28 | /** | ||
| 29 | * Contains a real backend stream for outputting samples to hardware, | ||
| 30 | * created only via a Sink (See Sink::AcquireSinkStream). | ||
| 31 | * | ||
| 32 | * Accepts a SinkBuffer and samples in PCM16 format to be output (see AppendBuffer). | ||
| 33 | * Appended buffers act as a FIFO queue, and will be held until played. | ||
| 34 | * You should regularly call IsBufferConsumed with the unique SinkBuffer tag to check if the buffer | ||
| 35 | * has been consumed. | ||
| 36 | * | ||
| 37 | * Since these are a FIFO queue, always check IsBufferConsumed in the same order you appended the | ||
| 38 | * buffers, skipping a buffer will result in all following buffers to never release. | ||
| 39 | * | ||
| 40 | * If the buffers appear to be stuck, you can stop and re-open an IAudioIn/IAudioOut service (this | ||
| 41 | * is what games do), or call ClearQueue to flush all of the buffers without a full restart. | ||
| 42 | */ | ||
| 43 | class SinkStream { | ||
| 44 | public: | ||
| 45 | virtual ~SinkStream() = default; | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Finalize the sink stream. | ||
| 49 | */ | ||
| 50 | virtual void Finalize() = 0; | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Start the sink stream. | ||
| 54 | * | ||
| 55 | * @param resume - Set to true if this is resuming the stream a previously-active stream. | ||
| 56 | * Default false. | ||
| 57 | */ | ||
| 58 | virtual void Start(bool resume = false) = 0; | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Stop the sink stream. | ||
| 62 | */ | ||
| 63 | virtual void Stop() = 0; | ||
| 64 | |||
| 65 | /** | ||
| 66 | * Append a new buffer and its samples to a waiting queue to play. | ||
| 67 | * | ||
| 68 | * @param buffer - Audio buffer information to be queued. | ||
| 69 | * @param samples - The s16 samples to be queue for playback. | ||
| 70 | */ | ||
| 71 | virtual void AppendBuffer(SinkBuffer& buffer, std::vector<s16>& samples) = 0; | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Release a buffer. Audio In only, will fill a buffer with recorded samples. | ||
| 75 | * | ||
| 76 | * @param num_samples - Maximum number of samples to receive. | ||
| 77 | * @return Vector of recorded samples. May have fewer than num_samples. | ||
| 78 | */ | ||
| 79 | virtual std::vector<s16> ReleaseBuffer(u64 num_samples) = 0; | ||
| 80 | |||
| 81 | /** | ||
| 82 | * Check if a certain buffer has been consumed (fully played). | ||
| 83 | * | ||
| 84 | * @param tag - Unique tag of a buffer to check for. | ||
| 85 | * @return True if the buffer has been played, otherwise false. | ||
| 86 | */ | ||
| 87 | virtual bool IsBufferConsumed(u64 tag) = 0; | ||
| 88 | |||
| 89 | /** | ||
| 90 | * Empty out the buffer queue. | ||
| 91 | */ | ||
| 92 | virtual void ClearQueue() = 0; | ||
| 93 | |||
| 94 | /** | ||
| 95 | * Check if the stream is paused. | ||
| 96 | * | ||
| 97 | * @return True if paused, otherwise false. | ||
| 98 | */ | ||
| 99 | bool IsPaused() { | ||
| 100 | return paused; | ||
| 101 | } | ||
| 102 | |||
| 103 | /** | ||
| 104 | * Get the number of system channels in this stream. | ||
| 105 | * | ||
| 106 | * @return Number of system channels. | ||
| 107 | */ | ||
| 108 | u32 GetSystemChannels() const { | ||
| 109 | return system_channels; | ||
| 110 | } | ||
| 111 | |||
| 112 | /** | ||
| 113 | * Set the number of channels the system expects. | ||
| 114 | * | ||
| 115 | * @param channels - New number of system channels. | ||
| 116 | */ | ||
| 117 | void SetSystemChannels(u32 channels) { | ||
| 118 | system_channels = channels; | ||
| 119 | } | ||
| 120 | |||
| 121 | /** | ||
| 122 | * Get the number of channels the hardware supports. | ||
| 123 | * | ||
| 124 | * @return Number of channels supported. | ||
| 125 | */ | ||
| 126 | u32 GetDeviceChannels() const { | ||
| 127 | return device_channels; | ||
| 128 | } | ||
| 129 | |||
| 130 | /** | ||
| 131 | * Get the total number of samples played by this stream. | ||
| 132 | * | ||
| 133 | * @return Number of samples played. | ||
| 134 | */ | ||
| 135 | u64 GetPlayedSampleCount() const { | ||
| 136 | return played_sample_count; | ||
| 137 | } | ||
| 138 | |||
| 139 | /** | ||
| 140 | * Set the number of samples played. | ||
| 141 | * This is started and stopped on system start/stop. | ||
| 142 | * | ||
| 143 | * @param played_sample_count_ - Number of samples to set. | ||
| 144 | */ | ||
| 145 | void SetPlayedSampleCount(u64 played_sample_count_) { | ||
| 146 | played_sample_count = played_sample_count_; | ||
| 147 | } | ||
| 148 | |||
| 149 | /** | ||
| 150 | * Add to the played sample count. | ||
| 151 | * | ||
| 152 | * @param num_samples - Number of samples to add. | ||
| 153 | */ | ||
| 154 | void AddPlayedSampleCount(u64 num_samples) { | ||
| 155 | played_sample_count += num_samples; | ||
| 156 | } | ||
| 157 | |||
| 158 | /** | ||
| 159 | * Get the system volume. | ||
| 160 | * | ||
| 161 | * @return The current system volume. | ||
| 162 | */ | ||
| 163 | f32 GetSystemVolume() const { | ||
| 164 | return system_volume; | ||
| 165 | } | ||
| 166 | |||
| 167 | /** | ||
| 168 | * Get the device volume. | ||
| 169 | * | ||
| 170 | * @return The current device volume. | ||
| 171 | */ | ||
| 172 | f32 GetDeviceVolume() const { | ||
| 173 | return device_volume; | ||
| 174 | } | ||
| 175 | |||
| 176 | /** | ||
| 177 | * Set the system volume. | ||
| 178 | * | ||
| 179 | * @param volume_ - The new system volume. | ||
| 180 | */ | ||
| 181 | void SetSystemVolume(f32 volume_) { | ||
| 182 | system_volume = volume_; | ||
| 183 | } | ||
| 184 | |||
| 185 | /** | ||
| 186 | * Set the device volume. | ||
| 187 | * | ||
| 188 | * @param volume_ - The new device volume. | ||
| 189 | */ | ||
| 190 | void SetDeviceVolume(f32 volume_) { | ||
| 191 | device_volume = volume_; | ||
| 192 | } | ||
| 193 | |||
| 194 | /** | ||
| 195 | * Get the number of queued audio buffers. | ||
| 196 | * | ||
| 197 | * @return The number of queued buffers. | ||
| 198 | */ | ||
| 199 | u32 GetQueueSize() { | ||
| 200 | return queued_buffers.load(); | ||
| 201 | } | ||
| 202 | |||
| 203 | protected: | ||
| 204 | /// Number of buffers waiting to be played | ||
| 205 | std::atomic<u32> queued_buffers{}; | ||
| 206 | /// Total samples played by this stream | ||
| 207 | std::atomic<u64> played_sample_count{}; | ||
| 208 | /// Set by the audio render/in/out system which uses this stream | ||
| 209 | f32 system_volume{1.0f}; | ||
| 210 | /// Set via IAudioDevice service calls | ||
| 211 | f32 device_volume{1.0f}; | ||
| 212 | /// Set by the audio render/in/out systen which uses this stream | ||
| 213 | u32 system_channels{2}; | ||
| 214 | /// Channels supported by hardware | ||
| 215 | u32 device_channels{2}; | ||
| 216 | /// Is this stream currently paused? | ||
| 217 | std::atomic<bool> paused{true}; | ||
| 218 | /// Was this stream previously playing? | ||
| 219 | std::atomic<bool> was_playing{false}; | ||
| 220 | }; | ||
| 221 | |||
| 222 | using SinkStreamPtr = std::unique_ptr<SinkStream>; | ||
| 223 | |||
| 224 | } // namespace AudioCore::Sink | ||