diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index 6ecfaed..ab1507a 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -31,6 +31,7 @@ #include "SchedulerPriority.hpp" #include "JackDriver.hpp" #include "AlsaDriver.hpp" +#include "PipeWireDriver.hpp" #include "DummyAudioDriver.hpp" #include "AtomConverter.hpp" #include @@ -482,6 +483,7 @@ private: Uris uris; std::unique_ptr audioDriver; + std::string driverType_; std::recursive_mutex mutex; int64_t overrunGracePeriodSamples = 0; @@ -1088,6 +1090,11 @@ private: { OnSnapshotTriggered(5); } + else if (this->realtimeActiveMixerEngine != nullptr && (event.buffer[0] & 0xF0) == 0xB0) + { + // Route MIDI CC to mixer engine's control surface mapper + this->realtimeActiveMixerEngine->processMidiEvent(event); + } else { ProcessMidiMonitor(eventBufferWriter, iterator, event); @@ -1418,7 +1425,7 @@ private: } bool processed = false; - if (pedalboard != nullptr) + if (pedalboard != nullptr || this->realtimeActiveMixerEngine != nullptr) { ProcessGlobalMidiInput(); } @@ -1444,7 +1451,7 @@ private: } public: - AudioHostImpl(IHost *pHost) + AudioHostImpl(IHost *pHost, const std::string &driverType) : inputRingBuffer(RING_BUFFER_SIZE), outputRingBuffer(RING_BUFFER_SIZE), realtimeReader(&this->inputRingBuffer), @@ -1454,7 +1461,8 @@ public: eventBufferUrids(pHost), pHost(pHost), uris(pHost), - atomConverter(pHost->GetMapFeature()) + atomConverter(pHost->GetMapFeature()), + driverType_(driverType) { realtimeAtomBuffer.resize(32 * 1024); lv2_atom_forge_init(&inputWriterForge, pHost->GetMapFeature().GetMap()); @@ -1881,7 +1889,16 @@ public: isOpen = true; this->isDummyAudioDriver = jackServerSettings.IsDummyAudioDevice(); - this->audioDriver = std::unique_ptr(CreateAlsaDriver(this)); + + if (driverType_ == "pipewire") + { + this->audioDriver = std::unique_ptr(CreatePipeWireDriver(this)); + Lv2Log::info("Using PipeWire audio driver."); + } + else + { + this->audioDriver = std::unique_ptr(CreateAlsaDriver(this)); + } this->currentSample = 0; this->underruns = 0; @@ -2497,9 +2514,9 @@ void AudioHostImpl::SetSystemMidiBindings(const std::vector &bindin } } -AudioHost *AudioHost::CreateInstance(IHost *pHost) +AudioHost *AudioHost::CreateInstance(IHost *pHost, const std::string &driverType) { - return new AudioHostImpl(pHost); + return new AudioHostImpl(pHost, driverType); } // Removed because any updates to state have to be sent to clients as well, diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index c0cc4ad..f117c2a 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -215,7 +215,7 @@ namespace pipedal AudioHost() {} public: - static AudioHost *CreateInstance(IHost *pHost); + static AudioHost *CreateInstance(IHost *pHost, const std::string &driverType = "alsa"); virtual ~AudioHost() {}; virtual void UpdateServerConfiguration(const JackServerSettings &jackServerSettings, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ffdbf7b..72359ac 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -366,6 +366,7 @@ set (PIPEDAL_SOURCES JackDriver.cpp JackDriver.hpp AlsaDriver.cpp AlsaDriver.hpp DummyAudioDriver.cpp DummyAudioDriver.hpp + PipeWireDriver.cpp PipeWireDriver.hpp AudioDriver.hpp AudioConfig.hpp @@ -374,6 +375,8 @@ set (PIPEDAL_SOURCES MixerBus.cpp MixerBus.hpp MixerEngine.cpp MixerEngine.hpp MixerApi.cpp MixerApi.hpp + MidiMapper.cpp MidiMapper.hpp + MidiLearnMode.cpp MidiLearnMode.hpp ${VST3_SOURCES} ) diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index e342180..a6986d7 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -438,7 +438,7 @@ void PiPedalModel::Load() UpdateDefaults(&this->pedalboard); - std::unique_ptr p{AudioHost::CreateInstance(pluginHost.asIHost())}; + std::unique_ptr p{AudioHost::CreateInstance(pluginHost.asIHost(), this->driverType_)}; this->audioHost = std::move(p); this->audioHost->SetNotificationCallbacks(this); diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 5e59c93..8b4d7eb 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -286,6 +286,7 @@ namespace pipedal void UpdateVst3Settings(Pedalboard &pedalboard); PiPedalConfiguration configuration; + std::string driverType_ = "alsa"; void CheckForResourceInitialization(Pedalboard &pedalboard); UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId, const std::string &patchPropertyUri); @@ -294,6 +295,9 @@ namespace pipedal PiPedalModel(); virtual ~PiPedalModel(); + void SetDriverType(const std::string &driverType) { driverType_ = driverType; } + const std::string &GetDriverType() const { return driverType_; } + enum class Direction { Increase, diff --git a/src/PipeWireDriver.cpp b/src/PipeWireDriver.cpp new file mode 100644 index 0000000..f2145ea --- /dev/null +++ b/src/PipeWireDriver.cpp @@ -0,0 +1,901 @@ +/* + * MIT License + * + * 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 + * 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 "PiPedalCommon.hpp" +#include "util.hpp" +#include +#include "Finally.hpp" +#include +#include +#include "ss.hpp" +#include "PipeWireDriver.hpp" +#include "JackServerSettings.hpp" +#include +#include "RtInversionGuard.hpp" +#include "PiPedalException.hpp" +#include "SchedulerPriority.hpp" +#include "CrashGuard.hpp" +#include +#include +#include "ChannelRouterSettings.hpp" + +#include "CpuUse.hpp" + +#include +#include +#include +#include + +#include "Lv2Log.hpp" +#include +#include "ss.hpp" + +using namespace pipedal; + +namespace pipedal +{ + + class PipeWireDriverImpl : public AudioDriver + { + private: + // ---- PipeWire state ---- + pw_filter *filter = nullptr; + pw_thread_loop *threadLoop = nullptr; + + void *inputPortData = nullptr; + void *outputPortData = nullptr; + + bool pwInitialized = false; + + // ---- Audio parameters ---- + uint32_t sampleRate = 0; + uint32_t bufferSize = 0; + uint32_t captureChannels = 0; + uint32_t playbackChannels = 0; + + // ---- Buffer management (mirrors AlsaDriver pattern) ---- + std::vector> allocatedBuffers; + + std::vector deviceCaptureBuffers; + std::vector devicePlaybackBuffers; + float *zeroInputBuffer = nullptr; + float *discardOutputBuffer = nullptr; + std::vector mainCaptureBuffers; + std::vector mainPlaybackBuffers; + std::vector auxCaptureBuffers; + std::vector auxPlaybackBuffers; + + // ---- Mix ops for channel routing ---- + using MixOp = std::function; + std::vector mixOps; + + void AddMixCopyOp(float *inputBuffer, float *outputBuffer) + { + mixOps.push_back([inputBuffer, outputBuffer](size_t nFrames) + { + float * PIPEDAL_RESTRICT pIn = inputBuffer; + float * PIPEDAL_RESTRICT pOut = outputBuffer; + for (size_t i = 0; i < nFrames; ++i) + pOut[i] = pIn[i]; }); + } + void AddMixAddOp(float *inputBuffer, float *outputBuffer) + { + mixOps.push_back([inputBuffer, outputBuffer](size_t nFrames) + { + float * PIPEDAL_RESTRICT pIn = inputBuffer; + float * PIPEDAL_RESTRICT pOut = outputBuffer; + for (size_t i = 0; i < nFrames; ++i) + pOut[i] += pIn[i]; }); + } + + // ---- CPU monitoring ---- + pipedal::CpuUse cpuUse; + + // ---- Lifecycle state ---- + bool open = false; + bool activated = false; + bool isDummyDriver = false; + + AudioDriverHost *driverHost = nullptr; + ChannelSelection channelSelection; + JackServerSettings jackServerSettings; + AlsaSequencer::ptr alsaSequencer; + + // ---- MIDI (unused by PipeWire, sequencer handles it) ---- + std::vector midiEvents; + size_t midiEventCount = 0; + + public: + PipeWireDriverImpl(AudioDriverHost *driverHost) + : driverHost(driverHost) + { + } + + virtual ~PipeWireDriverImpl() + { + Close(); + } + + // ---------------------------------------------------------------- + // AudioDriver interface implementation + // ---------------------------------------------------------------- + + virtual float CpuUse() override + { + return cpuUse.GetCpuUse(); + } + + virtual float CpuOverhead() override + { + return cpuUse.GetCpuOverhead(); + } + + virtual uint32_t GetSampleRate() override + { + return this->sampleRate; + } + + virtual size_t GetMidiInputEventCount() override + { + return midiEventCount; + } + + virtual MidiEvent *GetMidiEvents() override + { + return this->midiEvents.data(); + } + + virtual const ChannelSelection &GetChannelSelection() const override + { + return channelSelection; + } + + virtual std::vector &DeviceInputBuffers() override { return this->deviceCaptureBuffers; } + virtual size_t DeviceInputBufferCount() const override { return deviceCaptureBuffers.size(); } + virtual float *GetDeviceInputBuffer(size_t channel) const override + { + if (channel >= deviceCaptureBuffers.size()) + return nullptr; + return deviceCaptureBuffers[channel]; + } + + virtual std::vector &DeviceOutputBuffers() override { return this->devicePlaybackBuffers; } + virtual size_t DeviceOutputBufferCount() const override { return devicePlaybackBuffers.size(); } + virtual float *GetDeviceOutputBuffer(size_t channel) const override + { + if (channel >= devicePlaybackBuffers.size()) + return nullptr; + return devicePlaybackBuffers[channel]; + } + + virtual std::vector &MainInputBuffers() override { return this->mainCaptureBuffers; } + virtual size_t MainInputBufferCount() const override { return mainCaptureBuffers.size(); } + virtual float *GetMainInputBuffer(size_t channel) override + { + if (channel >= (int64_t)mainCaptureBuffers.size()) + throw std::runtime_error("Argument out of range."); + return mainCaptureBuffers[channel]; + } + + virtual std::vector &MainOutputBuffers() override { return this->mainPlaybackBuffers; } + virtual size_t MainOutputBufferCount() const override { return mainPlaybackBuffers.size(); } + virtual float *GetMainOutputBuffer(size_t channel) override + { + return mainPlaybackBuffers[channel]; + } + + virtual std::vector &AuxInputBuffers() override { return this->auxCaptureBuffers; } + virtual size_t AuxInputBufferCount() const override { return auxCaptureBuffers.size(); } + virtual float *GetAuxInputBuffer(size_t channel) override + { + return auxCaptureBuffers[channel]; + } + + virtual std::vector &AuxOutputBuffers() override { return this->auxPlaybackBuffers; } + virtual size_t AuxOutputBufferCount() const override { return auxPlaybackBuffers.size(); } + virtual float *GetAuxOutputBuffer(size_t channel) override + { + return auxPlaybackBuffers[channel]; + } + + virtual float *GetZeroInputBuffer() override + { + if (zeroInputBuffer == nullptr) + zeroInputBuffer = AllocateAudioBuffer(); + return zeroInputBuffer; + } + + virtual float *GetDiscardOutputBuffer() override + { + if (discardOutputBuffer == nullptr) + discardOutputBuffer = AllocateAudioBuffer(); + return discardOutputBuffer; + } + + // ---------------------------------------------------------------- + // Open - Initialize PipeWire and create the filter + // ---------------------------------------------------------------- + virtual void Open(const JackServerSettings &jackServerSettings_, + const ChannelSelection &channelSelection_) override + { + if (open) + throw PiPedalStateException("Already open."); + + this->jackServerSettings = jackServerSettings_; + this->channelSelection = channelSelection_; + this->isDummyDriver = jackServerSettings_.IsDummyAudioDevice(); + this->bufferSize = jackServerSettings_.GetBufferSize(); + this->sampleRate = (uint32_t)jackServerSettings_.GetSampleRate(); + + if (this->sampleRate == 0) + this->sampleRate = 48000; + if (this->bufferSize == 0) + this->bufferSize = 256; + + open = true; + + try + { + if (!pwInitialized) + { + pw_init(nullptr, nullptr); + pwInitialized = true; + } + + // Create thread loop for non-blocking lifecycle management + threadLoop = pw_thread_loop_new("pipedal-pw-loop", nullptr); + if (!threadLoop) + { + throw PiPedalStateException("Failed to create PipeWire thread loop."); + } + + // Create filter using the thread loop's pw_loop + struct pw_properties *props = pw_properties_new( + PW_KEY_NODE_NAME, "PiPedal", + PW_KEY_NODE_DESCRIPTION, "PiPedal Guitar Effects Processor", + PW_KEY_MEDIA_NAME, "PiPedal Audio", + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Filter", + PW_KEY_MEDIA_CLASS, "Audio/Sink", + PW_KEY_APP_NAME, "PiPedal", + PW_KEY_APP_PROCESS_BINARY, "pipedald", + PW_KEY_PRIORITY_SESSION, "0", + nullptr); + + static const struct pw_filter_events filterEvents = { + .version = PW_VERSION_FILTER_EVENTS, + .state_changed = on_filter_state_changed_static, + .process = on_filter_process_static, + }; + + filter = pw_filter_new_simple( + pw_thread_loop_get_loop(threadLoop), + "PiPedal", + props, + &filterEvents, + this); + + if (!filter) + { + pw_thread_loop_destroy(threadLoop); + threadLoop = nullptr; + throw PiPedalStateException("Failed to create PipeWire filter."); + } + + // Determine channel count from channel selection + captureChannels = (uint32_t)channelSelection_.mainInputChannels().size(); + playbackChannels = (uint32_t)channelSelection_.mainOutputChannels().size(); + + if (captureChannels == 0) + captureChannels = 2; // default stereo + if (playbackChannels == 0) + playbackChannels = 2; + + // Build audio format params for input (capture) port + { + uint8_t buffer[1024]; + spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + spa_audio_info_raw audioFormat = {}; + audioFormat.format = SPA_AUDIO_FORMAT_F32; + audioFormat.rate = this->sampleRate; + audioFormat.channels = captureChannels; + + // Set channel positions + SetChannelPositions(audioFormat, captureChannels); + + const spa_pod *params[1]; + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &audioFormat); + + inputPortData = pw_filter_add_port( + filter, + PW_DIRECTION_INPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(void *), + pw_properties_new( + PW_KEY_PORT_NAME, "Input", + PW_KEY_AUDIO_CHANNELS, std::to_string(captureChannels).c_str(), + nullptr), + params, 1); + + if (!inputPortData) + { + pw_filter_destroy(filter); + filter = nullptr; + pw_thread_loop_destroy(threadLoop); + threadLoop = nullptr; + throw PiPedalStateException("Failed to add PipeWire input port."); + } + } + + // Build audio format params for output (playback) port + { + uint8_t buffer[1024]; + spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + spa_audio_info_raw audioFormat = {}; + audioFormat.format = SPA_AUDIO_FORMAT_F32; + audioFormat.rate = this->sampleRate; + audioFormat.channels = playbackChannels; + + SetChannelPositions(audioFormat, playbackChannels); + + const spa_pod *params[1]; + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &audioFormat); + + outputPortData = pw_filter_add_port( + filter, + PW_DIRECTION_OUTPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(void *), + pw_properties_new( + PW_KEY_PORT_NAME, "Output", + PW_KEY_AUDIO_CHANNELS, std::to_string(playbackChannels).c_str(), + nullptr), + params, 1); + + if (!outputPortData) + { + pw_filter_destroy(filter); + filter = nullptr; + pw_thread_loop_destroy(threadLoop); + threadLoop = nullptr; + throw PiPedalStateException("Failed to add PipeWire output port."); + } + } + + // Connect the filter to the PipeWire graph + int res = pw_filter_connect( + filter, + PW_FILTER_FLAG_RT_PROCESS, + nullptr, 0); + + if (res < 0) + { + pw_filter_destroy(filter); + filter = nullptr; + pw_thread_loop_destroy(threadLoop); + threadLoop = nullptr; + throw PiPedalStateException( + std::string("Failed to connect PipeWire filter: ") + strerror(-res)); + } + + // Start the thread loop + pw_thread_loop_start(threadLoop); + + Lv2Log::info(SS("PipeWire driver opened: " << captureChannels + << " capture channels, " + << playbackChannels + << " playback channels, " + << this->sampleRate << "Hz, " + << this->bufferSize << " frames")); + } + catch (const std::exception &e) + { + Close(); + throw; + } + } + + // ---------------------------------------------------------------- + // Activate - allocate buffers and start processing + // ---------------------------------------------------------------- + virtual void Activate() override + { + if (activated) + throw PiPedalStateException("Already activated."); + + activated = true; + + // Reset previously allocated buffers + allocatedBuffers.resize(0); + + // Allocate device capture buffers + zeroInputBuffer = AllocateAudioBuffer(); + deviceCaptureBuffers.resize(captureChannels); + for (size_t i = 0; i < captureChannels; ++i) + { + deviceCaptureBuffers[i] = AllocateAudioBuffer(); + } + + // Allocate device playback buffers + devicePlaybackBuffers.resize(playbackChannels); + for (size_t i = 0; i < playbackChannels; ++i) + { + devicePlaybackBuffers[i] = AllocateAudioBuffer(); + } + + // Allocate input channel routing (main) + AllocateInputChannels( + channelSelection.mainInputChannels(), + this->mainCaptureBuffers); + + // Allocate output channel routing (main) + AllocateOutputChannels( + channelSelection.mainOutputChannels(), + this->mainPlaybackBuffers); + + // Allocate aux channels + AllocateAuxChannels(); + + // Set up mix operations for channel routing + AddMixOps(); + } + + // ---------------------------------------------------------------- + // Deactivate - stop processing + // ---------------------------------------------------------------- + virtual void Deactivate() override + { + if (!activated) + return; + + activated = false; + + // The pw_filter will continue to call process but we won't + // do anything since activated is false. The PipeWire graph + // will eventually stop scheduling us. + + // Clear mix ops + mixOps.clear(); + } + + // ---------------------------------------------------------------- + // Close - tear down PipeWire resources + // ---------------------------------------------------------------- + virtual void Close() override + { + if (!open) + return; + open = false; + + Deactivate(); + + // Clean up PipeWire filter + if (filter) + { + pw_filter_disconnect(filter); + pw_filter_destroy(filter); + filter = nullptr; + } + + // Stop and destroy thread loop + if (threadLoop) + { + pw_thread_loop_stop(threadLoop); + pw_thread_loop_destroy(threadLoop); + threadLoop = nullptr; + } + + // Deinitialize PipeWire (only if we initialized it) + if (pwInitialized) + { + // pw_deinit(); // Note: we don't call this to avoid issues with + // other PipeWire users in the process. It's safe to leave initialized. + } + + DeleteBuffers(); + this->alsaSequencer = nullptr; + + Lv2Log::info("PipeWire driver closed."); + } + + // ---------------------------------------------------------------- + // SetAlsaSequencer - store for MIDI routing (not used by PipeWire) + // ---------------------------------------------------------------- + virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override + { + this->alsaSequencer = alsaSequencer; + } + + // ---------------------------------------------------------------- + // GetConfigurationDescription - human-readable description + // ---------------------------------------------------------------- + virtual std::string GetConfigurationDescription() override + { + std::stringstream s; + s << "PipeWire: " << captureChannels << " in, " + << playbackChannels << " out, " + << this->sampleRate << " Hz, " + << this->bufferSize << " frames"; + return s.str(); + } + + // ---------------------------------------------------------------- + // DumpBufferTrace - stub (no PipeWire equivalent) + // ---------------------------------------------------------------- + virtual void DumpBufferTrace(size_t nEntries) override + { + // Not implemented for PipeWire driver + } + + private: + // ---------------------------------------------------------------- + // Static PipeWire filter event callbacks + // ---------------------------------------------------------------- + static void on_filter_process_static(void *data, struct spa_io_position *position) + { + auto *self = static_cast(data); + self->on_filter_process(position); + } + + static void on_filter_state_changed_static( + void *data, + enum pw_filter_state old, + enum pw_filter_state state, + const char *error) + { + auto *self = static_cast(data); + self->on_filter_state_changed(old, state, error); + } + + // ---------------------------------------------------------------- + // Filter state change handler + // ---------------------------------------------------------------- + void on_filter_state_changed( + enum pw_filter_state old, + enum pw_filter_state state, + const char *error) + { + if (state == PW_FILTER_STATE_ERROR) + { + Lv2Log::error(SS("PipeWire filter error: " << (error ? error : "unknown"))); + } + } + + // ---------------------------------------------------------------- + // Main processing callback - called from PipeWire RT thread + // ---------------------------------------------------------------- + void on_filter_process(struct spa_io_position *position) + { + if (!activated) + return; + + pw_buffer *inBuf = nullptr; + pw_buffer *outBuf = nullptr; + + // Dequeue input buffer (capture) + inBuf = (pw_buffer *)pw_filter_dequeue_buffer(inputPortData); + if (!inBuf) + return; + + // Dequeue output buffer (playback) + outBuf = (pw_buffer *)pw_filter_dequeue_buffer(outputPortData); + if (!outBuf) + { + pw_filter_queue_buffer(inputPortData, inBuf); + return; + } + + spa_buffer *spaIn = inBuf->buffer; + spa_buffer *spaOut = outBuf->buffer; + + uint32_t nInputChannels = std::min(spaIn->n_datas, captureChannels); + uint32_t nOutputChannels = std::min(spaOut->n_datas, playbackChannels); + + // Determine frame count from the input buffer data size + uint32_t nFrames = 0; + if (spaIn->n_datas > 0 && spaIn->datas[0].chunk) + { + nFrames = spaIn->datas[0].chunk->size / sizeof(float); + if (nInputChannels > 1) + nFrames /= nInputChannels; + } + + if (nFrames == 0) + nFrames = this->bufferSize; + + // Clamp to our buffer size + if (nFrames > this->bufferSize) + nFrames = this->bufferSize; + + // ---- Read input from PipeWire ---- + // Copy input data from PipeWire buffers to our device capture buffers. + // PipeWire uses planar (non-interleaved) format where each data chunk is one channel. + for (uint32_t ch = 0; ch < nInputChannels; ++ch) + { + if (ch < deviceCaptureBuffers.size() && spaIn->datas[ch].data != nullptr) + { + float *src = (float *)((uint8_t *)spaIn->datas[ch].data + + (spaIn->datas[ch].chunk ? spaIn->datas[ch].chunk->offset : 0)); + float *dst = deviceCaptureBuffers[ch]; + for (uint32_t i = 0; i < nFrames; ++i) + { + dst[i] = src[i]; + } + } + } + + // Zero out any remaining input channels that PipeWire didn't fill + for (uint32_t ch = nInputChannels; ch < captureChannels; ++ch) + { + if (ch < deviceCaptureBuffers.size()) + { + float *dst = deviceCaptureBuffers[ch]; + memset(dst, 0, nFrames * sizeof(float)); + } + } + + // ---- Channel routing (input) ---- + for (auto &mixOp : mixOps) + { + mixOp(nFrames); + } + + // ---- Process audio via the host ---- + // This is where PiPedal's DSP pipeline runs (on the RT thread) + driverHost->OnProcess(nFrames); + + // ---- Write output to PipeWire ---- + for (uint32_t ch = 0; ch < nOutputChannels; ++ch) + { + if (ch < devicePlaybackBuffers.size() && spaOut->datas[ch].data != nullptr) + { + float *dst = (float *)((uint8_t *)spaOut->datas[ch].data + + (spaOut->datas[ch].chunk ? spaOut->datas[ch].chunk->offset : 0)); + float *src = devicePlaybackBuffers[ch]; + uint32_t copyFrames = std::min(nFrames, + (uint32_t)(spaOut->datas[ch].chunk ? + spaOut->datas[ch].chunk->size / sizeof(float) : nFrames)); + for (uint32_t i = 0; i < copyFrames; ++i) + { + dst[i] = src[i]; + } + } + } + + // Queue both buffers back to PipeWire + pw_filter_queue_buffer(outputPortData, outBuf); + pw_filter_queue_buffer(inputPortData, inBuf); + } + + // ---------------------------------------------------------------- + // Buffer allocation helpers (mirrors AlsaDriver pattern) + // ---------------------------------------------------------------- + float *AllocateAudioBuffer() + { + std::vector buffer; + buffer.resize(this->bufferSize); + float *pBuffer = buffer.data(); + allocatedBuffers.push_back(std::move(buffer)); + return pBuffer; + } + + void DeleteBuffers() + { + mainCaptureBuffers.clear(); + mainPlaybackBuffers.clear(); + auxCaptureBuffers.clear(); + auxPlaybackBuffers.clear(); + deviceCaptureBuffers.clear(); + devicePlaybackBuffers.clear(); + zeroInputBuffer = nullptr; + discardOutputBuffer = nullptr; + allocatedBuffers.clear(); + } + + // ---------------------------------------------------------------- + // Channel allocation helpers (mirrors AlsaDriver pattern) + // ---------------------------------------------------------------- + void AllocateInputChannels( + const std::vector &channelSelection, + std::vector &channelBuffers) + { + size_t nChannels = channelSelection.size(); + if (nChannels == 0) + { + channelBuffers.resize(0); + return; + } + + channelBuffers.resize(nChannels); + for (size_t i = 0; i < nChannels; ++i) + { + int64_t deviceChannel = channelSelection[i]; + if (deviceChannel == -1 || (size_t)deviceChannel >= captureChannels) + { + channelBuffers[i] = zeroInputBuffer; + } + else + { + channelBuffers[i] = deviceCaptureBuffers[deviceChannel]; + } + } + } + + void AllocateOutputChannels( + const std::vector &channelSelection, + std::vector &channelBuffers) + { + size_t nChannels = channelSelection.size(); + if (nChannels == 0) + { + channelBuffers.resize(0); + return; + } + + channelBuffers.resize(nChannels); + for (size_t i = 0; i < nChannels; ++i) + { + int64_t deviceChannel = channelSelection[i]; + if (deviceChannel == -1) + { + channelBuffers[i] = this->GetDiscardOutputBuffer(); + } + else + { + float *mixBuffer = AllocateAudioBuffer(); + channelBuffers[i] = mixBuffer; + } + } + } + + void AllocateAuxChannels() + { + for (auto ix : channelSelection.auxInputChannels()) + { + if ((size_t)ix < deviceCaptureBuffers.size()) + auxCaptureBuffers.push_back(this->deviceCaptureBuffers[ix]); + else + auxCaptureBuffers.push_back(this->zeroInputBuffer); + } + for (auto ix : channelSelection.auxOutputChannels()) + { + if ((size_t)ix < devicePlaybackBuffers.size()) + auxPlaybackBuffers.push_back(this->devicePlaybackBuffers[ix]); + else + auxPlaybackBuffers.push_back(this->GetDiscardOutputBuffer()); + } + } + + // ---------------------------------------------------------------- + // Mix operations for channel routing (mirrors AlsaDriver) + // ---------------------------------------------------------------- + void AddMixOps() + { + // Main input: copy from device capture channels to main capture channels + for (size_t i = 0; i < mainCaptureBuffers.size(); ++i) + { + if (mainCaptureBuffers[i] != deviceCaptureBuffers[channelSelection.mainInputChannels()[i]] && + mainCaptureBuffers[i] != zeroInputBuffer) + { + // Direct pointer, no copy needed (already set up in AllocateInputChannels) + } + } + + // Main output: copy from main playback to device playback channels + for (size_t i = 0; i < mainPlaybackBuffers.size(); ++i) + { + int64_t deviceChannel = channelSelection.mainOutputChannels()[i]; + if (deviceChannel >= 0 && (size_t)deviceChannel < devicePlaybackBuffers.size()) + { + if (mainPlaybackBuffers[i] != devicePlaybackBuffers[deviceChannel]) + { + // Mix copy: main playback buffer → device playback buffer + AddMixCopyOp(mainPlaybackBuffers[i], devicePlaybackBuffers[deviceChannel]); + } + } + } + } + + // ---------------------------------------------------------------- + // Channel position helper for SPA audio format + // ---------------------------------------------------------------- + static void SetChannelPositions(spa_audio_info_raw &format, uint32_t channels) + { + switch (channels) + { + case 1: + format.position[0] = SPA_AUDIO_CHANNEL_MONO; + break; + case 2: + format.position[0] = SPA_AUDIO_CHANNEL_FL; + format.position[1] = SPA_AUDIO_CHANNEL_FR; + break; + case 3: + format.position[0] = SPA_AUDIO_CHANNEL_FL; + format.position[1] = SPA_AUDIO_CHANNEL_FR; + format.position[2] = SPA_AUDIO_CHANNEL_FC; + break; + case 4: + format.position[0] = SPA_AUDIO_CHANNEL_FL; + format.position[1] = SPA_AUDIO_CHANNEL_FR; + format.position[2] = SPA_AUDIO_CHANNEL_FC; + format.position[3] = SPA_AUDIO_CHANNEL_LFE; + break; + case 5: + format.position[0] = SPA_AUDIO_CHANNEL_FL; + format.position[1] = SPA_AUDIO_CHANNEL_FR; + format.position[2] = SPA_AUDIO_CHANNEL_FC; + format.position[3] = SPA_AUDIO_CHANNEL_LFE; + format.position[4] = SPA_AUDIO_CHANNEL_RL; + break; + case 6: + format.position[0] = SPA_AUDIO_CHANNEL_FL; + format.position[1] = SPA_AUDIO_CHANNEL_FR; + format.position[2] = SPA_AUDIO_CHANNEL_FC; + format.position[3] = SPA_AUDIO_CHANNEL_LFE; + format.position[4] = SPA_AUDIO_CHANNEL_RL; + format.position[5] = SPA_AUDIO_CHANNEL_RR; + break; + case 7: + format.position[0] = SPA_AUDIO_CHANNEL_FL; + format.position[1] = SPA_AUDIO_CHANNEL_FR; + format.position[2] = SPA_AUDIO_CHANNEL_FC; + format.position[3] = SPA_AUDIO_CHANNEL_LFE; + format.position[4] = SPA_AUDIO_CHANNEL_RL; + format.position[5] = SPA_AUDIO_CHANNEL_RR; + format.position[6] = SPA_AUDIO_CHANNEL_SL; + break; + case 8: + format.position[0] = SPA_AUDIO_CHANNEL_FL; + format.position[1] = SPA_AUDIO_CHANNEL_FR; + format.position[2] = SPA_AUDIO_CHANNEL_FC; + format.position[3] = SPA_AUDIO_CHANNEL_LFE; + format.position[4] = SPA_AUDIO_CHANNEL_RL; + format.position[5] = SPA_AUDIO_CHANNEL_RR; + format.position[6] = SPA_AUDIO_CHANNEL_SL; + format.position[7] = SPA_AUDIO_CHANNEL_SR; + break; + default: + // For > 8 channels, assign Aux channels + for (uint32_t i = 0; i < channels && i < SPA_AUDIO_MAX_CHANNELS; ++i) + { + format.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; + } + break; + } + } + }; + + // ---------------------------------------------------------------- + // Factory function + // ---------------------------------------------------------------- + AudioDriver *CreatePipeWireDriver(AudioDriverHost *driverHost) + { + return new PipeWireDriverImpl(driverHost); + } + +} // namespace pipedal diff --git a/src/PipeWireDriver.hpp b/src/PipeWireDriver.hpp new file mode 100644 index 0000000..bb9b742 --- /dev/null +++ b/src/PipeWireDriver.hpp @@ -0,0 +1,33 @@ +/* + * MIT License + * + * 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 + * 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" + +namespace pipedal { + + AudioDriver* CreatePipeWireDriver(AudioDriverHost* driverHost); + +} diff --git a/src/main.cpp b/src/main.cpp index 27c77cf..7027685 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -185,6 +185,7 @@ int main(int argc, char *argv[]) bool testExtraDevice = false; std::string logLevel; std::string portOption; + std::string driverOption = "alsa"; CommandLineParser parser; parser.AddOption("-h", &help); @@ -193,6 +194,7 @@ int main(int argc, char *argv[]) parser.AddOption("-log-level", &logLevel); parser.AddOption("-port", &portOption); parser.AddOption("-test-extra-device", &testExtraDevice); // advertise two different devices (for testing multi-device connect) + parser.AddOption("--driver", &driverOption); try { @@ -226,6 +228,7 @@ int main(int argc, char *argv[]) << " -systemd: Log to systemd journals instead of to the console.\n" << " -port: Port to listen on e.g. 80, or 0.0.0.0:80\n" << " -log-level: (debug|info|warning|error)\n" + << " --driver: Audio driver (alsa|pipewire, default: alsa)\n" << "Example:\n" << " pipedald /etc/pipedal/config /etc/pipedal/react -port 80 \n" "\n" @@ -398,6 +401,7 @@ int main(int argc, char *argv[]) }); model.Init(configuration); + model.SetDriverType(driverOption); // Get heavy IO out of the way before letting dependent (Jack/ALSA) services run. model.LoadLv2PluginInfo();