Main Content

Automated Design of Audio Filters for Room Equalization

This example combines Optimization Toolbox™ and Audio Toolbox™ to develop an algorithm that automatically tunes a set of filter parameters.

There are many audio applications where it is desirable to compute parametric equalizer parameters to fit an arbitrary frequency response. For instance, one could fit a filter response to a measured impulse response (IR) to obtain a lower-order implementation of the same filter. Alternatively, one can apply correction to a measured loudspeaker response (anechoic or in-room) to smooth out any imperfections and create a perceptually flat frequency response. The latter is demonstrated here by designing an algorithm that automatically tunes the parameters of N parametric equalizers such that when the resulting EQ is applied to the speaker, the frequency response is perceived as flat in the room.

This example goes over the following steps:

  • Measure the in-room response of a loudspeaker using the Impulse Response Measurer

  • Compute a fractional-octave smoothed response

  • Take into account the microphone calibration data

  • Compute a target response that is perceptually optimal for the given loudspeaker system and room configuration

  • Optimize a set of filter parameters that modifies the response to better fit the target

  • Produce an audio filter or audio files to evaluate the results using headphones or listening room

In-Room Measurement

The first step is to obtain measurements for the system that needs to be improved.

Set up a full duplex sound interface so that it can both play on the loudspeaker, and record with a calibration microphone (such as a Behringer ECM8000 connected to a sound interface capable of supplying phantom power). Place the microphone on a stand so that you can move it into the listening position(s). You can start with the microphone centered in the listener's position, but measuring at several positions will help reduce the chances of overcorrecting for issues like a high frequency dip in the response that could only be present in a region smaller than a listener's head.

Start the Impulse Response Measurer app by entering impulseResponseMeasurer at the command prompt. You can also click the app icon on the Apps tab of the MATLAB® Toolstrip.

Verify that the correct audio device is selected. Change the sample rate if desired (this example uses 96 kHz). Set the player and recorder channels to the loudspeaker and microphone, respectively.

Select the Swept Sine method. Set the number of runs to average several measurement together. This example uses five. Set the duration per run to have time for a long enough swept sine and a period of silence that is long enough for the reverberation to completely die off, which can be several seconds in a typical room.

In the advanced settings, set a pause between runs that allows time to move the microphone around the initial "center" position. You must keep silent during the "silence" part of the measurement, but you can move the microphone and make noise during this pause. The start frequency should be set below the range of the loudspeaker (10 Hz might be a good starting point). The stop frequency can be set to half the sampling rate, unless measuring a subwoofer with limited high-frequency range. Set the sweep duration to a few seconds, and make sure there is enough duration left for silence at the end.

You may test levels with the level meter or try a first capture with 1 short run. Set playback level loud enough to hide any background noise in the room and set the microphone level so that it is high but does not overload/clip.

Now you can capture and save export the data to a MAT file. The rest of this example uses a file provided here.

Import the Measurement

Import the last measurement that was exported by the application (by addressing with end). The data used here is in a similar (but compatible) format.

load('measured_ir_data.mat','measurementData');
Fs = measurementData.SampleRate(end);
ir = measurementData.ImpulseResponse(end).Amplitude;
frequency = measurementData.MagnitudeResponse(end).Frequency;
if isfield(measurementData.MagnitudeResponse,'PowerDb')
    magnitudeDB = measurementData.MagnitudeResponse(end).PowerDb;
else % Version R2022b of IRM has renamed this field to Magnitude (dB)
    magnitudeDB = measurementData.MagnitudeResponse(end).MagnitudeDB;
end

Using a helper function provided here, smooth the response by 1/24-octave sections. Since powerDB is a measurement, add extra smoothing (last argument set to true).

fullRange = [10,Fs/2]; % Full audio range (for the plots)
[powerFR,cfFR] = octaveAverage(frequency,db2mag(magnitudeDB),24,fullRange,true);
pdbFR = 20*log10(powerFR);

Plot the measurement and the smoothed response.

semilogx(frequency,magnitudeDB,':',Color="#77AC30")
hold on
plot(cfFR,pdbFR,'b',LineWidth=2,Color="#0072BD")
title('Measured Speaker Response')
legend('Raw Data','Octave Smoothed',Location='southwest')
xlabel('Frequency (Hz) \rightarrow')
ylabel('Magnitude (dB) \rightarrow')
yrange = [-50 0];
axis([30 22e3 yrange])
hold off
grid on

Figure contains an axes object. The axes object with title Measured Speaker Response, xlabel Frequency (Hz) blank rightarrow, ylabel Magnitude (dB) blank rightarrow contains 2 objects of type line. These objects represent Raw Data, Octave Smoothed.

Microphone Calibration

If there is calibration data available for the microphone, subtract it from the measurement.

In this case, generic calibration data for the Behringer ECM8000 microphone is used. Calling the helper function with no output arguments plots the microphone frequency response.

getMicCalibration()

Figure contains an axes object. The axes object with title Microphone Frequency Response, xlabel Frequency (Hz), ylabel Amplitude (dB) contains 2 objects of type line. One or more of the lines displays its values using only markers These objects represent Data Interpolation, Raw Data Points.

Subtract the microphone calibration data from the measured response, using the same frequency values.

micGainDB = getMicCalibration(cfFR);
pdbFRmic = pdbFR - micGainDB;

Determine a "range of interest" (ROI) for the optimization based on the measurement above and the manufacturer specifications for our loudspeaker. The bookshelf speaker measured above has a range of 60 Hz to 22 kHz according to the manufacturer. Set the ROI to a range of 40 Hz to 20 kHz to slightly increase the low end and to take into account the steep decline above 20 kHz.

ROI = [40 20e3]; % specify the region of interest for the optimization

Plot the corrected response and the ROI region.

semilogx(frequency,magnitudeDB,':',Color="#77AC30")
hold on
plot(cfFR,pdbFRmic,Color="#0072BD",LineWidth=2)
plot([ROI(1) ROI(1)],yrange,':',Color="#A2142F",LineWidth=1.5);
plot([ROI(2) ROI(2)],yrange,':',Color="#A2142F",LineWidth=1.5);
title('Measured Speaker Response')
legend('Raw Data','Octave Smoothed',...
       'ROI Low End','ROI High End',Location='southwest')
xlabel('Frequency (Hz) \rightarrow')
ylabel('Magnitude (dB) \rightarrow')
axis([30 24e3 -50 0])
hold off
grid on

Figure contains an axes object. The axes object with title Measured Speaker Response, xlabel Frequency (Hz) blank rightarrow, ylabel Magnitude (dB) blank rightarrow contains 4 objects of type line. These objects represent Raw Data, Octave Smoothed, ROI Low End, ROI High End.

Compute a Target Response

The next step is to determine a suitable target response for the system in the ROI. The main goal is to provide a response that is perceived as "flat", and potentially extend the low frequencies (within reasonable limits).

It is important to consider whether the response being optimized is a loudspeaker measurement in an anechoic chamber, or a loudspeaker in a room measured from a listening position. In the former case, the target could be a constant level (with a roll off outside of the loudspeaker's range). In the later case, which is the case for this example, the response at the listening position also depends on the room. The loudspeaker is perceived as "flat" if it exhibits a constant slope down. The degree of that slope depends on factors such as the distance of the listener. The perceived quality also depends on the low frequency cutoff point (approximately -10 dB), so the target curve can be used to extend the low frequencies if the boost remains moderate. Keep in mind that loudspeaker distortion increases in the lower frequencies, and the overall limits of the amplifier and loudspeaker should not be exceeded.

To compute a target response, fit a straight line (in the log-frequency domain) onto the frequency response (in dB) for a subset of the ROI. Add a roll off to the lower frequencies.

% Compute the octave average (only for the region of interest)
[power,cf] = octaveAverage(frequency,db2mag(magnitudeDB),24,ROI,true);
pdb = 20*log10(power) - getMicCalibration(cf);

% Set the range of the linear fit
lfCutOff = 2.5*ROI(1); % lowest frequency to linearize
hfMaxFit = 0.6*ROI(2); % max frequency to fit for

% Fit a straight line in the log-frequency domain
linIdx = cf>=lfCutOff & cf<=hfMaxFit;
if license('test','statistics_toolbox')
    pfit = robustfit(log(cf(linIdx)),pdb(linIdx));
else % if this toolbox is not available, use a simple linear regression
    pfit = [ones(nnz(linIdx),1) log(cf(linIdx))] \ pdb(linIdx);
end
targetResp = pfit(1)+pfit(2)*log(cf);

% Roll off the low frequencies, starting slightly above the linear range above
lfcutoff = 1.05*lfCutOff;
idx = cf<lfcutoff;
targetResp(idx) = targetResp(idx) - min(30,ROI(1)/2)*((lfcutoff-cf(idx))/lfcutoff).^2;

% Plot the target response
semilogx(frequency,magnitudeDB,':',Color="#77AC30");
hold on
axis([20 24e3 yrange]);
plot(cfFR,pdbFRmic,Color="#0072BD",LineWidth=2)
plot(cf,targetResp,Color="#000000",LineWidth=2.5);
plot([ROI(1) ROI(1)],yrange,':',Color="#A2142F",LineWidth=1.5);
plot([ROI(2) ROI(2)],yrange,':',Color="#A2142F",LineWidth=1.5);
title('Target Response for Loudspeaker');
legend('Measured Response (raw data)','Measured Response (octave smoothed)',...
       'Target Response','ROI Low','ROI High',Location='southwest')
xlabel('Frequency (Hz) \rightarrow')
ylabel('Magnitude (dB) \rightarrow')
grid on
hold off

Figure contains an axes object. The axes object with title Target Response for Loudspeaker, xlabel Frequency (Hz) blank rightarrow, ylabel Magnitude (dB) blank rightarrow contains 5 objects of type line. These objects represent Measured Response (raw data), Measured Response (octave smoothed), Target Response, ROI Low, ROI High.

The target response (in black) has a slight downward tilt and a roll off for the low frequencies that allows for some boost (10 to 12 dB).

Parametric Filter Overview

The variables being tuned by the optimization algorithm are typical audio parametric EQ parameters: Center Frequency, Filter Bandwidth, and Peak Gain.

Use the response to produce settings for a 12-band parametric filter (10 peak filters and 2 shelf filters).

Use the designParamEQ function from Audio Toolbox to design the filter. Use the lsqnonlin (Optimization Toolbox) function to perform the fit by tuning the parameters of the EQ bands until the speaker response is as flat as possible.

Before configuring the optimization algorithm, you can look at what a manual filter design looks like for 2 filters. Use the following controls to manually tune the filter parameters and observe the output response using filterAnalyzer. This allow us to visualize the parameters that the optimization algorithm automatically tunes.

gain = [4, ...
      -6.6]; 
  
centerFreq = [3520, ...
              7230]; 
          
bandwidth = [1920, ...
              4110];

Next, generate the filter coefficients using the specified parameters by calling designParamEQ:

[B,A] = designParamEQ(2,gain,(centerFreq/(Fs/2)),(bandwidth/(Fs/2)),Orientation='row');

Visualize the filter design.

filterAnalyzer(B,A,SampleRates=Fs)

Parametric Filter Optimization

To produce the optimized parametric filters, call a helper function that sets starting values and limits for every filter parameter, then calls lsqnonlin. The optimizer uses the eqObjectiveFct function that computes the response of the given EQ and compares it to the desired response. The lsqnonlin optimizer attempts to minimize that error on every iteration.

% Run the optimization with a selected number of filters
numFilters = 10+2; % 10 peak filters, one low shelf, and one high shelf
[EQ,outputResp] = eqOptimizer(numFilters,frequency,pdb,targetResp,ROI,Fs);
                                            Norm of      First-order 
 Iteration  Func-count      Resnorm            step       optimality
     0         37          0.162731                            0.425
     1         74         0.0545925              10            0.524      
     2        111         0.0290028              20            0.107      
     3        148         0.0279642         27.7143            0.223      
     4        185         0.0190037         6.92858            0.186      
     5        222         0.0154974        0.358522            0.173      
     6        259         0.0128502         13.8572            0.115      
     7        296         0.0100923         3.46429           0.0784      
     8        333         0.0100923         6.92858           0.0784      
     9        370        0.00950391         1.73215           0.0954      
    10        407         0.0082267         3.46429          0.00596      
    11        444          0.007321         6.92858           0.0923      
    12        481        0.00647468         13.8572           0.0382      
    13        518        0.00601745         27.7143           0.0407      
    14        555        0.00601745         27.7143           0.0407      
    15        592        0.00548673         6.92858           0.0454      
    16        629        0.00548673          14.993           0.0454      
    17        666        0.00496145         3.46429           0.0117      
    18        703         0.0044269         6.92858          0.00467      
    19        740         0.0044269         8.60961          0.00467      
    20        777        0.00405319          2.1524            0.022      
    21        814        0.00360236          4.3048          0.00209      
    22        851        0.00360236         7.15549          0.00209      
    23        888         0.0034787         1.78887           0.0176      
    24        925        0.00326625         2.29703           0.0188      
    25        962        0.00318511         1.50731           0.0188      
    26        999        0.00317997         1.78755          0.00471      
    27       1036        0.00317785        0.894889          0.00554      
    28       1073        0.00317591         0.63816          0.00235      
    29       1110        0.00317591         3.57775          0.00235      
    30       1147        0.00317558        0.894437          0.00106      
    31       1184        0.00317492        0.421078          0.00225      
    32       1221        0.00317459        0.816728            0.005      
    33       1258        0.00317438        0.894437          0.00287      
    34       1295        0.00317415        0.223609          0.00386      
    35       1332        0.00317407        0.223609          0.00138      
    36       1369          0.003174        0.447218         0.000363      
    37       1406        0.00317395        0.239862         0.000148      
    38       1443        0.00317394        0.447218          9.4e-05      
    39       1480        0.00317393        0.114499         5.44e-05      

Optimization stopped because the relative sum of squares (r) is changing
by less than options.FunctionTolerance = 1.000000e-08.

Results

Examine the results. Start by computing the filter frequency response over a larger frequency range.

freqzResp = eqFreqz(EQ,frequency,Fs);
pFR = octaveAverage(frequency,abs(freqzResp),24,fullRange,false);
filtRespFR = 20*log10(pFR);
outputRespFR = pdbFRmic + filtRespFR;

Plot the system response.

% Plot the system response
semilogx(frequency,magnitudeDB,':',Color="#77AC30");
hold on
axis([20 24e3 yrange]);
plot(cfFR,pdbFRmic,Color="#0072BD",LineWidth=2)
plot(cf,targetResp,Color="#000000",LineWidth=2.5);
plot([ROI(1) ROI(1)],yrange,':',Color="#A2142F",LineWidth=1.5);
plot([ROI(2) ROI(2)],yrange,':',Color="#A2142F",LineWidth=1.5);
plot(cfFR,outputRespFR,Color="#D95319",LineWidth=2);
title('Measured Speaker Response with and without EQ');
legend('Measured Response (raw data)',...
       'Measured Response (octave smoothed)',...
       'Target Response','ROI Low','ROI High',...
       'Corrected Response',Location='southwest')
xlabel('Frequency (Hz) \rightarrow')
ylabel('Magnitude (dB) \rightarrow')
grid on
hold off

Figure contains an axes object. The axes object with title Measured Speaker Response with and without EQ, xlabel Frequency (Hz) blank rightarrow, ylabel Magnitude (dB) blank rightarrow contains 6 objects of type line. These objects represent Measured Response (raw data), Measured Response (octave smoothed), Target Response, ROI Low, ROI High, Corrected Response.

Also plot error relative to target, but invert the error curves to make it easier to compare them with the optimized EQ.

% Plot error relative to target
errorOld = targetResp - pdb;
errorNew = targetResp - outputResp;
semilogx(cf([1,end]),[0,0],Color="#000000",LineWidth=2);
hold on;
plot(cf,errorOld,Color="#0072BD",LineWidth=2);
plot(cf,errorNew,Color="#77AC30",LineWidth=2);
plot(cfFR,filtRespFR,Color="#D95319",LineWidth=1.5);
hold off; grid on;
axis([20 24e3 -15 15])
title('Error Relative to Target');
xlabel('Frequency (Hz) \rightarrow');
ylabel('Magnitude (dB) \rightarrow');
legend('Target','Original Error (inverted)',...
       'Corrected Error (inverted)',...
       'Optimized EQ',Location='southwest');

Figure contains an axes object. The axes object with title Error Relative to Target, xlabel Frequency (Hz) blank rightarrow, ylabel Magnitude (dB) blank rightarrow contains 4 objects of type line. These objects represent Target, Original Error (inverted), Corrected Error (inverted), Optimized EQ.

Sort peak filters by center frequency and convert to table format.

EQpk = EQ(1:end-2,:);
[~,idx] = sort(EQpk(:,3));
EQ(1:end-2,:) = EQpk(idx,:);

% Create a table with all the EQ settings
filterType =  [repmat("PK",numFilters-2,1);"LS";"HS"];
EQt = table(filterType,EQ(:,3),EQ(:,1),EQ(:,2),VariableNames=...
                   {'Type','Frequency (Hz)','Gain (dB)','Q/S'})
EQt=12×4 table
    Type    Frequency (Hz)    Gain (dB)      Q/S  
    ____    ______________    _________    _______

    "PK"        82.323          7.1503     0.66135
    "PK"        120.19          -8.933      2.1717
    "PK"        207.45         -8.1045      1.1655
    "PK"        489.05          -5.334      4.5925
    "PK"        1066.3          6.0301      7.3903
    "PK"        1237.2          2.9252      6.8405
    "PK"        2036.6         -6.7607      1.9202
    "PK"          6864          7.6145      1.0826
    "PK"        9483.1         -10.043      1.2221
    "PK"         12008         -1.5737      5.1652
    "LS"        3307.5           3.373           5
    "HS"         16602         -2.3225      4.3731

Compute an overall gain that avoids any tones increasing in amplitude. This is not a guarantee that signals cannot clip but is generally more than sufficient in practice.

gainDB = -max(filtRespFR)-.1;

Instantiate a multibandParametricEQ object with the EQ settings. You can use this object to visualize the filter, create an audio plugin, or load either the object or plugin into the Audio Test Bench.

% Create EQ object
mbpeq = multibandParametricEQ(HasLowShelfFilter=true, HasHighShelfFilter=true, ...
                              NumEQBands=numFilters-2, EQOrder=2, SampleRate=Fs, ...
                              Frequencies=EQ(1:end-2,3)', QualityFactors=EQ(1:end-2,2)', PeakGains=EQ(1:end-2,1)', ...
                              LowShelfCutoff=EQ(end-1,3), LowShelfSlope=EQ(end-1,2), LowShelfGain=EQ(end-1,1), ...
                              HighShelfCutoff=EQ(end,3), HighShelfSlope=EQ(end,2), HighShelfGain=EQ(end,1));

Produce output files to subjectively evaluate the results, either with headphones or in the actual listening room. The IR is included for headphone evaluation but should be omitted when testing in the actual room (which produces that response itself).

[rock,fileFs] = audioread('RockDrums-44p1-stereo-11secs.mp3');

% Resample the test file to match the sample rate of the IR measurement
rock = resample(rock,Fs,fileFs,100);

% Convolve the IR with the test audio. This will simulate the
% effect of the room when evaluating the EQ using headphones.
rockIR = conv(rock(:,1),ir);
rockIR(:,2) = conv(rock(:,2),ir);
rockIR = rockIR*.97/max(abs(rockIR),[],'all');
audiowrite('RockDrums-with-IR.wav',rockIR,Fs,Comment=...
       'Convolution of rock drums with impulse response (for simulated evaluation on headphones)');

Type audioTestBench(mbpeq) at the command prompt to try the plugin in the Audio Test Bench app.

In the Audio Test Bench, click the "Audio File Reader" button with the gear icon and select 'RockDrums-with-IR.wav' if using headphones, or simply use 'RockDrums-44p1-stereo-11secs.mp3' if playing back over the same system that was used to measure the impulse response. With the Audio Test Bench, you can toggle the EQ on and off, and you can even further tune the EQ settings to your liking.

You can also process files with the EQ to listen outside of the Audio Test Bench.

% Apply the EQ to the original file (for use in the measured room).
% Use the gain that was computed to avoid clipping.
rockEQ = db2mag(gainDB)*mbpeq(rock);
reset(mbpeq); % reset the EQ before processing another file
audiowrite('RockDrums-with-Correction-only.wav',rockEQ,Fs,Comment=...
          ['Convolution of rock drums with correction (for listening '...
           'in the same environment the IR was measured in)']);

% Apply the EQ to the IR-processed file (for use with headphones).
% Use an output level that avoids clipping.
rockIREQ = mbpeq(rockIR);
reset(mbpeq); % reset the EQ so it is free to process another file
rockIREQ = rockIREQ/max(abs(rockIREQ),[],'all')*.97;
audiowrite('RockDrums-with-IR-and-Correction.wav',rockIREQ,Fs,Comment=...
          ['Convolution of rock drums with impulse response and '...
           'correction (IR simulation for evaluation over headphones)']);

Alternatively, to apply the EQ to any playback on a selected device, Windows users can export the EQ settings in a Room EQ Wizard (REW) compatible format and load it in Equalizer APO.

eqExport2APO("myroomeq.txt",EQ,gainDB);
type("myroomeq.txt")
Filter Settings file

MATLAB Filter Export
Dated: 06-Jun-2024 15:23:57

Notes: Generated using MATLAB example

Equalizer: Generic
Preamp: -9.1 dB
Filter: ON  PK  Fc    82.32 Hz  Gain   7.15 dB  Q 0.66135
Filter: ON  PK  Fc   120.19 Hz  Gain  -8.93 dB  Q 2.17173
Filter: ON  PK  Fc   207.45 Hz  Gain  -8.10 dB  Q 1.16551
Filter: ON  PK  Fc   489.05 Hz  Gain  -5.33 dB  Q 4.59252
Filter: ON  PK  Fc  1066.27 Hz  Gain   6.03 dB  Q 7.39026
Filter: ON  PK  Fc  1237.16 Hz  Gain   2.93 dB  Q 6.84047
Filter: ON  PK  Fc  2036.61 Hz  Gain  -6.76 dB  Q 1.92023
Filter: ON  PK  Fc  6864.01 Hz  Gain   7.61 dB  Q 1.08258
Filter: ON  PK  Fc  9483.11 Hz  Gain -10.04 dB  Q 1.22215
Filter: ON  PK  Fc 12008.03 Hz  Gain  -1.57 dB  Q 5.16518
Filter: ON  LS  Fc  3307.46 Hz  Gain   3.37 dB  Q 1.64456
Filter: ON  HS  Fc 16602.47 Hz  Gain  -2.32 dB  Q 1.50154