Merge 1.4.86 Dev branch.
1.4.86 Dev merge.
This commit is contained in:
+170
-72
@@ -245,6 +245,9 @@ namespace pipedal
|
||||
class AlsaDriverImpl : public AudioDriver
|
||||
{
|
||||
private:
|
||||
void TraceBuffers(size_t framesInBuffers, char code = ' ');
|
||||
std::recursive_mutex restartMutex;
|
||||
|
||||
pipedal::CpuUse cpuUse;
|
||||
|
||||
#ifdef ALSADRIVER_CONFIG_DBG
|
||||
@@ -378,16 +381,21 @@ namespace pipedal
|
||||
}
|
||||
|
||||
private:
|
||||
void AlsaCleanup()
|
||||
void AlsaCloseAudio()
|
||||
{
|
||||
std::lock_guard lock{restartMutex};
|
||||
|
||||
if (captureHandle)
|
||||
{
|
||||
Lv2Log::debug("ALSA capture handle closed.");
|
||||
snd_pcm_drain(captureHandle);
|
||||
snd_pcm_close(captureHandle);
|
||||
captureHandle = nullptr;
|
||||
}
|
||||
if (playbackHandle)
|
||||
{
|
||||
Lv2Log::debug("ALSA playback handle closed.");
|
||||
snd_pcm_drain(playbackHandle);
|
||||
snd_pcm_close(playbackHandle);
|
||||
playbackHandle = nullptr;
|
||||
}
|
||||
@@ -411,7 +419,10 @@ namespace pipedal
|
||||
snd_pcm_sw_params_free(playbackSwParams);
|
||||
playbackSwParams = nullptr;
|
||||
}
|
||||
this->alsaSequencer = nullptr;
|
||||
}
|
||||
void AlsaCleanup()
|
||||
{
|
||||
AlsaCloseAudio();
|
||||
}
|
||||
|
||||
std::string discover_alsa_using_apps()
|
||||
@@ -1085,6 +1096,40 @@ namespace pipedal
|
||||
}
|
||||
}
|
||||
|
||||
void RestartAlsa()
|
||||
{
|
||||
std::lock_guard lock{restartMutex};
|
||||
Lv2Log::info("Restarting ALSA devices.");
|
||||
|
||||
try
|
||||
{
|
||||
AlsaCloseAudio();
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(SS("Error cleaning up ALSA: " << e.what()));
|
||||
throw std::runtime_error("Unable to restart the audio stream.");
|
||||
}
|
||||
try
|
||||
{
|
||||
OpenAudio(this->jackServerSettings, this->channelSelection);
|
||||
validate_capture_handle();
|
||||
FillOutputBuffer();
|
||||
audioRunning = true;
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(SS("Error opening ALSA: " << e.what()));
|
||||
throw std::runtime_error("Unable to restart the audio stream.");
|
||||
}
|
||||
int err;
|
||||
if ((err = snd_pcm_start(captureHandle)) < 0)
|
||||
{
|
||||
Lv2Log::error(SS("Unable to restart ALSA capture: " << snd_strerror(err)));
|
||||
throw PiPedalStateException("Unable to restart ALSA capture.");
|
||||
}
|
||||
}
|
||||
|
||||
void PrepareCaptureFunctions(snd_pcm_format_t captureFormat)
|
||||
{
|
||||
this->captureFormat = captureFormat;
|
||||
@@ -1221,6 +1266,8 @@ namespace pipedal
|
||||
|
||||
void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection)
|
||||
{
|
||||
std::lock_guard lock{restartMutex};
|
||||
|
||||
int err;
|
||||
|
||||
alsa_device_name = jackServerSettings.GetAlsaInputDevice();
|
||||
@@ -1245,7 +1292,7 @@ namespace pipedal
|
||||
{
|
||||
message =
|
||||
SS("Device " << alsa_device_name << " in use. The following applications are using your soundcard: " << apps
|
||||
<< ". Stop them as neccesary before trying to pipedald.");
|
||||
<< ". Stop them as neccesary before trying to start pipedald.");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1370,7 +1417,7 @@ namespace pipedal
|
||||
{
|
||||
throw PiPedalStateException(SS("Audio playback failed. " << snd_strerror(err)));
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
if (avail == 0)
|
||||
@@ -1386,94 +1433,106 @@ namespace pipedal
|
||||
}
|
||||
validate_capture_handle();
|
||||
}
|
||||
void recover_from_output_underrun(snd_pcm_t *capture_handle, snd_pcm_t *playback_handle, int err)
|
||||
void recover_from_output_underrun(snd_pcm_t *capture_handle, snd_pcm_t *playback_handle, int err, size_t framesRead)
|
||||
{
|
||||
validate_capture_handle();
|
||||
if (err == -EPIPE)
|
||||
try
|
||||
{
|
||||
err = snd_pcm_prepare(playback_handle);
|
||||
if (err < 0)
|
||||
|
||||
TraceBuffers(framesRead,'w');
|
||||
if (err == -EPIPE)
|
||||
{
|
||||
throw PiPedalStateException(SS("Can't recover from ALSA output underrun. (" << snd_strerror(err) << ")"));
|
||||
err = snd_pcm_prepare(playback_handle);
|
||||
if (err < 0)
|
||||
{
|
||||
Lv2Log::error(SS("Can't recover from ALSA output underrun. (" << snd_strerror(err) << ")"));
|
||||
throw PiPedalStateException(SS("Can't recover from ALSA output underrun. (" << snd_strerror(err) << ")"));
|
||||
}
|
||||
snd_pcm_drain(capture_handle);
|
||||
FillOutputBuffer();
|
||||
}
|
||||
else
|
||||
{
|
||||
Lv2Log::error(SS("Can't recover from ALSA output underrun. (" << snd_strerror(err) << ")"));
|
||||
throw PiPedalStateException(SS("Can't recover from ALSA output error. (" << snd_strerror(err) << ")"));
|
||||
}
|
||||
FillOutputBuffer();
|
||||
}
|
||||
else
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
throw PiPedalStateException(SS("Can't recover from ALSA output error. (" << snd_strerror(err) << ")"));
|
||||
Lv2Log::info(SS("Soft audio restart failed. " << e.what()));
|
||||
RestartAlsa();
|
||||
audioRunning = true;
|
||||
}
|
||||
validate_capture_handle();
|
||||
}
|
||||
void recover_from_input_underrun(snd_pcm_t *capture_handle, snd_pcm_t *playback_handle, int err)
|
||||
void recover_from_input_underrun(snd_pcm_t *capture_handle, snd_pcm_t *playback_handle, int err, size_t bufferedFrames)
|
||||
{
|
||||
validate_capture_handle();
|
||||
|
||||
if (err == -EPIPE)
|
||||
try
|
||||
{
|
||||
TraceBuffers(bufferedFrames,'r');
|
||||
if (err == -EPIPE)
|
||||
{
|
||||
|
||||
// Unlink the streams before recovery
|
||||
snd_pcm_unlink(capture_handle);
|
||||
// Unlink the streams before recovery
|
||||
snd_pcm_unlink(capture_handle);
|
||||
|
||||
err = snd_pcm_drop(capture_handle);
|
||||
if (err < 0)
|
||||
{
|
||||
throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")"));
|
||||
}
|
||||
err = snd_pcm_drop(playback_handle);
|
||||
if (err < 0)
|
||||
{
|
||||
throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")"));
|
||||
}
|
||||
// Prepare both streams
|
||||
if ((err = snd_pcm_prepare(playback_handle)) < 0)
|
||||
{
|
||||
throw std::runtime_error(SS("Cannot prepare playback stream: " << snd_strerror(err)));
|
||||
}
|
||||
if ((err = snd_pcm_prepare(capture_handle)) < 0)
|
||||
{
|
||||
throw std::runtime_error(SS("Cannot prepare capture stream: " << snd_strerror(err)));
|
||||
}
|
||||
|
||||
// Prepare both streams
|
||||
if ((err = snd_pcm_prepare(playback_handle)) < 0)
|
||||
{
|
||||
throw std::runtime_error(SS("Cannot prepare playback stream: " << snd_strerror(err)));
|
||||
}
|
||||
if ((err = snd_pcm_prepare(capture_handle)) < 0)
|
||||
{
|
||||
throw std::runtime_error(SS("Cannot prepare capture stream: " << snd_strerror(err)));
|
||||
}
|
||||
// Resynchronize the streams
|
||||
if ((err = snd_pcm_link(capture_handle, playback_handle)) < 0)
|
||||
{
|
||||
throw std::runtime_error(SS("Cannot relink streams: " << snd_strerror(err)));
|
||||
}
|
||||
|
||||
// Fill the playback buffer with silence
|
||||
FillOutputBuffer();
|
||||
// Start the streams
|
||||
FillOutputBuffer();
|
||||
if ((err = snd_pcm_start(capture_handle)) < 0)
|
||||
{
|
||||
throw std::runtime_error(SS("Cannot restart capture stream: " << snd_strerror(err)));
|
||||
}
|
||||
|
||||
// Resynchronize the streams
|
||||
if ((err = snd_pcm_link(capture_handle, playback_handle)) < 0)
|
||||
{
|
||||
throw std::runtime_error(SS("Cannot relink streams: " << snd_strerror(err)));
|
||||
validate_capture_handle();
|
||||
}
|
||||
else if (err == ESTRPIPE)
|
||||
{
|
||||
audioRunning = false;
|
||||
validate_capture_handle();
|
||||
|
||||
// Start the streams
|
||||
if ((err = snd_pcm_start(capture_handle)) < 0)
|
||||
{
|
||||
throw std::runtime_error(SS("Cannot restart capture stream: " << snd_strerror(err)));
|
||||
}
|
||||
validate_capture_handle();
|
||||
}
|
||||
else if (err == ESTRPIPE)
|
||||
{
|
||||
audioRunning = false;
|
||||
validate_capture_handle();
|
||||
|
||||
while ((err = snd_pcm_resume(capture_handle)) == -EAGAIN)
|
||||
{
|
||||
sleep(1);
|
||||
}
|
||||
if (err < 0)
|
||||
{
|
||||
err = snd_pcm_prepare(capture_handle);
|
||||
while ((err = snd_pcm_resume(capture_handle)) == -EAGAIN)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
if (err < 0)
|
||||
{
|
||||
throw PiPedalStateException(SS("Can't recover from ALSA suspend. (" << snd_strerror(err) << ")"));
|
||||
err = snd_pcm_prepare(capture_handle);
|
||||
if (err < 0)
|
||||
{
|
||||
throw PiPedalStateException(SS("Can't recover from ALSA suspend. (" << snd_strerror(err) << ")"));
|
||||
}
|
||||
}
|
||||
audioRunning = true;
|
||||
validate_capture_handle();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw PiPedalStateException(SS("Can't recover from ALSA input error. (" << snd_strerror(err) << ")"));
|
||||
}
|
||||
audioRunning = true;
|
||||
validate_capture_handle();
|
||||
}
|
||||
else
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
throw std::runtime_error(SS("Can't restart audio: " << snd_strerror(err)));
|
||||
Lv2Log::info(SS("Soft audio restart failed. " << e.what()));
|
||||
RestartAlsa();
|
||||
audioRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1516,6 +1575,7 @@ namespace pipedal
|
||||
{
|
||||
snd_pcm_wait(captureHandle, 1);
|
||||
}
|
||||
TraceBuffers(framesRead);
|
||||
} while (frames > 0);
|
||||
return framesRead;
|
||||
}
|
||||
@@ -1526,6 +1586,11 @@ namespace pipedal
|
||||
AlsaMidiMessage message;
|
||||
|
||||
midiEventCount = 0;
|
||||
auto alsaSequener = this->alsaSequencer; // take an addref
|
||||
if (!alsaSequener)
|
||||
{
|
||||
return;
|
||||
}
|
||||
while (alsaSequencer->ReadMessage(message, 0))
|
||||
{
|
||||
size_t messageSize = message.size;
|
||||
@@ -1580,6 +1645,7 @@ namespace pipedal
|
||||
void AudioThread()
|
||||
{
|
||||
SetThreadName("alsaDriver");
|
||||
|
||||
try
|
||||
{
|
||||
SetThreadPriority(SchedulerPriority::RealtimeAudio);
|
||||
@@ -1597,7 +1663,7 @@ namespace pipedal
|
||||
}
|
||||
|
||||
CrashGuardLock crashGuardLock;
|
||||
|
||||
|
||||
cpuUse.SetStartTime(cpuUse.Now());
|
||||
while (true)
|
||||
{
|
||||
@@ -1628,7 +1694,7 @@ namespace pipedal
|
||||
framesToRead)) < 0)
|
||||
{
|
||||
this->driverHost->OnUnderrun();
|
||||
recover_from_input_underrun(captureHandle, playbackHandle, nFrames);
|
||||
recover_from_input_underrun(captureHandle, playbackHandle, nFrames, framesRead);
|
||||
xrun = true;
|
||||
break;
|
||||
}
|
||||
@@ -1665,7 +1731,8 @@ namespace pipedal
|
||||
if (err < 0)
|
||||
{
|
||||
this->driverHost->OnUnderrun();
|
||||
recover_from_output_underrun(captureHandle, playbackHandle, err);
|
||||
|
||||
recover_from_output_underrun(captureHandle, playbackHandle, err, framesRead);
|
||||
}
|
||||
cpuUse.AddSample(ProfileCategory::Write);
|
||||
}
|
||||
@@ -1675,11 +1742,11 @@ namespace pipedal
|
||||
Lv2Log::error(e.what());
|
||||
Lv2Log::error("ALSA audio thread terminated abnormally.");
|
||||
}
|
||||
this->driverHost->OnAlsaDriverStopped();
|
||||
|
||||
// if we terminated abnormally, pump messages until we have been terminated.
|
||||
if (!terminateAudio())
|
||||
{
|
||||
this->driverHost->OnAlsaDriverStopped();
|
||||
// zero out input buffers.
|
||||
for (size_t i = 0; i < this->captureBuffers.size(); ++i)
|
||||
{
|
||||
@@ -1694,7 +1761,6 @@ namespace pipedal
|
||||
while (!terminateAudio())
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
// zero out input buffers.
|
||||
this->driverHost->OnProcess(this->bufferSize);
|
||||
}
|
||||
}
|
||||
@@ -1766,7 +1832,7 @@ namespace pipedal
|
||||
}
|
||||
|
||||
audioThread = std::make_unique<std::jthread>([this]()
|
||||
{ AudioThread(); });
|
||||
{ AudioThread(); });
|
||||
}
|
||||
|
||||
virtual void Deactivate()
|
||||
@@ -1848,6 +1914,8 @@ namespace pipedal
|
||||
Deactivate();
|
||||
AlsaCleanup();
|
||||
DeleteBuffers();
|
||||
this->alsaSequencer = nullptr;
|
||||
|
||||
std::atomic_thread_fence(std::memory_order::release);
|
||||
}
|
||||
|
||||
@@ -1940,7 +2008,7 @@ namespace pipedal
|
||||
try
|
||||
{
|
||||
int err;
|
||||
for (int retry = 0; retry < 2; ++retry)
|
||||
for (int retry = 0; retry < 4; ++retry)
|
||||
{
|
||||
err = snd_pcm_open(&playbackHandle, alsaDeviceName.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
|
||||
if (err < 0) // field report of a device that is present, but won't immediately open.
|
||||
@@ -2202,6 +2270,36 @@ namespace pipedal
|
||||
#endif
|
||||
}
|
||||
|
||||
struct BufferTrace
|
||||
{
|
||||
snd_pcm_sframes_t inAvail;
|
||||
snd_pcm_sframes_t outAvail;
|
||||
snd_pcm_sframes_t buffered;
|
||||
snd_pcm_sframes_t total;
|
||||
char code;
|
||||
};
|
||||
|
||||
std::vector<BufferTrace> bufferTraces(1000);
|
||||
size_t bufferTraceIndex;
|
||||
|
||||
void AlsaDriverImpl::TraceBuffers(size_t framesInBuffer,char code)
|
||||
{
|
||||
auto inAvail = snd_pcm_avail_update(this->captureHandle);
|
||||
auto outAvail = snd_pcm_avail_update(this->playbackHandle);
|
||||
|
||||
auto total = inAvail + outAvail + framesInBuffer;
|
||||
bufferTraces[bufferTraceIndex++] = {
|
||||
inAvail,
|
||||
outAvail,
|
||||
(snd_pcm_sframes_t)framesInBuffer,
|
||||
(snd_pcm_sframes_t)total,
|
||||
code};
|
||||
if (bufferTraceIndex == bufferTraces.size())
|
||||
{
|
||||
bufferTraceIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void FreeAlsaGlobals()
|
||||
{
|
||||
snd_config_update_free_global(); // to get a clean Valgrind report.
|
||||
|
||||
+4
-2
@@ -34,6 +34,7 @@
|
||||
#include "PluginHost.hpp"
|
||||
#include "PatchPropertyWriter.hpp"
|
||||
#include "CpuTemperatureMonitor.hpp"
|
||||
#include "restrict.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
@@ -618,7 +619,7 @@ private:
|
||||
{
|
||||
for (size_t i = 0; i < audioDriver->OutputBufferCount(); ++i)
|
||||
{
|
||||
float *out = (float *)audioDriver->GetOutputBuffer(i);
|
||||
float * out = (float *)audioDriver->GetOutputBuffer(i);
|
||||
if (out)
|
||||
{
|
||||
ZeroBuffer(out, nframes);
|
||||
@@ -1141,7 +1142,7 @@ private:
|
||||
{
|
||||
try
|
||||
{
|
||||
float *in, *out;
|
||||
float * restrict in , * restrict out;
|
||||
|
||||
Lv2Pedalboard *pedalboard = nullptr;
|
||||
pedalboard = this->realtimeActivePedalboard;
|
||||
@@ -1724,6 +1725,7 @@ public:
|
||||
{
|
||||
Close();
|
||||
active = false;
|
||||
isOpen = false;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
+66
-28
@@ -40,6 +40,40 @@
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
int avahiPollLockCount = 0;
|
||||
static std::mutex avahiLockMutex;
|
||||
class AvahiPollLock {
|
||||
public:
|
||||
AvahiPollLock() = delete;
|
||||
AvahiPollLock(AvahiThreadedPoll*threadedPoll)
|
||||
: threadedPoll(threadedPoll) {
|
||||
std::lock_guard<std::mutex> lock(avahiLockMutex);
|
||||
if (!threadedPoll) {
|
||||
throw std::runtime_error("AvahiPollLock: threadedPoll is null.");
|
||||
}
|
||||
if (++avahiPollLockCount > 1) {
|
||||
throw std::runtime_error("AvahiPollLock: nested locks are not allowed.");
|
||||
}
|
||||
avahi_threaded_poll_lock(threadedPoll);
|
||||
}
|
||||
void release() {
|
||||
std::lock_guard<std::mutex> lock(avahiLockMutex);
|
||||
if (threadedPoll)
|
||||
{
|
||||
if (avahiPollLockCount <= 0) {
|
||||
throw std::runtime_error("AvahiPollLock: release called without a lock.");
|
||||
}
|
||||
--avahiPollLockCount;
|
||||
avahi_threaded_poll_unlock(threadedPoll);
|
||||
threadedPoll = nullptr;
|
||||
}
|
||||
}
|
||||
~AvahiPollLock() {
|
||||
release();
|
||||
}
|
||||
private:
|
||||
AvahiThreadedPoll *threadedPoll;
|
||||
};
|
||||
void AvahiService::Announce(
|
||||
int portNumber,
|
||||
const std::string &name,
|
||||
@@ -75,7 +109,7 @@ void AvahiService::Announce(
|
||||
else
|
||||
{
|
||||
// already started, so we have to use poll_lock instead.
|
||||
avahi_threaded_poll_lock(threadedPoll);
|
||||
AvahiPollLock lock(threadedPoll);
|
||||
|
||||
// all state that we have to protect with the lock now that the avahi serv3ce is running.
|
||||
this->portNumber = portNumber;
|
||||
@@ -104,7 +138,7 @@ void AvahiService::Announce(
|
||||
Lv2Log::error("Failed to update mDNS service because of a synch problem.");
|
||||
}
|
||||
}
|
||||
avahi_threaded_poll_unlock(threadedPoll);
|
||||
lock.release();
|
||||
if (wait)
|
||||
{
|
||||
Wait();
|
||||
@@ -118,30 +152,31 @@ void AvahiService::Unannounce(bool wait)
|
||||
return;
|
||||
if (this->threadedPoll)
|
||||
{
|
||||
avahi_threaded_poll_lock(threadedPoll);
|
||||
if (started)
|
||||
{
|
||||
if (this->createPending)
|
||||
AvahiPollLock lock(threadedPoll);
|
||||
if (started)
|
||||
{
|
||||
// if service is starting up, and we haven't yet made our first announcement,
|
||||
// just signal that we don't want to announc.
|
||||
this->makeAnnouncement = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->makeAnnouncement = false;
|
||||
// we have previously made a successful announcement. Retract it.
|
||||
if (group)
|
||||
if (this->createPending)
|
||||
{
|
||||
int rc = avahi_entry_group_reset(group);
|
||||
if (rc < 0)
|
||||
// if service is starting up, and we haven't yet made our first announcement,
|
||||
// just signal that we don't want to announc.
|
||||
this->makeAnnouncement = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->makeAnnouncement = false;
|
||||
// we have previously made a successful announcement. Retract it.
|
||||
if (group)
|
||||
{
|
||||
Lv2Log::error("Avahi: failed to un-announce.");
|
||||
int rc = avahi_entry_group_reset(group);
|
||||
if (rc < 0)
|
||||
{
|
||||
Lv2Log::error("Avahi: failed to un-announce.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
avahi_threaded_poll_unlock(threadedPoll);
|
||||
if (wait)
|
||||
{
|
||||
WaitForUnannounce();
|
||||
@@ -411,6 +446,7 @@ void AvahiService::Start()
|
||||
|
||||
if (!client)
|
||||
{
|
||||
AvahiPollLock lock(threadedPoll);
|
||||
/* Allocate a new client */
|
||||
client = avahi_client_new(avahi_threaded_poll_get(threadedPoll), AvahiClientFlags::AVAHI_CLIENT_NO_FAIL, client_callback, (void *)this, &error);
|
||||
/* Check wether creating the client object succeeded */
|
||||
@@ -442,20 +478,22 @@ void AvahiService::Stop()
|
||||
|
||||
if (threadedPoll)
|
||||
{
|
||||
avahi_threaded_poll_lock(threadedPoll);
|
||||
if (group)
|
||||
{
|
||||
avahi_entry_group_free(group);
|
||||
group = nullptr;
|
||||
AvahiPollLock lock(threadedPoll);
|
||||
if (group)
|
||||
{
|
||||
avahi_entry_group_free(group);
|
||||
group = nullptr;
|
||||
}
|
||||
}
|
||||
avahi_threaded_poll_unlock(threadedPoll);
|
||||
avahi_threaded_poll_lock(threadedPoll);
|
||||
if (client)
|
||||
{
|
||||
avahi_client_free(client);
|
||||
client = nullptr;
|
||||
AvahiPollLock lock(threadedPoll);
|
||||
if (client)
|
||||
{
|
||||
avahi_client_free(client);
|
||||
client = nullptr;
|
||||
}
|
||||
}
|
||||
avahi_threaded_poll_unlock(threadedPoll);
|
||||
|
||||
avahi_threaded_poll_stop(threadedPoll);
|
||||
avahi_threaded_poll_free(threadedPoll);
|
||||
|
||||
@@ -46,7 +46,6 @@ namespace pipedal {
|
||||
virtual int GetNumberOfInputAudioChannels() const = 0;
|
||||
virtual int GetNumberOfOutputAudioChannels() const = 0;
|
||||
virtual std::shared_ptr<Lv2PluginInfo> GetPluginInfo(const std::string &uri) const = 0;
|
||||
virtual std::shared_ptr<HostWorkerThread> GetHostWorkerThread() = 0;
|
||||
|
||||
virtual IEffect *CreateEffect(PedalboardItem &pedalboard) = 0;
|
||||
|
||||
|
||||
+26
-21
@@ -18,6 +18,7 @@
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#include "pch.h"
|
||||
#include "restrict.hpp"
|
||||
#include "Lv2Effect.hpp"
|
||||
#include "PiPedalException.hpp"
|
||||
#include <lv2/lv2plug.in/ns/ext/worker/worker.h>
|
||||
@@ -41,6 +42,7 @@
|
||||
#include "AudioHost.hpp"
|
||||
#include <exception>
|
||||
#include "RingBufferReader.hpp"
|
||||
#include "Worker.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
namespace fs = std::filesystem;
|
||||
@@ -70,6 +72,11 @@ Lv2Effect::Lv2Effect(
|
||||
|
||||
this->bypass = pedalboardItem.isEnabled();
|
||||
|
||||
this->workerThread = std::make_unique<HostWorkerThread>();
|
||||
if (info_->WantsWorkerThread())
|
||||
{
|
||||
workerThread->StartThread();
|
||||
}
|
||||
// stash a list of known file properties that we want to keep synced.
|
||||
if (info->piPedalUI())
|
||||
{
|
||||
@@ -179,10 +186,8 @@ Lv2Effect::Lv2Effect(
|
||||
const LV2_Worker_Interface *worker_interface =
|
||||
(const LV2_Worker_Interface *)lilv_instance_get_extension_data(pInstance,
|
||||
LV2_WORKER__interface);
|
||||
if (worker_interface)
|
||||
{
|
||||
this->worker = std::make_unique<Worker>(pHost->GetHostWorkerThread(), pInstance, worker_interface);
|
||||
}
|
||||
this->worker = std::make_unique<Worker>(workerThread, pInstance, worker_interface);
|
||||
|
||||
const LV2_State_Interface *state_interface =
|
||||
(const LV2_State_Interface *)lilv_instance_get_extension_data(pInstance,
|
||||
LV2_STATE__interface);
|
||||
@@ -510,15 +515,15 @@ int Lv2Effect::GetControlIndex(const std::string &key) const
|
||||
|
||||
Lv2Effect::~Lv2Effect()
|
||||
{
|
||||
if (activated)
|
||||
{
|
||||
Deactivate();
|
||||
}
|
||||
if (worker)
|
||||
{
|
||||
worker->Close();
|
||||
worker = nullptr; // delete the worker first!
|
||||
}
|
||||
if (activated)
|
||||
{
|
||||
Deactivate();
|
||||
}
|
||||
if (pInstance)
|
||||
{
|
||||
lilv_instance_free(pInstance);
|
||||
@@ -631,7 +636,7 @@ void Lv2Effect::Deactivate()
|
||||
lilv_instance_deactivate(pInstance);
|
||||
}
|
||||
|
||||
static inline void CopyBuffer(float *input, float *output, uint32_t frames)
|
||||
static inline void CopyBuffer(float *restrict input, float *restrict output, uint32_t frames)
|
||||
{
|
||||
for (uint32_t i = 0; i < frames; ++i)
|
||||
{
|
||||
@@ -669,7 +674,7 @@ void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBuff
|
||||
{
|
||||
for (size_t i = 0; i < this->outputMixBuffers.size(); ++i)
|
||||
{
|
||||
float *__restrict input;
|
||||
float *restrict input;
|
||||
if (i >= this->inputAudioBuffers.size())
|
||||
{
|
||||
if (this->inputAudioBuffers.size() == 0)
|
||||
@@ -682,8 +687,8 @@ void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBuff
|
||||
{
|
||||
input = this->inputAudioBuffers[i];
|
||||
}
|
||||
float *__restrict pluginOutput = this->outputMixBuffers[i].data();
|
||||
float *__restrict finalOutput = this->outputAudioBuffers[i];
|
||||
float *restrict pluginOutput = this->outputMixBuffers[i].data();
|
||||
float *restrict finalOutput = this->outputAudioBuffers[i];
|
||||
|
||||
for (uint32_t i = 0; i < samples; ++i)
|
||||
{
|
||||
@@ -694,11 +699,11 @@ void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBuff
|
||||
else if (this->outputAudioPortIndices.size() == 1 && this->outputAudioBuffers.size() == 2)
|
||||
{
|
||||
// 1 plugin output into 2 outputs.
|
||||
float *__restrict pluginOutput = this->outputMixBuffers[0].data();
|
||||
float *restrict pluginOutput = this->outputMixBuffers[0].data();
|
||||
for (size_t i = 0; i < this->outputMixBuffers.size(); ++i)
|
||||
{
|
||||
float *__restrict input = this->inputAudioBuffers[i];
|
||||
float *__restrict finalOutput = this->outputAudioBuffers[i];
|
||||
float *restrict input = this->inputAudioBuffers[i];
|
||||
float *restrict finalOutput = this->outputAudioBuffers[i];
|
||||
|
||||
for (uint32_t i = 0; i < samples; ++i)
|
||||
{
|
||||
@@ -746,8 +751,8 @@ void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBuff
|
||||
|
||||
if (this->outputAudioBuffers.size() == 1)
|
||||
{
|
||||
float *input = this->inputAudioBuffers[0];
|
||||
float *output = this->outputAudioBuffers[0];
|
||||
float * restrict input = this->inputAudioBuffers[0];
|
||||
float * restrict output = this->outputAudioBuffers[0];
|
||||
for (uint32_t i = 0; i < samples; ++i)
|
||||
{
|
||||
output[i] = currentBypass * output[i] + (1 - currentBypass) * input[i];
|
||||
@@ -762,8 +767,8 @@ void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBuff
|
||||
}
|
||||
else
|
||||
{
|
||||
float *inputL;
|
||||
float *inputR;
|
||||
float * restrict inputL;
|
||||
float * restrict inputR;
|
||||
if (this->inputAudioBuffers.size() == 1)
|
||||
{
|
||||
inputL = inputR = inputAudioBuffers[0];
|
||||
@@ -773,8 +778,8 @@ void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBuff
|
||||
inputL = inputAudioBuffers[0];
|
||||
inputR = inputAudioBuffers[1];
|
||||
}
|
||||
float *outputL = outputAudioBuffers[0];
|
||||
float *outputR = outputAudioBuffers[1];
|
||||
float * restrict outputL = outputAudioBuffers[0];
|
||||
float * restrict outputR = outputAudioBuffers[1];
|
||||
for (uint32_t i = 0; i < samples; ++i)
|
||||
{
|
||||
outputL[i] = currentBypass * outputL[i] + (1 - currentBypass) * inputL[i];
|
||||
|
||||
+5
-2
@@ -40,12 +40,12 @@
|
||||
#include "StateInterface.hpp"
|
||||
#include "LogFeature.hpp"
|
||||
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
|
||||
class RealtimeRingBufferWriter;
|
||||
class IPatchWriterCallback;
|
||||
class HostWorkerThread;
|
||||
|
||||
class Lv2Effect : public IEffect, private LogFeature::LogMessageListener
|
||||
{
|
||||
@@ -57,6 +57,10 @@ namespace pipedal
|
||||
|
||||
private:
|
||||
|
||||
|
||||
std::shared_ptr<HostWorkerThread> workerThread;
|
||||
std::unique_ptr<Worker> worker;
|
||||
|
||||
std::unordered_map<std::string,int> controlIndex;
|
||||
|
||||
FileBrowserFilesFeature fileBrowserFilesFeature;
|
||||
@@ -70,7 +74,6 @@ namespace pipedal
|
||||
int numberOfOutputs = 0;
|
||||
int numberOfMidiInputs = 0;
|
||||
|
||||
std::unique_ptr<Worker> worker;
|
||||
LilvInstance *pInstance;
|
||||
std::shared_ptr<Lv2PluginInfo> info;
|
||||
std::vector<float> controlValues;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "Lv2EventBufferWriter.hpp"
|
||||
#include "Lv2Log.hpp"
|
||||
#include "CrashGuard.hpp"
|
||||
#include "restrict.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
@@ -417,7 +418,7 @@ void Lv2Pedalboard::Deactivate()
|
||||
}
|
||||
}
|
||||
|
||||
static void Copy(float *input, float *output, uint32_t samples)
|
||||
static void Copy(float *restrict input, float *restrict output, uint32_t samples)
|
||||
{
|
||||
for (uint32_t i = 0; i < samples; ++i)
|
||||
{
|
||||
@@ -434,6 +435,7 @@ bool Lv2Pedalboard::Run(float **inputBuffers, float **outputBuffers, uint32_t sa
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < samples; ++i)
|
||||
{
|
||||
float volume = this->inputVolume.Tick();
|
||||
|
||||
+15
-5
@@ -47,6 +47,7 @@
|
||||
#include "StdErrorCapture.hpp"
|
||||
#include "util.hpp"
|
||||
#include "ModFileTypes.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
#include "Locale.hpp"
|
||||
|
||||
@@ -301,7 +302,6 @@ PluginHost::PluginHost()
|
||||
|
||||
this->urids = new Urids(mapFeature);
|
||||
|
||||
pHostWorkerThread = std::make_shared<HostWorkerThread>();
|
||||
}
|
||||
|
||||
void PluginHost::OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings)
|
||||
@@ -870,6 +870,13 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
|
||||
const LilvPluginClass *pClass = lilv_plugin_get_class(pPlugin);
|
||||
this->plugin_class_ = nodeAsString(lilv_plugin_class_get_uri(pClass));
|
||||
|
||||
if (this->plugin_class_ == "") // carla does this.
|
||||
{
|
||||
// set to plain Plugin.
|
||||
this->plugin_class_ = "http://lv2plug.in/ns/lv2core#Plugin";
|
||||
}
|
||||
|
||||
|
||||
AutoLilvNodes required_features = lilv_plugin_get_required_features(pPlugin);
|
||||
this->required_features_ = nodeAsStringArray(required_features);
|
||||
|
||||
@@ -1668,10 +1675,6 @@ bool Lv2PluginInfo::isSplit() const
|
||||
{
|
||||
return uri_ == SPLIT_PEDALBOARD_ITEM_URI;
|
||||
}
|
||||
std::shared_ptr<HostWorkerThread> PluginHost::GetHostWorkerThread()
|
||||
{
|
||||
return pHostWorkerThread;
|
||||
}
|
||||
|
||||
class ResourceInfo
|
||||
{
|
||||
@@ -1883,6 +1886,13 @@ Lv2PatchPropertyInfo::Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNod
|
||||
}
|
||||
}
|
||||
|
||||
// ffs.
|
||||
static inline bool contains(const std::vector<std::string> &vec, const std::string &value) {
|
||||
return std::find(vec.begin(),vec.end(),value) != vec.end();
|
||||
}
|
||||
bool Lv2PluginInfo::WantsWorkerThread() const {
|
||||
return contains(this->required_features_,LV2_WORKER__schedule) || contains(this->supported_features_,LV2_WORKER__schedule);
|
||||
}
|
||||
// void PiPedalHostLogError(const std::string &error)
|
||||
// {
|
||||
// Lv2Log::error("%s",error.c_str());
|
||||
|
||||
+2
-2
@@ -454,6 +454,8 @@ namespace pipedal
|
||||
LV2_PROPERTY_GETSET(patchProperties)
|
||||
LV2_PROPERTY_GETSET(hasDefaultState)
|
||||
|
||||
bool WantsWorkerThread() const;
|
||||
|
||||
const Lv2PortInfo &getPort(const std::string &symbol)
|
||||
{
|
||||
for (size_t i = 0; i < ports_.size(); ++i)
|
||||
@@ -913,7 +915,6 @@ namespace pipedal
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<HostWorkerThread> pHostWorkerThread;
|
||||
// IHost implementation.
|
||||
virtual void SetMaxAudioBufferSize(size_t size) { maxBufferSize = size; }
|
||||
virtual size_t GetMaxAudioBufferSize() const { return maxBufferSize; }
|
||||
@@ -922,7 +923,6 @@ namespace pipedal
|
||||
virtual int GetNumberOfInputAudioChannels() const { return numberOfAudioInputChannels; }
|
||||
virtual int GetNumberOfOutputAudioChannels() const { return numberOfAudioOutputChannels; }
|
||||
virtual LV2_Feature *const *GetLv2Features() const { return (LV2_Feature *const *)&(this->lv2Features[0]); }
|
||||
virtual std::shared_ptr<HostWorkerThread> GetHostWorkerThread();
|
||||
|
||||
public:
|
||||
virtual MapFeature &GetMapFeature() override { return this->mapFeature; }
|
||||
|
||||
@@ -104,7 +104,7 @@ static void SetPriority(int realtimePriority, const char *priorityName)
|
||||
int result = sched_setscheduler(0, SCHED_RR, ¶m);
|
||||
if (result == 0)
|
||||
{
|
||||
Lv2Log::debug("Service thread priority successfully boosted.");
|
||||
Lv2Log::debug(SS("Service thread priority successfully boosted. (" << priorityName << ")"));
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
+44
-18
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
// (Borrows heavily from worker.c by David Robillard.)
|
||||
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
@@ -143,6 +143,7 @@ bool Worker::EmitResponses()
|
||||
|
||||
void Worker::WaitForAllResponses()
|
||||
{
|
||||
// do what we can to clear the response queue in order to avoid memory/object leaks
|
||||
using Clock = std::chrono::steady_clock;
|
||||
auto startTime = Clock::now();
|
||||
while (true)
|
||||
@@ -157,11 +158,15 @@ void Worker::WaitForAllResponses()
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::chrono::seconds waitDuration = std::chrono::duration_cast<std::chrono::seconds>(Clock::now()-startTime);
|
||||
if (waitDuration.count() > 5) {
|
||||
throw std::logic_error("Timed out waiting for a Worker task to complete.");
|
||||
// pump the plugin with a zero-length buffer.
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
std::chrono::milliseconds waitDuration = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now()-startTime);
|
||||
if (waitDuration.count() > 1500) {
|
||||
// better to leak than to terminate the application.
|
||||
Lv2Log::error("Timed out waiting for a Worker task to complete.");
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,26 +247,34 @@ void HostWorkerThread::ThreadProc() noexcept
|
||||
HostWorkerThread::HostWorkerThread()
|
||||
{
|
||||
this->dataBuffer.resize(16*1024);
|
||||
pThread = std::make_unique<std::thread>([this]()
|
||||
{ this->ThreadProc(); });
|
||||
}
|
||||
|
||||
bool HostWorkerThread::StartThread()
|
||||
{
|
||||
if (!pThread)
|
||||
{
|
||||
if (closed) {
|
||||
return false;
|
||||
}
|
||||
pThread = std::make_unique<std::thread>([this]()
|
||||
{ this->ThreadProc(); });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostWorkerThread::Close()
|
||||
{
|
||||
|
||||
bool sendClose = false;
|
||||
{
|
||||
std::lock_guard lock{submitMutex};
|
||||
if (!closed)
|
||||
{
|
||||
closed = true;
|
||||
sendClose = true;
|
||||
if (pThread) {
|
||||
if (!closed)
|
||||
{
|
||||
closed = true;
|
||||
ScheduleWorkNoLock(nullptr, 0, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sendClose)
|
||||
{
|
||||
ScheduleWork(nullptr, 0, nullptr);
|
||||
}
|
||||
}
|
||||
HostWorkerThread::~HostWorkerThread()
|
||||
{
|
||||
@@ -278,7 +291,10 @@ LV2_Worker_Status HostWorkerThread::ScheduleWork(Worker *worker, size_t size, co
|
||||
{
|
||||
std::lock_guard lock(submitMutex);
|
||||
|
||||
if (exiting)
|
||||
if (!StartThread()) { // ensures thread is running, when plugins haven't declared they want a worker thread.
|
||||
return LV2_Worker_Status::LV2_WORKER_ERR_NO_SPACE;
|
||||
}
|
||||
if (exiting )
|
||||
{
|
||||
return LV2_Worker_Status::LV2_WORKER_ERR_NO_SPACE;
|
||||
}
|
||||
@@ -286,6 +302,11 @@ LV2_Worker_Status HostWorkerThread::ScheduleWork(Worker *worker, size_t size, co
|
||||
{
|
||||
exiting = true;
|
||||
}
|
||||
return ScheduleWorkNoLock(worker, size, data);
|
||||
|
||||
}
|
||||
LV2_Worker_Status HostWorkerThread::ScheduleWorkNoLock(Worker *worker, size_t size, const void *data)
|
||||
{
|
||||
|
||||
if (requestRingBuffer.writeSpace() < sizeof(worker) + sizeof(size) + size)
|
||||
{
|
||||
@@ -307,7 +328,12 @@ LV2_Worker_Status HostWorkerThread::ScheduleWork(Worker *worker, size_t size, co
|
||||
|
||||
void Worker::RunBackgroundTask(size_t size, uint8_t *data)
|
||||
{
|
||||
workerInterface->work(lilvInstance->lv2_handle, worker_respond_fn, (LV2_Handle)this, size, data);
|
||||
try {
|
||||
workerInterface->work(lilvInstance->lv2_handle, worker_respond_fn, (LV2_Handle)this, size, data);
|
||||
} catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(SS("Unhandled exception on LV2 Worker thread: " << e.what()));
|
||||
}
|
||||
{
|
||||
std::lock_guard lock { this->outstandingRequestMutex};
|
||||
--this->outstandingRequests;
|
||||
|
||||
+9
-6
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
// Copyright (c) 2025 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "lv2/atom/atom.h"
|
||||
#include "lv2/worker/worker.h"
|
||||
#include "condition_variable"
|
||||
#include <atomic>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
@@ -51,10 +52,13 @@ namespace pipedal {
|
||||
HostWorkerThread();
|
||||
~HostWorkerThread();
|
||||
|
||||
bool StartThread();
|
||||
void Close();
|
||||
bool Closed() const { return closed || pThread == nullptr; }
|
||||
LV2_Worker_Status ScheduleWork(Worker*worker, size_t size, const void*data);
|
||||
private:
|
||||
bool closed = false;
|
||||
LV2_Worker_Status ScheduleWorkNoLock(Worker*worker, size_t size, const void*data);
|
||||
std::atomic<bool> closed = false;
|
||||
std::unique_ptr<std::thread> pThread;
|
||||
void ThreadProc() noexcept;
|
||||
|
||||
@@ -64,8 +68,6 @@ namespace pipedal {
|
||||
|
||||
std::vector<uint8_t> dataBuffer;
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
class Worker {
|
||||
@@ -75,8 +77,8 @@ namespace pipedal {
|
||||
LilvInstance*lilvInstance;
|
||||
const LV2_Worker_Interface*workerInterface;
|
||||
|
||||
bool closed = false;
|
||||
bool exiting = false;
|
||||
std::atomic<bool> closed = false;
|
||||
std::atomic<bool> exiting = false;
|
||||
RingBuffer<true,false> responseRingBuffer;
|
||||
|
||||
std::vector<uint8_t> responseBuffer;
|
||||
@@ -94,6 +96,7 @@ namespace pipedal {
|
||||
public:
|
||||
Worker(const std::shared_ptr<HostWorkerThread>& pHostWorker,LilvInstance *instance, const LV2_Worker_Interface *iface);
|
||||
~Worker();
|
||||
|
||||
void Close();
|
||||
|
||||
LV2_Worker_Status ScheduleWork(
|
||||
|
||||
@@ -106,6 +106,27 @@ static bool isJackServiceRunning()
|
||||
// }
|
||||
#endif
|
||||
|
||||
#define ENABLE_SEGV_DEBUG 0
|
||||
#if ENABLE_SEGV_DEBUG
|
||||
void debug_segvHandler(int sig)
|
||||
{
|
||||
// Print out all the frames to stderr
|
||||
const char *message = "Error: SEGV signal received.\n";
|
||||
auto _ = write(STDERR_FILENO, message, strlen(message));
|
||||
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
static void EnableDebugSevHandler()
|
||||
{
|
||||
signal(SIGSEGV, debug_segvHandler);
|
||||
signal(SIGILL, debug_segvHandler);
|
||||
signal(SIGKILL, debug_segvHandler);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
static bool TryGetLogLevel(const std::string &strLogLevel, LogLevel *result)
|
||||
{
|
||||
if (strLogLevel == "debug")
|
||||
@@ -319,14 +340,25 @@ int main(int argc, char *argv[])
|
||||
sigaddset(&sigSet, SIGTERM);
|
||||
sigaddset(&sigSet, SIGUSR1);
|
||||
|
||||
// block these signasl for all threads.
|
||||
s = pthread_sigmask(SIG_BLOCK, &sigSet, NULL);
|
||||
if (s != 0)
|
||||
{
|
||||
throw std::logic_error("pthread_sigmask failed.");
|
||||
}
|
||||
|
||||
// Clear any pending signals before waiting
|
||||
struct timespec timeout = {0, 0};
|
||||
while (sigtimedwait(&sigSet, NULL, &timeout) > 0) {
|
||||
// Consume any pending signals
|
||||
}
|
||||
#if ENABLE_SEGV_DEBUG
|
||||
EnableDebugSevHandler();
|
||||
#endif
|
||||
|
||||
PiPedalModel model;
|
||||
|
||||
|
||||
model.SetNetworkChangedListener(
|
||||
[&server]() mutable
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user