From 35cf34de6d5627e878c88ef8c8a5e89b7c72b3be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20M=C3=BCller?= Date: Sat, 27 Mar 2021 12:22:23 +0100 Subject: Fix T86851: PulseAudio randomly asserts in background rendering Upstream fix from Audaspace with simplified PulseAudio code. Maniphest Tasks: T86851 Differential Revision: https://developer.blender.org/D10840 --- extern/audaspace/CMakeLists.txt | 2 + extern/audaspace/include/devices/SoftwareDevice.h | 1 + extern/audaspace/include/devices/ThreadedDevice.h | 95 ++++++++++++ .../plugins/pulseaudio/PulseAudioDevice.cpp | 62 ++++---- .../plugins/pulseaudio/PulseAudioDevice.h | 19 +-- .../plugins/pulseaudio/PulseAudioSymbols.h | 14 +- extern/audaspace/plugins/wasapi/WASAPIDevice.cpp | 169 ++++++--------------- extern/audaspace/plugins/wasapi/WASAPIDevice.h | 29 +--- extern/audaspace/src/devices/SoftwareDevice.cpp | 6 +- extern/audaspace/src/devices/ThreadedDevice.cpp | 65 ++++++++ 10 files changed, 255 insertions(+), 207 deletions(-) create mode 100644 extern/audaspace/include/devices/ThreadedDevice.h create mode 100644 extern/audaspace/src/devices/ThreadedDevice.cpp (limited to 'extern') diff --git a/extern/audaspace/CMakeLists.txt b/extern/audaspace/CMakeLists.txt index fe1bf5dc742..1599c03cbad 100644 --- a/extern/audaspace/CMakeLists.txt +++ b/extern/audaspace/CMakeLists.txt @@ -42,6 +42,7 @@ set(SRC src/devices/NULLDevice.cpp src/devices/ReadDevice.cpp src/devices/SoftwareDevice.cpp + src/devices/ThreadedDevice.cpp src/Exception.cpp src/file/File.cpp src/file/FileManager.cpp @@ -148,6 +149,7 @@ set(PUBLIC_HDR include/devices/NULLDevice.h include/devices/ReadDevice.h include/devices/SoftwareDevice.h + include/devices/ThreadedDevice.h include/Exception.h include/file/File.h include/file/FileManager.h diff --git a/extern/audaspace/include/devices/SoftwareDevice.h b/extern/audaspace/include/devices/SoftwareDevice.h index a350550048b..209be9941b1 100644 --- a/extern/audaspace/include/devices/SoftwareDevice.h +++ b/extern/audaspace/include/devices/SoftwareDevice.h @@ -255,6 +255,7 @@ protected: /** * This function tells the device, to start or pause playback. * \param playing True if device should playback. + * \note This method is only called when the device is locked. */ virtual void playing(bool playing)=0; diff --git a/extern/audaspace/include/devices/ThreadedDevice.h b/extern/audaspace/include/devices/ThreadedDevice.h new file mode 100644 index 00000000000..36c2e68e36f --- /dev/null +++ b/extern/audaspace/include/devices/ThreadedDevice.h @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright 2009-2016 Jörg Müller + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +#pragma once + +/** + * @file ThreadedDevice.h + * @ingroup plugin + * The ThreadedDevice class. + */ + +#include "devices/SoftwareDevice.h" + +#include + +AUD_NAMESPACE_BEGIN + +/** + * This device extends the SoftwareDevice with code for running mixing in a separate thread. + */ +class AUD_PLUGIN_API ThreadedDevice : public SoftwareDevice +{ +private: + /** + * Whether there is currently playback. + */ + bool m_playing; + + /** + * Whether the current playback should stop. + */ + bool m_stop; + + /** + * The streaming thread. + */ + std::thread m_thread; + + /** + * Starts the streaming thread. + */ + AUD_LOCAL void start(); + + /** + * Streaming thread main function. + */ + AUD_LOCAL virtual void runMixingThread()=0; + + // delete copy constructor and operator= + ThreadedDevice(const ThreadedDevice&) = delete; + ThreadedDevice& operator=(const ThreadedDevice&) = delete; + +protected: + virtual void playing(bool playing); + + /** + * Empty default constructor. To setup the device call the function create() + * and to uninitialize call destroy(). + */ + ThreadedDevice(); + + /** + * Indicates that the mixing thread should be stopped. + * \return Whether the mixing thread should be stopping. + * \warning For thread safety, the device needs to be locked, when this method is called. + */ + inline bool shouldStop() { return m_stop; } + + /** + * This method needs to be called when the mixing thread is stopping. + * \warning For thread safety, the device needs to be locked, when this method is called. + */ + inline void doStop() { m_stop = m_playing = false; } + + /** + * Stops all playback and notifies the mixing thread to stop. + * \warning The device has to be unlocked to not run into a deadlock. + */ + void stopMixingThread(); +}; + +AUD_NAMESPACE_END diff --git a/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.cpp b/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.cpp index 0a50d5db2c7..3ffe97661d8 100644 --- a/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.cpp +++ b/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.cpp @@ -27,9 +27,9 @@ void PulseAudioDevice::PulseAudio_state_callback(pa_context *context, void *data { PulseAudioDevice* device = (PulseAudioDevice*)data; - device->m_state = AUD_pa_context_get_state(context); + std::lock_guard lock(*device); - AUD_pa_threaded_mainloop_signal(device->m_mainloop, 0); + device->m_state = AUD_pa_context_get_state(context); } void PulseAudioDevice::PulseAudio_request(pa_stream *stream, size_t num_bytes, void *data) @@ -68,29 +68,40 @@ void PulseAudioDevice::PulseAudio_underflow(pa_stream *stream, void *data) } } -void PulseAudioDevice::playing(bool playing) +void PulseAudioDevice::runMixingThread() { - m_playback = playing; + for(;;) + { + { + std::lock_guard lock(*this); + + if(shouldStop()) + { + AUD_pa_stream_cork(m_stream, 1, nullptr, nullptr); + doStop(); + return; + } + } + + if(AUD_pa_stream_is_corked(m_stream)) + AUD_pa_stream_cork(m_stream, 0, nullptr, nullptr); - AUD_pa_stream_cork(m_stream, playing ? 0 : 1, nullptr, nullptr); + AUD_pa_mainloop_iterate(m_mainloop, true, nullptr); + } } PulseAudioDevice::PulseAudioDevice(std::string name, DeviceSpecs specs, int buffersize) : - m_playback(false), m_state(PA_CONTEXT_UNCONNECTED), m_buffersize(buffersize), m_underflows(0) { - m_mainloop = AUD_pa_threaded_mainloop_new(); + m_mainloop = AUD_pa_mainloop_new(); - AUD_pa_threaded_mainloop_lock(m_mainloop); - - m_context = AUD_pa_context_new(AUD_pa_threaded_mainloop_get_api(m_mainloop), name.c_str()); + m_context = AUD_pa_context_new(AUD_pa_mainloop_get_api(m_mainloop), name.c_str()); if(!m_context) { - AUD_pa_threaded_mainloop_unlock(m_mainloop); - AUD_pa_threaded_mainloop_free(m_mainloop); + AUD_pa_mainloop_free(m_mainloop); AUD_THROW(DeviceException, "Could not connect to PulseAudio."); } @@ -99,26 +110,21 @@ PulseAudioDevice::PulseAudioDevice(std::string name, DeviceSpecs specs, int buff AUD_pa_context_connect(m_context, nullptr, PA_CONTEXT_NOFLAGS, nullptr); - AUD_pa_threaded_mainloop_start(m_mainloop); - while(m_state != PA_CONTEXT_READY) { switch(m_state) { case PA_CONTEXT_FAILED: case PA_CONTEXT_TERMINATED: - AUD_pa_threaded_mainloop_unlock(m_mainloop); - AUD_pa_threaded_mainloop_stop(m_mainloop); - AUD_pa_context_disconnect(m_context); AUD_pa_context_unref(m_context); - AUD_pa_threaded_mainloop_free(m_mainloop); + AUD_pa_mainloop_free(m_mainloop); AUD_THROW(DeviceException, "Could not connect to PulseAudio."); break; default: - AUD_pa_threaded_mainloop_wait(m_mainloop); + AUD_pa_mainloop_iterate(m_mainloop, true, nullptr); break; } } @@ -166,13 +172,10 @@ PulseAudioDevice::PulseAudioDevice(std::string name, DeviceSpecs specs, int buff if(!m_stream) { - AUD_pa_threaded_mainloop_unlock(m_mainloop); - AUD_pa_threaded_mainloop_stop(m_mainloop); - AUD_pa_context_disconnect(m_context); AUD_pa_context_unref(m_context); - AUD_pa_threaded_mainloop_free(m_mainloop); + AUD_pa_mainloop_free(m_mainloop); AUD_THROW(DeviceException, "Could not create PulseAudio stream."); } @@ -188,32 +191,27 @@ PulseAudioDevice::PulseAudioDevice(std::string name, DeviceSpecs specs, int buff buffer_attr.prebuf = -1U; buffer_attr.tlength = buffersize; - if(AUD_pa_stream_connect_playback(m_stream, nullptr, &buffer_attr, static_cast(PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE), nullptr, nullptr) < 0) + if(AUD_pa_stream_connect_playback(m_stream, nullptr, &buffer_attr, static_cast(PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE), nullptr, nullptr) < 0) { - AUD_pa_threaded_mainloop_unlock(m_mainloop); - AUD_pa_threaded_mainloop_stop(m_mainloop); - AUD_pa_context_disconnect(m_context); AUD_pa_context_unref(m_context); - AUD_pa_threaded_mainloop_free(m_mainloop); + AUD_pa_mainloop_free(m_mainloop); AUD_THROW(DeviceException, "Could not connect PulseAudio stream."); } - AUD_pa_threaded_mainloop_unlock(m_mainloop); - create(); } PulseAudioDevice::~PulseAudioDevice() { - AUD_pa_threaded_mainloop_stop(m_mainloop); + stopMixingThread(); AUD_pa_context_disconnect(m_context); AUD_pa_context_unref(m_context); - AUD_pa_threaded_mainloop_free(m_mainloop); + AUD_pa_mainloop_free(m_mainloop); destroy(); } diff --git a/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.h b/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.h index 9efae5128b1..be34cc9032b 100644 --- a/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.h +++ b/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.h @@ -26,7 +26,7 @@ * The PulseAudioDevice class. */ -#include "devices/SoftwareDevice.h" +#include "devices/ThreadedDevice.h" #include @@ -35,15 +35,10 @@ AUD_NAMESPACE_BEGIN /** * This device plays back through PulseAudio, the simple direct media layer. */ -class AUD_PLUGIN_API PulseAudioDevice : public SoftwareDevice +class AUD_PLUGIN_API PulseAudioDevice : public ThreadedDevice { private: - /** - * Whether there is currently playback. - */ - volatile bool m_playback; - - pa_threaded_mainloop* m_mainloop; + pa_mainloop* m_mainloop; pa_context* m_context; pa_stream* m_stream; pa_context_state_t m_state; @@ -74,13 +69,15 @@ private: */ AUD_LOCAL static void PulseAudio_underflow(pa_stream* stream, void* data); + /** + * Streaming thread main function. + */ + AUD_LOCAL void runMixingThread(); + // delete copy constructor and operator= PulseAudioDevice(const PulseAudioDevice&) = delete; PulseAudioDevice& operator=(const PulseAudioDevice&) = delete; -protected: - virtual void playing(bool playing); - public: /** * Opens the PulseAudio audio device for playback. diff --git a/extern/audaspace/plugins/pulseaudio/PulseAudioSymbols.h b/extern/audaspace/plugins/pulseaudio/PulseAudioSymbols.h index 9cefbc0c7e2..4b9e1ffea2b 100644 --- a/extern/audaspace/plugins/pulseaudio/PulseAudioSymbols.h +++ b/extern/audaspace/plugins/pulseaudio/PulseAudioSymbols.h @@ -24,18 +24,14 @@ PULSEAUDIO_SYMBOL(pa_context_unref); PULSEAUDIO_SYMBOL(pa_stream_begin_write); PULSEAUDIO_SYMBOL(pa_stream_connect_playback); PULSEAUDIO_SYMBOL(pa_stream_cork); +PULSEAUDIO_SYMBOL(pa_stream_is_corked); PULSEAUDIO_SYMBOL(pa_stream_new); PULSEAUDIO_SYMBOL(pa_stream_set_buffer_attr); PULSEAUDIO_SYMBOL(pa_stream_set_underflow_callback); PULSEAUDIO_SYMBOL(pa_stream_set_write_callback); PULSEAUDIO_SYMBOL(pa_stream_write); -PULSEAUDIO_SYMBOL(pa_threaded_mainloop_free); -PULSEAUDIO_SYMBOL(pa_threaded_mainloop_get_api); -PULSEAUDIO_SYMBOL(pa_threaded_mainloop_lock); -PULSEAUDIO_SYMBOL(pa_threaded_mainloop_new); -PULSEAUDIO_SYMBOL(pa_threaded_mainloop_signal); -PULSEAUDIO_SYMBOL(pa_threaded_mainloop_start); -PULSEAUDIO_SYMBOL(pa_threaded_mainloop_stop); -PULSEAUDIO_SYMBOL(pa_threaded_mainloop_unlock); -PULSEAUDIO_SYMBOL(pa_threaded_mainloop_wait); +PULSEAUDIO_SYMBOL(pa_mainloop_free); +PULSEAUDIO_SYMBOL(pa_mainloop_get_api); +PULSEAUDIO_SYMBOL(pa_mainloop_new); +PULSEAUDIO_SYMBOL(pa_mainloop_iterate); diff --git a/extern/audaspace/plugins/wasapi/WASAPIDevice.cpp b/extern/audaspace/plugins/wasapi/WASAPIDevice.cpp index 4f213dc8468..b4632ebb83e 100644 --- a/extern/audaspace/plugins/wasapi/WASAPIDevice.cpp +++ b/extern/audaspace/plugins/wasapi/WASAPIDevice.cpp @@ -31,159 +31,83 @@ template void SafeRelease(T **ppT) } } -void WASAPIDevice::start() -{ - lock(); - - // thread is still running, we can abort stopping it - if(m_stop) - m_stop = false; - // thread is not running, let's start it - else if(!m_playing) - { - if(m_thread.joinable()) - m_thread.join(); - - m_playing = true; - - m_thread = std::thread(&WASAPIDevice::updateStream, this); - } - - unlock(); -} - -void WASAPIDevice::updateStream() +void WASAPIDevice::runMixingThread() { UINT32 buffer_size; + UINT32 padding; + UINT32 length; data_t* buffer; - lock(); + IAudioRenderClient* render_client = nullptr; - if(FAILED(m_audio_client->GetBufferSize(&buffer_size))) { - m_playing = false; - m_stop = false; - unlock(); - return; - } + std::lock_guard lock(*this); - IAudioRenderClient* render_client = nullptr; - const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient); + const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient); - if(FAILED(m_audio_client->GetService(IID_IAudioRenderClient, reinterpret_cast(&render_client)))) - { - m_playing = false; - m_stop = false; - unlock(); - return; - } + if(FAILED(m_audio_client->GetBufferSize(&buffer_size))) + goto init_error; - UINT32 padding; + if(FAILED(m_audio_client->GetService(IID_IAudioRenderClient, reinterpret_cast(&render_client)))) + goto init_error; - if(FAILED(m_audio_client->GetCurrentPadding(&padding))) - { - SafeRelease(&render_client); - m_playing = false; - m_stop = false; - unlock(); - return; - } + if(FAILED(m_audio_client->GetCurrentPadding(&padding))) + goto init_error; - UINT32 length = buffer_size - padding; + length = buffer_size - padding; - if(FAILED(render_client->GetBuffer(length, &buffer))) - { - SafeRelease(&render_client); - m_playing = false; - m_stop = false; - unlock(); - return; - } + if(FAILED(render_client->GetBuffer(length, &buffer))) + goto init_error; - mix((data_t*)buffer, length); + mix((data_t*)buffer, length); - if(FAILED(render_client->ReleaseBuffer(length, 0))) - { - SafeRelease(&render_client); - m_playing = false; - m_stop = false; - unlock(); - return; + if(FAILED(render_client->ReleaseBuffer(length, 0))) + { + init_error: + SafeRelease(&render_client); + doStop(); + return; + } } - unlock(); - m_audio_client->Start(); auto sleepDuration = std::chrono::milliseconds(buffer_size * 1000 / int(m_specs.rate) / 2); for(;;) { - lock(); - - if(FAILED(m_audio_client->GetCurrentPadding(&padding))) { - m_audio_client->Stop(); - SafeRelease(&render_client); - m_playing = false; - m_stop = false; - unlock(); - return; - } + std::lock_guard lock(*this); - length = buffer_size - padding; + if(FAILED(m_audio_client->GetCurrentPadding(&padding))) + goto stop_thread; - if(FAILED(render_client->GetBuffer(length, &buffer))) - { - m_audio_client->Stop(); - SafeRelease(&render_client); - m_playing = false; - m_stop = false; - unlock(); - return; - } + length = buffer_size - padding; - mix((data_t*)buffer, length); + if(FAILED(render_client->GetBuffer(length, &buffer))) + goto stop_thread; - if(FAILED(render_client->ReleaseBuffer(length, 0))) - { - m_audio_client->Stop(); - SafeRelease(&render_client); - m_playing = false; - m_stop = false; - unlock(); - return; - } + mix((data_t*)buffer, length); - // stop thread - if(m_stop) - { - m_audio_client->Stop(); - SafeRelease(&render_client); - m_playing = false; - m_stop = false; - unlock(); - return; - } + if(FAILED(render_client->ReleaseBuffer(length, 0))) + goto stop_thread; - unlock(); + // stop thread + if(shouldStop()) + { + stop_thread: + m_audio_client->Stop(); + SafeRelease(&render_client); + doStop(); + return; + } + } std::this_thread::sleep_for(sleepDuration); } } -void WASAPIDevice::playing(bool playing) -{ - if((!m_playing || m_stop) && playing) - start(); - else - m_stop = true; -} - WASAPIDevice::WASAPIDevice(DeviceSpecs specs, int buffersize) : - m_playing(false), - m_stop(false), - m_imm_device_enumerator(nullptr), m_imm_device(nullptr), m_audio_client(nullptr), @@ -361,14 +285,7 @@ WASAPIDevice::WASAPIDevice(DeviceSpecs specs, int buffersize) : WASAPIDevice::~WASAPIDevice() { - lock(); - - stopAll(); - - unlock(); - - if(m_thread.joinable()) - m_thread.join(); + stopMixingThread(); SafeRelease(&m_audio_client); SafeRelease(&m_imm_device); diff --git a/extern/audaspace/plugins/wasapi/WASAPIDevice.h b/extern/audaspace/plugins/wasapi/WASAPIDevice.h index ae25a09c432..375f03bd255 100644 --- a/extern/audaspace/plugins/wasapi/WASAPIDevice.h +++ b/extern/audaspace/plugins/wasapi/WASAPIDevice.h @@ -26,7 +26,7 @@ * The WASAPIDevice class. */ -#include "devices/SoftwareDevice.h" +#include "devices/ThreadedDevice.h" #include @@ -40,46 +40,23 @@ AUD_NAMESPACE_BEGIN /** * This device plays back through WASAPI, the Windows audio API. */ -class AUD_PLUGIN_API WASAPIDevice : public SoftwareDevice +class AUD_PLUGIN_API WASAPIDevice : public ThreadedDevice { private: - /** - * Whether there is currently playback. - */ - bool m_playing; - - /** - * Whether the current playback should stop. - */ - bool m_stop; - IMMDeviceEnumerator* m_imm_device_enumerator; IMMDevice* m_imm_device; IAudioClient* m_audio_client; WAVEFORMATEXTENSIBLE m_wave_format_extensible; - /** - * The streaming thread. - */ - std::thread m_thread; - - /** - * Starts the streaming thread. - */ - AUD_LOCAL void start(); - /** * Streaming thread main function. */ - AUD_LOCAL void updateStream(); + AUD_LOCAL void runMixingThread(); // delete copy constructor and operator= WASAPIDevice(const WASAPIDevice&) = delete; WASAPIDevice& operator=(const WASAPIDevice&) = delete; -protected: - virtual void playing(bool playing); - public: /** * Opens the WASAPI audio device for playback. diff --git a/extern/audaspace/src/devices/SoftwareDevice.cpp b/extern/audaspace/src/devices/SoftwareDevice.cpp index c8c1c6081c2..7a2561515f4 100644 --- a/extern/audaspace/src/devices/SoftwareDevice.cpp +++ b/extern/audaspace/src/devices/SoftwareDevice.cpp @@ -737,7 +737,7 @@ void SoftwareDevice::mix(data_t* buffer, int length) { m_buffer.assureSize(length * AUD_SAMPLE_SIZE(m_specs)); - std::lock_guard lock(m_mutex); + std::lock_guard lock(*this); { std::shared_ptr sound; @@ -880,7 +880,7 @@ std::shared_ptr SoftwareDevice::play(std::shared_ptr reader, b // play sound std::shared_ptr sound = std::shared_ptr(new SoftwareDevice::SoftwareHandle(this, reader, pitch, resampler, mapper, keep)); - std::lock_guard lock(m_mutex); + std::lock_guard lock(*this); m_playingSounds.push_back(sound); @@ -897,7 +897,7 @@ std::shared_ptr SoftwareDevice::play(std::shared_ptr sound, boo void SoftwareDevice::stopAll() { - std::lock_guard lock(m_mutex); + std::lock_guard lock(*this); while(!m_playingSounds.empty()) m_playingSounds.front()->stop(); diff --git a/extern/audaspace/src/devices/ThreadedDevice.cpp b/extern/audaspace/src/devices/ThreadedDevice.cpp new file mode 100644 index 00000000000..44ceccb8a60 --- /dev/null +++ b/extern/audaspace/src/devices/ThreadedDevice.cpp @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright 2009-2016 Jörg Müller + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +#include "devices/ThreadedDevice.h" + +#include + +AUD_NAMESPACE_BEGIN + +void ThreadedDevice::start() +{ + std::lock_guard lock(*this); + + // thread is still running, we can abort stopping it + if(m_stop) + m_stop = false; + + // thread is not running, let's start it + else if(!m_playing) + { + if(m_thread.joinable()) + m_thread.join(); + + m_playing = true; + + m_thread = std::thread(&ThreadedDevice::runMixingThread, this); + } +} + +void ThreadedDevice::playing(bool playing) +{ + if((!m_playing || m_stop) && playing) + start(); + else + m_stop = true; +} + +ThreadedDevice::ThreadedDevice() : + m_playing(false), + m_stop(false) +{ +} + +void aud::ThreadedDevice::stopMixingThread() +{ + stopAll(); + + if(m_thread.joinable()) + m_thread.join(); +} + +AUD_NAMESPACE_END -- cgit v1.2.3