classdef BeatDetector < audioPlugin
%BeatDetector Determine BPM of music signal
%   bD = audiopluginexample.BeatDetector returns a basic audio plugin,
%   bD, that estimates and displays a beats per minute (BPM) decision.
%   This is a passthrough plugin (input==output) used for analysis
%   purposes. It is only compatible with mono input.
%
%   BeatDetector methods:
%   process       - Determine BPM of music signal
%   visualize     - Visualize BPM decision
%   reset         - Reset internal states to initial conditions
%   getClickTrack - Return click track of detected onsets since reset
%
%   BeatDetector properties:
%   OnsetDetectionType - Method to detect onsets
%
%
%   % Example 1 -------------------------------------------------------
%   % Open the Audio Test Bench and visualize BPM decision.
%     bD = audiopluginexample.BeatDetector;
%     visualize(bD)
%     audioTestBench(bD)
%
%   % Example 2--------------------------------------------------------
%   % Perform real-time beat detection and then listen to a click track
%   % with onset decisions.
%
%     aFR = dsp.AudioFileReader('FunkyDrums-44p1-stereo-25secs.mp3');
%     bD  = audiopluginexample.BeatDetector( ...
%               'OnsetDetectionType',audioexample.OnsetDetectionEnum.SpectralFlux);
%
%     setSampleRate(bD,aFR.SampleRate);
%     reset(bD);
%     while ~isDone(aFR)
%       audioIn = aFR();
%       process(bD,audioIn(:,1));
%     end
%     release(aFR);
%
%     [audio,fs] = audioread('FunkyDrums-44p1-stereo-25secs.mp3');
%     clickTrack = getClickTrack(bD);
%
%     audio = (audio(:,1)+audio(:,2))/2;
%
%     myMin = min(numel(audio),numel(clickTrack));
%
%     plot(audio(1:myMin));hold on
%     plot(clickTrack(1:myMin),'r');
%
%     sound(audio(1:myMin)+clickTrack(1:myMin),fs);
%
%   See also audiopluginexample.SpeechPitchDetector
 
%   Copyright 2016-2022 The MathWorks, Inc.

%#codegen
    
    %-----------------------------------------------------------------------
    % Public Properties - Tunable properties
    %-----------------------------------------------------------------------
    properties (Access = public)
        %OnsetDetectionType Onset detection type
        %
        %   Specify OnsetDetectionType as
        %   audioexample.OnsetDetectionEnum.SpectralFlux,
        %   audioexample.OnsetDetectionEnum.EnergyFlux, or
        %   audioexample.OnsetDetectionEnum.TimeEnvelope.
        %
        %   Spectral Flux - analyzes the signal in 0.1 second frames
        % with 75% overlap. The spectral history is saved for the last ten
        % analysis frames and used to determine a mean.  The flux
        % is calculated for three frames in the past (to account for changing
        % mean) and if spectral flux is above a given threshold the frame
        % is declared as containing an onset.
        %
        %   Energy Flux - analyzes the signal in 0.01 second frames with
        % 0% overlap using a Hann window. The spectral history
        % is saved for the last one hundred analysis frames and used to
        % determine a mean.  The flux is calculated for three frames in the
        % past (to account for changing mean) and if spectral flux is above
        % a given threshold the frame is declared as containing an onset.
        %
        %   Time Envelope - analyzes the signal in 1 second frames
        % with 0% overlap. The envelope is calculated using a Hilbert
        % transform, and then peaks of the envelope above a relative
        % threshold are declared onsets.
        %
        % The default is audioexample.OnsetDetectionEnum.SpectralFlux.
        % This property is tunable.
        %
        OnsetDetectionType = audioexample.OnsetDetectionEnum.SpectralFlux;
    end
    
    %-----------------------------------------------------------------------
    % Private Properties - Used for internal storage
    %-----------------------------------------------------------------------
    properties (Access = private)
        BPMCache
        MyOnsetDetector
    end

    %-----------------------------------------------------------------------
    % Protected Properties - Used by beat meter visualization
    %-----------------------------------------------------------------------
    properties (Access = protected)
        pElapsedAnalyzedTime = 0;
        Meter
    end
    
    %-----------------------------------------------------------------------
    % Constant Properties - Used to define plugin interface
    %-----------------------------------------------------------------------
    properties (Constant)
        PluginInterface = audioPluginInterface( ...
            audioPluginParameter('OnsetDetectionType', ...
                'DisplayName','Onset Detection Method', ...
                'Mapping',{'enum','Time Envelope','Spectral Flux','Energy Flux'},...
                'Layout', [2 1], 'DisplayNameLocation', 'above'), ...
            'InputChannels',1, ...
            'OutputChannels',1, ...
            'PluginName','Beat Detector',...
            audioPluginGridLayout('RowHeight',[20 30], 'ColumnWidth', 200, ...
            'Padding', [10 10 10 30]),...
            'BackgroundImage', audiopluginexample.private.mwatlogo);
    end

    %-----------------------------------------------------------------------
    % Public Methods - End user interacts with these
    %-----------------------------------------------------------------------
    methods
        %-------------------------------------------------------------------
        % Construct objects used in plugin and set property values
        %-------------------------------------------------------------------
        function plugin = BeatDetector(varargin)
            plugin.MyOnsetDetector = audiopluginexample.private.OnsetDetector;
            for i = 1:2:nargin
                plugin.(varargin{i}) = varargin{i+1};
            end
            plugin.MyOnsetDetector.OnsetDetectionType = plugin.OnsetDetectionType;
            plugin.BPMCache = dsp.AsyncBuffer(100);
            setup(plugin.BPMCache,0);
        end
        
        %-------------------------------------------------------------------
        % Main processing algorithm
        %-------------------------------------------------------------------
        function out = process(plugin,in)
            % PROCESS Determine BPM of music signal
            %
            % audioOut = PROCESS(bD,audioIn) detects signal onsets and
            %   estimates BPM. The audio is passed through the plugin
            %   unchanged (audioIn==audioOut).
            
            % Validate input attributes. In a DAW environment, the DAW is
            % responsible for passing valid inputs. You can comment-out the
            % input validation if generating the plugin.
            validateattributes(in,{'double'},{'column'},'process','audioIn',2)
            
            % Run signal through onset detector
            process(plugin.MyOnsetDetector,in);
            
            % Get onset history
            onsets = plugin.MyOnsetDetector.OnsetHistory;
            
            % Use onset detections to estimate BPM
            BPM = tempoInduction(plugin,onsets);
            
            if isempty(coder.target)
                % Update beat meter visualization
                if ~isempty(plugin.Meter)
                    beatDisplay  = sprintf('%0.1f',BPM);
                    elapsedTime = plugin.MyOnsetDetector.ElapsedAnalyzedTime;
                    aTimeDisplay = char(duration(0,0,elapsedTime,'Format','hh:mm:ss'));
                    update(plugin.Meter,aTimeDisplay,beatDisplay);
                end
            end
            
            out = in;
        end
        
        %-------------------------------------------------------------------
        % Reset memory states and update samplerate-dependent parameters
        %-------------------------------------------------------------------
        function reset(plugin)
            % RESET resets internal states to initial conditions
            %
            % RESET(bD) clears history of onset and BPM tracking, and
            %   resets all states to initial conditions.

            % Set sample rate of OnsetDetector and reset
            setSampleRate(plugin.MyOnsetDetector,getSampleRate(plugin))
            reset(plugin.MyOnsetDetector)
            
            % Reset BPM cache
            reset(plugin.BPMCache);
            
            % Reset visualization
            if isempty(coder.target) && ~isempty(plugin.Meter) && isFigureValid(plugin.Meter)
            	reset(plugin.Meter)
            end
        end
        
        %-------------------------------------------------------------------
        % Visualize 
        %-------------------------------------------------------------------
        function varargout = visualize(plugin)
            % VISUALIZE Visualize BPM decision
            %
            % VISUALIZE(bD) opens a meter that displays the current BPM
            %   estimate and the current analyzed time.
            %
            % VIS = visualize(bD,...) returns a handle to the display
            % figure.
            %
            
            if ~isempty(plugin.Meter) && isvalid(plugin.Meter) && isFigureValid(plugin.Meter)
                show(plugin.Meter);
            else
                plugin.Meter = audiopluginexample.private.MeterUI('Beat Meter','Analyzed Time','BPM');
            end

            if nargout>0
                varargout{1} = getFigureHandle(plugin.Meter);
            end
        end
        function clickTrack = getClickTrack(plugin)
            % GETCLICKTRACK Returns click track of detected onsets since reset
            %
            % clickTrack = getClickTrack(bD)
            %
            
            clickTrack = plugin.MyOnsetDetector.getClickTrack;
        end
        function set.OnsetDetectionType(plugin,type)
            plugin.MyOnsetDetector.OnsetDetectionType = type; %#ok<MCSUP>
        end
        function type = get.OnsetDetectionType(plugin)
            type = plugin.MyOnsetDetector.OnsetDetectionType; 
        end
        function s = saveobj(obj)
            s = saveobj@audioPlugin(obj);
            s.MyOnsetDetector = saveobj(obj.MyOnsetDetector);
            s.BPMCache =  saveobj(clone(obj.BPMCache));
            s.OnsetDetectionType = obj.OnsetDetectionType;
            s.pElapsedAnalyzedTime = obj.pElapsedAnalyzedTime;
        end
        function obj = reload(obj,s)
            obj = reload@audioPlugin(obj,s);
            obj.OnsetDetectionType = s.OnsetDetectionType;   
            obj.pElapsedAnalyzedTime = s.pElapsedAnalyzedTime;
            obj.BPMCache = loadobj(s.BPMCache);
            obj.MyOnsetDetector = audiopluginexample.private.OnsetDetector.loadobj(s.MyOnsetDetector);
        end
    end

    %-----------------------------------------------------------------------
    % Private methods - Used for beat detection functions
    %-----------------------------------------------------------------------
    methods (Access = private)
        function BPM = tempoInduction(plugin,onsets)         
            % Locate valid onsets
            validLocs = ~isnan(onsets);
            
            if sum(validLocs) > 10

                % Convert onsets locations in time to period
                period = diff(onsets(validLocs));
                
                % Take mean of inner quartiles
                periodSorted = sort(period);
                lower25 = round(numel(periodSorted)*0.25);
                upper25 = round(numel(periodSorted)*0.75);
                periodInnerQuartiles = periodSorted(lower25:upper25);
                avgPeriod = mean(periodInnerQuartiles);
                
                % Convert period to BPM
                freq       = 1/avgPeriod;
                BPMCurrent = 60*freq;
                
                % Write current BPM to cache
                write(plugin.BPMCache,BPMCurrent);
                
                % Get current and past 99 BPM decisions and average
                allBPM = read(plugin.BPMCache,100,99);
                BPM = round(mean(allBPM(allBPM~=0)));
            else
                % If no new BPM decision, output average of past 100 BPM decisions.
                allBPM = read(plugin.BPMCache,100,100);
                BPM = round(mean(allBPM(allBPM~=0)));
            end

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