classdef VolumeController < audioPlugin
% VolumeController Control the volume and balance of an audio signal
%   This plugin controls volume and left/right balance. The changes
%   occur gradually over a transition time to avoid audible artifacts
%   when modifying volume, mute, and balance.
% 
%   VOLUME = audiopluginexample.VolumeController() returns an audio
%   plugin object VOLUME with properties set to their default values.
%
%   VolumeController methods:
%
%   Y = process(VOLUME, X) applies volume, mute, and balance to the
%   audio input X and returns it as output Y. Each column of X is
%   treated as individual input channels. Mute and Volume are applied
%   to all channels. Balance is only applied to two-channel signals.
%
%   VolumeController properties:
%
%   Mute            - Mute output
%   Gain            - Amplitude gain (dB)
%   Balance         - Left/right balance (stereo only)
%   TransitionTime  - Transition time when changing other properties (ms)
%
%   % Example 1: Simulate volume in MATLAB
%   reader = dsp.AudioFileReader( ...
%       "RockGuitar.wav", SamplesPerFrame=1024, PlayCount=1);
%
%   player = audioDeviceWriter(SampleRate=reader.SampleRate);
%
%   volume = audiopluginexample.VolumeController;
%
%   parameterTuner(volume)
%   while ~isDone(reader)
%       x = reader();
%       y = process(volume, x);
%       player(y);
%       drawnow limitrate;
%   end
%
%   release(reader);
%   release(player);
%
%   % Example 2: Launch a test bench for the volume and echo objects
%     volume = audiopluginexample.VolumeController;
%     echo = audiopluginexample.Echo;
%     audioTestBench(echo,volume)
%
%   See also: audiopluginexample.Echo

%   Copyright 2015-2023 The MathWorks, Inc.

%#codegen

    properties
        %Gain Output gain (dB)
        %   Specify the gain value in decibels (dB) as a scalar. This value must
        %   be in the range between -120 and 20. The default value is 0 dB.
        Gain (1,1) {mustBeNumeric} = 0

        %Mute Mute output
        %   Specify the Mute value as a logical scalar. This value
        %   must be true or false. The default value is false.
        Mute (1,1) logical = false

        %Balance Left/right balance
        %   Specify the Balance value as a positive scalar. This value must be
        %   in the range between -1 and 1. Negative values set the balance left
        %   by attenuating the right channel. Positive values set the balance
        %   right by attenuating the left channel. The default value is 0.
        Balance (1,1) {mustBeNumeric} = 0

        %TransitionTime Transition time from one parameter to the next (ms)
        %   Specify the transition time from the previous gain and
        %   balance parameters to the new one, as a positive scalar
        %   value in milliseconds. This value must be in the range
        %   between 0 and 200 ms. The default value is 10 ms.
        TransitionTime (1,1) {mustBeNumeric} = 10
    end

    properties (Constant)
        % Define plugin interface to DAW
        PluginInterface = audioPluginInterface( ...
            audioPluginParameter("Mute", DisplayName="Mute", ...
                Mapping={"enum","off","on"}, Style="checkbox", Layout=[1 1]), ...
            audioPluginParameter("Gain", DisplayName="Volume", Label="dB", ...
                DisplayNameLocation="above", Mapping={"pow",1/2,-120,20}, ...
                Style="vslider", Layout=[3 1; 5 1]), ...
            audioPluginParameter("Balance", DisplayName="Balance", ...
                DisplayNameLocation="above", Mapping={"lin",-1,1}, ...
                Style="rotaryknob", Layout=[3 2]), ...
            audioPluginParameter("TransitionTime", DisplayName="Transition Time", ...
                Label="ms", DisplayNameLocation="above", Mapping={"pow",2,0,200}, ...
                Style="rotaryknob", Layout=[5 2]), ...
            audioPluginGridLayout(RowHeight=[20 20 100 20 100], ...
                ColumnWidth=[100 100], Padding=[10 10 10 35]), ...
            BackgroundImage=audiopluginexample.private.mwatlogo, ...
            PluginName="Volume Controller", ...
            VendorName="MathWorks", ...
            VendorVersion="2.0.1", ...
            UniqueId="aVlY", ...
            InputChannels=2, ...
            OutputChannels=2);
    end

    properties (Access=private)
        DampedGain
        DampedBalance
    end

    methods
        function plugin = VolumeController
            plugin.DampedGain = audioexample.DampedParameter(0,10e-3,getSampleRate(plugin));
            plugin.DampedBalance = audioexample.DampedParameter(0,10e-3,getSampleRate(plugin));
        end

        function set.Mute(plugin,isMute)
            plugin.Mute = isMute;
            if isMute
                minGain = audiopluginexample.VolumeController.PluginInterface.Parameters.Gain.Min;
                plugin.DampedGain.Value = minGain; %#ok<*MCSUP>
            else
                plugin.DampedGain.Value = plugin.Gain;
            end
        end

        function set.Gain(plugin,gain)
            plugin.Gain = gain;
            if ~plugin.Mute
                plugin.DampedGain.Value = gain;
            end
        end

        function set.Balance(plugin,bal)
            plugin.Balance = bal;
            plugin.DampedBalance.Value = bal;
        end

        function set.TransitionTime(plugin,period)
            plugin.TransitionTime = period;
            plugin.DampedGain.TransitionTime = period/1000;
            plugin.DampedBalance.TransitionTime = period/1000;
        end

        function y = process(plugin,x)
            filteredGain = getDampedValue(plugin.DampedGain,size(x,1));
            minGainDB = audiopluginexample.VolumeController.PluginInterface.Parameters.Gain.Min;
            if plugin.Mute && all(filteredGain == minGainDB)
                y = zeros(size(x),"like",x);
                return
            end
            
            % Convert the dB gain to linear units and multiply by input
            linGain = repmat(10.^(filteredGain./20),1,size(x,2));
            y = linGain .* x;

            % Balance is only supported for stereo
            if size(x,2) == 2
                filteredBalance = getDampedValue(plugin.DampedBalance,size(x,1));
                if filteredBalance < 0
                    % balance left: reduce right channel
                    y(:,2) = (1+filteredBalance).*y(:,2);
                elseif filteredBalance > 0
                    % balance right: reduce left channel
                    y(:,1) = (1-filteredBalance).*y(:,1);
                end
            end
        end

        function reset(plugin)
            reset(plugin.DampedGain);
            reset(plugin.DampedBalance);
        end

        function s = saveobj(plugin)
            s = saveobj@audioPlugin(plugin);
            s.Gain = plugin.Gain;
            s.Mute = plugin.Mute;
            s.Balance = plugin.Balance;
            s.TransitionTime = plugin.TransitionTime;
            s.DampedGain = saveobj(plugin.DampedGain);
            s.DampedBalance = saveobj(plugin.DampedBalance);
        end

        function plugin = reload(plugin,s)
            plugin = reload@audioPlugin(plugin,s);
            plugin.Gain = s.Gain;
            plugin.Mute = s.Mute;
            plugin.Balance = s.Balance;
            plugin.TransitionTime = s.TransitionTime;
            plugin.DampedGain = audioexample.DampedParameter.loadobj(s.DampedGain);
            plugin.DampedBalance = audioexample.DampedParameter.loadobj(s.DampedBalance);
        end
    end

    methods(Static)
        function plugin = loadobj(s)
            if isstruct(s)
                plugin = audiopluginexample.VolumeController;
                plugin = reload(plugin,s);
            end
        end
    end
end
