summaryrefslogtreecommitdiff
path: root/src/audio_core/sink
diff options
context:
space:
mode:
Diffstat (limited to 'src/audio_core/sink')
-rw-r--r--src/audio_core/sink/cubeb_sink.cpp651
-rw-r--r--src/audio_core/sink/cubeb_sink.h110
-rw-r--r--src/audio_core/sink/null_sink.h52
-rw-r--r--src/audio_core/sink/sdl2_sink.cpp556
-rw-r--r--src/audio_core/sink/sdl2_sink.h101
-rw-r--r--src/audio_core/sink/sink.h106
-rw-r--r--src/audio_core/sink/sink_details.cpp91
-rw-r--r--src/audio_core/sink/sink_details.h43
-rw-r--r--src/audio_core/sink/sink_stream.h224
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
26namespace AudioCore::Sink {
27/**
28 * Cubeb sink stream, responsible for sinking samples to hardware.
29 */
30class CubebSinkStream final : public SinkStream {
31public:
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, &params, &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 &params, 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, &params, 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
336private:
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
518CubebSink::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
551CubebSink::~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
569SinkStream* 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
577void 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
587void CubebSink::CloseStreams() {
588 sink_streams.clear();
589}
590
591void CubebSink::PauseStreams() {
592 for (auto& stream : sink_streams) {
593 stream->Stop();
594 }
595}
596
597void CubebSink::UnpauseStreams() {
598 for (auto& stream : sink_streams) {
599 stream->Start(true);
600 }
601}
602
603f32 CubebSink::GetDeviceVolume() const {
604 if (sink_streams.empty()) {
605 return 1.0f;
606 }
607
608 return sink_streams[0]->GetDeviceVolume();
609}
610
611void CubebSink::SetDeviceVolume(const f32 volume) {
612 for (auto& stream : sink_streams) {
613 stream->SetDeviceVolume(volume);
614 }
615}
616
617void CubebSink::SetSystemVolume(const f32 volume) {
618 for (auto& stream : sink_streams) {
619 stream->SetSystemVolume(volume);
620 }
621}
622
623std::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
13namespace Core {
14class System;
15}
16
17namespace AudioCore::Sink {
18class 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 */
24class CubebSink final : public Sink {
25public:
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
87private:
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 */
108std::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
9namespace AudioCore::Sink {
10/**
11 * A no-op sink for when no audio out is wanted.
12 */
13class NullSink final : public Sink {
14public:
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
35private:
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
30namespace AudioCore::Sink {
31/**
32 * SDL sink stream, responsible for sinking samples to hardware.
33 */
34class SDLSinkStream final : public SinkStream {
35public:
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
309private:
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
466SDLSink::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
483SDLSink::~SDLSink() = default;
484
485SinkStream* 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
492void 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
502void SDLSink::CloseStreams() {
503 sink_streams.clear();
504}
505
506void SDLSink::PauseStreams() {
507 for (auto& stream : sink_streams) {
508 stream->Stop();
509 }
510}
511
512void SDLSink::UnpauseStreams() {
513 for (auto& stream : sink_streams) {
514 stream->Start();
515 }
516}
517
518f32 SDLSink::GetDeviceVolume() const {
519 if (sink_streams.empty()) {
520 return 1.0f;
521 }
522
523 return sink_streams[0]->GetDeviceVolume();
524}
525
526void SDLSink::SetDeviceVolume(const f32 volume) {
527 for (auto& stream : sink_streams) {
528 stream->SetDeviceVolume(volume);
529 }
530}
531
532void SDLSink::SetSystemVolume(const f32 volume) {
533 for (auto& stream : sink_streams) {
534 stream->SetSystemVolume(volume);
535 }
536}
537
538std::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
11namespace Core {
12class System;
13}
14
15namespace AudioCore::Sink {
16class 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 */
22class SDLSink final : public Sink {
23public:
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
85private:
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 */
99std::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
12namespace Common {
13class Event;
14}
15namespace Core {
16class System;
17}
18
19namespace AudioCore::Sink {
20
21constexpr 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 */
27class Sink {
28public:
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
99protected:
100 /// Number of device channels supported by the hardware
101 u32 device_channels{2};
102};
103
104using 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
18namespace AudioCore::Sink {
19namespace {
20struct 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.
33constexpr 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
55const 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
74std::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
83std::vector<std::string> GetDeviceListForSink(std::string_view sink_id, bool capture) {
84 return GetOutputSinkDetails(sink_id).list_devices(capture);
85}
86
87std::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
10namespace AudioCore {
11class AudioManager;
12
13namespace Sink {
14
15class Sink;
16
17/**
18 * Retrieves the IDs for all available audio sinks.
19 *
20 * @return Vector of available sink names.
21 */
22std::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 */
31std::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 */
40std::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
13namespace AudioCore::Sink {
14
15enum class StreamType {
16 Render,
17 Out,
18 In,
19};
20
21struct 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 */
43class SinkStream {
44public:
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
203protected:
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
222using SinkStreamPtr = std::unique_ptr<SinkStream>;
223
224} // namespace AudioCore::Sink