From eadeabf3862e795cf2c96e52ca0c2214f629a49c Mon Sep 17 00:00:00 2001 From: Dalton Messmer Date: Wed, 29 Jan 2025 03:43:17 -0500 Subject: [PATCH] Plugin audio ports (#3) * Add PluginAudioPort class (WIP) Refactors how the pin connector and audio buffers are incorporated into AudioPlugin, how buffer updates occur, and how buffer customization works. Also uses more class type NTTP to simplify more of the template parameters. * Add ConfigurableAudioPort; Fix build * Some fixes (WIP) * Update (WIP) * Replace AudioDataLayout with bool * Remove SampleType Reasons: - It's clunky to use - It bloats the PR - Without a strong typedef it does nothing that can't be accomplished with a comment * Update tests * Fix ZynAddSubFX when switching back to local * Minor cleanup --- include/AudioData.h | 90 +++---- include/AudioPlugin.h | 223 ++++++++--------- include/AudioPluginBuffer.h | 274 ++++++++++++-------- include/AudioPluginConfig.h | 53 ++++ include/PluginAudioPort.h | 169 +++++++++++++ include/PluginPinConnector.h | 103 ++++---- include/PluginPinConnectorView.h | 2 +- include/RemotePlugin.h | 30 +-- include/RemotePluginAudioPort.h | 288 ++++++++++++++++++++++ include/RemotePluginBase.h | 2 - include/RemotePluginClient.h | 12 - include/SampleFrame.h | 20 +- include/lmms_basics.h | 1 + plugins/Sid/SidInstrument.cpp | 2 +- plugins/Vestige/Vestige.cpp | 11 +- plugins/Vestige/Vestige.h | 12 +- plugins/VstBase/VstPlugin.cpp | 45 +--- plugins/VstBase/VstPlugin.h | 16 +- plugins/VstEffect/VstEffect.cpp | 18 +- plugins/VstEffect/VstEffect.h | 13 +- plugins/ZynAddSubFx/RemoteZynAddSubFx.cpp | 5 +- plugins/ZynAddSubFx/ZynAddSubFx.cpp | 56 ++--- plugins/ZynAddSubFx/ZynAddSubFx.h | 26 +- src/core/CMakeLists.txt | 1 + src/core/PluginPinConnector.cpp | 39 +-- src/core/RemotePlugin.cpp | 38 ++- src/core/RemotePluginAudioPort.cpp | 54 ++++ src/gui/PluginPinConnectorView.cpp | 2 +- tests/src/core/PluginPinConnectorTest.cpp | 116 ++++----- 29 files changed, 1122 insertions(+), 599 deletions(-) create mode 100644 include/AudioPluginConfig.h create mode 100644 include/PluginAudioPort.h create mode 100644 include/RemotePluginAudioPort.h create mode 100644 src/core/RemotePluginAudioPort.cpp diff --git a/include/AudioData.h b/include/AudioData.h index 1364b069cad..b1b349a218b 100644 --- a/include/AudioData.h +++ b/include/AudioData.h @@ -1,7 +1,7 @@ /* * AudioData.h - Audio data types * - * Copyright (c) 2024 Dalton Messmer + * Copyright (c) 2025 Dalton Messmer * * This file is part of LMMS - https://lmms.io * @@ -33,62 +33,30 @@ namespace lmms { -//! Conventions for passing audio data -enum class AudioDataLayout + +//! Types of audio data supported in LMMS +enum class AudioDataKind : std::uint8_t { - /** - * Given: - * - N == Frame count - * - C == Number of channels - * - i == Sample index, where 0 <= i < N - * - `samples` has the type sample_t* - * - `samples` size == N * C - */ + SampleFrame, + F32, + // F64, + // I16, + // etc. +}; - /** - * Layout where the samples for each channel are interleaved. - * i.e. "LRLRLRLR" - * - * Samples for individual channels can be accessed like this: - * - Channel #0 samples: samples[C*i] - * - Channel #1 samples: samples[C*i + 1] - * - Channel #2 samples: samples[C*i + 2] - * - Channel #3 samples: samples[C*i + 3] - * - ... - */ - Interleaved, +namespace detail { - /** - * Layout where all samples for a particular channel are grouped together. - * i.e. "LLLLRRRR" - * - * Samples for individual channels can be accessed like this: - * - Channel #0 samples: samples[i] - * - Channel #1 samples: samples[1*N + i] - * - Channel #2 samples: samples[2*N + i] - * - Channel #3 samples: samples[3*N + i] - * - ... - */ - Split -}; +//! Specialize this struct to enable the use of an audio data kind +template struct AudioDataType; +template<> struct AudioDataType { using type = float; }; -/** - * A simple type alias for floating point audio data types which documents the data layout. - * - * For example, `const InterleavedSampleType*` can be used as a replacement for `const sample_t*` - * parameters in order to document that the data layout of the audio is interleaved. - * - * NOTE: Can add support for integer sample types later - */ -template, bool> = true> -using SampleType = T; +} // namespace detail -template -using SplitSampleType = SampleType; -template -using InterleavedSampleType = SampleType; +//! Metafunction to convert `AudioDataKind` to its type +template +using GetAudioDataType = typename detail::AudioDataType::type; //! Use when the number of channels is not known at compile time @@ -96,11 +64,11 @@ inline constexpr int DynamicChannelCount = -1; /** - * Non-owning view for multi-channel "split" (non-interleaved) audio data + * Non-owning view for multi-channel non-interleaved audio data * * TODO C++23: Use std::mdspan */ -template> +template class SplitAudioData { public: @@ -112,7 +80,7 @@ class SplitAudioData * data[channels][frames] * Each buffer contains `frames` frames. */ - SplitAudioData(SplitSampleType* const* data, pi_ch_t channels, f_cnt_t frames) + SplitAudioData(SampleT* const* data, pi_ch_t channels, f_cnt_t frames) : m_data{data} , m_channels{channels} , m_frames{frames} @@ -133,17 +101,19 @@ class SplitAudioData * Returns pointer to the buffer of a given channel. * The size of the buffer is `frames()`. */ - auto buffer(pi_ch_t channel) const -> SplitSampleType* + auto buffer(pi_ch_t channel) const -> SampleT* { assert(channel < m_channels); + assert(m_data != nullptr); return m_data[channel]; } template - auto buffer() const -> SplitSampleType* + auto buffer() const -> SampleT* { static_assert(channel != DynamicChannelCount); static_assert(channel < channelCount); + assert(m_data != nullptr); return m_data[channel]; } @@ -161,21 +131,23 @@ class SplitAudioData auto frames() const -> f_cnt_t { return m_frames; } + auto empty() const -> bool { return !m_data || m_channels == 0 || m_frames == 0; } + /** * WARNING: This method assumes that internally there is a single * contiguous buffer for all channels whose size is channels() * frames(). * Whether this is true depends on the implementation of the source buffer. */ - auto sourceBuffer() const -> SplitSampleType* + auto sourceBuffer() const -> Span { assert(m_data != nullptr); - return m_data[0]; + return Span{m_data[0], channels() * frames()}; } - auto data() const -> SplitSampleType* const* { return m_data; } + auto data() const -> SampleT* const* { return m_data; } private: - SplitSampleType* const* m_data = nullptr; + SampleT* const* m_data = nullptr; pi_ch_t m_channels = 0; f_cnt_t m_frames = 0; }; diff --git a/include/AudioPlugin.h b/include/AudioPlugin.h index 1db78e18579..2a847e2359d 100644 --- a/include/AudioPlugin.h +++ b/include/AudioPlugin.h @@ -2,7 +2,7 @@ * AudioPlugin.h - Interface for audio plugins which provides * pin connector support and compile-time customizations * - * Copyright (c) 2024 Dalton Messmer + * Copyright (c) 2025 Dalton Messmer * * This file is part of LMMS - https://lmms.io * @@ -26,13 +26,13 @@ #ifndef LMMS_AUDIO_PLUGIN_H #define LMMS_AUDIO_PLUGIN_H +#include #include "AudioData.h" -#include "AudioPluginBuffer.h" +#include "AudioPluginConfig.h" #include "Effect.h" #include "Instrument.h" #include "InstrumentTrack.h" -#include "PluginPinConnector.h" -#include "SampleFrame.h" +#include "PluginAudioPort.h" namespace lmms { @@ -49,37 +49,18 @@ enum class ProcessStatus Sleep }; -//! Compile time customizations for `AudioPlugin` to meet the needs of a plugin implementation -struct PluginConfig -{ - //! The audio data layout used by the plugin - AudioDataLayout layout; - - //! The number of plugin input channels, or `DynamicChannelCount` if unknown at compile time - int inputs = DynamicChannelCount; - - //! The number of plugin output channels, or `DynamicChannelCount` if unknown at compile time - int outputs = DynamicChannelCount; - - //! In-place processing - true (always in-place) or false (dynamic, customizable in audio buffer impl) - bool inplace = false; - - //! If true, plugin implementation will provide an `AudioPluginBufferInterfaceProvider` - bool customBuffer = false; -}; - class NotePlayHandle; namespace detail { //! Provides the correct `processImpl` interface for instruments or effects to implement -template +template class AudioProcessingMethod; //! Instrument specialization template -class AudioProcessingMethod +class AudioProcessingMethod { protected: //! The main audio processing method for NotePlayHandle-based Instruments @@ -92,7 +73,7 @@ class AudioProcessingMethod //! Instrument specialization (in-place) template -class AudioProcessingMethod +class AudioProcessingMethod { protected: //! The main audio processing method for NotePlayHandle-based Instruments @@ -105,7 +86,7 @@ class AudioProcessingMethod //! Instrument specialization (custom working buffers) template -class AudioProcessingMethod +class AudioProcessingMethod { protected: /** @@ -124,7 +105,7 @@ class AudioProcessingMethod //! Effect specialization template -class AudioProcessingMethod +class AudioProcessingMethod { protected: /** @@ -136,7 +117,7 @@ class AudioProcessingMethod //! Effect specialization (in-place) template -class AudioProcessingMethod +class AudioProcessingMethod { protected: /** @@ -148,7 +129,7 @@ class AudioProcessingMethod //! Effect specialization (custom working buffers) template -class AudioProcessingMethod +class AudioProcessingMethod { protected: /** @@ -160,60 +141,65 @@ class AudioProcessingMethod }; //! Connects the core audio channels to the instrument or effect using the pin connector -template +template class AudioPlugin { static_assert(always_false_v, "ParentT must be either Instrument or Effect"); }; //! Instrument specialization -template -class AudioPlugin +template +class AudioPlugin : public Instrument , public AudioProcessingMethod::type, - typename AudioDataTypeSelector::type, - config.inplace, config.customBuffer> - , public std::conditional_t, - AudioPluginBufferDefaultImpl> + typename AudioDataViewSelector::type, + typename AudioDataViewSelector::type, + config.inplace, AudioPortT::provideProcessBuffers()> { public: + template AudioPlugin(const Plugin::Descriptor* desc, InstrumentTrack* parent = nullptr, const Plugin::Descriptor::SubPluginFeatures::Key* key = nullptr, - Instrument::Flags flags = Instrument::Flag::NoFlags) + Instrument::Flags flags = Instrument::Flag::NoFlags, + AudioPortArgsT&&... audioPortArgs) : Instrument{desc, parent, key, flags} - , m_pinConnector{config.inputs, config.outputs, true, this} + , m_audioPort{true, this, std::forward(audioPortArgs)...} { - connect(&m_pinConnector, &PluginPinConnector::pluginBuffersChanged, - this, &AudioPlugin::updatePluginBuffers); + m_audioPort.init(); } - auto pinConnector() const -> const PluginPinConnector* final { return &m_pinConnector; } - protected: + auto audioPort() -> AudioPortT& { return m_audioPort; } + + auto pinConnector() const -> const PluginPinConnector* final + { + return m_audioPort.active() + ? &m_audioPort.pinConnector() + : nullptr; + } + void playImpl(CoreAudioDataMut inOut) final { - SampleFrame* temp = inOut.data(); - const auto bus = CoreAudioBusMut{&temp, 1, inOut.size()}; - auto bufferInterface = this->bufferInterface(); - if (!bufferInterface) + auto buffers = m_audioPort.buffers(); + if (!buffers) { // Plugin is not running return; } - auto router = m_pinConnector.getRouter(); + SampleFrame* temp = inOut.data(); + const auto bus = CoreAudioBusMut{&temp, 1, inOut.size()}; + auto router = m_audioPort.getRouter(); if constexpr (config.inplace) { // Write core to plugin input buffer - const auto pluginInOut = bufferInterface->inputBuffer(); + const auto pluginInOut = buffers->inputOutputBuffer(); router.routeToPlugin(bus, pluginInOut); // Process - if constexpr (config.customBuffer) { this->processImpl(); } - else { this->processImpl(pluginInOut); } + if constexpr (AudioPortT::provideProcessBuffers()) { this->processImpl(pluginInOut); } + else { this->processImpl(); } // Write plugin output buffer to core router.routeFromPlugin(pluginInOut, bus); @@ -221,13 +207,13 @@ class AudioPlugin else { // Write core to plugin input buffer - const auto pluginIn = bufferInterface->inputBuffer(); - const auto pluginOut = bufferInterface->outputBuffer(); + const auto pluginIn = buffers->inputBuffer(); + const auto pluginOut = buffers->outputBuffer(); router.routeToPlugin(bus, pluginIn); // Process - if constexpr (config.customBuffer) { this->processImpl(); } - else { this->processImpl(pluginIn, pluginOut); } + if constexpr (AudioPortT::provideProcessBuffers()) { this->processImpl(pluginIn, pluginOut); } + else { this->processImpl(); } // Write plugin output buffer to core router.routeFromPlugin(pluginOut, bus); @@ -247,47 +233,40 @@ class AudioPlugin */ } - auto pinConnector() -> PluginPinConnector* { return &m_pinConnector; } - private: - void updatePluginBuffers() - { - auto iface = this->bufferInterface(); - if (!iface) { return; } - iface->updateBuffers( - m_pinConnector.in().channelCount(), - m_pinConnector.out().channelCount() - ); - } - - PluginPinConnector m_pinConnector; + AudioPortT m_audioPort; }; //! Effect specialization -template -class AudioPlugin +template +class AudioPlugin : public Effect , public AudioProcessingMethod::type, - typename AudioDataTypeSelector::type, - config.inplace, config.customBuffer> - , public std::conditional_t, - AudioPluginBufferDefaultImpl> + typename AudioDataViewSelector::type, + typename AudioDataViewSelector::type, + config.inplace, AudioPortT::provideProcessBuffers()> { public: + template AudioPlugin(const Plugin::Descriptor* desc, Model* parent = nullptr, - const Plugin::Descriptor::SubPluginFeatures::Key* key = nullptr) + const Plugin::Descriptor::SubPluginFeatures::Key* key = nullptr, + AudioPortArgsT&&... audioPortArgs) : Effect{desc, parent, key} - , m_pinConnector{config.inputs, config.outputs, false, this} + , m_audioPort{false, this, std::forward(audioPortArgs)...} { - connect(&m_pinConnector, &PluginPinConnector::pluginBuffersChanged, - this, &AudioPlugin::updatePluginBuffers); + m_audioPort.init(); } - auto pinConnector() const -> const PluginPinConnector* final { return &m_pinConnector; } - protected: + auto audioPort() -> AudioPortT& { return m_audioPort; } + + auto pinConnector() const -> const PluginPinConnector* final + { + return m_audioPort.active() + ? &m_audioPort.pinConnector() + : nullptr; + } + auto processAudioBufferImpl(CoreAudioDataMut inOut) -> bool final { if (isSleeping()) @@ -296,22 +275,24 @@ class AudioPlugin return false; } + auto buffers = m_audioPort.buffers(); + assert(buffers != nullptr); + SampleFrame* temp = inOut.data(); const auto bus = CoreAudioBusMut{&temp, 1, inOut.size()}; - auto bufferInterface = this->bufferInterface(); - auto router = m_pinConnector.getRouter(); + auto router = m_audioPort.getRouter(); ProcessStatus status; if constexpr (config.inplace) { // Write core to plugin input buffer - const auto pluginInOut = bufferInterface->inputBuffer(); + const auto pluginInOut = buffers->inputOutputBuffer(); router.routeToPlugin(bus, pluginInOut); // Process - if constexpr (config.customBuffer) { status = this->processImpl(); } - else { status = this->processImpl(pluginInOut); } + if constexpr (AudioPortT::provideProcessBuffers()) { status = this->processImpl(pluginInOut); } + else { status = this->processImpl(); } // Write plugin output buffer to core router.routeFromPlugin(pluginInOut, bus); @@ -319,13 +300,13 @@ class AudioPlugin else { // Write core to plugin input buffer - const auto pluginIn = bufferInterface->inputBuffer(); - const auto pluginOut = bufferInterface->outputBuffer(); + const auto pluginIn = buffers->inputBuffer(); + const auto pluginOut = buffers->outputBuffer(); router.routeToPlugin(bus, pluginIn); // Process - if constexpr (config.customBuffer) { status = this->processImpl(); } - else { status = this->processImpl(pluginIn, pluginOut); } + if constexpr (AudioPortT::provideProcessBuffers()) { status = this->processImpl(pluginIn, pluginOut); } + else { status = this->processImpl(); } // Write plugin output buffer to core router.routeFromPlugin(pluginOut, bus); @@ -363,20 +344,8 @@ class AudioPlugin { } - auto pinConnector() -> PluginPinConnector* { return &m_pinConnector; } - private: - void updatePluginBuffers() - { - auto iface = this->bufferInterface(); - if (!iface) { return; } - iface->updateBuffers( - m_pinConnector.in().channelCount(), - m_pinConnector.out().channelCount() - ); - } - - PluginPinConnector m_pinConnector; + AudioPortT m_audioPort; }; @@ -392,37 +361,51 @@ class AudioPlugin * interfaces with LMMS Core. * * This design allows for some compile-time customization over aspects of the plugin implementation - * such as the number of in/out channels and the audio data layout, so plugin developers can - * implement their plugin in whatever way works best for them. All the mapping from their plugin to/from + * such as the number of in/out channels and whether samples are interleaved, so plugin developers can + * implement their plugin in whatever way works best for them. All the mapping of their plugin to/from * LMMS Core is handled here, at compile-time where possible for best performance. * * A `processImpl` interface method is provided which must be implemented by the plugin implementation. * * @param ParentT Either `Instrument` or `Effect` - * @param SampleT The plugin's sample type - i.e. float, double, int32_t, `SampleFrame`, etc. * @param config Compile time configuration to customize `AudioPlugin` + * @param AudioPortT The plugin's audio port - must fully implement `PluginAudioPort` */ -template +template> class AudioPlugin - : public detail::AudioPlugin + : public detail::AudioPlugin { - static_assert(!std::is_const_v); - static_assert(!std::is_same_v + static_assert(config.kind != AudioDataKind::SampleFrame || ((config.inputs == 0 || config.inputs == 2) && (config.outputs == 0 || config.outputs == 2)), "Don't use SampleFrame if more than 2 processor channels are needed"); - using Base = detail::AudioPlugin; + static_assert(std::is_base_of_v, + "AudioPortT must be `PluginAudioPort` or inherit from it"); + + using Base = typename detail::AudioPlugin; public: - using Base::AudioPlugin; + //! The last parameter(s) are variadic template parameters passed to the audio port constructor + using Base::Base; + + static constexpr auto pluginConfig() -> AudioPluginConfig { return config; } }; -// NOTE: NotePlayHandle-based instruments are not supported yet -using DefaultMidiInstrument = AudioPlugin; -using DefaultEffect = AudioPlugin; +// NOTE: NotePlayHandle-based instruments are not supported yet +using DefaultMidiInstrument = AudioPlugin; + +using DefaultEffect = AudioPlugin; } // namespace lmms diff --git a/include/AudioPluginBuffer.h b/include/AudioPluginBuffer.h index bb511d52f1b..394b9b46e5b 100644 --- a/include/AudioPluginBuffer.h +++ b/include/AudioPluginBuffer.h @@ -1,7 +1,7 @@ /* * AudioPluginBuffer.h - Customizable working buffer for plugins * - * Copyright (c) 2024 Dalton Messmer + * Copyright (c) 2025 Dalton Messmer * * This file is part of LMMS - https://lmms.io * @@ -25,136 +25,141 @@ #ifndef LMMS_AUDIO_PLUGIN_BUFFER_H #define LMMS_AUDIO_PLUGIN_BUFFER_H -#include #include #include #include "AudioData.h" -#include "AudioEngine.h" -#include "Engine.h" +#include "AudioPluginConfig.h" #include "SampleFrame.h" #include "lmms_basics.h" namespace lmms { -namespace detail -{ + +namespace detail { /** * Metafunction to select the appropriate non-owning audio buffer view * given the layout, sample type, and channel count */ -template -struct AudioDataTypeSelector +template +struct AudioDataViewSelector { - static_assert(always_false_v>, + static_assert(always_false_v>, "Unsupported audio data type"); }; -template -struct AudioDataTypeSelector +//! Non-interleaved specialization +template +struct AudioDataViewSelector { - using type = SplitAudioData; + using type = SplitAudioData< + std::conditional_t, GetAudioDataType>, + channels>; }; -template -struct AudioDataTypeSelector +//! SampleFrame specialization +template +struct AudioDataViewSelector { - static_assert(channelCount == 0 || channelCount == 2, + static_assert(channels == 0 || channels == 2, "Plugins using SampleFrame buffers must have exactly 0 or 2 inputs or outputs"); - using type = CoreAudioDataMut; + using type = std::conditional_t; }; -template -struct AudioDataTypeSelector + +//! Provides a view into a plugin's input and output audio buffers +template +class AudioPluginBufferInterface; + +//! Non-inplace specialization +template +class AudioPluginBufferInterface { - static_assert(channelCount == 0 || channelCount == 2, - "Plugins using SampleFrame buffers must have exactly 0 or 2 inputs or outputs"); - using type = CoreAudioData; -}; +public: + virtual ~AudioPluginBufferInterface() = default; + virtual auto inputBuffer() + -> typename detail::AudioDataViewSelector::type = 0; -} // namespace detail + virtual auto outputBuffer() + -> typename detail::AudioDataViewSelector::type = 0; + virtual auto frames() const -> fpp_t = 0; -//! Provides a view into a plugin's input and output audio buffers -template -class AudioPluginBufferInterface + virtual void updateBuffers(int channelsIn, int channelsOut, f_cnt_t frames) = 0; +}; + +//! Inplace specialization +template +class AudioPluginBufferInterface { public: virtual ~AudioPluginBufferInterface() = default; - virtual auto inputBuffer() -> typename detail::AudioDataTypeSelector::type = 0; - virtual auto outputBuffer() -> typename detail::AudioDataTypeSelector::type = 0; - virtual void updateBuffers(int channelsIn, int channelsOut) = 0; -}; + virtual auto inputOutputBuffer() + -> typename detail::AudioDataViewSelector::type = 0; -//! This lets plugin implementations provide their own buffers -template -class AudioPluginBufferInterfaceProvider -{ -protected: - virtual auto bufferInterface() - -> AudioPluginBufferInterface* = 0; + virtual auto frames() const -> fpp_t = 0; + + virtual void updateBuffers(int channelsIn, int channelsOut, f_cnt_t frames) = 0; }; //! Default implementation of `AudioPluginBufferInterface` -template +template class AudioPluginBufferDefaultImpl; -//! Specialization for split (non-interleaved) buffers -template -class AudioPluginBufferDefaultImpl - : public AudioPluginBufferInterface - , public AudioPluginBufferInterfaceProvider +//! Specialization for non-inplace, non-interleaved buffers +template +class AudioPluginBufferDefaultImpl + : public AudioPluginBufferInterface { static constexpr bool s_hasStaticChannelCount - = numChannelsIn != DynamicChannelCount && numChannelsOut != DynamicChannelCount; + = config.inputs != DynamicChannelCount && config.outputs != DynamicChannelCount; + + using SampleT = GetAudioDataType; // Optimization to avoid need for std::vector if size is known at compile time using AccessBufferType = std::conditional_t< s_hasStaticChannelCount, - std::array*, static_cast(numChannelsIn + numChannelsOut)>, - std::vector*>>; + std::array(config.inputs + config.outputs)>, + std::vector>; public: - static_assert(numChannelsIn == numChannelsOut || !inplace, - "compile-time inplace buffers must have same number of input channels and output channels"); - - AudioPluginBufferDefaultImpl() - : m_frames{Engine::audioEngine()->framesPerPeriod()} - { - updateBuffers(numChannelsIn, numChannelsOut); - } - + AudioPluginBufferDefaultImpl() = default; ~AudioPluginBufferDefaultImpl() override = default; - auto inputBuffer() -> SplitAudioData final + auto inputBuffer() -> SplitAudioData final { - return SplitAudioData { + return SplitAudioData { m_accessBuffer.data(), static_cast(m_channelsIn), m_frames }; } - auto outputBuffer() -> SplitAudioData final + auto outputBuffer() -> SplitAudioData final { - return SplitAudioData { + return SplitAudioData { m_accessBuffer.data() + static_cast(m_channelsIn), static_cast(m_channelsOut), m_frames }; } - void updateBuffers(int channelsIn, int channelsOut) final + auto frames() const -> fpp_t final + { + return m_frames; + } + + void updateBuffers(int channelsIn, int channelsOut, f_cnt_t frames) final { if (channelsIn == DynamicChannelCount || channelsOut == DynamicChannelCount) { return; } - m_frames = Engine::audioEngine()->framesPerPeriod(); const auto channels = static_cast(channelsIn + channelsOut); - m_sourceBuffer.resize(channels * m_frames); + m_sourceBuffer.resize(channels * frames); if constexpr (!s_hasStaticChannelCount) { m_accessBuffer.resize(channels); @@ -162,84 +167,157 @@ class AudioPluginBufferDefaultImpl AudioPluginBufferDefaultImpl* final - { - return this; - } - private: //! All input buffers followed by all output buffers - std::vector> m_sourceBuffer; + std::vector m_sourceBuffer; //! Provides [channel][frame] view into `m_sourceBuffer` AccessBufferType m_accessBuffer; - int m_channelsIn = numChannelsIn; - int m_channelsOut = numChannelsOut; + int m_channelsIn = config.inputs; + int m_channelsOut = config.outputs; f_cnt_t m_frames = 0; }; -//! Specialization for 2-channel SampleFrame buffers -template -class AudioPluginBufferDefaultImpl - : public AudioPluginBufferInterface - , public AudioPluginBufferInterfaceProvider + +//! Specialization for inplace, non-interleaved buffers +template +class AudioPluginBufferDefaultImpl + : public AudioPluginBufferInterface { + static constexpr bool s_hasStaticChannelCount + = config.inputs != DynamicChannelCount && config.outputs != DynamicChannelCount; + + static_assert(config.inputs == config.outputs || config.inputs == 0 || config.outputs == 0, + "compile-time inplace buffers must have same number of input channels and output channels, " + "or one of the channel counts must be fixed at zero"); + + using SampleT = GetAudioDataType; + + // Optimization to avoid need for std::vector if size is known at compile time + using AccessBufferType = std::conditional_t< + s_hasStaticChannelCount, + std::array(config.outputs)>, + std::vector>; + public: - static_assert(inplace, "SampleFrame buffers are always processed in-place"); + AudioPluginBufferDefaultImpl() = default; + ~AudioPluginBufferDefaultImpl() override = default; - AudioPluginBufferDefaultImpl() + auto inputOutputBuffer() -> SplitAudioData final { - updateBuffers(numChannelsIn, numChannelsOut); + return SplitAudioData { + m_accessBuffer.data(), static_cast(m_channels), m_frames + }; } - ~AudioPluginBufferDefaultImpl() override = default; + auto frames() const -> fpp_t final + { + return m_frames; + } - auto inputBuffer() -> CoreAudioDataMut final + void updateBuffers(int channelsIn, int channelsOut, f_cnt_t frames) final { - return CoreAudioDataMut{m_buffer.data(), m_buffer.size()}; + assert(channelsIn == channelsOut || channelsIn == 0 || channelsOut == 0); + if (channelsIn == DynamicChannelCount || channelsOut == DynamicChannelCount) { return; } + + const auto channels = std::max(channelsIn, channelsOut); + + m_sourceBuffer.resize(channels * frames); + if constexpr (!s_hasStaticChannelCount) + { + m_accessBuffer.resize(channels); + } + else + { + // If channel counts are known at compile time, they should never change + assert(channelsIn == config.inputs); + assert(channelsOut == config.outputs); + } + + m_frames = frames; + + SampleT* ptr = m_sourceBuffer.data(); + for (std::size_t channel = 0; channel < channels; ++channel) + { + m_accessBuffer[channel] = ptr; + ptr += frames; + } + + m_channels = channelsOut; } - auto outputBuffer() -> CoreAudioDataMut final +private: + //! All input buffers followed by all output buffers + std::vector m_sourceBuffer; + + //! Provides [channel][frame] view into `m_sourceBuffer` + AccessBufferType m_accessBuffer; + + int m_channels = config.outputs; + f_cnt_t m_frames = 0; +}; + + +//! Specialization for 2-channel SampleFrame buffers +template +class AudioPluginBufferDefaultImpl + : public AudioPluginBufferInterface +{ +public: + AudioPluginBufferDefaultImpl() = default; + ~AudioPluginBufferDefaultImpl() override = default; + + auto inputOutputBuffer() -> CoreAudioDataMut final { return CoreAudioDataMut{m_buffer.data(), m_buffer.size()}; } - void updateBuffers(int channelsIn, int channelsOut) final + auto frames() const -> fpp_t final { - (void)channelsIn; - (void)channelsOut; - m_buffer.resize(Engine::audioEngine()->framesPerPeriod()); + return m_buffer.size(); } -protected: - auto bufferInterface() -> AudioPluginBufferDefaultImpl* final + void updateBuffers(int channelsIn, int channelsOut, f_cnt_t frames) final { - return this; + (void)channelsIn; + (void)channelsOut; + m_buffer.resize(frames); } private: std::vector m_buffer; }; +} // namespace detail + + +//! Provides a view into a plugin's input and output audio buffers +template +using AudioPluginBufferInterface = detail::AudioPluginBufferInterface; + + +//! Default implementation of `AudioPluginBufferInterface` +template +using DefaultAudioPluginBuffer = detail::AudioPluginBufferDefaultImpl; + } // namespace lmms diff --git a/include/AudioPluginConfig.h b/include/AudioPluginConfig.h new file mode 100644 index 00000000000..8a7e4de4c0e --- /dev/null +++ b/include/AudioPluginConfig.h @@ -0,0 +1,53 @@ +/* + * AudioPluginConfig.h - Audio plugin configuration + * + * Copyright (c) 2025 Dalton Messmer + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_AUDIO_PLUGIN_CONFIG_H +#define LMMS_AUDIO_PLUGIN_CONFIG_H + +#include "AudioData.h" + +namespace lmms { + +//! Compile time customizations for an audio plugin +struct AudioPluginConfig +{ + //! The audio data type used by the plugin + AudioDataKind kind; + + //! The audio data layout used by the plugin: interleaved or non-interleaved + bool interleaved; + + //! The number of plugin input channels, or `DynamicChannelCount` if unknown at compile time + int inputs = DynamicChannelCount; + + //! The number of plugin output channels, or `DynamicChannelCount` if unknown at compile time + int outputs = DynamicChannelCount; + + //! In-place processing - true (always in-place) or false (dynamic, customizable in audio buffer impl) + bool inplace = false; +}; + +} // namespace lmms + +#endif // LMMS_AUDIO_PLUGIN_CONFIG_H diff --git a/include/PluginAudioPort.h b/include/PluginAudioPort.h new file mode 100644 index 00000000000..d28fbfbbb54 --- /dev/null +++ b/include/PluginAudioPort.h @@ -0,0 +1,169 @@ +/* + * PluginAudioPort.h + * + * Copyright (c) 2025 Dalton Messmer + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_PLUGIN_AUDIO_PORT_H +#define LMMS_PLUGIN_AUDIO_PORT_H + +#include "AudioEngine.h" +#include "AudioPluginBuffer.h" +#include "AudioPluginConfig.h" +#include "Engine.h" +#include "PluginPinConnector.h" + +namespace lmms +{ + + +namespace detail { +struct PluginAudioPortTag {}; +} // namespace detail + + +/** + * Interface for an audio port implementation. + * Contains a pin connector and provides access to the audio buffers. + */ +template +class PluginAudioPort + : public PluginPinConnector + , public detail::PluginAudioPortTag +{ +public: + PluginAudioPort(bool isInstrument, Model* parent) + : PluginPinConnector{config.inputs, config.outputs, isInstrument, parent} + { + } + + /** + * Must be called after constructing an audio port. + * + * NOTE: This cannot be called in the constructor due + * to the use of a virtual method. + */ + void init() + { + if (auto buffers = this->buffers()) + { + buffers->updateBuffers(in().channelCount(), out().channelCount(), + Engine::audioEngine()->framesPerPeriod()); + } + } + + auto pinConnector() const -> const PluginPinConnector& + { + return *static_cast(this); + } + + auto pinConnector() -> PluginPinConnector& + { + return *static_cast(this); + } + + //! Returns the pin connector's router + auto getRouter() const -> PluginPinConnector::Router + { + return static_cast(this)->getRouter(); + } + + //! Returns nullptr if the port is unavailable (i.e. Vestige with no plugin loaded) + virtual auto buffers() -> AudioPluginBufferInterface* = 0; + + /** + * Returns false if the plugin is not loaded. + * Custom audio ports with a "plugin not loaded" state should override this. + */ + virtual auto active() const -> bool { return true; } + + /** + * `AudioPlugin` calls this to decide whether to pass the audio buffers to + * the `processImpl` methods. + * + * Sending the audio buffers to `processImpl` in the plugin implementation may be + * pointless for custom audio port implementations that manage their own buffers, + * so in that case reimplementing this method in a child class to return `false` + * results in a cleaner interface. + */ + constexpr static auto provideProcessBuffers() -> bool { return true; } + + static constexpr auto pluginConfig() -> AudioPluginConfig { return config; } +}; + + +/** + * The default audio port for plugins that do not provide their own. + * Contains a pin connector and audio buffers. + * + * This audio port still has *some* ability for customization by using a custom `BufferT`, + * but for full control, you'll need to provide your own audio port implementation. + */ +template class BufferT> +class PluginAudioPortDefaultImpl + : public PluginAudioPort + , public BufferT +{ + static_assert(std::is_base_of_v, BufferT>, + "BufferT must derive from AudioPluginBufferInterface"); + +public: + using PluginAudioPort::PluginAudioPort; + + auto buffers() -> BufferT* final { return static_cast*>(this); } + +private: + void bufferPropertiesChanged(int inChannels, int outChannels, f_cnt_t frames) final + { + // Connects the pin connector to the buffers + this->updateBuffers(inChannels, outChannels, frames); + } +}; + + +//! Default audio port +template +using DefaultPluginAudioPort = PluginAudioPortDefaultImpl; + + +//! Custom audio port - audio buffer interface to be implementated in child class +template +class CustomPluginAudioPort + : public PluginAudioPort + , public AudioPluginBufferInterface +{ +public: + using PluginAudioPort::PluginAudioPort; + + constexpr static auto provideProcessBuffers() -> bool { return false; } + +private: + void bufferPropertiesChanged(int inChannels, int outChannels, f_cnt_t frames) final + { + // Connects the pin connector to the buffers + this->updateBuffers(inChannels, outChannels, frames); + } +}; + + +} // namespace lmms + +#endif // LMMS_PLUGIN_AUDIO_PORT_H diff --git a/include/PluginPinConnector.h b/include/PluginPinConnector.h index 52043d057a8..fe23a79060d 100644 --- a/include/PluginPinConnector.h +++ b/include/PluginPinConnector.h @@ -2,7 +2,7 @@ * PluginPinConnector.h - Specifies how to route audio channels * in and out of a plugin. * - * Copyright (c) 2024 Dalton Messmer + * Copyright (c) 2025 Dalton Messmer * * This file is part of LMMS - https://lmms.io * @@ -34,6 +34,7 @@ #include #include "AudioData.h" +#include "AudioPluginConfig.h" #include "AutomatableModel.h" #include "SampleFrame.h" #include "SerializingObject.h" @@ -198,46 +199,48 @@ class LMMS_EXPORT PluginPinConnector * `inOut` : track channels from/to LMMS core * `inOut.frames` provides the number of frames in each `in`/`inOut` audio buffer */ - template - class Router; + template + class Router + { + static_assert(always_false_v>, + "A router for the requested configuration is not implemented yet"); + }; //! Non-`SampleFrame` routing - template - class Router + template + class Router { - public: - static_assert(layout == AudioDataLayout::Split, "Only split data is implemented so far"); + using SampleT = GetAudioDataType; - Router(PluginPinConnector& parent) : m_pc{&parent} {} + public: + explicit Router(const PluginPinConnector& parent) : m_pc{&parent} {} - void routeToPlugin(CoreAudioBus in, SplitAudioData out) const; - void routeFromPlugin(SplitAudioData in, CoreAudioBusMut inOut) const; + void routeToPlugin(CoreAudioBus in, SplitAudioData, config.inputs> out) const; + void routeFromPlugin(SplitAudioData, config.outputs> in, CoreAudioBusMut inOut) const; private: - PluginPinConnector* m_pc; + const PluginPinConnector* m_pc; }; //! `SampleFrame` routing - template - class Router + template + class Router { public: - static_assert(layout == AudioDataLayout::Interleaved); - - Router(PluginPinConnector& parent) : m_pc{&parent} {} + explicit Router(const PluginPinConnector& parent) : m_pc{&parent} {} void routeToPlugin(CoreAudioBus in, CoreAudioDataMut out) const; void routeFromPlugin(CoreAudioData in, CoreAudioBusMut inOut) const; private: - PluginPinConnector* m_pc; + const PluginPinConnector* m_pc; }; - template - auto getRouter() -> Router + template + auto getRouter() const -> Router { - return Router{*this}; + return Router{*this}; } @@ -248,7 +251,8 @@ class LMMS_EXPORT PluginPinConnector void loadSettings(const QDomElement& elem) override; auto nodeName() const -> QString override { return "pins"; } - auto instantiateView(QWidget* parent) const -> gui::PluginPinConnectorView*; + virtual auto instantiateView(QWidget* parent) const -> gui::PluginPinConnectorView*; + auto getChannelCountText() const -> QString; static constexpr std::size_t MaxTrackChannels = 256; // TODO: Move somewhere else @@ -261,14 +265,21 @@ class LMMS_EXPORT PluginPinConnector //! Called when the plugin channel counts change or the track channel counts change //void propertiesChanged(); [from Model base class] - //! Called when the plugin channel counts change or if the sample rate changes - void pluginBuffersChanged(); - public slots: void setTrackChannelCount(int count); void updateRoutedChannels(unsigned int trackChannel); +protected: + /** + * To be implemented by the plugin's audio port. + * Called when channel counts or sample rate changes. + * + * NOTE: Virtual method, do not call in constructor. + */ + virtual void bufferPropertiesChanged(int inChannels, int outChannels, f_cnt_t frames) {} + private: + void setPluginChannelCountsImpl(int inCount, int outCount); void updateAllRoutedChannels(); Matrix m_in{false}; //!< LMMS --> Plugin @@ -300,11 +311,11 @@ public slots: // Non-`SampleFrame` Router out-of-class definitions -template -inline void PluginPinConnector::Router::routeToPlugin( - CoreAudioBus in, SplitAudioData out) const +template +inline void PluginPinConnector::Router::routeToPlugin( + CoreAudioBus in, SplitAudioData out) const { - if constexpr (channelCountIn == 0) { return; } + if constexpr (config.inputs == 0) { return; } assert(m_pc->m_in.channelCount() != DynamicChannelCount); if (m_pc->m_in.channelCount() == 0) { return; } @@ -312,15 +323,18 @@ inline void PluginPinConnector::Routerm_trackChannelsUpperBound / 2; assert(inSizeConstrained <= in.channelPairs); - assert(out.sourceBuffer() != nullptr); // Zero the output buffer - TODO: std::memcpy? - std::fill_n(out.sourceBuffer(), out.channels() * out.frames(), SampleT{}); - //std::memset(out.sourceBuffer(), 0, out.channels() * out.frames() * sizeof(SampleT)); + { + auto source = out.sourceBuffer(); + assert(source.data() != nullptr); + std::fill_n(source.data(), source.size(), SampleT{}); + //std::memset(source.data(), 0, source.size_bytes()); + } for (std::uint32_t outChannel = 0; outChannel < out.channels(); ++outChannel) { - SampleType* outPtr = out.buffer(outChannel); + SampleT* outPtr = out.buffer(outChannel); for (std::uint8_t inChannelPairIdx = 0; inChannelPairIdx < inSizeConstrained; ++inChannelPairIdx) { @@ -366,11 +380,11 @@ inline void PluginPinConnector::Router -inline void PluginPinConnector::Router::routeFromPlugin( - SplitAudioData in, CoreAudioBusMut inOut) const +template +inline void PluginPinConnector::Router::routeFromPlugin( + SplitAudioData in, CoreAudioBusMut inOut) const { - if constexpr (channelCountOut == 0) { return; } + if constexpr (config.outputs == 0) { return; } assert(m_pc->m_out.channelCount() != DynamicChannelCount); if (m_pc->m_out.channelCount() == 0) { return; } @@ -378,7 +392,6 @@ inline void PluginPinConnector::Routerm_trackChannelsUpperBound / 2; assert(inOutSizeConstrained <= inOut.channelPairs); - assert(in.sourceBuffer() != nullptr); /* * Routes plugin audio to track channel pair and normalizes the result. For track channels @@ -420,7 +433,7 @@ inline void PluginPinConnector::Router* inPtr = in.buffer(inChannel); + const SampleT* inPtr = in.buffer(inChannel); if constexpr (rc == 0b11) { @@ -507,11 +520,12 @@ inline void PluginPinConnector::Router -inline void PluginPinConnector::Router::routeToPlugin( +template +inline void PluginPinConnector::Router::routeToPlugin( CoreAudioBus in, CoreAudioDataMut out) const { - if constexpr (channelCountIn == 0) { return; } + if constexpr (config.inputs == 0) { return; } assert(m_pc->m_in.channelCount() != DynamicChannelCount); if (m_pc->m_in.channelCount() == 0) { return; } @@ -598,11 +612,12 @@ inline void PluginPinConnector::Router -inline void PluginPinConnector::Router::routeFromPlugin( +template +inline void PluginPinConnector::Router::routeFromPlugin( CoreAudioData in, CoreAudioBusMut inOut) const { - if constexpr (channelCountOut == 0) { return; } + if constexpr (config.outputs == 0) { return; } assert(m_pc->m_out.channelCount() != DynamicChannelCount); if (m_pc->m_out.channelCount() == 0) { return; } diff --git a/include/PluginPinConnectorView.h b/include/PluginPinConnectorView.h index ef284f724f1..f1b3abe5c9a 100644 --- a/include/PluginPinConnectorView.h +++ b/include/PluginPinConnectorView.h @@ -1,7 +1,7 @@ /* * PluginPinConnectorView.h - Displays pin connectors * - * Copyright (c) 2024 Dalton Messmer + * Copyright (c) 2025 Dalton Messmer * * This file is part of LMMS - https://lmms.io * diff --git a/include/RemotePlugin.h b/include/RemotePlugin.h index dc45bb0972b..7b77d1dee42 100644 --- a/include/RemotePlugin.h +++ b/include/RemotePlugin.h @@ -27,8 +27,6 @@ #include "AudioData.h" #include "RemotePluginBase.h" - -#include "PluginPinConnector.h" #include "SharedMemory.h" #include "lmms_basics.h" @@ -39,8 +37,8 @@ namespace lmms { - class RemotePlugin; +class RemotePluginAudioPortController; class SampleFrame; class ProcessWatcher : public QThread @@ -74,7 +72,7 @@ class LMMS_EXPORT RemotePlugin : public QObject, public RemotePluginBase { Q_OBJECT public: - explicit RemotePlugin(PluginPinConnector* pinConnector, Model* parent = nullptr); + explicit RemotePlugin(RemotePluginAudioPortController& audioPort); ~RemotePlugin() override; inline bool isRunning() @@ -103,7 +101,7 @@ class LMMS_EXPORT RemotePlugin : public QObject, public RemotePluginBase bool process(); - void updateBuffer(int channelsIn, int channelsOut); + void updateBuffer(int channelsIn, int channelsOut, fpp_t frames); void processMidiEvent( const MidiEvent&, const f_cnt_t _offset ); @@ -147,6 +145,14 @@ class LMMS_EXPORT RemotePlugin : public QObject, public RemotePluginBase m_commMutex.unlock(); } + auto audioPort() -> RemotePluginAudioPortController* + { + return m_audioPort; + } + + auto inputBuffer() const -> Span { return m_inputBuffer; } + auto outputBuffer() const -> Span { return m_outputBuffer; } + public slots: virtual void showUI(); virtual void hideUI(); @@ -154,18 +160,6 @@ public slots: protected: bool m_failed; - //! Signal to derived classes - virtual void bufferUpdated() {} - - auto frames() const -> f_cnt_t { return m_frames; } - auto channelsIn() const -> pi_ch_t { return m_channelsIn; } - auto channelsOut() const -> pi_ch_t { return m_channelsOut; } - - auto inputBuffer() const -> Span { return m_inputBuffer; } - auto outputBuffer() const -> Span { return m_outputBuffer; } - - PluginPinConnector* const m_pinConnector = nullptr; - private: QProcess m_process; ProcessWatcher m_watcher; @@ -179,6 +173,8 @@ public slots: QMutex m_commMutex; #endif + RemotePluginAudioPortController* const m_audioPort = nullptr; + SharedMemory m_audioBuffer; // NOLINT std::size_t m_audioBufferSize = 0; // TODO: Move to `SharedMemory`? diff --git a/include/RemotePluginAudioPort.h b/include/RemotePluginAudioPort.h new file mode 100644 index 00000000000..6eb7aaad3c7 --- /dev/null +++ b/include/RemotePluginAudioPort.h @@ -0,0 +1,288 @@ +/* + * RemotePluginAudioPort.h - PluginAudioPort implementation for RemotePlugin + * + * Copyright (c) 2025 Dalton Messmer + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_REMOTE_PLUGIN_AUDIO_PORT_H +#define LMMS_REMOTE_PLUGIN_AUDIO_PORT_H + +#include "PluginAudioPort.h" +#include "lmms_basics.h" +#include "lmms_export.h" + +namespace lmms +{ + +class RemotePlugin; + +/* + * TODO: A better design would just have `RemotePluginAudioPort` passed to + * `RemotePlugin` from the plugin implementation rather than + * `RemotePluginAudioPortController`, and `RemotePlugin` would be a class + * template with an `AudioPluginConfig` template parameter. + * There would also be `RemotePluginAudioBuffer`, a custom buffer implementation + * for remote plugins and remote plugin clients which `RemotePlugin` would + * derive from. However, this design requires C++20's class type NTTP on the + * remote plugin's client side but we're compiling that with C++17 due to a + * weird regression. + */ + +class LMMS_EXPORT RemotePluginAudioPortController +{ +public: + RemotePluginAudioPortController(PluginPinConnector& pinConnector); + + //! Connects RemotePlugin's buffers to audio port; Call after buffers are created + void connectBuffers(RemotePlugin* buffers) + { + m_buffers = buffers; + } + + //! Disconnects RemotePlugin's buffers from audio port; Call before buffers are destroyed + void disconnectBuffers() + { + m_buffers = nullptr; + } + + //! Call after the RemotePlugin is fully initialized + virtual void activate(f_cnt_t frames) = 0; + + auto pc() -> PluginPinConnector& + { + return *m_pinConnector; + } + +protected: + void remotePluginUpdateBuffers(int channelsIn, int channelsOut, fpp_t frames); + auto remotePluginInputBuffer() const -> float*; + auto remotePluginOutputBuffer() const -> float*; + + RemotePlugin* m_buffers = nullptr; + PluginPinConnector* m_pinConnector = nullptr; + + fpp_t m_frames = 0; +}; + + +//! `PluginAudioPort` implementation for `RemotePlugin` +template +class RemotePluginAudioPort + : public CustomPluginAudioPort + , public RemotePluginAudioPortController +{ + using SampleT = GetAudioDataType; + +public: + RemotePluginAudioPort(bool isInstrument, Model* parent) + : CustomPluginAudioPort{isInstrument, parent} + , RemotePluginAudioPortController{*static_cast(this)} + { + } + + static_assert(config.kind == AudioDataKind::F32, "RemotePlugin only supports float"); + static_assert(config.interleaved == false, "RemotePlugin only supports non-interleaved"); + static_assert(!config.inplace, "RemotePlugin does not support inplace processing"); + + auto controller() -> RemotePluginAudioPortController& + { + return *static_cast(this); + } + + void activate(f_cnt_t frames) override + { + assert(m_buffers != nullptr); + updateBuffers(this->in().channelCount(), this->out().channelCount(), frames); + m_remoteActive = true; + } + + /* + * `PluginAudioPort` implementation + */ + + //! Only returns the buffer interface if audio port is active + auto buffers() -> AudioPluginBufferInterface* override + { + return remoteActive() ? this : nullptr; + } + + /* + * `AudioPluginBufferInterface` implementation + */ + + auto inputBuffer() -> SplitAudioData override + { + if (!remoteActive()) { return SplitAudioData{}; } + + return SplitAudioData { + m_audioBufferIn.data(), + static_cast(this->in().channelCount()), + m_frames + }; + } + + auto outputBuffer() -> SplitAudioData override + { + if (!remoteActive()) { return SplitAudioData{}; } + + return SplitAudioData { + m_audioBufferOut.data(), + static_cast(this->out().channelCount()), + m_frames + }; + } + + auto frames() const -> fpp_t override + { + return remoteActive() ? m_frames : 0; + } + + void updateBuffers(int channelsIn, int channelsOut, f_cnt_t frames) override + { + if (!m_buffers) { return; } + remotePluginUpdateBuffers(channelsIn, channelsOut, frames); + + m_frames = frames; + + // Update the views into the RemotePlugin buffer + float* ptr = remotePluginInputBuffer(); + m_audioBufferIn.resize(channelsIn); + for (pi_ch_t idx = 0; idx < channelsIn; ++idx) + { + m_audioBufferIn[idx] = ptr; + ptr += frames; + } + + ptr = remotePluginOutputBuffer(); + m_audioBufferOut.resize(channelsOut); + for (pi_ch_t idx = 0; idx < channelsOut; ++idx) + { + m_audioBufferOut[idx] = ptr; + ptr += frames; + } + } + + auto active() const -> bool override { return remoteActive(); } + +private: + auto remoteActive() const -> bool { return m_buffers != nullptr && m_remoteActive; } + + // Views into RemotePlugin's shared memory buffer + std::vector m_audioBufferIn; + std::vector m_audioBufferOut; + +protected: + bool m_remoteActive = false; +}; + + +//! An audio port that can choose between RemotePlugin or a local buffer at runtime +template> +class ConfigurableAudioPort + : public RemotePluginAudioPort +{ + using SampleT = GetAudioDataType; + +public: + ConfigurableAudioPort(bool isInstrument, Model* parent, bool beginAsRemote = true) + : RemotePluginAudioPort{isInstrument, parent} + { + useRemote(beginAsRemote); + } + + void useRemote(bool remote = true) + { + if (remote) { m_localActive = false; } + else { RemotePluginAudioPort::m_remoteActive = false; } + + m_isRemote = remote; + } + + auto isRemote() const -> bool { return m_isRemote; } + + void activate(f_cnt_t frames) override + { + if (isRemote()) { RemotePluginAudioPort::activate(frames); return; } + + updateBuffers(this->in().channelCount(), this->out().channelCount(), frames); + m_localActive = true; + } + + auto buffers() -> AudioPluginBufferInterface* override + { + if (isRemote()) { return RemotePluginAudioPort::buffers(); } + return localActive() ? &m_localBuffer.value() : nullptr; + } + + auto inputBuffer() -> SplitAudioData override + { + if (isRemote()) { return RemotePluginAudioPort::inputBuffer(); } + return localActive() + ? m_localBuffer->inputBuffer() + : SplitAudioData{}; + } + + auto outputBuffer() -> SplitAudioData override + { + if (isRemote()) { return RemotePluginAudioPort::outputBuffer(); } + return localActive() + ? m_localBuffer->outputBuffer() + : SplitAudioData{}; + } + + auto frames() const -> fpp_t override + { + if (isRemote()) { return RemotePluginAudioPort::frames(); } + return localActive() ? m_localBuffer->frames() : 0; + } + + void updateBuffers(int channelsIn, int channelsOut, f_cnt_t frames) override + { + if (isRemote()) + { + RemotePluginAudioPort::updateBuffers(channelsIn, channelsOut, frames); + } + else + { + if (!m_localBuffer) { m_localBuffer.emplace(); } + m_localBuffer->updateBuffers(channelsIn, channelsOut, frames); + } + } + + auto active() const -> bool override + { + return isRemote() + ? RemotePluginAudioPort::active() + : localActive(); + } + +private: + auto localActive() const -> bool { return m_localBuffer.has_value() && m_localActive; } + + std::optional m_localBuffer; + bool m_localActive = false; + bool m_isRemote = true; +}; + + +} // namespace lmms + +#endif // LMMS_REMOTE_PLUGIN_AUDIO_PORT_H diff --git a/include/RemotePluginBase.h b/include/RemotePluginBase.h index bbf14207ac7..7dcdff0a522 100644 --- a/include/RemotePluginBase.h +++ b/include/RemotePluginBase.h @@ -350,8 +350,6 @@ enum RemoteMessageIDs IdStartProcessing, IdProcessingDone, IdChangeSharedMemoryKey, - IdChangeInputCount, - IdChangeOutputCount, IdChangeInputOutputCount, IdShowUI, IdHideUI, diff --git a/include/RemotePluginClient.h b/include/RemotePluginClient.h index 40bbcd2247f..1ddc8f64716 100644 --- a/include/RemotePluginClient.h +++ b/include/RemotePluginClient.h @@ -84,18 +84,6 @@ class RemotePluginClient : public RemotePluginBase return m_bufferSize; } - void setInputCount( int _i ) - { - m_inputCount = _i; - sendMessage( message( IdChangeInputCount ).addInt( _i ) ); - } - - void setOutputCount( int _i ) - { - m_outputCount = _i; - sendMessage( message( IdChangeOutputCount ).addInt( _i ) ); - } - void setInputOutputCount( int i, int o ) { m_inputCount = i; diff --git a/include/SampleFrame.h b/include/SampleFrame.h index 6debd65e105..d99b89da145 100644 --- a/include/SampleFrame.h +++ b/include/SampleFrame.h @@ -53,12 +53,14 @@ class SampleFrame { } - InterleavedSampleType* data() + //! 2 channels, interleaved + sample_t* data() { return m_samples.data(); } - const InterleavedSampleType* data() const + //! 2 channels, interleaved + const sample_t* data() const { return m_samples.data(); } @@ -186,7 +188,7 @@ class SampleFrame } private: - std::array, DEFAULT_CHANNELS> m_samples; + std::array m_samples; }; inline void zeroSampleFrames(SampleFrame* buffer, size_t frames) @@ -209,7 +211,8 @@ inline SampleFrame getAbsPeakValues(SampleFrame* buffer, size_t frames) return peaks; } -inline void copyToSampleFrames(SampleFrame* target, const InterleavedSampleType* source, size_t frames) +//! `source` is 2-channel interleaved data with length of 2 * `frames` +inline void copyToSampleFrames(SampleFrame* target, const float* source, size_t frames) { for (size_t i = 0; i < frames; ++i) { @@ -218,7 +221,8 @@ inline void copyToSampleFrames(SampleFrame* target, const InterleavedSampleType< } } -inline void copyFromSampleFrames(InterleavedSampleType* target, const SampleFrame* source, size_t frames) +//! `target` is 2-channel interleaved data with length of 2 * `frames` +inline void copyFromSampleFrames(float* target, const SampleFrame* source, size_t frames) { for (size_t i = 0; i < frames; ++i) { @@ -227,8 +231,12 @@ inline void copyFromSampleFrames(InterleavedSampleType* target, const Sam } } +// Enable SampleFrame to use the AudioDataType metafunction +namespace detail { +template<> struct AudioDataType { using type = SampleFrame; }; +} // namespace detail -//! A non-owning `SampleFrame` buffer (interleaved, 2-channel) +//! A non-owning SampleFrame buffer (interleaved, 2-channel) using CoreAudioData = Span; //! Mutable CoreAudioData diff --git a/include/lmms_basics.h b/include/lmms_basics.h index 5cc0962f71d..e22e52bd5a8 100644 --- a/include/lmms_basics.h +++ b/include/lmms_basics.h @@ -122,6 +122,7 @@ class Span else { return extents; } } constexpr auto size_bytes() const -> std::size_t { return size() * sizeof(T); } // NOLINT + constexpr auto empty() const -> bool { return size() == 0; } constexpr auto operator[](std::size_t idx) const -> const T& { return m_data[idx]; } constexpr auto operator[](std::size_t idx) -> T& { return m_data[idx]; } diff --git a/plugins/Sid/SidInstrument.cpp b/plugins/Sid/SidInstrument.cpp index cccd834c5df..1bd39de566a 100644 --- a/plugins/Sid/SidInstrument.cpp +++ b/plugins/Sid/SidInstrument.cpp @@ -284,7 +284,7 @@ static int sid_fillbuffer(unsigned char* sidreg, reSID::SID *sid, int tdelta, sh - +// TODO: The real sample type is `short` and there is only one output channel void SidInstrument::playNoteImpl(NotePlayHandle* _n, CoreAudioDataMut out) { const int clockrate = C64_PAL_CYCLES_PER_SEC; diff --git a/plugins/Vestige/Vestige.cpp b/plugins/Vestige/Vestige.cpp index 8f804e5d82e..ee986b5d4a6 100644 --- a/plugins/Vestige/Vestige.cpp +++ b/plugins/Vestige/Vestige.cpp @@ -365,7 +365,7 @@ void VestigeInstrument::loadFile( const QString & _file ) } m_pluginMutex.lock(); - m_plugin = new VstInstrumentPlugin{m_pluginDLL, pinConnector(), this}; + m_plugin = new VstInstrumentPlugin{m_pluginDLL, audioPort().controller()}; if( m_plugin->failed() ) { m_pluginMutex.unlock(); @@ -479,15 +479,6 @@ void VestigeInstrument::closePlugin( void ) -auto VestigeInstrument::bufferInterface() -> AudioPluginBufferInterface* -{ - return m_plugin; -} - - - - gui::PluginView * VestigeInstrument::instantiateView( QWidget * _parent ) { return new gui::VestigeInstrumentView( this, _parent ); diff --git a/plugins/Vestige/Vestige.h b/plugins/Vestige/Vestige.h index 771841d0a6a..e756dd4c1bc 100644 --- a/plugins/Vestige/Vestige.h +++ b/plugins/Vestige/Vestige.h @@ -32,6 +32,7 @@ #include "AudioPlugin.h" #include "InstrumentView.h" +#include "RemotePluginAudioPort.h" class QPixmap; @@ -55,9 +56,13 @@ class VestigeInstrumentView; } // namespace gui +constexpr auto VestigeConfig = AudioPluginConfig { + .kind = AudioDataKind::F32, + .interleaved = false +}; + class VestigeInstrument - : public AudioPlugin + : public AudioPlugin> { Q_OBJECT public: @@ -85,9 +90,6 @@ protected slots: private: void closePlugin(); - auto bufferInterface() -> AudioPluginBufferInterface* override; - VstPlugin * m_plugin; QMutex m_pluginMutex; diff --git a/plugins/VstBase/VstPlugin.cpp b/plugins/VstBase/VstPlugin.cpp index 0add4859554..f1d4bf2a95c 100644 --- a/plugins/VstBase/VstPlugin.cpp +++ b/plugins/VstBase/VstPlugin.cpp @@ -53,7 +53,7 @@ #include "LocaleHelper.h" #include "MainWindow.h" #include "PathUtil.h" -#include "PluginPinConnector.h" +#include "RemotePluginAudioPort.h" #include "Song.h" #include "FileDialog.h" @@ -122,8 +122,8 @@ enum class ExecutableType Unknown, Win32, Win64, Linux64, }; -VstPlugin::VstPlugin(const QString& plugin, PluginPinConnector* pinConnector, Model* parent) - : RemotePlugin{pinConnector, parent} +VstPlugin::VstPlugin(const QString& plugin, RemotePluginAudioPortController& audioPort) + : RemotePlugin{audioPort} , m_plugin{PathUtil::toAbsolute(plugin)} , m_pluginWindowID{0} , m_embedMethod{(gui::getGUI() != nullptr) @@ -265,7 +265,7 @@ void VstPlugin::loadSettings( const QDomElement & _this ) setParameterDump( dump ); } - m_pinConnector->loadSettings(_this); + audioPort()->pc().loadSettings(_this); } @@ -309,7 +309,7 @@ void VstPlugin::saveSettings( QDomDocument & _doc, QDomElement & _this ) } _this.setAttribute( "program", currentProgram() ); - m_pinConnector->saveSettings(_doc, _this); + audioPort()->pc().saveSettings(_doc, _this); } void VstPlugin::toggleUI() @@ -817,40 +817,5 @@ QString VstPlugin::embedMethod() const return m_embedMethod; } -auto VstPlugin::inputBuffer() -> SplitAudioData -{ - return {m_audioBufferIn.data(), channelsIn(), frames()}; -} - -auto VstPlugin::outputBuffer() -> SplitAudioData -{ - return {m_audioBufferOut.data(), channelsOut(), frames()}; -} - -void VstPlugin::updateBuffers(int channelsIn, int channelsOut) -{ - RemotePlugin::updateBuffer(channelsIn, channelsOut); -} - -void VstPlugin::bufferUpdated() -{ - // Update the views into the RemotePlugin buffer - float* ptr = RemotePlugin::inputBuffer().data(); - m_audioBufferIn.resize(channelsIn()); - for (pi_ch_t idx = 0; idx < channelsIn(); ++idx) - { - m_audioBufferIn[idx] = ptr; - ptr += frames(); - } - - ptr = RemotePlugin::outputBuffer().data(); - m_audioBufferOut.resize(channelsOut()); - for (pi_ch_t idx = 0; idx < channelsOut(); ++idx) - { - m_audioBufferOut[idx] = ptr; - ptr += frames(); - } -} - } // namespace lmms diff --git a/plugins/VstBase/VstPlugin.h b/plugins/VstBase/VstPlugin.h index 9f551dac770..54b8b182c58 100644 --- a/plugins/VstBase/VstPlugin.h +++ b/plugins/VstBase/VstPlugin.h @@ -31,7 +31,6 @@ #include #include -#include "AudioPluginBuffer.h" #include "JournallingObject.h" #include "RemotePlugin.h" @@ -40,16 +39,15 @@ namespace lmms { -class PluginPinConnector; +class RemotePluginAudioPortController; class VSTBASE_EXPORT VstPlugin : public RemotePlugin , public JournallingObject - , public AudioPluginBufferInterface { Q_OBJECT public: - VstPlugin(const QString& plugin, PluginPinConnector* pinConnector, Model* parent = nullptr); + VstPlugin(const QString& plugin, RemotePluginAudioPortController& audioPort); ~VstPlugin() override; void tryLoad( const QString &remoteVstPluginExecutable ); @@ -128,12 +126,6 @@ class VSTBASE_EXPORT VstPlugin QString embedMethod() const; - auto inputBuffer() -> SplitAudioData override; - auto outputBuffer() -> SplitAudioData override; - void updateBuffers(int channelsIn, int channelsOut) override; - - void bufferUpdated() override; - public slots: void setTempo( lmms::bpm_t _bpm ); void updateSampleRate(); @@ -181,10 +173,6 @@ public slots: int m_currentProgram; QTimer m_idleTimer; - - // Views into RemotePlugin's shared memory buffer - std::vector*> m_audioBufferIn; - std::vector*> m_audioBufferOut; }; diff --git a/plugins/VstEffect/VstEffect.cpp b/plugins/VstEffect/VstEffect.cpp index dbfb9cd2343..e7bfa302dbb 100644 --- a/plugins/VstEffect/VstEffect.cpp +++ b/plugins/VstEffect/VstEffect.cpp @@ -98,7 +98,10 @@ auto VstEffect::processImpl() -> ProcessStatus // Wet/dry mixing only applies to those channels and any additional // channels remain as-is. - const auto in = m_plugin->inputBuffer(); + auto buffers = audioPort().buffers(); + assert(buffers != nullptr); + + const auto in = buffers->inputBuffer(); if (in.channels() == 0) { // Do not process wet/dry for an instrument loaded as an effect @@ -106,7 +109,7 @@ auto VstEffect::processImpl() -> ProcessStatus return ProcessStatus::ContinueIfNotQuiet; } - auto out = m_plugin->outputBuffer(); + auto out = buffers->outputBuffer(); const float w = wetLevel(); const float d = dryLevel(); @@ -128,15 +131,6 @@ auto VstEffect::processImpl() -> ProcessStatus -auto VstEffect::bufferInterface() -> AudioPluginBufferInterface* -{ - return m_plugin.get(); -} - - - - bool VstEffect::openPlugin(const QString& plugin) { gui::TextFloat* tf = nullptr; @@ -149,7 +143,7 @@ bool VstEffect::openPlugin(const QString& plugin) } QMutexLocker ml( &m_pluginMutex ); Q_UNUSED( ml ); - m_plugin = QSharedPointer(new VstPlugin{plugin, pinConnector(), this}); + m_plugin = QSharedPointer(new VstPlugin{plugin, audioPort().controller()}); if( m_plugin->failed() ) { m_plugin.clear(); diff --git a/plugins/VstEffect/VstEffect.h b/plugins/VstEffect/VstEffect.h index c81aba03769..60c2f78e35c 100644 --- a/plugins/VstEffect/VstEffect.h +++ b/plugins/VstEffect/VstEffect.h @@ -29,6 +29,7 @@ #include #include "AudioPlugin.h" +#include "RemotePluginAudioPort.h" #include "VstEffectControls.h" namespace lmms @@ -36,9 +37,12 @@ namespace lmms class VstPlugin; -class VstEffect - : public AudioPlugin +constexpr auto VstEffectConfig = AudioPluginConfig { + .kind = AudioDataKind::F32, + .interleaved = false +}; + +class VstEffect : public AudioPlugin> { public: VstEffect( Model * _parent, @@ -54,9 +58,6 @@ class VstEffect private: - auto bufferInterface() -> AudioPluginBufferInterface* override; - //! Returns true if plugin was loaded (m_plugin != nullptr) bool openPlugin(const QString& plugin); void closePlugin(); diff --git a/plugins/ZynAddSubFx/RemoteZynAddSubFx.cpp b/plugins/ZynAddSubFx/RemoteZynAddSubFx.cpp index 8b2d6710ada..db3ff21d223 100644 --- a/plugins/ZynAddSubFx/RemoteZynAddSubFx.cpp +++ b/plugins/ZynAddSubFx/RemoteZynAddSubFx.cpp @@ -58,7 +58,7 @@ class RemoteZynAddSubFx : public RemotePluginClient, public LocalZynAddSubFx { Nio::start(); - setInputCount( 0 ); + setInputOutputCount(0, 2); sendMessage( IdInitDone ); waitForMessage( IdInitDone ); @@ -144,7 +144,8 @@ class RemoteZynAddSubFx : public RemotePluginClient, public LocalZynAddSubFx void process(const float* in, float* out) override { (void)in; - auto output = SplitAudioData{&out, 2, bufferSize()}; + auto accessBuffer = std::array{out, out + bufferSize()}; + auto output = SplitAudioData{accessBuffer.data(), 2, bufferSize()}; LocalZynAddSubFx::process(output); } diff --git a/plugins/ZynAddSubFx/ZynAddSubFx.cpp b/plugins/ZynAddSubFx/ZynAddSubFx.cpp index a2643564c65..1bc4894746c 100644 --- a/plugins/ZynAddSubFx/ZynAddSubFx.cpp +++ b/plugins/ZynAddSubFx/ZynAddSubFx.cpp @@ -22,6 +22,7 @@ * */ +#include "RemotePluginAudioPort.h" #include "lmmsconfig.h" #include @@ -76,9 +77,8 @@ Plugin::Descriptor PLUGIN_EXPORT zynaddsubfx_plugin_descriptor = -ZynAddSubFxRemotePlugin::ZynAddSubFxRemotePlugin(PluginPinConnector* pinConnector) - : RemotePlugin{pinConnector} - , m_accessBuffer{} +ZynAddSubFxRemotePlugin::ZynAddSubFxRemotePlugin(RemotePluginAudioPortController& audioPort) + : RemotePlugin{audioPort} { init( "RemoteZynAddSubFx", false ); } @@ -97,31 +97,13 @@ bool ZynAddSubFxRemotePlugin::processMessage( const message & _m ) return RemotePlugin::processMessage( _m ); } -auto ZynAddSubFxRemotePlugin::inputBuffer() -> SplitAudioData -{ - return {}; -} - -auto ZynAddSubFxRemotePlugin::outputBuffer() -> SplitAudioData -{ - return {m_accessBuffer.data(), 2, frames()}; -} - -void ZynAddSubFxRemotePlugin::updateBuffers(int channelsIn, int channelsOut) -{ - RemotePlugin::updateBuffer(channelsIn, channelsOut); - - auto sourceBuffer = RemotePlugin::outputBuffer(); - m_accessBuffer[0] = sourceBuffer.data(); - m_accessBuffer[1] = sourceBuffer.data() + frames(); -} - ZynAddSubFxInstrument::ZynAddSubFxInstrument( InstrumentTrack * _instrumentTrack ) : - AudioPlugin(&zynaddsubfx_plugin_descriptor, _instrumentTrack, nullptr, Flag::IsSingleStreamed | Flag::IsMidiBased), + AudioPlugin(&zynaddsubfx_plugin_descriptor, _instrumentTrack, nullptr, + Flag::IsSingleStreamed | Flag::IsMidiBased, /* beginAsRemote */ false), m_hasGUI( false ), m_localPlugin(nullptr), m_remotePlugin( nullptr ), @@ -360,8 +342,7 @@ void ZynAddSubFxInstrument::processImpl() } else { - assert(m_localPluginBuffer.has_value()); - m_localPlugin->process(m_localPluginBuffer->outputBuffer()); + m_localPlugin->process(audioPort().outputBuffer()); } m_pluginMutex.unlock(); } @@ -459,7 +440,8 @@ void ZynAddSubFxInstrument::initPlugin() if( m_hasGUI ) { - m_remotePlugin = new ZynAddSubFxRemotePlugin(pinConnector()); + audioPort().useRemote(true); + m_remotePlugin = new ZynAddSubFxRemotePlugin(audioPort().controller()); m_remotePlugin->lock(); m_remotePlugin->waitForInitDone( false ); @@ -486,10 +468,13 @@ void ZynAddSubFxInstrument::initPlugin() } else { + audioPort().useRemote(false); + m_localPlugin = new LocalZynAddSubFx{}; - m_localPlugin->setSampleRate( Engine::audioEngine()->outputSampleRate() ); - m_localPlugin->setBufferSize( Engine::audioEngine()->framesPerPeriod() ); - m_localPluginBuffer.emplace(); + m_localPlugin->setSampleRate(Engine::audioEngine()->outputSampleRate()); + m_localPlugin->setBufferSize(Engine::audioEngine()->framesPerPeriod()); + + audioPort().activate(Engine::audioEngine()->framesPerPeriod()); } m_pluginMutex.unlock(); @@ -512,19 +497,6 @@ gui::PluginView* ZynAddSubFxInstrument::instantiateView( QWidget * _parent ) -auto ZynAddSubFxInstrument::bufferInterface() -> AudioPluginBufferInterface* -{ - if (m_remotePlugin) - { - return m_remotePlugin; - } - else - { - return &m_localPluginBuffer.value(); - } -} - - namespace gui { diff --git a/plugins/ZynAddSubFx/ZynAddSubFx.h b/plugins/ZynAddSubFx/ZynAddSubFx.h index 1e9f39b6638..e6ebef7c71d 100644 --- a/plugins/ZynAddSubFx/ZynAddSubFx.h +++ b/plugins/ZynAddSubFx/ZynAddSubFx.h @@ -33,9 +33,11 @@ #include #include "AudioPlugin.h" +#include "AudioPluginBuffer.h" #include "AutomatableModel.h" #include "InstrumentView.h" #include "RemotePlugin.h" +#include "RemotePluginAudioPort.h" class QPushButton; @@ -55,29 +57,28 @@ class ZynAddSubFxView; class ZynAddSubFxRemotePlugin : public RemotePlugin - , public AudioPluginBufferInterface { Q_OBJECT public: - ZynAddSubFxRemotePlugin(PluginPinConnector* pinConnector); + ZynAddSubFxRemotePlugin(RemotePluginAudioPortController& audioPort); bool processMessage( const message & _m ) override; - auto inputBuffer() -> SplitAudioData override; - auto outputBuffer() -> SplitAudioData override; - void updateBuffers(int channelsIn, int channelsOut) override; - signals: void clickedCloseButton(); +}; -private: - std::array m_accessBuffer; + +constexpr auto ZynConfig = AudioPluginConfig { + .kind = AudioDataKind::F32, + .interleaved = false, + .inputs = 0, + .outputs = 2 }; class ZynAddSubFxInstrument - : public AudioPlugin + : public AudioPlugin> { Q_OBJECT public: @@ -98,8 +99,6 @@ class ZynAddSubFxInstrument gui::PluginView* instantiateView( QWidget * _parent ) override; - auto bufferInterface() -> AudioPluginBufferInterface* override; - private slots: void reloadPlugin(); @@ -123,9 +122,6 @@ private slots: LocalZynAddSubFx * m_localPlugin; ZynAddSubFxRemotePlugin * m_remotePlugin; - // LocalZynAddSubFx needs to be supplied with a buffer - std::optional> m_localPluginBuffer; - FloatModel m_portamentoModel; FloatModel m_filterFreqModel; FloatModel m_filterQModel; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 95274e575af..b80a969d58c 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -65,6 +65,7 @@ set(LMMS_SRCS core/ProjectRenderer.cpp core/ProjectVersion.cpp core/RemotePlugin.cpp + core/RemotePluginAudioPort.cpp core/RenderManager.cpp core/RingBuffer.cpp core/Sample.cpp diff --git a/src/core/PluginPinConnector.cpp b/src/core/PluginPinConnector.cpp index 87bc4b250e3..3a26fac38ff 100644 --- a/src/core/PluginPinConnector.cpp +++ b/src/core/PluginPinConnector.cpp @@ -2,7 +2,7 @@ * PluginPinConnector.cpp - Specifies how to route audio channels * in and out of a plugin. * - * Copyright (c) 2024 Dalton Messmer + * Copyright (c) 2025 Dalton Messmer * * This file is part of LMMS - https://lmms.io * @@ -41,35 +41,47 @@ PluginPinConnector::PluginPinConnector(bool isInstrument, Model* parent) : Model{parent} , m_isInstrument{isInstrument} { - connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, this, &PluginPinConnector::pluginBuffersChanged); setTrackChannelCount(s_totalTrackChannels); + + connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, [this]() { + bufferPropertiesChanged(in().channelCount(), out().channelCount(), Engine::audioEngine()->framesPerPeriod()); + }); } PluginPinConnector::PluginPinConnector(int pluginChannelCountIn, int pluginChannelCountOut, bool isInstrument, Model* parent) : Model{parent} , m_isInstrument{isInstrument} { - connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, this, &PluginPinConnector::pluginBuffersChanged); setTrackChannelCount(s_totalTrackChannels); + setPluginChannelCountsImpl(pluginChannelCountIn, pluginChannelCountOut); - if (pluginChannelCountIn == 0 && pluginChannelCountOut == 0) - { - throw std::invalid_argument{"At least one port count must be non-zero"}; - } - - if (pluginChannelCountIn != DynamicChannelCount || pluginChannelCountOut != DynamicChannelCount) - { - setPluginChannelCounts(pluginChannelCountIn, pluginChannelCountOut); - } + connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, [this]() { + bufferPropertiesChanged(in().channelCount(), out().channelCount(), Engine::audioEngine()->framesPerPeriod()); + }); } void PluginPinConnector::setPluginChannelCounts(int inCount, int outCount) +{ + setPluginChannelCountsImpl(inCount, outCount); + + // Now tell the audio buffer to update + bufferPropertiesChanged(inCount, outCount, Engine::audioEngine()->framesPerPeriod()); + + emit propertiesChanged(); +} + +void PluginPinConnector::setPluginChannelCountsImpl(int inCount, int outCount) { if (m_trackChannelsUpperBound > MaxTrackChannels) { throw std::runtime_error{"Only up to 256 track channels are allowed"}; } + if (inCount == DynamicChannelCount || outCount == DynamicChannelCount) + { + return; + } + if (inCount < 0) { qWarning() << "Invalid input count"; @@ -96,9 +108,6 @@ void PluginPinConnector::setPluginChannelCounts(int inCount, int outCount) m_in.setPluginChannelCount(this, inCount, QString::fromUtf16(u"Pin in [%1 \U0001F82E %2]")); m_out.setPluginChannelCount(this, outCount, QString::fromUtf16(u"Pin out [%2 \U0001F82E %1]")); - - emit propertiesChanged(); - emit pluginBuffersChanged(); } void PluginPinConnector::setPluginChannelCountIn(int inCount) diff --git a/src/core/RemotePlugin.cpp b/src/core/RemotePlugin.cpp index 94238b12143..033c0ff4d8e 100644 --- a/src/core/RemotePlugin.cpp +++ b/src/core/RemotePlugin.cpp @@ -38,6 +38,8 @@ #include "BufferManager.h" #include "AudioEngine.h" #include "Engine.h" +#include "Model.h" +#include "RemotePluginAudioPort.h" #include "Song.h" #include @@ -132,7 +134,7 @@ void ProcessWatcher::run() -RemotePlugin::RemotePlugin(PluginPinConnector* pinConnector, Model* parent) +RemotePlugin::RemotePlugin(RemotePluginAudioPortController& audioPort) : QObject{} #ifdef SYNC_WITH_SHM_FIFO , RemotePluginBase{new shmFifo(), new shmFifo()} @@ -140,11 +142,11 @@ RemotePlugin::RemotePlugin(PluginPinConnector* pinConnector, Model* parent) , RemotePluginBase{} #endif , m_failed{true} - , m_pinConnector{pinConnector} , m_watcher{this} #if (QT_VERSION < QT_VERSION_CHECK(5,14,0)) , m_commMutex{QMutex::Recursive} #endif + , m_audioPort{&audioPort} { #ifndef SYNC_WITH_SHM_FIFO struct sockaddr_un sa; @@ -175,6 +177,8 @@ RemotePlugin::RemotePlugin(PluginPinConnector* pinConnector, Model* parent) } #endif + m_audioPort->connectBuffers(this); + connect( &m_process, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(processFinished(int,QProcess::ExitStatus)), Qt::DirectConnection ); @@ -190,6 +194,8 @@ RemotePlugin::RemotePlugin(PluginPinConnector* pinConnector, Model* parent) RemotePlugin::~RemotePlugin() { + m_audioPort->disconnectBuffers(); + m_watcher.stop(); m_watcher.wait(); @@ -315,6 +321,9 @@ bool RemotePlugin::init(const QString &pluginExecutable, { waitForInitDone(); } + + m_audioPort->activate(Engine::audioEngine()->framesPerPeriod()); + unlock(); return failed(); @@ -327,7 +336,7 @@ bool RemotePlugin::process() { if (m_failed || !isRunning()) { - if (m_outputBuffer.size() != 0) + if (!m_outputBuffer.empty()) { std::memset(m_outputBuffer.data(), 0, m_outputBuffer.size_bytes()); } @@ -346,7 +355,7 @@ bool RemotePlugin::process() fetchAndProcessAllMessages(); unlock(); } - if (m_outputBuffer.size() != 0) + if (!m_outputBuffer.empty()) { std::memset(m_outputBuffer.data(), 0, m_outputBuffer.size_bytes()); } @@ -356,7 +365,7 @@ bool RemotePlugin::process() lock(); sendMessage(IdStartProcessing); - if (m_failed || m_outputBuffer.size() == 0) + if (m_failed || m_outputBuffer.empty()) { unlock(); return false; @@ -371,7 +380,7 @@ bool RemotePlugin::process() -void RemotePlugin::updateBuffer(int channelsIn, int channelsOut) +void RemotePlugin::updateBuffer(int channelsIn, int channelsOut, fpp_t frames) { if (channelsIn < 0 || channelsOut < 0) { @@ -379,8 +388,6 @@ void RemotePlugin::updateBuffer(int channelsIn, int channelsOut) return; } - const auto frames = Engine::audioEngine()->framesPerPeriod(); - if (channelsIn == static_cast(m_channelsIn) && channelsOut == static_cast(m_channelsOut) && frames == m_frames) @@ -410,8 +417,6 @@ void RemotePlugin::updateBuffer(int channelsIn, int channelsOut) m_inputBuffer = Span{m_audioBuffer.get(), m_channelsIn * m_frames}; m_outputBuffer = Span{m_audioBuffer.get() + m_inputBuffer.size(), m_channelsOut * m_frames}; - bufferUpdated(); - sendMessage(message(IdChangeSharedMemoryKey).addString(m_audioBuffer.key())); } @@ -498,19 +503,8 @@ bool RemotePlugin::processMessage( const message & _m ) reply_message.addInt( Engine::audioEngine()->framesPerPeriod() ); break; - case IdChangeInputCount: - m_pinConnector->setPluginChannelCountIn(_m.getInt(0)); - updateBuffer(m_pinConnector->in().channelCount(), m_pinConnector->out().channelCount()); - break; - - case IdChangeOutputCount: - m_pinConnector->setPluginChannelCountOut(_m.getInt(0)); - updateBuffer(m_pinConnector->in().channelCount(), m_pinConnector->out().channelCount()); - break; - case IdChangeInputOutputCount: - m_pinConnector->setPluginChannelCounts(_m.getInt(0), _m.getInt(1)); - updateBuffer(m_pinConnector->in().channelCount(), m_pinConnector->out().channelCount()); + m_audioPort->pc().setPluginChannelCounts(_m.getInt(0), _m.getInt(1)); break; case IdDebugMessage: diff --git a/src/core/RemotePluginAudioPort.cpp b/src/core/RemotePluginAudioPort.cpp new file mode 100644 index 00000000000..b3de4d5c850 --- /dev/null +++ b/src/core/RemotePluginAudioPort.cpp @@ -0,0 +1,54 @@ +/* + * RemotePluginAudioPort.cpp - PluginAudioPort implementation for RemotePlugin + * + * Copyright (c) 2025 Dalton Messmer + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "RemotePluginAudioPort.h" + +#include "RemotePlugin.h" + +namespace lmms { + +RemotePluginAudioPortController::RemotePluginAudioPortController(PluginPinConnector& pinConnector) + : m_pinConnector{&pinConnector} +{ +} + +void RemotePluginAudioPortController::remotePluginUpdateBuffers(int channelsIn, int channelsOut, fpp_t frames) +{ + assert(m_buffers != nullptr); + m_buffers->updateBuffer(channelsIn, channelsOut, frames); +} + +auto RemotePluginAudioPortController::remotePluginInputBuffer() const -> float* +{ + assert(m_buffers != nullptr); + return m_buffers->inputBuffer().data(); +} + +auto RemotePluginAudioPortController::remotePluginOutputBuffer() const -> float* +{ + assert(m_buffers != nullptr); + return m_buffers->outputBuffer().data(); +} + +} // namespace lmms diff --git a/src/gui/PluginPinConnectorView.cpp b/src/gui/PluginPinConnectorView.cpp index 92ee9dddeff..29e9391cefe 100644 --- a/src/gui/PluginPinConnectorView.cpp +++ b/src/gui/PluginPinConnectorView.cpp @@ -1,7 +1,7 @@ /* * PluginPinConnectorView.cpp - Displays pin connectors * - * Copyright (c) 2024 Dalton Messmer + * Copyright (c) 2025 Dalton Messmer * * This file is part of LMMS - https://lmms.io * diff --git a/tests/src/core/PluginPinConnectorTest.cpp b/tests/src/core/PluginPinConnectorTest.cpp index 7ec47c4d48b..4c358d73e86 100644 --- a/tests/src/core/PluginPinConnectorTest.cpp +++ b/tests/src/core/PluginPinConnectorTest.cpp @@ -1,7 +1,7 @@ /* * PluginPinConnectorTest.cpp * - * Copyright (c) 2024 Dalton Messmer + * Copyright (c) 2025 Dalton Messmer * * This file is part of LMMS - https://lmms.io * @@ -33,6 +33,7 @@ #include "AudioEngine.h" #include "AudioPluginBuffer.h" #include "Model.h" +#include "PluginAudioPort.h" #include "PluginPinConnector.h" #include "SampleFrame.h" #include "lmms_basics.h" @@ -83,12 +84,11 @@ void transformBuffer(CoreAudioBus in, CoreAudioBusMut out, const F& func) } template -void transformBuffer(CoreAudioData in, CoreAudioDataMut out, const F& func) +void transformBuffer(CoreAudioDataMut inOut, const F& func) { - assert(in.size() == out.size()); - for (std::size_t frame = 0; frame < in.size(); ++frame) + for (SampleFrame& sf : inOut) { - out[frame] = func(in[frame]); + sf = func(sf); } } @@ -351,16 +351,17 @@ private slots: QCOMPARE(pc.m_routedChannels[1], true); } - //! Verifies correct default routing for 1x1 non-interleaved (split) plugin - void Routing_Split1x1_Default() + //! Verifies correct default routing for 1x1 non-interleaved plugin + void Routing_NonInterleaved1x1_Default() { using namespace lmms; // Setup + constexpr auto config = AudioPluginConfig{AudioDataKind::F32, false, 1, 1}; auto model = Model{nullptr}; - auto pc = PluginPinConnector{1, 1, false, &model}; + auto ap = DefaultPluginAudioPort{false, &model}; + ap.init(); auto coreBus = getCoreBus(); - SampleFrame* trackChannels = coreBus.bus[0]; // channels 0/1 // Use left channel as plugin input, upmix mono plugin output to stereo // In Out @@ -375,6 +376,7 @@ private slots: // plugin input, following what REAPER does by default. // Data on frames 0, 1, and 33 + SampleFrame* trackChannels = coreBus.bus[0]; // channels 0/1 trackChannels[0].setLeft(123.f); trackChannels[0].setRight(321.f); trackChannels[1].setLeft(456.f); @@ -383,12 +385,11 @@ private slots: trackChannels[33].setRight(987.f); // Plugin input and output buffers - auto bufferSplit1x1 = AudioPluginBufferDefaultImpl{}; - auto ins = bufferSplit1x1.inputBuffer(); - auto outs = bufferSplit1x1.outputBuffer(); + auto ins = ap.inputBuffer(); + auto outs = ap.outputBuffer(); // Route to plugin - auto router = pc.getRouter(); + auto router = ap.getRouter(); router.routeToPlugin(coreBus, ins); // Check that plugin inputs have data on frames 0, 1, and 33 (should be left channel's data) @@ -422,18 +423,20 @@ private slots: compareBuffers(coreBus, coreBusExpected); } - //! Verifies correct default routing for 2x2 non-interleaved (split) plugin - void Routing_Split2x2_Default() + //! Verifies correct default routing for 2x2 non-interleaved plugin + void Routing_NonInterleaved2x2_Default() { using namespace lmms; // Setup + constexpr auto config = AudioPluginConfig{AudioDataKind::F32, false, 2, 2}; auto model = Model{nullptr}; - auto pc = PluginPinConnector{2, 2, false, &model}; + auto ap = DefaultPluginAudioPort{false, &model}; + ap.init(); auto coreBus = getCoreBus(); - SampleFrame* trackChannels = coreBus.bus[0]; // channels 0/1 // Data on frames 0, 1, and 33 + SampleFrame* trackChannels = coreBus.bus[0]; // channels 0/1 trackChannels[0].setLeft(123.f); trackChannels[0].setRight(321.f); trackChannels[1].setLeft(456.f); @@ -442,12 +445,11 @@ private slots: trackChannels[33].setRight(987.f); // Plugin input and output buffers - auto bufferSplit2x2 = AudioPluginBufferDefaultImpl{}; - auto ins = bufferSplit2x2.inputBuffer(); - auto outs = bufferSplit2x2.outputBuffer(); + auto ins = ap.inputBuffer(); + auto outs = ap.outputBuffer(); // Route to plugin - auto router = pc.getRouter(); + auto router = ap.getRouter(); router.routeToPlugin(coreBus, ins); // Check that plugin inputs have data on frames 0, 1, and 33 @@ -493,16 +495,17 @@ private slots: compareBuffers(coreBus, coreBusExpected); } - //! Verifies correct partially-bypassed routing for 2x2 non-interleaved (split) plugin - void Routing_Split2x2_Bypass() + //! Verifies correct partially-bypassed routing for 2x2 non-interleaved plugin + void Routing_NonInterleaved2x2_Bypass() { using namespace lmms; // Setup + constexpr auto config = AudioPluginConfig{AudioDataKind::F32, false, 2, 2}; auto model = Model{nullptr}; - auto pc = PluginPinConnector{2, 2, false, &model}; + auto ap = DefaultPluginAudioPort{false, &model}; + ap.init(); auto coreBus = getCoreBus(); - SampleFrame* trackChannels = coreBus.bus[0]; // channels 0/1 // Default input connections, disable right output channel // In Out @@ -510,6 +513,7 @@ private slots: // |X| | |X| | // | |X| | | | // --- --- + auto& pc = ap.pinConnector(); pc.in().pins(0)[0]->setValue(true); pc.in().pins(0)[1]->setValue(false); pc.in().pins(1)[0]->setValue(false); @@ -520,6 +524,7 @@ private slots: pc.out().pins(1)[1]->setValue(false); // Data on frames 0, 1, and 33 + SampleFrame* trackChannels = coreBus.bus[0]; // channels 0/1 trackChannels[0].setLeft(123.f); trackChannels[0].setRight(321.f); trackChannels[1].setLeft(456.f); @@ -528,12 +533,11 @@ private slots: trackChannels[33].setRight(987.f); // Plugin input and output buffers - auto bufferSplit2x2 = AudioPluginBufferDefaultImpl{}; - auto ins = bufferSplit2x2.inputBuffer(); - auto outs = bufferSplit2x2.outputBuffer(); + auto ins = ap.inputBuffer(); + auto outs = ap.outputBuffer(); // Route to plugin - auto router = pc.getRouter(); + auto router = ap.getRouter(); router.routeToPlugin(coreBus, ins); // Check that plugin inputs have data on frames 0, 1, and 33 @@ -580,12 +584,16 @@ private slots: using namespace lmms; // Setup + constexpr auto config = AudioPluginConfig { + AudioDataKind::SampleFrame, true, 2, 2, true + }; auto model = Model{nullptr}; - auto pc = PluginPinConnector{2, 2, false, &model}; + auto ap = DefaultPluginAudioPort{false, &model}; + ap.init(); auto coreBus = getCoreBus(); - SampleFrame* trackChannels = coreBus.bus[0]; // channels 0/1 // Data on frames 0, 1, and 33 + SampleFrame* trackChannels = coreBus.bus[0]; // channels 0/1 trackChannels[0].setLeft(123.f); trackChannels[0].setRight(321.f); trackChannels[1].setLeft(456.f); @@ -593,27 +601,23 @@ private slots: trackChannels[33].setLeft(789.f); trackChannels[33].setRight(987.f); - // Plugin input and output buffers - auto pluginBuffers = AudioPluginBufferDefaultImpl{}; - auto ins = pluginBuffers.inputBuffer(); - auto outs = pluginBuffers.outputBuffer(); - - QCOMPARE(ins.data(), outs.data()); // in-place processing - should use same buffer for inputs and outputs + // Plugin input/output buffer + auto inOut = ap.inputOutputBuffer(); // Route to plugin - auto router = pc.getRouter(); - router.routeToPlugin(coreBus, ins); + auto router = ap.getRouter(); + router.routeToPlugin(coreBus, inOut); // Check that plugin inputs have data on frames 0, 1, and 33 - QCOMPARE(ins[0].left(), 123.f); - QCOMPARE(ins[0].right(), 321.f); - QCOMPARE(ins[1].left(), 456.f); - QCOMPARE(ins[1].right(), 654.f); - QCOMPARE(ins[33].left(), 789.f); - QCOMPARE(ins[33].right(), 987.f); + QCOMPARE(inOut[0].left(), 123.f); + QCOMPARE(inOut[0].right(), 321.f); + QCOMPARE(inOut[1].left(), 456.f); + QCOMPARE(inOut[1].right(), 654.f); + QCOMPARE(inOut[33].left(), 789.f); + QCOMPARE(inOut[33].right(), 987.f); // Do work of processImpl - in this case it doubles the amplitude - transformBuffer(ins, outs, [](auto s) { return s * 2; }); + transformBuffer(inOut, [](auto s) { return s * 2; }); // Construct buffer with the expected core bus result auto coreBufferExpected = std::vector(MaxFrames); @@ -625,7 +629,7 @@ private slots: lmms::zeroBuffer(coreBus); // Route from plugin back to Core - router.routeFromPlugin(outs, coreBus); + router.routeFromPlugin(inOut, coreBus); // Should be double the original QCOMPARE(coreBus.bus[0][0].left(), 123.f * 2); @@ -639,16 +643,17 @@ private slots: compareBuffers(coreBus, coreBusExpected); } - //! Verifies correct signal summing when routing a 1x2 non-interleaved (split) plugin - void Routing_Split1x2_Sum() + //! Verifies correct signal summing when routing a 1x2 non-interleaved plugin + void Routing_NonInterleaved1x2_Sum() { using namespace lmms; // Setup + constexpr auto config = AudioPluginConfig{AudioDataKind::F32, false, 1, 2}; auto model = Model{nullptr}; - auto pc = PluginPinConnector{1, 2, false, &model}; + auto ap = DefaultPluginAudioPort{false, &model}; + ap.init(); auto coreBus = getCoreBus(); - SampleFrame* trackChannels = coreBus.bus[0]; // channels 0/1 // Sum both track channels together for plugin input, and sum the // two plugin output channels together for the left track channel output @@ -657,6 +662,7 @@ private slots: // |X| |X|X| // |X| | | | // - --- + auto& pc = ap.pinConnector(); pc.in().pins(0)[0]->setValue(true); pc.in().pins(1)[0]->setValue(true); pc.out().pins(0)[0]->setValue(true); @@ -665,6 +671,7 @@ private slots: pc.out().pins(1)[1]->setValue(false); // Data on frames 0, 1, and 33 + SampleFrame* trackChannels = coreBus.bus[0]; // channels 0/1 trackChannels[0].setLeft(123.f); trackChannels[0].setRight(321.f); trackChannels[1].setLeft(456.f); @@ -673,12 +680,11 @@ private slots: trackChannels[33].setRight(987.f); // Plugin input and output buffers - auto bufferSplit1x2 = AudioPluginBufferDefaultImpl{}; - auto ins = bufferSplit1x2.inputBuffer(); - auto outs = bufferSplit1x2.outputBuffer(); + auto ins = ap.inputBuffer(); + auto outs = ap.outputBuffer(); // Route to plugin - auto router = pc.getRouter(); + auto router = ap.getRouter(); router.routeToPlugin(coreBus, ins); // Check that plugin inputs have data on frames 0, 1, and 33 (should be both track channels summed together)