diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 5402a532f..d1d1436da 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -53,10 +53,10 @@ endif() # SDL2 if (NOT SDL2_FOUND AND ENABLE_SDL2) if (NOT WIN32) - # Yuzu itself needs: Events Joystick Haptic Sensor Timers + # Yuzu itself needs: Events Joystick Haptic Sensor Timers Audio # Yuzu-cmd also needs: Video (depends on Loadso/Dlopen) set(SDL_UNUSED_SUBSYSTEMS - Atomic Audio Render Power Threads + Atomic Render Power Threads File CPUinfo Filesystem Locale) foreach(_SUB ${SDL_UNUSED_SUBSYSTEMS}) string(TOUPPER ${_SUB} _OPT) diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index a0ae07752..d25a1a645 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -42,6 +42,7 @@ add_library(audio_core STATIC voice_context.h $<$:cubeb_sink.cpp cubeb_sink.h> + $<$:sdl2_sink.cpp sdl2_sink.h> ) create_target_directory_groups(audio_core) @@ -71,3 +72,7 @@ if(ENABLE_CUBEB) target_link_libraries(audio_core PRIVATE cubeb) target_compile_definitions(audio_core PRIVATE -DHAVE_CUBEB=1) endif() +if(ENABLE_SDL2) + target_link_libraries(audio_core PRIVATE SDL2) + target_compile_definitions(audio_core PRIVATE HAVE_SDL2) +endif() diff --git a/src/audio_core/sdl2_sink.cpp b/src/audio_core/sdl2_sink.cpp new file mode 100644 index 000000000..62d3716a6 --- /dev/null +++ b/src/audio_core/sdl2_sink.cpp @@ -0,0 +1,163 @@ +// 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/sdl2_sink.h" +#include "audio_core/stream.h" +#include "audio_core/time_stretch.h" +#include "common/assert.h" +#include "common/logging/log.h" +//#include "common/settings.h" + +// Ignore -Wimplicit-fallthrough due to https://github.com/libsdl-org/SDL/issues/4307 +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wimplicit-fallthrough" +#endif +#include +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +namespace AudioCore { + +class SDLSinkStream final : public SinkStream { +public: + SDLSinkStream(u32 sample_rate, u32 num_channels_, const std::string& output_device) + : num_channels{std::min(num_channels_, 6u)}, time_stretch{sample_rate, num_channels} { + + SDL_AudioSpec spec; + spec.freq = sample_rate; + spec.channels = static_cast(num_channels); + spec.format = AUDIO_S16SYS; + spec.samples = 4096; + spec.callback = nullptr; + + SDL_AudioSpec obtained; + if (output_device.empty()) { + dev = SDL_OpenAudioDevice(nullptr, 0, &spec, &obtained, 0); + } else { + dev = SDL_OpenAudioDevice(output_device.c_str(), 0, &spec, &obtained, 0); + } + + if (dev == 0) { + LOG_CRITICAL(Audio_Sink, "Error opening sdl audio device: {}", SDL_GetError()); + return; + } + + SDL_PauseAudioDevice(dev, 0); + } + + ~SDLSinkStream() override { + if (dev == 0) { + return; + } + + SDL_CloseAudioDevice(dev); + } + + void EnqueueSamples(u32 source_num_channels, const std::vector& samples) override { + if (source_num_channels > num_channels) { + // Downsample 6 channels to 2 + ASSERT_MSG(source_num_channels == 6, "Channel count must be 6"); + + std::vector buf; + buf.reserve(samples.size() * num_channels / source_num_channels); + for (std::size_t i = 0; i < samples.size(); i += source_num_channels) { + // Downmixing implementation taken from the ATSC standard + const s16 left{samples[i + 0]}; + const s16 right{samples[i + 1]}; + const s16 center{samples[i + 2]}; + const s16 surround_left{samples[i + 4]}; + const s16 surround_right{samples[i + 5]}; + // Not used in the ATSC reference implementation + [[maybe_unused]] const s16 low_frequency_effects{samples[i + 3]}; + + constexpr s32 clev{707}; // center mixing level coefficient + constexpr s32 slev{707}; // surround mixing level coefficient + + buf.push_back(static_cast(left + (clev * center / 1000) + + (slev * surround_left / 1000))); + buf.push_back(static_cast(right + (clev * center / 1000) + + (slev * surround_right / 1000))); + } + int ret = SDL_QueueAudio(dev, static_cast(buf.data()), + static_cast(buf.size() * sizeof(s16))); + if (ret < 0) + LOG_WARNING(Audio_Sink, "Could not queue audio buffer: {}", SDL_GetError()); + return; + } + + int ret = SDL_QueueAudio(dev, static_cast(samples.data()), + static_cast(samples.size() * sizeof(s16))); + if (ret < 0) + LOG_WARNING(Audio_Sink, "Could not queue audio buffer: {}", SDL_GetError()); + } + + std::size_t SamplesInQueue(u32 channel_count) const override { + if (dev == 0) + return 0; + + return SDL_GetQueuedAudioSize(dev) / (channel_count * sizeof(s16)); + } + + void Flush() override { + should_flush = true; + } + + u32 GetNumChannels() const { + return num_channels; + } + +private: + SDL_AudioDeviceID dev = 0; + u32 num_channels{}; + std::atomic should_flush{}; + TimeStretcher time_stretch; +}; + +SDLSink::SDLSink(std::string_view target_device_name) { + if (!SDL_WasInit(SDL_INIT_AUDIO)) { + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError()); + return; + } + } + + if (target_device_name != auto_device_name && !target_device_name.empty()) { + output_device = target_device_name; + } else { + output_device.clear(); + } +} + +SDLSink::~SDLSink() = default; + +SinkStream& SDLSink::AcquireSinkStream(u32 sample_rate, u32 num_channels, const std::string&) { + sink_streams.push_back( + std::make_unique(sample_rate, num_channels, output_device)); + return *sink_streams.back(); +} + +std::vector ListSDLSinkDevices() { + std::vector device_list; + + if (!SDL_WasInit(SDL_INIT_AUDIO)) { + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError()); + return {}; + } + } + + const int device_count = SDL_GetNumAudioDevices(0); + for (int i = 0; i < device_count; ++i) { + device_list.emplace_back(SDL_GetAudioDeviceName(i, 0)); + } + + return device_list; +} + +} // namespace AudioCore diff --git a/src/audio_core/sdl2_sink.h b/src/audio_core/sdl2_sink.h new file mode 100644 index 000000000..8ec1526d8 --- /dev/null +++ b/src/audio_core/sdl2_sink.h @@ -0,0 +1,29 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +#include "audio_core/sink.h" + +namespace AudioCore { + +class SDLSink final : public Sink { +public: + explicit SDLSink(std::string_view device_id); + ~SDLSink() override; + + SinkStream& AcquireSinkStream(u32 sample_rate, u32 num_channels, + const std::string& name) override; + +private: + std::string output_device; + std::vector sink_streams; +}; + +std::vector ListSDLSinkDevices(); + +} // namespace AudioCore diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink_details.cpp index a848eb1c9..de10aecd2 100644 --- a/src/audio_core/sink_details.cpp +++ b/src/audio_core/sink_details.cpp @@ -11,6 +11,9 @@ #ifdef HAVE_CUBEB #include "audio_core/cubeb_sink.h" #endif +#ifdef HAVE_SDL2 +#include "audio_core/sdl2_sink.h" +#endif #include "common/logging/log.h" namespace AudioCore { @@ -35,6 +38,13 @@ constexpr SinkDetails sink_details[] = { return std::make_unique(device_id); }, &ListCubebSinkDevices}, +#endif +#ifdef HAVE_SDL2 + SinkDetails{"sdl2", + [](std::string_view device_id) -> std::unique_ptr { + return std::make_unique(device_id); + }, + &ListSDLSinkDevices}, #endif SinkDetails{"null", [](std::string_view device_id) -> std::unique_ptr { diff --git a/src/yuzu_cmd/default_ini.h b/src/yuzu_cmd/default_ini.h index efa1b1d18..37d895ebd 100644 --- a/src/yuzu_cmd/default_ini.h +++ b/src/yuzu_cmd/default_ini.h @@ -260,7 +260,10 @@ swap_screen = [Audio] # Which audio output engine to use. -# auto (default): Auto-select, null: No audio output, cubeb: Cubeb audio engine (if available) +# auto (default): Auto-select +# cubeb: Cubeb audio engine (if available) +# sdl2: SDL2 audio engine (if available) +# null: No audio output output_engine = # Whether or not to enable the audio-stretching post-processing effect.