/* * 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