From dd8a5008915a26819a8483813867af06187ad675 Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Thu, 11 Sep 2025 08:59:49 -0400 Subject: [PATCH] Alsa device enumeration, Audio Device dialog tuning, minimum buffer size support (gx plugins). --- src/AlsaDriver.hpp | 2 +- src/AudioHost.cpp | 1 + src/BufferPool.hpp | 2 +- src/JackServerSettings.hpp | 5 +- src/Lv2Effect.cpp | 596 +++++++++++++++--- src/Lv2Effect.hpp | 41 +- src/Lv2Pedalboard.cpp | 61 +- src/OptionsFeature.hpp | 3 + src/PiPedalAlsa.cpp | 349 ++++++---- src/PiPedalAlsa.hpp | 2 + src/PluginHost.cpp | 17 +- src/PluginHost.hpp | 9 +- vite/src/pipedal/AlsaDeviceInfo.tsx | 4 + vite/src/pipedal/JackServerSettings.tsx | 6 +- vite/src/pipedal/JackServerSettingsDialog.tsx | 35 +- vite/src/pipedal/SettingsDialog.tsx | 4 +- 16 files changed, 880 insertions(+), 257 deletions(-) diff --git a/src/AlsaDriver.hpp b/src/AlsaDriver.hpp index c579813..1b9ee0e 100644 --- a/src/AlsaDriver.hpp +++ b/src/AlsaDriver.hpp @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2022 Robin E. R. Davies + * Copyright (c) 2025 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 diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index 2740516..09dcc6d 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -227,6 +227,7 @@ namespace pipedal for (const auto &patchProperty : pathPatchProperties) { + // yyy: only if the property changed!. effect->SetPatchProperty( patchProperty.propertyUrid, patchProperty.atomBuffer.size(), (LV2_Atom *)patchProperty.atomBuffer.data()); } diff --git a/src/BufferPool.hpp b/src/BufferPool.hpp index 6c62458..ea2eeb3 100644 --- a/src/BufferPool.hpp +++ b/src/BufferPool.hpp @@ -49,7 +49,7 @@ public: void Clear() { for (int i = 0; i < allocatedBuffers.size(); ++i) { - delete[] (char*)allocatedBuffers[i]; + delete[] (char*)(allocatedBuffers[i]); } allocatedBuffers.resize(0); } diff --git a/src/JackServerSettings.hpp b/src/JackServerSettings.hpp index 4fb0932..d55b7b0 100644 --- a/src/JackServerSettings.hpp +++ b/src/JackServerSettings.hpp @@ -72,11 +72,14 @@ namespace pipedal this->valid_ = true; if (sampleRate_ == 0) sampleRate_ = 48000; this->alsaDevice_ = "dummy:channels_2"; + this->alsaInputDevice_ = "dummy:channels_2"; + this->alsaOutputDevice_ = "dummy:channels_2"; } bool IsDummyAudioDevice() const { return this->alsaDevice_.starts_with("__DUMMY_AUDIO__") - || this->alsaDevice_.starts_with("dummy:"); + || this->alsaDevice_.starts_with("dummy:") + || this->alsaInputDevice_.starts_with("dummy:"); } void ReadJackDaemonConfiguration(); diff --git a/src/Lv2Effect.cpp b/src/Lv2Effect.cpp index 2b32224..fd339b0 100644 --- a/src/Lv2Effect.cpp +++ b/src/Lv2Effect.cpp @@ -58,6 +58,28 @@ static fs::path makeAbsolutePath(const std::filesystem::path &path, const std::f return parentPath / path; } +inline void Lv2Effect::CheckStagingBufferSentries() +{ +#ifndef NDEBUG + for (size_t i = 0; i < inputStagingBuffers.size(); ++i) + { + if (inputStagingBuffers[i].at(stagingBufferSize) != 99.9f) + { + throw std::logic_error("Staging buffer sentry overwritten."); + } + + } + for (size_t i = 0; i < outputStagingBuffers.size(); ++i) + { + if (outputStagingBuffers[i].at(stagingBufferSize) != 99.9f) + { + throw std::logic_error("Staging buffer sentry overwritten."); + } + + } +#endif +} + Lv2Effect::Lv2Effect( IHost *pHost_, const std::shared_ptr &info_, @@ -66,8 +88,12 @@ Lv2Effect::Lv2Effect( { auto pWorld = pHost_->getWorld(); + size_t stagedBufferSize = GetStagedBufferSize(); + logFeature.Prepare(&pHost_->GetMapFeature(), info_->name() + ": ", this); + optionsFeature.Prepare(pHost->GetMapFeature(), 44100, stagedBufferSize, pHost->GetAtomBufferSize()); + this->bypassStartingSamples = (uint32_t)(pHost->GetSampleRate() * BYPASS_TIME_S); this->bypass = pedalboardItem.isEnabled(); @@ -97,14 +123,15 @@ Lv2Effect::Lv2Effect( LV2_URID_Map *map = this->pHost->GetLv2UridMap(); lv2_atom_forge_init(&inputForgeRt, map); lv2_atom_forge_init(&outputForgeRt, map); + lv2_atom_forge_init(&stagedInputForgeRt, map); const LilvPlugins *plugins = lilv_world_get_all_plugins(pWorld); - // xxx: could we not stash the pPlugin in the plugin info? + // FIXME: could we not stash the pPlugin in the plugin info? auto uriNode = lilv_new_uri(pWorld, pedalboardItem.uri().c_str()); const LilvPlugin *pPlugin = lilv_plugins_get_by_uri(plugins, uriNode); - for (auto&port : info->ports()) + for (auto &port : info->ports()) { if (port->is_bypass()) { @@ -115,7 +142,7 @@ Lv2Effect::Lv2Effect( lilv_node_free(uriNode); { AutoLilvNode bundleUri = lilv_plugin_get_bundle_uri(pPlugin); - char* bundleUriString = lilv_file_uri_parse(bundleUri.AsUri().c_str(), nullptr); + char *bundleUriString = lilv_file_uri_parse(bundleUri.AsUri().c_str(), nullptr); std::string storagePath = pHost_->GetPluginStoragePath(); @@ -123,7 +150,7 @@ Lv2Effect::Lv2Effect( pHost_->GetMapFeature().GetMap(), logFeature.GetLog(), bundleUriString, - storagePath); + storagePath); mapPathFeature.Prepare(&(pHost_->GetMapFeature())); mapPathFeature.SetPluginStoragePath(pHost_->GetPluginStoragePath()); @@ -132,12 +159,10 @@ Lv2Effect::Lv2Effect( const auto &fileProperties = info_->piPedalUI()->fileProperties(); for (const auto &fileProperty : fileProperties) { - fs::path targetPath = fileProperty->directory() / std::filesystem::path(bundleUriString).parent_path().filename(); - mapPathFeature.AddResourceFileMapping({ - bundleUriString, - storagePath / targetPath, - fileProperty->fileTypes() - }); + fs::path targetPath = fileProperty->directory() / std::filesystem::path(bundleUriString).parent_path().filename(); + mapPathFeature.AddResourceFileMapping({bundleUriString, + storagePath / targetPath, + fileProperty->fileTypes()}); } } @@ -146,12 +171,16 @@ Lv2Effect::Lv2Effect( LV2_Feature *const *features = pHost_->GetLv2Features(); - this->features.push_back(logFeature.GetFeature()); - for (auto p = features; *p != nullptr; ++p) { - this->features.push_back(*p); + if (strcmp((*p)->URI, LV2_LOG__log) != 0) + { // ommit the host's LOG feature. + this->features.push_back(*p); + } } + this->features.push_back(logFeature.GetFeature()); + this->features.push_back(optionsFeature.GetFeature()); + this->features.push_back(mapPathFeature.GetMapPathFeature()); this->features.push_back(mapPathFeature.GetMakePathFeature()); this->features.push_back(mapPathFeature.GetFreePathFeature()); @@ -173,7 +202,7 @@ Lv2Effect::Lv2Effect( } this->features.push_back(nullptr); - const LV2_Feature **myFeatures = &this->features[0]; + const LV2_Feature **myFeatures = &this->features.at(0); LilvInstance *pInstance = nullptr; try @@ -195,14 +224,14 @@ Lv2Effect::Lv2Effect( (const LV2_Worker_Interface *)lilv_instance_get_extension_data(pInstance, LV2_WORKER__interface); this->worker = std::make_unique(workerThread, pInstance, worker_interface); - + const LV2_State_Interface *state_interface = (const LV2_State_Interface *)lilv_instance_get_extension_data(pInstance, LV2_STATE__interface); if (state_interface) { - this->stateInterface = std::make_unique(pHost, &(this->features[0]), pInstance, state_interface); + this->stateInterface = std::make_unique(pHost, &(this->features.at(0)), pInstance, state_interface); } this->instanceId = pedalboardItem.instanceId(); @@ -235,7 +264,7 @@ Lv2Effect::Lv2Effect( int index = GetControlIndex(v.key()); if (index != -1) { - this->controlValues[index] = v.value(); + this->controlValues.at(index) = v.value(); } } @@ -322,20 +351,20 @@ void Lv2Effect::ConnectControlPorts() int controlArrayLength = 0; for (int i = 0; i < info->ports().size(); ++i) { - if (info->ports()[i]->index() >= controlArrayLength) + if (info->ports().at(i)->index() >= controlArrayLength) { - controlArrayLength = info->ports()[i]->index() + 1; + controlArrayLength = info->ports().at(i)->index() + 1; } } this->realtimePortInfo.resize(controlArrayLength); for (int i = 0; i < info->ports().size(); ++i) { - const auto &port = info->ports()[i]; + const auto &port = info->ports().at(i); if (port->is_control_port()) { int index = port->index(); - realtimePortInfo[index] = port.get(); - lilv_instance_connect_port(pInstance, i, &this->controlValues[index]); + realtimePortInfo.at(index) = port.get(); + lilv_instance_connect_port(pInstance, i, &this->controlValues.at(index)); } } } @@ -348,7 +377,7 @@ void Lv2Effect::PreparePortIndices() for (int i = 0; i < info->ports().size(); ++i) { - const auto &port = info->ports()[i]; + const auto &port = info->ports().at(i); int portIndex = port->index(); if (port->is_audio_port()) @@ -386,17 +415,17 @@ void Lv2Effect::PreparePortIndices() controlIndex[port->symbol()] = portIndex; if (port->is_input()) { - this->isInputControlPort[portIndex] = true; - this->defaultInputControlValues[portIndex] = port->default_value(); + this->isInputControlPort.at(portIndex) = true; + this->defaultInputControlValues.at(portIndex) = port->default_value(); if (port->trigger_property()) { - this->isInputTriggerControlPort[portIndex] = true; + this->isInputTriggerControlPort.at(portIndex) = true; } } } } size_t maxInputControlPort = isInputControlPort.size(); - while (maxInputControlPort != 0 && !isInputControlPort[maxInputControlPort - 1]) + while (maxInputControlPort != 0 && !isInputControlPort.at(maxInputControlPort - 1)) { --maxInputControlPort; } @@ -406,6 +435,14 @@ void Lv2Effect::PreparePortIndices() outputAudioBuffers.resize(outputAudioPortIndices.size()); inputAtomBuffers.resize(inputAtomPortIndices.size()); outputAtomBuffers.resize(outputAtomPortIndices.size()); + + if (RequiresBufferStaging()) + { + EnableBufferStaging( + GetStagedBufferSize(), + this->GetNumberOfInputAudioBuffers(), + this->GetNumberOfOutputAudioBuffers()); + } } void Lv2Effect::PrepareNoInputEffect(int numberOfInputs, size_t maxBufferSize) @@ -425,20 +462,20 @@ void Lv2Effect::PrepareNoInputEffect(int numberOfInputs, size_t maxBufferSize) outputMixBuffers.resize(outputAudioPortIndices.size()); for (size_t i = 0; i < outputMixBuffers.size(); ++i) { - outputMixBuffers[i].resize(maxBufferSize); + outputMixBuffers.at(i).resize(maxBufferSize); } // connect the plugin to the mix buffer instead of output buffer. for (size_t i = 0; i < outputAudioPortIndices.size(); ++i) { - int pluginIndex = this->outputAudioPortIndices[i]; - lilv_instance_connect_port(this->pInstance, pluginIndex, outputMixBuffers[i].data()); + int pluginIndex = this->outputAudioPortIndices.at(i); + lilv_instance_connect_port(this->pInstance, pluginIndex, outputMixBuffers.at(i).data()); } } } void Lv2Effect::SetAudioInputBuffer(int index, float *buffer) { - this->inputAudioBuffers[index] = buffer; + this->inputAudioBuffers.at(index) = buffer; if (borrowedEffect) { @@ -449,8 +486,20 @@ void Lv2Effect::SetAudioInputBuffer(int index, float *buffer) if (inputAudioPortIndices.size() == inputAudioBuffers.size()) { - int pluginIndex = this->inputAudioPortIndices[index]; - lilv_instance_connect_port(this->pInstance, pluginIndex, buffer); + if (stagingBufferSize != 0) + { + int pluginIndex = this->inputAudioPortIndices.at(index); + if (index >= inputStagingBufferPointers.size()) + { + throw std::runtime_error("Invalid input staging buffer index."); + } + lilv_instance_connect_port(this->pInstance, pluginIndex, inputStagingBufferPointers.at(index)); + } + else + { + int pluginIndex = this->inputAudioPortIndices.at(index); + lilv_instance_connect_port(this->pInstance, pluginIndex, buffer); + } } else { @@ -458,7 +507,7 @@ void Lv2Effect::SetAudioInputBuffer(int index, float *buffer) // // cases: 1->0, 1->1, 2->0, 2->1 // if (index < inputAudioPortIndices.size()) // { - // int pluginIndex = this->inputAudioPortIndices[index]; + // int pluginIndex = this->inputAudioPortIndices.at(index); // lilv_instance_connect_port(this->pInstance, pluginIndex, buffer); // } } @@ -492,7 +541,7 @@ void Lv2Effect::SetAudioInputBuffers(float *left, float *right) void Lv2Effect::SetAudioOutputBuffer(int index, float *buffer) { - this->outputAudioBuffers[index] = buffer; + this->outputAudioBuffers.at(index) = buffer; if (borrowedEffect) { @@ -503,10 +552,25 @@ void Lv2Effect::SetAudioOutputBuffer(int index, float *buffer) if (this->inputAudioPortIndices.size() != 0) // i.e. we're not mixing a zero-input control { - if ((size_t)index < this->outputAudioPortIndices.size()) + if (this->stagingBufferSize != 0) { - int pluginIndex = this->outputAudioPortIndices[index]; - lilv_instance_connect_port(pInstance, pluginIndex, buffer); + if ((size_t)index < this->outputStagingBufferPointers.size()) + { + int pluginIndex = this->outputAudioPortIndices.at(index); + lilv_instance_connect_port(pInstance, pluginIndex, outputStagingBufferPointers.at(index)); + } + else + { + throw std::runtime_error("outputStagingBufferPointers index out of range."); + } + } + else + { + if ((size_t)index < this->outputAudioPortIndices.size()) + { + int pluginIndex = this->outputAudioPortIndices.at(index); + lilv_instance_connect_port(pInstance, pluginIndex, buffer); + } } } } @@ -523,6 +587,16 @@ int Lv2Effect::GetControlIndex(const std::string &key) const Lv2Effect::~Lv2Effect() { + if (deleted) + { + try { + throw std::runtime_error("Deleted twice!"); + } catch (const std::exception&e) + { + std::terminate(); + } + } + deleted = true; if (worker) { worker->Close(); @@ -531,6 +605,7 @@ Lv2Effect::~Lv2Effect() if (activated) { Deactivate(); + activated = false; } if (pInstance) { @@ -540,7 +615,9 @@ Lv2Effect::~Lv2Effect() if (work_schedule_feature) { free(work_schedule_feature->data); + work_schedule_feature->data = nullptr; free(work_schedule_feature); + work_schedule_feature = nullptr; } } @@ -556,9 +633,11 @@ void Lv2Effect::Activate() if (this->bypassControlIndex == -1) { this->BypassDezipperSet(this->bypass ? 1.0f : 0.0f); - } else { + } + else + { this->BypassDezipperSet(1.0f); - this->controlValues[this->bypassControlIndex] = this->bypass ? 1.0f: 0.0f; + this->controlValues.at(this->bypassControlIndex) = this->bypass ? 1.0f : 0.0f; } } @@ -567,21 +646,91 @@ void Lv2Effect::UpdateAudioPorts() // called on realtime thread to switch borrowed effects to the new buffer pointers. if (borrowedEffect) { - for (size_t i = 0; i < this->inputAudioPortIndices.size(); ++i) + if (stagingBufferSize != 0) { - int portIndex = this->inputAudioPortIndices[i]; - if (GetAudioInputBuffer(i) != nullptr) + for (size_t i = 0; i < this->inputAudioPortIndices.size(); ++i) { - lilv_instance_connect_port(pInstance, portIndex, GetAudioInputBuffer(i)); + int portIndex = this->inputAudioPortIndices.at(i); + if (inputStagingBufferPointers.at(i) != nullptr) + { + lilv_instance_connect_port(pInstance, portIndex, inputStagingBufferPointers.at(i)); + } } - } - for (size_t i = 0; i < this->outputAudioPortIndices.size(); ++i) - { - int portIndex = this->outputAudioPortIndices[i]; - if (GetAudioOutputBuffer(i) != nullptr) + for (size_t i = 0; i < this->outputAudioPortIndices.size(); ++i) { - lilv_instance_connect_port(pInstance, portIndex, GetAudioOutputBuffer(i)); + int portIndex = this->outputAudioPortIndices.at(i); + if (outputStagingBufferPointers.at(i) != nullptr) + { + lilv_instance_connect_port(pInstance, portIndex, outputStagingBufferPointers.at(i)); + } } + for (size_t i = 0; i < this->inputAtomPortIndices.size(); ++i) + { + if (i == 0) + { + if (stagedInputAtomBufferPointer == nullptr) + { + throw std::runtime_error("Invalid astagedInputAtomBufferPointer"); + } + lilv_instance_connect_port(pInstance, inputAtomPortIndices[i],stagedInputAtomBufferPointer); + } else { + auto atomInputBuffer = this->GetAtomInputBuffer(i); + lilv_instance_connect_port(pInstance, inputAtomPortIndices[i],atomInputBuffer); + } + } + for (size_t i = 0; i < this->outputAtomPortIndices.size(); ++i) + { + if (i == 0) + { + if (stagedOutputAtomBufferPointer == nullptr) + { + throw std::runtime_error("Invalid astagedOutputAtomBufferPointer"); + } + lilv_instance_connect_port(pInstance, outputAtomPortIndices[i],stagedOutputAtomBufferPointer); + } else { + auto atomOutputBuffer = this->GetAtomOutputBuffer(i); + lilv_instance_connect_port(pInstance, outputAtomPortIndices[i],atomOutputBuffer); + } + } + + } else { + for (size_t i = 0; i < this->inputAudioPortIndices.size(); ++i) + { + int portIndex = this->inputAudioPortIndices.at(i); + if (GetAudioInputBuffer(i) != nullptr) + { + lilv_instance_connect_port(pInstance, portIndex, GetAudioInputBuffer(i)); + } + } + for (size_t i = 0; i < this->outputAudioPortIndices.size(); ++i) + { + int portIndex = this->outputAudioPortIndices.at(i); + if (GetAudioOutputBuffer(i) != nullptr) + { + lilv_instance_connect_port(pInstance, portIndex, GetAudioOutputBuffer(i)); + } + } + for (size_t i = 0; i < this->inputAtomPortIndices.size(); ++i) + { + auto atomInputBuffer = this->GetAtomInputBuffer(i); + lilv_instance_connect_port(pInstance, inputAtomPortIndices[i],atomInputBuffer); + } + for (size_t i = 0; i < this->outputAtomPortIndices.size(); ++i) + { + auto atomOutputBuffer = this->GetAtomOutputBuffer(i); + lilv_instance_connect_port(pInstance, outputAtomPortIndices[i],atomOutputBuffer); + } + // for (size_t i = 0; i < this->inputMidiPortIndices.size(); ++i) + // { + // auto midiInputBuffer = this->GetMidiInputBuffer(i); + // lilv_instance_connect_port(pInstance, inputMidiPortIndices[i],midiInputBuffer); + // } + // for (size_t i = 0; i < this->outputMidiPortIndices.size(); ++i) + // { + // auto midiOutputBuffer = this->GetMidiOutputBuffer(i); + // lilv_instance_connect_port(pInstance, outputMidiPortIndices[i],midiOutputBuffer); + // } + } } } @@ -592,7 +741,7 @@ void Lv2Effect::AssignUnconnectedPorts() { if (GetAudioInputBuffer(i) == nullptr) { - int pluginIndex = this->inputAudioPortIndices[i]; + int pluginIndex = this->inputAudioPortIndices.at(i); float *buffer = bufferPool.AllocateBuffer(pHost->GetMaxAudioBufferSize()); lilv_instance_connect_port(pInstance, pluginIndex, buffer); @@ -606,7 +755,7 @@ void Lv2Effect::AssignUnconnectedPorts() if (GetAudioOutputBuffer(i) == nullptr) { float *buffer = bufferPool.AllocateBuffer(pHost->GetMaxAudioBufferSize()); - int pluginIndex = this->outputAudioPortIndices[i]; + int pluginIndex = this->outputAudioPortIndices.at(i); lilv_instance_connect_port(pInstance, pluginIndex, buffer); } } @@ -615,24 +764,42 @@ void Lv2Effect::AssignUnconnectedPorts() { if (GetAtomInputBuffer(i) == nullptr) { - int pluginIndex = this->inputAtomPortIndices[i]; + int pluginIndex = this->inputAtomPortIndices.at(i); uint8_t *buffer = bufferPool.AllocateBuffer(pHost->GetAtomBufferSize()); - lilv_instance_connect_port(pInstance, pluginIndex, buffer); + if (stagedInputAtomBufferPointer && i == 0) + { + lilv_instance_connect_port(pInstance, pluginIndex, stagedInputAtomBufferPointer); + ResetInputAtomBuffer((char *)(stagedInputAtomBufferPointer)); + } + else + { + lilv_instance_connect_port(pInstance, pluginIndex, buffer); + } ResetInputAtomBuffer((char *)buffer); - this->inputAtomBuffers[i] = (char *)buffer; + this->inputAtomBuffers.at(i) = (char *)buffer; } } for (int i = 0; i < this->GetNumberOfOutputAtomPorts(); ++i) { if (GetAtomOutputBuffer(i) == nullptr) { - int pluginIndex = this->outputAtomPortIndices[i]; + int pluginIndex = this->outputAtomPortIndices.at(i); uint8_t *buffer = bufferPool.AllocateBuffer(pHost->GetAtomBufferSize()); ResetOutputAtomBuffer((char *)buffer); + + if (stagedOutputAtomBufferPointer && i == 0) + { + lilv_instance_connect_port(pInstance, pluginIndex, stagedOutputAtomBufferPointer); + } + else + { + lilv_instance_connect_port(pInstance, pluginIndex, buffer); + } + lilv_instance_connect_port(pInstance, pluginIndex, buffer); - this->outputAtomBuffers[i] = (char *)buffer; + this->outputAtomBuffers.at(i) = (char *)buffer; } } } @@ -658,21 +825,154 @@ static inline void CopyBuffer(float *restrict input, float *restrict output, uin } } -void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBufferWriter) +size_t Lv2Effect::stageToOutput(size_t outputIndex, size_t nFrames) { - // close off the atom input frame. + size_t thisTime = nFrames - outputIndex; + size_t stagedOutputAvailable = this->stagingBufferSize - this->stagingOutputIx; + if (stagedOutputAvailable < thisTime) + { + thisTime = stagedOutputAvailable; + } + if (thisTime) + { + for (size_t ch = 0; ch < this->GetNumberOfOutputAudioBuffers(); ++ch) + { + float *restrict pIn = this->outputStagingBufferPointers.at(ch) + this->stagingOutputIx; + float *restrict pOut = this->GetAudioOutputBuffer(ch) + outputIndex; + for (size_t i = 0; i < thisTime; ++i) + { + pOut[i] = pIn[i]; + } + } + this->stagingOutputIx += thisTime; + } + return outputIndex + thisTime; +} + +void Lv2Effect::copyAtomBufferEventSequence(LV2_Atom_Sequence *controlInput, LV2_Atom_Forge &outputForge) +{ + LV2_ATOM_SEQUENCE_FOREACH(controlInput, ev) + { + lv2_atom_forge_frame_time(&outputForge, ev->time.frames); + lv2_atom_forge_raw(&outputForge, &(ev->body), ev->body.size); // literal copy of the atom body. + } +} + +size_t Lv2Effect::stageToInput(size_t inputSampleOffset, size_t samples) +{ + + size_t thisTime = samples - inputSampleOffset; + size_t inputAvailable = this->stagingBufferSize - this->stagingInputIx; + if (thisTime > inputAvailable) + { + thisTime = inputAvailable; + } + // copy into staging buffers. + for (size_t nInput = 0; nInput < this->inputAudioBuffers.size(); ++nInput) + { + float *restrict pInput = this->inputAudioBuffers[nInput] + inputSampleOffset; + float *restrict pOutput = this->inputStagingBufferPointers.at(nInput) + this->stagingInputIx; + for (size_t i = 0; i < thisTime; ++i) + { + pOutput[i] = pInput[i]; + } + } + this->stagingInputIx += thisTime; + inputSampleOffset += thisTime; + + if (stagingInputIx == this->stagingBufferSize) + { + // close off the atom input frame. + if (stagedInputAtomBufferPointer) + { + lv2_atom_forge_pop(&this->stagedInputForgeRt, &staged_input_frame); + } + if (stagedOutputAtomBufferPointer) + { + ResetOutputAtomBuffer((char *)stagedOutputAtomBufferPointer); + } + + lilv_instance_run(pInstance, this->stagingBufferSize); + + if (worker) + { + + worker->EmitResponses(); + } + if (stagedOutputAtomBufferPointer) + { + copyAtomBufferEventSequence((LV2_Atom_Sequence *)stagedOutputAtomBufferPointer, this->outputForgeRt); + } + + this->stagingInputIx = 0; + this->stagingOutputIx = 0; + this->resetStagedInputAtomBuffer(); + } + return inputSampleOffset; +} + +void Lv2Effect::resetStagedInputAtomBuffer() +{ + if (stagedInputAtomBufferPointer) + { + const uint32_t notify_capacity = pHost->GetAtomBufferSize(); + lv2_atom_forge_set_buffer( + &(this->stagedInputForgeRt), (uint8_t *)(this->stagedInputAtomBufferPointer), notify_capacity); + lv2_atom_forge_sequence_head(&this->inputForgeRt, &staged_input_frame, urids.units__frame); + } +} +void Lv2Effect::RunWithBufferStaging(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBufferWriter) +{ + // accumulte control input sequence until we can execute a run operation. if (this->inputAtomBuffers.size() != 0) { lv2_atom_forge_pop(&this->inputForgeRt, &input_frame); + + LV2_Atom_Sequence *controlInput = (LV2_Atom_Sequence *)GetAtomInputBuffer(0); + copyAtomBufferEventSequence(controlInput, this->stagedInputForgeRt); } - - lilv_instance_run(pInstance, samples); - - if (worker) + // Prepare ACTUAL control output port. + if (this->stagedOutputAtomBufferPointer) { - // relay worker response - worker->EmitResponses(); + const uint32_t notify_capacity = pHost->GetAtomBufferSize(); + lv2_atom_forge_set_buffer( + &(this->outputForgeRt), (uint8_t *)(this->inputAtomBuffers.at(0)), notify_capacity); + lv2_atom_forge_sequence_head(&this->outputForgeRt, &output_frame, urids.units__frame); } + + uint32_t inputSampleOffset = 0; + uint32_t outputSampleOffset = 0; + + while (true) + { + outputSampleOffset = stageToOutput(outputSampleOffset, samples); + + CheckStagingBufferSentries(); + + if (inputSampleOffset == samples) + { + break; + } + inputSampleOffset = stageToInput(inputSampleOffset, samples); + } + // no staging data avaialble? Output zeros. + if (outputSampleOffset != samples) + { + size_t thisTime = samples - outputSampleOffset; + for (size_t ch = 0; ch < this->GetNumberOfOutputAudioBuffers(); ++ch) + { + float *pOut = this->GetAudioOutputBuffer(ch) + outputSampleOffset; + for (size_t i = 0; i < thisTime; ++i) + { + pOut[i] = 0; + } + } + } + MixOutput(samples, realtimeRingBufferWriter); +} + +inline void Lv2Effect::MixOutput(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBufferWriter) +{ // for zero-input plugins, mix the plugin output with the input signal. if (this->inputAudioPortIndices.size() == 0) { @@ -695,14 +995,14 @@ void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBuff { break; } - input = this->inputAudioBuffers[0]; + input = this->inputAudioBuffers.at(0); } else { - input = this->inputAudioBuffers[i]; + input = this->inputAudioBuffers.at(i); } - float *restrict pluginOutput = this->outputMixBuffers[i].data(); - float *restrict finalOutput = this->outputAudioBuffers[i]; + float *restrict pluginOutput = this->outputMixBuffers.at(i).data(); + float *restrict finalOutput = this->outputAudioBuffers.at(i); for (uint32_t i = 0; i < samples; ++i) { @@ -713,11 +1013,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.at(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.at(i); + float *restrict finalOutput = this->outputAudioBuffers.at(i); for (uint32_t i = 0; i < samples; ++i) { @@ -740,19 +1040,19 @@ void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBuff // replace the contents of the output buffer(s) with the input buffer(s). if (this->outputAudioBuffers.size() == 1) { - CopyBuffer(this->inputAudioBuffers[0], this->outputAudioBuffers[0], samples); + CopyBuffer(this->inputAudioBuffers.at(0), this->outputAudioBuffers.at(0), samples); } else { if (this->inputAudioBuffers.size() == 1) { - CopyBuffer(this->inputAudioBuffers[0], this->outputAudioBuffers[0], samples); - CopyBuffer(this->inputAudioBuffers[0], this->outputAudioBuffers[1], samples); + CopyBuffer(this->inputAudioBuffers.at(0), this->outputAudioBuffers.at(0), samples); + CopyBuffer(this->inputAudioBuffers.at(0), this->outputAudioBuffers.at(1), samples); } else { - CopyBuffer(this->inputAudioBuffers[0], this->outputAudioBuffers[0], samples); - CopyBuffer(this->inputAudioBuffers[1], this->outputAudioBuffers[1], samples); + CopyBuffer(this->inputAudioBuffers.at(0), this->outputAudioBuffers.at(0), samples); + CopyBuffer(this->inputAudioBuffers.at(1), this->outputAudioBuffers.at(1), samples); } } } // else leave the output alone. @@ -765,8 +1065,8 @@ void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBuff if (this->outputAudioBuffers.size() == 1) { - float * restrict input = this->inputAudioBuffers[0]; - float * restrict output = this->outputAudioBuffers[0]; + float *restrict input = this->inputAudioBuffers.at(0); + float *restrict output = this->outputAudioBuffers.at(0); for (uint32_t i = 0; i < samples; ++i) { output[i] = currentBypass * output[i] + (1 - currentBypass) * input[i]; @@ -781,19 +1081,19 @@ void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBuff } else { - float * restrict inputL; - float * restrict inputR; + float *restrict inputL; + float *restrict inputR; if (this->inputAudioBuffers.size() == 1) { - inputL = inputR = inputAudioBuffers[0]; + inputL = inputR = inputAudioBuffers.at(0); } else { - inputL = inputAudioBuffers[0]; - inputR = inputAudioBuffers[1]; + inputL = inputAudioBuffers.at(0); + inputR = inputAudioBuffers.at(1); } - float * restrict outputL = outputAudioBuffers[0]; - float * restrict outputR = outputAudioBuffers[1]; + float *restrict outputL = outputAudioBuffers.at(0); + float *restrict outputR = outputAudioBuffers.at(1); for (uint32_t i = 0; i < samples; ++i) { outputL[i] = currentBypass * outputL[i] + (1 - currentBypass) * inputL[i]; @@ -822,6 +1122,24 @@ void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBuff RelayPatchSetMessages(this->instanceId, realtimeRingBufferWriter); } +void Lv2Effect::Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBufferWriter) +{ + // close off the atom input frame. + if (this->inputAtomBuffers.size() != 0) + { + lv2_atom_forge_pop(&this->inputForgeRt, &input_frame); + } + lilv_instance_run(pInstance, samples); + + if (worker) + { + // relay worker response + worker->EmitResponses(); + } + + MixOutput(samples, realtimeRingBufferWriter); +} + LV2_Worker_Status Lv2Effect::worker_schedule_fn(LV2_Worker_Schedule_Handle handle, uint32_t size, const void *data) @@ -880,17 +1198,17 @@ void Lv2Effect::ResetAtomBuffers() { for (size_t i = 0; i < this->inputAtomBuffers.size(); ++i) { - ResetInputAtomBuffer(this->inputAtomBuffers[i]); + ResetInputAtomBuffer(this->inputAtomBuffers.at(i)); } for (size_t i = 0; i < this->outputAtomBuffers.size(); ++i) { - ResetOutputAtomBuffer(this->outputAtomBuffers[i]); + ResetOutputAtomBuffer(this->outputAtomBuffers.at(i)); } if (inputAtomBuffers.size() != 0) { const uint32_t notify_capacity = pHost->GetAtomBufferSize(); lv2_atom_forge_set_buffer( - &(this->inputForgeRt), (uint8_t *)(this->inputAtomBuffers[0]), notify_capacity); + &(this->inputForgeRt), (uint8_t *)(this->inputAtomBuffers.at(0)), notify_capacity); // Start a sequence in the notify input port. @@ -1147,13 +1465,13 @@ uint64_t Lv2Effect::GetMaxInputControl() const { return maxInputControlPort; } bool Lv2Effect::IsInputControl(uint64_t index) const { - if (index < 0 || index >= isInputControlPort.size()) + if (index >= isInputControlPort.size()) return false; - return isInputControlPort[index]; + return isInputControlPort.at(index); } float Lv2Effect::GetDefaultInputControlValue(uint64_t index) const { - return defaultInputControlValues[index]; + return defaultInputControlValues.at(index); } std::string Lv2Effect::GetPathPatchProperty(const std::string &propertyUri) @@ -1168,3 +1486,97 @@ void Lv2Effect::SetPathPatchProperty(const std::string &propertyUri, const std:: { mainThreadPathProperties[propertyUri] = jsonAtom; } + +void Lv2Effect::EnableBufferStaging(size_t bufferSize, size_t nInputs, size_t nOutputs) +{ + + stagingBufferSize = bufferSize; + stagingOutputIx = bufferSize; + stagingInputIx = 0; + inputStagingBuffers.resize(nInputs); + outputStagingBuffers.resize(nOutputs); + inputStagingBufferPointers.resize(nInputs); + outputStagingBufferPointers.resize(nOutputs); + + if (inputAtomBuffers.size() != 0) + { + stagedInputAtomBuffer.resize(pHost->GetAtomBufferSize()); + stagedInputAtomBufferPointer = stagedInputAtomBuffer.data(); + resetStagedInputAtomBuffer(); + } + else + { + stagedInputAtomBufferPointer = nullptr; + } + stagedOutputAtomBufferPointer = nullptr; + if (outputAtomBuffers.size() != 0) + { + stagedOutputAtomBuffer.resize(pHost->GetAtomBufferSize()); + stagedOutputAtomBufferPointer = stagedOutputAtomBuffer.data(); + } + for (size_t i = 0; i < nInputs; ++i) + { + inputStagingBuffers.at(i).resize(bufferSize + 1); + inputStagingBuffers[i][bufferSize] = 99.9f; // guard entry + inputStagingBufferPointers.at(i) = inputStagingBuffers.at(i).data(); + } + for (size_t i = 0; i < nOutputs; ++i) + { + outputStagingBuffers.at(i).resize(bufferSize + 1); + outputStagingBuffers[i][bufferSize] = 99.9f; // guard entry + outputStagingBufferPointers.at(i) = outputStagingBuffers.at(i).data(); + } +} + +static size_t NextPowerOfTwo(size_t value) +{ + + size_t i = 1; + while (i < value && i < 65536UL) + { + i *= 2; + } + return i; +} + +size_t Lv2Effect::GetStagedBufferSize() const +{ + size_t pluginBlockLength = pHost->GetMaxAudioBufferSize(); + if (info->minBlockLength() != -1 || info->maxBlockLength() != -1) + { + if (info->minBlockLength() != -1 && pluginBlockLength < info->minBlockLength()) + { + pluginBlockLength = info->minBlockLength(); + } + if (info->maxBlockLength() != -1 && pluginBlockLength > info->maxBlockLength()) + { + pluginBlockLength = info->maxBlockLength(); + } + if (info->powerOf2BlockLength()) + { + pluginBlockLength = NextPowerOfTwo(pluginBlockLength); + } + } + return pluginBlockLength; +} + +bool Lv2Effect::RequiresBufferStaging() const +{ + return GetStagedBufferSize() != pHost->GetMaxAudioBufferSize(); +} + +float *Lv2Effect::GetAudioInputBuffer(int index) const +{ + if (index < 0 || index >= this->inputAudioBuffers.size()) + throw std::range_error("Lv2Effect::GetAudioInputBuffer"); + return this->inputAudioBuffers.at(index); +} + +float *Lv2Effect::GetAudioOutputBuffer(int index) const +{ + if (index < 0 || index >= this->outputAudioBuffers.size()) + { + throw std::range_error("Lv2Effect::GetAudioOutputBuffer"); + } + return this->outputAudioBuffers.at(index); +} diff --git a/src/Lv2Effect.hpp b/src/Lv2Effect.hpp index 6dd7c63..4abba0d 100644 --- a/src/Lv2Effect.hpp +++ b/src/Lv2Effect.hpp @@ -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 @@ -28,6 +28,7 @@ #include "PatchPropertyWriter.hpp" #include #include "MapPathFeature.hpp" +#include "OptionsFeature.hpp" #include "IEffect.hpp" #include "Worker.hpp" @@ -56,7 +57,7 @@ namespace pipedal virtual void OnLogDebug(const char*message); private: - + size_t GetStagedBufferSize() const; std::shared_ptr workerThread; std::unique_ptr worker; @@ -115,6 +116,7 @@ namespace pipedal LV2_Atom_Forge_Frame input_frame; LV2_Atom_Forge outputForgeRt; + LV2_Atom_Forge_Frame output_frame; std::vector pathProperties; std::vector pathPropertyWriters; @@ -210,8 +212,11 @@ namespace pipedal bool borrowedEffect = false; bool activated = false; + void EnableBufferStaging(size_t bufferSize, size_t nInputs, size_t nOutputs); + void CheckStagingBufferSentries(); public: + bool RequiresBufferStaging() const; bool IsBorrowedEffect() const { return borrowedEffect; } void SetBorrowedEffect(bool value) { borrowedEffect = value; } void UpdateAudioPorts(); @@ -247,9 +252,36 @@ namespace pipedal return (uint8_t*)this->outputAtomBuffers[0]; } + OptionsFeature optionsFeature; bool hasErrorMessage = false; char errorMessage[1024]; + + bool deleted = false; + size_t stagingBufferSize = 0; + size_t stagingInputIx = 0; + size_t stagingOutputIx = 0; + std::vector> inputStagingBuffers; + std::vector> outputStagingBuffers; + std::vector inputStagingBufferPointers; + std::vector outputStagingBufferPointers; + + std::vector stagedInputAtomBuffer; + void *stagedInputAtomBufferPointer = nullptr; + std::vector stagedOutputAtomBuffer; + void *stagedOutputAtomBufferPointer = nullptr; + + size_t stageToOutput(size_t outputIndex, size_t nFrames); + size_t stageToInput(size_t inputIndex, size_t nFrames); + + + LV2_Atom_Forge stagedInputForgeRt; + LV2_Atom_Forge_Frame staged_input_frame; + + void MixOutput(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBufferWriter); + void copyAtomBufferEventSequence(LV2_Atom_Sequence *sequence, LV2_Atom_Forge &outputForge); + void resetStagedInputAtomBuffer(); + public: Lv2Effect( IHost *pHost, @@ -280,14 +312,14 @@ namespace pipedal virtual int GetNumberOfOutputAudioBuffers() const {return this->outputAudioBuffers.size(); } virtual void SetAudioInputBuffer(int index, float *buffer); - virtual float *GetAudioInputBuffer(int index) const { return this->inputAudioBuffers[index]; } + virtual float *GetAudioInputBuffer(int index) const; virtual void SetAudioInputBuffer(float *buffer); virtual void SetAudioInputBuffers(float *left, float *right); virtual void SetAudioOutputBuffer(int index, float *buffer); - virtual float *GetAudioOutputBuffer(int index) const { return this->outputAudioBuffers[index]; } + virtual float *GetAudioOutputBuffer(int index) const; virtual void SetAtomInputBuffer(int index, void *buffer) { this->inputAtomBuffers[index] = (char*)buffer;} virtual void *GetAtomInputBuffer(int index) const { return this->inputAtomBuffers[index]; } @@ -345,6 +377,7 @@ namespace pipedal virtual void Activate(); virtual void Run(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBufferWriter); + virtual void RunWithBufferStaging(uint32_t samples, RealtimeRingBufferWriter *realtimeRingBufferWriter); virtual void Deactivate(); }; diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index 4721f95..780ce82 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -59,6 +59,7 @@ int Lv2Pedalboard::GetControlIndex(uint64_t instanceId, const std::string &symbo } return -1; } + std::vector Lv2Pedalboard::PrepareItems( std::vector &items, std::vector inputBuffers, @@ -88,8 +89,8 @@ std::vector Lv2Pedalboard::PrepareItems( this->processActions.push_back(preMixAction); - std::vector topResult = PrepareItems(item.topChain(), topInputs, errorList,existingEffects); - std::vector bottomResult = PrepareItems(item.bottomChain(), bottomInputs, errorList,existingEffects); + std::vector topResult = PrepareItems(item.topChain(), topInputs, errorList, existingEffects); + std::vector bottomResult = PrepareItems(item.bottomChain(), bottomInputs, errorList, existingEffects); this->processActions.push_back( [pSplit](uint32_t frames) @@ -114,11 +115,10 @@ std::vector Lv2Pedalboard::PrepareItems( { std::shared_ptr pLv2Effect; - if (existingEffects && existingEffects->contains(item.instanceId()) - ) + if (existingEffects && existingEffects->contains(item.instanceId())) { pLv2Effect = existingEffects->at(item.instanceId()); - ((Lv2Effect*)pLv2Effect.get())->SetBorrowedEffect(true); + ((Lv2Effect *)pLv2Effect.get())->SetBorrowedEffect(true); } else { @@ -177,16 +177,38 @@ std::vector Lv2Pedalboard::PrepareItems( } } - this->processActions.push_back( - [pLv2Effect, this](uint32_t frames) - { - pLv2Effect->Run(frames, this->ringBufferWriter); - }); - - // Reset any trigger controls to default state after processing + // check to see whether we need buffer staging. + bool requiresBufferStaging = false; if (pLv2Effect->IsLv2Effect()) { Lv2Effect *lv2Effect = (Lv2Effect *)pLv2Effect.get(); + + if (lv2Effect->RequiresBufferStaging()) + { + requiresBufferStaging = true; + this->processActions.push_back( + [lv2Effect, this](uint32_t frames) + { + lv2Effect->RunWithBufferStaging(frames, this->ringBufferWriter); + }); + } + } + + + if (!requiresBufferStaging) + { + this->processActions.push_back( + [pLv2Effect, this](uint32_t frames) + { + pLv2Effect->Run(frames, this->ringBufferWriter); + }); + } + + // reset any trigger controls to default state after processing + if (pLv2Effect->IsLv2Effect()) + { + Lv2Effect *lv2Effect = (Lv2Effect *)pLv2Effect.get(); + auto pluginInfo = pHost->GetPluginInfo(item.uri()); if (pluginInfo) { @@ -212,7 +234,8 @@ std::vector Lv2Pedalboard::PrepareItems( } if (pEffect) { - this->effects.push_back(pEffect); // for ownership. + this->effects.push_back(pEffect); // for ownership. + this->realtimeEffects.push_back(pEffect.get()); // because std::shared_ptr is not threadsafe. std::vector effectOutput; @@ -326,7 +349,6 @@ void Lv2Pedalboard::PrepareMidiMap(const PedalboardItem &pedalboardItem) mapping.midiBinding = binding; mapping.instanceId = pedalboardItem.instanceId(); - if (pPortInfo->mod_momentaryOffByDefault() || pPortInfo->mod_momentaryOnByDefault()) { mapping.mappingType = MidiControlType::MomentarySwitch; @@ -397,14 +419,14 @@ void Lv2Pedalboard::UpdateAudioPorts() { Lv2Effect *lv2Effect = (Lv2Effect *)effect; lv2Effect->UpdateAudioPorts(); - } + } } } void Lv2Pedalboard::Activate() { CrashGuardLock crashGuardLock; - + for (int i = 0; i < this->effects.size(); ++i) { this->realtimeEffects[i]->Activate(); @@ -435,7 +457,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(); @@ -581,12 +603,13 @@ void Lv2Pedalboard::ProcessParameterRequests(RealtimePatchPropertyRequest *pPara else if (pEffect->IsVst3()) { pParameterRequests->errorMessage = "Not supported for VST3 plugins"; - } else if (pParameterRequests->sampleTimeout < 0) + } + else if (pParameterRequests->sampleTimeout < 0) { pParameterRequests->sampleTimeout = 0; pParameterRequests->errorMessage = "Timed out."; } - else + else { if (pEffect->IsLv2Effect()) { diff --git a/src/OptionsFeature.hpp b/src/OptionsFeature.hpp index 23212a8..8730e7a 100644 --- a/src/OptionsFeature.hpp +++ b/src/OptionsFeature.hpp @@ -42,6 +42,9 @@ namespace pipedal { double sampleRate, int32_t blockLength, int32_t atomBufferBlockLength); + + void SetBlockLength(int32_t blockLength) { this->blockLength = blockLength; } + ~OptionsFeature(); public: const LV2_Feature* GetFeature() diff --git a/src/PiPedalAlsa.cpp b/src/PiPedalAlsa.cpp index a4fb233..d3c60ba 100644 --- a/src/PiPedalAlsa.cpp +++ b/src/PiPedalAlsa.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Robin Davies +// Copyright (c) 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 @@ -52,9 +52,106 @@ static bool isSupportedAudioDevice(const AlsaDeviceInfo &d) std::string name = d.name_ + " " + d.longName_; std::transform(name.begin(), name.end(), name.begin(), [](char c) { return std::tolower(c); }); - return name.find("hdmi") != std::string::npos || name.find("bcm2835") != std::string::npos; + if (name.find("hdmi") != std::string::npos) return false; +// if (name.find("bcm2835") != std::string::npos) return false; + return true; }; + +struct ProcAlsaDevice { + int cardId; + int subdeviceId; + bool audioCapture; + bool audioPlayback; + bool rawMidi; +}; + +static std::vector getProcAlsaDevices() +{ + std::vector result; + + std::ifstream f {"/proc/asound/devices"}; + if (f.is_open()) + { + std::string line; + while (std::getline(f, line)) + { + // Parse each line of /proc/alsa/devices + // Format: cardnum: [devicenum- subdevicenum]: type : name + std::istringstream iss(line); + std::string token; + + // Skip leading whitespace and get card number + if (!std::getline(iss, token, ':')) + { + continue; + } + try { + int cardId = std::stoi(token.substr(token.find_first_not_of(" \t"))); + + // Get device-subdevice part + if (!std::getline(iss, token, ':')) + { + continue; + } + size_t dashPos = token.find('-'); + if (dashPos == std::string::npos) + { + continue; + } + int deviceId = std::stoi(token.substr(token.find_first_not_of(" \t["), dashPos)); + int subdeviceId = std::stoi(token.substr(dashPos + 1, token.find(']') - dashPos - 1)); + + // Get type + if (!std::getline(iss, token, ':')) + { + continue; + } + std::string type = token.substr(token.find_first_not_of(" \t")); + + ProcAlsaDevice *pDevice = nullptr; + for (size_t i = 0; i < result.size(); ++i) + { + if (result[i].cardId == deviceId && result[i].subdeviceId == subdeviceId) + { + pDevice = &result[i]; + break; + } + } + if (pDevice == nullptr) + { + ProcAlsaDevice newDevice; + newDevice.cardId = deviceId; + newDevice.subdeviceId = subdeviceId; + newDevice.audioCapture = false; + newDevice.audioPlayback = false; + newDevice.rawMidi = false; + result.push_back(newDevice); + pDevice = &(result[result.size()-1]); + } + if (type.find("digital audio capture") != std::string::npos) + { + pDevice->audioCapture = true; + } + if ((type.find("digital audio playback") != std::string::npos)) + { + pDevice->audioPlayback = true; + } + if (type.find("rawmidi") != std::string::npos) + { + pDevice->rawMidi = true; + } + } catch (const std::exception &e) + { + Lv2Log::error(SS("invalid ALSA proc entry: " << line)); + } + } + } + + return result; +} + + std::vector PiPedalAlsaDevices::GetAlsaDevices() { @@ -65,152 +162,163 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() int cardNum = -1; // Start with first card int err; - for (;;) + std::vector procAlsaDevices = getProcAlsaDevices(); + + for (const auto &procAlsaDevice: procAlsaDevices) { - if ((err = snd_card_next(&cardNum)) < 0) - { - Lv2Log::error("Unexpected error enumerating ALSA devices."); - break; + std::stringstream ss; + if (!procAlsaDevice.audioCapture && !procAlsaDevice.audioPlayback) { + continue; } - if (cardNum < 0) - // No more cards - break; + ss << "hw:" << procAlsaDevice.cardId; + std::string cardId = ss.str(); + + snd_ctl_t *hDevice = nullptr; + + if ((err = snd_ctl_open(&hDevice, cardId.c_str(), 0)) < 0) { - std::stringstream ss; - ss << "hw:" << cardNum; - std::string cardId = ss.str(); + continue; + } - snd_ctl_t *hDevice = nullptr; + Finally ffhDevice{[hDevice]() + { snd_ctl_close(hDevice); }}; - if ((err = snd_ctl_open(&hDevice, cardId.c_str(), 0)) < 0) + snd_ctl_card_info_t *alsaInfo = nullptr; + if (snd_ctl_card_info_malloc(&alsaInfo) != 0) + { + Lv2Log::error("Failed to allocate ALSA card info"); + continue; + } + + Finally ffCardInfo{[alsaInfo]() + { snd_ctl_card_info_free(alsaInfo); }}; + + err = snd_ctl_card_info(hDevice, alsaInfo); + if (err == 0) + { + AlsaDeviceInfo info; + info.cardId_ = cardNum; + info.id_ = std::string("hw:") + snd_ctl_card_info_get_id(alsaInfo); + const char *driver = snd_ctl_card_info_get_driver(alsaInfo); + (void)driver; + + info.name_ = snd_ctl_card_info_get_name(alsaInfo); + info.longName_ = snd_ctl_card_info_get_longname(alsaInfo); + + // we can't read our own device if it's open so use data that gets + // cached before we open audio devices. + + AlsaDeviceInfo cachedInfo; + if (getCachedDevice(info.name_, &cachedInfo)) { + // may have been plugged into a different USB connector. + cachedInfo.cardId_ = info.cardId_; + cachedInfo.id_ = info.id_; + result.push_back(cachedInfo); continue; } + snd_pcm_t *captureDevice = nullptr; + snd_pcm_t *playbackDevice = nullptr; + auto rc = snd_pcm_open(&captureDevice, cardId.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK); + bool captureOk = rc == 0; - Finally ffhDevice{[hDevice]() - { snd_ctl_close(hDevice); }}; + Finally ffCaptureDevice{ + [captureDevice] + { if (captureDevice) snd_pcm_close(captureDevice); }}; - snd_ctl_card_info_t *alsaInfo = nullptr; - if (snd_ctl_card_info_malloc(&alsaInfo) != 0) + rc = snd_pcm_open(&playbackDevice, cardId.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); + bool playbackOk = rc == 0; + + Finally ffPlaybackDevice{ + [playbackDevice] + { if (playbackDevice) snd_pcm_close(playbackDevice); }}; + if (procAlsaDevice.audioCapture && !captureOk) { - Lv2Log::error("Failed to allocate ALSA card info"); - continue; + info.captureBusy_ = true; + } + if (procAlsaDevice.audioPlayback && !playbackOk) + { + info.playbackBusy_ = true; } - Finally ffCardInfo{[alsaInfo]() - { snd_ctl_card_info_free(alsaInfo); }}; + info.supportsCapture_ = captureOk; + info.supportsPlayback_ = playbackOk; - err = snd_ctl_card_info(hDevice, alsaInfo); - if (err == 0) + if (captureOk || playbackOk) { - AlsaDeviceInfo info; - info.cardId_ = cardNum; - info.id_ = std::string("hw:") + snd_ctl_card_info_get_id(alsaInfo); - const char *driver = snd_ctl_card_info_get_driver(alsaInfo); - (void)driver; + snd_pcm_t *hDevice = captureOk ? captureDevice : playbackDevice; + snd_pcm_hw_params_t *params = nullptr; + err = snd_pcm_hw_params_malloc(¶ms); - info.name_ = snd_ctl_card_info_get_name(alsaInfo); - info.longName_ = snd_ctl_card_info_get_longname(alsaInfo); - - // we can't read our own device if it's open so use data that gets - // cached before we open audio devices. - - AlsaDeviceInfo cachedInfo; - if (getCachedDevice(info.name_, &cachedInfo)) + if (err == 0) { - // may have been plugged into a different USB connector. - cachedInfo.cardId_ = info.cardId_; - cachedInfo.id_ = info.id_; - result.push_back(cachedInfo); + Finally ffParams{[params] + { snd_pcm_hw_params_free(params); }}; - } - else - { - snd_pcm_t *captureDevice = nullptr; - snd_pcm_t *playbackDevice = nullptr; - bool captureOk = snd_pcm_open(&captureDevice, cardId.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK) == 0; - bool playbackOk = snd_pcm_open(&playbackDevice, cardId.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK) == 0; - Finally ffCaptureDevice{ - [captureDevice] - { if (captureDevice) snd_pcm_close(captureDevice); }}; - Finally ffPlaybackDevice{ - [playbackDevice] - { if (playbackDevice) snd_pcm_close(playbackDevice); }}; - - info.supportsCapture_ = captureOk; - info.supportsPlayback_ = playbackOk; - - if (captureOk || playbackOk) + err = snd_pcm_hw_params_any(hDevice, params); + if (err == 0) { - snd_pcm_t *hDevice = captureOk ? captureDevice : playbackDevice; - snd_pcm_hw_params_t *params = nullptr; - err = snd_pcm_hw_params_malloc(¶ms); + unsigned int minRate = 0, maxRate = 0; + snd_pcm_uframes_t minBufferSize = 0, maxBufferSize = 0; + int dir; + + err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir); + if (err == 0) + { + err = snd_pcm_hw_params_get_rate_max(params, &maxRate, &dir); + if (err == 0) + { + for (size_t i = 0; i < sizeof(RATES) / sizeof(RATES[0]); ++i) + { + uint32_t rate = RATES[i]; + if (rate >= minRate && rate <= maxRate) + { + info.sampleRates_.push_back(rate); + } + } + } + else + { + Lv2Log::warning(SS("Failed to get maximum sample rate for device '" << info.name_ << "'.")); + } + } + else + { + Lv2Log::warning(SS("Failed to get minimum sample rate for device '" << info.name_ << "'.")); + } if (err == 0) { - Finally ffParams{[params] - { snd_pcm_hw_params_free(params); }}; - - err = snd_pcm_hw_params_any(hDevice, params); + err = snd_pcm_hw_params_get_buffer_size_min(params, &minBufferSize); if (err == 0) { - unsigned int minRate = 0, maxRate = 0; - snd_pcm_uframes_t minBufferSize = 0, maxBufferSize = 0; - int dir; - - err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir); - if (err == 0) - { - err = snd_pcm_hw_params_get_rate_max(params, &maxRate, &dir); - if (err == 0) - { - for (size_t i = 0; i < sizeof(RATES) / sizeof(RATES[0]); ++i) - { - uint32_t rate = RATES[i]; - if (rate >= minRate && rate <= maxRate) - { - info.sampleRates_.push_back(rate); - } - } - } - else - { - Lv2Log::warning(SS("Failed to get maximum sample rate for device '" << info.name_ << "'.")); - } - } - else - { - Lv2Log::warning(SS("Failed to get minimum sample rate for device '" << info.name_ << "'.")); - } - - if (err == 0) - { - err = snd_pcm_hw_params_get_buffer_size_min(params, &minBufferSize); - if (err == 0) - { - err = snd_pcm_hw_params_get_buffer_size_max(params, &maxBufferSize); - } - } - if (err == 0) - { - if (minBufferSize < 16) - { - minBufferSize = 16; - } - - info.minBufferSize_ = (uint32_t)minBufferSize; - info.maxBufferSize_ = (uint32_t)maxBufferSize; - } + err = snd_pcm_hw_params_get_buffer_size_max(params, &maxBufferSize); } } if (err == 0) { - cacheDevice(info.name_, info); - result.push_back(info); + if (minBufferSize < 16) + { + minBufferSize = 16; + } + + info.minBufferSize_ = (uint32_t)minBufferSize; + info.maxBufferSize_ = (uint32_t)maxBufferSize; } } } + if (!info.captureBusy_ && !info.playbackBusy_) + { + cacheDevice(info.name_, info); + result.push_back(info); + } + } else { + if (info.captureBusy_ || info.playbackBusy_) + { + result.push_back(info); + } + } } } @@ -220,14 +328,17 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() std::vector filtered; for (auto &device : result) { - if (!isSupportedAudioDevice(device)) + if (isSupportedAudioDevice(device)) { filtered.push_back(device); Lv2Log::debug( SS(" " << device.name_ << " " << device.longName_ << " " << device.cardId_ << (device.supportsCapture_ ? " in" : "") - << (device.supportsPlayback_ ? " out" : ""))); + << (device.supportsPlayback_ ? " out" : "") + << (device.captureBusy_ ? " in(busy)" : "") + << (device.captureBusy_ ? " out(busy)" : "") + )); } } return filtered; @@ -535,6 +646,8 @@ JSON_MAP_REFERENCE(AlsaDeviceInfo, minBufferSize) JSON_MAP_REFERENCE(AlsaDeviceInfo, maxBufferSize) JSON_MAP_REFERENCE(AlsaDeviceInfo, supportsCapture) JSON_MAP_REFERENCE(AlsaDeviceInfo, supportsPlayback) +JSON_MAP_REFERENCE(AlsaDeviceInfo, captureBusy) +JSON_MAP_REFERENCE(AlsaDeviceInfo, playbackBusy) JSON_MAP_END() JSON_MAP_BEGIN(AlsaMidiDeviceInfo) diff --git a/src/PiPedalAlsa.hpp b/src/PiPedalAlsa.hpp index 7a131a8..b9d82a8 100644 --- a/src/PiPedalAlsa.hpp +++ b/src/PiPedalAlsa.hpp @@ -33,6 +33,8 @@ namespace pipedal { uint32_t minBufferSize_ = 0,maxBufferSize_ = 0; bool supportsCapture_ = false; bool supportsPlayback_ = false; + bool captureBusy_ = false; + bool playbackBusy_ = false; bool isDummyDevice() const { return id_.starts_with("dummy:"); diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index 3fa4ef3..8aff3dd 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -30,7 +30,6 @@ #include "Pedalboard.hpp" #include "Lv2Effect.hpp" #include "Lv2Pedalboard.hpp" -#include "OptionsFeature.hpp" #include "JackConfiguration.hpp" #include "lv2/urid/urid.h" #include "lv2/ui/ui.h" @@ -303,8 +302,6 @@ PluginHost::PluginHost() lv2Features.push_back(mapFeature.GetMapFeature()); lv2Features.push_back(mapFeature.GetUnmapFeature()); - optionsFeature.Prepare(mapFeature, 44100, this->GetMaxAudioBufferSize(), this->GetAtomBufferSize()); - lv2Features.push_back(optionsFeature.GetFeature()); fileMetadataFeature.Prepare(mapFeature); lv2Features.push_back(fileMetadataFeature.GetFeature()); @@ -322,7 +319,6 @@ void PluginHost::OnConfigurationChanged(const JackConfiguration &configuration, this->numberOfAudioInputChannels = settings.GetInputAudioPorts().size(); this->numberOfAudioOutputChannels = settings.GetOutputAudioPorts().size(); this->maxBufferSize = configuration.blockLength(); - optionsFeature.Prepare(this->mapFeature, configuration.sampleRate(), configuration.blockLength(), GetAtomBufferSize()); } } @@ -941,6 +937,11 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP isValid = false; } } + AutoLilvNode minBlockLength = lilv_world_get(pWorld,plugUri, lv2Host->lilvUris->buf_size__minBlockLength,nullptr); + this->minBlockLength_ = minBlockLength.AsFloat(-1); + AutoLilvNode maxBlockLength = lilv_world_get(pWorld,plugUri, lv2Host->lilvUris->buf_size__maxBlockLength,nullptr); + this->maxBlockLength_ = maxBlockLength.AsFloat(-1); + std::sort(ports_.begin(), ports_.end(), ports_sort_compare); // Fetch patch properties. @@ -2217,7 +2218,11 @@ json_map::storage_type Lv2PortInfo::jmap{ json_map::enum_reference("units", &Lv2PortInfo::units_, get_units_enum_converter()), MAP_REF(Lv2PortInfo, custom_units), - MAP_REF(Lv2PortInfo, comment)}}; + MAP_REF(Lv2PortInfo, comment), + +}}; + + json_map::storage_type Lv2PortGroup::jmap{{ MAP_REF(Lv2PortGroup, uri), @@ -2249,6 +2254,8 @@ json_map::storage_type Lv2PluginInfo::jmap{{ json_map::reference("modGui", &Lv2PluginInfo::modGui_), json_map::reference("patchProperties", &Lv2PluginInfo::patchProperties_), json_map::reference("hasDefaultState", &Lv2PluginInfo::hasDefaultState_), + json_map::reference("minBlockLength", &Lv2PluginInfo::minBlockLength_), + json_map::reference("maxBlockLength", &Lv2PluginInfo::maxBlockLength_), }}; diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index d90a9e3..c0edb66 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -26,7 +26,6 @@ #include #include "MapFeature.hpp" #include "FileMetadataFeature.hpp" -#include "OptionsFeature.hpp" #include #include #include @@ -432,6 +431,10 @@ namespace pipedal bool IsSupportedFeature(const std::string &feature) const; + bool powerOf2BlockLength_ = false; + float minBlockLength_ = -1; + float maxBlockLength_ = -1; + public: LV2_PROPERTY_GETSET(bundle_path) LV2_PROPERTY_GETSET(uri) @@ -457,6 +460,9 @@ namespace pipedal LV2_PROPERTY_GETSET(modGui) LV2_PROPERTY_GETSET(patchProperties) LV2_PROPERTY_GETSET(hasDefaultState) + LV2_PROPERTY_GETSET(minBlockLength) + LV2_PROPERTY_GETSET(maxBlockLength) + LV2_PROPERTY_GETSET(powerOf2BlockLength) bool WantsWorkerThread() const; @@ -877,7 +883,6 @@ namespace pipedal std::vector lv2Features; MapFeature mapFeature; FileMetadataFeature fileMetadataFeature; - OptionsFeature optionsFeature; std::string pluginStoragePath; static void fn_LilvSetPortValueFunc(const char *port_symbol, diff --git a/vite/src/pipedal/AlsaDeviceInfo.tsx b/vite/src/pipedal/AlsaDeviceInfo.tsx index bc181fa..01563d2 100644 --- a/vite/src/pipedal/AlsaDeviceInfo.tsx +++ b/vite/src/pipedal/AlsaDeviceInfo.tsx @@ -11,6 +11,8 @@ export default class AlsaDeviceInfo { this.maxBufferSize = input.maxBufferSize; this.supportsCapture = input.supportsCapture ? true : false; this.supportsPlayback = input.supportsPlayback ? true : false; + this.captureBusy = input.captureBusy; + this.playbackBusy = input.playbackBusy; return this; } static deserialize_array(input: any): AlsaDeviceInfo[] @@ -67,4 +69,6 @@ export default class AlsaDeviceInfo { maxBufferSize: number = 0; supportsCapture: boolean = true; supportsPlayback: boolean = true; + captureBusy: boolean = false; + playbackBusy: boolean = false; }; \ No newline at end of file diff --git a/vite/src/pipedal/JackServerSettings.tsx b/vite/src/pipedal/JackServerSettings.tsx index 8b39ab7..ab1e1e7 100644 --- a/vite/src/pipedal/JackServerSettings.tsx +++ b/vite/src/pipedal/JackServerSettings.tsx @@ -81,12 +81,14 @@ export default class JackServerSettings { ? this.alsaOutputDevice.substring(3) : this.alsaOutputDevice; + let device: string; if (inDev === outDev) { - return inDev; + device = inDev; } else { - return inDev + "-> " + outDev; + device = inDev + "-> " + outDev; } + return `${device} ${this.sampleRate} ${this.bufferSize}x${this.numberOfBuffers}`; } } \ No newline at end of file diff --git a/vite/src/pipedal/JackServerSettingsDialog.tsx b/vite/src/pipedal/JackServerSettingsDialog.tsx index 2c0c3a1..3bb5e71 100644 --- a/vite/src/pipedal/JackServerSettingsDialog.tsx +++ b/vite/src/pipedal/JackServerSettingsDialog.tsx @@ -50,7 +50,9 @@ import ResizeResponsiveComponent from './ResizeResponsiveComponent'; function filterDevices(devices: AlsaDeviceInfo[]): AlsaDeviceInfo[] { return devices.filter(d => { const name = (d.name + ' ' + d.longName).toLowerCase(); - return !(name.includes('hdmi') || name.includes('bcm2835')); + return !(name.includes('hdmi') + || name.includes('bcm2835') // Pi 4 headphones out. + ); }); } @@ -345,7 +347,7 @@ const JackServerSettingsDialog = withStyles( } }) .catch((error) => { - // Error requesting ALSA info. + console.log("Failed to get ALSA device info: " + error.message); }); } @@ -621,16 +623,16 @@ const JackServerSettingsDialog = withStyles( const devicesSelected = (selectedInputDevice && selectedOutputDevice); let bufferSizes: number[] = devicesSelected ? - getValidBufferSizesMultiple(selectedInputDevice, selectedOutputDevice) : []; + getValidBufferSizesMultiple(selectedInputDevice, selectedOutputDevice) : [this.state.jackServerSettings.bufferSize]; let bufferCounts = devicesSelected ? - getValidBufferCountsMultiple(this.state.jackServerSettings.bufferSize, selectedInputDevice, selectedOutputDevice) : []; + getValidBufferCountsMultiple(this.state.jackServerSettings.bufferSize, selectedInputDevice, selectedOutputDevice) : [this.state.jackServerSettings.numberOfBuffers ]; let bufferSizeDisabled = !devicesSelected; let bufferCountDisabled = !devicesSelected; let sampleRates = devicesSelected && selectedInputDevice && selectedOutputDevice ? intersectArrays(selectedInputDevice.sampleRates, selectedOutputDevice.sampleRates) : []; let sampleRateOptions = sampleRates.length === 0 && this.state.jackServerSettings.sampleRate ? [this.state.jackServerSettings.sampleRate] : sampleRates; - let ultraCompact = this.windowSize.width < 500; + return ( <> { this.handleClose(); }} aria-labelledby="select-channels-title" open={this.props.open} @@ -654,9 +656,14 @@ const JackServerSettingsDialog = withStyles( disabled={!this.state.alsaDevices || this.state.alsaDevices.length === 0} style={{ width: 220 }} > - {sortedDevices.filter(d => d.supportsCapture).map(d => ( - {d.name} - )) || Loading...} + {sortedDevices.filter(d => + (d.supportsCapture || d.captureBusy) + ).map(d => ( + {d.name} + ))} @@ -670,9 +677,15 @@ const JackServerSettingsDialog = withStyles( disabled={!this.state.alsaDevices || this.state.alsaDevices.length === 0} style={{ width: 220 }} > - {sortedDevices.filter(d => d.supportsPlayback).map(d => ( - {d.name} - )) || Loading...} + {sortedDevices.filter( + d => (d.supportsPlayback + || d.playbackBusy) + ).map(d => ( + {d.name} + ))} diff --git a/vite/src/pipedal/SettingsDialog.tsx b/vite/src/pipedal/SettingsDialog.tsx index 534799a..71722b8 100644 --- a/vite/src/pipedal/SettingsDialog.tsx +++ b/vite/src/pipedal/SettingsDialog.tsx @@ -23,6 +23,7 @@ import OkCancelDialog from './OkCancelDialog'; import RadioSelectDialog from './RadioSelectDialog'; import IconButtonEx from './IconButtonEx'; import Typography from '@mui/material/Typography'; +import {isDarkMode} from './DarkMode'; import { PiPedalModel, PiPedalModelFactory, State } from './PiPedalModel'; import { ColorTheme } from './DarkMode'; import ButtonBase from "@mui/material/ButtonBase"; @@ -661,7 +662,8 @@ const SettingsDialog = withStyles( {(!isConfigValid) ? (
- Status: Not configured. + Status: + Not configured. {(!this.props.onboarding) && ( Governor: )}