├── interpolation ├── cspline2_init.m ├── cspline2_step.m └── cspline.m ├── utils ├── horner.m ├── gen_sine_sweep.m ├── add_harmonic_marks.m ├── myfft.m └── myspec.m ├── README.md ├── metrics ├── metrics_sin.m ├── bandlimit_signal.m └── calc_nmr.m ├── hard_clipper.m └── diode_clipper.m /interpolation/cspline2_init.m: -------------------------------------------------------------------------------- 1 | % initialise strictly causal cubic spline in Meinsma et al. formulation 2 | % input: 3 | % u1 --- initial value. 4 | % output: 5 | % mem --- initial coefficients for pre-filter. 6 | function mem = cspline2_init(u1) 7 | mem = [(4-sqrt(3))*u1; (3-sqrt(3))*u1]; 8 | end -------------------------------------------------------------------------------- /utils/horner.m: -------------------------------------------------------------------------------- 1 | % evaluate polynomial using Horner's rule 2 | % input: 3 | % x --- input value or vector; 4 | % coefs --- polynomial coefficients (from the highest degree to the lowest). 5 | % output: 6 | % y --- output. 7 | function y = horner(x, coefs) 8 | y = coefs(1).*x; 9 | for i = 2:length(coefs)-1 10 | y = (y + coefs(i)).*x; 11 | end 12 | y = y + coefs(end); 13 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interpolation Filters for Antiderivative Antialiasing 2 | 3 | This repository includes accompanying code for the DAFx24 paper "Interpolation Filters for Antiderivative Antialiasing". You can access the full text [here](https://www.researchgate.net/publication/383789514_Interpolation_Filters_for_Antiderivative_Antialiasing). 4 | 5 | The AA-IIR method with linear and cubic interpolation is applied for simulation of the hard clipping nonlinearity (`hard_clipper.m`) and the diode clipper circuit (`diode_clipper.m`). 6 | 7 | Included libraries are `interpolation` for cubic interpolation methods, `metrics` for signal-to-noise ratio (SNR) and noise-to-mask ratio (NMR) calculation, `utils` for miscellaneous functions (plotting, signal generation, etc.). 8 | -------------------------------------------------------------------------------- /utils/gen_sine_sweep.m: -------------------------------------------------------------------------------- 1 | % generate linear sine sweep 2 | % input: 3 | % t_dur --- sweep duration [sec]; 4 | % fs --- sample rate [Hz]; 5 | % f0 --- lowest sweep frequency [Hz]; 6 | % f1 --- highest sweep frequency [Hz]; 7 | % amp --- maximum amplitude of the sweep. 8 | % output: 9 | % t --- time vector; 10 | % x --- sweep signal. 11 | function [t,x] = gen_sine_sweep(t_dur, fs, f0, f1, amp) 12 | % calculate sweep parameters 13 | t = (0:1/fs:t_dur).'; 14 | 15 | % generate sweep 16 | sweep_arg = 2*pi*f0*t + 2*pi*t.*(t/t_dur)*(f1-f0)/2; 17 | x = amp*sin(sweep_arg); 18 | len_x = length(x); 19 | 20 | % add fade-in/out 21 | t_fade = 0.1; 22 | len_fade = t_fade*fs; 23 | fade = 0.5*(1 - cos(2*pi.*(0:len_fade-1)./(2*len_fade))).'; 24 | fade = [fade; ones(len_x-2*len_fade, 1); flipud(fade)]; 25 | x = x.*fade; 26 | end -------------------------------------------------------------------------------- /metrics/metrics_sin.m: -------------------------------------------------------------------------------- 1 | % calculate SNR and NMR for a nonlinearly processed sine signal 2 | % input: 3 | % sig --- test signal; 4 | % fs --- sample rate [Hz]; 5 | % f0 --- fundamental frequency of input sine [Hz]; 6 | % IS_SYM --- symmetry flag for nonlinearity; 7 | % APPLY_LP --- low-pass filter flag (optional); 8 | % PLOT_SIG --- flag to plot input and bandlimited signals (optional). 9 | % output: 10 | % snr --- signal-to-noise ratio; 11 | % nmr --- noise-to-mask ratio. 12 | function [snr, nmr] = metrics_sin(sig, fs, f0, IS_SYM, varargin) 13 | % bit truncation for NMR calculation 14 | bit = 24; 15 | 16 | % obtain bandlimited version of a signal 17 | [sig, sig_lim, alias] = bandlimit_signal(sig, fs, f0, IS_SYM, varargin{:}); 18 | 19 | % calclate SNR via Parseval's theorem 20 | snr = 10*log10(sum(sig_lim.^2)./sum(alias.^2)); 21 | 22 | % calculate NMR 23 | nmr = calc_nmr(sig, sig_lim, fs, bit); 24 | end -------------------------------------------------------------------------------- /utils/add_harmonic_marks.m: -------------------------------------------------------------------------------- 1 | % add harmonic marks to a spectrum plot of a nonlinearly processed sine signal 2 | % input: 3 | % Y --- FFT; 4 | % fig_fft --- FFT plot; 5 | % fs --- sampling rate [Hz]; 6 | % f0 --- fundamental frequency [Hz]; 7 | % IS_SYM --- symmetry flag for nonlinearity. 8 | function [] = add_harmonic_marks(Y, fig_fft, fs, f0, IS_SYM) 9 | % calculate harmonics 10 | num_harmonics = floor(0.5*fs/f0); 11 | if IS_SYM == true 12 | % even harmonics for symmetric nonlinearity 13 | harmonics = f0 * (1:2:num_harmonics).'; 14 | else 15 | % even + odd harmonics for asymmetric nonlinearity 16 | harmonics = f0 * (1:num_harmonics).'; 17 | end 18 | 19 | % extract magnitude and phase 20 | NFFT_2 = length(Y); 21 | NFFT = 2*(NFFT_2-1); 22 | bins = round(harmonics*NFFT/fs) + 1; 23 | 24 | % plot 25 | figure(fig_fft); hold on; 26 | fvec = (0:fs/NFFT:fs/2).'; 27 | plot(fvec(bins), 20*log10(abs(Y(bins))./max(abs(Y))), 'kx', 'Linewidth', 0.5); 28 | end -------------------------------------------------------------------------------- /interpolation/cspline2_step.m: -------------------------------------------------------------------------------- 1 | % calculate step of strictly causal cubic spline in Meinsma et al. formulation [1] 2 | % input: 3 | % u --- next sample; 4 | % u1 --- current sample; 5 | % fs --- sampling rate [Hz]; 6 | % mem --- memory bus (previous pre-filter output). 7 | % output: 8 | % coefs --- polynomial coefficients (from the highest degree to the lowest); 9 | % mem --- updated memory bus. 10 | % references: 11 | % [1] G. Meinsma and L. Mirkin, "L2 Sampled signal reconstruction with 12 | % causality constraints - Part I: Setup and solutions," in IEEE 13 | % Transactions on Signal Processing, vol. 60, no. 5, pp. 2260-2272, 14 | % May 2012, doi: 10.1109/TSP.2012.2185228. 15 | function [coefs, mem] = cspline2_step(u, u1, fs, mem) 16 | %% parameters 17 | alpha = sqrt(3) - 2; 18 | w11_1 = mem(1); 19 | w12_1 = mem(2); 20 | 21 | %% prefilter 22 | % w1 (future by 1 sample) 23 | w11 = alpha*w11_1 + (4-sqrt(3))*u + alpha*(2+sqrt(3))*u1; 24 | w12 = alpha*w12_1 + (3-sqrt(3))*u + alpha*(3+sqrt(3))*u1; 25 | 26 | % w2 (current) 27 | w21_1 = 6*fs^3*(-4+3*sqrt(3))*u + ... 28 | alpha*fs^3*(6*sqrt(3))*w11 + ... 29 | alpha*fs^3*(3-3*sqrt(3))*w12 + ... 30 | fs^3*(6-6*sqrt(3))*w11_1 + ... 31 | fs^3*(3*sqrt(3)-3)*w12_1; 32 | w22_1 = 0; 33 | 34 | %% output 35 | % current (curve between u1 and u) 36 | coefs = [-w21_1/6,... 37 | (w21_1 + w22_1)/(2*fs),... 38 | (w21_1 + w22_1)/(2*sqrt(3)*fs^2) + w12_1*fs,... 39 | w11_1 - w12_1]; 40 | 41 | %% write to memory 42 | mem = [w11; w12]; 43 | end -------------------------------------------------------------------------------- /utils/myfft.m: -------------------------------------------------------------------------------- 1 | % calculate and plot FFT 2 | % input: 3 | % y --- input signal; 4 | % Fs --- sampling rate [Hz]; 5 | % window --- window; 6 | % NORM_MAG --- normalise magnitude (optional); 7 | % fig_fft --- figure handle (optional); 8 | % clr --- line color (optional). 9 | % output: 10 | % Y --- FFT; 11 | % fig_fft --- FFT plot. 12 | function [Y, fig_fft] = myfft(y, Fs, window, varargin) 13 | % check for 0 dB normalisation 14 | if length(varargin) >= 1 15 | NORM_MAG = varargin{1}; 16 | else 17 | NORM_MAG = true; 18 | end 19 | 20 | % check prespecified figure 21 | if length(varargin) >= 2 22 | fig_fft = varargin{2}; 23 | USE_FIG = true; 24 | else 25 | fig_fft = figure; 26 | USE_FIG = false; 27 | end 28 | 29 | % check prespecified color 30 | if length(varargin) >= 3 31 | clr = varargin{3}; 32 | end 33 | USE_COLOR = exist('clr', 'var'); 34 | 35 | % calculate FFT parameters 36 | N = length(y); 37 | NFFT = 2^(ceil(log(N)/log(2))); 38 | NFFT_2 = NFFT / 2 + 1; 39 | 40 | % generate window 41 | if strcmp(window, "hann") 42 | win = hann(N, 'periodic'); 43 | elseif strcmp(window, "hamming") 44 | win = hamming(N, 'periodic'); 45 | elseif strcmp(window, "blackman") 46 | win = blackman(N, 'periodic'); 47 | end 48 | 49 | % calculate FFT 50 | Y = fft(y.*win, NFFT); 51 | Y = Y(1:NFFT_2); 52 | 53 | % plot FFT 54 | figure(fig_fft); 55 | fvec = (0:Fs/NFFT:Fs/2).'; 56 | if NORM_MAG == true 57 | YdB = 20*log10(abs(Y)./max(abs(Y))); 58 | max_dB = 0; 59 | else 60 | YdB = 20*log10(abs(Y)); 61 | max_dB = max(YdB); 62 | end 63 | plt = plot(fvec, YdB, 'k', 'Linewidth', 0.5); 64 | if USE_COLOR == true 65 | set(plt, 'Color', clr); 66 | end 67 | if USE_FIG == false 68 | xlim([0 Fs/2]); 69 | ylim([-60+max_dB, 5+max_dB]); 70 | xlabel('Frequency [Hz]', 'Interpreter', 'latex'); 71 | ylabel('Magnitude [dB]', 'Interpreter', 'latex'); 72 | title('Spectrum', 'Interpreter', 'latex'); 73 | ax_spec = fig_fft.CurrentAxes; 74 | set(ax_spec.XAxis, 'TickLabelInterpreter', 'latex'); 75 | set(ax_spec.YAxis, 'TickLabelInterpreter', 'latex'); 76 | end 77 | end -------------------------------------------------------------------------------- /utils/myspec.m: -------------------------------------------------------------------------------- 1 | % create a spectogram plot of an input signal 2 | % input: 3 | % x --- mono input signal; 4 | % Fs --- sampling rate [Hz]; 5 | % N --- frame length; 6 | % O --- overlap factor (between 0 and 1); 7 | % window --- window; 8 | % PLOT_SPEC --- plot flag (optional). 9 | % output: 10 | % STFT --- short time Fourier transform; 11 | % fig_spec --- figure handle (optional). 12 | function [STFT, varargout] = myspec(x, Fs, N, O, window, varargin) 13 | % check plot flag 14 | if length(varargin) >= 1 15 | PLOT_SPEC = varargin{1}; 16 | else 17 | PLOT_SPEC = true; 18 | end 19 | 20 | % find hop size 21 | HA = round(N - O*N); 22 | 23 | % generate window 24 | if strcmp(window, "hann") 25 | win = hann(N, 'periodic'); 26 | elseif strcmp(window, "hamming") 27 | win = hamming(N, 'periodic'); 28 | elseif strcmp(window, "blackman") 29 | win = blackman(N, 'periodic'); 30 | end 31 | 32 | % calculate number of frames 33 | L = length(x); 34 | NF = ceil(L/HA); 35 | x = [x; zeros((NF-1)*HA+N-L,1)]; 36 | 37 | % STFT size 38 | NFFT = 2^(ceil(log(N)/log(2))); % next power of 2 39 | NFFT_2 = NFFT / 2 + 1; 40 | 41 | % calculate STFT 42 | STFT = zeros(NFFT_2, NF); 43 | for m = 0:NF-1 44 | x_frame = win.*x((1:N).'+m*HA); 45 | X = fft(x_frame, NFFT); 46 | STFT(:,m+1) = X(1:NFFT_2); 47 | end 48 | 49 | % plot spectogram 50 | if PLOT_SPEC == true 51 | fig_spec = figure; 52 | t = ((0:NF-1).*HA/Fs).'; 53 | freq = (0:Fs/NFFT:Fs/2).'; 54 | STFT_dB = 20*log10(abs(STFT)./max(abs(STFT))); 55 | imagesc(t, freq, STFT_dB, 'CDataMapping', 'scaled'); 56 | c = colorbar; 57 | c.Label.String = 'dB'; 58 | colormap hot 59 | clim([-60 0]); 60 | xlim([0 t(end)]); 61 | ylim([0 freq(end)]); 62 | ax_spec = fig_spec.CurrentAxes; 63 | set(ax_spec, 'YDir', 'normal'); 64 | set(ax_spec, 'YTick', 0:5000:Fs/2); 65 | set(ax_spec, 'YTickLabel', 0:5:Fs/2/1e3); 66 | xlabel('Time [s]', 'interpreter', 'latex'); 67 | ylabel('Frequency [kHz]', 'interpreter', 'latex'); 68 | title_str = sprintf("Spectogram with frame length = $%d$ ms and overlap factor = $%d$\\%%", floor((N/Fs)*1e3), O*1e2); 69 | title(title_str, 'interpreter', 'latex'); 70 | set(ax_spec.XAxis, 'TickLabelInterpreter', 'latex'); 71 | set(ax_spec.YAxis, 'TickLabelInterpreter', 'latex'); 72 | set(c, 'TickLabelInterpreter', 'latex'); 73 | set(c.Label, 'Interpreter', 'latex'); 74 | varargout{1} = fig_spec; 75 | end 76 | end -------------------------------------------------------------------------------- /interpolation/cspline.m: -------------------------------------------------------------------------------- 1 | % causal cubic spline based on truncation of anticausal IIR filter [1,2] 2 | % (cascade pre-filter and De Boor's output filter) 3 | % input: 4 | % t --- time vector (column); 5 | % u --- input signal (column); 6 | % fs --- sampling rate [Hz]; 7 | % M --- pre-filter truncation. 8 | % output: 9 | % pp --- piecewise cubic polynomial approximation of input signal. 10 | % references: 11 | % [1] Petrinovic, Davor. (2008). Causal Cubic Splines: Formulations, 12 | % Interpolation Properties and Implementations. Signal Processing, 13 | % IEEE Transactions on. 56. 5442 - 5453. 10.1109/TSP.2008.929133. 14 | % [2] Petrinovic, D.. (2009). Continuous time domain properties of causal 15 | % cubic splines. Signal Processing. 89. 1941-1958. 16 | % 10.1016/j.sigpro.2009.03.031. 17 | function pp = cspline(t,u,fs,M) 18 | % filters parameters 19 | alpha = -2 + sqrt(3); 20 | s = 1;%1/(1-alpha^(M+1)); 21 | 22 | % direct casual B-spline cascade filter coefficients 23 | b = 1; 24 | a = [1 -alpha]; 25 | b_fir = alpha.^(M:-1:1).'; 26 | 27 | %% cascade casual B-spline filter + De Boor's output filter 28 | % FIR filter 29 | u_fir = filter(b_fir,1,u); 30 | u_zM = zeroshift(u,M,1); 31 | 32 | % IIR filter 33 | w_tmp = -alpha*s*(u_fir+u_zM); 34 | w = filter(b,a,w_tmp); 35 | 36 | % output filter (De Boor's formulation) 37 | b31 = [3 3 -3 -3]*fs^3; 38 | b32 = [0 -2 2] *fs^3; 39 | b21 = [-3 -6 3 6]*fs^2; 40 | b22 = [0 3 -3] *fs^2; 41 | b11 = [0 3 0 -3] *fs; 42 | 43 | coefs = zeros(length(u), 4); 44 | coefs(:,1) = filter(b31,1,w) + filter(b32,1,u_zM); 45 | coefs(:,2) = filter(b21,1,w) + filter(b22,1,u_zM); 46 | coefs(:,3) = filter(b11,1,w); 47 | coefs(:,4) = zeroshift(u_zM,2,1); 48 | 49 | % compensate delay and remove first segment 50 | % (output FIR filters require one past sample => n = 1 is undefied) 51 | del = M+2; 52 | coefs = zeroshift(coefs,-del,1); 53 | coefs(1,:) = 0; 54 | coefs = coefs(1:end-1,:); 55 | breaks = t; 56 | 57 | % create piecewise polynomial 58 | pp = mkpp(breaks, coefs); 59 | end 60 | 61 | %% FUNCTIONS 62 | % shift 2d array values and fill boundary with zeros 63 | % input: 64 | % in --- input 2d array; 65 | % l --- integer shift (positive shifts toward the end and negative shifts toward the beginning); 66 | % d --- dimension (d = 1 to shift rows, d = 2 to shift columns). 67 | % output: 68 | % out --- shifted array. 69 | function out = zeroshift(in,l,d) 70 | out = circshift(in,l,d); 71 | if l > 0 72 | if d == 1 73 | out(1:l,:) = 0; 74 | elseif d == 2 75 | out(:,1:l) = 0; 76 | end 77 | elseif l < 0 78 | if d == 1 79 | out(end+l+1:end,:) = 0; 80 | elseif d == 2 81 | out(:,end+l+1:end) = 0; 82 | end 83 | end 84 | end -------------------------------------------------------------------------------- /metrics/bandlimit_signal.m: -------------------------------------------------------------------------------- 1 | % calculate bandlimited version of a nonlinearly processed sine signal 2 | % input: 3 | % sig --- test signal; 4 | % fs --- base sample rate [Hz]; 5 | % f0 --- fundamental frequency of input sine [Hz]; 6 | % IS_SYM --- symmetry flag for nonlinearity; 7 | % APPLY_LP --- low-pass filter flag (optional); 8 | % PLOT_SIG --- flag to plot input and bandlimited signals (optional). 9 | % output: 10 | % sig --- test signal (truncated and DC compensated => aligned with 11 | % bandlimited version); 12 | % sig_lim --- bandlimited version of a signal; 13 | % alias --- aliases from a signal. 14 | % references: 15 | % [1] Carson, A. (2020). "Aliasing Reduction in Virtual Analogue Modelling". 16 | % thesis: https://www.researchgate.net/publication/344362244_Aliasing_Reduction_in_Virtual_Analogue_Modelling 17 | % code: was provided via email request. 18 | % [2] Kahles, J., Esqueda, F., & Valimaki, V. (2019). "Oversampling for 19 | % Nonlinear Waveshaping: Choosing the Right Filters". Journal of the 20 | % Audio Engineering Society. 21 | % https://www.aes.org/tmpFiles/elib/20230722/20485.pdf 22 | function [sig, sig_lim, alias] = bandlimit_signal(sig, fs, f0, IS_SYM, varargin) 23 | % parameters 24 | sidelobe_att = 120; % chebyshev window sidelobe attenuation [dB] 25 | dc_thres = 40; % DC detection threshold [dB] 26 | 27 | % check filtering flag 28 | if length(varargin) >= 1 29 | APPLY_LP = varargin{1}; 30 | else 31 | APPLY_LP = false; 32 | end 33 | 34 | % check plot flag for debugging 35 | if length(varargin) >= 2 36 | PLOT_SIG = varargin{2}; 37 | else 38 | PLOT_SIG = false; 39 | end 40 | 41 | % filter input signal [1] 42 | if APPLY_LP == true 43 | % use the same filter as in decimate() for oversampled algorithms 44 | [b,a] = cheby1(8, 0.05, 0.8); 45 | sig = filter(b, a, sig); 46 | end 47 | 48 | % truncate signal to remove transient from initial condition [1] 49 | % and leave one-second fragment [2] 50 | if length(sig) > fs 51 | sig = sig(end-fs+1:end); 52 | else 53 | warning('Signal is too short for robust metrics calculation!'); 54 | end 55 | N = length(sig); 56 | 57 | % check odd length 58 | assert(rem(N,2) == 0, 'Signal should have an odd length after truncation!'); 59 | 60 | % calculate harmonics [1] 61 | num_harmonics = floor(0.5*fs/f0); 62 | if IS_SYM == true 63 | % even harmonics for symmetric nonlinearity 64 | harmonics = f0 * (1:2:num_harmonics).'; 65 | else 66 | % even + odd harmonics for asymmetric nonlinearity 67 | harmonics = f0 * (1:num_harmonics).'; 68 | end 69 | 70 | % Chebyshev window with 120 dB sidelobe attenuation [2] 71 | win = chebwin(N, sidelobe_att); 72 | 73 | % calculate single-sided spectrum 74 | S = fft(win.*sig, N); % calculate FFT 75 | S = S(1:N/2+1); % keep only the half for the real signal 76 | 77 | % adjust for scalloping loss (including window) 78 | bins_exact = (harmonics*N/fs) + 1; % exact bins for harmonics 79 | bins = round(bins_exact); % discrete bins for harmonics 80 | d = bins_exact - bins; % difference between exact and discrete bins 81 | idx = (0:N-1).'; % indexes for summation 82 | amps = zeros(size(bins)); % array for storing adjusted FFT data 83 | phases = zeros(size(bins)); 84 | for i = 1:length(bins) 85 | % calculate adjusted FFT value at discrete bin 86 | tmp = S(bins(i))/sum(win.*exp(1j*2*pi*d(i)/N.*idx)); 87 | 88 | % extract magnitude and phase 89 | amps(i) = 2*abs(tmp); % multiplying by 2 for a real-valued signal 90 | phases(i) = angle(tmp); 91 | end 92 | 93 | % synthesise bandlimited signal [1] 94 | t = ((0:N-1)/fs).'; 95 | sig_lim = zeros(size(t)); 96 | for i = 1:length(harmonics) 97 | sig_lim = sig_lim + amps(i)*cos(2*pi*harmonics(i)*t + phases(i)); 98 | end 99 | 100 | % remove DC if needed 101 | if 20*log10(abs(S(1))./max(abs(S))) > -dc_thres 102 | disp("bandlimit_signal.m: DC detected!"); 103 | DC = abs(S(1)/sum(win)); 104 | sig = sig - DC; 105 | end 106 | 107 | % calculate aliased components [1] 108 | alias = sig - sig_lim; 109 | 110 | % debug plots 111 | if PLOT_SIG == true 112 | fig_sig = figure; hold on; 113 | plot(sig, 'color', "#e6194b"); 114 | plot(sig_lim, 'color', "#3cb44b"); 115 | plot(alias, 'color', "#4363d8"); 116 | xlim([-100, length(sig)+100]); 117 | xlabel('Samples [n]', 'Interpreter', 'latex'); 118 | ylabel('Amplitude', 'Interpreter', 'latex'); 119 | legend({'Input signal', 'Bandlimited signal', 'Alias signal'}, 'Interpreter', 'latex'); 120 | title(sprintf("Input signal at $f_0 = %.2f$ Hz", f0), 'Interpreter', 'latex'); 121 | ax_spec = fig_sig.CurrentAxes; 122 | set(ax_spec.XAxis, 'TickLabelInterpreter', 'latex'); 123 | set(ax_spec.YAxis, 'TickLabelInterpreter', 'latex'); 124 | end 125 | end -------------------------------------------------------------------------------- /hard_clipper.m: -------------------------------------------------------------------------------- 1 | % ------------------------------------------------------------------------- 2 | % This script considers application of the AA-IIR method with linear or 3 | % cubic interpolation and a Chebyshev type 1 antialiasing filter 4 | % for the hard clipping nonlinearity. 5 | % 6 | % Input signal can be either a sine tone or a sine sweep. For a sine 7 | % tone input signal, signal-to-noise ratio (SNR) and noise-to-mask ratio 8 | % (NMR) are computed for an output signal to measure aliasing. 9 | % 10 | % For cubic AA-IIR, the midpoint quadrature is used for integral 11 | % calculation. 12 | % 13 | % Author: Victor Zheleznov 14 | % ------------------------------------------------------------------------- 15 | 16 | clear all; close all; 17 | 18 | % add libraries 19 | addpath("interpolation"); 20 | addpath("metrics"); 21 | addpath("utils"); 22 | 23 | %% parameters 24 | % global parameters 25 | fs = 44.1e3; % base sampling rate [Hz] 26 | 27 | % input signal parameters 28 | input = "sweep"; % "sine", "sweep" 29 | t_dur = 5; % signal duration [sec] 30 | amp = 10; % amplitude 31 | if strcmp(input, "sine") == 1 32 | f0 = 2.^((83-69)/12) * 440; % input sine frequency [Hz] 33 | elseif strcmp(input, "sweep") == 1 34 | f0 = 20; % lowest sweep frequency [Hz] 35 | f1 = 22e3; % highest sweep frequency [Hz] 36 | end 37 | 38 | % interpolation parameters 39 | interp = "cubic"; % "linear", "cubic" 40 | L = 6; % look-ahead for cubic interpolation 41 | 42 | % antialiasing filter parameters (Chebyshev type 1 filter) 43 | K = 8; % order 44 | fc = 0.4; % normalised cutoff 45 | Rp = 0.05; % passband ripple [dB] 46 | 47 | % numerical integration (for cubic AA-IIR) 48 | M = 8; % number of quadrature points 49 | 50 | % algorithm tolerance (for linear AA-IIR) 51 | tol = 1e-12; 52 | 53 | % output trimming to remove boundary effects (look-ahead, filtering etc.) 54 | cut = 100; 55 | 56 | %% pre-processing 57 | % synthesise input 58 | if strcmp(input, "sine") == 1 59 | t = (0:1/fs:t_dur).'; 60 | u = amp.*sin(2*pi*f0*t); 61 | elseif strcmp(input, "sweep") == 1 62 | [t,u] = gen_sine_sweep(t_dur, fs, f0, f1, amp); 63 | end 64 | 65 | % calculate partial fraction decomposition of the antialiasing filter 66 | wc = 2*pi*fc*fs; 67 | [b,a] = cheby1(K, Rp, wc, 's'); 68 | [r,p,k] = residue(b, a); 69 | assert(length(unique(p)) == length(p), "Only distinct poles are supported!"); 70 | 71 | % initialise the AA-IIR method 72 | y = zeros(size(u)); % output of the AA-IIR method 73 | m = 1; % pole counter 74 | if strcmp(interp, "cubic") == 1 75 | pp = cspline(t, u, fs, L-2); % construct interpolating piecewise cubic polynomial 76 | end 77 | 78 | %% processing 79 | % iterate through poles of the antialiasing filter 80 | while m <= length(p) 81 | if strcmp(interp, "linear") == 1 82 | % process linear AA-IIR 83 | y = y + hard_aaiir_linear(u, fs, p(m), r(m), tol); 84 | elseif strcmp(interp, "cubic") == 1 85 | % process cubic AA-IIR 86 | y = y + hard_aaiir_cubic(pp, fs, p(m), r(m), M); 87 | end 88 | 89 | % increment pole index 90 | m = m + 1 + (imag(p(m)) ~= 0); 91 | end 92 | 93 | % add constant term 94 | if ~isempty(k) 95 | assert(length(k) == 1, 'Direct term of degree >= 1 is not supported!') 96 | y = y + k*hard(u); 97 | end 98 | 99 | %% output 100 | % trim the output signal 101 | t = t(cut:end-cut); 102 | y = y(cut:end-cut); 103 | 104 | % plot output waveform 105 | fig_t = figure; hold on; 106 | plot(t, y, 'k'); 107 | xlim([t(1), t(1)+4/f0]); 108 | xlabel("Time [sec]", 'Interpreter', 'latex'); 109 | ylabel("Amplitude", 'Interpreter', 'latex'); 110 | title("Hard clipper waveform for AA-IIR with " + interp + " interpolation", 'Interpreter', 'latex'); 111 | ax_spec = fig_t.CurrentAxes; 112 | set(ax_spec.XAxis, 'TickLabelInterpreter', 'latex'); 113 | set(ax_spec.YAxis, 'TickLabelInterpreter', 'latex'); 114 | 115 | if strcmp(input, "sine") == 1 116 | % calculate metrics 117 | [snr, nmr] = metrics_sin(y, fs, f0, true); 118 | cmd_str_metr = sprintf("SNR = %.2f dB\nNMR = %.2f dB\n", snr, nmr); 119 | title_str_metr = sprintf("SNR = $%.2f$ dB, NMR = $%.2f$ dB", snr, nmr); 120 | disp(cmd_str_metr); 121 | 122 | % plot spectrum 123 | [spec, fig_spec] = myfft(y, fs, "blackman"); 124 | add_harmonic_marks(spec, fig_spec, fs, f0, true); 125 | title("Hard clipper spectrum for AA-IIR with " + interp + " interpolation"); 126 | subtitle(title_str_metr, 'Interpreter', 'latex'); 127 | ylim([-120 10]); 128 | elseif strcmp(input, "sweep") == 1 129 | % plot spectrogram 130 | [spec, fig_spec] = myspec(y, fs, 1024, 0.9921875, "blackman"); 131 | title("Hard clipper spectogram for AA-IIR with " + interp + " interpolation"); 132 | end 133 | 134 | %% FUNCTIONS 135 | % calculate hard clipper output 136 | % input: 137 | % x - input. 138 | % output: 139 | % y - output. 140 | function y = hard(x) 141 | y = min(max(x, -1), 1); 142 | end 143 | 144 | % calculate linear AA-IIR solution for hard clipper 145 | % input: 146 | % x --- input vector; 147 | % fs --- sample rate [Hz]; 148 | % p --- single pole value (real or complex); 149 | % r --- residue value; 150 | % tol --- tolerance value. 151 | % output: 152 | % y --- output of the AA-IIR method. 153 | function y = hard_aaiir_linear(x, fs, p, r, tol) 154 | % calculate parameters 155 | N = length(x); 156 | T = 1/fs; 157 | E = exp(p*T); 158 | F = (E - 1)/p; 159 | 160 | % check real or complex pole 161 | if imag(p) ~= 0 162 | r = 2*r; 163 | end 164 | 165 | % time loop 166 | y = zeros(N,1); 167 | x1 = x(1); 168 | for n = 2:N 169 | % calculate convolution integral (analytical solution) 170 | if (abs(x(n)-x1) < tol) 171 | I = hard(0.5*(x(n)+x1)) * F; 172 | else 173 | if x1 < x(n) 174 | if x(n) <= -1 175 | I = (1 - E)/p; 176 | elseif x1 <= -1 && x(n) > -1 && x(n) <= 1 177 | I = ((x(n)-x1)/T*(exp(p*T*(x(n)+1)/(x(n)-x1)) - 1) - p*(x(n) + E))/p^2; 178 | elseif x1 <= -1 && x(n) > 1 179 | I = ((x(n)-x1)/T*(exp(p*T*(x(n)+1)/(x(n)-x1)) - exp(p*T*(x(n)-1)/(x(n)-x1))) - p*(1+E))/p^2; 180 | elseif x1 > -1 && x(n) <= 1 181 | I = (E*((x(n)-x1)/T + p*x1) - (x(n)-x1)/T -p*x(n))/p^2; 182 | elseif x1 > -1 && x1 <= 1 && x(n) > 1 183 | I = ((x1-x(n))/T*exp(p*T*(x(n)-1)/(x(n)-x1)) + E*((x(n)-x1)/T + p*x1) - p)/p^2; 184 | else 185 | I = (E - 1)/p; 186 | end 187 | else 188 | if x1 <= -1 189 | I = (1 - E)/p; 190 | elseif x1 > -1 && x1 <= 1 && x(n) <= -1 191 | I = ((x1-x(n))/T*exp(p*T*(x(n)+1)/(x(n)-x1)) + E*((x(n)-x1)/T + p*x1) + p)/p^2; 192 | elseif x1 <= 1 && x(n) > -1 193 | I = (E*((x(n)-x1)/T + p*x1) + (x1-x(n))/T - p*x(n))/p^2; 194 | elseif x1 > 1 && x(n) <= -1 195 | I = ((x(n)-x1)/T*(exp(p*T*(x(n)-1)/(x(n)-x1)) - exp(p*T*(x(n)+1)/(x(n)-x1))) + p*(1+E))/p^2; 196 | elseif x1 > 1 && x(n) > -1 && x(n) <= 1 197 | I = (exp(p*T*(x(n)-1)/(x(n)-x1))*(x(n)-x1)/T - p*x(n) - (x(n)-x1)/T + p*E)/p^2; 198 | else 199 | I = (E - 1)/p; 200 | end 201 | end 202 | end 203 | 204 | % calculate AA-IIR step 205 | y(n) = r*I + E*y(n-1); 206 | 207 | % shift input 208 | x1 = x(n); 209 | end 210 | 211 | % check imaginary part 212 | if imag(p) ~= 0 213 | y = real(y); 214 | end 215 | end 216 | 217 | % calculate cubic AA-IIR solution for hard clipper 218 | % input: 219 | % pp --- piecewise cubic polynomial interpolation of input signal; 220 | % fs --- sample rate [Hz]; 221 | % p --- single pole value (real or complex); 222 | % r --- residue value; 223 | % M --- number of quadrature points for numerical integration. 224 | % output: 225 | % y --- output of the AA-IIR method. 226 | function y = hard_aaiir_cubic(pp, fs, p, r, M) 227 | % calculate parameters 228 | N = length(pp.breaks); 229 | T = 1/fs; 230 | E = exp(p*T); 231 | 232 | % define midpoint quadrature 233 | tq = (T/M)*((1:M)-0.5).'; 234 | wq = (T/M)*ones(size(tq)); 235 | 236 | % check real or complex pole 237 | if imag(p) ~= 0 238 | r = 2*r; 239 | end 240 | 241 | % define the integrated expression 242 | f = @(t,idx) hard(horner(t, pp.coefs(idx-1,:))); 243 | g = @(t,idx) f(t,idx).*exp(p*(T-t)); 244 | 245 | % time loop 246 | y = zeros(N,1); 247 | for n = 2:N 248 | % calculate convolution integral (numerical integration) 249 | I = sum(wq.*g(tq,n)); 250 | 251 | % calculate AA-IIR step 252 | y(n) = r*I + E*y(n-1); 253 | end 254 | 255 | % check imaginary part 256 | if imag(p) ~= 0 257 | y = real(y); 258 | end 259 | end -------------------------------------------------------------------------------- /diode_clipper.m: -------------------------------------------------------------------------------- 1 | % ------------------------------------------------------------------------- 2 | % This script considers application of the AA-IIR method with linear or 3 | % cubic interpolation and a first-order Butterworth antialiasing filter 4 | % for simulation of the diode clipper circuit. 5 | % 6 | % Input signal can be either a sine tone or a sine sweep. For a sine 7 | % tone input signal, signal-to-noise ratio (SNR) and noise-to-mask ratio 8 | % (NMR) are computed for an output signal to measure aliasing. 9 | % 10 | % Circuit equation is discretized using the trapezoidal rule. Damped 11 | % Newton-Raphson method is chosen to solve the nonlinear equation. For 12 | % cubic AA-IIR, the trapezoidal quadrature is used for integral 13 | % calculation. 14 | % 15 | % Author: Victor Zheleznov 16 | % ------------------------------------------------------------------------- 17 | 18 | clear all; close all; 19 | 20 | % add libraries 21 | addpath("interpolation"); 22 | addpath("metrics"); 23 | addpath("utils"); 24 | 25 | %% parameters 26 | % global parameters 27 | fs = 44.1e3; % base sampling rate [Hz] 28 | 29 | % input signal parameters 30 | input = "sine"; % "sine", "sweep" 31 | t_dur = 1.5; % duration [sec] 32 | amp = 10; % amplitude [V] 33 | if strcmp(input, "sine") == 1 34 | f0 = 2.^((83-69)/12) * 440; % input frequency [Hz] 35 | elseif strcmp(input, "sweep") == 1 36 | f0 = 20; % lowest sweep frequency [Hz] 37 | f1 = 22e3; % highest sweep frequency [Hz] 38 | end 39 | 40 | % interpolation parameters 41 | interp = "cubic"; % "linear", "cubic" 42 | 43 | % antialiasing filter parameters (first-order Butterworth filter) 44 | fc = 0.33; % normalised cutoff 45 | 46 | % numerical integration (for cubic AA-IIR) 47 | M = 8; % number of quadrature points 48 | 49 | % Newton-Raphson parameters 50 | rel_tol = 1e-12; % relative tolerance 51 | abs_tol = 1e-14; % absolute tolerance 52 | max_iter = 50; % maximum number of iterations 53 | max_subiter = 5; % maximum number of sub-iterations 54 | 55 | % physical parameters 56 | res = 1e3; % resistance [Ohms] 57 | cap = 33e-9; % capacitance [F] 58 | Is = 2.52e-9; % diode saturation current [A] 59 | Vt = 25.83e-3; % diode thermal voltage [V] 60 | Ni = 1.752; % diode ideality factor 61 | 62 | %% pre-processing 63 | % check stability 64 | assert((strcmp(interp, "linear") && fc > 0) || (strcmp(interp, "cubic") && fc >= 0.327), "Compensation filter is unstable!"); 65 | 66 | % synthesise input 67 | if strcmp(input, "sine") == 1 68 | t = (0:1/fs:t_dur).'; 69 | u = amp.*sin(2*pi*f0*t); 70 | elseif strcmp(input, "sweep") == 1 71 | [t,u] = gen_sine_sweep(t_dur, fs, f0, f1, amp); 72 | end 73 | 74 | % define differential equation (dv/dt = Av + Bu + Cf(v)) 75 | A = -1/(res*cap); 76 | B = 1/(res*cap); 77 | C = -1/cap; 78 | 79 | % define coefficient and input gain for nonlinear function sinh() 80 | coef = 2*Is; 81 | gain = 1/(Ni*Vt); 82 | 83 | % precompute discretization constants for trapezoidal rule 84 | T = 1/fs; 85 | Hm = (2/T) - A; 86 | Hp = (2/T) + A; 87 | K = C/Hm; 88 | 89 | % calculate partial fraction decomposition of the antialiasing filter 90 | wc = 2*pi*fc*fs; 91 | [b,a] = butter(1, wc, 's'); 92 | [r,p] = residue(b, a); 93 | 94 | % define compensation filter transfer function 95 | E = exp(p*T); 96 | if strcmp(interp, "linear") == 1 97 | % define linearization 98 | b_lin = (r/p^2)*[(1/T)*(E-1) - p, (p-1/T)*E + (1/T)]; 99 | a_lin = [1, -E]; 100 | elseif strcmp(interp, "cubic") == 1 101 | % define trapezoidal quadrature for integral calculation 102 | tq = (T/M*(0:M)).'; 103 | wq = (T/M)*[0.5; ones(length(tq)-2,1); 0.5]; 104 | 105 | % calculate convolution integrals for each summand of polynomial 106 | S = r*[sum(wq.*exp(p*(T-tq))); % bias 107 | sum(wq.*tq.*exp(p*(T-tq))); % linear 108 | sum(wq.*tq.^2.*exp(p*(T-tq))); % quadratic 109 | sum(wq.*tq.^3.*exp(p*(T-tq)))]; % cubic 110 | 111 | % define transfer function numerators for each polynomial coefficient 112 | gamma = sqrt(3) - 2; 113 | Hc = [[ 0, 1, -gamma ]; % bias 114 | [-sqrt(3)*gamma, 3-sqrt(3)+2*sqrt(3)*gamma, 3*gamma]*fs; % linear 115 | [-3*gamma, 6*gamma, -3*gamma]*fs^2; % quadratic 116 | [ gamma, -2*gamma, gamma ]*fs^3]; % cubic 117 | denom = [1, -gamma]; 118 | 119 | % calculate linearization transfer function 120 | b_lin = sum(S.*Hc, 1); 121 | a_lin = conv(denom, [1 -E]); 122 | end 123 | b_comp = a_lin ./ b_lin(1); 124 | a_comp = b_lin ./ b_lin(1); 125 | 126 | % initialise system state 127 | v1 = 0; 128 | v = 0; 129 | 130 | % initialise the AA-IIR method 131 | y1 = 0; % output of the regular AA-IIR method 132 | f1 = 0; % output of the compensation filter 133 | zc1 = zeros(max(length(a_comp),length(b_comp))-1, 1); % delays of the compensation filter 134 | if strcmp(interp, "cubic") == 1 135 | w1 = cspline2_init(v1); % output of the interpolation pre-filter 136 | end 137 | 138 | % initialiase output 139 | out = zeros(size(u)); % diode clipper output 140 | num_iter = zeros(size(u)); % number of Newton-Raphson iterations 141 | N = length(u); 142 | 143 | %% processing 144 | % time loop 145 | for n = 2:N 146 | % compute known quantity 147 | q = (Hp*v1 + B*(u(n-1) + u(n)) + C*f1) / Hm; 148 | 149 | % initialise Newton-Raphson method 150 | iter = 0; % iteration count 151 | stop = false; % criteria stopping flag 152 | v = v1; % initial guess 153 | 154 | % apply Newton-Raphson method 155 | while (stop == false) && (iter < max_iter) 156 | % calculate step 157 | if strcmp(interp, "linear") == 1 158 | [f,df] = sinh_aaiir_linear(v, v1, y1, zc1, fs, p, r, coef, gain, b_comp, a_comp); 159 | elseif strcmp(interp, "cubic") == 1 160 | [f,df] = sinh_aaiir_cubic(v, v1, y1, zc1, w1, fs, p, r, coef, gain, b_comp, a_comp, tq, wq); 161 | end 162 | G = q + K*f - v; 163 | DG = K*df - 1; 164 | step = G / DG; 165 | 166 | % apply damping 167 | iter_damped = 0; 168 | Gnext = inf; 169 | while (abs(Gnext) > abs(G)) && (iter_damped < max_subiter) 170 | if iter_damped ~= 0 171 | step = 0.5*step; 172 | end 173 | v_next = v - step; 174 | if strcmp(interp, "linear") == 1 175 | [f_next,~,y,zc] = sinh_aaiir_linear(v_next, v1, y1, zc1, fs, p, r, coef, gain, b_comp, a_comp); 176 | elseif strcmp(interp, "cubic") == 1 177 | [f_next,~,y,zc,w] = sinh_aaiir_cubic(v_next, v1, y1, zc1, w1, fs, p, r, coef, gain, b_comp, a_comp, tq, wq); 178 | end 179 | Gnext = q + K*f_next - v_next; 180 | iter_damped = iter_damped + 1; 181 | end 182 | 183 | % move to the next iteration 184 | v = v_next; 185 | iter = iter + 1; 186 | num_iter(n) = num_iter(n) + iter_damped; 187 | 188 | % check stopping scriteria 189 | stop = (abs(step) <= (abs_tol + rel_tol*abs(v1))); 190 | end 191 | 192 | % write output 193 | out(n) = v; 194 | 195 | % shift system state 196 | v1 = v; 197 | 198 | % shift the AA-IIR method 199 | f1 = f; 200 | y1 = y; 201 | zc1 = zc; 202 | if strcmp(interp, "cubic") == 1 203 | w1 = w; 204 | end 205 | end 206 | 207 | %% output 208 | % display number of Newton-Raphson iterations 209 | disp("Number of Newton-Raphson iterations:"); 210 | disp("Mean = " + mean(num_iter)); 211 | disp("Max = " + max(num_iter)); 212 | fprintf("\n"); 213 | 214 | % plot output waveform 215 | fig_t = figure; hold on; 216 | plot(t, out, 'k'); 217 | xlim([0 4/f0]); 218 | xlabel("Time [sec]", 'Interpreter', 'latex'); 219 | ylabel("Voltage [V]", 'Interpreter', 'latex'); 220 | title("Diode clipper waveform for AA-IIR with " + interp + " interpolation", 'Interpreter', 'latex'); 221 | ax_spec = fig_t.CurrentAxes; 222 | set(ax_spec.XAxis, 'TickLabelInterpreter', 'latex'); 223 | set(ax_spec.YAxis, 'TickLabelInterpreter', 'latex'); 224 | 225 | if strcmp(input, "sine") == 1 226 | % calculate metrics 227 | [snr, nmr] = metrics_sin(out, fs, f0, true, true); 228 | cmd_str_metr = sprintf("SNR = %.2f dB\nNMR = %.2f dB\n", snr, nmr); 229 | title_str_metr = sprintf("SNR = $%.2f$ dB, NMR = $%.2f$ dB", snr, nmr); 230 | disp("Metrics:"); 231 | disp(cmd_str_metr); 232 | 233 | % plot spectrum 234 | [spec, fig_spec] = myfft(out, fs, "blackman"); 235 | add_harmonic_marks(spec, fig_spec, fs, f0, true); 236 | title("Diode clipper spectrum for AA-IIR with " + interp + " interpolation", 'Interpreter', 'latex'); 237 | subtitle(title_str_metr, 'Interpreter', 'latex'); 238 | ylim([-120 10]); 239 | elseif strcmp(input, "sweep") == 1 240 | % plot spectrogram 241 | [spec, fig_spec] = myspec(out, fs, 1024, 0.9921875, "blackman"); 242 | title("Diode clipper spectogram for AA-IIR with " + interp + " interpolation", 'Interpreter', 'latex'); 243 | end 244 | 245 | %% FUNCTIONS 246 | % calculate linear AA-IIR solution for sinh() nonlinearity 247 | % input: 248 | % x --- current input sample; 249 | % x1 --- previous input sample; 250 | % y1 --- previous output of the regular AA-IIR method; 251 | % zc1 --- previous delays of the compensation filter; 252 | % fs --- sample rate [Hz]; 253 | % p --- single real pole value; 254 | % r --- residue value; 255 | % coef --- coefficient for sinh(); 256 | % gain --- input gain for sinh(); 257 | % b_comp --- numerator of the compensation filter; 258 | % a_comp --- denominator of the compensation filter. 259 | % output: 260 | % y_comp --- output of the compensation filter; 261 | % dy_comp --- derivative of this output with respect to the current input sample; 262 | % y --- output of the regular AA-IIR method; 263 | % zc --- delays of the compensation filter. 264 | function [y_comp,dy_comp,y,zc] = sinh_aaiir_linear(x, x1, y1, zc1, fs, p, r, coef, gain, b_comp, a_comp) 265 | % calculate parameters 266 | T = 1/fs; 267 | E = exp(p*T); 268 | 269 | % apply input signal gain 270 | x = x * gain; 271 | x1 = x1 * gain; 272 | 273 | % calculate AA-IIR step 274 | I = p*T^2/((p*T)^2 - (x-x1)^2)*(E*(sinh(x1) + (x-x1)/(p*T)*cosh(x1)) - (sinh(x) + (x-x1)/(p*T)*cosh(x))); 275 | I = coef * I; 276 | y = r*I + E*y1; 277 | 278 | % calculate derivative of the AA-IIR output with respect to the current input sample 279 | dI = 2*p*T^2 * (x-x1) * E * sinh(x1); 280 | dI = dI + T * E * ((p*T)^2 + (x-x1)^2) * cosh(x1); 281 | dI = dI + T * (x-x1) * ((x-x1)^2 - 2*p*T - (p*T)^2) * sinh(x); 282 | dI = dI + T * ((p*T-1)*(x-x1)^2 - (p*T+1)*(p*T)^2) * cosh(x); 283 | dI = dI / ((p*T)^2 - (x-x1)^2)^2; % derivative with respect to the amplified sample 284 | dI = dI * gain; % adjust for input gain 285 | dI = dI * coef; % adjust for function coefficient 286 | dy = r*dI; % multiply by residue 287 | 288 | % apply compensation filter 289 | [y_comp,zc] = filter(b_comp, a_comp, y, zc1); 290 | dy_comp = b_comp(1)*dy; 291 | end 292 | 293 | % calculate cubic AA-IIR solution for sinh() nonlinearity 294 | % input: 295 | % x --- current input sample; 296 | % x1 --- previous input sample; 297 | % y1 --- previous output of the regular AA-IIR method; 298 | % zc1 --- previous delays of the compensation filter; 299 | % w1 --- previous output of the interpolation pre-filter; 300 | % fs --- sample rate [Hz]; 301 | % p --- single real pole value; 302 | % r --- residue value; 303 | % coef --- coefficient for sinh(); 304 | % gain --- input gain for sinh(); 305 | % b_comp --- numerator of the compensation filter; 306 | % a_comp --- denominator of the compensation filter; 307 | % tq --- quadrature nodes; 308 | % wq --- quadrature weights. 309 | % output: 310 | % y_comp --- output of the compensation filter; 311 | % dy_comp --- derivative of this output with respect to the current input sample; 312 | % y --- output of the regular AA-IIR method; 313 | % zc --- delays of the compensation filter; 314 | % w --- output of the interpolation pre-filter 315 | function [y_comp,dy_comp,y,zc,w] = sinh_aaiir_cubic(x, x1, y1, zc1, w1, fs, p, r, coef, gain, b_comp, a_comp, tq, wq) 316 | % calculate parameters 317 | T = 1/fs; 318 | E = exp(p*T); 319 | 320 | % calculate coefficients of interpolating polynomial 321 | [c,w] = cspline2_step(x, x1, fs, w1); 322 | 323 | % define the integrated expression 324 | f = @(t) coef * sinh(gain * horner(t, c)); 325 | g = @(t) f(t).*exp(p*(T-t)); 326 | 327 | % calculate AA-IIR step 328 | I = sum(wq.*g(tq)); 329 | y = r*I + E*y1; 330 | 331 | % calculate derivative of the AA-IIR output with respect to the current input sample 332 | gamma = sqrt(3) - 2; 333 | dc = [gamma*fs^3, -3*gamma*fs^2, -sqrt(3)*gamma*fs, 0]; % partial derivative of polynomial coefficients to the current input sample 334 | df = @(t) coef * gain * cosh(gain * horner(t, c)) .* horner(t, dc); % partial derivative of the nonlinear function to the current input sample 335 | dg = @(t) df(t).*exp(p*(T-t)); % partial derivative of the integrated expression to the current input sample 336 | dI = sum(wq.*dg(tq)); 337 | dy = r*dI; 338 | 339 | % apply compensation filter 340 | [y_comp,zc] = filter(b_comp, a_comp, y, zc1); 341 | dy_comp = b_comp(1)*dy; 342 | end -------------------------------------------------------------------------------- /metrics/calc_nmr.m: -------------------------------------------------------------------------------- 1 | % calculate noise-to-mask ratio (NMR) using basic version of PEAQ [1] 2 | % input: 3 | % sig --- signal at test; 4 | % sig_ref --- reference signal (without noise); 5 | % fs --- sample rate [Hz]. 6 | % output: 7 | % nmr_total --- total NMR. 8 | % 9 | % Implementation is largely based on the code provided in [2] but is given 10 | % in a less computationally optimised form to facilitate readability. In 11 | % addition, the function is meant to be used at different sample rates 12 | % compared to [2]. 13 | % 14 | % Data boundaries aren't checked since clean data is assumed for reference 15 | % and aliased signals (plus signals are truncated so zero padding doesn't 16 | % affect the ratio). Time spreading isn't implemented since test signals 17 | % don't have transients. 18 | % 19 | % references: 20 | % [1] BS.1387-2 (05/2023) 21 | % Method for objective measurements of perceived audio quality 22 | % https://www.itu.int/rec/R-REC-BS.1387 23 | % [2] Peter Kabal, "An examination and interpretation of ITU-R BS.1387: 24 | % Perceptual evaluation of audio quality," Tech. Rep., Department of 25 | % Electrical & Computer Engineering, McGill University, 2003 26 | % report: https://mmsp.ece.mcgill.ca/Documents/Reports/2002/KabalR2002v2.pdf 27 | % code: https://www-mmsp.ece.mcgill.ca/Documents/Downloads/PQevalAudio/PQevalAudio-v1r0.tar.gz 28 | function [nmr_total, nmr] = calc_nmr(sig, sig_ref, fs, bit) 29 | % parameters 30 | N = 2048; % frame size 31 | O = 0.5; % overlap factor 32 | Emin = 1e-12; % energy threshold 33 | 34 | % calculate STFT parameters 35 | win = hann(N, 'symmetric'); % window ([1,2] use symmetric window) 36 | HA = round(N - O*N); % hop size 37 | L = length(sig); % input vector length 38 | NF = floor((L-N)/HA); % number of frames (truncating signal) 39 | 40 | % outter and middle ear response 41 | w_sq = ears_response(fs, N); 42 | 43 | % critical bands parameters 44 | [Nc, fc, fl, fu, dz] = critical_band_param(); 45 | U = group_matrix(fs, N, fl, fu); 46 | 47 | % internal noise 48 | Ein = internal_noise(fc); 49 | 50 | % normalazing factor for frequency spread 51 | Bs = freq_spread_fast(ones(Nc,1), ones(Nc,1), fc, dz); 52 | 53 | % quantize audio to signed integer 54 | Amax = 2^(bit-1); 55 | sig = round(sig*Amax); 56 | sig_ref = round(sig_ref*Amax); 57 | 58 | % loudness calibration and window scaling compensation 59 | ftest = 1019.5; 60 | Lp = 92; 61 | GL = scaling_factor(N, Amax, ftest/fs, Lp); 62 | sig = sig*GL; 63 | sig_ref = sig_ref*GL; 64 | 65 | % frames loop 66 | Eb_err = zeros(Nc,1); 67 | Es_ref = zeros(Nc,1); 68 | for i = 1:NF 69 | % get current frame 70 | idx = (1:N).'+(i-1)*HA; 71 | X = get_stft_frame(sig(idx), win); 72 | X_ref = get_stft_frame(sig_ref(idx), win); 73 | 74 | % square magnitude 75 | X_sq = abs(X).^2; 76 | X_sq_ref = abs(X_ref).^2; 77 | 78 | % outer and middle ear filtering 79 | Xw_sq = w_sq.*X_sq; 80 | Xw_sq_ref = w_sq.*X_sq_ref; 81 | 82 | % difference magnitude signal 83 | Xw_sq_err = Xw_sq - 2*sqrt(Xw_sq.*Xw_sq_ref) + Xw_sq_ref; 84 | 85 | % group into critical bands 86 | Eb_err(:,i) = max(U*Xw_sq_err, Emin); 87 | Eb_ref = max(U*Xw_sq_ref, Emin); 88 | 89 | % add internal noise 90 | E_ref = Eb_ref + Ein; 91 | 92 | % frequency spreading 93 | Es_ref(:,i) = freq_spread_fast(E_ref, Bs, fc, dz); 94 | end 95 | 96 | % calculate noise-to-mask 97 | gm = mask_offset(Nc, dz); 98 | M = gm.*Es_ref; % masking threshold 99 | nmr = Eb_err./M; 100 | nmr_total = 10*log10(mean(nmr,'all')); 101 | end 102 | 103 | % get STFT frame 104 | % input: 105 | % x --- current input; 106 | % win --- window; 107 | % output: 108 | % X --- FFT of current frame. 109 | function X = get_stft_frame(x, win) 110 | N = length(win); 111 | x_frame = win.*x; 112 | X = fft(x_frame, N); 113 | X = X(1:N/2+1); 114 | end 115 | 116 | % calculate mask offset (`PQ_MaskOffset` from [2]) 117 | % input: 118 | % Nc --- number of bands; 119 | % dz --- Bark scale band step. 120 | % output: 121 | % gm --- mask offset. 122 | function gm = mask_offset(Nc, dz) 123 | % the amount in dB by which the masking threshold lies below the time-frequency spread Bark energy 124 | mdb = @(k) (k<=12/dz).*3 + (k>12/dz).*(0.25*k*dz); 125 | 126 | % weighting vector for masking threshold in energy units 127 | idx = (0:Nc-1).'; 128 | gm = 10.^(-mdb(idx)/10); 129 | end 130 | 131 | % calculate parameters for time domain spreading (`PQtConst` from [2]) 132 | % input: 133 | % fs --- sample rate [Hz]; 134 | % N --- FFT size; 135 | % fc --- center band frequencies (column). 136 | % output: 137 | % alpha --- parameters for time filtering in each band. 138 | function alpha = time_param(fs, N, fc) 139 | % frame rate (frames are updated every N/2 samples for assumed 50% overlap) 140 | fss = fs / (N/2); 141 | 142 | % time parameters for smallest case and 100 Hz 143 | t100 = 3e-2; 144 | tmin = 8e-3; 145 | 146 | % time parameters for every band 147 | t = tmin + (100./fc).*(t100-tmin); 148 | alpha = exp(-1./(fss*t)); 149 | end 150 | 151 | % calculate frequency spread effect across critical bands using regular 152 | % spreading function definition 153 | % input: 154 | % E --- energy in critical bands ("pitch patterns" [2]); 155 | % Bs --- normalising factor (calculated for E = 1 across all bands); 156 | % fc --- center band frequencies; 157 | % dz --- Bark scale band step. 158 | % output: 159 | % Es --- energy in critical bands after frequency spreading effect 160 | % ("unsmeared (in time) excitation patterns" [2]) 161 | function Es = freq_spread(E, Bs, fc, dz) 162 | % number of bands 163 | Nc = length(E); 164 | 165 | % power law for addition of spreading 166 | e = 0.4; 167 | 168 | % spreading function (indexes are assumed in range from 0 to Nc-1) 169 | Sdb = @(i,l,E) ((i<=l).*27 + (i>l).*(-24-230./fc(l+1)+2*log10(E))).*(i-l)*dz; % spreading function in dB scale 170 | S = @(i,l,E) 10.^(Sdb(i,l,E)/10); % regular spreading function 171 | A = @(l,E) sum(S(0:Nc-1,l,E)); % normalisation factor 172 | Sn = @(i,l,E) S(i,l,E)./A(l,E); % normalised spreading function 173 | 174 | % bands loop 175 | Es = zeros(Nc,1); 176 | for i = 0:Nc-1 177 | % spreading loop 178 | for l = 0:Nc-1 179 | Es(i+1) = Es(i+1) + (E(l+1)*Sn(i,l,E(l+1)))^e; 180 | end 181 | Es(i+1) = Es(i+1)^(1/e); 182 | Es(i+1) = Es(i+1)/Bs(i+1); 183 | end 184 | end 185 | 186 | % calculate frequency spread effect across critical bands using recursive 187 | % implementation (`PQ_SpreadCB` from [2]) 188 | % input: 189 | % E --- energy in critical bands ("pitch patterns" [2]); 190 | % Bs --- normalising factor (calculated for E = 1 across all bands); 191 | % fc --- center band frequencies; 192 | % dz --- Bark scale band step. 193 | % output: 194 | % Es --- energy in critical bands after frequency spreading effect 195 | % ("unsmeared (in time) excitation patterns" [2]) 196 | function Es = freq_spread_fast(E, Bs, fc, dz) 197 | % number of bands 198 | Nc = length(E); 199 | 200 | % power law for addition of spreading 201 | e = 0.4; 202 | 203 | % allocate storage 204 | aUCEe = zeros(Nc,1); 205 | Ene = zeros(Nc,1); 206 | Es = zeros(Nc,1); 207 | 208 | % calculate energy dependent terms 209 | aL = 10^(-2.7 * dz); 210 | for m = 0:Nc-1 211 | aUC = 10^((-2.4 - 23 / fc(m+1)) * dz); 212 | aUCE = aUC * E(m+1)^(0.2 * dz); 213 | gIL = (1 - aL^(m+1)) / (1 - aL); 214 | gIU = (1 - aUCE^(Nc-m)) / (1 - aUCE); 215 | En = E(m+1) / (gIL + gIU - 1); 216 | aUCEe(m+1) = aUCE^e; 217 | Ene(m+1) = En^e; 218 | end 219 | 220 | % lower spreading 221 | Es(Nc-1+1) = Ene(Nc-1+1); 222 | aLe = aL^e; 223 | for m = Nc-2:-1:0 224 | Es(m+1) = aLe * Es(m+1+1) + Ene(m+1); 225 | end 226 | 227 | % upper spreading i > m 228 | for m = 0:Nc-2 229 | r = Ene(m+1); 230 | a = aUCEe(m+1); 231 | for i = m+1:Nc-1 232 | r = r * a; 233 | Es(i+1) = Es(i+1) + r; 234 | end 235 | end 236 | 237 | for i = 0:Nc-1 238 | Es(i+1) = (Es(i+1))^(1/e) / Bs(i+1); 239 | end 240 | end 241 | 242 | % calculate intertal noise in the ear (`PQIntNoise` from [2]) 243 | % input: 244 | % fc --- center band frequencies (column). 245 | % output: 246 | % Ein --- internal noise value for each band (column). 247 | function Ein = internal_noise(fc) 248 | f = fc / 1000; % [kHz] 249 | Edb = 1.456*f.^(-0.8); 250 | Ein = 10.^(Edb/10); 251 | end 252 | 253 | % calculate grouping matrix from FFT bins to critical bands 254 | % input: 255 | % fs --- sample rate [Hz]; 256 | % N --- FFT length; 257 | % fl --- lower frequency edges (column); 258 | % fu --- upper frequency edges (column); 259 | % output: 260 | % U --- grouping matrix (number of bands x number of real FFT bins) 261 | function U = group_matrix(fs, N, fl, fu) 262 | df = fs/N; 263 | idx = (0:N/2); 264 | ku = (2*idx+1)/2; 265 | kl = (2*idx-1)/2; 266 | U = max(0, min(fu, ku*df) - max(fl, kl*df)) ./ df; % eq. (13) in [2] 267 | end 268 | 269 | % calculate squared outer and middle ears frequency response (`PQWOME` from [2]) 270 | % input: 271 | % fs --- sample rate [Hz]; 272 | % N --- FFT length. 273 | % output: 274 | % w_sq --- squared ears response (column). 275 | function w_sq = ears_response(fs, N) 276 | f = (linspace(0, fs/2, N/2+1) / 1000).'; % [kHz] 277 | Adb = -2.184*f.^(-0.8) + 6.5*exp(-0.6*(f - 3.3).^2) - 0.001*f.^(3.6); 278 | w_sq = 10.^(Adb/10); 279 | end 280 | 281 | % calculate parameters for critical bands (`PQCB` from [2]) 282 | % output: 283 | % Nc --- number of bands; 284 | % fc --- center frequencies (column); 285 | % fl --- lower frequency edges (column); 286 | % fu --- upper frequency edges (column); 287 | % dz --- band step in Bark scale. 288 | function [Nc, fc, fl, fu, dz] = critical_band_param() 289 | dz = 1/4; 290 | fL = 80; % frequency bnd start [Hz] 291 | fU = 18000; % frequency band stop [Hz] 292 | 293 | % Bark scale conversion 294 | B = @(f) 7*asinh(f/650); 295 | Binv = @(z) 650*sinh(z/7); 296 | 297 | % compute number of bands 298 | zL = B(fL); 299 | zU = B(fU); 300 | Nc = ceil((zU - zL) / dz); 301 | 302 | % obtain frequency bands using Bark scale conversion 303 | %zl = zL + (0:Nc-1) * dz; 304 | %zu = min(zL + (1:Nc)*dz, zU); 305 | %zc = 0.5*(zl + zu); 306 | %fl = Binv(zl); 307 | %fc = Binv(zc); 308 | %fu = Binv(zu); 309 | 310 | % frequency bands from BS.1387-2 standard [1,2] 311 | fl = [ 80.000, 103.445, 127.023, 150.762, 174.694, ... 312 | 198.849, 223.257, 247.950, 272.959, 298.317, ... 313 | 324.055, 350.207, 376.805, 403.884, 431.478, ... 314 | 459.622, 488.353, 517.707, 547.721, 578.434, ... 315 | 609.885, 642.114, 675.161, 709.071, 743.884, ... 316 | 779.647, 816.404, 854.203, 893.091, 933.119, ... 317 | 974.336, 1016.797, 1060.555, 1105.666, 1152.187, ... 318 | 1200.178, 1249.700, 1300.816, 1353.592, 1408.094, ... 319 | 1464.392, 1522.559, 1582.668, 1644.795, 1709.021, ... 320 | 1775.427, 1844.098, 1915.121, 1988.587, 2064.590, ... 321 | 2143.227, 2224.597, 2308.806, 2395.959, 2486.169, ... 322 | 2579.551, 2676.223, 2776.309, 2879.937, 2987.238, ... 323 | 3098.350, 3213.415, 3332.579, 3455.993, 3583.817, ... 324 | 3716.212, 3853.817, 3995.399, 4142.547, 4294.979, ... 325 | 4452.890, 4616.482, 4785.962, 4961.548, 5143.463, ... 326 | 5331.939, 5527.217, 5729.545, 5939.183, 6156.396, ... 327 | 6381.463, 6614.671, 6856.316, 7106.708, 7366.166, ... 328 | 7635.020, 7913.614, 8202.302, 8501.454, 8811.450, ... 329 | 9132.688, 9465.574, 9810.536, 10168.013, 10538.460, ... 330 | 10922.351, 11320.175, 11732.438, 12159.670, 12602.412, ... 331 | 13061.229, 13536.710, 14029.458, 14540.103, 15069.295, ... 332 | 15617.710, 16186.049, 16775.035, 17385.420 ]; 333 | fc = [ 91.708, 115.216, 138.870, 162.702, 186.742, ... 334 | 211.019, 235.566, 260.413, 285.593, 311.136, ... 335 | 337.077, 363.448, 390.282, 417.614, 445.479, ... 336 | 473.912, 502.950, 532.629, 562.988, 594.065, ... 337 | 625.899, 658.533, 692.006, 726.362, 761.644, ... 338 | 797.898, 835.170, 873.508, 912.959, 953.576, ... 339 | 995.408, 1038.511, 1082.938, 1128.746, 1175.995, ... 340 | 1224.744, 1275.055, 1326.992, 1380.623, 1436.014, ... 341 | 1493.237, 1552.366, 1613.474, 1676.641, 1741.946, ... 342 | 1809.474, 1879.310, 1951.543, 2026.266, 2103.573, ... 343 | 2183.564, 2266.340, 2352.008, 2440.675, 2532.456, ... 344 | 2627.468, 2725.832, 2827.672, 2933.120, 3042.309, ... 345 | 3155.379, 3272.475, 3393.745, 3519.344, 3649.432, ... 346 | 3784.176, 3923.748, 4068.324, 4218.090, 4373.237, ... 347 | 4533.963, 4700.473, 4872.978, 5051.700, 5236.866, ... 348 | 5428.712, 5627.484, 5833.434, 6046.825, 6267.931, ... 349 | 6497.031, 6734.420, 6980.399, 7235.284, 7499.397, ... 350 | 7773.077, 8056.673, 8350.547, 8655.072, 8970.639, ... 351 | 9297.648, 9636.520, 9987.683, 10351.586, 10728.695, ... 352 | 11119.490, 11524.470, 11944.149, 12379.066, 12829.775, ... 353 | 13294.850, 13780.887, 14282.503, 14802.338, 15341.057, ... 354 | 15899.345, 16477.914, 17077.504, 17690.045 ]; 355 | fu = [ 103.445, 127.023, 150.762, 174.694, 198.849, ... 356 | 223.257, 247.950, 272.959, 298.317, 324.055, ... 357 | 350.207, 376.805, 403.884, 431.478, 459.622, ... 358 | 488.353, 517.707, 547.721, 578.434, 609.885, ... 359 | 642.114, 675.161, 709.071, 743.884, 779.647, ... 360 | 816.404, 854.203, 893.091, 933.113, 974.336, ... 361 | 1016.797, 1060.555, 1105.666, 1152.187, 1200.178, ... 362 | 1249.700, 1300.816, 1353.592, 1408.094, 1464.392, ... 363 | 1522.559, 1582.668, 1644.795, 1709.021, 1775.427, ... 364 | 1844.098, 1915.121, 1988.587, 2064.590, 2143.227, ... 365 | 2224.597, 2308.806, 2395.959, 2486.169, 2579.551, ... 366 | 2676.223, 2776.309, 2879.937, 2987.238, 3098.350, ... 367 | 3213.415, 3332.579, 3455.993, 3583.817, 3716.212, ... 368 | 3853.348, 3995.399, 4142.547, 4294.979, 4452.890, ... 369 | 4643.482, 4785.962, 4961.548, 5143.463, 5331.939, ... 370 | 5527.217, 5729.545, 5939.183, 6156.396, 6381.463, ... 371 | 6614.671, 6856.316, 7106.708, 7366.166, 7635.020, ... 372 | 7913.614, 8202.302, 8501.454, 8811.450, 9132.688, ... 373 | 9465.574, 9810.536, 10168.013, 10538.460, 10922.351, ... 374 | 11320.175, 11732.438, 12159.670, 12602.412, 13061.229, ... 375 | 13536.710, 14029.458, 14540.103, 15069.295, 15617.710, ... 376 | 16186.049, 16775.035, 17385.420, 18000.000 ]; 377 | 378 | % transpose 379 | fl = fl.'; 380 | fc = fc.'; 381 | fu = fu.'; 382 | end 383 | 384 | % calculate sclaing for loudness and Hann window (`PQ_GL` from [2]) 385 | % input: 386 | % N --- frame length; 387 | % Amax --- maximum amplitude; 388 | % fn --- normalised frequency; 389 | % Lp --- sound pressure level. 390 | % output: 391 | % GL --- scaling constant. 392 | function GL = scaling_factor(N, Amax, fn, Lp) 393 | W = N - 1; 394 | gp = peak_factor(fn, N, W); 395 | GL = 10^(Lp/20) / (gp * Amax/4 * W); 396 | end 397 | 398 | % calculate peak factor (`PQ_gp` from [2]) 399 | % (since sinusoid can fall between FFt bins the largest bin value will be 400 | % the peak factor times the peak of the continuous response) 401 | % input: 402 | % fn --- normalised input frequency; 403 | % N --- frame length. 404 | % output: 405 | % gp --- peak factor. 406 | function gp = peak_factor(fn, N, W) 407 | % distance to the nearest DFT bin 408 | df = 1 / N; 409 | k = floor(fn / df); 410 | dfN = min((k+1)*df - fn, fn - k*df); 411 | 412 | dfW = dfN * W; 413 | gp = sin(pi * dfW) / (pi * dfW * (1 - dfW^2)); 414 | end --------------------------------------------------------------------------------