function varargout = mirenvelope(orig,varargin)
% e = mirenvelope(x) extracts the envelope of x, showing the global shape
% of the waveform.
% mirenvelope(...,m) specifies envelope extraction method.
% Possible values:
% m = 'Filter' uses a low-pass filtering. (Default strategy)
% m = 'Spectro' uses a spectrogram.
%
% Options related to the 'Filter' method:
% mirenvelope(...,'Hilbert'): performs a preliminary Hilbert
% transform.
% mirenvelope(...,'PreDecim',N) downsamples by a factor N>1, where
% N is an integer, before the low-pass filtering (Klapuri, 1999).
% Default value: N = 1.
% mirenvelope(...,'Filtertype',f) specifies the filter type.
% Possible values are:
% f = 'IIR': filter with one autoregressive coefficient
% (default)
% f = 'HalfHann': half-Hanning (raised cosine) filter
% (Scheirer, 1998)
% Option related to the 'IIR' option:
% mirenvelope(...,'Tau',t): time constant of low-pass filter in
% seconds.
% Default value: t = 0.02 s.
% mirenvelope(...,'PostDecim',N) downsamples by a factor N>1, where
% N is an integer, after the low-pass filtering.
% Default value: N = 16 if 'PreDecim' is not used, else N = 1.
% mirenvelope(...,'Trim'): trims the initial ascending phase of the
% curves related to the transitory state.
%
% Options related to the 'Spectro' method:
% mirenvelope(...,b) specifies whether the frequency range is further
% decomposed into bands. Possible values:
% b = 'Freq': no band decomposition (default value)
% b = 'Mel': Mel-band decomposition
% b = 'Bark': Bark-band decomposition
% b = 'Cents': decompositions into cents
% mirenvelope(...,'Frame',...) specifies the frame configuration.
% Default value: length: .1 s, hop factor: 10 %.
% mirenvelope(...,'UpSample',N) upsamples by a factor N>1, where
% N is an integer.
% Default value if 'UpSample' called: N = 2
% mirenvelope(...,'Complex') toggles on the 'Complex' method for the
% spectral flux computation.
%
% Other available for all methods:
% mirenvelope(...,'Sampling',r): resamples to rate r (in Hz).
% 'Down' and 'Sampling' options cannot therefore be combined.
% mirenvelope(...,'Halfwave'): performs a half-wave rectification.
% mirenvelope(...,'Center'): centers the extracted envelope.
% mirenvelope(...,'HalfwaveCenter'): performs a half-wave
% rectification on the centered envelope.
% mirenvelope(...,'Log'): computes the common logarithm (base 10) of
% the envelope.
% mirenvelope(...,'Mu',mu): computes the logarithm of the
% envelope, before the eventual differentiation, using a mu-law
% compression (Klapuri, 2006).
% Default value for mu: 100
% mirenvelope(...,'Log'): computes the logarithm of the envelope.
% mirenvelope(...,'Power'): computes the power (square) of the
% envelope.
% mirenvelope(...,'Diff'): computes the differentation of the
% envelope, i.e., the differences between successive samples.
% mirenvelope(...,'HalfwaveDiff'): performs a half-wave
% rectification on the differentiated envelope.
% mirenvelope(...,'Normal'): normalizes the values of the envelope by
% fixing the maximum value to 1.
% mirenvelope(...,'Lambda',l): sums the half-wave rectified envelope
% with the non-differentiated envelope, using the respective
% weight 0<l<1 and (1-l). (Klapuri et al., 2006)
% mirenvelope(...,'Smooth',o): smooths the envelope using a moving
% average of order o.
% Default value when the option is toggled on: o=30
% mirenvelope(...,'Gauss',o): smooths the envelope using a gaussian
% of standard deviation o samples.
% Default value when the option is toggled on: o=30
% mirenvelope(...,'Klapuri06'): follows the model proposed in
% (Klapuri et al., 2006).
method.type = 'String';
method.choice = {'Filter','Spectro'};
method.default = 'Filter';
option.method = method;
%% options related to 'Filter':
hilb.key = 'Hilbert';
hilb.type = 'Boolean';
hilb.default = 0;
option.hilb = hilb;
decim.key = {'Decim','PreDecim'};
decim.type = 'Integer';
decim.default = 0;
option.decim = decim;
filter.key = 'FilterType';
filter.type = 'String';
filter.choice = {'IIR','HalfHann',0};
if isamir(orig,'mirenvelope')
filter.default = 0; % no more envelope extraction, already done
else
filter.default = 'IIR';
end
option.filter = filter;
%% options related to 'IIR':
tau.key = 'Tau';
tau.type = 'Integer';
tau.default = .02;
option.tau = tau;
zp.key = 'ZeroPhase'; % internal use: for manual filtfilt
zp.type = 'Boolean';
if isamir(orig,'mirenvelope')
zp.default = 0;
else
zp.default = NaN;
end
option.zp = zp;
ds.key = {'Down','PostDecim'};
ds.type = 'Integer';
if isamir(orig,'mirenvelope')
ds.default = 1;
else
ds.default = NaN; % 0 if 'PreDecim' is used, else 16
end
ds.when = 'After';
ds.chunkcombine = 'During';
option.ds = ds;
trim.key = 'Trim';
trim.type = 'Boolean';
trim.default = 0;
trim.when = 'After';
option.trim = trim;
%% Options related to 'Spectro':
band.type = 'String';
band.choice = {'Freq','Mel','Bark','Cents'};
band.default = 'Freq';
option.band = band;
up.key = {'UpSample'};
up.type = 'Integer';
up.default = 0;
up.keydefault = 2;
up.when = 'After';
option.up = up;
complex.key = 'Complex';
complex.type = 'Boolean';
complex.default = 0;
complex.when = 'After';
option.complex = complex;
%% Options related to all methods:
sampling.key = 'Sampling';
sampling.type = 'Integer';
sampling.default = 0;
sampling.when = 'After';
option.sampling = sampling;
hwr.key = 'Halfwave';
hwr.type = 'Boolean';
hwr.default = 0;
hwr.when = 'After';
option.hwr = hwr;
c.key = 'Center';
c.type = 'Boolean';
c.default = 0;
c.when = 'After';
option.c = c;
chwr.key = 'HalfwaveCenter';
chwr.type = 'Boolean';
chwr.default = 0;
chwr.when = 'After';
option.chwr = chwr;
mu.key = 'Mu';
mu.type = 'Integer';
mu.default = 0;
mu.keydefault = 100;
mu.when = 'After';
option.mu = mu;
oplog.key = 'Log';
oplog.type = 'Boolean';
oplog.default = 0;
oplog.when = 'After';
option.log = oplog;
oppow.key = 'Power';
oppow.type = 'Integer';
oppow.default = 0;
oppow.when = 'After';
option.power = oppow;
diff.key = 'Diff';
diff.type = 'Integer';
diff.default = 0;
diff.keydefault = 1;
diff.when = 'After';
option.diff = diff;
diffhwr.key = 'HalfwaveDiff';
diffhwr.type = 'Integer';
diffhwr.default = 0;
diffhwr.keydefault = 1;
diffhwr.when = 'After';
option.diffhwr = diffhwr;
lambda.key = 'Lambda';
lambda.type = 'Integer';
lambda.default = 1;
lambda.when = 'After';
option.lambda = lambda;
aver.key = 'Smooth';
aver.type = 'Integer';
aver.default = 0;
aver.keydefault = 30;
aver.when = 'After';
option.aver = aver;
gauss.key = 'Gauss';
gauss.type = 'Integer';
gauss.default = 0;
gauss.keydefault = 30;
gauss.when = 'After';
option.gauss = gauss;
norm.key = 'Normal';
norm.type = 'Boolean';
norm.default = 0;
norm.when = 'After';
option.norm = norm;
presel.type = 'String';
presel.choice = {'Klapuri06'};
presel.default = 0;
option.presel = presel;
frame.key = 'Frame';
frame.type = 'Integer';
frame.number = 2;
frame.default = [.1 .1];
option.frame = frame;
specif.option = option;
specif.eachchunk = 'Normal';
specif.combinechunk = 'Concat';
specif.extensive = 1;
varargout = mirfunction(@mirenvelope,orig,varargin,nargout,specif,@init,@main);
function [x type] = init(x,option)
type = 'mirenvelope';
if isamir(x,'mirscalar')
return
end
if ischar(option.presel) && strcmpi(option.presel,'Klapuri06')
option.method = 'Spectro';
end
if not(isamir(x,'mirenvelope'))
if strcmpi(option.method,'Filter')
if isnan(option.zp)
if strcmpi(option.filter,'IIR')
option.zp = 1;
else
option.zp = 0;
end
end
if option.zp == 1
x = mirenvelope(x,'ZeroPhase',2,'Down',1,...
'Tau',option.tau,'PreDecim',option.decim);
end
elseif strcmpi(option.method,'Spectro')
x = mirspectrum(x,'Frame',option.frame.length.val,...
option.frame.length.unit,...
option.frame.hop.val,...
option.frame.hop.unit,...
'Window','hanning',...
option.band,'Power');
end
end
function e = main(orig,option,postoption)
if iscell(orig)
orig = orig{1};
end
if isamir(orig,'mirscalar')
d = get(orig,'Data');
fp = get(orig,'FramePos');
for i = 1:length(d)
for j = 1:length(d{i})
d{i}{j} = reshape(d{i}{j},size(d{i}{j},2),1,size(d{i}{j},3));
p{i}{j} = mean(fp{i}{j})';
end
end
e.downsampl = 0;
e.hwr = 0;
e.diff = 0;
e.method = 'Spectro';
e.phase = {{}};
e = class(e,'mirenvelope',mirtemporal(orig));
e = set(e,'Title','Envelope','Data',d,'Pos',p,'FramePos',{{}});
postoption.trim = 0;
postoption.ds = 0;
e = post(e,postoption);
return
end
if isfield(option,'presel') && ischar(option.presel) && ...
strcmpi(option.presel,'Klapuri06')
option.method = 'Spectro';
postoption.up = 2;
postoption.mu = 100;
postoption.diffhwr = 1;
postoption.lambda = .8;
end
if isfield(postoption,'ds') && isnan(postoption.ds)
if option.decim
postoption.ds = 0;
else
postoption.ds = 16;
end
end
if not(isfield(option,'filter')) || not(ischar(option.filter))
e = post(orig,postoption);
elseif strcmpi(option.method,'Spectro')
d = get(orig,'Data');
fp = get(orig,'FramePos');
sr = get(orig,'Sampling');
ch = get(orig,'Channels');
ph = get(orig,'Phase');
for h = 1:length(d)
sr{h} = 0;
for i = 1:length(d{h})
if size(d{h}{i},3)>1 % Already in bands (channels in 3d dim)
d{h}{i} = permute(sum(d{h}{i}),[2 1 3]);
if ~isempty(ph)
ph{h}{i} = permute(ph{h}{i},[2 1 3]);
end
else % Simple spectrogram, frequency range sent to 3d dim
d{h}{i} = permute(d{h}{i},[2 3 1]);
if ~isempty(ph)
ph{h}{i} = permute(ph{h}{i},[2 3 1]);
end
end
p{h}{i} = mean(fp{h}{i})';
if not(sr{h}) && size(fp{h}{i},2)>1
sr{h} = 1/(fp{h}{i}(1,2)-fp{h}{i}(1,1));
end
end
if not(sr{h})
warning('WARNING IN MIRENVELOPE: The frame decomposition did not succeed. Either the input is of too short duration, or the chunk size is too low.');
end
ch{h} = (1:size(d{h}{1},3))';
end
e.downsampl = 0;
e.hwr = 0;
e.diff = 0;
%e.todelete = {};
e.method = 'Spectro';
e.phase = ph;
e = class(e,'mirenvelope',mirtemporal(orig));
e = set(e,'Title','Envelope','Data',d,'Pos',p,...
'Sampling',sr,'Channels',ch);
postoption.trim = 0;
postoption.ds = 0;
e = post(e,postoption);
else
if isnan(option.zp)
if strcmpi(option.filter,'IIR')
option.zp = 1;
else
option.zp = 0;
end
end
if option.zp == 1
option.decim = 0;
end
e.downsampl = 1;
e.hwr = 0;
e.diff = 0;
e.method = option.filter;
e.phase = {};
e = class(e,'mirenvelope',mirtemporal(orig));
e = purgedata(e);
e = set(e,'Title','Envelope');
sig = get(e,'Data');
x = get(e,'Pos');
sr = get(e,'Sampling');
disp('Extracting envelope...')
d = cell(1,length(sig));
for k = 1:length(sig)
if length(sig)==1
[state e] = gettmp(orig,e);
else
state = [];
end
if option.decim
sr{k} = sr{k}/option.decim;
end
if strcmpi(option.filter,'IIR')
a2 = exp(-1/(option.tau*sr{k})); % filter coefficient
a = [1 -a2];
b = 1-a2;
elseif strcmpi(option.filter,'HalfHann')
a = 1;
b = hann(sr{k}*.4);
b = b(ceil(length(b)/2):end);
end
d{k} = cell(1,length(sig{k}));
for i = 1:length(sig{k})
sigi = sig{k}{i};
if option.zp == 2
sigi = flipdim(sigi,1);
end
if option.hilb
try
for h = 1:size(sigi,2)
for j = 1:size(sigi,3)
sigi(:,h,j) = hilbert(sigi(:,h,j));
end
end
catch
disp('Signal Processing Toolbox does not seem to be installed. No Hilbert transform.');
end
end
sigi = abs(sigi);
if option.decim
dsigi = zeros(ceil(size(sigi,1)/option.decim),...
size(sigi,2),size(sigi,3));
for f = 1:size(sigi,2)
for c = 1:size(sigi,3)
dsigi(:,f,c) = decimate(sigi(:,f,c),option.decim);
end
end
sigi = dsigi;
clear dsigi
x{k}{i} = x{k}{i}(1:option.decim:end,:,:);
end
% tmp = filtfilt(1-a,[1 -a],sigi); % zero-phase IIR filter for smoothing the envelope
% Manual filtfilt
emptystate = isempty(state);
tmp = zeros(size(sigi));
for c = 1:size(sigi,3)
if emptystate
[tmp(:,:,c) state(:,c,1)] = filter(b,a,sigi(:,:,c));
else
[tmp(:,:,c) state(:,c,1)] = filter(b,a,sigi(:,:,c),...
state(:,c,1));
end
end
tmp = max(tmp,0); % For security reason...
if option.zp == 2
tmp = flipdim(tmp,1);
end
d{k}{i} = tmp;
%td{k} = round(option.tau*sr{k}*1.5);
end
end
e = set(e,'Data',d,'Pos',x,'Sampling',sr); %,'ToDelete',td
if length(sig)==1
e = settmp(e,state);
end
if not(option.zp == 2)
e = post(e,postoption);
end
end
if isfield(option,'presel') && ischar(option.presel) && ...
strcmpi(option.presel,'Klapuri06')
e = mirsum(e,'Adjacent',10);
end
function e = post(e,postoption)
if isempty(postoption)
return
end
if isfield(postoption,'lambda') && not(postoption.lambda)
postoption.lambda = 1;
end
d = get(e,'Data');
tp = get(e,'Time');
sr = get(e,'Sampling');
ds = get(e,'DownSampling');
ph = get(e,'Phase');
for k = 1:length(d)
if isfield(postoption,'sampling')
if postoption.sampling
newsr = postoption.sampling;
elseif isfield(postoption,'ds') && postoption.ds>1
newsr = sr{k}/postoption.ds;
else
newsr = sr{k};
end
end
if isfield(postoption,'up') && postoption.up
[z,p,gain] = butter(6,10/newsr/postoption.up*2,'low');
[sos,g] = zp2sos(z,p,gain);
Hd = dfilt.df2tsos(sos,g);
end
for i = 1:length(d{k})
if isfield(postoption,'sampling') && postoption.sampling
if and(sr{k}, not(sr{k} == postoption.sampling))
dk = d{k}{i};
for j = 1:size(dk,3)
if not(sr{k} == round(sr{k}))
mirerror('mirenvelope','The ''Sampling'' postoption cannot be used after using the ''Down'' postoption.');
end
rk(:,:,j) = resample(dk(:,:,j),postoption.sampling,sr{k});
end
d{k}{i} = rk;
tp{k}{i} = repmat((0:size(d{k}{i},1)-1)',...
[1 1 size(tp{k}{i},3)])...
/postoption.sampling + tp{k}{i}(1,:,:);
if not(iscell(ds))
ds = cell(length(d));
end
ds{k} = round(sr{k}/postoption.sampling);
end
elseif isfield(postoption,'ds') && postoption.ds>1
if not(postoption.ds == round(postoption.ds))
mirerror('mirenvelope','The ''Down'' sampling rate should be an integer.');
end
ds = postoption.ds;
tp{k}{i} = tp{k}{i}(1:ds:end,:,:); % Downsampling...
d{k}{i} = d{k}{i}(1:ds:end,:,:);
end
if isfield(postoption,'sampling')
if not(strcmpi(e.method,'Spectro')) && postoption.trim
tdk = round(newsr*.1);
d{k}{i}(1:tdk,:,:) = repmat(d{k}{i}(tdk,:,:),[tdk,1,1]);
d{k}{i}(end-tdk+1:end,:,:) = repmat(d{k}{i}(end-tdk,:,:),[tdk,1,1]);
end
if postoption.log
d{k}{i} = log10(d{k}{i});
end
if postoption.mu
dki = max(0,d{k}{i});
mu = postoption.mu;
dki = log(1+mu*dki)/log(1+mu);
dki(~isfinite(d{k}{i})) = NaN;
d{k}{i} = dki;
end
if postoption.power
d{k}{i} = d{k}{i}.^2;
end
if postoption.up
dki = zeros(size(d{k}{i},1).*postoption.up,...
size(d{k}{i},2),size(d{k}{i},3));
dki(1:postoption.up:end,:,:) = d{k}{i};
dki = filter(Hd,[dki;...
zeros(6,size(d{k}{i},2),size(d{k}{i},3))]);
d{k}{i} = dki(1+ceil(6/2):end-floor(6/2),:,:);
tki = zeros(size(tp{k}{i},1).*postoption.up,...
size(tp{k}{i},2),...
size(tp{k}{i},3));
dt = repmat((tp{k}{i}(2)-tp{k}{i}(1))...
/postoption.up,...
[size(tp{k}{i},1),1,1]);
for j = 1:postoption.up
tki(j:postoption.up:end,:,:) = tp{k}{i}+dt*(j-1);
end
tp{k}{i} = tki;
newsr = sr{k}*postoption.up;
end
if (postoption.diffhwr || postoption.diff) && ...
not(get(e,'Diff'))
tp{k}{i} = tp{k}{i}(1:end-1,:,:);
order = max(postoption.diffhwr,postoption.diff);
if postoption.complex
dph = diff(ph{k}{i},2);
dph = dph/(2*pi);% - round(dph/(2*pi));
ddki = sqrt(d{k}{i}(3:end,:,:).^2 + d{k}{i}(2:end-1,:,:).^2 ...
- 2.*d{k}{i}(3:end,:,:)...
.*d{k}{i}(2:end-1,:,:)...
.*cos(dph));
d{k}{i} = d{k}{i}(2:end,:,:);
tp{k}{i} = tp{k}{i}(2:end,:,:);
elseif order == 1
ddki = diff(d{k}{i},1,1);
else
b = firls(order,[0 0.9],[0 0.9*pi],'differentiator');
ddki = filter(b,1,...
[repmat(d{k}{i}(1,:,:),[order,1,1]);...
d{k}{i};...
repmat(d{k}{i}(end,:,:),[order,1,1])]);
ddki = ddki(order+1:end-order-1,:,:);
end
if postoption.diffhwr
ddki = hwr(ddki);
end
d{k}{i} = (1-postoption.lambda)*d{k}{i}(1:end-1,:,:)...
+ postoption.lambda*sr{k}/10*ddki;
end
if postoption.aver
y = filter(ones(1,postoption.aver),1,...
[d{k}{i};zeros(postoption.aver,...
size(d{k}{i},2),...
size(d{k}{i},3))]);
d{k}{i} = y(1+ceil(postoption.aver/2):...
end-floor(postoption.aver/2),:,:);
end
if postoption.gauss
sigma = postoption.gauss;
gauss = 1/sigma/2/pi...
*exp(- (-4*sigma:4*sigma).^2 /2/sigma^2);
y = filter(gauss,1,[d{k}{i};zeros(4*sigma,1,size(d{k}{i},3))]);
y = y(4*sigma:end,:,:);
d{k}{i} = y(1:size(d{k}{i},1),:,:);
end
if postoption.chwr
d{k}{i} = center(d{k}{i});
d{k}{i} = hwr(d{k}{i});
end
if postoption.hwr
d{k}{i} = hwr(d{k}{i});
end
if postoption.c
d{k}{i} = center(d{k}{i});
end
if postoption.norm
d{k}{i} = d{k}{i}./repmat(max(abs(d{k}{i})),...
[size(d{k}{i},1),1,1]);
end
end
end
if isfield(postoption,'sampling')
sr{k} = newsr;
end
end
if isfield(postoption,'ds') && postoption.ds>1
e = set(e,'DownSampling',postoption.ds,'Sampling',sr);
elseif isfield(postoption,'sampling') && postoption.sampling
e = set(e,'DownSampling',ds,'Sampling',sr);
elseif isfield(postoption,'up') && postoption.up
e = set(e,'Sampling',sr);
end
if isfield(postoption,'sampling')
if postoption.hwr
e = set(e,'Halfwave',1);
end
if postoption.diff
e = set(e,'Diff',1,'Halfwave',0,'Title','Differentiated envelope');
end
if postoption.diffhwr
e = set(e,'Diff',1,'Halfwave',1,'Centered',0);
end
if postoption.c
e = set(e,'Centered',1);
end
if postoption.chwr
e = set(e,'Halfwave',1,'Centered',1);
end
end
e = set(e,'Data',d,'Time',tp);
%if isfield(postoption,'frame') && isstruct(postoption.frame)
% e = mirframenow(e,postoption);
%end