-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathQRetroAudio.cpp
More file actions
370 lines (320 loc) · 10.2 KB
/
Copy pathQRetroAudio.cpp
File metadata and controls
370 lines (320 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
#include <QtGlobal>
#if QRETRO_HAVE_MULTIMEDIA
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <QAudioDevice>
#include <QAudioSink>
#include <QMediaDevices>
#else
#include <QAudioDeviceInfo>
#include <QAudioOutput>
#endif
#endif
#include "QRetroAudio.h"
#include "libretro.h"
#include <cmath>
/**
* The number of channels the implementation plays audio through by default.
* As of now, libretro supports exactly two. If this is ever expanded, this
* value should be made mutable.
*/
#define QRETRO_AUDIO_CHANNELS 2
/* How long the queue may fail to drain before the device is presumed stuck. */
#define QRETRO_AUDIO_NO_DRAIN_MS 250
/**
* Linearly resamples interleaved frames, reading `ratio` input frames per output
* frame. `phase` carries the fractional read position between calls so blocks
* join without a seam. Returns the number of frames written.
*/
static size_t qretro_resample(const sample_t *in, size_t in_frames, sample_t *out, size_t out_max,
double ratio, double *phase, sample_t *prev)
{
size_t written = 0;
double pos = *phase;
unsigned c;
while (written < out_max && pos < static_cast<double>(in_frames))
{
const size_t i = static_cast<size_t>(pos);
const double frac = pos - static_cast<double>(i);
for (c = 0; c < QRETRO_AUDIO_CHANNELS; c++)
{
/* Position 0 sits on the frame carried over from the last call, so blocks
* join without a seam. */
const double a = i ? in[(i - 1) * QRETRO_AUDIO_CHANNELS + c] : prev[c];
const double b = in[i * QRETRO_AUDIO_CHANNELS + c];
out[written * QRETRO_AUDIO_CHANNELS + c] = static_cast<sample_t>(a + (b - a) * frac);
}
written++;
pos += ratio;
}
if (in_frames)
{
for (c = 0; c < QRETRO_AUDIO_CHANNELS; c++)
prev[c] = in[(in_frames - 1) * QRETRO_AUDIO_CHANNELS + c];
pos -= static_cast<double>(in_frames);
}
*phase = pos < 0.0 ? 0.0 : pos;
return written;
}
QRetroAudio::QRetroAudio(void)
{
m_SampleRateBase = m_TargetSampleRate;
m_FramesPerSecond = 60.0;
setTimingMultiplier(1.0);
}
QRetroAudio::QRetroAudio(double frequency, double core_fps)
{
if (frequency < 1.0)
frequency = m_TargetSampleRate;
m_FramesPerSecond = core_fps;
setTimingMultiplier(1.0);
}
QRetroAudio::QRetroAudio(double frequency, double core_fps, double emu_fps)
{
double mult = emu_fps / core_fps;
if (frequency < 1.0)
frequency = m_TargetSampleRate;
m_SampleRateBase = frequency;
m_FramesPerSecond = emu_fps;
/*
TODO: We can't play samples faster than they arrive, but we need a better
solution.
*/
setTimingMultiplier(mult);
}
QRetroAudio::~QRetroAudio(void)
{
#if QRETRO_HAVE_MULTIMEDIA
if (m_AudioOutput)
{
m_AudioOutput->stop();
delete m_AudioOutput;
}
#endif
}
int QRetroAudio::framesInBuffer(void)
{
#if QRETRO_HAVE_MULTIMEDIA
/* Without a device nothing can drain the queue, so report it empty rather than
* let emulation be held on audio that will never play. */
if (m_AudioOutput && m_AudioDevice)
return m_AudioBuffer.size() / m_SampleRateBytesPerFrame;
#endif
return 0;
}
int QRetroAudio::excessFramesInBuffer(void)
{
int result = framesInBuffer() - m_BufferFrames;
return (result > 0) ? result : 0;
}
void QRetroAudio::playFrame(void)
{
#if QRETRO_HAVE_MULTIMEDIA
if (m_AudioOutput && m_AudioDevice && m_AudioBuffer.size() >= m_SampleRateBytesPerFrame &&
m_AudioOutput->bytesFree() >= m_SampleRateBytesPerFrame)
{
m_AudioDevice->write(m_AudioBuffer.data(), m_SampleRateBytesPerFrame);
m_AudioBuffer.remove(0, m_SampleRateBytesPerFrame);
}
#endif
}
bool QRetroAudio::shouldStallEmulation(void)
{
#if QRETRO_HAVE_MULTIMEDIA
int frames = excessFramesInBuffer();
qint64 played;
if (!frames)
{
m_StallTimer.invalidate();
m_Restarted = false;
return false;
}
/* The device is the only thing that empties the queue, so if it has not played
* anything for a while it has stopped, however full the queue looks. */
played = m_AudioOutput ? m_AudioOutput->processedUSecs() : 0;
if (!m_StallTimer.isValid() || played != m_PlayedUSecs)
{
m_PlayedUSecs = played;
m_StallTimer.start();
}
else if (m_StallTimer.hasExpired(QRETRO_AUDIO_NO_DRAIN_MS))
{
emit logMessage(
RETRO_LOG_WARN, QString("Audio device stopped after playing %1 ms, %2 frames queued.")
.arg(played / 1000)
.arg(frames));
m_StallTimer.invalidate();
/* Never hold emulation on a device that is not playing. Try once to get it
* back; after that just keep the queue clear. */
flush();
if (!m_Restarted)
{
m_Restarted = true;
start();
}
return false;
}
return true;
#else
return false;
#endif
}
void QRetroAudio::flush(void)
{
m_AudioBuffer.clear();
m_AudioBuffer.resize(m_BufferFrames * QRETRO_AUDIO_CHANNELS * sizeof(sample_t));
m_AudioBuffer.fill(0);
}
void QRetroAudio::reset(void)
{
#if QRETRO_HAVE_MULTIMEDIA
if (!m_AudioOutput)
return;
m_AudioOutput->reset();
flush();
m_AudioDevice = m_AudioOutput->start();
applyVolume();
#endif
}
void QRetroAudio::pushSamples(const sample_t *data, size_t frames)
{
if (m_ResampleRatio != 1.0)
{
const size_t max = static_cast<size_t>(frames / m_ResampleRatio) + 2;
size_t written;
m_ResampleScratch.resize(static_cast<int>(max * QRETRO_AUDIO_CHANNELS * sizeof(sample_t)));
written = qretro_resample(data, frames, reinterpret_cast<sample_t *>(m_ResampleScratch.data()),
max, m_ResampleRatio, &m_ResamplePhase, m_ResamplePrev);
m_AudioBuffer.append(m_ResampleScratch.constData(),
static_cast<int>(written * QRETRO_AUDIO_CHANNELS * sizeof(sample_t)));
return;
}
m_AudioBuffer.append(reinterpret_cast<const char *>(data),
static_cast<int>(frames * QRETRO_AUDIO_CHANNELS * sizeof(sample_t)));
}
void QRetroAudio::setEnabled(bool v)
{
if (v == m_Enabled)
return;
m_Enabled = v;
applyVolume();
}
void QRetroAudio::setVolume(float v)
{
if (v == m_Volume)
return;
m_Volume = v;
applyVolume();
}
void QRetroAudio::setMute(bool mute)
{
if (mute == m_Muted)
return;
m_Muted = mute;
applyVolume();
}
void QRetroAudio::applyVolume(void)
{
#if QRETRO_HAVE_MULTIMEDIA
if (m_AudioOutput)
m_AudioOutput->setVolume((m_Enabled && !m_Muted) ? m_Volume : 0.0f);
#endif
}
void QRetroAudio::setTimingMultiplier(double mult)
{
m_SampleRateCurrent = m_SampleRateBase * mult;
m_SampleRateBytesPerFrame = static_cast<unsigned>(m_SampleRateBase / m_FramesPerSecond) *
sizeof(sample_t) * QRETRO_AUDIO_CHANNELS;
/** @todo check - ensure amount is even (dolphin goes wacky without this) */
m_SampleRateBytesPerFrame &= static_cast<unsigned>(~1);
m_SampleRateMultiplier = mult;
}
bool QRetroAudio::start(void)
{
#if QRETRO_HAVE_MULTIMEDIA
if (m_SampleRateCurrent > 0)
{
QAudioFormat format;
emit logMessage(RETRO_LOG_INFO, QString("Audio: core %1 Hz at %2 fps, timing x%3")
.arg(m_SampleRateBase)
.arg(m_FramesPerSecond)
.arg(m_SampleRateMultiplier));
format.setSampleRate(m_SampleRateCurrent);
format.setChannelCount(QRETRO_AUDIO_CHANNELS);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
format.setSampleFormat(QAudioFormat::Int16);
#else
format.setSampleType(QAudioFormat::SignedInt);
format.setSampleSize(8 * sizeof(sample_t));
format.setCodec("audio/pcm");
#endif
/* Close and free audio handlers if they exist */
if (m_AudioOutput)
{
m_AudioOutput->stop();
delete m_AudioOutput;
m_AudioOutput = nullptr;
}
/* The device belongs to the sink, which just took it with it. */
m_AudioDevice = nullptr;
/* Fill the buffer with dummy data */
m_AudioBuffer.clear();
m_AudioBuffer.resize(m_BufferFrames * QRETRO_AUDIO_CHANNELS * sizeof(sample_t));
m_AudioBuffer.fill(0);
/* Cores ask for rates the device may not take, and an
* unsupported format opens without an error but never plays. */
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
const QAudioDevice info = QMediaDevices::defaultAudioOutput();
const QString name = info.description();
#else
const QAudioDeviceInfo info = QAudioDeviceInfo::defaultOutputDevice();
const QString name = info.deviceName();
#endif
const bool supported = info.isFormatSupported(format);
emit logMessage(supported ? RETRO_LOG_INFO : RETRO_LOG_WARN,
QString("Audio: requesting %1 Hz, %2 ch on \"%3\" (supported: %4, prefers %5 Hz)")
.arg(format.sampleRate())
.arg(format.channelCount())
.arg(name)
.arg(supported ? "yes" : "NO")
.arg(info.preferredFormat().sampleRate()));
m_ResampleRatio = 1.0;
m_ResamplePhase = 0.0;
if (!supported)
{
const int rate = info.preferredFormat().sampleRate();
if (rate > 0)
{
emit logMessage(RETRO_LOG_INFO,
QString("Audio: converting %1 Hz to %2 Hz").arg(format.sampleRate()).arg(rate));
m_ResampleRatio = m_SampleRateCurrent / rate;
format.setSampleRate(rate);
}
}
/* The queue holds device-rate samples now, so pace the drain off that. */
m_SampleRateBytesPerFrame = static_cast<int>(format.sampleRate() / m_FramesPerSecond) *
sizeof(sample_t) * QRETRO_AUDIO_CHANNELS;
m_SampleRateBytesPerFrame &= ~1;
/* Start the new audio output */
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
m_AudioOutput = new QAudioSink(info, format);
#else
m_AudioOutput = new QAudioOutput(info, format);
#endif
m_AudioDevice = m_AudioOutput->start();
applyVolume();
if (m_AudioDevice)
{
emit logMessage(RETRO_LOG_INFO, QString("Audio: started, state %1, %2 bytes free")
.arg(static_cast<int>(m_AudioOutput->state()))
.arg(static_cast<qint64>(m_AudioOutput->bytesFree())));
return true;
}
/* The rate the core wants is not one this device takes */
emit logMessage(RETRO_LOG_ERROR,
QString("Audio: device would not open at %1 Hz, error %2. Continuing without sound.")
.arg(format.sampleRate())
.arg(static_cast<int>(m_AudioOutput->error())));
}
#endif
return false;
}