// Copyright 2018 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #include #include #include "audio_core/cubeb_sink.h" #include "audio_core/stream.h" #include "audio_core/time_stretch.h" #include "common/logging/log.h" #include "common/ring_buffer.h" #include "core/settings.h" namespace AudioCore { class CubebSinkStream final : public SinkStream { public: CubebSinkStream(cubeb* ctx, u32 sample_rate, u32 num_channels_, cubeb_devid output_device, const std::string& name) : ctx{ctx}, num_channels{std::min(num_channels_, 2u)}, time_stretch{sample_rate, num_channels} { cubeb_stream_params params{}; params.rate = sample_rate; params.channels = num_channels; params.format = CUBEB_SAMPLE_S16NE; params.layout = num_channels == 1 ? CUBEB_LAYOUT_MONO : CUBEB_LAYOUT_STEREO; u32 minimum_latency{}; if (cubeb_get_min_latency(ctx, ¶ms, &minimum_latency) != CUBEB_OK) { LOG_CRITICAL(Audio_Sink, "Error getting minimum latency"); } if (cubeb_stream_init(ctx, &stream_backend, name.c_str(), nullptr, nullptr, output_device, ¶ms, std::max(512u, minimum_latency), &CubebSinkStream::DataCallback, &CubebSinkStream::StateCallback, this) != CUBEB_OK) { LOG_CRITICAL(Audio_Sink, "Error initializing cubeb stream"); return; } if (cubeb_stream_start(stream_backend) != CUBEB_OK) { LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); return; } } ~CubebSinkStream() { if (!ctx) { return; } if (cubeb_stream_stop(stream_backend) != CUBEB_OK) { LOG_CRITICAL(Audio_Sink, "Error stopping cubeb stream"); } cubeb_stream_destroy(stream_backend); } void EnqueueSamples(u32 source_num_channels, const std::vector& samples) override { if (source_num_channels > num_channels) { // Downsample 6 channels to 2 std::vector buf; buf.reserve(samples.size() * num_channels / source_num_channels); for (size_t i = 0; i < samples.size(); i += source_num_channels) { for (size_t ch = 0; ch < num_channels; ch++) { buf.push_back(samples[i + ch]); } } queue.Push(buf); return; } queue.Push(samples); } size_t SamplesInQueue(u32 num_channels) const override { if (!ctx) return 0; return queue.Size() / num_channels; } void Flush() override { should_flush = true; } u32 GetNumChannels() const { return num_channels; } private: std::vector device_list; cubeb* ctx{}; cubeb_stream* stream_backend{}; u32 num_channels{}; Common::RingBuffer queue; std::array last_frame; std::atomic should_flush{}; TimeStretcher time_stretch; static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, void* output_buffer, long num_frames); static void StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state); }; CubebSink::CubebSink(std::string target_device_name) { if (cubeb_init(&ctx, "yuzu", nullptr) != CUBEB_OK) { LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); return; } if (target_device_name != auto_device_name && !target_device_name.empty()) { cubeb_device_collection collection; if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) { LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); } else { const auto collection_end{collection.device + collection.count}; const auto device{ std::find_if(collection.device, collection_end, [&](const cubeb_device_info& info) { return target_device_name == info.friendly_name; })}; if (device != collection_end) { output_device = device->devid; } cubeb_device_collection_destroy(ctx, &collection); } } } CubebSink::~CubebSink() { if (!ctx) { return; } for (auto& sink_stream : sink_streams) { sink_stream.reset(); } cubeb_destroy(ctx); } SinkStream& CubebSink::AcquireSinkStream(u32 sample_rate, u32 num_channels, const std::string& name) { sink_streams.push_back( std::make_unique(ctx, sample_rate, num_channels, output_device, name)); return *sink_streams.back(); } long CubebSinkStream::DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, void* output_buffer, long num_frames) { CubebSinkStream* impl = static_cast(user_data); u8* buffer = reinterpret_cast(output_buffer); if (!impl) { return {}; } const size_t num_channels = impl->GetNumChannels(); const size_t samples_to_write = num_channels * num_frames; size_t samples_written; if (Settings::values.enable_audio_stretching) { const std::vector in{impl->queue.Pop()}; const size_t num_in{in.size() / num_channels}; s16* const out{reinterpret_cast(buffer)}; const size_t out_frames = impl->time_stretch.Process(in.data(), num_in, out, num_frames); samples_written = out_frames * num_channels; if (impl->should_flush) { impl->time_stretch.Flush(); impl->should_flush = false; } } else { samples_written = impl->queue.Pop(buffer, samples_to_write); } if (samples_written >= num_channels) { std::memcpy(&impl->last_frame[0], buffer + (samples_written - num_channels) * sizeof(s16), num_channels * sizeof(s16)); } // Fill the rest of the frames with last_frame for (size_t i = samples_written; i < samples_to_write; i += num_channels) { std::memcpy(buffer + i * sizeof(s16), &impl->last_frame[0], num_channels * sizeof(s16)); } return num_frames; } void CubebSinkStream::StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state) {} std::vector ListCubebSinkDevices() { std::vector device_list; cubeb* ctx; if (cubeb_init(&ctx, "Citra Device Enumerator", nullptr) != CUBEB_OK) { LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); return {}; } cubeb_device_collection collection; if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) { LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); } else { for (size_t i = 0; i < collection.count; i++) { const cubeb_device_info& device = collection.device[i]; if (device.friendly_name) { device_list.emplace_back(device.friendly_name); } } cubeb_device_collection_destroy(ctx, &collection); } cubeb_destroy(ctx); return device_list; } } // namespace AudioCore