classdefitaMSPlaybackRecord<itaMSRecord% <ITA-Toolbox>% This file is part of the application Measurement for the ITA-Toolbox. All rights reserved.% You can find the license for this m-file in the application folder.% </ITA-Toolbox>% This is a class for playing back and recording at the same time, but% without deconvolution%% See also: ita_measurement, itaMSRecord, itaMeasurementChainproperties(Access=public,Hidden=true)mOutputamplification=-40;mExcitation=[];mOutputChannels=[];mOutputMeasurementChain=itaMeasuringStation.loadCurrentOutputMC;%itaMeasurementChain('output');endproperties(Dependent=true,Hidden=false,Transient=true)outputamplification% Attenuation factor of the output signal in dBFS (Fullscale)excitation% itaAudio containing the excitation signal, for this class. 1 channel.endproperties(Dependent=true,Hidden=false,Transient=true,AbortSet=true,SetObservable=true)%triggers @this.init !!!outputChannels% Vector specifying the output channel IDs e.g. [1 5]outputMeasurementChain% itaMeasurementChain('output') defining all output measurement chain elementsendproperties(Dependent=true,Hidden=true,Transient=true,AbortSet=true)outputamplification_lin% Linear factor from dBFS 'outputamplification' for multiplicationfinal_excitation% Excitation including compensation of relative output sensitivites/spectra - number of channels equal outputMCoutputVoltage% used to set outputamplification for a calibrated output chainendproperties(Hidden=false,Transient=true,AbortSet=true,SetObservable=true)latencysamples=0;% number of samples (positive integer) used to compensate the delay of the AD/DA conversionendproperties(SetObservable=true,AbortSet=true)outputEqualization=false;endmethods%% CONSTRUCT / INIT / EDIT / COMMANDLINEfunctionthis=itaMSPlaybackRecord(varargin)% itaMSPlaybackRecord - Constructs an itaMSPlaybackRecord object.ifnargin==0% For the creation of itaMSPlaybackRecord objects from commandline strings% like the ones created with the commandline method of this% class, 2 or more input arguments have to be allowed. All% desired properties have to be given in pairs of two, the% first element being an identifying string which will be used% as field name for the property, and the value of the% specified property.elseifnargin>=2if~isnatural(nargin/2)error('Even number of input arguments expected!');end% For all given pairs of two, use the first element as% field name, the second one as value. The validity of the% field names will NOT be checked.foridx=1:2:narginthis.(varargin{idx})=varargin{idx+1};end% Only one input argument is required for the creation of an% itaMSPlaybackRecord class object from a struct, created by the saveobj% method, or as a copy of an already existing itaMSPlaybackRecord class% object. In the latter case, only the properties contained in% the list of saved properties will be copied.elseifisstruct(varargin{1})||isa(varargin{1},'itaMSPlaybackRecord')% Check type of given argument and obtain the list of saved% properties accordingly.ifisa(varargin{1},'itaMSPlaybackRecord')%The save struct is obtained by using the saveobj% method, as in the case in which a struct is given% from the start (see if-case above).varargin{1}=saveobj(varargin{1});% have to delete the dateSaved field to make clear it% might be from an inherited classvarargin{1}=rmfield(varargin{1},'dateSaved');endifisfield(varargin{1},'dateSaved')varargin{1}=rmfield(varargin{1},'dateSaved');fieldName=fieldnames(varargin{1});else%we have a class instance here, maybe a childfieldName=fieldnames(rmfield(this.saveobj,'dateSaved'));endforind=1:numel(fieldName);trythis.(fieldName{ind})=varargin{1}.(fieldName{ind});catcherrmsgdisp(errmsg);endendelseerror('itaMSPlaybackRecord::wrong input arguments given to the constructor');end% Define listeners to automatically call the init function of% this class in case of a change the the below specified% properties.% not needed here, only in classes inherited from this oneendfunctionthis=edit(this)% edit - Start GUI.%% This function calls the itaMSPlaybackRecord GUI.this=ita_msplaybackrecord_gui(this);end%% CALIBRATIONfunctionthis=calibrate(this)% this will guide you thru the calibration processthis=calibrate_input(this);% outputMeasurementChainthis=calibrate_output(this);endfunctionMS=calibrationMS(this)% Generates a simple Measurement Setup for calibration purposes.saveStruct=saveobj(this);% delete all fields that itaMSTF cannot handle% this is important when calling from inherited classesfieldNames=fieldnames(saveStruct);classFields=[itaMSRecord.propertiesSaveditaMSPlaybackRecord.propertiesSaveditaMSTF.propertiesSaved'dateSaved'];additionalFields=fieldNames(~cellfun(@(x)any(strcmpi(classFields,x)),fieldNames));if~isempty(additionalFields)saveStruct=rmfield(saveStruct,additionalFields);endsaveStruct.mExcitation=itaAudio();% erase excitation so it will be rebuiltsaveStruct.mNSamples=2^15;saveStruct.mType='exp';saveStruct.stopMargin=min(0.3,2^14/this.samplingRate);% for high sampling frequenciessaveStruct.mOutputamplification=-50;% Low amplification, to be safe. Will be autoranged later.saveStruct.mFreqrange=[1this.samplingRate/2];% Full range for calibration.saveStruct.applyBandpass=0;saveStruct.pause=0;% no pause for calibrationMS=itaMSTF(saveStruct);% Init new MSTF object.endfunctionthis=calibrate_output(this,input_chain_number)% Calibrates all output chains, using only the first% (hopefully calibrated) input chain. Input chain calibrationif~exist('input_chain_number','var')input_chain_number=find(this.inputMeasurementChain.hw_ch==this.inputChannels(1));endita_verbose_info(['Calibrating using input channel 'num2str(this.inputMeasurementChain(input_chain_number).hardware_channel)],1);MS=this.calibrationMS;% Get new simple Measurement Setup for calibration. See above.mco=this.outputMeasurementChain;% Get all output measurement chains.outChannels=this.outputChannels;% Get all output channels.% The calibration of the multiple output measurement chains /% outout channels will be executed one-by-one.foroutIdx=1:numel(outChannels)chIdx=find(mco.hw_ch==outChannels(outIdx));% Return single index of entry in 'mco', equal to the out channel, which is to be calibrated.MS.outputMeasurementChain=mco(chIdx);% Set Measurement Setup's single output chain to match the one, which is to be calibrated.MS.outputChannels=outChannels(outIdx);% Set Measurement Setup's single output channel to match the one, which is to be calibrated.% Execute calibration for every single element in the% current output measurement chain.% 'ita_mstfoutput_calibration' determines if the object can% be calibrated at all.forele_idx=1:length(mco(chIdx).elements)MS=ita_measurement_chain_output_calibration(MS,input_chain_number,ele_idx);end% if there was no latency info before, copy it from the% calibrationMS because latency was measured in the output% calibration routineifthis.latencysamples==0this.latencysamples=MS.latencysamples;end% Put the current calibrated measurement chain back into% its appropriate position in the list of all output% measurment chains.mco(chIdx)=MS.outputMeasurementChain;endthis.outputMeasurementChain=mco;% Copy over the list of all calibrated output measurement chains into the real Measurement Setup.end%% RUNfunctioncheckready(this)%check if the instance is ready for measurement run and ask for%missing entriesifisempty(this.inputChannels)||isempty(this.outputChannels)||isempty(this.excitation)this.edit;endendfunction[result,max_rec_lvl]=run_raw(this)% run_raw - Run measurementthis.checkready;singleprecision=strcmpi(this.precision,'single');% Bool for single precision for portaudio.result=ita_portaudio(this.final_excitation,'InputChannels',this.inputChannels,...'OutputChannels',this.outputChannels,'repeats',1,...'latencysamples',this.latencysamples,'singleprecision',singleprecision,'reset',this.reset);ifthis.outputVoltage~=1% only if output is calibratedresult.comment=[result.comment' @'num2str(round(this.outputVoltage*1000)/1000)'Vrms'];endmax_rec_lvl=max(abs(result.timeData),[],1);endfunction[result,max_rec_lvl]=run(this)% run - Run standard measurement.%% This function runs a measurement, and compensates for the% input measurement chain. If the output is calibrated, the% output voltage will be stored in the comment, but it will% not be compensated for.[result,max_rec_lvl]=run_raw_imc(this);ifthis.applyBandpassresult=ita_mpb_filter(result,this.freqRange,'zerophase');endendfunction[result,max_rec_lvl]=run_backgroundNoise(this)% run_backgroundNoise - Run simple background noise% measurement.%% This function runs a measurement with -500dBFS% outputamplification to measure the background noise.original_outputamplification=this.outputamplification;this.outputamplification=-500;[result,max_rec_lvl]=run_raw_imc(this);this.outputamplification=original_outputamplification;endfunction[result,max_rec_lvl]=run_raw_imc_omc(this)% run_raw_imc_omc - Run raw and compensate both input and output.[result,max_rec_lvl]=run_raw_imc(this);result=result/this.outputamplification_lin;result=compensateOutputMeasurementChain(this,result);endfunction[result,max_rec_lvl]=run_latency(this)% run_latency - Run measurement and determine the latency% samples.%% This function runs a not input/output measurement chain% compensated measurement, does the deconvolution itself,% analyzes the resulting impulse response and sets the latency% samples in the measurement setup. It returns the not% compensated impulse response and max recording level.ita_verbose_info('Measuring latency samples...',1);MS=this.calibrationMS;MS.inputChannels=MS.inputChannels(1);ita_verbose_info(['Using channel 'num2str(MS.inputChannels)' for latency measurement'],1);MS.outputamplification=this.outputamplification;MS.latencysamples=0;[result,max_rec_lvl]=run_raw(MS);result=result*MS.compensation/MS.outputamplification_lin;result.signalType='energy';[maxamplitude,lsamples]=max(abs(result.timeData),[],1);% Get the measurement's max absolute amplitude and exact sample position of max amplitude for each channel.[maxamplitude,idx]=max(maxamplitude);%#ok<ASGLU>lsamples=lsamples(idx)-1;% Get the max of all position samples of all channels ans substract 1, to prevent anti-causal impuls responses.if(~isempty(lsamples)&&lt(lsamples,0))||isempty(lsamples)% If result would be acausal... suppress it!ita_verbose_info('Could not find a suitable impulse! Try a higher output amplification.',0);this.latencysamples=[];elsethis.latencysamples=lsamples;endendfunction[result,signal,noise]=run_snr(this,fraction)% run noise and signal and compareifnargin<2fraction=3;endita_verbose_info('Recording noise level for SNR',1);noise=this.run_backgroundNoise;N=ita_spk2frequencybands(noise,'bandsperoctave',fraction,'freqRange',[min(this.freqRange(:))max(this.freqRange(:))]);ita_verbose_info('Recording signal level for SNR',1);signal=this.run_raw_imc;sig=sqrt(abs(signal')^2 - abs(noise')^2);S=ita_spk2frequencybands(sig,'bandsperoctave',fraction,'freqRange',[min(this.freqRange(:))max(this.freqRange(:))]);result=S/N;result.comment=['Signal-to-Noise Ratio in 1/'num2str(fraction)' octave bands'];end%% Auxfunctionresult=compensateOutputMeasurementChain(this,result)% Apply the output measurement chain and the output% amplification to the result. It only makes sense to apply the% final response of the output measurement chain if only one% channel has been used during the measurement (no MIMO% calibration possible, here).if~isempty(this.outputMeasurementChain)iflength(this.outputChannels)<=1outChannel=this.outputChannels;ifthis.outputEqualization&&isa(this.outputMeasurementChain.hw_ch(outChannel).final_response,'itaAudio')omc=this.outputMeasurementChain.hw_ch(outChannel);final_response=ita_extend_dat(omc.final_response,this.fftDegree,'symmetric');omcTypes={omc.elements.type};lsIdx=find(strcmpi(omcTypes,'loudspeaker'));if~isempty(lsIdx)&&~isempty(omc.elements(lsIdx).response)% Houston, we have a Loudspeaker% better use smaller freqRange, if LS is included% lower frequency of 1 would lead to very high amplificationfinal_response=ita_smooth_notches(final_response,'bandwidth',1,'squeezeFactor',0.3);outputFilter=ita_invert_spk_regularization(final_response,this.finalFreqRange,'filter');else% otherwise just electricaloutputFilter=ita_invert_spk_regularization(final_response,[1this.samplingRate/2],'filter');endoutputFilter.channelNames(:)={''};result=result*outputFilter;elsefinal_response=this.outputMeasurementChain.hw_ch(outChannel).sensitivity;result=result/final_response;endelseita_verbose_info('Too many output channels. Output chain compensation not possible',0);endendend%% PLOTfunctionplot(this)% plot - Plot ideal FRF and IR of excitation * compensation.ita_plot_all(this.excitation);end%% GET / SET% outputamplificationfunctionset.outputamplification(this,value)ifischar(value)value=str2num(value(~isstrprop(value,'alpha')));%#ok<ST2NM>endthis.mOutputamplification=-abs(value);endfunctionres=get.outputamplification(this)res=[num2str(round(this.mOutputamplification)),'dBFS'];endfunctionres=get.outputamplification_lin(this)res=10^(this.mOutputamplification/20);endfunctionplus(this,value)% increase output amplificationthis.outputamplification=min(this.mOutputamplification+value,0);disp(['Output amplification: 'this.outputamplification]);endfunctionminus(this,value)% decrease output amplificationplus(this,-value);endfunctionset.outputVoltage(this,value)ifnumel(this.outputChannels)~=1ita_verbose_info('Multiple output channels, selecting the one with maximum gain',0);[dummy,idx]=max(double(this.outputMeasurementChain.hw_ch(this.outputChannels).sensitivity('loudspeaker')));omc=this.outputMeasurementChain.hw_ch(this.outputChannels(idx));elseomc=this.outputMeasurementChain.hw_ch(this.outputChannels);endif~omc.calibrated||omc.sensitivity.value==1ita_verbose_info('The outputMeasurementChain is not calibrated, this makes no sense then',1);ita_verbose_info('Leaving outputamplification unchanged',0);elseoutSens=omc.sensitivity('loudspeaker');this.outputamplification=20*log10(abs(double(value/(this.raw_excitation.rms*outSens))));endendfunctionres=get.outputVoltage(this)% including outputamplificationomc=this.outputMeasurementChain.hw_ch(this.outputChannels);foriCh=1:numel(omc)if~omc(iCh).calibrated||double(omc(iCh).sensitivity)==1ita_verbose_info('The outputMeasurementChain is not calibrated, this makes no sense then',1);res=1;return;endendoutSens=double(omc.sensitivity('loudspeaker'));res=this.raw_excitation.rms*this.outputamplification_lin.*outSens;endfunctionset.excitation(this,value)set_excitation(this,value);endfunctionset_excitation(this,value)%trick to overload in derivativesifisempty(value)error('itaMSPlaybackRecord::I cannot play empty signals');elseifisa(value,'itaAudio')value.dataType=this.precision;value.dataTypeOutput=this.precision;this.samplingRate=value.samplingRate;this.fftDegree=value.fftDegree;this.mExcitation=ita_normalize_dat(value);elseerror(['itaMSPlaybackRecord::what kind of playback signal is this (class is: 'class(value)')']);endendfunctionres=get.excitation(this)% get final excitationres=this.final_excitation;endfunctionres=raw_excitation(this)%trick to overload in derivatives% build the elementary/raw excitation signalres=this.mExcitation;endfunctionset.final_excitation(this,value)this.mExcitation=value;endfunctionres=get.final_excitation(this)res=get_final_excitation(this);%trick to overload in derivativesendfunctionres=get_final_excitation(this)% get the corrected excitation (outputamplification)res=this.raw_excitation*this.outputamplification_lin;endfunctionset.outputChannels(this,value)if~all(ismember(value,this.outputMeasurementChain.hw_ch))% pdi: this works as hell! ask joe for any commentsthis.mOutputChannels=value;newChannels=value(~ismember(value,this.outputMeasurementChain.hw_ch));% if the output chain is empty create a new oneifnumel(this.outputMeasurementChain)==1&&this.outputMeasurementChain.hw_ch==0ifthis.useMeasurementChainthis.outputMeasurementChain=ita_measurement_chain_output(newChannels);else% create an empty dummy chaindummyChain=itaMeasurementChain(numel(newChannels));foriCh=1:numel(newChannels)dummyChain(iCh).type='output';dummyChain(iCh).hardware_channel=newChannels(iCh);endthis.outputMeasurementChain=dummyChain;end% otherwise add new channelselseifthis.useMeasurementChainthis.outputMeasurementChain=[this.outputMeasurementChainita_measurement_chain_output(newChannels)];else% create an empty dummy chaindummyChain=itaMeasurementChain(numel(newChannels));foriCh=1:numel(newChannels)dummyChain(iCh).type='output';dummyChain(iCh).hardware_channel=newChannels(iCh);endthis.outputMeasurementChain=[this.outputMeasurementChaindummyChain];endendthis.mOutputChannels=value;elsethis.mOutputChannels=value;endset_outputChannels(this,value);endfunctionset_outputChannels(this,value)this.mOutputChannels=value;endfunctionres=get.outputChannels(this)res=this.mOutputChannels;endfunctionset.outputMeasurementChain(this,value)this.mOutputMeasurementChain=value;endfunctionres=get.outputMeasurementChain(this)res=this.mOutputMeasurementChain;end%% commandlinefunctionstr=commandline(this)% commandline - Generate comandline string.%% This function creates a commandline string for creating the% exact same measurement setup.% first get the values from parent classparentStr=commandline@itaMSRecord(this);str=['itaMSPlaybackRecord'parentStr(12:end-2)','];list={'fftDegree','freqRange','outputamplification','latencysamples','outputEqualization','outputChannels'};foridx=1:numel(list)token=this.(list{idx});ifisempty(token)continue;end;ifischar(token)token=[''''token''''];elseifisnumeric(token)||islogical(token)token=num2str(token);ifnumel(token)>1token=['['token']'];endelseerror([upper(mfilename)'.commandline: What kind of field value is this?']);end;str=[str''''list{idx}''''','token];ifidx<numel(list)str=[str','];%#ok<*AGROW>endendstr=[str');'];endend%% Hidden methodsmethods(Hidden=true)functiondisplay(this)% Begin Display Start LineclassnameString=['|'class(this)'|'];result=repmat('=',1,itaSuper.LINE_LENGTH);result(3:(2+length(classnameString)))=classnameString;disp(result);% End Display Start Lineif~isempty(this.excitation)commentStr=this.excitation.comment;iflength(commentStr)>10commentStr=[commentStr(1:7)'...'];elsecommentStr=[commentStr,repmat(' ',1,10-length(commentStr))];endtrackLength=this.excitation.trackLength;elsecommentStr='empty';trackLength=0;end% Start Display Valuesdisp([' excitation = 'commentStr' samplingRate = 'num2str(this.samplingRate)' nSamples = 'num2str(this.nSamples)])oa=repmat(' ',1,7);oa_temp=(this.outputamplification);oa(1:length(oa_temp))=oa_temp;disp([' length = 'num2str(trackLength,5)' s '' level = 'oa' freqRange = ['num2str(this.freqRange(:)') ']'])disp([' averages = 'num2str(this.averages)' repeats = 'num2str(this.repeats)' latency = 'num2str(this.latencysamples)])disp([' output ch. = ['num2str(this.outputChannels)'] input ch. = ['num2str(this.inputChannels)']'])% End Display ValuesgloballastDiplayedVariableNamelastDiplayedVariableName=inputname(1);ifita_preferences('dispVerboseFunctions')display_line4commands({' MS ',{'__.edit','.edit'},{'plot(__)','plot excitation'},{'builtin(''disp'',__)','Show Inside of Class'},' Level:',{'__ - 5','-5dB'},{'__ - 1','-1dB'},{'__ + 1','+1dB'},{'__ + 5','+5dB'}},lastDiplayedVariableName);display_line4commands({' Measure ',{'__.run','.run'},{'__.run_raw','.run_raw'},{'__.run_latency','.run_latency'},...{'__.run_backgroundNoise','.run_backgroundNoise'},{'__.run_snr','.run_SNR'}},lastDiplayedVariableName);elsedisplay_line4commands({' ',...{'ita_preferences(''dispVerboseFunctions'',1); display(__)','What to do...?'}},lastDiplayedVariableName);endendfunctionthis=force_calibration(this)%set all measurement chain components to status: 'calibrated'this.inputMeasurementChain=this.inputMeasurementChain.force_calibration;this.outputMeasurementChain=this.outputMeasurementChain.force_calibration;endfunctionsObj=saveobj(this)% saveobj - Saves the important properties of the current% measurement setup to a struct.sObj=saveobj@itaMSRecord(this);% Get list of properties to be saved for this measurement% class.propertylist=itaMSPlaybackRecord.propertiesSaved;% Write the content of every item in the list of the to be saved% properties into its own field in the save struct.foridx=1:numel(propertylist)sObj.(propertylist{idx})=this.(propertylist{idx});endendendmethods(Static,Hidden=true)functionthis=loadobj(sObj)this=itaMSPlaybackRecord(sObj);% Just call constructor, he will take careendfunctionresult=propertiesSaved% propertiesSaved - Creates a list of all the properties to be% saved of the current measurement setup.%% This function gets the list of all% properties to be saved during the saving process.% Get list of saved properties for this class.result={'mOutputamplification','mOutputChannels','mOutputMeasurementChain','mExcitation','latencysamples','outputEqualization'};endendend