classdef MASSEF < handle
%MASSEF Multichannel audio source separation evaluation framework.
% 
%   The multichannel audio source separation evaluation framework is
%   designed to faciliate the development and evaluation of audio
%   source separation algorithms. Algorithms and estimates are evaluated
%   using a range of metrics, including SNR, STOI, and PEASS. The choice of
%   metrics is configurable. Furthermore, if the algorithm is intended to
%   perform localisation, then this can also be evaluated.
% 
%   The framework can be run in two ways:
%     1) by providing iosr.bss.mixture objects and separation algorithms,
%        or
%     2) providing estimate and true source wav files.
%   
%   If 1), the framework generates the mixture(s), calculates the ideal
%   binary and ratio masks, provides the mixture(s) to the separation
%   algorithm(s), and evaluates the outputs of the separation algorithm(s).
%   The framework also evaluates: the ideal masks for the purposes of
%   comparison, and any azimuth/elevation estimates returned by the
%   algorithm. Use the EXECUTE() method to operate in this mode.
%   
%   If 2), the framework evaluates only the supplied estimate(s) using
%   signal-based metrics. Use the EVALUATE() method to operate in this
%   mode.
% 
%   Sources may have any number of channels; the framework evaluates each
%   channel. The use of iosr.bss.mixture objects facilitate the evaluation
%   of spatialised mixtures (e.g. binaural).
%   
%   Type
% 
%       MASSEF.start
%       MASSEF.doc
% 
%   for more information.
% 
%   MASSEF can be used to evaluate and compare separation algorithms,
%   provided that the algorithms conform to the required format. Consult
%   the help documentation for more information.
% 
%   MASSEF properties:
%       blocksize           - When using the parallel computing toolbox,
%                             this parameter determines the maximum number
%                             of parallel.FevalFuture objects that are
%                             considered at any one time. The default is
%                             128.
%       creationDate        - Date the object was created (read-only).
%       dir                 - The MASSEF installation directory
%                             (read-only).
%       evalPEASS           - A logical value indicating whether PEASS
%                             evaluation should be executed. The default is
%                             false.
%       evalSTOI            - A logical value indicating whether STOI
%                             evaluation should be executed. The default is
%                             false.
%       parpool             - A parallel.Pool object on which to perform 
%                             the parallel separations. If the parallel
%                             computing toolbox is available, MASSEF
%                             will use the current pool by default (as
%                             determined by gcp('nocreate')), if one is
%                             open. If the toolbox is not available, or no
%                             pool is open, separations will be performed
%                             serially.
%       results             - A MASSEFresults object containing results
%                             generated by the framework (read-only).
%       results_filename    - The name of the results file
%                             returned when MASSEF.EXECUTE finishes.
%                             The default is 'Results/results.mat'.
%       saveDate            - Date the object was last saved (read-only).
%   
%   MASSEF methods:
%       MASSEF              - Create an instance of MASSEF.
%       evaluate            - Run the framework using input audio files.
%       execute             - Run the framework using the input mixtures
%                             and separators.
%       save                - Save the framework's data and results.
%     Static methods:
%       doc                 - Display the framework documentation.
%       start               - Start the framework.
%       install             - Download and install MASSEF dependencies.
% 
%   See also IOSR.BSS.MIXTURE, MASSEFRESULTS.

%   Copyright 2016 University of Surrey.
    
    properties
        blocksize        % number of simultaneous parallel.FevalFuture objects
        evalPEASS        % logical flag determing evaluation using PEASS
        evalSTOI         % logical flag determing evaluation using STOI
        parpool          % parallel.pool object on which to perform separations
        results_filename % the name of the results file
    end
    
    properties (Constant)
        dir = fileparts(which(mfilename('fullpath'))) % directory in which this file is stored
    end
    
    properties (SetAccess = private)
        results          % the results data
        creationDate     % date this object was created
        saveDate         % date this object was last saved
    end
    
    properties (Access = private)
        hWaitBar         % handle to waitbar
        iterations       % total iterations
        PEASSoptions     % PEASS options
    end
    
    properties (Constant, Access = private)
        IDEAL = -1         % algorithm number for ideal estimates
    end
    
    methods
        
        % constructor
        
        function obj = MASSEF(options)
        %MASSEF Create an instance of MASSEF
        %   
        %   M = MASSEF instantiates MASSEF, returning an instance to M.
        %   Evaluations are performed using the EXECUTE method.
        % 
        %   M = MASSEF(OPTIONS) instantiates MASSEF using the options
        %   contained in the scalar structure OPTIONS. See MASSEF for a
        %   description of valid fields.
            
            if nargin<1
                options = struct;
            end
        
            options = obj.validate_options(options);
            
            obj.evalPEASS = options.evalPEASS;
            obj.evalSTOI = options.evalSTOI;
            obj.parpool = options.parpool;
            obj.results_filename = options.results_filename;
            obj.blocksize = options.blocksize;
            
            % create empty results object array
            obj.results = MASSEFresults();
            
            obj.creationDate = date;
            
        end
        
        % validate properties
        
        function set.blocksize(obj,val)
            assert(isnumeric(val) && isscalar(val),'MASSEF:blocksize:invalid','blocksize must be a numeric scalar.')
            assert(round(val)==val,'MASSEF:invalidBlocksize','blocksize must be an integer.')
            assert(val>=1,'MASSEF:invalidBlocksize','blocksize must be greater than or equal to 1.')
            obj.blocksize = val;
        end
        
        function set.evalPEASS(obj,val)
            assert(islogical(val) && isscalar(val),'MASSEF:evalPEASS:invalid','evalPEASS must be a logical scalar.')
            obj.evalPEASS = val;
        end
        
        function set.evalSTOI(obj,val)
            assert(islogical(val) && isscalar(val),'MASSEF:evalSTOI:invalid','evalSTOI must be a logical scalar.')
            obj.evalSTOI = val;
        end
        
        function set.parpool(obj,val)
            if ~isempty(val)
                assert(isa(val,'parallel.Pool'),'MASSEF:parpool:invalid','parpool must be a parallel.pool object.')
            end
            obj.parpool = val;
        end
        
        function set.results_filename(obj,val)
            assert(ischar(val),'MASSEF:results_filename:invalid','results_filename must be a char array')
            obj.results_filename = val;
        end
        
        % other methods
        
        function execute(obj,mixtures,separators)
        %EXECUTE Run the framework using the input mixtures and separators
        %   
        %   OBJ.EXECUTE(MIXTURES) runs MASSEF using the array of
        %   iosr.bss.mixture objects MIXTURES and calculates performance
        %   metrics using only ideal estimates (IBM and IRM).
        %   
        %   OBJ.EXECUTE(MIXTURES,SEPARATORS) runs MASSEF using the array of
        %   iosr.bss.mixture objects MIXTURES and the array (or cell array)
        %   of instances of separation algorithms contained in SEPARATORS.
        %   Separation is performed for all combinations of these
        %   variables.
        %         
        %   MASSEF.EXECUTE(MIXTURES,SEPARATORS) runs MASSEF using the
        %   MASSEF instance MASSEF, the array of IOSR.BSS.MIXTURE objects
        %   MIXTURES and the array (or cell array) of instances of
        %   separation algorithms contained in SEPARATORS. Separation is
        %   performed for all combinations of separators and mixtures. The
        %   separation algorithm instances contained in SEPARATORS should
        %   conform to the required format. Type
        % 
        %       MASSEF.doc
        % 
        %   for more information.
        % 
        %   The EXECUTE method performs evaluations of the algorithm
        %   according to the data returned by the algorithm, and the
        %   options provided to MASSEF.
        % 
        %   If the separation algorithm returns a signal, then
        %   MASSEF.EXECUTE evaluates:
        % 
        %       - signal-to-noise ratio (SNR);
        %       - signal-to-ideal-noise ratio (SINR) (the SNR with respect 
        %         to the signal reconstructed using the ideal binary and
        %         ratio masks).
        % 
        %   In addition, if 'evalPEASS' is true, then PEASS and BSSeval
        %   evaluation is performed. If 'evalSTOI' is true, then STOI
        %   evaluation is performed.
        % 
        %   If the separation algorithm returns a mask, and if the ideal
        %   mask dimensions match the estimated mask dimensions, then
        %   MASSEF.EXECUTE evaluates:
        % 
        %       - ideal mask ratio (IMR) with respect to the ideal binary
        %         and ratio masks.
        % 
        %   The EXECUTE method also calculates and evaluates the ideal
        %   binary and ratio masks using the signal-level metrics utilised
        %   for the algorithm evaluation. The masks are calculated using
        %   the settings that are local to each mixture object. The masks
        %   are then resynthesised using the inverse transform. For the
        %   gammatone filterbank, a windowed-sinc function is used for
        %   resynthesis in order to minimise the magnitude and phase
        %   distortion.
        % 
        %   Lastly, the framework captures the estimated azimuth and
        %   elevation of the targets and interferers, if they are returned.
        % 
        %   Once the evaluation is complete, the MASSEF object, which
        %   contains the results data in MASSEF.results as a MASSEFresults
        %   object, is saved to the file given by MASSEF.results_filename.
        
            currDir = pwd;
        
            % check the mixtures
            assert(isa(mixtures,'iosr.bss.mixture'),'MASSEF:execute:invalidMixtures','The MIXTURES input must contain one or more objects of class ''iosr.bss.mixture''.')
            
            if exist('separators','var')~=1
                separators = {};
            end
            
            % check separators have required property and method
            if ~isempty(separators)
                if ~iscell(separators) % force to cell array
                    separators_old = separators;
                    separators = cell(size(separators_old));
                    for s = 1:numel(separators)
                        separators{s} = separators_old(s);
                    end
                end
                for s = 1:numel(separators) % check each separator
                    obj.check_separator(separators{s})
                end
            end

            directory = fileparts(which([mfilename '.m']));
            cd(directory)
            tempdir = [cd filesep 'massef_temp'];
            % ensure temp directory exists:
            if exist(tempdir,'dir')~=7
                success = mkdir(tempdir);
                if ~success
                    error('MASSEF:execute:mkdir','Unable to create directory %s. Please create it.',tempdir)
                end
            end

            %% IVs

            % enumerate combinations of mixtures and separators
            [IVs,obj.iterations] = obj.initialise_IVs(length(mixtures),length(separators));

            %% PEASS options

            obj.PEASSoptions.destDir = [tempdir filesep];
            obj.PEASSoptions.segmentationFactor = 1;
            if exist(obj.PEASSoptions.destDir,'dir')~=7
                mkdir(obj.PEASSoptions.destDir)
            end

            %% Perform separations for each mixture and separator

            % create a waitbar
            obj.hWaitBar = obj.initWaitDisp();

            if ~MASSEF.pctexists() || isempty(obj.parpool) % run in serial linear loop

                fprintf('Performing %d separations serially.\n',obj.iterations)
                nchars = obj.updateWaitDisp(0,0);

                for M = 1:obj.iterations

                    % processing for this iteration
                    sepnum = IVs(M).algo_num;
                    mixnum = IVs(M).mixture_num;
                    if isempty(separators)
                        obj.process(mixtures(mixnum), ...
                            {},mixnum,sepnum,M);
                    else
                        obj.process(mixtures(mixnum), ...
                            separators{sepnum},mixnum,sepnum,M);
                    end

                    % Check to see if the cancel button was pressed
                    if obj.breakWaitDisp()
                        break;
                    end

                    % Update waitbar
                    nchars = obj.updateWaitDisp(M,nchars);
                end

            else % execute asynchronously on parallel workers

                fprintf('Performing %d separations on %d workers.\n',obj.iterations,obj.parpool.NumWorkers)
                nchars = obj.updateWaitDisp(0,0);

                % work on BLOCKSIZE chunks of data at a time
                totalNumCompleted = 0; % overall complete count for display
                N = 1; % block start

                while N <= obj.iterations % work through blocks

                    % do not exceed number of iterations
                    blockMax = min(obj.iterations,N+obj.blocksize-1);

                    % pass iterations to pool
                    for M = blockMax:-1:N % backwards prevents growing the array
                        sepnum = IVs(M).algo_num;
                        mixnum = IVs(M).mixture_num;
                        if isempty(separators)
                            futures(M-N+1) = parfeval(obj.parpool,@obj.process,1,mixtures(mixnum), ...
                                {},mixnum,sepnum,N+M-1);
                        else
                            futures(M-N+1) = parfeval(obj.parpool,@obj.process,1,mixtures(mixnum), ...
                                separators{sepnum},mixnum,sepnum,N+M-1);
                        end
                    end

                    % this cancels futures when cancelFutures is destroyed
                    cancelFutures = onCleanup(@() cancel(futures));

                    % capture data returned by workers
                    numCompleted = 0;
                    while numCompleted < blockMax-N+1

                        % return completed iteration
                        [~,data] = fetchNext(futures); % return data
                        obj.results = [obj.results; data];
                        obj.results = obj.results.merge();
                        numCompleted = numCompleted + 1; % increment local counter
                        totalNumCompleted = totalNumCompleted + 1; % increment total counter

                        % Check to see if the cancel button was pressed
                        br = obj.breakWaitDisp();
                        if br; break; end

                        % Update waitbar
                        nchars = obj.updateWaitDisp(totalNumCompleted,nchars);
                    end

                    % cancel the futures
                    cancel(futures);
                    clear futures;

                    % be sure to break out of this loop too
                    if br; break; end

                    % move to the next data block
                    N = N + obj.blocksize;

                end

            end

            % clean up
            delete(obj.hWaitBar);

            %% Wrap up
            
            obj.results.algorithmInfo(obj.IDEAL,'algorithmLabel','Ideal');
            if ~isempty(separators)
                for n = 1:length(separators)
                    obj.results.algorithmInfo(n,'algorithmLabel',separators{n}.label);
                end
            end
            for n = 1:length(mixtures)
                obj.results.mixtureInfo(n,'azi_sep',mixtures(n).azi_sep, ...
                    'elevation', mixtures(n).elevation, ...
                    'filename_t', mixtures(n).target.filename, ...
                    'filename_i', mixtures(n).int_fns, ...
                    'sofa_path', mixtures(n).sofa_path, ...
                    'target_azi', mixtures(n).target.azimuth, ...
                    'target_ele', mixtures(n).target.elevation, ...
                    'tir', mixtures(n).tir ...
                    );
                mixtures(n).clearCache();
                mixtures(n).deleteFiles();
            end
            
            obj.save();
            save(obj.results_filename,'mixtures','separators','-append');
            
            % delete temporary files
            delete(sprintf('%s*',[obj.PEASSoptions.destDir filesep]))
            
            fprintf('\n')
            cd(currDir);
            disp('MASSEF finished.');
            
        end
        
        function evaluate(obj,originalFiles,estimateFile,tag,mixnum,sepnum,estnum)
        %EVALUATE Run the framework using input audio files
        %   
        %   OBJ.EVALUATE(ORIGINALFILES,ESTIMATEFILE) runs the framework
        %   using the true sources provided in the wav files whose
        %   filenames are contained in the cell array ORIGINALFILES (the
        %   target source is the first one) and the estimate provided in
        %   the wav file with filename ESTIMATEFILE.
        %   
        %   The method may be called as many times as desired. Use
        %   OBJ.SAVE() when finished to save the framework and its data to
        %   a file.
        %   
        %   OBJ.EVALUATE(ORIGINALFILES,ESTIMATEFILE,TAG) writes the char
        %   array TAG to the results data. Use the tag to identify
        %   different estimates in the results data.
        %   
        %   OBJ.EVALUATE(ORIGINALFILES,ESTIMATEFILE,TAG,MIXNUM) uses the
        %   mixture number MIXNUM to identify the separation of a
        %   particular mixture. MIXNUM is a key that can be used with
        %   MASSEFRESULTS.MIXTUREINFO() in order to add information about a
        %   particular mixture.
        %   
        %   OBJ.EVALUATE(ORIGINALFILES,ESTIMATEFILE,TAG,MIXNUM,SEPNUM) uses
        %   the separator number SEPNUM to identify the separation from a
        %   particular algorithm. SEPNUM is a key that can be used with
        %   MASSEFRESULTS.ALGORITHMINFO() in order to add information about a
        %   particular algorithm.
        %   
        %   OBJ.EVALUATE(ORIGINALFILES,ESTIMATEFILE,TAG,MIXNUM,SEPNUM,ESTNUM)
        %   uses the estimate number ESTNUM to identify different estimates
        %   from a given algorithm (e.g. a binary or soft mask output).
        %
        %   See also IOSR.BSS.MASSEFRESULTS.
            
            if exist('tag','var')~=1
                tag = '';
            else
                assert(ischar(tag),'MASSEF:evaluate:invalidTag','TAG must be a char array')
            end
            if exist('mixnum','var')~=1
                mixnum = 1;
            else
                assert(isscalar(mixnum),'MASSEF:evaluate:invalidMixnum','MIXNUM must be a scalar')
            end
            if exist('sepnum','var')~=1
                sepnum = 1;
            else
                assert(isscalar(sepnum),'MASSEF:evaluate:invalidSepnum','SEPNUM must be a scalar')
            end
            if exist('estnum','var')~=1
                estnum = 1;
            else
                assert(isscalar(estnum),'MASSEF:evaluate:invalidEstnum','ESTNUM must be a scalar')
            end
            
            [estimate,fs] = audioread(estimateFile);
            target = audioread(originalFiles{1});
            interferers = zeros(1,size(target,2));
            for i = 2:numel(originalFiles)
                int = audioread(originalFiles{i});
                newlength = max(length(int),length(interferers));
                int = obj.setlength(int,newlength);
                interferers = obj.setlength(interferers,newlength);
                interferers = interferers + int;
            end
            
            for C = 1:size(target,2) % iterate through each channel
                        
                % SNR
                snr = iosr.bss.calcSnr(estimate(:,min(C,size(estimate,2))),target(:,C));
                obj.results.input(mixnum,sepnum,estnum,'SNR',C,tag,snr);
                
                % STOI
                if obj.evalSTOI
                    stoi = taal2011(target(:,C), estimate(:,min(C,size(estimate,2))), fs);
                    obj.results.input(mixnum,sepnum,estnum,'STOI',C,tag,stoi);
                end
            end

            % PEASS
            if obj.evalPEASS
                obj.evaluatePEASS(estimateFile,originalFiles, ...
                    mixnum,sepnum,estnum,1,tag);
            end
            
        end
        
        function save(obj)
        %SAVE Save the framework's data and results
        %   
        %   OBJ.SAVE() save the MASSEF framework object to the file
        %   determined by OBJ.RESULTS_FILENAME. The method is called
        %   automatically when using the EXECUTE() method; in this case,
        %   the mixtures and separators are also saved.
        
            % Create results files
            if exist(obj.results_filename,'file')==2
                % do not overwrite
                newfilename = obj.results_filename;
                while exist(newfilename,'file')==2
                    fprintf('\n')
                    newfilename = input(...
                        ['The results file ''' newfilename ''' already exists.\nPlease enter a new filename:\n'],'s');
                end
                obj.results_filename = newfilename;
            end
            obj.saveDate = date;
            save(obj.results_filename,'obj');
            fprintf('\n')
            disp(['File saved to: ' obj.results_filename]);
            
        end
        
    end % public methods
    
    methods (Static)
        
        function doc
        %DOC Display the framework documentation.
            try
                web(fullfile(MASSEF.dir, 'help_html', 'help_Index.html'), '-helpbrowser')
            catch
                web(fullfile(MASSEF.dir, 'help_html', 'help_Index.html'))
            end
        end
        
        function start
        %START Start the framework.
        %
        %   MASSEF.START starts the framework and its dependencies,
        %   adds the required folders to the Matlab path, and updates the
        %   HTML help documentation.

            addpath(cd,...
                [MASSEF.dir filesep 'Library'],...
                [MASSEF.dir filesep 'Utilities'],...
                [MASSEF.dir filesep 'Utilities' filesep 'AMT'],...
                [MASSEF.dir filesep 'Utilities' filesep 'PEASS'],...
                [MASSEF.dir filesep 'Utilities' filesep 'PEASS' filesep 'gammatone'],...
                [MASSEF.dir filesep 'Stimuli']);

            SOFAstart(0);
            amtstart;
            
            d = pwd;
            cd([MASSEF.dir filesep 'help_html' filesep 'source'])
            publishHelp;
            cd(d);
            
        end
        
        function install
        %INSTALL Download and install MASSEF dependencies.
        % 
        %   MASSEF.INSTALL downloads and installs the MASSEF
        %   dependencies. These dependencies are:
        %       - The Auditory Modelling Toolbox
        %         (http://amtoolbox.sourceforge.net);
        %       - The Large Time-Frequency Analysis Toolbox
        %         (http://ltfat.sourceforge.net);
        %       - Perceptual Evaluation methods for Audio Source Separation
        %         Toolkit (http://bass-db.gforge.inria.fr/peass/);
        %       - IoSR Matlab Toolbox
        %         (https://github.com/IoSR-Surrey/MatlabToolbox)
        %       - SOFA API
        %         (https://sourceforge.net/projects/sofacoustics/).
            
            currDir = pwd;
            directory = MASSEF.dir;
            cd(directory);

            %% download and install STOI

            amt_folder = [directory filesep 'Utilities' filesep 'AMT'];
            ltfat_folder = [directory filesep 'Utilities' filesep 'AMT' filesep 'thirdparty' filesep 'ltfat'];

            if ~(exist(amt_folder, 'dir') == 7)   
                % AMT
                amt_filename = 'amtoolbox-0.9.7.zip';
                websave(amt_filename,'http://vorboss.dl.sourceforge.net/project/amtoolbox/amtoolbox-0.9.7.zip');
                unzip(amt_filename,amt_folder);
                amt_temp_folder = [amt_folder filesep 'release' filesep];
                movefile([amt_temp_folder '*'],[amt_folder filesep]);
                delete(amt_filename)
                rmdir(amt_temp_folder,'s')

                % LTFAT
                ltfat_filename = 'ltfat-2.1.2.tgz';
                websave(ltfat_filename,'http://netix.dl.sourceforge.net/project/ltfat/ltfat/2.0/ltfat-2.1.2.tgz');
                untar(ltfat_filename,ltfat_folder);
                ltfat_temp_folder = [ltfat_folder filesep 'ltfat' filesep];
                movefile([ltfat_temp_folder '*'],[ltfat_folder filesep]);
                delete(ltfat_filename)
                rmdir(ltfat_temp_folder,'s')
            else
                display(strcat('Found existing AMT Toolbox directory: ', amt_folder))
            end

            %% download and install PEASS

            % install dir
            peass_folder = [directory filesep 'Utilities' filesep 'PEASS'];

            % PEASS
            if ~(exist(peass_folder, 'dir') == 7)
                peass_filename = 'PEASS-Software-v2.0.zip';
                websave(peass_filename,'http://bass-db.gforge.inria.fr/peass/PEASS-Software-v2.0.zip');
                unzip(peass_filename,peass_folder);
                peass_temp_folder = [peass_folder filesep 'PEASS-Software-v2.0' filesep];
                movefile([peass_temp_folder '*'],[peass_folder filesep]);

                % clean up
                delete(peass_filename)
                rmdir(peass_temp_folder,'s')
            else
                display(strcat('Found existing PEASS directory: ', peass_folder))
            end
            % Hair cell model
            adapt_filename = 'adapt_loop.zip';
            websave(adapt_filename,'http://medi.uni-oldenburg.de/download/demo/adaption-loops/adapt_loop.zip');
            unzip(adapt_filename,peass_folder);
            cd(peass_folder);
            compile;
            cd(directory);
            
            %% download and install IoSR Matlab Toolbox
            
            if ~MASSEF.gitExists
                % IoSR Matlab Toolbox
                if ~(exist([directory filesep 'Library' filesep '+iosr'], 'dir') == 7)
                    iosrMTB = 'iosr.zip';
                    iosr_dir = [directory filesep 'Library'];
                    websave(iosrMTB,'https://github.com/IoSR-Surrey/MatlabToolbox/archive/master.zip');
                    unzip(iosrMTB,iosr_dir);
                    movefile([iosr_dir filesep 'MatlabToolbox-master' filesep '*'],[iosr_dir filesep]);
                    delete(iosrMTB)
                    rmdir([iosr_dir filesep 'MatlabToolbox-master'],'s')
                else
                    display('Found existing IoSR Matlab Toolbox directory')
                end
            end
            
            iosr.install;

            %% Remaining clean up

            delete(adapt_filename)
            cd(currDir);
            disp('MASSEF successfully installed.')

        end
        
    end % Static public methods
    
    methods (Static, Hidden)
        
        function e = gitExists
        %GITEXISTS Check if install is a git repo
            e = exist([MASSEF.dir filesep '.git'],'dir')==7;
        end
        
    end
    
    methods (Access = private)
        
        function r = process(obj,mix,separator,mixnum,sepnum,iteration)
        %PROCESS Main callback for performing the separation and analysis

            %% Create mixture

            % write mixture, target and interferers
            mix.write(sprintf('%smix-%d.wav',obj.PEASSoptions.destDir,mixnum));
            originalFiles = {mix.filename_t, mix.filename_i};

            % create mixture signal
            mixture = mix.signal;
            
            %% Analyse separator
            
            % calculate ideal outputs
            ibm = mix.ibm;
            irm = mix.irm;
            output_ibm = mix.applyMask(ibm);
            output_irm = mix.applyMask(irm);
            
            if ~isempty(separator)
            
                % calculate separator output
                [signals,masks,est_azis,est_eles] = separator.separate(mixture);

                % apply mask if signals are not calculated
                if isempty(signals) && ~isempty(masks)
                    for E = 1:size(masks,4)
                        signals(:,:,E) = mix.applyMask(masks(:,:,:,E));
                    end
                end

                % Evaluate signals
                if ~isempty(signals)
                    for E = 1:size(signals,3)

                        estimateFileBase = sprintf('%ssignal-%d',obj.PEASSoptions.destDir,iteration);
                        estimateFile = sprintf('%s.wav',estimateFileBase);
                        audiowrite(estimateFile,iosr.dsp.audio.normalize(signals(:,:,E)),mix.fs);

                        obj.evaluate(originalFiles,estimateFile,...
                            separator.estTag{E},mixnum,sepnum,E);

                        % SINR
                        for C = 1:size(mixture,2)
                            sinr = iosr.bss.calcSnr(signals(:,C,E),output_irm(:,C));
                            obj.results.input(mixnum,sepnum,E,'SINR',C,separator.estTag{E},sinr)
                        end

                        % delete temp files
                        delete(estimateFile);

                    end

                end

                % estimated azimuths
                if ~isempty(est_azis)
                    for s = 1:length(est_azis)
                        if s==1
                            obj.results.input(mixnum,sepnum,1,'est_azi_target',1,[],est_azis(s))
                        else
                            obj.results.input(mixnum,sepnum,1,sprintf('est_azi_int_%02d',s-1),1,[],est_azis(s))
                        end
                    end
                end

                % estimated elevation
                if ~isempty(est_eles)
                    for s = 1:length(est_eles)
                        if s==1
                            obj.results.input(mixnum,sepnum,1,'est_ele_target',1,[],est_eles(s))
                        else
                            obj.results.input(mixnum,sepnum,1,sprintf('est_ele_int_%02d',s-1),1,[],est_eles(s))
                        end
                    end
                end

                % only do mask comparisons when the masks are equal size
                masks_size = size(masks);
                ideal_size = size(ibm);
                if isequal(masks_size(1:2),ideal_size(1:2))
                    for E = 1:size(masks,4) % iterature through each estimate/mask
                        for C = 1:size(mixture,2) % iterate through each channel
                            maskC = masks(:,:,min(C,size(masks,3)),E);

                            % IMR - binary
                            imrb = iosr.bss.calcImr(maskC,ibm(:,:,C));
                            obj.results.input(mixnum,sepnum,E,'IMR (IBM)',C,separator.estTag{E},imrb)

                            % IMR - ratio
                            imrr = iosr.bss.calcImr(maskC,irm(:,:,C));
                            obj.results.input(mixnum,sepnum,E,'IMR (IRM)',C,separator.estTag{E},imrr)
                        end

                    end

                end
                
            end
            
            %% Analyse ideal masks

            if sepnum==1 % evaluations of ideal masks
                
                ibmFile = sprintf('%sibm-%d.wav',obj.PEASSoptions.destDir,iteration);
                audiowrite(ibmFile,iosr.dsp.audio.normalize(output_ibm),mix.fs);
                irmFile = sprintf('%sirm-%d.wav',obj.PEASSoptions.destDir,iteration);
                audiowrite(irmFile,iosr.dsp.audio.normalize(output_irm),mix.fs);
                
                obj.evaluate(originalFiles,ibmFile,...
                        'Binary',mixnum,obj.IDEAL,1);
                obj.evaluate(originalFiles,irmFile,...
                        'Ratio',mixnum,obj.IDEAL,2);
                    
                % delete temp files
                delete(ibmFile);
                delete(irmFile);
            end
            
            r = obj.results;

        end
        
        function evaluatePEASS(obj,estimateFile,originalfiles, ...
            mixnum,sepnum,estnum,channelnum,esttag)
        %EVALUATE Evaluate resynthesised signals against ground truth

            % calculate metrics
            PEASS_output = PEASS_ObjectiveMeasure(originalfiles,estimateFile,obj.PEASSoptions);

            % write data to outputs
            obj.results.input(mixnum,sepnum,estnum,'OPS',channelnum,esttag,PEASS_output.OPS)
            obj.results.input(mixnum,sepnum,estnum,'TPS',channelnum,esttag,PEASS_output.TPS)
            obj.results.input(mixnum,sepnum,estnum,'IPS',channelnum,esttag,PEASS_output.IPS)
            obj.results.input(mixnum,sepnum,estnum,'APS',channelnum,esttag,PEASS_output.APS)
            obj.results.input(mixnum,sepnum,estnum,'SDR',channelnum,esttag,PEASS_output.SDR)
            obj.results.input(mixnum,sepnum,estnum,'SAR',channelnum,esttag,PEASS_output.SAR)
            obj.results.input(mixnum,sepnum,estnum,'SIR',channelnum,esttag,PEASS_output.SIR)
            obj.results.input(mixnum,sepnum,estnum,'ISR',channelnum,esttag,PEASS_output.ISR)
            
            [pathstr,name] = fileparts(estimateFile);
            delete(fullfile(pathstr,[name '_true.wav']));
            delete(fullfile(pathstr,[name '_eTarget.wav']));
            delete(fullfile(pathstr,[name '_eInterf.wav']));
            delete(fullfile(pathstr,[name '_eArtif.wav']));

        end
        
        function nchars = updateWaitDisp(obj,numComplete,nchars)
        %UPDATEWAITDISP Update the wait display

            fraction = numComplete/obj.iterations;

            if ~isempty(obj.hWaitBar) % update waitbar if it exists
                waitbar(fraction, obj.hWaitBar);
            end
            fprintf(1,repmat('\b',1,nchars)); % delete old message
            if numComplete==obj.iterations
                format = '%.0f'; % special format for 100%
            else
                format = '%.1f'; % normal format
            end

            % display message and return number of chars to delete for next message
            nchars = fprintf(1,[format '%% complete.'],100*fraction);

        end

        function b = breakWaitDisp(obj)
        %BREAKWAITDISP Determine whether to break the calculations

            b = false; % don't by default
            if ~isempty(obj.hWaitBar) % if waitbar exists
                if getappdata(obj.hWaitBar, 'Cancelled') % determine if "Cancel" was clicked
                    b = true; % break if so
                end
            end
            if b % display message if breaking
                fprintf('\nMASSEF cancelled.\n');
            end

        end
        
    end % private methods
    
    methods (Static, Access = private)
        
        function options = validate_options(options)
        %VALIDATE_OPTIONS Validate the options structure input to this function

            % check format
            if numel(options)>1
                error('MASSEF:validate_options:invalidInput','The input to MASSEF should be a 1x1 structure. Cell-array fields should be encapsulated within a single cell.')
            end

            fields_in = fieldnames(options);

            % default values
            if MASSEF.pctexists()
                default_parpool = gcp('nocreate');
            else
                default_parpool = [];
            end
            default_values = struct( ...
                'evalPEASS',false, ...
                'evalSTOI',false, ...
                'parpool',default_parpool, ...
                'results_filename',[MASSEF.dir filesep 'Results' filesep 'results.mat'], ...
                'blocksize',128 ...
            );
            def_fields = fieldnames(default_values);

            % check for invalid options
            for r = 1:length(fields_in)
                assert(any(strcmpi(fields_in{r},def_fields)),'MASSEF:validate_options:invalidOption',['Invalid option ''' fields_in{r} ''' specified.'])
            end

            % write defaults
            for r = 1:numel(def_fields)
                field_name = def_fields{r};
                if ~isfield(options,field_name)
                    options.(field_name) = default_values.(field_name);
                end
            end

        end
        
        function [IVs,iterations] = initialise_IVs(numMixtures,numSeparators)
        %INITIALISE_IVs Initialise experiment results structure

            % Independent variables space
            IV_space = [numMixtures max(1,numSeparators)];

            iterations = prod(IV_space); % total number of results
            assert(numMixtures>0,'MASSEF:initialise_IVs:noMixtures','No mixtures have been specified')
            %assert(numSeparators>0,'No separators have been specified')

            IVs = struct('algo_num',[],'mixture_num',[]);

            % fill IV cells with data
            for N = 1:iterations
                [n,p] = ind2sub(IV_space,N);
                IVs(N).algo_num = p;
                IVs(N).mixture_num = n;
            end
        end
        
        function check_separator(obj)
        %CHECK_SEPARATOR Check separator meets requirements

            assert(isprop(obj,'label'),'MASSEF:check_separator:invalidSepNoLabel',['The object of class ''' class(obj) ''' does not have the required property ''label'''])
            assert(ischar(obj.label),'MASSEF:check_separator:invalidSepLabelProp',['The ''label'' property of the ''' class(obj) ''' object should return a char array'])
            assert(isprop(obj,'estTag'),'MASSEF:check_separator:invalidSepNoEstTag',['The object of class ''' class(obj) ''' does not have the required property ''estTag'''])
            assert(iscellstr(obj.estTag),'MASSEF:check_separator:invalidSepEstTagProp',['The ''estTag'' property of the ''' class(obj) ''' object should return a cell array of strings'])
            assert(ismethod(obj,'separate'),'MASSEF:check_separator:invalidSepNoSepMethod',['The object of class ''' class(obj) ''' does not have the required method ''separate'''])

        end
        
        function e = pctexists
        %PCTEXISTS Determine whether the parallel computing toolbox exists

            if isempty(ver('distcomp'))
                e = false;
            else
                e = true;
            end

        end

        function h = initWaitDisp
        %INITWAITDISP Initialise the waiting display

            if usejava('jvm') % only call waitbar if JVM is running
                h = waitbar(0, 'MASSEF processing...', 'CreateCancelBtn', ...
                    @(src, event) setappdata(gcbf(), 'Cancelled', true));
                setappdata(h, 'Cancelled', false);
            else % do nothing
                h = [];
            end
        end
        
        function y = setlength(x,signal_length)
        %SETLENGTH Crop or zero-pad signal to specified length
            
            d = size(x);
            if size(x,1)>signal_length % need to crop
                subsidx = [{1:signal_length} repmat({':'},1,ndims(x)-1)];
                y = x(subsidx{:});
            elseif size(x,1)<signal_length % need to zero-pad
                y = [x; zeros([signal_length-size(x,1),d(2:end)])];
            else % do nothing
                y = x;
            end
            
        end
        
    end % static private methods
    
end
