Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/util/cubeb_audio_stream.cpp
7365 views
1
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#include "imgui_manager.h"
5
#include "translation.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/string_util.h"
13
14
#include "cubeb/cubeb.h"
15
#include "fmt/format.h"
16
17
#include <mutex>
18
#include <string>
19
20
LOG_CHANNEL(AudioStream);
21
22
namespace {
23
24
struct CubebContextHolder
25
{
26
std::mutex mutex;
27
cubeb* context = nullptr;
28
u32 reference_count = 0;
29
std::string driver_name;
30
};
31
32
class CubebAudioStream final : public AudioStream
33
{
34
public:
35
CubebAudioStream();
36
~CubebAudioStream() override;
37
38
bool Initialize(u32 sample_rate, u32 channels, u32 output_latency_frames, bool output_latency_minimal,
39
std::string_view driver_name, std::string_view device_name, AudioStreamSource* source,
40
bool auto_start, Error* error);
41
42
bool Start(Error* error) override;
43
bool Stop(Error* error) override;
44
45
private:
46
static long DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer,
47
long nframes);
48
static void StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state);
49
50
cubeb* m_context = nullptr;
51
cubeb_stream* stream = nullptr;
52
};
53
} // namespace
54
55
static CubebContextHolder s_cubeb_context;
56
57
static void FormatCubebError(Error* error, const char* prefix, int rv)
58
{
59
const char* str;
60
switch (rv)
61
{
62
// clang-format off
63
#define C(e) case e: str = #e; break
64
// clang-format on
65
66
C(CUBEB_OK);
67
C(CUBEB_ERROR);
68
C(CUBEB_ERROR_INVALID_FORMAT);
69
C(CUBEB_ERROR_INVALID_PARAMETER);
70
C(CUBEB_ERROR_NOT_SUPPORTED);
71
C(CUBEB_ERROR_DEVICE_UNAVAILABLE);
72
73
default:
74
str = "CUBEB_ERROR_UNKNOWN";
75
break;
76
77
#undef C
78
}
79
80
Error::SetStringFmt(error, "{}: {} ({})", prefix, str, rv);
81
}
82
83
static void CubebLogCallback(const char* fmt, ...)
84
{
85
if (!Log::IsLogVisible(Log::Level::Dev, Log::Channel::AudioStream))
86
return;
87
88
LargeString str;
89
std::va_list ap;
90
va_start(ap, fmt);
91
str.vsprintf(fmt, ap);
92
va_end(ap);
93
DEV_LOG(str);
94
}
95
96
static cubeb* GetCubebContext(std::string_view driver_name, Error* error)
97
{
98
std::lock_guard<std::mutex> lock(s_cubeb_context.mutex);
99
if (s_cubeb_context.context)
100
{
101
// Check if the requested driver/device matches the existing context.
102
if (driver_name != s_cubeb_context.driver_name)
103
ERROR_LOG("Cubeb context initialized with driver {}, but requested {}", driver_name, s_cubeb_context.driver_name);
104
105
s_cubeb_context.reference_count++;
106
return s_cubeb_context.context;
107
}
108
109
Assert(s_cubeb_context.reference_count == 0);
110
111
INFO_LOG("Creating Cubeb context with {} driver...", driver_name.empty() ? std::string_view("default") : driver_name);
112
cubeb_set_log_callback(CUBEB_LOG_NORMAL, CubebLogCallback);
113
114
std::string driver_name_str = std::string(driver_name);
115
const int rv =
116
cubeb_init(&s_cubeb_context.context, "DuckStation", driver_name_str.empty() ? nullptr : driver_name_str.c_str());
117
if (rv != CUBEB_OK)
118
{
119
FormatCubebError(error, "Could not initialize cubeb context: ", rv);
120
return nullptr;
121
}
122
123
s_cubeb_context.driver_name = std::move(driver_name_str);
124
s_cubeb_context.reference_count = 1;
125
return s_cubeb_context.context;
126
}
127
128
static void ReleaseCubebContext(cubeb* ctx)
129
{
130
std::lock_guard<std::mutex> lock(s_cubeb_context.mutex);
131
AssertMsg(s_cubeb_context.context == ctx, "Cubeb context mismatch on release.");
132
Assert(s_cubeb_context.reference_count > 0);
133
s_cubeb_context.reference_count--;
134
if (s_cubeb_context.reference_count > 0)
135
return;
136
137
VERBOSE_LOG("Destroying Cubeb context...");
138
cubeb_destroy(s_cubeb_context.context);
139
s_cubeb_context.context = nullptr;
140
s_cubeb_context.driver_name = {};
141
}
142
143
CubebAudioStream::CubebAudioStream() = default;
144
145
CubebAudioStream::~CubebAudioStream()
146
{
147
if (stream)
148
{
149
cubeb_stream_stop(stream);
150
cubeb_stream_destroy(stream);
151
stream = nullptr;
152
}
153
154
if (m_context)
155
ReleaseCubebContext(m_context);
156
}
157
158
bool CubebAudioStream::Initialize(u32 sample_rate, u32 channels, u32 output_latency_frames, bool output_latency_minimal,
159
std::string_view driver_name, std::string_view device_name, AudioStreamSource* source,
160
bool auto_start, Error* error)
161
{
162
static constexpr std::array channel_layout_mapping = {
163
CUBEB_LAYOUT_UNDEFINED, // 0
164
CUBEB_LAYOUT_MONO, // 1
165
CUBEB_LAYOUT_STEREO, // 2
166
CUBEB_LAYOUT_STEREO_LFE, // 3
167
CUBEB_LAYOUT_QUAD, // 4
168
CUBEB_LAYOUT_QUAD_LFE, // 5
169
CUBEB_LAYOUT_3F2_BACK, // 6
170
CUBEB_LAYOUT_3F3R_LFE, // 7
171
CUBEB_LAYOUT_3F4_LFE, // 8
172
};
173
174
if (channels >= channel_layout_mapping.size())
175
{
176
Error::SetStringFmt(error, "Unsupported channel count: {}", channels);
177
return false;
178
}
179
180
m_context = GetCubebContext(driver_name, error);
181
if (!m_context)
182
return false;
183
184
cubeb_stream_params params = {};
185
params.format = CUBEB_SAMPLE_S16LE;
186
params.rate = sample_rate;
187
params.channels = channels;
188
params.layout = channel_layout_mapping[channels];
189
params.prefs = CUBEB_STREAM_PREF_NONE;
190
191
u32 min_latency_frames = 0;
192
int rv = cubeb_get_min_latency(m_context, &params, &min_latency_frames);
193
if (rv == CUBEB_ERROR_NOT_SUPPORTED)
194
{
195
DEV_LOG("Cubeb backend does not support latency queries, using latency of {} ms ({} frames).",
196
FramesToMS(sample_rate, output_latency_frames), output_latency_frames);
197
}
198
else
199
{
200
if (rv != CUBEB_OK)
201
{
202
FormatCubebError(error, "cubeb_get_min_latency() failed: {}", rv);
203
return false;
204
}
205
206
if (output_latency_minimal)
207
{
208
// use minimum
209
output_latency_frames = min_latency_frames;
210
}
211
else if (min_latency_frames > output_latency_frames)
212
{
213
WARNING_LOG("Minimum latency is above requested latency: {} vs {}, adjusting to compensate.", min_latency_frames,
214
output_latency_frames);
215
output_latency_frames = min_latency_frames;
216
}
217
}
218
219
DEV_LOG("Output latency: {} ms ({} audio frames)", FramesToMS(sample_rate, output_latency_frames),
220
min_latency_frames);
221
222
cubeb_devid selected_device = nullptr;
223
cubeb_device_collection devices;
224
bool devices_valid = false;
225
if (!device_name.empty())
226
{
227
rv = cubeb_enumerate_devices(m_context, CUBEB_DEVICE_TYPE_OUTPUT, &devices);
228
devices_valid = (rv == CUBEB_OK);
229
if (rv == CUBEB_OK)
230
{
231
for (size_t i = 0; i < devices.count; i++)
232
{
233
const cubeb_device_info& di = devices.device[i];
234
if (di.device_id && device_name == di.device_id)
235
{
236
INFO_LOG("Using output device '{}' ({}).", di.device_id, di.friendly_name ? di.friendly_name : di.device_id);
237
selected_device = di.devid;
238
break;
239
}
240
}
241
242
if (!selected_device)
243
{
244
Host::AddOSDMessage(OSDMessageType::Error,
245
fmt::format("Requested audio output device '{}' not found, using default.", device_name));
246
}
247
}
248
else
249
{
250
Error enumerate_error;
251
FormatCubebError(&enumerate_error, "cubeb_enumerate_devices() failed: ", rv);
252
WARNING_LOG("{}, using default device.", enumerate_error.GetDescription());
253
}
254
}
255
256
char stream_name[32];
257
std::snprintf(stream_name, sizeof(stream_name), "%p", this);
258
259
rv =
260
cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, selected_device, &params,
261
output_latency_frames, &CubebAudioStream::DataCallback, &CubebAudioStream::StateCallback, source);
262
263
if (devices_valid)
264
cubeb_device_collection_destroy(m_context, &devices);
265
266
if (rv != CUBEB_OK)
267
{
268
FormatCubebError(error, "cubeb_stream_init() failed: ", rv);
269
return false;
270
}
271
272
if (auto_start)
273
{
274
rv = cubeb_stream_start(stream);
275
if (rv != CUBEB_OK)
276
{
277
FormatCubebError(error, "cubeb_stream_start() failed: ", rv);
278
return false;
279
}
280
}
281
282
return true;
283
}
284
285
void CubebAudioStream::StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state)
286
{
287
// noop
288
}
289
290
long CubebAudioStream::DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer,
291
long nframes)
292
{
293
static_cast<AudioStreamSource*>(user_ptr)->ReadFrames(static_cast<s16*>(output_buffer), static_cast<u32>(nframes));
294
return nframes;
295
}
296
297
bool CubebAudioStream::Start(Error* error)
298
{
299
const int rv = cubeb_stream_start(stream);
300
if (rv != CUBEB_OK)
301
{
302
FormatCubebError(error, "cubeb_stream_start() failed: ", rv);
303
return false;
304
}
305
306
return true;
307
}
308
309
bool CubebAudioStream::Stop(Error* error)
310
{
311
const int rv = cubeb_stream_stop(stream);
312
if (rv != CUBEB_OK)
313
{
314
FormatCubebError(error, "cubeb_stream_stop() failed: ", rv);
315
return false;
316
}
317
318
return true;
319
}
320
321
std::unique_ptr<AudioStream> AudioStream::CreateCubebAudioStream(
322
u32 sample_rate, u32 channels, u32 output_latency_frames, bool output_latency_minimal, std::string_view driver_name,
323
std::string_view device_name, AudioStreamSource* source, bool auto_start, Error* error)
324
{
325
std::unique_ptr<CubebAudioStream> stream = std::make_unique<CubebAudioStream>();
326
if (!stream->Initialize(sample_rate, channels, output_latency_frames, output_latency_minimal, driver_name,
327
device_name, source, auto_start, error))
328
{
329
stream.reset();
330
}
331
332
return stream;
333
}
334
335
std::vector<std::pair<std::string, std::string>> AudioStream::GetCubebDriverNames()
336
{
337
std::vector<std::pair<std::string, std::string>> names;
338
names.emplace_back(std::string(), TRANSLATE_STR("AudioStream", "Default"));
339
340
const char** cubeb_names = cubeb_get_backend_names();
341
for (u32 i = 0; cubeb_names[i] != nullptr; i++)
342
names.emplace_back(cubeb_names[i], cubeb_names[i]);
343
return names;
344
}
345
346
std::vector<AudioStream::DeviceInfo> AudioStream::GetCubebOutputDevices(std::string_view driver, u32 sample_rate)
347
{
348
Error error;
349
350
std::vector<AudioStream::DeviceInfo> ret;
351
ret.emplace_back(std::string(), TRANSLATE_STR("AudioStream", "Default"), 0);
352
353
cubeb* context;
354
TinyString driver_str(driver);
355
int rv = cubeb_init(&context, "DuckStation", driver_str.empty() ? nullptr : driver_str.c_str());
356
if (rv != CUBEB_OK)
357
{
358
FormatCubebError(&error, "cubeb_init() failed: ", rv);
359
ERROR_LOG(error.GetDescription());
360
return ret;
361
}
362
363
cubeb_device_collection devices;
364
rv = cubeb_enumerate_devices(context, CUBEB_DEVICE_TYPE_OUTPUT, &devices);
365
if (rv != CUBEB_OK)
366
{
367
FormatCubebError(&error, "cubeb_enumerate_devices() failed: ", rv);
368
ERROR_LOG(error.GetDescription());
369
cubeb_destroy(context);
370
return ret;
371
}
372
373
// we need stream parameters to query latency
374
cubeb_stream_params params = {};
375
params.format = CUBEB_SAMPLE_S16LE;
376
params.rate = sample_rate;
377
params.channels = 2;
378
params.layout = CUBEB_LAYOUT_UNDEFINED;
379
params.prefs = CUBEB_STREAM_PREF_NONE;
380
381
u32 min_latency = 0;
382
cubeb_get_min_latency(context, &params, &min_latency);
383
ret[0].minimum_latency_frames = min_latency;
384
385
for (size_t i = 0; i < devices.count; i++)
386
{
387
const cubeb_device_info& di = devices.device[i];
388
if (!di.device_id)
389
continue;
390
391
ret.emplace_back(di.device_id, di.friendly_name ? di.friendly_name : di.device_id, min_latency);
392
}
393
394
cubeb_device_collection_destroy(context, &devices);
395
cubeb_destroy(context);
396
return ret;
397
}
398
399