cubeb_audio_stream.cpp (11043B)
1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "host.h" 5 #include "imgui_manager.h" 6 7 #include "core/settings.h" 8 9 #include "common/assert.h" 10 #include "common/error.h" 11 #include "common/log.h" 12 #include "common/scoped_guard.h" 13 #include "common/string_util.h" 14 15 #include "cubeb/cubeb.h" 16 #include "fmt/format.h" 17 18 Log_SetChannel(CubebAudioStream); 19 20 namespace { 21 22 class CubebAudioStream : public AudioStream 23 { 24 public: 25 CubebAudioStream(u32 sample_rate, const AudioStreamParameters& parameters); 26 ~CubebAudioStream(); 27 28 void SetPaused(bool paused) override; 29 30 bool Initialize(const char* driver_name, const char* device_name, Error* error); 31 32 private: 33 static void LogCallback(const char* fmt, ...); 34 static long DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer, 35 long nframes); 36 static void StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state); 37 38 void DestroyContextAndStream(); 39 40 cubeb* m_context = nullptr; 41 cubeb_stream* stream = nullptr; 42 }; 43 } // namespace 44 45 static TinyString GetCubebErrorString(int rv) 46 { 47 TinyString ret; 48 switch (rv) 49 { 50 // clang-format off 51 #define C(e) case e: ret.assign(#e); break 52 // clang-format on 53 54 C(CUBEB_OK); 55 C(CUBEB_ERROR); 56 C(CUBEB_ERROR_INVALID_FORMAT); 57 C(CUBEB_ERROR_INVALID_PARAMETER); 58 C(CUBEB_ERROR_NOT_SUPPORTED); 59 C(CUBEB_ERROR_DEVICE_UNAVAILABLE); 60 61 default: 62 return "CUBEB_ERROR_UNKNOWN"; 63 64 #undef C 65 } 66 67 ret.append_format(" ({})", rv); 68 return ret; 69 } 70 71 CubebAudioStream::CubebAudioStream(u32 sample_rate, const AudioStreamParameters& parameters) 72 : AudioStream(sample_rate, parameters) 73 { 74 } 75 76 CubebAudioStream::~CubebAudioStream() 77 { 78 DestroyContextAndStream(); 79 } 80 81 void CubebAudioStream::LogCallback(const char* fmt, ...) 82 { 83 LargeString str; 84 std::va_list ap; 85 va_start(ap, fmt); 86 str.vsprintf(fmt, ap); 87 va_end(ap); 88 DEV_LOG(str); 89 } 90 91 void CubebAudioStream::DestroyContextAndStream() 92 { 93 if (stream) 94 { 95 cubeb_stream_stop(stream); 96 cubeb_stream_destroy(stream); 97 stream = nullptr; 98 } 99 100 if (m_context) 101 { 102 cubeb_destroy(m_context); 103 m_context = nullptr; 104 } 105 } 106 107 bool CubebAudioStream::Initialize(const char* driver_name, const char* device_name, Error* error) 108 { 109 cubeb_set_log_callback(CUBEB_LOG_NORMAL, LogCallback); 110 111 int rv = 112 cubeb_init(&m_context, "DuckStation", g_settings.audio_driver.empty() ? nullptr : g_settings.audio_driver.c_str()); 113 if (rv != CUBEB_OK) 114 { 115 Error::SetStringFmt(error, "Could not initialize cubeb context: {}", GetCubebErrorString(rv)); 116 return false; 117 } 118 119 static constexpr const std::array<std::pair<cubeb_channel_layout, SampleReader>, 120 static_cast<size_t>(AudioExpansionMode::Count)> 121 channel_setups = {{ 122 // Disabled 123 {CUBEB_LAYOUT_STEREO, StereoSampleReaderImpl}, 124 // StereoLFE 125 {CUBEB_LAYOUT_STEREO_LFE, &SampleReaderImpl<AudioExpansionMode::StereoLFE, READ_CHANNEL_FRONT_LEFT, 126 READ_CHANNEL_FRONT_RIGHT, READ_CHANNEL_LFE>}, 127 // Quadraphonic 128 {CUBEB_LAYOUT_QUAD, &SampleReaderImpl<AudioExpansionMode::Quadraphonic, READ_CHANNEL_FRONT_LEFT, 129 READ_CHANNEL_FRONT_RIGHT, READ_CHANNEL_REAR_LEFT, READ_CHANNEL_REAR_RIGHT>}, 130 // QuadraphonicLFE 131 {CUBEB_LAYOUT_QUAD_LFE, 132 &SampleReaderImpl<AudioExpansionMode::QuadraphonicLFE, READ_CHANNEL_FRONT_LEFT, READ_CHANNEL_FRONT_RIGHT, 133 READ_CHANNEL_LFE, READ_CHANNEL_REAR_LEFT, READ_CHANNEL_REAR_RIGHT>}, 134 // Surround51 135 {CUBEB_LAYOUT_3F2_LFE_BACK, 136 &SampleReaderImpl<AudioExpansionMode::Surround51, READ_CHANNEL_FRONT_LEFT, READ_CHANNEL_FRONT_RIGHT, 137 READ_CHANNEL_FRONT_CENTER, READ_CHANNEL_LFE, READ_CHANNEL_REAR_LEFT, READ_CHANNEL_REAR_RIGHT>}, 138 // Surround71 139 {CUBEB_LAYOUT_3F4_LFE, 140 &SampleReaderImpl<AudioExpansionMode::Surround71, READ_CHANNEL_FRONT_LEFT, READ_CHANNEL_FRONT_RIGHT, 141 READ_CHANNEL_FRONT_CENTER, READ_CHANNEL_LFE, READ_CHANNEL_REAR_LEFT, READ_CHANNEL_REAR_RIGHT, 142 READ_CHANNEL_SIDE_LEFT, READ_CHANNEL_SIDE_RIGHT>}, 143 }}; 144 145 cubeb_stream_params params = {}; 146 params.format = CUBEB_SAMPLE_S16LE; 147 params.rate = m_sample_rate; 148 params.channels = m_output_channels; 149 params.layout = channel_setups[static_cast<size_t>(m_parameters.expansion_mode)].first; 150 params.prefs = CUBEB_STREAM_PREF_NONE; 151 152 u32 latency_frames = GetBufferSizeForMS( 153 m_sample_rate, (m_parameters.output_latency_ms == 0) ? m_parameters.buffer_ms : m_parameters.output_latency_ms); 154 u32 min_latency_frames = 0; 155 rv = cubeb_get_min_latency(m_context, ¶ms, &min_latency_frames); 156 if (rv == CUBEB_ERROR_NOT_SUPPORTED) 157 { 158 DEV_LOG("Cubeb backend does not support latency queries, using latency of {} ms ({} frames).", 159 m_parameters.buffer_ms, latency_frames); 160 } 161 else 162 { 163 if (rv != CUBEB_OK) 164 { 165 Error::SetStringFmt(error, "cubeb_get_min_latency() failed: {}", GetCubebErrorString(rv)); 166 DestroyContextAndStream(); 167 return false; 168 } 169 170 const u32 minimum_latency_ms = GetMSForBufferSize(m_sample_rate, min_latency_frames); 171 DEV_LOG("Minimum latency: {} ms ({} audio frames)", minimum_latency_ms, min_latency_frames); 172 if (m_parameters.output_latency_minimal) 173 { 174 // use minimum 175 latency_frames = min_latency_frames; 176 } 177 else if (minimum_latency_ms > m_parameters.output_latency_ms) 178 { 179 WARNING_LOG("Minimum latency is above requested latency: {} vs {}, adjusting to compensate.", min_latency_frames, 180 latency_frames); 181 latency_frames = min_latency_frames; 182 } 183 } 184 185 cubeb_devid selected_device = nullptr; 186 const std::string& selected_device_name = g_settings.audio_output_device; 187 cubeb_device_collection devices; 188 bool devices_valid = false; 189 if (!selected_device_name.empty()) 190 { 191 rv = cubeb_enumerate_devices(m_context, CUBEB_DEVICE_TYPE_OUTPUT, &devices); 192 devices_valid = (rv == CUBEB_OK); 193 if (rv == CUBEB_OK) 194 { 195 for (size_t i = 0; i < devices.count; i++) 196 { 197 const cubeb_device_info& di = devices.device[i]; 198 if (di.device_id && selected_device_name == di.device_id) 199 { 200 INFO_LOG("Using output device '{}' ({}).", di.device_id, di.friendly_name ? di.friendly_name : di.device_id); 201 selected_device = di.devid; 202 break; 203 } 204 } 205 206 if (!selected_device) 207 { 208 Host::AddOSDMessage( 209 fmt::format("Requested audio output device '{}' not found, using default.", selected_device_name), 10.0f); 210 } 211 } 212 else 213 { 214 WARNING_LOG("cubeb_enumerate_devices() returned {}, using default device.", GetCubebErrorString(rv)); 215 } 216 } 217 218 BaseInitialize(channel_setups[static_cast<size_t>(m_parameters.expansion_mode)].second); 219 220 char stream_name[32]; 221 std::snprintf(stream_name, sizeof(stream_name), "%p", this); 222 223 rv = cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, selected_device, ¶ms, latency_frames, 224 &CubebAudioStream::DataCallback, StateCallback, this); 225 226 if (devices_valid) 227 cubeb_device_collection_destroy(m_context, &devices); 228 229 if (rv != CUBEB_OK) 230 { 231 Error::SetStringFmt(error, "cubeb_stream_init() failed: {}", GetCubebErrorString(rv)); 232 DestroyContextAndStream(); 233 return false; 234 } 235 236 rv = cubeb_stream_start(stream); 237 if (rv != CUBEB_OK) 238 { 239 Error::SetStringFmt(error, "cubeb_stream_start() failed: {}", GetCubebErrorString(rv)); 240 DestroyContextAndStream(); 241 return false; 242 } 243 244 return true; 245 } 246 247 void CubebAudioStream::StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state) 248 { 249 // noop 250 } 251 252 long CubebAudioStream::DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer, 253 long nframes) 254 { 255 static_cast<CubebAudioStream*>(user_ptr)->ReadFrames(static_cast<s16*>(output_buffer), static_cast<u32>(nframes)); 256 return nframes; 257 } 258 259 void CubebAudioStream::SetPaused(bool paused) 260 { 261 if (paused == m_paused || !stream) 262 return; 263 264 const int rv = paused ? cubeb_stream_stop(stream) : cubeb_stream_start(stream); 265 if (rv != CUBEB_OK) 266 { 267 ERROR_LOG("Could not {} stream: {}", paused ? "pause" : "resume", rv); 268 return; 269 } 270 271 m_paused = paused; 272 } 273 274 std::unique_ptr<AudioStream> AudioStream::CreateCubebAudioStream(u32 sample_rate, 275 const AudioStreamParameters& parameters, 276 const char* driver_name, const char* device_name, 277 Error* error) 278 { 279 std::unique_ptr<CubebAudioStream> stream = std::make_unique<CubebAudioStream>(sample_rate, parameters); 280 if (!stream->Initialize(driver_name, device_name, error)) 281 stream.reset(); 282 return stream; 283 } 284 285 std::vector<std::pair<std::string, std::string>> AudioStream::GetCubebDriverNames() 286 { 287 std::vector<std::pair<std::string, std::string>> names; 288 names.emplace_back(std::string(), TRANSLATE_STR("AudioStream", "Default")); 289 290 const char** cubeb_names = cubeb_get_backend_names(); 291 for (u32 i = 0; cubeb_names[i] != nullptr; i++) 292 names.emplace_back(cubeb_names[i], cubeb_names[i]); 293 return names; 294 } 295 296 std::vector<AudioStream::DeviceInfo> AudioStream::GetCubebOutputDevices(const char* driver, u32 sample_rate) 297 { 298 std::vector<AudioStream::DeviceInfo> ret; 299 ret.emplace_back(std::string(), TRANSLATE_STR("AudioStream", "Default"), 0); 300 301 cubeb* context; 302 int rv = cubeb_init(&context, "DuckStation", (driver && *driver) ? driver : nullptr); 303 if (rv != CUBEB_OK) 304 { 305 ERROR_LOG("cubeb_init() failed: {}", GetCubebErrorString(rv)); 306 return ret; 307 } 308 309 ScopedGuard context_cleanup([context]() { cubeb_destroy(context); }); 310 311 cubeb_device_collection devices; 312 rv = cubeb_enumerate_devices(context, CUBEB_DEVICE_TYPE_OUTPUT, &devices); 313 if (rv != CUBEB_OK) 314 { 315 ERROR_LOG("cubeb_enumerate_devices() failed: {}", GetCubebErrorString(rv)); 316 return ret; 317 } 318 319 ScopedGuard devices_cleanup([context, &devices]() { cubeb_device_collection_destroy(context, &devices); }); 320 321 // we need stream parameters to query latency 322 cubeb_stream_params params = {}; 323 params.format = CUBEB_SAMPLE_S16LE; 324 params.rate = sample_rate; 325 params.channels = 2; 326 params.layout = CUBEB_LAYOUT_UNDEFINED; 327 params.prefs = CUBEB_STREAM_PREF_NONE; 328 329 u32 min_latency = 0; 330 cubeb_get_min_latency(context, ¶ms, &min_latency); 331 ret[0].minimum_latency_frames = min_latency; 332 333 for (size_t i = 0; i < devices.count; i++) 334 { 335 const cubeb_device_info& di = devices.device[i]; 336 if (!di.device_id) 337 continue; 338 339 ret.emplace_back(di.device_id, di.friendly_name ? di.friendly_name : di.device_id, min_latency); 340 } 341 342 return ret; 343 }