c@0: classdef MASSEF < handle c@0: %MASSEF Multichannel audio source separation evaluation framework. c@0: % c@0: % The multichannel audio source separation evaluation framework is c@0: % designed to faciliate the development and evaluation of audio c@0: % source separation algorithms. Algorithms and estimates are evaluated c@0: % using a range of metrics, including SNR, STOI, and PEASS. The choice of c@0: % metrics is configurable. Furthermore, if the algorithm is intended to c@0: % perform localisation, then this can also be evaluated. c@0: % c@0: % The framework can be run in two ways: c@0: % 1) by providing iosr.bss.mixture objects and separation algorithms, c@0: % or c@0: % 2) providing estimate and true source wav files. c@0: % c@0: % If 1), the framework generates the mixture(s), calculates the ideal c@0: % binary and ratio masks, provides the mixture(s) to the separation c@0: % algorithm(s), and evaluates the outputs of the separation algorithm(s). c@0: % The framework also evaluates: the ideal masks for the purposes of c@0: % comparison, and any azimuth/elevation estimates returned by the c@0: % algorithm. Use the EXECUTE() method to operate in this mode. c@0: % c@0: % If 2), the framework evaluates only the supplied estimate(s) using c@0: % signal-based metrics. Use the EVALUATE() method to operate in this c@0: % mode. c@0: % c@0: % Sources may have any number of channels; the framework evaluates each c@0: % channel. The use of iosr.bss.mixture objects facilitate the evaluation c@0: % of spatialised mixtures (e.g. binaural). c@0: % c@0: % Type c@0: % c@0: % MASSEF.start c@12: % MASSEF.doc c@0: % c@12: % for more information. c@0: % c@0: % MASSEF can be used to evaluate and compare separation algorithms, c@0: % provided that the algorithms conform to the required format. Consult c@0: % the help documentation for more information. c@0: % c@0: % MASSEF properties: c@0: % blocksize - When using the parallel computing toolbox, c@0: % this parameter determines the maximum number c@0: % of parallel.FevalFuture objects that are c@0: % considered at any one time. The default is c@0: % 128. c@0: % creationDate - Date the object was created (read-only). c@0: % dir - The MASSEF installation directory c@0: % (read-only). c@0: % evalPEASS - A logical value indicating whether PEASS c@0: % evaluation should be executed. The default is c@0: % false. c@0: % evalSTOI - A logical value indicating whether STOI c@0: % evaluation should be executed. The default is c@0: % false. c@0: % parpool - A parallel.Pool object on which to perform c@0: % the parallel separations. If the parallel c@0: % computing toolbox is available, MASSEF c@0: % will use the current pool by default (as c@0: % determined by gcp('nocreate')), if one is c@0: % open. If the toolbox is not available, or no c@0: % pool is open, separations will be performed c@0: % serially. c@0: % results - A MASSEFresults object containing results c@0: % generated by the framework (read-only). c@0: % results_filename - The name of the results file c@0: % returned when MASSEF.EXECUTE finishes. c@0: % The default is 'Results/results.mat'. c@0: % saveDate - Date the object was last saved (read-only). c@0: % c@0: % MASSEF methods: c@0: % MASSEF - Create an instance of MASSEF. c@0: % evaluate - Run the framework using input audio files. c@0: % execute - Run the framework using the input mixtures c@0: % and separators. c@0: % save - Save the framework's data and results. c@0: % Static methods: c@12: % doc - Display the framework documentation. c@0: % start - Start the framework. c@0: % install - Download and install MASSEF dependencies. c@0: % c@0: % See also IOSR.BSS.MIXTURE, MASSEFRESULTS. c@0: c@0: % Copyright 2016 University of Surrey. c@0: c@0: properties c@0: blocksize % number of simultaneous parallel.FevalFuture objects c@0: evalPEASS % logical flag determing evaluation using PEASS c@0: evalSTOI % logical flag determing evaluation using STOI c@0: parpool % parallel.pool object on which to perform separations c@0: results_filename % the name of the results file c@0: end c@0: c@0: properties (Constant) c@0: dir = fileparts(which(mfilename('fullpath'))) % directory in which this file is stored c@0: end c@0: c@0: properties (SetAccess = private) c@0: results % the results data c@0: creationDate % date this object was created c@0: saveDate % date this object was last saved c@0: end c@0: c@0: properties (Access = private) c@0: hWaitBar % handle to waitbar c@0: iterations % total iterations c@0: PEASSoptions % PEASS options c@0: end c@0: c@0: properties (Constant, Access = private) c@0: IDEAL = -1 % algorithm number for ideal estimates c@0: end c@0: c@0: methods c@0: c@0: % constructor c@0: c@0: function obj = MASSEF(options) c@0: %MASSEF Create an instance of MASSEF c@0: % c@0: % M = MASSEF instantiates MASSEF, returning an instance to M. c@0: % Evaluations are performed using the EXECUTE method. c@0: % c@0: % M = MASSEF(OPTIONS) instantiates MASSEF using the options c@0: % contained in the scalar structure OPTIONS. See MASSEF for a c@0: % description of valid fields. c@0: c@0: if nargin<1 c@0: options = struct; c@0: end c@0: c@0: options = obj.validate_options(options); c@0: c@0: obj.evalPEASS = options.evalPEASS; c@0: obj.evalSTOI = options.evalSTOI; c@0: obj.parpool = options.parpool; c@0: obj.results_filename = options.results_filename; c@0: obj.blocksize = options.blocksize; c@0: c@0: % create empty results object array c@0: obj.results = MASSEFresults(); c@0: c@0: obj.creationDate = date; c@0: c@0: end c@0: c@0: % validate properties c@0: c@0: function set.blocksize(obj,val) c@23: assert(isnumeric(val) && isscalar(val),'MASSEF:blocksize:invalid','blocksize must be a numeric scalar.') c@23: assert(round(val)==val,'MASSEF:invalidBlocksize','blocksize must be an integer.') c@23: assert(val>=1,'MASSEF:invalidBlocksize','blocksize must be greater than or equal to 1.') c@0: obj.blocksize = val; c@0: end c@0: c@0: function set.evalPEASS(obj,val) c@23: assert(islogical(val) && isscalar(val),'MASSEF:evalPEASS:invalid','evalPEASS must be a logical scalar.') c@0: obj.evalPEASS = val; c@0: end c@0: c@0: function set.evalSTOI(obj,val) c@23: assert(islogical(val) && isscalar(val),'MASSEF:evalSTOI:invalid','evalSTOI must be a logical scalar.') c@0: obj.evalSTOI = val; c@0: end c@0: c@0: function set.parpool(obj,val) c@0: if ~isempty(val) c@23: assert(isa(val,'parallel.Pool'),'MASSEF:parpool:invalid','parpool must be a parallel.pool object.') c@0: end c@0: obj.parpool = val; c@0: end c@0: c@0: function set.results_filename(obj,val) c@23: assert(ischar(val),'MASSEF:results_filename:invalid','results_filename must be a char array') c@0: obj.results_filename = val; c@0: end c@0: c@0: % other methods c@0: c@0: function execute(obj,mixtures,separators) c@0: %EXECUTE Run the framework using the input mixtures and separators c@0: % c@0: % OBJ.EXECUTE(MIXTURES) runs MASSEF using the array of c@0: % iosr.bss.mixture objects MIXTURES and calculates performance c@0: % metrics using only ideal estimates (IBM and IRM). c@0: % c@0: % OBJ.EXECUTE(MIXTURES,SEPARATORS) runs MASSEF using the array of c@0: % iosr.bss.mixture objects MIXTURES and the array (or cell array) c@0: % of instances of separation algorithms contained in SEPARATORS. c@0: % Separation is performed for all combinations of these c@0: % variables. c@0: % c@0: % MASSEF.EXECUTE(MIXTURES,SEPARATORS) runs MASSEF using the c@0: % MASSEF instance MASSEF, the array of IOSR.BSS.MIXTURE objects c@0: % MIXTURES and the array (or cell array) of instances of c@0: % separation algorithms contained in SEPARATORS. Separation is c@0: % performed for all combinations of separators and mixtures. The c@0: % separation algorithm instances contained in SEPARATORS should c@0: % conform to the required format. Type c@0: % c@12: % MASSEF.doc c@0: % c@12: % for more information. c@0: % c@0: % The EXECUTE method performs evaluations of the algorithm c@0: % according to the data returned by the algorithm, and the c@0: % options provided to MASSEF. c@0: % c@0: % If the separation algorithm returns a signal, then c@0: % MASSEF.EXECUTE evaluates: c@0: % c@0: % - signal-to-noise ratio (SNR); c@0: % - signal-to-ideal-noise ratio (SINR) (the SNR with respect c@0: % to the signal reconstructed using the ideal binary and c@0: % ratio masks). c@0: % c@0: % In addition, if 'evalPEASS' is true, then PEASS and BSSeval c@0: % evaluation is performed. If 'evalSTOI' is true, then STOI c@0: % evaluation is performed. c@0: % c@0: % If the separation algorithm returns a mask, and if the ideal c@0: % mask dimensions match the estimated mask dimensions, then c@0: % MASSEF.EXECUTE evaluates: c@0: % c@0: % - ideal mask ratio (IMR) with respect to the ideal binary c@0: % and ratio masks. c@0: % c@0: % The EXECUTE method also calculates and evaluates the ideal c@0: % binary and ratio masks using the signal-level metrics utilised c@0: % for the algorithm evaluation. The masks are calculated using c@0: % the settings that are local to each mixture object. The masks c@0: % are then resynthesised using the inverse transform. For the c@0: % gammatone filterbank, a windowed-sinc function is used for c@0: % resynthesis in order to minimise the magnitude and phase c@0: % distortion. c@0: % c@0: % Lastly, the framework captures the estimated azimuth and c@0: % elevation of the targets and interferers, if they are returned. c@0: % c@0: % Once the evaluation is complete, the MASSEF object, which c@0: % contains the results data in MASSEF.results as a MASSEFresults c@0: % object, is saved to the file given by MASSEF.results_filename. c@0: c@35: currDir = pwd; c@35: c@0: % check the mixtures c@23: assert(isa(mixtures,'iosr.bss.mixture'),'MASSEF:execute:invalidMixtures','The MIXTURES input must contain one or more objects of class ''iosr.bss.mixture''.') c@0: c@0: if exist('separators','var')~=1 c@0: separators = {}; c@0: end c@0: c@0: % check separators have required property and method c@0: if ~isempty(separators) c@0: if ~iscell(separators) % force to cell array c@0: separators_old = separators; c@0: separators = cell(size(separators_old)); c@0: for s = 1:numel(separators) c@0: separators{s} = separators_old(s); c@0: end c@0: end c@0: for s = 1:numel(separators) % check each separator c@0: obj.check_separator(separators{s}) c@0: end c@0: end c@0: c@0: directory = fileparts(which([mfilename '.m'])); c@0: cd(directory) c@35: tempdir = [cd filesep 'massef_temp']; c@0: % ensure temp directory exists: c@0: if exist(tempdir,'dir')~=7 c@0: success = mkdir(tempdir); c@0: if ~success c@23: error('MASSEF:execute:mkdir','Unable to create directory %s. Please create it.',tempdir) c@0: end c@0: end c@0: c@0: %% IVs c@0: c@0: % enumerate combinations of mixtures and separators c@0: [IVs,obj.iterations] = obj.initialise_IVs(length(mixtures),length(separators)); c@0: c@0: %% PEASS options c@0: c@0: obj.PEASSoptions.destDir = [tempdir filesep]; c@0: obj.PEASSoptions.segmentationFactor = 1; c@0: if exist(obj.PEASSoptions.destDir,'dir')~=7 c@0: mkdir(obj.PEASSoptions.destDir) c@0: end c@0: c@0: %% Perform separations for each mixture and separator c@0: c@0: % create a waitbar c@0: obj.hWaitBar = obj.initWaitDisp(); c@0: c@0: if ~MASSEF.pctexists() || isempty(obj.parpool) % run in serial linear loop c@0: c@0: fprintf('Performing %d separations serially.\n',obj.iterations) c@0: nchars = obj.updateWaitDisp(0,0); c@0: c@0: for M = 1:obj.iterations c@0: c@0: % processing for this iteration c@0: sepnum = IVs(M).algo_num; c@0: mixnum = IVs(M).mixture_num; c@0: if isempty(separators) c@0: obj.process(mixtures(mixnum), ... c@0: {},mixnum,sepnum,M); c@0: else c@0: obj.process(mixtures(mixnum), ... c@0: separators{sepnum},mixnum,sepnum,M); c@0: end c@0: c@0: % Check to see if the cancel button was pressed c@0: if obj.breakWaitDisp() c@0: break; c@0: end c@0: c@0: % Update waitbar c@0: nchars = obj.updateWaitDisp(M,nchars); c@0: end c@0: c@0: else % execute asynchronously on parallel workers c@0: c@0: fprintf('Performing %d separations on %d workers.\n',obj.iterations,obj.parpool.NumWorkers) c@0: nchars = obj.updateWaitDisp(0,0); c@0: c@0: % work on BLOCKSIZE chunks of data at a time c@0: totalNumCompleted = 0; % overall complete count for display c@0: N = 1; % block start c@0: c@0: while N <= obj.iterations % work through blocks c@0: c@0: % do not exceed number of iterations c@0: blockMax = min(obj.iterations,N+obj.blocksize-1); c@0: c@0: % pass iterations to pool c@0: for M = blockMax:-1:N % backwards prevents growing the array c@0: sepnum = IVs(M).algo_num; c@0: mixnum = IVs(M).mixture_num; c@0: if isempty(separators) c@0: futures(M-N+1) = parfeval(obj.parpool,@obj.process,1,mixtures(mixnum), ... c@0: {},mixnum,sepnum,N+M-1); c@0: else c@0: futures(M-N+1) = parfeval(obj.parpool,@obj.process,1,mixtures(mixnum), ... c@0: separators{sepnum},mixnum,sepnum,N+M-1); c@0: end c@0: end c@0: c@0: % this cancels futures when cancelFutures is destroyed c@0: cancelFutures = onCleanup(@() cancel(futures)); c@0: c@0: % capture data returned by workers c@0: numCompleted = 0; c@0: while numCompleted < blockMax-N+1 c@0: c@0: % return completed iteration c@0: [~,data] = fetchNext(futures); % return data c@0: obj.results = [obj.results; data]; c@0: obj.results = obj.results.merge(); c@0: numCompleted = numCompleted + 1; % increment local counter c@0: totalNumCompleted = totalNumCompleted + 1; % increment total counter c@0: c@0: % Check to see if the cancel button was pressed c@0: br = obj.breakWaitDisp(); c@0: if br; break; end c@0: c@0: % Update waitbar c@0: nchars = obj.updateWaitDisp(totalNumCompleted,nchars); c@0: end c@0: c@0: % cancel the futures c@0: cancel(futures); c@0: clear futures; c@0: c@0: % be sure to break out of this loop too c@0: if br; break; end c@0: c@0: % move to the next data block c@0: N = N + obj.blocksize; c@0: c@0: end c@0: c@0: end c@0: c@0: % clean up c@0: delete(obj.hWaitBar); c@0: c@0: %% Wrap up c@0: c@0: obj.results.algorithmInfo(obj.IDEAL,'algorithmLabel','Ideal'); c@0: if ~isempty(separators) c@0: for n = 1:length(separators) c@0: obj.results.algorithmInfo(n,'algorithmLabel',separators{n}.label); c@0: end c@0: end c@0: for n = 1:length(mixtures) c@0: obj.results.mixtureInfo(n,'azi_sep',mixtures(n).azi_sep, ... c@0: 'elevation', mixtures(n).elevation, ... c@0: 'filename_t', mixtures(n).target.filename, ... c@0: 'filename_i', mixtures(n).int_fns, ... c@0: 'sofa_path', mixtures(n).sofa_path, ... c@0: 'target_azi', mixtures(n).target.azimuth, ... c@0: 'target_ele', mixtures(n).target.elevation, ... c@0: 'tir', mixtures(n).tir ... c@0: ); c@26: mixtures(n).clearCache(); c@35: mixtures(n).deleteFiles(); c@0: end c@0: c@0: obj.save(); c@0: save(obj.results_filename,'mixtures','separators','-append'); c@0: c@0: % delete temporary files c@0: delete(sprintf('%s*',[obj.PEASSoptions.destDir filesep])) c@0: c@0: fprintf('\n') c@35: cd(currDir); c@0: disp('MASSEF finished.'); c@0: c@0: end c@0: c@0: function evaluate(obj,originalFiles,estimateFile,tag,mixnum,sepnum,estnum) c@0: %EVALUATE Run the framework using input audio files c@0: % c@0: % OBJ.EVALUATE(ORIGINALFILES,ESTIMATEFILE) runs the framework c@0: % using the true sources provided in the wav files whose c@0: % filenames are contained in the cell array ORIGINALFILES (the c@0: % target source is the first one) and the estimate provided in c@0: % the wav file with filename ESTIMATEFILE. c@0: % c@0: % The method may be called as many times as desired. Use c@0: % OBJ.SAVE() when finished to save the framework and its data to c@0: % a file. c@0: % c@0: % OBJ.EVALUATE(ORIGINALFILES,ESTIMATEFILE,TAG) writes the char c@0: % array TAG to the results data. Use the tag to identify c@0: % different estimates in the results data. c@0: % c@0: % OBJ.EVALUATE(ORIGINALFILES,ESTIMATEFILE,TAG,MIXNUM) uses the c@0: % mixture number MIXNUM to identify the separation of a c@0: % particular mixture. MIXNUM is a key that can be used with c@0: % MASSEFRESULTS.MIXTUREINFO() in order to add information about a c@0: % particular mixture. c@0: % c@0: % OBJ.EVALUATE(ORIGINALFILES,ESTIMATEFILE,TAG,MIXNUM,SEPNUM) uses c@0: % the separator number SEPNUM to identify the separation from a c@0: % particular algorithm. SEPNUM is a key that can be used with c@0: % MASSEFRESULTS.ALGORITHMINFO() in order to add information about a c@0: % particular algorithm. c@0: % c@0: % OBJ.EVALUATE(ORIGINALFILES,ESTIMATEFILE,TAG,MIXNUM,SEPNUM,ESTNUM) c@0: % uses the estimate number ESTNUM to identify different estimates c@0: % from a given algorithm (e.g. a binary or soft mask output). c@0: % c@0: % See also IOSR.BSS.MASSEFRESULTS. c@0: c@0: if exist('tag','var')~=1 c@0: tag = ''; c@0: else c@23: assert(ischar(tag),'MASSEF:evaluate:invalidTag','TAG must be a char array') c@0: end c@0: if exist('mixnum','var')~=1 c@0: mixnum = 1; c@0: else c@23: assert(isscalar(mixnum),'MASSEF:evaluate:invalidMixnum','MIXNUM must be a scalar') c@0: end c@0: if exist('sepnum','var')~=1 c@0: sepnum = 1; c@0: else c@23: assert(isscalar(sepnum),'MASSEF:evaluate:invalidSepnum','SEPNUM must be a scalar') c@0: end c@0: if exist('estnum','var')~=1 c@0: estnum = 1; c@0: else c@23: assert(isscalar(estnum),'MASSEF:evaluate:invalidEstnum','ESTNUM must be a scalar') c@0: end c@0: c@0: [estimate,fs] = audioread(estimateFile); c@0: target = audioread(originalFiles{1}); c@0: interferers = zeros(1,size(target,2)); c@0: for i = 2:numel(originalFiles) c@0: int = audioread(originalFiles{i}); c@0: newlength = max(length(int),length(interferers)); c@0: int = obj.setlength(int,newlength); c@0: interferers = obj.setlength(interferers,newlength); c@0: interferers = interferers + int; c@0: end c@0: c@0: for C = 1:size(target,2) % iterate through each channel c@0: c@0: % SNR c@0: snr = iosr.bss.calcSnr(estimate(:,min(C,size(estimate,2))),target(:,C)); c@0: obj.results.input(mixnum,sepnum,estnum,'SNR',C,tag,snr); c@0: c@0: % STOI c@0: if obj.evalSTOI c@0: stoi = taal2011(target(:,C), estimate(:,min(C,size(estimate,2))), fs); c@0: obj.results.input(mixnum,sepnum,estnum,'STOI',C,tag,stoi); c@0: end c@0: end c@0: c@0: % PEASS c@0: if obj.evalPEASS c@0: obj.evaluatePEASS(estimateFile,originalFiles, ... c@0: mixnum,sepnum,estnum,1,tag); c@0: end c@0: c@0: end c@0: c@0: function save(obj) c@0: %SAVE Save the framework's data and results c@0: % c@0: % OBJ.SAVE() save the MASSEF framework object to the file c@0: % determined by OBJ.RESULTS_FILENAME. The method is called c@0: % automatically when using the EXECUTE() method; in this case, c@0: % the mixtures and separators are also saved. c@0: c@0: % Create results files c@0: if exist(obj.results_filename,'file')==2 c@0: % do not overwrite c@0: newfilename = obj.results_filename; c@0: while exist(newfilename,'file')==2 c@0: fprintf('\n') c@0: newfilename = input(... c@0: ['The results file ''' newfilename ''' already exists.\nPlease enter a new filename:\n'],'s'); c@0: end c@0: obj.results_filename = newfilename; c@0: end c@0: obj.saveDate = date; c@0: save(obj.results_filename,'obj'); c@0: fprintf('\n') c@0: disp(['File saved to: ' obj.results_filename]); c@0: c@0: end c@0: c@0: end % public methods c@0: c@0: methods (Static) c@0: c@12: function doc c@12: %DOC Display the framework documentation. c@12: try c@12: web(fullfile(MASSEF.dir, 'help_html', 'help_Index.html'), '-helpbrowser') c@12: catch c@12: web(fullfile(MASSEF.dir, 'help_html', 'help_Index.html')) c@12: end c@12: end c@12: c@0: function start c@0: %START Start the framework. c@0: % c@0: % MASSEF.START starts the framework and its dependencies, c@0: % adds the required folders to the Matlab path, and updates the c@0: % HTML help documentation. c@0: c@0: addpath(cd,... c@0: [MASSEF.dir filesep 'Library'],... c@0: [MASSEF.dir filesep 'Utilities'],... c@0: [MASSEF.dir filesep 'Utilities' filesep 'AMT'],... c@0: [MASSEF.dir filesep 'Utilities' filesep 'PEASS'],... c@0: [MASSEF.dir filesep 'Utilities' filesep 'PEASS' filesep 'gammatone'],... c@0: [MASSEF.dir filesep 'Stimuli']); c@0: c@0: SOFAstart(0); c@0: amtstart; c@0: c@0: d = pwd; c@0: cd([MASSEF.dir filesep 'help_html' filesep 'source']) c@0: publishHelp; c@0: cd(d); c@0: c@0: end c@0: c@0: function install c@0: %INSTALL Download and install MASSEF dependencies. c@0: % c@0: % MASSEF.INSTALL downloads and installs the MASSEF c@0: % dependencies. These dependencies are: c@0: % - The Auditory Modelling Toolbox c@0: % (http://amtoolbox.sourceforge.net); c@0: % - The Large Time-Frequency Analysis Toolbox c@0: % (http://ltfat.sourceforge.net); c@0: % - Perceptual Evaluation methods for Audio Source Separation c@0: % Toolkit (http://bass-db.gforge.inria.fr/peass/); c@0: % - IoSR Matlab Toolbox c@0: % (https://github.com/IoSR-Surrey/MatlabToolbox) c@0: % - SOFA API c@0: % (https://sourceforge.net/projects/sofacoustics/). c@35: c@35: currDir = pwd; c@0: directory = MASSEF.dir; c@0: cd(directory); c@0: c@0: %% download and install STOI c@0: c@0: amt_folder = [directory filesep 'Utilities' filesep 'AMT']; c@0: ltfat_folder = [directory filesep 'Utilities' filesep 'AMT' filesep 'thirdparty' filesep 'ltfat']; c@0: c@0: if ~(exist(amt_folder, 'dir') == 7) c@0: % AMT c@0: amt_filename = 'amtoolbox-0.9.7.zip'; c@0: websave(amt_filename,'http://vorboss.dl.sourceforge.net/project/amtoolbox/amtoolbox-0.9.7.zip'); c@0: unzip(amt_filename,amt_folder); c@0: amt_temp_folder = [amt_folder filesep 'release' filesep]; c@0: movefile([amt_temp_folder '*'],[amt_folder filesep]); c@0: delete(amt_filename) c@0: rmdir(amt_temp_folder,'s') c@0: c@0: % LTFAT c@0: ltfat_filename = 'ltfat-2.1.2.tgz'; c@0: websave(ltfat_filename,'http://netix.dl.sourceforge.net/project/ltfat/ltfat/2.0/ltfat-2.1.2.tgz'); c@0: untar(ltfat_filename,ltfat_folder); c@0: ltfat_temp_folder = [ltfat_folder filesep 'ltfat' filesep]; c@0: movefile([ltfat_temp_folder '*'],[ltfat_folder filesep]); c@0: delete(ltfat_filename) c@0: rmdir(ltfat_temp_folder,'s') c@0: else c@0: display(strcat('Found existing AMT Toolbox directory: ', amt_folder)) c@0: end c@0: c@0: %% download and install PEASS c@0: c@0: % install dir c@0: peass_folder = [directory filesep 'Utilities' filesep 'PEASS']; c@0: c@0: % PEASS c@0: if ~(exist(peass_folder, 'dir') == 7) c@0: peass_filename = 'PEASS-Software-v2.0.zip'; c@0: websave(peass_filename,'http://bass-db.gforge.inria.fr/peass/PEASS-Software-v2.0.zip'); c@0: unzip(peass_filename,peass_folder); c@0: peass_temp_folder = [peass_folder filesep 'PEASS-Software-v2.0' filesep]; c@0: movefile([peass_temp_folder '*'],[peass_folder filesep]); c@0: c@0: % clean up c@0: delete(peass_filename) c@0: rmdir(peass_temp_folder,'s') c@0: else c@0: display(strcat('Found existing PEASS directory: ', peass_folder)) c@0: end c@0: % Hair cell model c@0: adapt_filename = 'adapt_loop.zip'; c@0: websave(adapt_filename,'http://medi.uni-oldenburg.de/download/demo/adaption-loops/adapt_loop.zip'); c@0: unzip(adapt_filename,peass_folder); c@0: cd(peass_folder); c@0: compile; c@0: cd(directory); c@0: c@0: %% download and install IoSR Matlab Toolbox c@0: c@0: if ~MASSEF.gitExists c@0: % IoSR Matlab Toolbox c@0: if ~(exist([directory filesep 'Library' filesep '+iosr'], 'dir') == 7) c@0: iosrMTB = 'iosr.zip'; c@0: iosr_dir = [directory filesep 'Library']; c@0: websave(iosrMTB,'https://github.com/IoSR-Surrey/MatlabToolbox/archive/master.zip'); c@0: unzip(iosrMTB,iosr_dir); c@0: movefile([iosr_dir filesep 'MatlabToolbox-master' filesep '*'],[iosr_dir filesep]); c@0: delete(iosrMTB) c@0: rmdir([iosr_dir filesep 'MatlabToolbox-master'],'s') c@0: else c@0: display('Found existing IoSR Matlab Toolbox directory') c@0: end c@0: end c@23: c@23: iosr.install; c@0: c@0: %% Remaining clean up c@0: c@0: delete(adapt_filename) c@35: cd(currDir); c@0: disp('MASSEF successfully installed.') c@0: c@0: end c@0: c@0: end % Static public methods c@0: c@0: methods (Static, Hidden) c@0: c@0: function e = gitExists c@0: %GITEXISTS Check if install is a git repo c@0: e = exist([MASSEF.dir filesep '.git'],'dir')==7; c@0: end c@0: c@0: end c@0: c@0: methods (Access = private) c@0: c@0: function r = process(obj,mix,separator,mixnum,sepnum,iteration) c@0: %PROCESS Main callback for performing the separation and analysis c@0: c@0: %% Create mixture c@0: c@0: % write mixture, target and interferers c@0: mix.write(sprintf('%smix-%d.wav',obj.PEASSoptions.destDir,mixnum)); c@0: originalFiles = {mix.filename_t, mix.filename_i}; c@0: c@0: % create mixture signal c@0: mixture = mix.signal; c@0: c@0: %% Analyse separator c@0: c@0: % calculate ideal outputs c@0: ibm = mix.ibm; c@0: irm = mix.irm; c@0: output_ibm = mix.applyMask(ibm); c@0: output_irm = mix.applyMask(irm); c@0: c@0: if ~isempty(separator) c@0: c@0: % calculate separator output c@0: [signals,masks,est_azis,est_eles] = separator.separate(mixture); c@0: c@0: % apply mask if signals are not calculated c@0: if isempty(signals) && ~isempty(masks) c@0: for E = 1:size(masks,4) c@0: signals(:,:,E) = mix.applyMask(masks(:,:,:,E)); c@0: end c@0: end c@0: c@0: % Evaluate signals c@0: if ~isempty(signals) c@0: for E = 1:size(signals,3) c@0: c@0: estimateFileBase = sprintf('%ssignal-%d',obj.PEASSoptions.destDir,iteration); c@0: estimateFile = sprintf('%s.wav',estimateFileBase); c@0: audiowrite(estimateFile,iosr.dsp.audio.normalize(signals(:,:,E)),mix.fs); c@0: c@0: obj.evaluate(originalFiles,estimateFile,... c@0: separator.estTag{E},mixnum,sepnum,E); c@0: c@0: % SINR c@0: for C = 1:size(mixture,2) c@0: sinr = iosr.bss.calcSnr(signals(:,C,E),output_irm(:,C)); c@0: obj.results.input(mixnum,sepnum,E,'SINR',C,separator.estTag{E},sinr) c@0: end c@0: c@0: % delete temp files c@0: delete(estimateFile); c@0: c@0: end c@0: c@0: end c@0: c@0: % estimated azimuths c@0: if ~isempty(est_azis) c@0: for s = 1:length(est_azis) c@0: if s==1 c@0: obj.results.input(mixnum,sepnum,1,'est_azi_target',1,[],est_azis(s)) c@0: else c@0: obj.results.input(mixnum,sepnum,1,sprintf('est_azi_int_%02d',s-1),1,[],est_azis(s)) c@0: end c@0: end c@0: end c@0: c@0: % estimated elevation c@0: if ~isempty(est_eles) c@0: for s = 1:length(est_eles) c@0: if s==1 c@0: obj.results.input(mixnum,sepnum,1,'est_ele_target',1,[],est_eles(s)) c@0: else c@0: obj.results.input(mixnum,sepnum,1,sprintf('est_ele_int_%02d',s-1),1,[],est_eles(s)) c@0: end c@0: end c@0: end c@0: c@0: % only do mask comparisons when the masks are equal size c@0: masks_size = size(masks); c@0: ideal_size = size(ibm); c@0: if isequal(masks_size(1:2),ideal_size(1:2)) c@0: for E = 1:size(masks,4) % iterature through each estimate/mask c@0: for C = 1:size(mixture,2) % iterate through each channel c@0: maskC = masks(:,:,min(C,size(masks,3)),E); c@0: c@0: % IMR - binary c@0: imrb = iosr.bss.calcImr(maskC,ibm(:,:,C)); c@0: obj.results.input(mixnum,sepnum,E,'IMR (IBM)',C,separator.estTag{E},imrb) c@0: c@0: % IMR - ratio c@0: imrr = iosr.bss.calcImr(maskC,irm(:,:,C)); c@0: obj.results.input(mixnum,sepnum,E,'IMR (IRM)',C,separator.estTag{E},imrr) c@0: end c@0: c@0: end c@0: c@0: end c@0: c@0: end c@0: c@0: %% Analyse ideal masks c@0: c@0: if sepnum==1 % evaluations of ideal masks c@0: c@0: ibmFile = sprintf('%sibm-%d.wav',obj.PEASSoptions.destDir,iteration); c@0: audiowrite(ibmFile,iosr.dsp.audio.normalize(output_ibm),mix.fs); c@35: irmFile = sprintf('%sirm-%d.wav',obj.PEASSoptions.destDir,iteration); c@0: audiowrite(irmFile,iosr.dsp.audio.normalize(output_irm),mix.fs); c@0: c@0: obj.evaluate(originalFiles,ibmFile,... c@0: 'Binary',mixnum,obj.IDEAL,1); c@0: obj.evaluate(originalFiles,irmFile,... c@0: 'Ratio',mixnum,obj.IDEAL,2); c@0: c@0: % delete temp files c@0: delete(ibmFile); c@0: delete(irmFile); c@0: end c@0: c@0: r = obj.results; c@0: c@0: end c@0: c@0: function evaluatePEASS(obj,estimateFile,originalfiles, ... c@0: mixnum,sepnum,estnum,channelnum,esttag) c@0: %EVALUATE Evaluate resynthesised signals against ground truth c@0: c@0: % calculate metrics c@0: PEASS_output = PEASS_ObjectiveMeasure(originalfiles,estimateFile,obj.PEASSoptions); c@0: c@0: % write data to outputs c@0: obj.results.input(mixnum,sepnum,estnum,'OPS',channelnum,esttag,PEASS_output.OPS) c@0: obj.results.input(mixnum,sepnum,estnum,'TPS',channelnum,esttag,PEASS_output.TPS) c@0: obj.results.input(mixnum,sepnum,estnum,'IPS',channelnum,esttag,PEASS_output.IPS) c@0: obj.results.input(mixnum,sepnum,estnum,'APS',channelnum,esttag,PEASS_output.APS) c@0: obj.results.input(mixnum,sepnum,estnum,'SDR',channelnum,esttag,PEASS_output.SDR) c@0: obj.results.input(mixnum,sepnum,estnum,'SAR',channelnum,esttag,PEASS_output.SAR) c@0: obj.results.input(mixnum,sepnum,estnum,'SIR',channelnum,esttag,PEASS_output.SIR) c@0: obj.results.input(mixnum,sepnum,estnum,'ISR',channelnum,esttag,PEASS_output.ISR) c@0: c@0: [pathstr,name] = fileparts(estimateFile); c@0: delete(fullfile(pathstr,[name '_true.wav'])); c@0: delete(fullfile(pathstr,[name '_eTarget.wav'])); c@0: delete(fullfile(pathstr,[name '_eInterf.wav'])); c@0: delete(fullfile(pathstr,[name '_eArtif.wav'])); c@0: c@0: end c@0: c@0: function nchars = updateWaitDisp(obj,numComplete,nchars) c@0: %UPDATEWAITDISP Update the wait display c@0: c@0: fraction = numComplete/obj.iterations; c@0: c@0: if ~isempty(obj.hWaitBar) % update waitbar if it exists c@0: waitbar(fraction, obj.hWaitBar); c@0: end c@0: fprintf(1,repmat('\b',1,nchars)); % delete old message c@0: if numComplete==obj.iterations c@0: format = '%.0f'; % special format for 100% c@0: else c@0: format = '%.1f'; % normal format c@0: end c@0: c@0: % display message and return number of chars to delete for next message c@0: nchars = fprintf(1,[format '%% complete.'],100*fraction); c@0: c@0: end c@0: c@0: function b = breakWaitDisp(obj) c@0: %BREAKWAITDISP Determine whether to break the calculations c@0: c@0: b = false; % don't by default c@0: if ~isempty(obj.hWaitBar) % if waitbar exists c@0: if getappdata(obj.hWaitBar, 'Cancelled') % determine if "Cancel" was clicked c@0: b = true; % break if so c@0: end c@0: end c@0: if b % display message if breaking c@0: fprintf('\nMASSEF cancelled.\n'); c@0: end c@0: c@0: end c@0: c@0: end % private methods c@0: c@0: methods (Static, Access = private) c@0: c@0: function options = validate_options(options) c@0: %VALIDATE_OPTIONS Validate the options structure input to this function c@0: c@0: % check format c@0: if numel(options)>1 c@23: error('MASSEF:validate_options:invalidInput','The input to MASSEF should be a 1x1 structure. Cell-array fields should be encapsulated within a single cell.') c@0: end c@0: c@0: fields_in = fieldnames(options); c@0: c@0: % default values c@0: if MASSEF.pctexists() c@0: default_parpool = gcp('nocreate'); c@0: else c@0: default_parpool = []; c@0: end c@0: default_values = struct( ... c@0: 'evalPEASS',false, ... c@0: 'evalSTOI',false, ... c@0: 'parpool',default_parpool, ... c@0: 'results_filename',[MASSEF.dir filesep 'Results' filesep 'results.mat'], ... c@0: 'blocksize',128 ... c@0: ); c@0: def_fields = fieldnames(default_values); c@0: c@0: % check for invalid options c@0: for r = 1:length(fields_in) c@23: assert(any(strcmpi(fields_in{r},def_fields)),'MASSEF:validate_options:invalidOption',['Invalid option ''' fields_in{r} ''' specified.']) c@0: end c@0: c@0: % write defaults c@0: for r = 1:numel(def_fields) c@0: field_name = def_fields{r}; c@0: if ~isfield(options,field_name) c@0: options.(field_name) = default_values.(field_name); c@0: end c@0: end c@0: c@0: end c@0: c@0: function [IVs,iterations] = initialise_IVs(numMixtures,numSeparators) c@0: %INITIALISE_IVs Initialise experiment results structure c@0: c@0: % Independent variables space c@0: IV_space = [numMixtures max(1,numSeparators)]; c@0: c@0: iterations = prod(IV_space); % total number of results c@23: assert(numMixtures>0,'MASSEF:initialise_IVs:noMixtures','No mixtures have been specified') c@0: %assert(numSeparators>0,'No separators have been specified') c@0: c@0: IVs = struct('algo_num',[],'mixture_num',[]); c@0: c@0: % fill IV cells with data c@0: for N = 1:iterations c@0: [n,p] = ind2sub(IV_space,N); c@0: IVs(N).algo_num = p; c@0: IVs(N).mixture_num = n; c@0: end c@0: end c@0: c@0: function check_separator(obj) c@0: %CHECK_SEPARATOR Check separator meets requirements c@0: c@23: assert(isprop(obj,'label'),'MASSEF:check_separator:invalidSepNoLabel',['The object of class ''' class(obj) ''' does not have the required property ''label''']) c@23: assert(ischar(obj.label),'MASSEF:check_separator:invalidSepLabelProp',['The ''label'' property of the ''' class(obj) ''' object should return a char array']) c@23: assert(isprop(obj,'estTag'),'MASSEF:check_separator:invalidSepNoEstTag',['The object of class ''' class(obj) ''' does not have the required property ''estTag''']) c@23: assert(iscellstr(obj.estTag),'MASSEF:check_separator:invalidSepEstTagProp',['The ''estTag'' property of the ''' class(obj) ''' object should return a cell array of strings']) c@23: assert(ismethod(obj,'separate'),'MASSEF:check_separator:invalidSepNoSepMethod',['The object of class ''' class(obj) ''' does not have the required method ''separate''']) c@0: c@0: end c@0: c@0: function e = pctexists c@0: %PCTEXISTS Determine whether the parallel computing toolbox exists c@0: c@0: if isempty(ver('distcomp')) c@0: e = false; c@0: else c@0: e = true; c@0: end c@0: c@0: end c@0: c@0: function h = initWaitDisp c@0: %INITWAITDISP Initialise the waiting display c@0: c@0: if usejava('jvm') % only call waitbar if JVM is running c@0: h = waitbar(0, 'MASSEF processing...', 'CreateCancelBtn', ... c@0: @(src, event) setappdata(gcbf(), 'Cancelled', true)); c@0: setappdata(h, 'Cancelled', false); c@0: else % do nothing c@0: h = []; c@0: end c@0: end c@0: c@0: function y = setlength(x,signal_length) c@0: %SETLENGTH Crop or zero-pad signal to specified length c@0: c@0: d = size(x); c@0: if size(x,1)>signal_length % need to crop c@0: subsidx = [{1:signal_length} repmat({':'},1,ndims(x)-1)]; c@0: y = x(subsidx{:}); c@0: elseif size(x,1)