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/sink_stream.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "common/scope_exit.h" | ||||
| #include "core/core.h" | ||||
|  | ||||
| #ifdef _WIN32 | ||||
| @@ -332,25 +333,38 @@ std::vector<std::string> ListCubebSinkDevices(bool capture) { | ||||
|     return device_list; | ||||
| } | ||||
|  | ||||
| u32 GetCubebLatency() { | ||||
|     cubeb* ctx; | ||||
| namespace { | ||||
| 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 | ||||
|     auto com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); | ||||
| #endif | ||||
|  | ||||
|     // Init cubeb | ||||
|     if (cubeb_init(&ctx, "yuzu Latency Getter", nullptr) != CUBEB_OK) { | ||||
|         LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); | ||||
|         // Return a large latency so we choose SDL instead. | ||||
|         return 10000u; | ||||
|         LOG_ERROR(Audio_Sink, "Cubeb failed to init, it is not suitable."); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     SCOPE_EXIT({ cubeb_destroy(ctx); }); | ||||
|  | ||||
| #ifdef _WIN32 | ||||
|     if (SUCCEEDED(com_init_result)) { | ||||
|         CoUninitialize(); | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     // Test min latency | ||||
|     cubeb_stream_params params{}; | ||||
|     params.rate = TargetSampleRate; | ||||
|     params.channels = 2; | ||||
| @@ -361,12 +375,32 @@ u32 GetCubebLatency() { | ||||
|     u32 latency{0}; | ||||
|     const auto latency_error = cubeb_get_min_latency(ctx, ¶ms, &latency); | ||||
|     if (latency_error != CUBEB_OK) { | ||||
|         LOG_CRITICAL(Audio_Sink, "Error getting minimum latency, error: {}", latency_error); | ||||
|         latency = TargetSampleCount * 2; | ||||
|         LOG_ERROR(Audio_Sink, "Cubeb could not get min latency, it is not suitable."); | ||||
|         return false; | ||||
|     } | ||||
|     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 | ||||
|   | ||||
| @@ -97,10 +97,11 @@ private: | ||||
| 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 | ||||
|   | ||||
| @@ -9,6 +9,7 @@ | ||||
| #include "audio_core/sink/sdl2_sink.h" | ||||
| #include "audio_core/sink/sink_stream.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "common/scope_exit.h" | ||||
| #include "core/core.h" | ||||
|  | ||||
| namespace AudioCore::Sink { | ||||
| @@ -84,6 +85,7 @@ public: | ||||
|         } | ||||
|  | ||||
|         Stop(); | ||||
|         SDL_ClearQueuedAudio(device); | ||||
|         SDL_CloseAudioDevice(device); | ||||
|     } | ||||
|  | ||||
| @@ -227,8 +229,42 @@ std::vector<std::string> ListSDLSinkDevices(bool capture) { | ||||
|     return device_list; | ||||
| } | ||||
|  | ||||
| u32 GetSDLLatency() { | ||||
|     return TargetSampleCount * 2; | ||||
| bool IsSDLSuitable() { | ||||
| #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 | ||||
|   | ||||
| @@ -88,10 +88,11 @@ private: | ||||
| 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 | ||||
|   | ||||
| @@ -22,7 +22,7 @@ namespace { | ||||
| struct SinkDetails { | ||||
|     using FactoryFn = std::unique_ptr<Sink> (*)(std::string_view); | ||||
|     using ListDevicesFn = std::vector<std::string> (*)(bool); | ||||
|     using LatencyFn = u32 (*)(); | ||||
|     using SuitableFn = bool (*)(); | ||||
|  | ||||
|     /// Name for this sink. | ||||
|     Settings::AudioEngine id; | ||||
| @@ -30,8 +30,8 @@ struct SinkDetails { | ||||
|     FactoryFn factory; | ||||
|     /// A method to call to list available devices. | ||||
|     ListDevicesFn list_devices; | ||||
|     /// Method to get the latency of this backend. | ||||
|     LatencyFn latency; | ||||
|     /// Check whether this backend is suitable to be used. | ||||
|     SuitableFn is_suitable; | ||||
| }; | ||||
|  | ||||
| // 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); | ||||
|         }, | ||||
|         &ListCubebSinkDevices, | ||||
|         &GetCubebLatency, | ||||
|         &IsCubebSuitable, | ||||
|     }, | ||||
| #endif | ||||
| #ifdef HAVE_SDL2 | ||||
| @@ -53,14 +53,17 @@ constexpr SinkDetails sink_details[] = { | ||||
|             return std::make_unique<SDLSink>(device_id); | ||||
|         }, | ||||
|         &ListSDLSinkDevices, | ||||
|         &GetSDLLatency, | ||||
|         &IsSDLSuitable, | ||||
|     }, | ||||
| #endif | ||||
|     SinkDetails{Settings::AudioEngine::Null, | ||||
|                 [](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; }}, | ||||
|     SinkDetails{ | ||||
|         Settings::AudioEngine::Null, | ||||
|         [](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 true; }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| const SinkDetails& GetOutputSinkDetails(Settings::AudioEngine sink_id) { | ||||
| @@ -72,18 +75,22 @@ const SinkDetails& GetOutputSinkDetails(Settings::AudioEngine sink_id) { | ||||
|     auto iter = find_backend(sink_id); | ||||
|  | ||||
|     if (sink_id == Settings::AudioEngine::Auto) { | ||||
|         // Auto-select a backend. Prefer CubeB, but it may report a large minimum latency which | ||||
|         // causes audio issues, in that case go with SDL. | ||||
| #if defined(HAVE_CUBEB) && defined(HAVE_SDL2) | ||||
|         iter = find_backend(Settings::AudioEngine::Cubeb); | ||||
|         if (iter->latency() > TargetSampleCount * 3) { | ||||
|             iter = find_backend(Settings::AudioEngine::Sdl2); | ||||
|         // Auto-select a backend. Use the sink details ordering, preferring cubeb first, checking | ||||
|         // that the backend is available and suitable to use. | ||||
|         for (auto& details : sink_details) { | ||||
|             if (details.is_suitable()) { | ||||
|                 iter = &details; | ||||
|                 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)) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user