Dummy audio thread to keep display responsive when no audio device.

This commit is contained in:
Robin Davies
2024-08-26 22:42:15 -04:00
parent 150ca25491
commit 179d1aa025
18 changed files with 972 additions and 332 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"socket_server_port": 80, "socket_server_port": 8080,
"socket_server_address": "*", "socket_server_address": "*",
"debug": true, "debug": true,
"max_upload_size": 536870912, "max_upload_size": 536870912,
+1 -1
View File
@@ -138,7 +138,7 @@ export default class JackHostStatus {
<Typography variant="caption" color="inherit">{label}</Typography> <Typography variant="caption" color="inherit">{label}</Typography>
<span style={{ color: RED_COLOR }}> <span style={{ color: RED_COLOR }}>
<Typography variant="caption" color="inherit">{status.errorMessage === "" ? "Stopped" : status.errorMessage}&nbsp;&nbsp;</Typography> <Typography variant="caption" color="inherit">{status.errorMessage === "" ? "Audio\u00A0Stopped" : status.errorMessage}&nbsp;&nbsp;</Typography>
</span> </span>
{ {
status.temperaturemC > -100000 && status.temperaturemC > -100000 &&
+42 -6
View File
@@ -239,10 +239,10 @@ namespace pipedal
unsigned int periods = 0; unsigned int periods = 0;
snd_pcm_hw_params_t *captureHwParams; snd_pcm_hw_params_t *captureHwParams = nullptr;
snd_pcm_sw_params_t *captureSwParams; snd_pcm_sw_params_t *captureSwParams = nullptr;
snd_pcm_hw_params_t *playbackHwParams; snd_pcm_hw_params_t *playbackHwParams = nullptr;
snd_pcm_sw_params_t *playbackSwParams; snd_pcm_sw_params_t *playbackSwParams = nullptr;
bool capture_and_playback_not_synced = false; bool capture_and_playback_not_synced = false;
@@ -1469,6 +1469,32 @@ namespace pipedal
Lv2Log::error("ALSA audio thread terminated abnormally."); Lv2Log::error("ALSA audio thread terminated abnormally.");
} }
this->driverHost->OnAudioStopped(); this->driverHost->OnAudioStopped();
// if we terminated abnormally, pump messages until we have been terminated.
if (!terminateAudio())
{
// zero out input buffers.
for (size_t i = 0; i < this->captureBuffers.size(); ++i)
{
float*pBuffer = captureBuffers[i];
for (size_t j = 0; j < this->bufferSize; ++j)
{
pBuffer[j] = 0;
}
}
while(!terminateAudio())
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// zero out input buffers.
this->driverHost->OnProcess(this->bufferSize);
}
}
this->driverHost->OnAudioTerminated();
} }
bool alsaActive = false; bool alsaActive = false;
@@ -1947,6 +1973,17 @@ namespace pipedal
std::vector<std::string> &inputAudioPorts, std::vector<std::string> &inputAudioPorts,
std::vector<std::string> &outputAudioPorts) std::vector<std::string> &outputAudioPorts)
{ {
if (jackServerSettings.IsDummyAudioDevice())
{
inputAudioPorts.clear();
inputAudioPorts.push_back("system::capture_0");
inputAudioPorts.push_back("system::capture_1");
outputAudioPorts.clear();
outputAudioPorts.push_back("system::playback_0");
outputAudioPorts.push_back("system::playback_1");
return true;
}
snd_pcm_t *playbackHandle = nullptr; snd_pcm_t *playbackHandle = nullptr;
snd_pcm_t *captureHandle = nullptr; snd_pcm_t *captureHandle = nullptr;
@@ -1958,7 +1995,7 @@ namespace pipedal
try try
{ {
int err; int err;
for (int retry = 0; retry < 15; ++retry) for (int retry = 0; retry < 2; ++retry)
{ {
err = snd_pcm_open(&playbackHandle, alsaDeviceName.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); 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. if (err < 0) // field report of a device that is present, but won't immediately open.
@@ -2044,7 +2081,6 @@ namespace pipedal
} }
catch (const std::exception &e) catch (const std::exception &e)
{ {
Lv2Log::warning(SS("Unable to read ALSA configuration for " << alsaDeviceName << ". " << e.what() << "."));
result = false; result = false;
throw; throw;
} }
+3
View File
@@ -208,6 +208,9 @@ public:
Oscillator oscillator; Oscillator oscillator;
LatencyMonitor latencyMonitor; LatencyMonitor latencyMonitor;
virtual void OnAudioTerminated() {
}
virtual void OnAudioStopped() { virtual void OnAudioStopped() {
} }
+2 -2
View File
@@ -24,8 +24,8 @@
#pragma once #pragma once
#ifndef JACK_HOST #ifndef JACK_HOST // jack is no longer supported.
#define JACK_HOST 0 #define JACK_HOST 0
#endif #endif
#ifndef ALSA_HOST #ifndef ALSA_HOST
+1
View File
@@ -51,6 +51,7 @@ namespace pipedal {
virtual void OnProcess(size_t nFrames) = 0; virtual void OnProcess(size_t nFrames) = 0;
virtual void OnUnderrun() = 0; virtual void OnUnderrun() = 0;
virtual void OnAudioStopped() = 0; virtual void OnAudioStopped() = 0;
virtual void OnAudioTerminated() = 0;
}; };
+167 -143
View File
@@ -24,6 +24,7 @@
#include "JackDriver.hpp" #include "JackDriver.hpp"
#include "AlsaDriver.hpp" #include "AlsaDriver.hpp"
#include "DummyAudioDriver.hpp"
#include "AtomConverter.hpp" #include "AtomConverter.hpp"
using namespace pipedal; using namespace pipedal;
@@ -49,6 +50,7 @@ using namespace pipedal;
#include <fstream> #include <fstream>
#include "Lv2EventBufferWriter.hpp" #include "Lv2EventBufferWriter.hpp"
#include "InheritPriorityMutex.hpp" #include "InheritPriorityMutex.hpp"
#include <atomic>
#ifdef __linux__ #ifdef __linux__
#include <sched.h> #include <sched.h>
@@ -105,73 +107,88 @@ static std::string GetGovernor()
return pipedal::GetCpuGovernor(); return pipedal::GetCpuGovernor();
} }
class SystemMidiBinding
class SystemMidiBinding { {
private: private:
MidiBinding currentBinding; MidiBinding currentBinding;
bool controlState = false; bool controlState = false;
public:
void SetBinding(const MidiBinding&binding) { currentBinding = binding; controlState = false; }
bool IsMatch(const MidiEvent&event); public:
bool IsTriggered(const MidiEvent&event); void SetBinding(const MidiBinding &binding)
{
currentBinding = binding;
controlState = false;
}
bool IsMatch(const MidiEvent &event);
bool IsTriggered(const MidiEvent &event);
}; };
bool SystemMidiBinding::IsTriggered(const MidiEvent &event) bool SystemMidiBinding::IsTriggered(const MidiEvent &event)
{ {
switch (currentBinding.bindingType()) switch (currentBinding.bindingType())
{ {
case BINDING_TYPE_NOTE: case BINDING_TYPE_NOTE:
{ {
if (event.size != 3) return false; if (event.size != 3)
auto command = event.buffer[0] & 0xF0;
if (command != 0x90) return false; // MIDI note on
if (event.buffer[1] != currentBinding.note()) return false;
return event.buffer[2] != 0; // note-on with velicity of zero is a note-off.
}
break;
case BINDING_TYPE_CONTROL:
{
if (event.size != 3) return false;
auto command = event.buffer[0] & 0xF0;
if (command != 0xB0) return false;
if (event.buffer[1] != currentBinding.control()) return false;
bool state = event.buffer[2] >= 0x64;
if (state != this->controlState)
{
this->controlState = state;
return state;
}
return false; return false;
} auto command = event.buffer[0] & 0xF0;
break; if (command != 0x90)
default: return false; // MIDI note on
if (event.buffer[1] != currentBinding.note())
return false; return false;
return event.buffer[2] != 0; // note-on with velicity of zero is a note-off.
}
break;
case BINDING_TYPE_CONTROL:
{
if (event.size != 3)
return false;
auto command = event.buffer[0] & 0xF0;
if (command != 0xB0)
return false;
if (event.buffer[1] != currentBinding.control())
return false;
bool state = event.buffer[2] >= 0x64;
if (state != this->controlState)
{
this->controlState = state;
return state;
}
return false;
}
break;
default:
return false;
} }
} }
bool SystemMidiBinding::IsMatch(const MidiEvent&event) bool SystemMidiBinding::IsMatch(const MidiEvent &event)
{ {
switch (currentBinding.bindingType()) switch (currentBinding.bindingType())
{ {
case BINDING_TYPE_NOTE: case BINDING_TYPE_NOTE:
{ {
if (event.size != 3) return false; if (event.size != 3)
auto command = event.buffer[0] & 0xF0;
if (command != 0x80 && command != 0x90) return false; // note on/note off.
return event.buffer[1] == currentBinding.note();
}
break;
case BINDING_TYPE_CONTROL:
{
if (event.size != 3) return false;
auto command = event.buffer[0] & 0xF0;
if (command != 0xB0) return false;
return event.buffer[1] == currentBinding.control();
}
break;
default:
return false; return false;
auto command = event.buffer[0] & 0xF0;
if (command != 0x80 && command != 0x90)
return false; // note on/note off.
return event.buffer[1] == currentBinding.note();
}
break;
case BINDING_TYPE_CONTROL:
{
if (event.size != 3)
return false;
auto command = event.buffer[0] & 0xF0;
if (command != 0xB0)
return false;
return event.buffer[1] == currentBinding.control();
}
break;
default:
return false;
} }
} }
@@ -181,7 +198,6 @@ private:
IHost *pHost = nullptr; IHost *pHost = nullptr;
LV2_Atom_Forge inputWriterForge; LV2_Atom_Forge inputWriterForge;
std::mutex atomConverterMutex; std::mutex atomConverterMutex;
AtomConverter atomConverter; AtomConverter atomConverter;
@@ -254,7 +270,7 @@ private:
Uris uris; Uris uris;
AudioDriver *audioDriver = nullptr; std::unique_ptr<AudioDriver> audioDriver;
std::recursive_mutex mutex; std::recursive_mutex mutex;
int64_t overrunGracePeriodSamples = 0; int64_t overrunGracePeriodSamples = 0;
@@ -282,7 +298,9 @@ private:
SystemMidiBinding prevMidiBinding; SystemMidiBinding prevMidiBinding;
JackChannelSelection channelSelection; JackChannelSelection channelSelection;
bool active = false; std::atomic<bool> active = false;
std::atomic<bool> audioStopped = false;
std::atomic<bool> isDummyAudioDriver = false;
std::shared_ptr<Lv2Pedalboard> currentPedalboard; std::shared_ptr<Lv2Pedalboard> currentPedalboard;
std::vector<std::shared_ptr<Lv2Pedalboard>> activePedalboards; // pedalboards that have been sent to the audio queue. std::vector<std::shared_ptr<Lv2Pedalboard>> activePedalboards; // pedalboards that have been sent to the audio queue.
@@ -307,7 +325,6 @@ private:
return pHost->Lv2UridToString(pAtom->body.otype); return pHost->Lv2UridToString(pAtom->body.otype);
} }
virtual std::string AtomToJson(const LV2_Atom *pAtom) override virtual std::string AtomToJson(const LV2_Atom *pAtom) override
{ {
std::lock_guard lock(atomConverterMutex); std::lock_guard lock(atomConverterMutex);
@@ -315,7 +332,7 @@ private:
std::stringstream s; std::stringstream s;
json_writer writer(s); json_writer writer(s);
writer.write(vAtom); writer.write(vAtom);
return s.str(); return s.str();
} }
@@ -328,7 +345,7 @@ private:
virtual void Close() virtual void Close()
{ {
std::lock_guard guard {mutex}; std::lock_guard guard{mutex};
if (!isOpen) if (!isOpen)
return; return;
@@ -350,7 +367,6 @@ private:
StopReaderThread(); StopReaderThread();
// release any pdealboards owned by the process thread. // release any pdealboards owned by the process thread.
this->activePedalboards.resize(0); this->activePedalboards.resize(0);
this->realtimeActivePedalboard = nullptr; this->realtimeActivePedalboard = nullptr;
@@ -375,7 +391,7 @@ private:
delete[] midiLv2Buffers[i]; delete[] midiLv2Buffers[i];
} }
midiLv2Buffers.resize(0); midiLv2Buffers.resize(0);
audioDriver = nullptr;
} }
void ZeroBuffer(float *buffer, size_t nframes) void ZeroBuffer(float *buffer, size_t nframes)
@@ -424,8 +440,7 @@ private:
} }
} }
virtual void SetSystemMidiBindings(const std::vector<MidiBinding> &bindings);
virtual void SetSystemMidiBindings(const std::vector<MidiBinding>&bindings);
void writeVu() void writeVu()
{ {
@@ -633,7 +648,7 @@ private:
return (event.size == 3 && event.buffer[0] == 0xB0 && event.buffer[1] == 0x00); return (event.size == 3 && event.buffer[0] == 0xB0 && event.buffer[1] == 0x00);
} }
bool onMidiEvent(Lv2EventBufferWriter&eventBufferWriter, Lv2EventBufferWriter::LV2_EvBuf_Iterator &iterator, MidiEvent&event) bool onMidiEvent(Lv2EventBufferWriter &eventBufferWriter, Lv2EventBufferWriter::LV2_EvBuf_Iterator &iterator, MidiEvent &event)
{ {
eventBufferWriter.writeMidiEvent(iterator, 0, event.size, event.buffer); eventBufferWriter.writeMidiEvent(iterator, 0, event.size, event.buffer);
@@ -654,7 +669,6 @@ private:
return true; return true;
} }
void ProcessMidiInput() void ProcessMidiInput()
{ {
Lv2EventBufferWriter eventBufferWriter(this->eventBufferUrids); Lv2EventBufferWriter eventBufferWriter(this->eventBufferUrids);
@@ -676,7 +690,6 @@ private:
Lv2EventBufferWriter::LV2_EvBuf_Iterator iterator = eventBufferWriter.begin(); Lv2EventBufferWriter::LV2_EvBuf_Iterator iterator = eventBufferWriter.begin();
MidiEvent event; MidiEvent event;
// write all deferred midi messages. // write all deferred midi messages.
if (deferredMidiMessageCount != 0 && !midiProgramChangePending) if (deferredMidiMessageCount != 0 && !midiProgramChangePending)
{ {
@@ -687,17 +700,16 @@ private:
if (deviceIndex == midiDeviceIx) if (deviceIndex == midiDeviceIx)
{ {
event.size = messageCount; event.size = messageCount;
event.buffer = deferredMidiMessages +i; event.buffer = deferredMidiMessages + i;
event.time = 0; event.time = 0;
onMidiEvent(eventBufferWriter,iterator,event); onMidiEvent(eventBufferWriter, iterator, event);
} }
i += messageCount; i += messageCount;
} }
} }
for (size_t frame = 0; frame < n; ++frame) for (size_t frame = 0; frame < n; ++frame)
{ {
if (audioDriver->GetMidiInputEvent(&event, portBuffer, frame)) if (audioDriver->GetMidiInputEvent(&event, portBuffer, frame))
@@ -708,32 +720,32 @@ private:
this->deferredMidiMessageCount = 0; // we can discard previous control changes. this->deferredMidiMessageCount = 0; // we can discard previous control changes.
midiProgramChangePending = true; midiProgramChangePending = true;
this->realtimeWriter.OnMidiProgramChange(++(this->midiProgramChangeId), selectedBank,event.buffer[1]); this->realtimeWriter.OnMidiProgramChange(++(this->midiProgramChangeId), selectedBank, event.buffer[1]);
} }
else if (isBankChange(event)) else if (isBankChange(event))
{ {
this->selectedBank = event.buffer[2]; this->selectedBank = event.buffer[2];
} else if (this->nextMidiBinding.IsMatch(event)) }
else if (this->nextMidiBinding.IsMatch(event))
{ {
if (nextMidiBinding.IsTriggered(event)) if (nextMidiBinding.IsTriggered(event))
{ {
midiProgramChangePending = true; midiProgramChangePending = true;
this->realtimeWriter.OnNextMidiProgram(++(this->midiProgramChangeId),1); this->realtimeWriter.OnNextMidiProgram(++(this->midiProgramChangeId), 1);
} }
} else if (this->prevMidiBinding.IsMatch(event)) }
else if (this->prevMidiBinding.IsMatch(event))
{ {
if (prevMidiBinding.IsTriggered(event)) if (prevMidiBinding.IsTriggered(event))
{ {
midiProgramChangePending = true; midiProgramChangePending = true;
this->realtimeWriter.OnNextMidiProgram(++(this->midiProgramChangeId),-1); this->realtimeWriter.OnNextMidiProgram(++(this->midiProgramChangeId), -1);
} }
} }
else if (midiProgramChangePending) else if (midiProgramChangePending)
{ {
// defer the message for processing after the program change has completed. // defer the message for processing after the program change has completed.
if (event.size > 0 if (event.size > 0 && event.size < 128 && event.size + 2 + this->deferredMidiMessageCount < DEFERRED_MIDI_BUFFER_SIZE)
&& event.size < 128
&& event.size + 2 + this->deferredMidiMessageCount < DEFERRED_MIDI_BUFFER_SIZE)
{ {
this->deferredMidiMessages[deferredMidiMessageCount++] = midiDeviceIx; this->deferredMidiMessages[deferredMidiMessageCount++] = midiDeviceIx;
this->deferredMidiMessages[deferredMidiMessageCount++] = (uint8_t)event.size; this->deferredMidiMessages[deferredMidiMessageCount++] = (uint8_t)event.size;
@@ -745,7 +757,7 @@ private:
} }
else else
{ {
onMidiEvent(eventBufferWriter,iterator,event); onMidiEvent(eventBufferWriter, iterator, event);
} }
} }
} }
@@ -757,18 +769,24 @@ private:
std::mutex audioStoppedMutex; std::mutex audioStoppedMutex;
bool IsAudioActive()
bool IsAudioRunning()
{ {
std::lock_guard lock{audioStoppedMutex}; return this->active && (!this->audioStopped) && !this->isDummyAudioDriver;
return this->active;
} }
virtual void OnAudioStopped() virtual void OnAudioStopped() override
{
this->audioStopped = true;
Lv2Log::info("Audio stopped.");
}
virtual void OnAudioTerminated() override
{ {
std::lock_guard lock{audioStoppedMutex};
this->active = false; this->active = false;
Lv2Log::info("Audio stopped."); Lv2Log::info("Audio stopped.");
} }
virtual void OnProcess(size_t nframes) virtual void OnProcess(size_t nframes)
{ {
try try
@@ -822,7 +840,7 @@ private:
{ {
if (this->realtimeVuBuffers != nullptr) if (this->realtimeVuBuffers != nullptr)
{ {
pedalboard->ComputeVus(this->realtimeVuBuffers, (uint32_t)nframes,inputBuffers,outputBuffers); pedalboard->ComputeVus(this->realtimeVuBuffers, (uint32_t)nframes, inputBuffers, outputBuffers);
vuSamplesRemaining -= nframes; vuSamplesRemaining -= nframes;
if (vuSamplesRemaining <= 0) if (vuSamplesRemaining <= 0)
@@ -862,6 +880,7 @@ private:
throw; throw;
} }
} }
public: public:
AudioHostImpl(IHost *pHost) AudioHostImpl(IHost *pHost)
: inputRingBuffer(RING_BUFFER_SIZE), : inputRingBuffer(RING_BUFFER_SIZE),
@@ -875,25 +894,17 @@ public:
uris(pHost), uris(pHost),
atomConverter(pHost->GetMapFeature()) atomConverter(pHost->GetMapFeature())
{ {
realtimeAtomBuffer.resize(32*1024); realtimeAtomBuffer.resize(32 * 1024);
lv2_atom_forge_init(&inputWriterForge,pHost->GetMapFeature().GetMap()); lv2_atom_forge_init(&inputWriterForge, pHost->GetMapFeature().GetMap());
#if JACK_HOST
audioDriver = CreateJackDriver(this);
#endif
#if ALSA_HOST
audioDriver = CreateAlsaDriver(this);
#endif
} }
virtual ~AudioHostImpl() virtual ~AudioHostImpl()
{ {
Close(); Close();
CleanRestartThreads(true); CleanRestartThreads(true);
audioDriver = nullptr;
delete audioDriver;
} }
virtual JackConfiguration GetServerConfiguration() virtual JackConfiguration GetServerConfiguration()
{ {
JackConfiguration result; JackConfiguration result;
@@ -907,8 +918,6 @@ public:
return this->sampleRate; return this->sampleRate;
} }
void OnAudioComplete() void OnAudioComplete()
{ {
// there is actually no compelling circumstance in which this should ever happen. // there is actually no compelling circumstance in which this should ever happen.
@@ -942,7 +951,7 @@ public:
{ {
Lv2Log::debug("Service thread priority successfully boosted."); Lv2Log::debug("Service thread priority successfully boosted.");
} }
SetThreadName("aout"); SetThreadName("rtsvc");
#else #else
xxx; // TODO! xxx; // TODO!
#endif #endif
@@ -957,9 +966,9 @@ public:
using clock_duration = clock_time::duration; using clock_duration = clock_time::duration;
// check for overruns every 30 seconds. // check for overruns every 30 seconds.
clock_duration waitPeriod = clock_duration waitPeriod =
std::chrono::duration_cast<clock_duration>(std::chrono::seconds(30)); std::chrono::duration_cast<clock_duration>(std::chrono::seconds(30));
clock_time waitTime = std::chrono::steady_clock::now(); clock_time waitTime = std::chrono::steady_clock::now();
while (true) while (true)
{ {
@@ -1022,7 +1031,7 @@ public:
RealtimePatchPropertyRequest *pRequest = nullptr; RealtimePatchPropertyRequest *pRequest = nullptr;
hostReader.read(&pRequest); hostReader.read(&pRequest);
std::shared_ptr<Lv2Pedalboard> currentpedalboard; std::shared_ptr<Lv2Pedalboard> currentpedalboard;
{ {
std::lock_guard guard(mutex); std::lock_guard guard(mutex);
@@ -1036,7 +1045,8 @@ public:
{ {
if (pRequest->errorMessage == nullptr) if (pRequest->errorMessage == nullptr)
{ {
if (pRequest->GetSize() != 0) { if (pRequest->GetSize() != 0)
{
IEffect *pEffect = currentPedalboard->GetEffect(pRequest->instanceId); IEffect *pEffect = currentPedalboard->GetEffect(pRequest->instanceId);
if (pEffect == nullptr) if (pEffect == nullptr)
{ {
@@ -1044,9 +1054,11 @@ public:
} }
else else
{ {
pRequest->jsonResponse = AtomToJson((LV2_Atom*)pRequest->GetBuffer()); pRequest->jsonResponse = AtomToJson((LV2_Atom *)pRequest->GetBuffer());
} }
} else { }
else
{
pRequest->errorMessage = "Plugin did not respond."; pRequest->errorMessage = "Plugin did not respond.";
} }
} }
@@ -1107,13 +1119,13 @@ public:
IEffect *pEffect = currentPedalboard->GetEffect(instanceId); IEffect *pEffect = currentPedalboard->GetEffect(instanceId);
if (pEffect != nullptr && this->pNotifyCallbacks) if (pEffect != nullptr && this->pNotifyCallbacks)
{ {
LV2_Atom *atom = (LV2_Atom*)&atomBuffer[0]; LV2_Atom *atom = (LV2_Atom *)&atomBuffer[0];
if (atom->type == uris.atom_Object) if (atom->type == uris.atom_Object)
{ {
LV2_Atom_Object *obj = (LV2_Atom_Object*)atom; LV2_Atom_Object *obj = (LV2_Atom_Object *)atom;
if (obj->body.otype == uris.patch_Set) if (obj->body.otype == uris.patch_Set)
{ {
// Get the property and value of the set message // Get the property and value of the set message
const LV2_Atom *property = nullptr; const LV2_Atom *property = nullptr;
const LV2_Atom *value = nullptr; const LV2_Atom *value = nullptr;
@@ -1125,8 +1137,8 @@ public:
0); 0);
if (property != nullptr && value != nullptr && property->type == uris.atom_URID) if (property != nullptr && value != nullptr && property->type == uris.atom_URID)
{ {
LV2_URID propertyUrid = ((LV2_Atom_URID*)property)->body; LV2_URID propertyUrid = ((LV2_Atom_URID *)property)->body;
this->pNotifyCallbacks->OnPatchSetReply(instanceId,propertyUrid,value); this->pNotifyCallbacks->OnPatchSetReply(instanceId, propertyUrid, value);
this->pNotifyCallbacks->OnNotifyMaybeLv2StateChanged(instanceId); this->pNotifyCallbacks->OnNotifyMaybeLv2StateChanged(instanceId);
} }
} }
@@ -1157,31 +1169,34 @@ public:
hostReader.read(&body); hostReader.read(&body);
OnAudioComplete(); OnAudioComplete();
return; return;
} else if (command == RingBufferCommand::MidiProgramChange) }
else if (command == RingBufferCommand::MidiProgramChange)
{ {
RealtimeMidiProgramRequest programRequest; RealtimeMidiProgramRequest programRequest;
hostReader.read(&programRequest); hostReader.read(&programRequest);
OnMidiProgramRequest(programRequest); OnMidiProgramRequest(programRequest);
} else if (command == RingBufferCommand::NextMidiProgram) }
else if (command == RingBufferCommand::NextMidiProgram)
{ {
RealtimeNextMidiProgramRequest request; RealtimeNextMidiProgramRequest request;
hostReader.read(&request); hostReader.read(&request);
pNotifyCallbacks->OnNotifyNextMidiProgram(request); pNotifyCallbacks->OnNotifyNextMidiProgram(request);
} else if (command == RingBufferCommand::Lv2ErrorMessage) }
else if (command == RingBufferCommand::Lv2ErrorMessage)
{ {
size_t size; size_t size;
int64_t instanceId; int64_t instanceId;
hostReader.read(&instanceId); hostReader.read(&instanceId);
hostReader.read(&size); hostReader.read(&size);
if (this->atomBuffer.size() < size+1) if (this->atomBuffer.size() < size + 1)
{ {
this->atomBuffer.resize(size+1); this->atomBuffer.resize(size + 1);
} }
hostReader.read(size, &(atomBuffer[0])); hostReader.read(size, &(atomBuffer[0]));
char *p = (char*)&(atomBuffer[0]); char *p = (char *)&(atomBuffer[0]);
p[size] = 0; p[size] = 0;
std::string message(p); std::string message(p);
pNotifyCallbacks->OnNotifyLv2RealtimeError(instanceId,message); pNotifyCallbacks->OnNotifyLv2RealtimeError(instanceId, message);
} }
else else
{ {
@@ -1245,6 +1260,22 @@ public:
{ {
std::lock_guard guard(mutex); std::lock_guard guard(mutex);
if (isOpen)
{
throw PiPedalStateException("Already open.");
}
isOpen = true;
if (jackServerSettings.IsDummyAudioDevice())
{
this->isDummyAudioDriver = true;
this->audioDriver = std::unique_ptr<AudioDriver>(CreateDummyAudioDriver(this));
} else {
this->isDummyAudioDriver = false;
this->audioDriver = std::unique_ptr<AudioDriver>(CreateAlsaDriver(this));
}
if (channelSelection.GetInputAudioPorts().size() == 0 || channelSelection.GetOutputAudioPorts().size() == 0) if (channelSelection.GetInputAudioPorts().size() == 0 || channelSelection.GetOutputAudioPorts().size() == 0)
{ {
return; return;
@@ -1253,11 +1284,6 @@ public:
this->currentSample = 0; this->currentSample = 0;
this->underruns = 0; this->underruns = 0;
if (isOpen)
{
throw PiPedalStateException("Already open.");
}
isOpen = true;
this->inputRingBuffer.reset(); this->inputRingBuffer.reset();
this->outputRingBuffer.reset(); this->outputRingBuffer.reset();
@@ -1272,7 +1298,6 @@ public:
try try
{ {
audioDriver->Open(jackServerSettings, this->channelSelection); audioDriver->Open(jackServerSettings, this->channelSelection);
this->sampleRate = audioDriver->GetSampleRate(); this->sampleRate = audioDriver->GetSampleRate();
@@ -1287,12 +1312,12 @@ public:
} }
active = true; active = true;
audioStopped = false;
audioDriver->Activate(); audioDriver->Activate();
Lv2Log::info(SS("Audio started. " << audioDriver->GetConfigurationDescription())); Lv2Log::info(SS("Audio started. " << audioDriver->GetConfigurationDescription()));
} }
catch (const std::exception &e) catch (const std::exception &e)
{ {
Lv2Log::error(SS("Failed to start audio. " << e.what()));
Close(); Close();
active = false; active = false;
throw; throw;
@@ -1368,7 +1393,6 @@ public:
} }
} }
} }
} }
void SetControlValue(uint64_t instanceId, const std::string &symbol, float value) void SetControlValue(uint64_t instanceId, const std::string &symbol, float value)
@@ -1387,22 +1411,22 @@ public:
} }
} }
virtual void SetInputVolume(float value) { virtual void SetInputVolume(float value)
{
std::lock_guard guard(mutex); std::lock_guard guard(mutex);
if (active && this->currentPedalboard) if (active && this->currentPedalboard)
{ {
hostWriter.SetInputVolume(value); hostWriter.SetInputVolume(value);
} }
} }
virtual void SetOutputVolume(float value) { virtual void SetOutputVolume(float value)
{
std::lock_guard guard(mutex); std::lock_guard guard(mutex);
if (active && this->currentPedalboard) if (active && this->currentPedalboard)
{ {
hostWriter.SetOutputVolume(value); hostWriter.SetOutputVolume(value);
} }
} }
virtual void SetVuSubscriptions(const std::vector<int64_t> &instanceIds) virtual void SetVuSubscriptions(const std::vector<int64_t> &instanceIds)
@@ -1436,7 +1460,9 @@ public:
vuConfig->vuUpdateWorkingData.push_back(v); vuConfig->vuUpdateWorkingData.push_back(v);
vuConfig->vuUpdateResponseData.push_back(v); vuConfig->vuUpdateResponseData.push_back(v);
} else if (instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID) { }
else if (instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID)
{
int index = (int)instanceId; int index = (int)instanceId;
VuUpdate v; VuUpdate v;
vuConfig->enabledIndexes.push_back(index); vuConfig->enabledIndexes.push_back(index);
@@ -1444,12 +1470,13 @@ public:
if (instanceId == Pedalboard::INPUT_VOLUME_ID) if (instanceId == Pedalboard::INPUT_VOLUME_ID)
{ {
v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfInputAudioChannels() > 1; v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfInputAudioChannels() > 1;
} else { }
v.isStereoInput_ = v.isStereoOutput_ =this->pHost->GetNumberOfOutputAudioChannels() > 1; else
{
v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfOutputAudioChannels() > 1;
} }
vuConfig->vuUpdateWorkingData.push_back(v); vuConfig->vuUpdateWorkingData.push_back(v);
vuConfig->vuUpdateResponseData.push_back(v); vuConfig->vuUpdateResponseData.push_back(v);
} }
} }
@@ -1499,8 +1526,7 @@ public:
} }
private: private:
virtual bool UpdatePluginStates(Pedalboard &pedalboard) override;
virtual bool UpdatePluginStates(Pedalboard& pedalboard) override;
virtual bool UpdatePluginState(PedalboardItem &pedalboardItem) override; virtual bool UpdatePluginState(PedalboardItem &pedalboardItem) override;
class RestartThread class RestartThread
@@ -1575,7 +1601,7 @@ public:
} }
void CleanRestartThreads(bool final) void CleanRestartThreads(bool final)
{ {
std::lock_guard guard {restart_mutex}; std::lock_guard guard{restart_mutex};
for (size_t i = 0; i < restartThreads.size(); ++i) for (size_t i = 0; i < restartThreads.size(); ++i)
{ {
if (final) if (final)
@@ -1641,7 +1667,7 @@ public:
result.temperaturemC_ = GetRaspberryPiTemperature(); result.temperaturemC_ = GetRaspberryPiTemperature();
result.active_ = IsAudioActive(); result.active_ = IsAudioRunning();
result.restarting_ = this->restarting; result.restarting_ = this->restarting;
if (this->audioDriver != nullptr) if (this->audioDriver != nullptr)
@@ -1680,31 +1706,31 @@ static std::string UriToFieldName(const std::string &uri)
return uri.substr(pos + 1); return uri.substr(pos + 1);
} }
void AudioHostImpl::SetSystemMidiBindings(const std::vector<MidiBinding>&bindings) void AudioHostImpl::SetSystemMidiBindings(const std::vector<MidiBinding> &bindings)
{ {
for (auto i = bindings.begin(); i != bindings.end(); ++i) for (auto i = bindings.begin(); i != bindings.end(); ++i)
{ {
if (i->symbol() == "nextProgram") if (i->symbol() == "nextProgram")
{ {
this->nextMidiBinding.SetBinding(*i); this->nextMidiBinding.SetBinding(*i);
} else if (i->symbol() == "prevProgram") }
else if (i->symbol() == "prevProgram")
{ {
this->prevMidiBinding.SetBinding(*i); this->prevMidiBinding.SetBinding(*i);
} }
} }
} }
AudioHost *AudioHost::CreateInstance(IHost *pHost) AudioHost *AudioHost::CreateInstance(IHost *pHost)
{ {
return new AudioHostImpl(pHost); return new AudioHostImpl(pHost);
} }
bool AudioHostImpl::UpdatePluginStates(Pedalboard& pedalboard) bool AudioHostImpl::UpdatePluginStates(Pedalboard &pedalboard)
{ {
bool changed = false; bool changed = false;
auto pedalboardItems = pedalboard.GetAllPlugins(); auto pedalboardItems = pedalboard.GetAllPlugins();
for (PedalboardItem* item : pedalboardItems) for (PedalboardItem *item : pedalboardItems)
{ {
if (!item->isSplit()) if (!item->isSplit())
{ {
@@ -1715,14 +1741,14 @@ bool AudioHostImpl::UpdatePluginStates(Pedalboard& pedalboard)
} }
} }
return true; return true;
} }
bool AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem) bool AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem)
{ {
IEffect*effect = currentPedalboard->GetEffect(pedalboardItem.instanceId()); IEffect *effect = currentPedalboard->GetEffect(pedalboardItem.instanceId());
if (effect != nullptr) if (effect != nullptr)
{ {
try { try
{
Lv2PluginState state; Lv2PluginState state;
if (!effect->GetLv2State(&state)) if (!effect->GetLv2State(&state))
{ {
@@ -1734,7 +1760,8 @@ bool AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem)
return true; return true;
} }
return false; return false;
} catch(const std::exception &e) }
catch (const std::exception &e)
{ {
Lv2Log::warning(SS(pedalboardItem.pluginName() << ": " << e.what())); Lv2Log::warning(SS(pedalboardItem.pluginName() << ": " << e.what()));
return false; return false;
@@ -1743,9 +1770,6 @@ bool AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem)
return false; return false;
} }
JSON_MAP_BEGIN(JackHostStatus) JSON_MAP_BEGIN(JackHostStatus)
JSON_MAP_REFERENCE(JackHostStatus, active) JSON_MAP_REFERENCE(JackHostStatus, active)
JSON_MAP_REFERENCE(JackHostStatus, errorMessage) JSON_MAP_REFERENCE(JackHostStatus, errorMessage)
+1 -1
View File
@@ -166,7 +166,7 @@ public:
class JackHostStatus { class JackHostStatus {
public: public:
bool active_; bool active_ = false;
std::string errorMessage_; std::string errorMessage_;
bool restarting_; bool restarting_;
uint64_t underruns_; uint64_t underruns_;
+1
View File
@@ -220,6 +220,7 @@ set (PIPEDAL_SOURCES
JackDriver.cpp JackDriver.hpp JackDriver.cpp JackDriver.hpp
AlsaDriver.cpp AlsaDriver.hpp AlsaDriver.cpp AlsaDriver.hpp
DummyAudioDriver.cpp DummyAudioDriver.hpp
AudioDriver.hpp AudioDriver.hpp
AudioConfig.hpp AudioConfig.hpp
+564
View File
@@ -0,0 +1,564 @@
/*
* MIT License
*
* Copyright (c) 2024 Robin E. R. 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
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "pch.h"
#include "util.hpp"
#include <bit>
#include <memory>
#include "ss.hpp"
#include "DummyAudioDriver.hpp"
#include "JackServerSettings.hpp"
#include <thread>
#include "RtInversionGuard.hpp"
#include "PiPedalException.hpp"
#include <atomic>
#include <chrono>
#include <thread>
#include "CpuUse.hpp"
#include <alsa/asoundlib.h>
#include "Lv2Log.hpp"
#include <limits>
#include "ss.hpp"
#undef ALSADRIVER_CONFIG_DBG
#ifdef ALSADRIVER_CONFIG_DBG
#include <stdio.h>
#endif
using namespace pipedal;
namespace pipedal
{
[[noreturn]] static void DummyError(const std::string &message)
{
throw PiPedalStateException(message);
}
class DummyDriverImpl : public AudioDriver
{
private:
pipedal::CpuUse cpuUse;
uint32_t sampleRate = 0;
uint32_t bufferSize;
uint32_t numberOfBuffers;
int playbackChannels = 2;
int captureChannels = 2;
uint32_t playbackSampleSize = 0;
uint32_t captureSampleSize = 0;
uint32_t playbackFrameSize = 0;
uint32_t captureFrameSize = 0;
std::vector<float *> activeCaptureBuffers;
std::vector<float *> activePlaybackBuffers;
std::vector<float *> captureBuffers;
std::vector<float *> playbackBuffers;
uint8_t *rawCaptureBuffer = nullptr;
uint8_t *rawPlaybackBuffer = nullptr;
AudioDriverHost *driverHost = nullptr;
public:
DummyDriverImpl(AudioDriverHost *driverHost)
: driverHost(driverHost)
{
}
virtual ~DummyDriverImpl()
{
Close();
}
private:
void OnShutdown()
{
Lv2Log::info("Dummy Audio Server has shut down.");
}
virtual uint32_t GetSampleRate()
{
return this->sampleRate;
}
JackServerSettings jackServerSettings;
unsigned int periods = 0;
std::atomic<bool> terminateAudio_ = false;
void terminateAudio(bool terminate)
{
this->terminateAudio_ = terminate;
}
bool terminateAudio()
{
return this->terminateAudio_;
}
private:
void DummyCleanup()
{
}
private:
void AllocateBuffers(std::vector<float *> &buffers, size_t n)
{
buffers.resize(n);
for (size_t i = 0; i < n; ++i)
{
buffers[i] = new float[this->bufferSize];
for (size_t j = 0; j < this->bufferSize; ++j)
{
buffers[i][j] = 0;
}
}
}
JackChannelSelection channelSelection;
bool open = false;
virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection)
{
terminateAudio_ = false;
if (open)
{
throw PiPedalStateException("Already open.");
}
this->jackServerSettings = jackServerSettings;
this->channelSelection = channelSelection;
this->sampleRate = jackServerSettings.GetSampleRate();
open = true;
try
{
OpenMidi(jackServerSettings, channelSelection);
OpenAudio(jackServerSettings, channelSelection);
}
catch (const std::exception &e)
{
Close();
throw;
}
}
virtual std::string GetConfigurationDescription()
{
std::string result = SS(
"DUMMY, "
<< "n/a"
<< ", " << "Native float"
<< ", " << this->sampleRate
<< ", " << this->bufferSize << "x" << this->numberOfBuffers
<< ", in: " << this->InputBufferCount() << "/" << this->captureChannels
<< ", out: " << this->OutputBufferCount() << "/" << this->playbackChannels);
return result;
}
void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection)
{
int err;
this->numberOfBuffers = jackServerSettings.GetNumberOfBuffers();
this->bufferSize = jackServerSettings.GetBufferSize();
AllocateBuffers(captureBuffers, 2);
AllocateBuffers(playbackBuffers, 2);
}
std::jthread *audioThread;
bool audioRunning;
bool block = false;
void AudioThread()
{
SetThreadName("dummyAudioDriver");
try
{
#if defined(__WIN32)
// bump thread prioriy two levels to
// ensure that the service thread doesn't
// get bogged down by UIwork. Doesn't have to be realtime, but it
// MUST run at higher priority than UI threads.
xxx; // TO DO.
#elif defined(__linux__)
int min = sched_get_priority_min(SCHED_RR);
int max = sched_get_priority_max(SCHED_RR);
struct sched_param param;
memset(&param, 0, sizeof(param));
param.sched_priority = RT_THREAD_PRIORITY;
int result = sched_setscheduler(0, SCHED_RR, &param);
if (result == 0)
{
Lv2Log::debug("Service thread priority successfully boosted.");
}
else
{
Lv2Log::error(SS("Failed to set ALSA AudioThread priority. (" << strerror(result) << ")"));
}
#else
xxx; // TODO for your platform.
#endif
bool ok = true;
while (true)
{
if (terminateAudio())
{
break;
}
ssize_t framesRead = this->bufferSize;
this->driverHost->OnProcess(framesRead);
/// no attempt at realtime. Just as long as we run occasionally.
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
}
catch (const std::exception &e)
{
Lv2Log::error(e.what());
Lv2Log::error("Dummy audio thread terminated abnormally.");
}
this->driverHost->OnAudioTerminated();
}
bool alsaActive = false;
static int IndexFromPortName(const std::string &s)
{
auto pos = s.find_last_of('_');
if (pos == std::string::npos)
{
throw std::invalid_argument("Bad port name.");
}
const char *p = s.c_str() + (pos + 1);
int v = atoi(p);
if (v < 0)
{
throw std::invalid_argument("Bad port name.");
}
return v;
}
bool activated = false;
virtual void Activate()
{
if (activated)
{
throw PiPedalStateException("Already activated.");
}
activated = true;
this->activeCaptureBuffers.resize(channelSelection.GetInputAudioPorts().size());
playbackBuffers.resize(2);
int ix = 0;
for (auto &x : channelSelection.GetInputAudioPorts())
{
int sourceIndex = IndexFromPortName(x);
if (sourceIndex >= captureBuffers.size())
{
Lv2Log::error(SS("Invalid audio input port: " << x));
}
else
{
this->activeCaptureBuffers[ix++] = this->captureBuffers[sourceIndex];
}
}
this->activePlaybackBuffers.resize(channelSelection.GetOutputAudioPorts().size());
ix = 0;
for (auto &x : channelSelection.GetOutputAudioPorts())
{
int sourceIndex = IndexFromPortName(x);
if (sourceIndex >= playbackBuffers.size())
{
Lv2Log::error(SS("Invalid audio output port: " << x));
}
else
{
this->activePlaybackBuffers[ix++] = this->playbackBuffers[sourceIndex];
}
}
audioThread = new std::jthread([this]()
{ AudioThread(); });
}
virtual void Deactivate()
{
if (!activated)
{
return;
}
activated = false;
terminateAudio(true);
if (audioThread)
{
this->audioThread->join();
this->audioThread = 0;
}
Lv2Log::debug("Audio thread joined.");
}
static constexpr size_t MIDI_BUFFER_SIZE = 16 * 1024;
static constexpr size_t MAX_MIDI_EVENT = 4 * 1024;
public:
class MidiState
{
private:
snd_rawmidi_t *hIn = nullptr;
snd_rawmidi_params_t *hInParams = nullptr;
std::string deviceName;
// running status state.
uint8_t runningStatus = 0;
int dataLength = 0;
int dataIndex = 0;
size_t statusBytesRemaining = 0;
size_t data0;
size_t data1;
size_t eventCount = 0;
MidiEvent events[MAX_MIDI_EVENT];
size_t bufferCount = 0;
uint8_t buffer[MIDI_BUFFER_SIZE];
uint8_t readBuffer[1024];
ssize_t sysexStartIndex = -1;
void checkError(int result, const char *message)
{
if (result < 0)
{
throw PiPedalStateException(SS("Unexpected error: " << message << " (" << this->deviceName));
}
}
public:
void Open(const AlsaMidiDeviceInfo &device)
{
bufferCount = 0;
eventCount = 0;
sysexStartIndex = -1;
runningStatus = 0;
dataIndex = 0;
dataLength = 0;
this->deviceName = device.description_;
}
void Close()
{
}
size_t GetMidiInputEventCount()
{
return 0;
}
bool GetMidiInputEvent(MidiEvent *event, size_t nFrame)
{
return false;
}
void MidiPut(uint8_t cc, uint8_t d0, uint8_t d1)
{
}
void FillBuffer()
{
}
void WriteBuffer(uint8_t *readBuffer, size_t nRead)
{
}
};
std::vector<MidiState *> midiStates;
void OpenMidi(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection)
{
const auto &devices = channelSelection.GetInputMidiDevices();
midiStates.resize(devices.size());
for (size_t i = 0; i < devices.size(); ++i)
{
const auto &device = devices[i];
MidiState *state = new MidiState();
midiStates[i] = state;
state->Open(device);
}
}
virtual size_t MidiInputBufferCount() const
{
return this->midiStates.size();
}
virtual void *GetMidiInputBuffer(size_t channel, size_t nFrames)
{
return (void *)midiStates[channel];
}
virtual size_t GetMidiInputEventCount(void *portBuffer)
{
MidiState *state = (MidiState *)portBuffer;
return state->GetMidiInputEventCount();
}
virtual bool GetMidiInputEvent(MidiEvent *event, void *portBuf, size_t nFrame)
{
MidiState *state = (MidiState *)portBuf;
return state->GetMidiInputEvent(event, nFrame);
}
virtual void FillMidiBuffers()
{
}
virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); }
virtual float *GetInputBuffer(size_t channel, size_t nFrames)
{
return activeCaptureBuffers[channel];
}
virtual size_t OutputBufferCount() const { return activePlaybackBuffers.size(); }
virtual float *GetOutputBuffer(size_t channel, size_t nFrames)
{
return activePlaybackBuffers[channel];
}
void FreeBuffers(std::vector<float *> &buffer)
{
for (size_t i = 0; i < buffer.size(); ++i)
{
// delete[] buffer[i];
buffer[i] = 0;
}
buffer.clear();
}
void DeleteBuffers()
{
activeCaptureBuffers.clear();
activePlaybackBuffers.clear();
FreeBuffers(this->playbackBuffers);
FreeBuffers(this->captureBuffers);
if (rawCaptureBuffer)
{
delete[] rawCaptureBuffer;
rawCaptureBuffer = nullptr;
}
if (rawPlaybackBuffer)
{
delete[] rawPlaybackBuffer;
rawPlaybackBuffer = nullptr;
}
}
virtual void Close()
{
if (!open)
{
return;
}
open = false;
Deactivate();
DummyCleanup();
DeleteBuffers();
}
virtual float CpuUse()
{
return 0;
}
virtual float CpuOverhead()
{
return 0.1;
}
};
AudioDriver *CreateDummyAudioDriver(AudioDriverHost *driverHost)
{
return new DummyDriverImpl(driverHost);
}
bool GetDummyChannels(const JackServerSettings &jackServerSettings,
std::vector<std::string> &inputAudioPorts,
std::vector<std::string> &outputAudioPorts)
{
bool result = false;
try
{
unsigned int playbackChannels = 2, captureChannels = 2;
inputAudioPorts.clear();
for (unsigned int i = 0; i < captureChannels; ++i)
{
inputAudioPorts.push_back(SS("system::capture_" << i));
}
outputAudioPorts.clear();
for (unsigned int i = 0; i < playbackChannels; ++i)
{
outputAudioPorts.push_back(SS("system::playback_" << i));
}
result = true;
}
catch (const std::exception &e)
{
throw;
}
return result;
}
} // namespace
+35
View File
@@ -0,0 +1,35 @@
/*
* MIT License
*
* Copyright (c) 2024 Robin E. R. 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
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include "AudioDriver.hpp"
#include "JackServerSettings.hpp"
namespace pipedal {
AudioDriver* CreateDummyAudioDriver(AudioDriverHost*driverHost);
}
+7 -2
View File
@@ -112,7 +112,12 @@ void JackConfiguration::AlsaInitialize(
this->isValid_ = false; this->isValid_ = false;
this->errorStatus_ = ""; this->errorStatus_ = "";
this->inputMidiDevices_ = GetAlsaMidiInputDevices(); if (jackServerSettings.IsDummyAudioDevice())
{
this->inputMidiDevices_.clear();
} else {
this->inputMidiDevices_ = GetAlsaMidiInputDevices();
}
if (jackServerSettings.IsValid()) if (jackServerSettings.IsValid())
{ {
this->blockLength_ = jackServerSettings.GetBufferSize(); this->blockLength_ = jackServerSettings.GetBufferSize();
@@ -126,7 +131,7 @@ void JackConfiguration::AlsaInitialize(
} }
} catch (const std::exception& /*ignore*/) } catch (const std::exception& /*ignore*/)
{ {
throw;
} }
} }
+8
View File
@@ -54,6 +54,14 @@ namespace pipedal
uint32_t GetBufferSize() const { return bufferSize_; } uint32_t GetBufferSize() const { return bufferSize_; }
uint32_t GetNumberOfBuffers() const { return numberOfBuffers_; } uint32_t GetNumberOfBuffers() const { return numberOfBuffers_; }
const std::string &GetAlsaInputDevice() const { return alsaDevice_; } const std::string &GetAlsaInputDevice() const { return alsaDevice_; }
void UseDummyAudioDevice() {
this->valid_ = true;
if (sampleRate_ == 0) sampleRate_ = 48000;
this->alsaDevice_ = "__DUMMY_AUDIO__";
}
bool IsDummyAudioDevice() const {
return this->alsaDevice_ == "__DUMMY_AUDIO__";
}
void ReadJackDaemonConfiguration(); void ReadJackDaemonConfiguration();
+2
View File
@@ -27,6 +27,7 @@
#include <chrono> #include <chrono>
#include <sys/eventfd.h> #include <sys/eventfd.h>
#include "PiPedalModel.hpp" #include "PiPedalModel.hpp"
#include "util.hpp"
using namespace pipedal; using namespace pipedal;
@@ -57,6 +58,7 @@ Lv2PluginChangeMonitor::~Lv2PluginChangeMonitor()
void Lv2PluginChangeMonitor::ThreadProc() void Lv2PluginChangeMonitor::ThreadProc()
{ {
SetThreadName("Lv2Change");
using clock = std::chrono::steady_clock; using clock = std::chrono::steady_clock;
int inotify_fd = inotify_init(); int inotify_fd = inotify_init();
+131 -113
View File
@@ -74,7 +74,7 @@ PiPedalModel::PiPedalModel()
this->jackServerSettings = this->storage.GetJackServerSettings(); this->jackServerSettings = this->storage.GetJackServerSettings();
#endif #endif
updater.SetUpdateListener( updater.SetUpdateListener(
[this](const UpdateStatus&updateStatus) [this](const UpdateStatus &updateStatus)
{ {
this->OnUpdateStatusChanged(updateStatus); this->OnUpdateStatusChanged(updateStatus);
}); });
@@ -247,64 +247,8 @@ void PiPedalModel::Load()
scheduler_params.sched_priority = 10; scheduler_params.sched_priority = 10;
memset(&scheduler_params, 0, sizeof(sched_param)); memset(&scheduler_params, 0, sizeof(sched_param));
sched_setscheduler(0, SCHED_RR, &scheduler_params); sched_setscheduler(0, SCHED_RR, &scheduler_params);
#if JACK_HOST
this->jackConfiguration = this->jackConfiguration.JackInitialize();
#else
this->jackServerSettings = storage.GetJackServerSettings();
if (jackServerSettings.IsValid())
{
this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
if (!jackServerSettings.IsValid())
{
for (size_t retry = 0; retry < 5; ++retry)
{
// retry until the device comes online.
sleep(3);
Lv2Log::info("Retrying AlsaInitialize");
jackConfiguration.isValid(true);
this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
if (jackConfiguration.isValid())
{
Lv2Log::info("Retry succeeded.");
break;
}
}
}
}
else
{
this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
}
#endif
if (this->jackConfiguration.isValid())
{
JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration);
selection = selection.RemoveInvalidChannels(jackConfiguration);
this->pluginHost.OnConfigurationChanged(jackConfiguration, selection); RestartAudio();
try
{
audioHost->Open(this->jackServerSettings, selection);
bool loadedSuccessfully = false;
try
{
LoadCurrentPedalboard();
loadedSuccessfully = true;
}
catch (const std::exception &e)
{
Lv2Log::error("Failed to load initial plugin. %s", e.what());
}
}
catch (std::exception &e)
{
Lv2Log::error("Failed to start audio device. %s", e.what());
}
}
else
{
Lv2Log::error("Audio device not configured.");
}
} }
IPiPedalModelSubscriber *PiPedalModel::GetNotificationSubscriber(int64_t clientId) IPiPedalModelSubscriber *PiPedalModel::GetNotificationSubscriber(int64_t clientId)
@@ -391,7 +335,6 @@ void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId)
item->stateUpdateCount(item->stateUpdateCount() + 1); item->stateUpdateCount(item->stateUpdateCount() + 1);
Lv2PluginState newState = item->lv2State(); Lv2PluginState newState = item->lv2State();
IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()];
for (size_t i = 0; i < subscribers.size(); ++i) for (size_t i = 0; i < subscribers.size(); ++i)
@@ -1143,59 +1086,65 @@ JackConfiguration PiPedalModel::GetJackConfiguration()
return this->jackConfiguration; return this->jackConfiguration;
} }
void PiPedalModel::RestartAudio() void PiPedalModel::RestartAudio(bool useDummyAudioDriver)
{ {
if (this->audioHost->IsOpen())
{
this->audioHost->Close();
}
// restarting is a bit dodgy. It was impossible with Jack, but
// now very plausible with the ALSA audio stack.
// Still bugs wrt/ restarting the circular buffers for the audio thread.
// for the meantime, just rely on the fact that the admin service will restart
// the process.
//...
// do a complete reload.
this->audioHost->SetPedalboard(nullptr);
this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
if (this->jackConfiguration.isValid())
{
JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration);
selection = selection.RemoveInvalidChannels(jackConfiguration);
}
else
{
jackConfiguration.setErrorStatus("Error");
}
FireJackConfigurationChanged(jackConfiguration);
if (!jackServerSettings.IsValid() || !jackConfiguration.isValid())
{
Lv2Log::error("Audio configuration not valid.");
return;
}
try try
{ {
JackChannelSelection channelSelection = GetJackChannelSelection(); if (this->audioHost->IsOpen())
{
this->audioHost->Close();
}
// restarting is a bit dodgy. It was impossible with Jack, but
// now very plausible with the ALSA audio stack.
// Still bugs wrt/ restarting the circular buffers for the audio thread.
// do a complete reload.
this->audioHost->SetPedalboard(nullptr);
auto jackServerSettings = this->jackServerSettings;
if (useDummyAudioDriver)
{
jackServerSettings.UseDummyAudioDevice();
}
auto jackConfiguration = this->jackConfiguration;
jackConfiguration.AlsaInitialize(jackServerSettings);
if (jackConfiguration.isValid())
{
JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration);
selection = selection.RemoveInvalidChannels(jackConfiguration);
}
else
{
jackConfiguration.setErrorStatus("Error");
}
if (!useDummyAudioDriver)
{
this->jackConfiguration = jackConfiguration;
FireJackConfigurationChanged(jackConfiguration);
}
if (!jackServerSettings.IsValid() || !jackConfiguration.isValid())
{
throw std::runtime_error("Audio configuration not valid.");
}
JackChannelSelection channelSelection = this->storage.GetJackChannelSelection(jackConfiguration);
if (this->jackConfiguration.isValid())
{
channelSelection.RemoveInvalidChannels(this->jackConfiguration);
}
if (!channelSelection.isValid()) if (!channelSelection.isValid())
{ {
Lv2Log::error("Audio configuration not valid."); throw std::runtime_error("Audio configuration not valid.");
return;
} }
this->audioHost->Open(this->jackServerSettings, channelSelection); this->audioHost->Open(jackServerSettings, channelSelection);
this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection); this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection);
std::vector<std::string> errorMessages;
LoadCurrentPedalboard(); LoadCurrentPedalboard();
this->UpdateRealtimeVuSubscriptions(); this->UpdateRealtimeVuSubscriptions();
@@ -1203,8 +1152,19 @@ void PiPedalModel::RestartAudio()
} }
catch (const std::exception &e) catch (const std::exception &e)
{ {
Lv2Log::error(SS("Failed to start audio. " << e.what())); this->audioHost->Close();
throw; if (useDummyAudioDriver)
{
Lv2Log::error(SS("Failed to start dummy audio driver. " << e.what()));
}
else
{
Lv2Log::error(SS("Failed to start audio. " << e.what()));
}
if (!useDummyAudioDriver)
{
RestartAudio(true); // use the dummy audio driver.
}
} }
} }
@@ -2178,7 +2138,7 @@ void PiPedalModel::SetRestartListener(std::function<void(void)> &&listener)
this->restartListener = std::move(listener); this->restartListener = std::move(listener);
} }
void PiPedalModel::OnUpdateStatusChanged(const UpdateStatus&updateStatus) void PiPedalModel::OnUpdateStatusChanged(const UpdateStatus &updateStatus)
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
@@ -2188,40 +2148,98 @@ void PiPedalModel::OnUpdateStatusChanged(const UpdateStatus&updateStatus)
FireUpdateStatusChanged(this->currentUpdateStatus); FireUpdateStatusChanged(this->currentUpdateStatus);
} }
} }
void PiPedalModel::FireUpdateStatusChanged(const UpdateStatus&updateStatus) void PiPedalModel::FireUpdateStatusChanged(const UpdateStatus &updateStatus)
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
std::vector<IPiPedalModelSubscriber *> t; std::vector<IPiPedalModelSubscriber *> t;
t.reserve(subscribers.size()); t.reserve(subscribers.size());
for (auto subscriber: subscribers) for (auto subscriber : subscribers)
{ {
t.push_back(subscriber); t.push_back(subscriber);
} }
for (auto subscriber: t) for (auto subscriber : t)
{ {
subscriber->OnUpdateStatusChanged(updateStatus); subscriber->OnUpdateStatusChanged(updateStatus);
} }
} }
UpdateStatus PiPedalModel::GetUpdateStatus() { UpdateStatus PiPedalModel::GetUpdateStatus()
{
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
return currentUpdateStatus; return currentUpdateStatus;
} }
void PiPedalModel::UpdateNow(const std::string&updateUrl) void PiPedalModel::UpdateNow(const std::string &updateUrl)
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
auto fileName = updater.DownloadUpdate(updateUrl); auto fileName = updater.DownloadUpdate(updateUrl);
adminClient.InstallUpdate(fileName); adminClient.InstallUpdate(fileName);
} }
void PiPedalModel::ForceUpdateCheck() { void PiPedalModel::ForceUpdateCheck()
{
updater.ForceUpdateCheck(); updater.ForceUpdateCheck();
} }
void PiPedalModel::SetUpdatePolicy(UpdatePolicyT updatePolicy) void PiPedalModel::SetUpdatePolicy(UpdatePolicyT updatePolicy)
{ {
updater.SetUpdatePolicy(updatePolicy); updater.SetUpdatePolicy(updatePolicy);
} }
static bool HasAlsaDevice(const std::vector<AlsaDeviceInfo> devices, const std::string &deviceId)
{
for (auto &device : devices)
{
if (device.id_ == deviceId)
return true;
}
return false;
}
void PiPedalModel::WaitForAudioDeviceToComeOnline()
{
auto serverSettings = this->GetJackServerSettings();
// Wait for selected audio device to be initialized.
// It may take some time for ALSA to publish all available devices when rebooting.
if (serverSettings.IsValid())
{
// wait up to 15 seconds for the midi device to come online.
auto devices = GetAlsaDevices();
bool found = false;
if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice()))
{
Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
}
else
{
Lv2Log::info(SS("Waiting for ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
for (int i = 0; i < 5; ++i)
{
sleep(2);
devices = GetAlsaDevices();
if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice()))
{
found = true;
break;
}
}
if (found)
{
Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
}
else
{
Lv2Log::info(SS("ALSA device " << serverSettings.GetAlsaInputDevice() << " not found."));
}
}
}
else
{
Lv2Log::info("No ALSA device selected.");
}
// pre-cache device info before we let audio services run.
GetAlsaDevices();
}
+3 -2
View File
@@ -169,9 +169,8 @@ namespace pipedal
void UpdateRealtimeVuSubscriptions(); void UpdateRealtimeVuSubscriptions();
void UpdateRealtimeMonitorPortSubscriptions(); void UpdateRealtimeMonitorPortSubscriptions();
void OnVuUpdate(const std::vector<VuUpdate> &updates);
void RestartAudio(); void RestartAudio(bool useDummyAudioDriver = false);
std::vector<RealtimePatchPropertyRequest *> outstandingParameterRequests; std::vector<RealtimePatchPropertyRequest *> outstandingParameterRequests;
@@ -204,6 +203,8 @@ namespace pipedal
PiPedalModel(); PiPedalModel();
virtual ~PiPedalModel(); virtual ~PiPedalModel();
void WaitForAudioDeviceToComeOnline();
UpdateStatus GetUpdateStatus(); UpdateStatus GetUpdateStatus();
void UpdateNow(const std::string&updateUrl); void UpdateNow(const std::string&updateUrl);
void FireUpdateStatusChanged(const UpdateStatus&updateStatus); void FireUpdateStatusChanged(const UpdateStatus&updateStatus);
+2
View File
@@ -18,6 +18,7 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "Updater.hpp" #include "Updater.hpp"
#include "util.hpp"
#include "json.hpp" #include "json.hpp"
#include "config.hpp" #include "config.hpp"
#include <sys/eventfd.h> #include <sys/eventfd.h>
@@ -165,6 +166,7 @@ void Updater::SetUpdateListener(UpdateListener &&listener)
void Updater::ThreadProc() void Updater::ThreadProc()
{ {
SetThreadName("UpdateMonitor");
struct pollfd pfd; struct pollfd pfd;
pfd.fd = this->event_reader; pfd.fd = this->event_reader;
pfd.events = POLLIN; pfd.events = POLLIN;
+1 -61
View File
@@ -54,15 +54,6 @@ using namespace pipedal;
#endif #endif
bool HasAlsaDevice(const std::vector<AlsaDeviceInfo> devices, const std::string &deviceId)
{
for (auto &device : devices)
{
if (device.id_ == deviceId)
return true;
}
return false;
}
class application_category : public boost::system::error_category class application_category : public boost::system::error_category
{ {
@@ -250,58 +241,7 @@ int main(int argc, char *argv[])
(unsigned long)getpid()); (unsigned long)getpid());
} }
auto serverSettings = model.GetJackServerSettings(); model.WaitForAudioDeviceToComeOnline();
{
// Wait for selected audio device to be initialized.
// It may take some time for ALSA to publish all available devices when rebooting.
if (serverSettings.IsValid())
{
// wait up to 15 seconds for the midi device to come online.
auto devices = model.GetAlsaDevices();
bool found = false;
if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice()))
{
Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
}
else
{
for (int i = 0; i < 5; ++i)
{
sleep(3);
devices = model.GetAlsaDevices();
if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice()))
{
found = true;
break;
}
if (g_SigBreak)
exit(1);
if (!systemd)
{
break;
}
Lv2Log::info(SS("Waiting for ALSA device " << serverSettings.GetAlsaInputDevice() << " to come online..."));
}
if (found)
{
Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
}
else
{
Lv2Log::info(SS("ALSA device " << serverSettings.GetAlsaInputDevice() << " not found."));
}
}
}
else
{
Lv2Log::info("No ALSA device selected.");
}
// pre-cache device info before we let audio services run.
model.GetAlsaDevices();
}
// only accept signals on the main thread. // only accept signals on the main thread.
int sig; int sig;