Pre-test opening a stream for audio backends, fall back to null if not suitable.
This commit is contained in:
		| @@ -8,6 +8,7 @@ | |||||||
| #include "audio_core/sink/cubeb_sink.h" | #include "audio_core/sink/cubeb_sink.h" | ||||||
| #include "audio_core/sink/sink_stream.h" | #include "audio_core/sink/sink_stream.h" | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
|  | #include "common/scope_exit.h" | ||||||
| #include "core/core.h" | #include "core/core.h" | ||||||
|  |  | ||||||
| #ifdef _WIN32 | #ifdef _WIN32 | ||||||
| @@ -332,25 +333,38 @@ std::vector<std::string> ListCubebSinkDevices(bool capture) { | |||||||
|     return device_list; |     return device_list; | ||||||
| } | } | ||||||
|  |  | ||||||
| u32 GetCubebLatency() { | namespace { | ||||||
|     cubeb* ctx; | static long TmpDataCallback(cubeb_stream*, void*, const void*, void*, long) { | ||||||
|  |     return TargetSampleCount; | ||||||
|  | } | ||||||
|  | static void TmpStateCallback(cubeb_stream*, void*, cubeb_state) {} | ||||||
|  | } // namespace | ||||||
|  |  | ||||||
|  | bool IsCubebSuitable() { | ||||||
|  | #if !defined(HAVE_CUBEB) | ||||||
|  |     return false; | ||||||
|  | #else | ||||||
|  |     cubeb* ctx{nullptr}; | ||||||
|  |  | ||||||
| #ifdef _WIN32 | #ifdef _WIN32 | ||||||
|     auto com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); |     auto com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  |     // Init cubeb | ||||||
|     if (cubeb_init(&ctx, "yuzu Latency Getter", nullptr) != CUBEB_OK) { |     if (cubeb_init(&ctx, "yuzu Latency Getter", nullptr) != CUBEB_OK) { | ||||||
|         LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); |         LOG_ERROR(Audio_Sink, "Cubeb failed to init, it is not suitable."); | ||||||
|         // Return a large latency so we choose SDL instead. |         return false; | ||||||
|         return 10000u; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     SCOPE_EXIT({ cubeb_destroy(ctx); }); | ||||||
|  |  | ||||||
| #ifdef _WIN32 | #ifdef _WIN32 | ||||||
|     if (SUCCEEDED(com_init_result)) { |     if (SUCCEEDED(com_init_result)) { | ||||||
|         CoUninitialize(); |         CoUninitialize(); | ||||||
|     } |     } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  |     // Test min latency | ||||||
|     cubeb_stream_params params{}; |     cubeb_stream_params params{}; | ||||||
|     params.rate = TargetSampleRate; |     params.rate = TargetSampleRate; | ||||||
|     params.channels = 2; |     params.channels = 2; | ||||||
| @@ -361,12 +375,32 @@ u32 GetCubebLatency() { | |||||||
|     u32 latency{0}; |     u32 latency{0}; | ||||||
|     const auto latency_error = cubeb_get_min_latency(ctx, ¶ms, &latency); |     const auto latency_error = cubeb_get_min_latency(ctx, ¶ms, &latency); | ||||||
|     if (latency_error != CUBEB_OK) { |     if (latency_error != CUBEB_OK) { | ||||||
|         LOG_CRITICAL(Audio_Sink, "Error getting minimum latency, error: {}", latency_error); |         LOG_ERROR(Audio_Sink, "Cubeb could not get min latency, it is not suitable."); | ||||||
|         latency = TargetSampleCount * 2; |         return false; | ||||||
|     } |     } | ||||||
|     latency = std::max(latency, TargetSampleCount * 2); |     latency = std::max(latency, TargetSampleCount * 2); | ||||||
|     cubeb_destroy(ctx); |  | ||||||
|     return latency; |     if (latency > TargetSampleCount * 3) { | ||||||
|  |         LOG_ERROR(Audio_Sink, "Cubeb latency is too high, it is not suitable."); | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Test opening a device with standard parameters | ||||||
|  |     cubeb_devid output_device{0}; | ||||||
|  |     cubeb_devid input_device{0}; | ||||||
|  |     std::string name{"Yuzu test"}; | ||||||
|  |     cubeb_stream* stream{nullptr}; | ||||||
|  |  | ||||||
|  |     if (cubeb_stream_init(ctx, &stream, name.c_str(), input_device, nullptr, output_device, ¶ms, | ||||||
|  |                           latency, &TmpDataCallback, &TmpStateCallback, nullptr) != CUBEB_OK) { | ||||||
|  |         LOG_CRITICAL(Audio_Sink, "Cubeb could not open a device, it is not suitable."); | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     cubeb_stream_stop(stream); | ||||||
|  |     cubeb_stream_destroy(stream); | ||||||
|  |     return true; | ||||||
|  | #endif | ||||||
| } | } | ||||||
|  |  | ||||||
| } // namespace AudioCore::Sink | } // namespace AudioCore::Sink | ||||||
|   | |||||||
| @@ -97,10 +97,11 @@ private: | |||||||
| std::vector<std::string> ListCubebSinkDevices(bool capture); | std::vector<std::string> ListCubebSinkDevices(bool capture); | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Get the reported latency for this sink. |  * Check if this backend is suitable for use. | ||||||
|  |  * Checks if enabled, its latency, whether it opens successfully, etc. | ||||||
|  * |  * | ||||||
|  * @return Minimum latency for this sink. |  * @return True is this backend is suitable, false otherwise. | ||||||
|  */ |  */ | ||||||
| u32 GetCubebLatency(); | bool IsCubebSuitable(); | ||||||
|  |  | ||||||
| } // namespace AudioCore::Sink | } // namespace AudioCore::Sink | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ | |||||||
| #include "audio_core/sink/sdl2_sink.h" | #include "audio_core/sink/sdl2_sink.h" | ||||||
| #include "audio_core/sink/sink_stream.h" | #include "audio_core/sink/sink_stream.h" | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
|  | #include "common/scope_exit.h" | ||||||
| #include "core/core.h" | #include "core/core.h" | ||||||
|  |  | ||||||
| namespace AudioCore::Sink { | namespace AudioCore::Sink { | ||||||
| @@ -84,6 +85,7 @@ public: | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         Stop(); |         Stop(); | ||||||
|  |         SDL_ClearQueuedAudio(device); | ||||||
|         SDL_CloseAudioDevice(device); |         SDL_CloseAudioDevice(device); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -227,8 +229,42 @@ std::vector<std::string> ListSDLSinkDevices(bool capture) { | |||||||
|     return device_list; |     return device_list; | ||||||
| } | } | ||||||
|  |  | ||||||
| u32 GetSDLLatency() { | bool IsSDLSuitable() { | ||||||
|     return TargetSampleCount * 2; | #if !defined(HAVE_SDL2) | ||||||
|  |     return false; | ||||||
|  | #else | ||||||
|  |     // Check SDL can init | ||||||
|  |     if (!SDL_WasInit(SDL_INIT_AUDIO)) { | ||||||
|  |         if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { | ||||||
|  |             LOG_ERROR(Audio_Sink, "SDL failed to init, it is not suitable. Error: {}", | ||||||
|  |                       SDL_GetError()); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // We can set any latency frequency we want with SDL, so no need to check that. | ||||||
|  |  | ||||||
|  |     // Check we can open a device with standard parameters | ||||||
|  |     SDL_AudioSpec spec; | ||||||
|  |     spec.freq = TargetSampleRate; | ||||||
|  |     spec.channels = 2u; | ||||||
|  |     spec.format = AUDIO_S16SYS; | ||||||
|  |     spec.samples = TargetSampleCount * 2; | ||||||
|  |     spec.callback = nullptr; | ||||||
|  |     spec.userdata = nullptr; | ||||||
|  |  | ||||||
|  |     SDL_AudioSpec obtained; | ||||||
|  |     auto device = SDL_OpenAudioDevice(nullptr, false, &spec, &obtained, false); | ||||||
|  |  | ||||||
|  |     if (device == 0) { | ||||||
|  |         LOG_ERROR(Audio_Sink, "SDL failed to open a device, it is not suitable. Error: {}", | ||||||
|  |                   SDL_GetError()); | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     SDL_CloseAudioDevice(device); | ||||||
|  |     return true; | ||||||
|  | #endif | ||||||
| } | } | ||||||
|  |  | ||||||
| } // namespace AudioCore::Sink | } // namespace AudioCore::Sink | ||||||
|   | |||||||
| @@ -88,10 +88,11 @@ private: | |||||||
| std::vector<std::string> ListSDLSinkDevices(bool capture); | std::vector<std::string> ListSDLSinkDevices(bool capture); | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Get the reported latency for this sink. |  * Check if this backend is suitable for use. | ||||||
|  |  * Checks if enabled, its latency, whether it opens successfully, etc. | ||||||
|  * |  * | ||||||
|  * @return Minimum latency for this sink. |  * @return True is this backend is suitable, false otherwise. | ||||||
|  */ |  */ | ||||||
| u32 GetSDLLatency(); | bool IsSDLSuitable(); | ||||||
|  |  | ||||||
| } // namespace AudioCore::Sink | } // namespace AudioCore::Sink | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ namespace { | |||||||
| struct SinkDetails { | struct SinkDetails { | ||||||
|     using FactoryFn = std::unique_ptr<Sink> (*)(std::string_view); |     using FactoryFn = std::unique_ptr<Sink> (*)(std::string_view); | ||||||
|     using ListDevicesFn = std::vector<std::string> (*)(bool); |     using ListDevicesFn = std::vector<std::string> (*)(bool); | ||||||
|     using LatencyFn = u32 (*)(); |     using SuitableFn = bool (*)(); | ||||||
|  |  | ||||||
|     /// Name for this sink. |     /// Name for this sink. | ||||||
|     Settings::AudioEngine id; |     Settings::AudioEngine id; | ||||||
| @@ -30,8 +30,8 @@ struct SinkDetails { | |||||||
|     FactoryFn factory; |     FactoryFn factory; | ||||||
|     /// A method to call to list available devices. |     /// A method to call to list available devices. | ||||||
|     ListDevicesFn list_devices; |     ListDevicesFn list_devices; | ||||||
|     /// Method to get the latency of this backend. |     /// Check whether this backend is suitable to be used. | ||||||
|     LatencyFn latency; |     SuitableFn is_suitable; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // sink_details is ordered in terms of desirability, with the best choice at the top. | // sink_details is ordered in terms of desirability, with the best choice at the top. | ||||||
| @@ -43,7 +43,7 @@ constexpr SinkDetails sink_details[] = { | |||||||
|             return std::make_unique<CubebSink>(device_id); |             return std::make_unique<CubebSink>(device_id); | ||||||
|         }, |         }, | ||||||
|         &ListCubebSinkDevices, |         &ListCubebSinkDevices, | ||||||
|         &GetCubebLatency, |         &IsCubebSuitable, | ||||||
|     }, |     }, | ||||||
| #endif | #endif | ||||||
| #ifdef HAVE_SDL2 | #ifdef HAVE_SDL2 | ||||||
| @@ -53,14 +53,17 @@ constexpr SinkDetails sink_details[] = { | |||||||
|             return std::make_unique<SDLSink>(device_id); |             return std::make_unique<SDLSink>(device_id); | ||||||
|         }, |         }, | ||||||
|         &ListSDLSinkDevices, |         &ListSDLSinkDevices, | ||||||
|         &GetSDLLatency, |         &IsSDLSuitable, | ||||||
|     }, |     }, | ||||||
| #endif | #endif | ||||||
|     SinkDetails{Settings::AudioEngine::Null, |     SinkDetails{ | ||||||
|                 [](std::string_view device_id) -> std::unique_ptr<Sink> { |         Settings::AudioEngine::Null, | ||||||
|                     return std::make_unique<NullSink>(device_id); |         [](std::string_view device_id) -> std::unique_ptr<Sink> { | ||||||
|                 }, |             return std::make_unique<NullSink>(device_id); | ||||||
|                 [](bool capture) { return std::vector<std::string>{"null"}; }, []() { return 0u; }}, |         }, | ||||||
|  |         [](bool capture) { return std::vector<std::string>{"null"}; }, | ||||||
|  |         []() { return true; }, | ||||||
|  |     }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const SinkDetails& GetOutputSinkDetails(Settings::AudioEngine sink_id) { | const SinkDetails& GetOutputSinkDetails(Settings::AudioEngine sink_id) { | ||||||
| @@ -72,18 +75,22 @@ const SinkDetails& GetOutputSinkDetails(Settings::AudioEngine sink_id) { | |||||||
|     auto iter = find_backend(sink_id); |     auto iter = find_backend(sink_id); | ||||||
|  |  | ||||||
|     if (sink_id == Settings::AudioEngine::Auto) { |     if (sink_id == Settings::AudioEngine::Auto) { | ||||||
|         // Auto-select a backend. Prefer CubeB, but it may report a large minimum latency which |         // Auto-select a backend. Use the sink details ordering, preferring cubeb first, checking | ||||||
|         // causes audio issues, in that case go with SDL. |         // that the backend is available and suitable to use. | ||||||
| #if defined(HAVE_CUBEB) && defined(HAVE_SDL2) |         for (auto& details : sink_details) { | ||||||
|         iter = find_backend(Settings::AudioEngine::Cubeb); |             if (details.is_suitable()) { | ||||||
|         if (iter->latency() > TargetSampleCount * 3) { |                 iter = &details; | ||||||
|             iter = find_backend(Settings::AudioEngine::Sdl2); |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         LOG_ERROR(Service_Audio, "Auto-selecting the {} backend", | ||||||
|  |                   Settings::CanonicalizeEnum(iter->id)); | ||||||
|  |     } else { | ||||||
|  |         if (iter != std::end(sink_details) && !iter->is_suitable()) { | ||||||
|  |             LOG_ERROR(Service_Audio, "Selected backend {} is not suitable, falling back to null", | ||||||
|  |                       Settings::CanonicalizeEnum(iter->id)); | ||||||
|  |             iter = find_backend(Settings::AudioEngine::Null); | ||||||
|         } |         } | ||||||
| #else |  | ||||||
|         iter = std::begin(sink_details); |  | ||||||
| #endif |  | ||||||
|         LOG_INFO(Service_Audio, "Auto-selecting the {} backend", |  | ||||||
|                  Settings::CanonicalizeEnum(iter->id)); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (iter == std::end(sink_details)) { |     if (iter == std::end(sink_details)) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user