Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/util/cubeb_audio_stream.cpp
4212 views
1
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: 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_CHANNEL(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
ret = "CUBEB_ERROR_UNKNOWN";
63
break;
64
65
#undef C
66
}
67
68
ret.append_format(" ({})", rv);
69
return ret;
70
}
71
72
CubebAudioStream::CubebAudioStream(u32 sample_rate, const AudioStreamParameters& parameters)
73
: AudioStream(sample_rate, parameters)
74
{
75
}
76
77
CubebAudioStream::~CubebAudioStream()
78
{
79
DestroyContextAndStream();
80
}
81
82
void CubebAudioStream::LogCallback(const char* fmt, ...)
83
{
84
LargeString str;
85
std::va_list ap;
86
va_start(ap, fmt);
87
str.vsprintf(fmt, ap);
88
va_end(ap);
89
DEV_LOG(str);
90
}
91
92
void CubebAudioStream::DestroyContextAndStream()
93
{
94
if (stream)
95
{
96
cubeb_stream_stop(stream);
97
cubeb_stream_destroy(stream);
98
stream = nullptr;
99
}
100
101
if (m_context)
102
{
103
cubeb_destroy(m_context);
104
m_context = nullptr;
105
}
106
}
107
108
bool CubebAudioStream::Initialize(const char* driver_name, const char* device_name, Error* error)
109
{
110
cubeb_set_log_callback(CUBEB_LOG_NORMAL, LogCallback);
111
112
int rv =
113
cubeb_init(&m_context, "DuckStation", g_settings.audio_driver.empty() ? nullptr : g_settings.audio_driver.c_str());
114
if (rv != CUBEB_OK)
115
{
116
Error::SetStringFmt(error, "Could not initialize cubeb context: {}", GetCubebErrorString(rv));
117
return false;
118
}
119
120
cubeb_stream_params params = {};
121
params.format = CUBEB_SAMPLE_S16LE;
122
params.rate = m_sample_rate;
123
params.channels = NUM_CHANNELS;
124
params.layout = CUBEB_LAYOUT_STEREO;
125
params.prefs = CUBEB_STREAM_PREF_NONE;
126
127
u32 latency_frames = GetBufferSizeForMS(
128
m_sample_rate, (m_parameters.output_latency_ms == 0) ? m_parameters.buffer_ms : m_parameters.output_latency_ms);
129
u32 min_latency_frames = 0;
130
rv = cubeb_get_min_latency(m_context, &params, &min_latency_frames);
131
if (rv == CUBEB_ERROR_NOT_SUPPORTED)
132
{
133
DEV_LOG("Cubeb backend does not support latency queries, using latency of {} ms ({} frames).",
134
m_parameters.buffer_ms, latency_frames);
135
}
136
else
137
{
138
if (rv != CUBEB_OK)
139
{
140
Error::SetStringFmt(error, "cubeb_get_min_latency() failed: {}", GetCubebErrorString(rv));
141
DestroyContextAndStream();
142
return false;
143
}
144
145
const u32 minimum_latency_ms = GetMSForBufferSize(m_sample_rate, min_latency_frames);
146
DEV_LOG("Minimum latency: {} ms ({} audio frames)", minimum_latency_ms, min_latency_frames);
147
if (m_parameters.output_latency_minimal)
148
{
149
// use minimum
150
latency_frames = min_latency_frames;
151
}
152
else if (minimum_latency_ms > m_parameters.output_latency_ms)
153
{
154
WARNING_LOG("Minimum latency is above requested latency: {} vs {}, adjusting to compensate.", min_latency_frames,
155
latency_frames);
156
latency_frames = min_latency_frames;
157
}
158
}
159
160
cubeb_devid selected_device = nullptr;
161
const std::string& selected_device_name = g_settings.audio_output_device;
162
cubeb_device_collection devices;
163
bool devices_valid = false;
164
if (!selected_device_name.empty())
165
{
166
rv = cubeb_enumerate_devices(m_context, CUBEB_DEVICE_TYPE_OUTPUT, &devices);
167
devices_valid = (rv == CUBEB_OK);
168
if (rv == CUBEB_OK)
169
{
170
for (size_t i = 0; i < devices.count; i++)
171
{
172
const cubeb_device_info& di = devices.device[i];
173
if (di.device_id && selected_device_name == di.device_id)
174
{
175
INFO_LOG("Using output device '{}' ({}).", di.device_id, di.friendly_name ? di.friendly_name : di.device_id);
176
selected_device = di.devid;
177
break;
178
}
179
}
180
181
if (!selected_device)
182
{
183
Host::AddOSDMessage(
184
fmt::format("Requested audio output device '{}' not found, using default.", selected_device_name), 10.0f);
185
}
186
}
187
else
188
{
189
WARNING_LOG("cubeb_enumerate_devices() returned {}, using default device.", GetCubebErrorString(rv));
190
}
191
}
192
193
BaseInitialize();
194
195
char stream_name[32];
196
std::snprintf(stream_name, sizeof(stream_name), "%p", this);
197
198
rv = cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, selected_device, &params, latency_frames,
199
&CubebAudioStream::DataCallback, StateCallback, this);
200
201
if (devices_valid)
202
cubeb_device_collection_destroy(m_context, &devices);
203
204
if (rv != CUBEB_OK)
205
{
206
Error::SetStringFmt(error, "cubeb_stream_init() failed: {}", GetCubebErrorString(rv));
207
DestroyContextAndStream();
208
return false;
209
}
210
211
rv = cubeb_stream_start(stream);
212
if (rv != CUBEB_OK)
213
{
214
Error::SetStringFmt(error, "cubeb_stream_start() failed: {}", GetCubebErrorString(rv));
215
DestroyContextAndStream();
216
return false;
217
}
218
219
return true;
220
}
221
222
void CubebAudioStream::StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state)
223
{
224
// noop
225
}
226
227
long CubebAudioStream::DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer,
228
long nframes)
229
{
230
static_cast<CubebAudioStream*>(user_ptr)->ReadFrames(static_cast<s16*>(output_buffer), static_cast<u32>(nframes));
231
return nframes;
232
}
233
234
void CubebAudioStream::SetPaused(bool paused)
235
{
236
if (paused == m_paused || !stream)
237
return;
238
239
const int rv = paused ? cubeb_stream_stop(stream) : cubeb_stream_start(stream);
240
if (rv != CUBEB_OK)
241
{
242
ERROR_LOG("Could not {} stream: {}", paused ? "pause" : "resume", rv);
243
return;
244
}
245
246
m_paused = paused;
247
}
248
249
std::unique_ptr<AudioStream> AudioStream::CreateCubebAudioStream(u32 sample_rate,
250
const AudioStreamParameters& parameters,
251
const char* driver_name, const char* device_name,
252
Error* error)
253
{
254
std::unique_ptr<CubebAudioStream> stream = std::make_unique<CubebAudioStream>(sample_rate, parameters);
255
if (!stream->Initialize(driver_name, device_name, error))
256
stream.reset();
257
return stream;
258
}
259
260
std::vector<std::pair<std::string, std::string>> AudioStream::GetCubebDriverNames()
261
{
262
std::vector<std::pair<std::string, std::string>> names;
263
names.emplace_back(std::string(), TRANSLATE_STR("AudioStream", "Default"));
264
265
const char** cubeb_names = cubeb_get_backend_names();
266
for (u32 i = 0; cubeb_names[i] != nullptr; i++)
267
names.emplace_back(cubeb_names[i], cubeb_names[i]);
268
return names;
269
}
270
271
std::vector<AudioStream::DeviceInfo> AudioStream::GetCubebOutputDevices(const char* driver, u32 sample_rate)
272
{
273
std::vector<AudioStream::DeviceInfo> ret;
274
ret.emplace_back(std::string(), TRANSLATE_STR("AudioStream", "Default"), 0);
275
276
cubeb* context;
277
int rv = cubeb_init(&context, "DuckStation", (driver && *driver) ? driver : nullptr);
278
if (rv != CUBEB_OK)
279
{
280
ERROR_LOG("cubeb_init() failed: {}", GetCubebErrorString(rv));
281
return ret;
282
}
283
284
ScopedGuard context_cleanup([context]() { cubeb_destroy(context); });
285
286
cubeb_device_collection devices;
287
rv = cubeb_enumerate_devices(context, CUBEB_DEVICE_TYPE_OUTPUT, &devices);
288
if (rv != CUBEB_OK)
289
{
290
ERROR_LOG("cubeb_enumerate_devices() failed: {}", GetCubebErrorString(rv));
291
return ret;
292
}
293
294
ScopedGuard devices_cleanup([context, &devices]() { cubeb_device_collection_destroy(context, &devices); });
295
296
// we need stream parameters to query latency
297
cubeb_stream_params params = {};
298
params.format = CUBEB_SAMPLE_S16LE;
299
params.rate = sample_rate;
300
params.channels = 2;
301
params.layout = CUBEB_LAYOUT_UNDEFINED;
302
params.prefs = CUBEB_STREAM_PREF_NONE;
303
304
u32 min_latency = 0;
305
cubeb_get_min_latency(context, &params, &min_latency);
306
ret[0].minimum_latency_frames = min_latency;
307
308
for (size_t i = 0; i < devices.count; i++)
309
{
310
const cubeb_device_info& di = devices.device[i];
311
if (!di.device_id)
312
continue;
313
314
ret.emplace_back(di.device_id, di.friendly_name ? di.friendly_name : di.device_id, min_latency);
315
}
316
317
return ret;
318
}
319
320