Contenuto principale

Tips and Tricks for Plugin Authoring

R2026b

To author your algorithm as an audio plugin, you must conform to the audio plugin API. When authoring audio plugins in the MATLAB® environment, keep these common pitfalls and best practices in mind.

To learn more about audio plugins in general, see Audio Plugins in MATLAB.

Avoid Disrupting the Event Queue in MATLAB

When the Audio Test Bench runs an audio plugin, it sequentially:

  1. Calls the reset method

  2. Sets tunable properties associated with parameters

  3. Calls the process method

While running, the Audio Test Bench calls in a loop the process method and then the set methods for tuned properties. The plugin API does not specify the order that the tuned properties are set.

It is possible to disrupt the normal methods timing by interrupting the event queue. Common ways to accidentally interrupt the event queue include using a plot or drawnow function.

Note

plot and drawnow are only available in the MATLAB environment. plot and drawnow cannot be included in generated plugins. See Separate Code for Features Not Supported for Plugin Generation for more information.

In the following code snippet, the gain applied to the left and right channels is not the same if the associated Gain parameter is tuned during the call to process:

...
L = plugin.Gain*in(:,1);
drawnow
R = plugin.Gain*in(:,2);
out = [L,R];
...

classdef badPlugin < audioPlugin
    properties
        Gain = 0.5;
    end
    properties (Constant)
        PluginInterface = audioPluginInterface(audioPluginParameter('Gain'));
    end
    methods
        function out = process(plugin,in)

            L = plugin.Gain*in(:,1);
            
            drawnow
            
            R = plugin.Gain*in(:,2);
            
            out = [L,R];
        end
        function set.Gain(plugin,val)
            plugin.Gain = val;
        end
    end
end

The author interrupts the event queue in the code snippet, causing the set methods of properties associated with parameters to be called while the process method is in the middle of execution.

Depending on your processing algorithm, interrupting the event queue can lead to inconsistent and buggy behavior. Also, the set method might not be explicit, which can make the issue difficult to track down. Possible fixes for the problem of event queue disruption include saving properties to local variables, and moving the queue disruption to the beginning or end of the process method.

Save Properties to Local Variables

You can save tunable property values to local variables at the start of your processing. This technique ensures that the values used during the process method are not updated within a single call to process. Because accessing the value of a local variable is cheaper than accessing the value of a property, saving properties to local variables that are accessed multiple times is a best practice.

...
gain = plugin.Gain;
L = gain*in(:,1);
drawnow
R = gain*in(:,2);
out = [L,R];
...

classdef goodPlugin < audioPlugin
    properties
        Gain = 0.5;
    end
    properties (Constant)
        PluginInterface = audioPluginInterface(audioPluginParameter('Gain'));
    end
    methods
        function out = process(plugin,in)
            gain = plugin.Gain;
            
            L = gain*in(:,1);
            
            drawnow
            
            R = gain*in(:,2);
            
            out = [L,R];
        end
        function set.Gain(plugin,val)
            plugin.Gain = val;
        end
    end
end

Move Queue Disruption to Bottom or Top of Process Method

You can move the disruption to the event queue to the bottom or top of the process method. This technique ensures that property values are not updated in the middle of the call.

...
L = plugin.Gain*in(:,1);
R = plugin.Gain*in(:,2);
out = [L,R];
drawnow
...

classdef goodPlugin < audioPlugin
    properties
        Gain = 0.5;
    end
    properties (Constant)
        PluginInterface = audioPluginInterface(audioPluginParameter('Gain'));
    end
    methods
        function out = process(plugin,in)
            
            L = plugin.Gain*in(:,1);
            
            R = plugin.Gain*in(:,2);
            
            out = [L,R];
            
            drawnow
        end
        function set.Gain(plugin,val)
            plugin.Gain = val;
        end
    end
end

Separate Code for Features Not Supported for Plugin Generation

The MATLAB environment offers functionality not supported for plugin generation. You can mark code to ignore during plugin generation by placing it inside a conditional statement by using coder.target (MATLAB Coder).

...
    if coder.target('MATLAB')
    ...
    end
...
If you generate the plugin using generateAudioPlugin, code inside the statement if coder.target('MATLAB') is ignored.

For example, timescope is not enabled for code generation. If you run the following plugin in MATLAB, you can use the visualize function to open a time scope that plots the input and output power per frame.

classdef  pluginWithMATLABOnlyFeatures < audioPlugin
    properties
        Threshold = -10;
    end
    properties (Access = private)
        aCompressor
        aScope
        SamplesPerFrame = 1;
    end
    properties (Constant)
        PluginInterface = audioPluginInterface( ...
            audioPluginParameter('Threshold','Mapping',{'lin',-60,20}));
    end
    methods
        function plugin = pluginWithMATLABOnlyFeatures
            plugin.aCompressor = compressor;
            setup(plugin.aCompressor,[0,0])
        end
        function out = process(plugin,in)
            out = plugin.aCompressor(in);
            
            % The contents of this if-statement are ignored during plugin
            % generation.
            if coder.target('MATLAB')
                if ~isempty(plugin.aScope) && isvalid(plugin.aScope)
                    numSamples = size(in,1);
                    
                    % The time scope object is not enabled for
                    % variable-size signals. Call release if the samples
                    % per frame is changed.
                    % Because this code is intended for use in MATLAB only,
                    % it is okay to call release on the time scope object.
                    % Do not call release on a System object in generated
                    % code.
                    if plugin.SamplesPerFrame(1) ~= numSamples
                        release(plugin.aScope)
                        plugin.SamplesPerFrame = numSamples;
                    end
                    
                    power = 20*log10(mean(var(in)))*ones(numSamples,1);
                    adjustedPower = 20*log10(mean(var(out)))*ones(numSamples,1);
                    plugin.aScope([power,adjustedPower]);
                end
            end
        end
        function reset(plugin)
            fs = getSampleRate(plugin);
            plugin.aCompressor.SampleRate = fs;
            reset(plugin.aCompressor)
            
            % The contents of this if-statement are ignored during plugin
            % genderation.
            if coder.target('MATLAB')
                if ~isempty(plugin.aScope)
                    % Because this code is intended for use in MATLAB only,
                    % it is okay to call release on the time scope object.
                    % Do not call release on a System object in generated
                    % code.
                    release(plugin.aScope)
                    plugin.aScope.SampleRate = fs;
                    plugin.aScope.BufferLength = 2*fs;
                end
            end
        end
        function visualize(plugin)
            % Visualization function. This function is public in the MATLAB
            % environment. Because the plugin does not call this function
            % directly, the function is not part of the code generated by
            % generateAudioPlugin.
            
            % Create a time scope object for visualization in the MATLAB
            % environment.
            plugin.aScope = timescope( ...
                'SampleRate',getSampleRate(plugin), ...
                'TimeSpan',1, ...
                'YLimits',[-40,0], ...
                'BufferLength',2*getSampleRate(plugin), ...
                'TimeSpanOverrunAction','Scroll', ...
                'YLabel','Power (dB)');
            show(plugin.aScope)
        end
        function set.Threshold(plugin,val)
            plugin.Threshold = val;
            plugin.aCompressor.Threshold = val;
        end
    end
end

Implement Reset Correctly

A common error in audio plugin authoring is misusing the reset method. Valid uses of the reset method include:

  • Clearing state

  • Passing down calls to reset to component objects

  • Updating properties which depend on sample rate

Invalid use of the reset method includes setting the value of any properties associated with parameters. Do not use your reset method to set properties associated with parameters to their initial conditions. Directly setting a property associated with a parameter causes the property to be out of sync with the parameter. For example, the following plugin is an example of incorrect use of the reset method.

classdef badReset < audioPlugin
    properties
        Gain = 1;
    end
    properties (Constant)
        PluginInterface = audioPluginInterface(audioPluginParameter('Gain'));
    end
    methods
        function out = process(plugin,in)
            out = in*plugin.Gain;
        end
        function reset(plugin) % <-- Incorrect use of reset method.
            plugin.Gain = 1;   % <-- Never set values of a property that is
        end                    %     associated with a plugin parameter.
    end
end

Implement Plugin Composition Correctly

If your plugin is composed of other plugins, then you must pass down the sample rate and calls to reset to the component plugins. Call setSampleRate in the reset method to pass down the sample rate to the component plugins. To tune parameters of the component plugins, create an audio plugin interface in the composite plugin for tunable parameters of the component plugins. Then pass down the values in the set methods for the associated properties. The following is an example of plugin composition that was constructed using best practices.

classdef compositePlugin < audioPlugin
    properties
        PhaserQ  = 1.6;
        EchoGain = 0.5;
    end
    properties (Access = private)
        aEcho
        aPhaser
    end
    properties (Constant)
        PluginInterface = audioPluginInterface( ...
            audioPluginParameter('PhaserQ', ...
                'DisplayName','Phaser Q', ...
                'Mapping',{'lin',0.5, 25}), ...
            audioPluginParameter('EchoGain', ...
                'DisplayName','Gain'));
    end
    methods
        function plugin = compositePlugin
            % Construct your component plugins in the composite plugin's
            % constructor.
            plugin.aPhaser = audiopluginexample.Phaser;
            plugin.aEcho   = audiopluginexample.Echo;
        end
        function out = process(plugin,in)
            % Call the process method of your component plugins inside the
            % call to the process method of your composite plugin.
            x = process(plugin.aPhaser,in);
            y = process(plugin.aEcho,x);
            out = y;
        end
        function reset(plugin)
            % Use the setSampleRate method to set the sample rate of
            % component plugins and pass the call to reset down.
            fs = getSampleRate(plugin);
            
            setSampleRate(plugin.aPhaser, fs)
            setSampleRate(plugin.aEcho, fs)
            
            reset(plugin.aPhaser)
            reset(plugin.aEcho);
        end
        % Use the set method of your properties to pass down property
        % values to your component plugins.
        function set.PhaserQ(plugin,val)
            plugin.PhaserQ = val;
            plugin.aPhaser.QualityFactor = val;
        end
        function set.EchoGain(plugin,val)
            plugin.EchoGain = val;
            plugin.aEcho.Gain = val;
        end
    end
end

Plugin composition using System objects has these key differences from plugin composition using basic plugins.

  • Immediately call setup on your component System object™ after it is constructed. Construction and setup of the component object occurs inside the constructor of the composite plugin.

  • If your component System object requires sample rate information, then it has a sample rate property. Set the sample rate property in the reset method.

classdef compositePluginWithSystemObjects < audioPlugin
    properties
        CrossoverFrequency  = 100;
        CompressorThreshold = -40;
    end
    properties (Access = private)
        aCrossoverFilter
        aCompressor
    end
    properties (Constant)
        PluginInterface = audioPluginInterface( ...
            audioPluginParameter('CrossoverFrequency', ...
                'DisplayName','Crossover Frequency', ...
                'Mapping',{'lin',50, 200}), ...
            audioPluginParameter('CompressorThreshold', ...
                'DisplayName','Compressor Threshold', ...
                'Mapping',{'lin',-100,0}));
    end
    methods
        function plugin = compositePluginWithSystemObjects
            % Construct your component System objects within the composite
            % plugin's constructor. Call setup immediately after
            % construction. 
            % 
            % The audio plugin API requires plugins to declare the number
            % of input and output channels in the plugin interface. This
            % plugin uses the default 2-in 2-out configuration. Call setup
            % with a sample input that has the same number of channels as
            % defined in the plugin interface.
            %
            sampleInput = zeros(1,2);
            
            plugin.aCrossoverFilter = crossoverFilter;
            setup(plugin.aCrossoverFilter,sampleInput)
            
            plugin.aCompressor = compressor;
            setup(plugin.aCompressor,sampleInput)
        end
        function out = process(plugin,in)
            % Call your component System objects inside the call to
            % process of your composite plugin.
            [band1,band2] = plugin.aCrossoverFilter(in);
            band1Compressed = plugin.aCompressor(band1);
            out = band1Compressed + band2;
        end
        function reset(plugin)
            % Set the sample rate properties of your component System
            % objects.
            fs = getSampleRate(plugin);
            
            plugin.aCrossoverFilter.SampleRate = fs;
            plugin.aCompressor.SampleRate = fs;
            
            reset(plugin.aCrossoverFilter)
            reset(plugin.aCompressor);
        end
        % Use the set method of your properties to pass down property
        % values to your component System objects.
        function set.CrossoverFrequency(plugin,val)
            plugin.CrossoverFrequency = val;
            plugin.aCrossoverFilter.CrossoverFrequencies = val;
        end
        function set.CompressorThreshold(plugin,val)
            plugin.CompressorThreshold = val;
            plugin.aCompressor.Threshold = val;
        end
    end
end

Address "A set method for a non-Dependent property should not access another property" Warning in Plugin

It is recommended that you suppress the warning when authoring audio plugins.

The following code snippet follows the plugin authoring best practice for processing changes in parameter property Cutoff.

classdef highpassFilter < audioPlugin
...
    properties (Constant)
        PluginInterface = audioPluginInterface( ...
            audioPluginParameter('Cutoff', ...
            'Label','Hz',...
            'Mapping',{'log',20,2000}));
    end
    methods
        function y = process(plugin,x)
            [y,plugin.State] = filter(plugin.B,plugin.A,x,plugin.State);
        end

        function set.Cutoff(plugin,val)
            plugin.Cutoff = val;
            [plugin.B,plugin.A] = highpassCoeffs(plugin,val,getSampleRate(plugin)); % <<<< warning occurs here
        end
    end
...
end

classdef highpassFilter < audioPlugin
    %-----------------------------------------------------------------------
    % Public Properties - End user interacts with these
    %-----------------------------------------------------------------------
    properties
        Cutoff = 20;
    end
    
    %-----------------------------------------------------------------------
    % Private Properties - Used for internal storage
    %-----------------------------------------------------------------------
    properties (Access = private)
        State = zeros(2);
        B     = zeros(1,3);
        A     = zeros(1,3);
    end
    
    %-----------------------------------------------------------------------
    % Constant Properties - Used to define plugin interface
    %-----------------------------------------------------------------------
    properties (Constant)
        PluginInterface = audioPluginInterface( ...
            audioPluginParameter('Cutoff', ...
            'Label','Hz', ...
            'Mapping',{'log',20,2000}));
    end
    
    methods
        %-------------------------------------------------------------------
        % Main processing function
        %-------------------------------------------------------------------
        function y = process(plugin,x)
            [y,plugin.State] = filter(plugin.B,plugin.A,x,plugin.State);
        end
        
        %-------------------------------------------------------------------
        % Set Method
        %-------------------------------------------------------------------
        function set.Cutoff(plugin,val)
            plugin.Cutoff = val;
            [plugin.B,plugin.A] = highpassCoeffs(plugin,val,getSampleRate(plugin)); % <<<< warning occurs here
        end
        
        %-------------------------------------------------------------------
        % Reset Method
        %-------------------------------------------------------------------
        function reset(plugin)
            plugin.State = zeros(2);
            [plugin.B,plugin.A] = highpassCoeffs(plugin,plugin.Cutoff,getSampleRate(plugin)); 
        end
    end
    methods (Access = private)
        %-------------------------------------------------------------------
        % Calculate Filter Coefficients
        %-------------------------------------------------------------------
        function [B,A] = highpassCoeffs(~,fc,fs)
            w0    = 2*pi*fc/fs;
            alpha = sin(w0)/sqrt(2);
            cosw0 = cos(w0);
            norm  = 1/(1+alpha);
            B     = (1 + cosw0)*norm * [.5 -1 .5];
            A     = [1 -2*cosw0*norm (1 - alpha)*norm];
        end
    end
end

The highpassCoeffs function might be expensive, and should be called only when necessary. You do not want to call highpassCoeffs in the process method, which runs in the real-time audio processing loop. The logical place to call highpassCoeffs is in set.Cutoff. However, mlint shows a warning for this practice. The warning is intended to help you avoid initialization order issues when saving and loading classes. See Avoid Property Initialization Order Dependency for more details. The solution recommended by the warning is to create a dependent property with a get method and compute the value there. However, following the recommendation complicates the design and pushes the computation back into the real-time processing method, which you are trying to avoid.

You might also incur the warning when correctly implementing plugin composition. For an example of a correct implementation of composition, see Implement Plugin Composition Correctly.

Use System Object That Does Not Support Variable-Size Signals

The audio plugin API requires audio plugins to support variable-size inputs and outputs. For a partial list of System objects that support variable-size signals, see Variable-Size Signal Support DSP System Objects. You might encounter issues if you attempt to use objects that do not support variable-size signals in your plugin.

For example, dsp.AnalyticSignal does not support variable-size signals. The BrokenAnalyticSignalTransformer plugin uses a dsp.AnalyticSignal object incorrectly and fails the validateAudioPlugin test bench:

validateAudioPlugin BrokenAnalyticSignalTransformer
Checking plug-in class 'BrokenAnalyticSignalTransformer'... passed.
Generating testbench file 'testbench_BrokenAnalyticSignalTransformer.m'... done.
Running testbench... 
Error using dsp.AnalyticSignal/parenReference
Changing the size on input 1 is not allowed without first calling the release() method.

Error in BrokenAnalyticSignalTransformer/process (line 13)
                analyticSignal = plugin.Transformer(in);

Error in testbench_BrokenAnalyticSignalTransformer (line 61)
        o1 = process(plugin, in(:,1));

Error in validateAudioPlugin

classdef BrokenAnalyticSignalTransformer < audioPlugin
    properties (Access = private)
        Transformer
    end
    properties (Constant)
        PluginInterface = audioPluginInterface('InputChannels',1,'OutputChannels',2);
    end
    methods
        function plugin = BrokenAnalyticSignalTransformer
            plugin.Transformer = dsp.AnalyticSignal;
        end
        function out = process(plugin,in)
                analyticSignal = plugin.Transformer(in);
                realPart = real(analyticSignal);
                imaginaryPart = imag(analyticSignal);
                out = [realPart,imaginaryPart];
        end
    end
end

If you want to use the functionality of a System object that does not support variable-size signals, you can buffer the input and output of the System object, or always call the object with one sample.

Always Call the Object with One Sample

You can create a loop around your call to an object. The loop iterates for the number of samples in your variable frame size. The call to the object inside the loop is always a single sample.

classdef ExpensiveAnalyticSignalTransformer < audioPlugin
    properties (Access = private)
        Transformer
    end
    properties (Constant)
        PluginInterface = audioPluginInterface('InputChannels',1,'OutputChannels',2);
    end
    methods
        function plugin = ExpensiveAnalyticSignalTransformer
            plugin.Transformer = dsp.AnalyticSignal;
        end
        function out = process(plugin,in)
            analyticSignal = complex(zeros(size(in,1),1),0);
            for i = 1:size(in,1)
                analyticSignal(i,:) = plugin.Transformer(in(i,1));
            end
            out = [real(analyticSignal),imag(analyticSignal)];
        end
    end
end

Note

Depending on your implementation and the particular object, calling an object sample by sample in a loop might result in significant computational cost.

Buffer Input and Output of Object

You can buffer the input to your object to a consistent frame size, and then buffer the output of your object back to the original frame size. The dsp.AsyncBuffer System object is well-suited for this task.

classdef DelayedAnalyticSignalTransformer < audioPlugin
    properties (Access = private)
        Transformer
        InputBuffer
        OutputBuffer
    end
    properties (Constant)
        PluginInterface = audioPluginInterface('InputChannels',1,'OutputChannels',2);
        MinSampleDelay = 256;
    end
    methods
        function plugin = DelayedAnalyticSignalTransformer
            plugin.Transformer = dsp.AnalyticSignal;
            setup(plugin.Transformer,ones(plugin.MinSampleDelay,1));
            
            plugin.InputBuffer = dsp.AsyncBuffer;
            setup(plugin.InputBuffer,1);
            
            plugin.OutputBuffer = dsp.AsyncBuffer;
            setup(plugin.OutputBuffer,[1,1]);
        end
        function out = process(plugin,in)
            write(plugin.InputBuffer,in);
            
            while plugin.InputBuffer.NumUnreadSamples >= plugin.MinSampleDelay
                x = read(plugin.InputBuffer,plugin.MinSampleDelay);
                analyticSignal = plugin.Transformer(x(1:plugin.MinSampleDelay,:));
                write(plugin.OutputBuffer,[real(analyticSignal),imag(analyticSignal)]);
            end
            
            if plugin.OutputBuffer.NumUnreadSamples >= size(in,1)
                out = read(plugin.OutputBuffer,size(in,1));
            else
                out = zeros(size(in,1),2);
            end
        end
        function reset(plugin)
            reset(plugin.Transformer)
            reset(plugin.InputBuffer)
            reset(plugin.OutputBuffer)
        end
    end
end

Note

Use of the asynchronous buffering object forces a minimum latency of your specified frame size.

Using Enumeration Parameter Mapping

It is often useful to associate a property with a set of strings or character vectors. However, restrictions on plugin generation require cached values, such as property values, to have a static size. To work around this issue, you can use a separate enumeration class that maps the strings to the enumerations, as described in the audioPluginParameter documentation.

Alternatively, if you want to avoid writing an enumeration class and keep all your code in one file, you can use a dependent property to map your parameter names to a set of values. In this scenario, you map your enumeration value to a value that you can cache.

classdef pluginWithEnumMapping < audioPlugin
    properties (Dependent)
        Mode = '+6 dB';
    end
    properties (Access = private)
        pMode = 1; % '+6 dB'
    end
    properties (Constant)
        PluginInterface = audioPluginInterface(...
            audioPluginParameter('Mode',...
                'Mapping',{'enum','+6 dB','-6 dB','silence','white noise'}));
    end
    methods
        function out = process(plugin,in)
            switch (plugin.pMode)
                case 1
                    out = in * 2;
                case 2
                    out = in / 2;
                case 3
                    out = zeros(size(in));
                otherwise % case 4
                    out = rand(size(in)) - 0.5;
            end
        end
        function set.Mode(plugin,val)
            validatestring(val,{'+6 dB','-6 dB','silence','white noise'},'set.Mode','Mode');
            switch val
                case '+6 dB'
                    plugin.pMode = 1;
                case '-6 dB'
                    plugin.pMode = 2;
                case 'silence'
                    plugin.pMode = 3;
                otherwise % 'white noise'
                    plugin.pMode = 4;
            end
        end
        function out = get.Mode(plugin)
            switch plugin.pMode
                case 1
                    out = '+6 dB';
                case 2
                    out = '-6 dB';
                case 3
                    out = 'silence';
                otherwise % case 4
                    out = 'white noise';
            end
        end
    end
end

See Also

Topics