Skip to content

Commit ec3c95b

Browse files
m0dBm0dB
authored andcommitted
wait for the portaudio finished callback to avoid sporadic deadlocks
1 parent 9786c03 commit ec3c95b

File tree

2 files changed

+52
-23
lines changed

2 files changed

+52
-23
lines changed

src/soundio/sounddeviceportaudio.cpp

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ int paV19CallbackClkRef(const void *inputBuffer, void *outputBuffer,
7171
(const CSAMPLE*) inputBuffer, timeInfo, statusFlags);
7272
}
7373

74+
void paFinishedCallback(void* soundDevice) {
75+
return ((SoundDevicePortAudio*)soundDevice)->finishedCallback();
76+
}
77+
7478
const QRegularExpression kAlsaHwDeviceRegex("(.*) \\((plug)?(hw:(\\d)+(,(\\d)+))?\\)");
7579

7680
const QString kAppGroup = QStringLiteral("[App]");
@@ -360,12 +364,14 @@ SoundDeviceStatus SoundDevicePortAudio::open(bool isClkRefDevice, int syncBuffer
360364
}
361365
#endif
362366

363-
// Start stream
367+
// Set the finished callback, used when stopping the stream
368+
m_bFinished = false;
369+
Pa_SetStreamFinishedCallback(pStream, paFinishedCallback);
364370

365-
// Tell the callback that we are starting but not ready yet.
366-
// The callback will return paContinue but not access m_pStream
367-
// yet.
371+
// Tell the callback to return paContinue, once running.
368372
m_callbackResult.store(paContinue, std::memory_order_release);
373+
374+
// Start stream
369375
err = Pa_StartStream(pStream);
370376
if (err != paNoError) {
371377
qWarning() << "PortAudio: Start stream error:" << Pa_GetErrorText(err);
@@ -407,12 +413,28 @@ bool SoundDevicePortAudio::isOpen() const {
407413
return m_pStream.load() != nullptr;
408414
}
409415

416+
void SoundDevicePortAudio::finishedCallback() {
417+
// This callback is called by PortAudio when the process callback returns
418+
// paAbort to indicate the stream has to finish. This is triggered in the
419+
// SoundDevicePortAudio::close() method.
420+
std::unique_lock lock(m_finishedMutex);
421+
m_bFinished = true;
422+
m_finishedCV.notify_one();
423+
}
424+
425+
void SoundDevicePortAudio::waitForFinished() {
426+
// Wait until finishedCallback has been called. The timeout should never be
427+
// reached, because when the process callback return paAbort, the stream
428+
// will finish as soon as possible.
429+
std::unique_lock lock(m_finishedMutex);
430+
if (!m_finishedCV.wait_for(lock, std::chrono::seconds(1), [&] { return m_bFinished; })) {
431+
qWarning() << "Timeout reached when waiting for PortAudio stream to finish";
432+
}
433+
}
434+
410435
SoundDeviceStatus SoundDevicePortAudio::close() {
411436
//qDebug() << "SoundDevicePortAudio::close()" << m_deviceId;
412437

413-
// Tell the callback to return paAbort. This stops the stream as soon as possible.
414-
// After stopping the stream using this technique, Pa_StopStream() still has to be called.
415-
m_callbackResult.store(paAbort, std::memory_order_release);
416438
PaStream* pStream = m_pStream.load(std::memory_order_relaxed);
417439
if (pStream) {
418440
// Make sure the stream is not stopped before we try stopping it.
@@ -430,27 +452,24 @@ SoundDeviceStatus SoundDevicePortAudio::close() {
430452
return SoundDeviceStatus::Error;
431453
}
432454

433-
// Stop the stream. Since the start of this function we are returning
434-
// paAbort from the callback. paAbort stops the stream as soon as possible.
435-
// When stopping the stream using this technique, we still need to call
436-
// Pa_StopStream() before starting the stream again.
455+
// Tell the process callback to return paAbort, which will cause
456+
// PortAudio to stop the stream as soon as possible. This has the
457+
// advantage over calling Pa_StopStream that it will not wait until all
458+
// the buffers have been flushed, which can take a few (annoying)
459+
// seconds when you're doing soundcard input.
460+
m_callbackResult.store(paAbort, std::memory_order_release);
437461

462+
// We wait until the callback set with Pa_SetStreamFinishedCallback has
463+
// been called. Without this, there is the risk of a deadlock when we
464+
// call Pa_StopStream and Pa_CloseStream below.
465+
waitForFinished();
466+
467+
// Call Pa_StopStream() in case the above failed.
438468
err = Pa_StopStream(pStream);
469+
439470
// We should now be able to safely set m_pStream to null
440471
m_pStream.store(nullptr, std::memory_order_release);
441472

442-
// Note: With the use of return value paAbort as described above,
443-
// the comment below is not relevant anymore, but I am leaving it
444-
// because of the BIG FAT WARNING, just in case.
445-
//
446-
// Trying Pa_AbortStream instead, because StopStream seems to wait
447-
// until all the buffers have been flushed, which can take a
448-
// few (annoying) seconds when you're doing soundcard input.
449-
// (it flushes the input buffer, and then some, or something)
450-
// BIG FAT WARNING: Pa_AbortStream() will kill threads while they're
451-
// waiting on a mutex, which will leave the mutex in an screwy
452-
// state. Don't use it!
453-
454473
if (err != paNoError) {
455474
qWarning() << "PortAudio: Stop stream error:"
456475
<< Pa_GetErrorText(err) << m_deviceId;

src/soundio/sounddeviceportaudio.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <portaudio.h>
44

55
#include <QString>
6+
#include <condition_variable>
67
#include <memory>
78

89
#include "control/pollingcontrolproxy.h"
@@ -47,6 +48,9 @@ class SoundDevicePortAudio : public SoundDevice {
4748
const PaStreamCallbackTimeInfo *timeInfo,
4849
PaStreamCallbackFlags statusFlags);
4950

51+
// Callback called once the process callback returns paAbort.
52+
void finishedCallback();
53+
5054
mixxx::audio::SampleRate getDefaultSampleRate() const override {
5155
return m_deviceInfo ? mixxx::audio::SampleRate::fromDouble(
5256
m_deviceInfo->defaultSampleRate)
@@ -58,6 +62,9 @@ class SoundDevicePortAudio : public SoundDevice {
5862
SINT framesPerBuffer, const PaStreamCallbackTimeInfo* timeInfo);
5963
void updateAudioLatencyUsage(const SINT framesPerBuffer);
6064

65+
// Wait until finishedCallback has been called.
66+
void waitForFinished();
67+
6168
// PortAudio stream for this device.
6269
std::atomic<PaStream*> m_pStream;
6370
// Struct containing information about this device. Don't free() it, it
@@ -85,4 +92,7 @@ class SoundDevicePortAudio : public SoundDevice {
8592
PerformanceTimer m_clkRefTimer;
8693
PaTime m_lastCallbackEntrytoDacSecs;
8794
std::atomic<int> m_callbackResult;
95+
std::mutex m_finishedMutex;
96+
std::condition_variable m_finishedCV;
97+
bool m_bFinished;
8898
};

0 commit comments

Comments
 (0)