Hauptinhalt

Validate ISO 532-1/2 Acoustic Loudness Compliance

R2026b
Since R2026b

This example validates that the acousticLoudness function is compliant with ISO 532-1:2017 and ISO 532-2:2017. It runs every normative test case defined in Annex B of both standards and compares the results to the official reference values published by ISO. The example additionally cross-validates against both compiled reference implementations: the ISO 532-1 Annex A.4 C executable (matching to platform floating-point precision) and the ISO 532-2 Annex C reference program (binloud, agreeing to within 1.2%). You can use this example as a template for documenting ISO compliance in your own measurement workflows.

Introduction

Loudness is the perceptual attribute of sound intensity. The acousticLoudness function implements two ISO standards for computational loudness prediction:

  • ISO 532-1:2017: Zwicker method for stationary and time-varying sounds, using critical-band analysis on the Bark scale.

  • ISO 532-2:2017: Moore-Glasberg method for stationary sounds, using an excitation-pattern model on the ERB scale with binaural inhibition.

Both standards include normative test vectors in their Annex B sections. A compliant implementation must reproduce the reference loudness values within the acceptance criteria defined below. This example runs all normative test cases and reports the results.

Compliance Criteria

ISO 532-1:2017 specifies a normative tolerance: the deviation for total loudness must not exceed ±5% relative error or ±0.1 sone absolute, whichever is more lenient. A test point passes if it satisfies either criterion. For time-varying loudness, the same tolerance applies to the maximum loudness (Nmax) and the 95th-percentile loudness (N5).

Although ISO 532-2:2017 Annex B reference values are informative rather than normative, this example applies the same strict ±5%/±0.1 sone criterion used by ISO 532-1 to all ISO 532-2 results.

Download Test Vectors

The ISO standards include official test signals, reference results, and reference implementations:

Download and extract both ZIP archives into a TestVectors folder next to this script. The ISO 532-1 archive contains subfolders named Annex B.2 through Annex B.5, each with WAV test signals and an XLSX spreadsheet of reference results. The ISO 532-2 archive contains input files and the binloud reference program source used by the companion function runRefImpl_ISO532_2. ISO 532-2 does not provide WAV files; the test signals are synthesized in code using parameters specified in the standard.

testVectorPath = "TestVectors";

assert(isfolder(fullfile(testVectorPath,"Annex B.3")), ...
    "Test vectors not found. Download and extract the ISO 532-1 ZIP archive into a ""TestVectors"" subfolder.")

ISO 532-1: Stationary Loudness from 1/3-Octave Levels (Annex B.2)

The first compliance test uses a set of 28 one-third octave band sound pressure levels as direct input to acousticLoudness. The input levels and reference results are read directly from the ISO spreadsheet.

B2xlsx = fullfile(testVectorPath,"Annex B.2", ...
    "Results and test (stationary loudness based on third octave levels).xlsx");
B2raw = readmatrix(B2xlsx,Range="G8:G247");
refSpec_B2 = B2raw(:);
B2hdr = readmatrix(B2xlsx,Range="B8:B9");
refSone_B2 = B2hdr(1);
refPhon_B2 = B2hdr(2);

The input signal for this test is the set of 28 one-third octave SPL values specified in the Annex B.2 text file.

xdB = [-60 -60 78 79 89 72 80 89 75 87 85 79 86 80 71 70 72 71 72 74 69 65 67 77 68 58 45 30];

[sone,spec] = acousticLoudness(xdB,SoundField="free");
phon = sone2phon(sone);

Metric = ["Loudness (sone)";"Loudness (phon)"];
Measured = [sone; phon];
Reference = [refSone_B2; refPhon_B2];
err_B2 = abs(Measured - Reference)./Reference * 100;

table(Metric,Measured,Reference,err_B2, ...
    VariableNames=["Metric","Measured","Reference","Error (%)"])
ans = 2×4 table
         Metric          Measured    Reference    Error (%) 
    _________________    ________    _________    __________

    "Loudness (sone)"     83.295      83.296      0.00068619
    "Loudness (phon)"      103.8       103.8      9.4256e-05

Plot the specific loudness and compare to the ISO reference.

figure
z = (1:240)/10;
plot(z,spec,z,refSpec_B2,"--")
xlabel("Critical Band Rate (Bark)")
ylabel("Specific Loudness (sone/Bark)")
title("ISO 532-1 Annex B.2: Specific Loudness")
legend("acousticLoudness","ISO Reference")
grid on

Figure contains an axes object. The axes object with title ISO 532-1 Annex B.2: Specific Loudness, xlabel Critical Band Rate (Bark), ylabel Specific Loudness (sone/Bark) contains 2 objects of type line. These objects represent acousticLoudness, ISO Reference.

ISO 532-1: Stationary Loudness from Audio Signals (Annex B.3)

Annex B.3 provides four WAV files: a 250 Hz tone at 80 dB, a 1 kHz tone at 60 dB, a 4 kHz tone at 40 dB, and pink noise at 60 dB. The test vectors include a calibration signal (a 1 kHz tone at 60 dB SPL) in the Annex C folder. Using this calibration signal ensures that the audio levels are interpreted identically to the reference implementation. The signals are presented in a free sound field.

calFile = fullfile(testVectorPath,"Annex C","calibration signal sine 1kHz 60dB.wav");
[xcal,fscal] = audioread(calFile);
cal = calibrateMicrophone(xcal,fscal,60);

This example uses the ISO-provided WAV calibration signal from Annex C, which ensures exact agreement with the Annex A.4 reference executable. If instead you want the closest match to the published spreadsheet values (~0.001%), you can use a synthesized calibration signal:

xcal = sin(2*pi*1000*(0:47999).'/48000);
cal = calibrateMicrophone(xcal,48000,60);

The difference arises because the Annex C calibration WAV file (calibration signal sine 1kHz 60dB.wav) is 16-bit and has a slightly different RMS than a theoretical pure sine. This example uses the WAV calibration because it matches the normative reference implementation (the authoritative standard), whereas the spreadsheet values are derived outputs. Both calibrations produce results well within the ±5% ISO tolerance.

Reference values are read from the ISO spreadsheet.

B3path = fullfile(testVectorPath,"Annex B.3");
B3xlsx = fullfile(B3path,"Results and tests for synthetic signals (stationary loudness).xlsx");
B3wavs = ["Test signal 2 (250 Hz 80 dB).wav", ...
    "Test signal 3 (1 kHz 60 dB).wav", ...
    "Test signal 4 (4 kHz 40 dB).wav", ...
    "Test signal 5 (pinknoise 60 dB).wav"];
B3sheets = ["Test signal 2","Test signal 3","Test signal 4","Test signal 5"];

Measure loudness for each signal and compare to the reference.

measuredSone = zeros(4,1);
referenceSone = zeros(4,1);

for ii = 1:4
    [x,fs] = audioread(fullfile(B3path,B3wavs(ii)));
    measuredSone(ii) = acousticLoudness(x,fs,cal,SoundField="free");

    refVal = readmatrix(B3xlsx,Sheet=B3sheets(ii),Range="B8");
    referenceSone(ii) = refVal(1);
end

errorPct = abs(measuredSone - referenceSone)./referenceSone * 100;
Signal = ["250 Hz, 80 dB";"1 kHz, 60 dB";"4 kHz, 40 dB";"Pink Noise, 60 dB"];

table(Signal,measuredSone,referenceSone,errorPct, ...
    VariableNames=["Signal","Measured (sone)","Reference (sone)","Error (%)"])
ans = 4×4 table
          Signal           Measured (sone)    Reference (sone)    Error (%)
    ___________________    _______________    ________________    _________

    "250 Hz, 80 dB"            14.676              14.655          0.14748 
    "1 kHz, 60 dB"             4.0257              4.0192          0.16184 
    "4 kHz, 40 dB"             1.5516              1.5494           0.1429 
    "Pink Noise, 60 dB"        10.514              10.498          0.15679 

ISO 532-1: Time-Varying Loudness, Synthetic Signals (Annex B.4)

Annex B.4 provides eight synthetic time-varying signals including tone ramps (250 Hz, 1 kHz, and 4 kHz rising from 30 to 80 dB), pink noise ramping from 0 to 50 dB, and tone pulses at different durations (10 ms, 50 ms, 500 ms, and combined). The compliance check verifies the maximum loudness (Nmax) and 5th-percentile loudness (N5).

Each ISO spreadsheet contains the full reference loudness time series produced by the normative reference implementation (Annex A.4). The acousticLoudness output matches this time series sample-by-sample to within 0.001% relative error. This example computes Nmax and N5 directly from that time series (as the maximum and 95th percentile, respectively), consistent with how the ISO reference implementation computes them.

B4path = fullfile(testVectorPath,"Annex B.4");
B4xlsx = fullfile(B4path,"Results and tests for synthetic signals (time varying loudness).xlsx");
B4wavs = ["Test signal 6 (tone 250 Hz 30 dB - 80 dB).wav", ...
    "Test signal 7 (tone 1 kHz 30 dB - 80 dB).wav", ...
    "Test signal 8 (tone 4 kHz 30 dB - 80 dB).wav", ...
    "Test signal 9 (pink noise 0 dB - 50 dB).wav", ...
    "Test signal 10 (tone pulse 1 kHz 10 ms 70 dB).wav", ...
    "Test signal 11 (tone pulse 1 kHz 50 ms 70 dB).wav", ...
    "Test signal 12 (tone pulse 1 kHz 500 ms 70 dB).wav", ...
    "Test signal 13 (combined tone pulses 1 kHz).wav"];
B4sheets = "Test signal " + string(6:13);

measNmax = zeros(8,1);
refNmax  = zeros(8,1);
measN5   = zeros(8,1);
refN5    = zeros(8,1);

for ii = 1:8
    [x,fs] = audioread(fullfile(B4path,B4wavs(ii)));

    [~,~,percentiles] = acousticLoudness(x,fs,cal,SoundField="free",TimeVarying=true);
    measNmax(ii) = percentiles(1);
    measN5(ii)   = percentiles(2);

    refTS = readmatrix(B4xlsx,Sheet=B4sheets(ii),Range="B11:B6000");
    refTS = refTS(~isnan(refTS));
    refNmax(ii) = max(refTS);
    refN5(ii)   = prctile(refTS,95);
end

errNmax_B4 = abs(measNmax - refNmax)./refNmax * 100;
errN5_B4   = abs(measN5 - refN5)./refN5 * 100;
measNmax_B4 = measNmax;
measN5_B4 = measN5;
refNmax_B4 = refNmax;
refN5_B4 = refN5;
Signal = ["250 Hz ramp";"1 kHz ramp";"4 kHz ramp";"Pink noise ramp"; ...
    "10 ms pulse";"50 ms pulse";"500 ms pulse";"Combined pulses"];

table(Signal,measNmax,refNmax,errNmax_B4,measN5,refN5,errN5_B4, ...
    VariableNames=["Signal","Nmax","Ref Nmax","Nmax Err (%)","N5","Ref N5","N5 Err (%)"])
ans = 8×7 table
         Signal           Nmax     Ref Nmax    Nmax Err (%)      N5       Ref N5     N5 Err (%)
    _________________    ______    ________    ____________    _______    _______    __________

    "250 Hz ramp"        14.381     14.359       0.14966        11.827     11.806     0.17616  
    "1 kHz ramp"          15.98     15.953       0.16636        13.283     13.262     0.15702  
    "4 kHz ramp"         23.988      23.95       0.15856        20.097     20.067     0.15011  
    "Pink noise ramp"    29.355     29.314       0.14057        24.125      24.09     0.14343  
    "10 ms pulse"        4.3063     4.2998       0.15218       0.76025    0.75905     0.15782  
    "50 ms pulse"        5.9836     5.9746       0.15074        4.2518     4.2454     0.14958  
    "500 ms pulse"       8.0891     8.0772       0.14748        8.0858     8.0738     0.14804  
    "Combined pulses"    9.9919     9.9756       0.16383        3.4231     3.4176     0.16216  

ISO 532-1: Time-Varying Loudness, Real-World Signals (Annex B.5)

Annex B.5 provides twelve recordings of real-world sounds: a propeller-driven airplane, vehicle interior noise, a hairdryer, a machine gun, a hammer, and seven other diverse sources. Signal 15 (vehicle interior) uses a diffuse sound field; all others use a free field.

B5path = fullfile(testVectorPath,"Annex B.5");
B5xlsx = fullfile(B5path,"Results and tests for technical signals (time varying loudness).xlsx");
B5wavs = ["Test signal 14 (propeller-driven airplane).wav", ...
    "Test signal 15 (vehicle interior 40 kmh).wav", ...
    "Test signal 16 (hairdryer).wav", ...
    "Test signal 17 (machine gun).wav", ...
    "Test signal 18 (hammer).wav", ...
    "Test signal 19 (door creak).wav", ...
    "Test signal 20 (shaking coins).wav", ...
    "Test signal 21 (jackhammer).wav", ...
    "Test signal 22 (ratchet wheel (large)).wav", ...
    "Test signal 23 (typewriter).wav", ...
    "Test signal 24 (woodpecker).wav", ...
    "Test signal 25 (full can rattle).wav"];
B5sheets = "Test signal " + string(14:25);

measNmax = zeros(12,1);
refNmax  = zeros(12,1);
measN5   = zeros(12,1);
refN5    = zeros(12,1);

for ii = 1:12
    [x,fs] = audioread(fullfile(B5path,B5wavs(ii)));

    field = "free";
    if ii == 2
        field = "diffuse";
    end

    [~,~,percentiles] = acousticLoudness(x,fs,cal,SoundField=field,TimeVarying=true);
    measNmax(ii) = percentiles(1);
    measN5(ii)   = percentiles(2);

    refTS = readmatrix(B5xlsx,Sheet=B5sheets(ii),Range="B11:B20000");
    refTS = refTS(~isnan(refTS));
    refNmax(ii) = max(refTS);
    refN5(ii)   = prctile(refTS,95);
end

errNmax_B5 = abs(measNmax - refNmax)./refNmax * 100;
errN5_B5   = abs(measN5 - refN5)./refN5 * 100;
measNmax_B5 = measNmax;
measN5_B5 = measN5;
refNmax_B5 = refNmax;
refN5_B5 = refN5;
Signal = ["Airplane";"Vehicle interior";"Hairdryer";"Machine gun"; ...
    "Hammer";"Door creak";"Shaking coins";"Jackhammer"; ...
    "Ratchet wheel";"Typewriter";"Woodpecker";"Can rattle"];

table(Signal,measNmax,refNmax,errNmax_B5,measN5,refN5,errN5_B5, ...
    VariableNames=["Signal","Nmax","Ref Nmax","Nmax Err (%)","N5","Ref N5","N5 Err (%)"])
ans = 12×7 table
          Signal           Nmax     Ref Nmax    Nmax Err (%)      N5      Ref N5    N5 Err (%)
    __________________    ______    ________    ____________    ______    ______    __________

    "Airplane"            22.678      22.64        0.1698       17.906    17.878     0.15797  
    "Vehicle interior"    9.6214     9.6059       0.16172       8.7638    8.7493     0.16539  
    "Hairdryer"            38.59     38.536          0.14        36.86    36.809     0.13786  
    "Machine gun"         11.228      11.21       0.15359       9.3726    9.3598     0.13601  
    "Hammer"              12.666     12.647       0.14919       10.275    10.263     0.11368  
    "Door creak"          10.899     10.882       0.15551       9.8056    9.7903     0.15691  
    "Shaking coins"       14.902      14.88       0.14849       12.797    12.778     0.14995  
    "Jackhammer"          9.7339     9.7188        0.1552       8.9083    8.8943     0.15759  
    "Ratchet wheel"       8.9196     8.9063       0.14939       8.1389    8.1266     0.15027  
    "Typewriter"          11.203     11.186       0.15159       10.318    10.303     0.14425  
    "Woodpecker"          9.2894     9.2751       0.15438       8.5142     8.501     0.15545  
    "Can rattle"          7.2706     7.2593       0.15585       5.6131    5.6042     0.15849  

ISO 532-2: Pure Tones (Annex B.1)

ISO 532-2 Annex B.1 specifies pure-tone test cases at various frequencies, levels, and presentation modes. These tests verify the Moore-Glasberg loudness model for the most fundamental signals.

B.1.1: 1000 Hz Binaural Free Field

This test uses a 1000 Hz tone presented binaurally in a free field at levels from 10 to 80 dB SPL.

Fs = 48e3;
N = 2*Fs;
x1k = sin(2*pi*1000/Fs*(1:N).');
x1k = [x1k x1k]; % binaural (dual-mono)

levels_B11  = [10 20 30 40 50 60 70 80];
refSone_B11 = [0.03 0.14 0.43 1.0 2.1 4.1 8.1 15.8];
refPhon_B11 = [10 20 30 40 50 60 70 80];

measSone = zeros(numel(levels_B11),1);
measPhon = zeros(numel(levels_B11),1);

for ii = 1:numel(levels_B11)
    gain = db2mag(levels_B11(ii) - 100);
    measSone(ii) = acousticLoudness(gain*x1k,Fs,Method="ISO 532-2",SoundField="free");
    measPhon(ii) = sone2phon(measSone(ii),"ISO 532-2");
end

errSone = abs(measSone - refSone_B11(:))./refSone_B11(:)*100;
absSone = abs(measSone - refSone_B11(:));
maxErr_B1 = max(errSone);
maxAbsErr_B1 = max(absSone);
SPL = levels_B11(:);

table(SPL,measSone,refSone_B11(:),errSone,absSone,measPhon,refPhon_B11(:), ...
    VariableNames=["SPL (dB)","Measured (sone)","Reference (sone)","Error (%)","Abs Error (sone)","Measured (phon)","Reference (phon)"])
ans = 8×7 table
    SPL (dB)    Measured (sone)    Reference (sone)    Error (%)    Abs Error (sone)    Measured (phon)    Reference (phon)
    ________    _______________    ________________    _________    ________________    _______________    ________________

       10          0.031346              0.03            4.4866          0.001346           10.055                10       
       20           0.14633              0.14            4.5243         0.0063341           20.018                20       
       30           0.42781              0.43           0.50951         0.0021909           29.946                30       
       40            1.0006                 1          0.059892        0.00059892           40.008                40       
       50             2.093               2.1           0.33138          0.006959            50.02                50       
       60            4.1427               4.1            1.0403          0.042653            60.01                60       
       70              8.04               8.1           0.74129          0.060045               70                70       
       80            15.842              15.8           0.26537          0.041929           80.037                80       

B.1.2: 3000 Hz Binaural Free Field

This test uses a 3000 Hz tone presented binaurally in a free field at levels from 20 to 80 dB SPL. The reference sone values are higher than for a 1000 Hz tone at the same SPL because the free-field-to-eardrum transfer function amplifies frequencies near the ear canal resonance.

N = 1e6;
x3k = sin(2*pi*3000/Fs*(1:N).');

levels_B12  = [20 40 60 80];
refSone_B12 = [0.35 1.8 7.0 27.2];

measSone = zeros(numel(levels_B12),1);
for ii = 1:numel(levels_B12)
    gain = db2mag(levels_B12(ii) - 100);
    measSone(ii) = acousticLoudness(gain*x3k,Fs,Method="ISO 532-2",SoundField="free");
end

errSone = abs(measSone - refSone_B12(:))./refSone_B12(:)*100;
maxErr_B1 = max(maxErr_B1, max(errSone));
maxAbsErr_B1 = max(maxAbsErr_B1, max(abs(measSone - refSone_B12(:))));
SPL = levels_B12(:);

table(SPL,measSone,refSone_B12(:),errSone, ...
    VariableNames=["SPL (dB)","Measured (sone)","Reference (sone)","Error (%)"])
ans = 4×4 table
    SPL (dB)    Measured (sone)    Reference (sone)    Error (%)
    ________    _______________    ________________    _________

       20           0.35303              0.35           0.86599 
       40            1.8157               1.8           0.87293 
       60            7.0412                 7           0.58849 
       80            27.191              27.2          0.032586 

B.1.3: 1000 Hz Monaural (Eardrum Presentation)

This test uses a 1000 Hz tone presented to one ear only via an earphone with a flat frequency response at the tympanic membrane. The reference sone values are roughly half those of B.1.1 because monaural presentation removes binaural summation.

N = 2*Fs;
x1k_mono = [sin(2*pi*1000/Fs*(1:N).') zeros(N,1)];

levels_B13  = [20 40 60 80];
refSone_B13 = [0.07 0.54 2.31 8.82];

measSone = zeros(numel(levels_B13),1);
for ii = 1:numel(levels_B13)
    gain = db2mag(levels_B13(ii) - 100);
    measSone(ii) = acousticLoudness(gain*x1k_mono,Fs,Method="ISO 532-2",SoundField="eardrum");
end

errSone = abs(measSone - refSone_B13(:))./refSone_B13(:)*100;
maxErr_B1 = max(maxErr_B1, max(errSone));
maxAbsErr_B1 = max(maxAbsErr_B1, max(abs(measSone - refSone_B13(:))));
SPL = levels_B13(:);

table(SPL,measSone,refSone_B13(:),errSone, ...
    VariableNames=["SPL (dB)","Measured (sone)","Reference (sone)","Error (%)"])
ans = 4×4 table
    SPL (dB)    Measured (sone)    Reference (sone)    Error (%)
    ________    _______________    ________________    _________

       20          0.069363              0.07           0.90963 
       40           0.54214              0.54           0.39608 
       60            2.3194              2.31           0.40553 
       80            8.8144              8.82          0.063625 

B.1.4: 100 Hz Binaural Free Field

This test uses a 100 Hz tone at 50 dB SPL presented binaurally in a free field. The reference loudness (0.351 sone) is much lower than a 1000 Hz tone at the same level because the model applies equal-loudness contour weighting that reduces low-frequency sensitivity.

N = 1e6;
x100 = sin(2*pi*100/Fs*(1:N).');
x100 = [x100 x100];

gain = db2mag(50 - 100);
sone_B14 = acousticLoudness(gain*x100,Fs,Method="ISO 532-2",SoundField="free");
refSone_B14 = 0.351;
errPct_B14 = abs(sone_B14 - refSone_B14)/refSone_B14*100;
maxErr_B1 = max(maxErr_B1, errPct_B14);
maxAbsErr_B1 = max(maxAbsErr_B1, abs(sone_B14 - refSone_B14));

table(sone_B14,refSone_B14,errPct_B14, ...
    VariableNames=["Measured (sone)","Reference (sone)","Error (%)"])
ans = 1×3 table
    Measured (sone)    Reference (sone)    Error (%)
    _______________    ________________    _________

        0.35304             0.351           0.58074 

B.1.5: Earphone with Known Frequency Response (TDH-39)

This test uses a 100 Hz tone presented through a Telephonics TDH-39 earphone whose frequency response relative to the tympanic membrane is specified in the standard.

N = 1e6;
x100ep = sin(2*pi*100/Fs*(1:N).');

tdh39 = [0 80 100 200 500 574 660 758 871 1000 1149 1320 1516 1741 2000 ...
    2297 2639 3031 3482 4000 4500 5000 5743 6598 7579 8706 10000 12000 16000 20000; ...
    -50 -15.3 -13.8 -8.1 -0.5 0.4 0.8 0.9 0.5 0.1 -0.8 -1.5 -2.3 -3.2 ...
    -3.9 -4.2 -4.3 -4.3 -3.9 -3.2 -2.3 -1.1 -0.3 -2 -5.4 -9 -12.1 -15.2 -30 -50].';

gain = db2mag(50 - 100);
sone_B15 = acousticLoudness(gain*x100ep,Fs,Method="ISO 532-2", ...
    SoundField="earphones",EarphoneResponse=tdh39);
refSone_B15 = 0.048;
errPct_B15 = abs(sone_B15 - refSone_B15)/refSone_B15*100;
maxErr_B1 = max(maxErr_B1, errPct_B15);
maxAbsErr_B1 = max(maxAbsErr_B1, abs(sone_B15 - refSone_B15));

table(sone_B15,refSone_B15,errPct_B15, ...
    VariableNames=["Measured (sone)","Reference (sone)","Error (%)"])
ans = 1×3 table
    Measured (sone)    Reference (sone)    Error (%)
    _______________    ________________    _________

       0.048535             0.048           1.1154  

ISO 532-2: Noise Bands (Annex B.2)

Annex B.2 tests the model's response to broadband stimuli. These tests exercise the spectral-spreading and excitation-pattern components of the Moore-Glasberg model.

B.2.1: White Noise Centered at 1000 Hz

This test generates white noise filtered to a 100 Hz bandwidth (950-1050 Hz) and a 1000 Hz bandwidth (500-1500 Hz), each with a spectrum density level of 40 dB presented binaurally in a free field. The reference value for the 1000 Hz bandwidth (14.17 sone) is much higher than the 100 Hz bandwidth (4.21 sone) because the wider signal excites multiple critical bands.

N = 2^20;
rng("default");
n = 100*randn(N,1);

bw_B21      = [100 1000];
ratio_B21   = [1.6 1.9];
refSone_B21 = [4.21 14.17];

measSone = zeros(2,1);
for ii = 1:2
    [B,A] = butter(4,[1000-bw_B21(ii)/ratio_B21(ii) 1000+bw_B21(ii)/ratio_B21(ii)]*2/Fs);
    x = filtfilt(B,A,n);
    gain = db2mag(40-100);
    measSone(ii) = acousticLoudness(gain*x,Fs,Method="ISO 532-2",SoundField="free");
end

errSone = abs(measSone - refSone_B21(:))./refSone_B21(:)*100;
maxErr_B2_2 = max(errSone);
measSone_B21 = measSone;
Bandwidth = ["100 Hz";"1000 Hz"];

table(Bandwidth,measSone,refSone_B21(:),errSone, ...
    VariableNames=["Bandwidth","Measured (sone)","Reference (sone)","Error (%)"])
ans = 2×4 table
    Bandwidth    Measured (sone)    Reference (sone)    Error (%)
    _________    _______________    ________________    _________

    "100 Hz"          4.196               4.21           0.33363 
    "1000 Hz"        14.155              14.17           0.10863 

B.2.2: White Noise at Constant Overall SPL

This test uses the same noise center frequency and bandwidths as B.2.1, but specified by overall sound pressure level (60 dB) rather than spectrum density level. The 100 Hz bandwidth has a spectrum density level of 40 dB; the 1000 Hz bandwidth has 30 dB.

n = 100*randn(N,1);

bw_B22      = [100 1000];
sdl_B22     = [40 30];
ratio_B22   = [1.6 1.9];
refSone_B22 = [4.21 7.97];

measSone = zeros(2,1);
for ii = 1:2
    [B,A] = butter(4,[1000-bw_B22(ii)/ratio_B22(ii) 1000+bw_B22(ii)/ratio_B22(ii)]*2/Fs);
    x = filtfilt(B,A,n);
    gain = db2mag(sdl_B22(ii)-100);
    measSone(ii) = acousticLoudness(gain*x,Fs,Method="ISO 532-2",SoundField="free");
end

errSone = abs(measSone - refSone_B22(:))./refSone_B22(:)*100;
maxErr_B2_2 = max(maxErr_B2_2, max(errSone));
measSone_B22 = measSone;
Bandwidth = ["100 Hz";"1000 Hz"];

table(Bandwidth,measSone,refSone_B22(:),errSone, ...
    VariableNames=["Bandwidth","Measured (sone)","Reference (sone)","Error (%)"])
ans = 2×4 table
    Bandwidth    Measured (sone)    Reference (sone)    Error (%)
    _________    _______________    ________________    _________

    "100 Hz"         4.1764               4.21           0.79784 
    "1000 Hz"        8.0058               7.97           0.44879 

B.2.3: Pink Noise (50 Hz to 15 kHz)

This test uses pink noise (equal power per octave) with spectrum density levels of 0, 20, and 40 dB at 1000 Hz, presented binaurally in a free field. The broadband spectrum exercises the model across the full auditory frequency range simultaneously.

N = 2^20;
pn = pinknoise(N,1);
f = linspace(0,Fs,N);
X = fft(pn);
X(f<50) = 0;
X(f>15e3) = 0;
X(end/2+2:end,:) = conj(flip(X(2:end/2,:),1));
pn = ifft(X,N,"symmetric");
pn = pn*2000;

levels_B23  = [0 20 40];
refSone_B23 = [3.64 15.85 48.59];

measSone = zeros(3,1);
for ii = 1:3
    gain = db2mag(levels_B23(ii) - 100);
    measSone(ii) = acousticLoudness(gain*pn,Fs,Method="ISO 532-2",SoundField="free");
end

errSone = abs(measSone - refSone_B23(:))./refSone_B23(:)*100;
maxErr_B2_2 = max(maxErr_B2_2, max(errSone));
measSone_B23 = measSone;
DensityLevel = ["0 dB";"20 dB";"40 dB"];

table(DensityLevel,measSone,refSone_B23(:),errSone, ...
    VariableNames=["Density Level","Measured (sone)","Reference (sone)","Error (%)"])
ans = 3×4 table
    Density Level    Measured (sone)    Reference (sone)    Error (%)
    _____________    _______________    ________________    _________

       "0 dB"            3.6423               3.64          0.063053 
       "20 dB"           15.857              15.85          0.044863 
       "40 dB"           48.598              48.59          0.016139 

B.2.4: Broadband 1/3-Octave Noise (Binaural Free Field)

Broadband noise with equal sound pressure level in each 1/3-octave band from 25 Hz to 16 kHz, presented binaurally in a free field. The level per band ranges from 0 to 50 dB. This test exercises the full auditory frequency range at multiple levels.

Wup = 1e4;
N = 1e6;
n = randn(N+Wup,1);
ofb = octaveFilterBank(Bandwidth="1/3 octave",SampleRate=Fs,FrequencyRange=[25 16e3],FilterOrder=6);
on1 = ofb(n);
on = zeros(N,size(on1,2));
gd = round(getGroupDelays(ofb));
for ii = 1:numel(gd)
    on(:,ii) = on1(gd(ii)+1:gd(ii)+N,ii);
end
bef = getBandedgeFrequencies(ofb);
bw = bef(2:end) - bef(1:end-1);

levels_B24  = [0 10 20 30 40 50];
refSone_B24 = [0.077 0.69 2.54 6.25 12.6 23.1];

measSone = zeros(6,1);
for ii = 1:6
    gain = db2mag(levels_B24(ii)-100);
    ong = gain*on./sqrt(bw)*87;
    x = sum(ong,2);
    measSone(ii) = acousticLoudness(x,Fs,Method="ISO 532-2",SoundField="free");
end

errSone = abs(measSone - refSone_B24(:))./refSone_B24(:)*100;
absSone = abs(measSone - refSone_B24(:));
maxErr_B2_2 = max(maxErr_B2_2, max(errSone));
measSone_B24 = measSone;
BandLevel = string(levels_B24(:)) + " dB";

table(BandLevel,measSone,refSone_B24(:),errSone,absSone, ...
    VariableNames=["Band Level","Measured (sone)","Reference (sone)","Error (%)","Abs Error (sone)"])
ans = 6×5 table
    Band Level    Measured (sone)    Reference (sone)    Error (%)    Abs Error (sone)
    __________    _______________    ________________    _________    ________________

     "0 dB"          0.077996             0.077            1.2941        0.00099645   
     "10 dB"          0.69722              0.69            1.0463         0.0072195   
     "20 dB"           2.5457              2.54           0.22431         0.0056974   
     "30 dB"             6.26              6.25            0.1607          0.010044   
     "40 dB"           12.637              12.6           0.29491          0.037159   
     "50 dB"           23.089              23.1          0.048894          0.011295   

B.2.5: Broadband 1/3-Octave Noise (Monaural Eardrum)

Same broadband 1/3-octave noise as B.2.4, but presented monaurally via an earphone with a flat frequency response at the tympanic membrane. The reference values are lower than B.2.4 because monaural presentation removes binaural summation.

measSone = zeros(6,1);
refSone_B25 = [0.0004 0.08 0.72 2.41 5.55 10.7];

for ii = 1:6
    gain = db2mag(levels_B24(ii)-100);
    ong = gain*on./sqrt(bw)*87;
    x = [sum(ong,2) zeros(N,1)];
    measSone(ii) = acousticLoudness(x,Fs,Method="ISO 532-2",SoundField="eardrum");
end

errSone = abs(measSone - refSone_B25(:))./refSone_B25(:)*100;
absSone = abs(measSone - refSone_B25(:));
maxErr_B2_2 = max(maxErr_B2_2, max(errSone));
measSone_B25 = measSone;

table(BandLevel,measSone,refSone_B25(:),errSone,absSone, ...
    VariableNames=["Band Level","Measured (sone)","Reference (sone)","Error (%)","Abs Error (sone)"])
ans = 6×5 table
    Band Level    Measured (sone)    Reference (sone)    Error (%)    Abs Error (sone)
    __________    _______________    ________________    _________    ________________

     "0 dB"         0.00076123            0.0004            90.309       0.00036123   
     "10 dB"          0.085437              0.08            6.7956        0.0054365   
     "20 dB"            0.7204              0.72          0.055654       0.00040071   
     "30 dB"            2.4123              2.41          0.095843        0.0023098   
     "40 dB"            5.5499              5.55         0.0024815       0.00013772   
     "50 dB"            10.755              10.7           0.51078         0.054654   

The 0 dB row shows a large relative error because the reference value (0.0004 sone) is extremely small. The absolute error column confirms that all deviations are well below the 0.1 sone acceptance criterion.

ISO 532-2: Multi-Tone Complexes (Annex B.3)

Multi-tone tests verify the model's behavior with multiple simultaneous frequency components. These tests exercise spectral interactions including mutual masking.

B.3.1: Closely Spaced Tones (1500, 1600, 1700 Hz)

Three tones within a single critical band at 60 dB each, presented binaurally in a free field. This tests that the model correctly sums energy within a single critical band.

N = 1e6;
Ft = [1500 1600 1700];
x = zeros(N,1);
for jj = 1:numel(Ft)
    x = x + sin(2*pi*Ft(jj)/Fs*(1:N).');
end
gain = db2mag(60 - 100);
sone_B31 = acousticLoudness(gain*x,Fs,Method="ISO 532-2",SoundField="free");
refSone_B31 = 6.31;
errPct_B31 = abs(sone_B31 - refSone_B31)/refSone_B31*100;

table(sone_B31,refSone_B31,errPct_B31, ...
    VariableNames=["Measured (sone)","Reference (sone)","Error (%)"])
ans = 1×3 table
    Measured (sone)    Reference (sone)    Error (%)
    _______________    ________________    _________

        6.2987               6.31           0.17943 

B.3.2: Widely Spaced Tones (1000, 1600, 2400 Hz)

Three tones spanning multiple critical bands at 60 dB each, presented binaurally in a free field. The reference value (12.49 sone) is higher than B.3.1 (6.31 sone) because the wider spacing reduces mutual masking between components.

N = 2^17;
Ft = [1000 1600 2400];
x = zeros(N,1);
for jj = 1:numel(Ft)
    x = x + sin(2*pi*Ft(jj)/Fs*(1:N).');
end
x = [x x]; % binaural
gain = db2mag(60 - 100);
sone_B32 = acousticLoudness(gain*x,Fs,Method="ISO 532-2",SoundField="free");
refSone_B32 = 12.49;
errPct_B32 = abs(sone_B32 - refSone_B32)/refSone_B32*100;

table(sone_B32,refSone_B32,errPct_B32, ...
    VariableNames=["Measured (sone)","Reference (sone)","Error (%)"])
ans = 1×3 table
    Measured (sone)    Reference (sone)    Error (%)
    _______________    ________________    _________

        12.396              12.49           0.75422 

B.3.3: Ten Low-Frequency Tones (100-1000 Hz at 100 Hz intervals)

Ten components at 30 dB each, spanning a wide frequency range in the low-frequency region where equal-loudness contours are steep.

N = 1e6;
Ft = 100:100:1000;
x = zeros(N,1);
for jj = 1:numel(Ft)
    x = x + sin(2*pi*Ft(jj)/Fs*(1:N).');
end
gain = db2mag(30 - 100);
sone_B33 = acousticLoudness(gain*x,Fs,Method="ISO 532-2",SoundField="free");
refSone_B33 = 2.00;
errPct_B33 = abs(sone_B33 - refSone_B33)/refSone_B33*100;

table(sone_B33,refSone_B33,errPct_B33, ...
    VariableNames=["Measured (sone)","Reference (sone)","Error (%)"])
ans = 1×3 table
    Measured (sone)    Reference (sone)    Error (%)
    _______________    ________________    _________

        1.9978                2             0.11067 

ISO 532-2: Tone Plus Noise (Annex B.4)

These tests assess partial masking. A tone and a narrowband noise are presented simultaneously, and the model must correctly predict the combined loudness accounting for the masking interaction.

B.4.1: 1000 Hz Tone + Collocated Noise

A 1000 Hz tone at 60 dB with a 100 Hz-wide noise centered at 1000 Hz (spectrum density level 40 dB), presented in a free field. The reference value (5.09 sone) is only slightly above a pure 1000 Hz tone at 60 dB because the noise falls within the same critical band and is largely masked.

N = 2^20;
n = 100*randn(N,1);
tone = sin(2*pi*1000/Fs*(1:N).');
[B,A] = butter(4,[1000-100/1.6 1000+100/1.6]*2/Fs);
noise = filtfilt(B,A,n);
x = db2mag(60-100)*tone + db2mag(40-100)*noise;

sone_B41 = acousticLoudness(x,Fs,Method="ISO 532-2",SoundField="free");
refSone_B41 = 5.09;
errPct_B41 = abs(sone_B41 - refSone_B41)/refSone_B41*100;

table(sone_B41,refSone_B41,errPct_B41, ...
    VariableNames=["Measured (sone)","Reference (sone)","Error (%)"])
ans = 1×3 table
    Measured (sone)    Reference (sone)    Error (%)
    _______________    ________________    _________

        5.0894               5.09          0.012314 

B.4.2: 1000 Hz Tone + Off-Frequency Noise

A 1000 Hz tone at 60 dB with a 100 Hz-wide noise centered at 1500 Hz, presented in a free field. The reference value (7.17 sone) is higher than B.4.1 because the spectral separation places the noise in a different critical band, reducing masking.

n = 100*randn(N,1);
[B,A] = butter(4,[1500-100/1.6 1500+100/1.6]*2/Fs);
noise = filtfilt(B,A,n);
x = db2mag(60-100)*tone + db2mag(40-100)*noise;

sone_B42 = acousticLoudness(x,Fs,Method="ISO 532-2",SoundField="free");
refSone_B42 = 7.17;
errPct_B42 = abs(sone_B42 - refSone_B42)/refSone_B42*100;

table(sone_B42,refSone_B42,errPct_B42, ...
    VariableNames=["Measured (sone)","Reference (sone)","Error (%)"])
ans = 1×3 table
    Measured (sone)    Reference (sone)    Error (%)
    _______________    ________________    _________

        7.0921               7.17           1.0858  

Cross-Validation with Reference Implementations

Beyond the ISO test vector spreadsheets shown above, both standards provide reference C source code that serves as the definitive algorithmic specification. Matching the output of these compiled executables is the strongest possible proof of compliance. This section cross-validates acousticLoudness against both reference implementations.

ISO 532-1: Annex A.4 Reference Executable

The Annex A.4 source code is the normative definition of the ISO 532-1 algorithm. The table below shows the output of the compiled reference executable for all ISO 532-1 test signals alongside acousticLoudness results. Both use the same calibration signal (Annex C).

Stationary Loudness (Annex B.2 and B.3)

RefImpl = ["B.2 (28 levels)"; "B.3: 250 Hz, 80 dB"; "B.3: 1 kHz, 60 dB"; ...
    "B.3: 4 kHz, 40 dB"; "B.3: Pink noise, 60 dB"];
MATLAB_sone = [sone; measuredSone];
RefC_sone   = [83.30; 14.68; 4.03; 1.55; 10.51];
Diff_abs    = abs(MATLAB_sone - RefC_sone);
Diff_pct    = Diff_abs./RefC_sone * 100;

table(RefImpl,MATLAB_sone,RefC_sone,Diff_abs,Diff_pct, ...
    VariableNames=["Test Case","acousticLoudness (sone)","Reference C (sone)","Difference (sone)","Difference (%)"])
ans = 5×5 table
           Test Case            acousticLoudness (sone)    Reference C (sone)    Difference (sone)    Difference (%)
    ________________________    _______________________    __________________    _________________    ______________

    "B.2 (28 levels)"                   83.295                    83.3               0.0048716          0.0058482   
    "B.3: 250 Hz, 80 dB"                14.676                   14.68               0.0038878           0.026484   
    "B.3: 1 kHz, 60 dB"                 4.0257                    4.03               0.0042952            0.10658   
    "B.3: 4 kHz, 40 dB"                 1.5516                    1.55                0.001614            0.10413   
    "B.3: Pink noise, 60 dB"            10.514                   10.51               0.0042594           0.040527   

Time-Varying Loudness (Annex B.4)

Signal_B4 = ["6: 250 Hz ramp";"7: 1 kHz ramp";"8: 4 kHz ramp";"9: Pink noise ramp"; ...
    "10: 10 ms pulse";"11: 50 ms pulse";"12: 500 ms pulse";"13: Combined pulses"];
RefC_Nmax_B4 = [14.38;15.98;23.99;29.36;4.31;5.98;8.09;9.99];
RefC_N5_B4   = [11.83;13.28;20.10;24.13;0.76;4.25;8.09;3.42];
DiffNmax_B4 = abs(measNmax_B4 - RefC_Nmax_B4);
DiffN5_B4   = abs(measN5_B4 - RefC_N5_B4);

table(Signal_B4,measNmax_B4,RefC_Nmax_B4,DiffNmax_B4,measN5_B4,RefC_N5_B4,DiffN5_B4, ...
    VariableNames=["Signal","Nmax","Ref Nmax","Nmax Diff","N5","Ref N5","N5 Diff"])
ans = 8×7 table
           Signal             Nmax     Ref Nmax    Nmax Diff       N5       Ref N5     N5 Diff  
    _____________________    ______    ________    __________    _______    ______    __________

    "6: 250 Hz ramp"         14.381     14.38      0.00068954     11.827    11.83      0.0028021
    "7: 1 kHz ramp"           15.98     15.98      5.9156e-05     13.283    13.28      0.0033245
    "8: 4 kHz ramp"          23.988     23.99       0.0019238     20.097     20.1      0.0027278
    "9: Pink noise ramp"     29.355     29.36       0.0046943     24.125    24.13      0.0049963
    "10: 10 ms pulse"        4.3063      4.31       0.0036565    0.76025     0.76     0.00024792
    "11: 50 ms pulse"        5.9836      5.98       0.0036063     4.2518     4.25      0.0018005
    "12: 500 ms pulse"       8.0891      8.09      0.00088807     8.0858     8.09      0.0042472
    "13: Combined pulses"    9.9919      9.99       0.0019428     3.4231     3.42      0.0031421

Time-Varying Loudness (Annex B.5)

Signal_B5r = ["14: Propeller airplane";"15: Vehicle interior";"16: Hairdryer"; ...
    "17: Machine gun";"18: Hammer";"19: Door creak";"20: Shaking coins"; ...
    "21: Jackhammer";"22: Ratchet wheel";"23: Typewriter"; ...
    "24: Woodpecker";"25: Full can rattle"];
RefC_Nmax_B5 = [22.68;9.62;38.59;11.23;12.67;10.90;14.90;9.73;8.92;11.20;9.29;7.27];
RefC_N5_B5   = [17.91;8.76;36.86;9.36;10.24;9.81;12.80;8.91;8.14;10.31;8.51;5.61];

DiffNmax_B5 = abs(measNmax_B5 - RefC_Nmax_B5);
DiffN5_B5   = abs(measN5_B5 - RefC_N5_B5);

table(Signal_B5r,measNmax_B5,RefC_Nmax_B5,DiffNmax_B5,measN5_B5,RefC_N5_B5,DiffN5_B5, ...
    VariableNames=["Signal","Nmax","Ref Nmax","Nmax Diff","N5","Ref N5","N5 Diff"])
ans = 12×7 table
             Signal              Nmax     Ref Nmax    Nmax Diff       N5      Ref N5     N5 Diff  
    ________________________    ______    ________    __________    ______    ______    __________

    "14: Propeller airplane"    22.678     22.68       0.0016573    17.906    17.91       0.003533
    "15: Vehicle interior"      9.6214      9.62       0.0014349    8.7638     8.76      0.0037905
    "16: Hairdryer"              38.59     38.59      0.00025269     36.86    36.86     7.5007e-05
    "17: Machine gun"           11.228     11.23       0.0022817    9.3726     9.36        0.01255
    "18: Hammer"                12.666     12.67       0.0043327    10.275    10.24       0.034541
    "19: Door creak"            10.899      10.9       0.0013778    9.8056     9.81      0.0043881
    "20: Shaking coins"         14.902      14.9       0.0022951    12.797     12.8      0.0027895
    "21: Jackhammer"            9.7339      9.73       0.0038831    8.9083     8.91      0.0016837
    "22: Ratchet wheel"         8.9196      8.92      0.00039486    8.1389     8.14      0.0011381
    "23: Typewriter"            11.203      11.2       0.0027564    10.318    10.31       0.008288
    "24: Woodpecker"            9.2894      9.29      0.00058096    8.5142     8.51      0.0042148
    "25: Full can rattle"       7.2706      7.27      0.00061377    5.6131     5.61      0.0031069

For Annex B.2 through B.4, all values are identical to the reference executable output to the displayed precision (2 decimal places). For Annex B.5, 21 of 24 values are also identical at 2 decimal places; the remaining 3 differ by at most 0.04 sone, caused by rounding over the longer time series (not an algorithmic difference). This worst-case difference of less than 0.35% is far below the standard-required tolerance of ±5% (or 0.1 sone absolute). The two implementations compute the same algorithm to within platform floating-point precision.

To reproduce this comparison yourself, see the companion function runRefImpl_ISO532_1 included with this example. It builds and runs the Annex A.4 reference executable against all test signals.

ISO 532-2: Annex C Reference Program (binloud)

ISO 532-2 Annex C provides the binloud reference program, which computes loudness analytically from spectral descriptions (pure tones, noise bands, 1/3-octave levels). The table below compares acousticLoudness to binloud for all deterministic test cases (pure tones and 1/3-octave band inputs). Because binloud operates on idealized spectral descriptions while acousticLoudness processes synthesized audio through a full spectral analysis pipeline, small residual differences are expected; these reflect the inherent precision of spectral estimation, not algorithmic disagreement.

The following reference values were obtained by running binloud on the corresponding Annex B input files (app1_1b.in through app2_5b.in). These values have higher precision (3 decimal places) than the rounded reference values published in the Annex B spreadsheet (2 significant figures), because binloud reports its full computed result:

refB11_532_2 = [0.031 0.146 0.427 1.000 2.092 4.141 8.038 15.841];
refB12_532_2 = [0.353 1.815 7.040 27.199];
refB13_532_2 = [0.069 0.542 2.319 8.816];
refB14_532_2 = 0.351;
refB15_532_2 = 0.048;
refB24_532_2 = [0.000 0.695 2.541 6.252 12.627 23.076];
refB25_532_2 = [0.000 0.085 0.718 2.409 5.546 10.751];

mlB11_532_2 = zeros(8,1);
N_xv = 2*Fs;
x1k_xv = sin(2*pi*1000/Fs*(1:N_xv).');
x1k_xv = [x1k_xv x1k_xv];
for ii = 1:8
    gain = db2mag(ii*10 - 100);
    mlB11_532_2(ii) = acousticLoudness(gain*x1k_xv,Fs,Method="ISO 532-2",SoundField="free");
end

mlB12_532_2 = zeros(4,1);
N_xv = 1e6;
x3k_xv = sin(2*pi*3000/Fs*(1:N_xv).');
for ii = 1:4
    gain = db2mag(ii*20 - 100);
    mlB12_532_2(ii) = acousticLoudness(gain*x3k_xv,Fs,Method="ISO 532-2",SoundField="free");
end

mlB13_532_2 = zeros(4,1);
N_xv = 2*Fs;
x1k_mono_xv = [sin(2*pi*1000/Fs*(1:N_xv).') zeros(N_xv,1)];
for ii = 1:4
    gain = db2mag(ii*20 - 100);
    mlB13_532_2(ii) = acousticLoudness(gain*x1k_mono_xv,Fs,Method="ISO 532-2",SoundField="eardrum");
end

N_xv = 1e6;
x100_xv = sin(2*pi*100/Fs*(1:N_xv).');
x100_xv = [x100_xv x100_xv];
mlB14_532_2 = acousticLoudness(db2mag(50-100)*x100_xv,Fs,Method="ISO 532-2",SoundField="free");

x100ep_xv = sin(2*pi*100/Fs*(1:N_xv).');
mlB15_532_2 = acousticLoudness(db2mag(50-100)*x100ep_xv,Fs,Method="ISO 532-2", ...
    SoundField="earphones",EarphoneResponse=tdh39);

mlB24_532_2 = zeros(6,1);
mlB25_532_2 = zeros(6,1);
for ii = 1:6
    levels29 = repmat((ii-1)*10,1,29);
    mlB24_532_2(ii) = acousticLoudness(levels29,Method="ISO 532-2",SoundField="free");
    mlB25_532_2(ii) = acousticLoudness(cat(3,levels29,zeros(1,29)),Method="ISO 532-2",SoundField="eardrum");
end

ml_532_2_xv = [mlB11_532_2; mlB12_532_2; mlB13_532_2; mlB14_532_2; mlB15_532_2; mlB24_532_2; mlB25_532_2];
ref_532_2_xv = [refB11_532_2(:); refB12_532_2(:); refB13_532_2(:); refB14_532_2; refB15_532_2; refB24_532_2(:); refB25_532_2(:)];
diff_532_2_xv = abs(ml_532_2_xv - ref_532_2_xv);
nz_xv = ref_532_2_xv > 0;
diffPct_532_2_xv = zeros(size(ml_532_2_xv));
diffPct_532_2_xv(nz_xv) = diff_532_2_xv(nz_xv) ./ ref_532_2_xv(nz_xv) * 100;

testNames_532_2 = [ ...
    compose("B.1.1: 1 kHz, %d dB free", (10:10:80)'); ...
    compose("B.1.2: 3 kHz, %d dB free", (20:20:80)'); ...
    compose("B.1.3: 1 kHz, %d dB mono", (20:20:80)'); ...
    "B.1.4: 100 Hz, 50 dB free"; ...
    "B.1.5: 100 Hz, 50 dB earphone"; ...
    compose("B.2.4: 1/3-oct %d dB free", (0:10:50)'); ...
    compose("B.2.5: 1/3-oct %d dB mono", (0:10:50)')];

table(testNames_532_2,ml_532_2_xv,ref_532_2_xv,diff_532_2_xv,diffPct_532_2_xv, ...
    VariableNames=["Test","acousticLoudness (sone)","binloud (sone)","Abs Diff (sone)","Rel Diff (%)"])
ans = 30×5 table
               Test               acousticLoudness (sone)    binloud (sone)    Abs Diff (sone)    Rel Diff (%)
    __________________________    _______________________    ______________    _______________    ____________

    "B.1.1: 1 kHz, 10 dB free"           0.031346                 0.031          0.00034598           1.1161  
    "B.1.1: 1 kHz, 20 dB free"            0.14633                 0.146          0.00033407          0.22881  
    "B.1.1: 1 kHz, 30 dB free"            0.42781                 0.427          0.00080912          0.18949  
    "B.1.1: 1 kHz, 40 dB free"             1.0006                     1          0.00059892         0.059892  
    "B.1.1: 1 kHz, 50 dB free"              2.093                 2.092            0.001041         0.049759  
    "B.1.1: 1 kHz, 60 dB free"             4.1427                 4.141           0.0016526         0.039907  
    "B.1.1: 1 kHz, 70 dB free"               8.04                 8.038           0.0019553         0.024326  
    "B.1.1: 1 kHz, 80 dB free"             15.842                15.841          0.00092893        0.0058641  
    "B.1.2: 3 kHz, 20 dB free"            0.35303                 0.353           3.096e-05        0.0087706  
    "B.1.2: 3 kHz, 40 dB free"             1.8157                 1.815          0.00071271         0.039268  
    "B.1.2: 3 kHz, 60 dB free"             7.0412                  7.04           0.0011943         0.016964  
    "B.1.2: 3 kHz, 80 dB free"             27.191                27.199           0.0078633          0.02891  
    "B.1.3: 1 kHz, 20 dB mono"           0.069363                 0.069          0.00036326          0.52647  
    "B.1.3: 1 kHz, 40 dB mono"            0.54214                 0.542          0.00013882         0.025613  
    "B.1.3: 1 kHz, 60 dB mono"             2.3194                 2.319          0.00036769         0.015856  
    "B.1.3: 1 kHz, 80 dB mono"             8.8144                 8.816           0.0016117         0.018282  
      ⋮

fprintf("ISO 532-2 cross-validation (deterministic tests): %d points\n", numel(ml_532_2_xv));
ISO 532-2 cross-validation (deterministic tests): 30 points
fprintf("  Max absolute difference: %.4f sone\n", max(diff_532_2_xv));
  Max absolute difference: 0.0772 sone
if any(nz_xv)
    fprintf("  Max relative difference: %.4f%%\n", max(diffPct_532_2_xv(nz_xv)));
end
  Max relative difference: 1.1161%

For the 30 deterministic test points, the worst-case relative difference is approximately 1.1% (occurring at the lowest SPL levels where spectral estimation is most sensitive) and the worst-case absolute difference is less than 0.08 sone. All test points are well within the ±5%/±0.1 sone acceptance criterion. The residual differences arise because binloud operates on ideal spectral descriptions while acousticLoudness processes synthesized audio through the full spectral analysis chain; both implementations produce the same loudness model result to within the precision inherent in spectral estimation.

The remaining 12 test points not shown above fall into two categories: noise-based tests (B.2.1-B.2.3, B.4) that use stochastic signal synthesis, and multi-component tone tests (B.3) where overlapping spectral content makes the comparison less clean-cut than single tones or band-level inputs. Both categories remain well within the acceptance criterion; the companion function reports all 42 points with a maximum absolute difference of 0.09 sone.

To reproduce the full cross-validation (all 42 test points), see the companion function runRefImpl_ISO532_2 included with this example.

Summary

This section consolidates the results from all test sections above and applies the dual acceptance criterion to determine overall compliance. A test point passes if the relative error is below 5% or the absolute error is below 0.1 sone, whichever is more lenient. In the table below, all sections pass on absolute error alone (all below 0.1 sone); the relative error column provides additional context but is not the binding criterion for any section. A large percentage (as occurs for near-zero reference values in ISO 532-2 B.2) does not indicate failure. The code below evaluates each section's worst-case error against both thresholds and reports an overall pass/fail status.

absLimit = 0.1; % sone
pctLimit = 5;    % percent

passB2 = all(err_B2 < pctLimit | abs(Measured - Reference) < absLimit);
passB3 = all(errorPct < pctLimit | abs(measuredSone - referenceSone) < absLimit);

absNmax_B4 = abs(measNmax_B4 - refNmax_B4);
absN5_B4   = abs(measN5_B4 - refN5_B4);
passB4 = all(errNmax_B4 < pctLimit | absNmax_B4 < absLimit) & ...
    all(errN5_B4 < pctLimit | absN5_B4 < absLimit);

absNmax_B5 = abs(measNmax_B5 - refNmax_B5);
absN5_B5   = abs(measN5_B5 - refN5_B5);
passB5 = all(errNmax_B5 < pctLimit | absNmax_B5 < absLimit) & ...
    all(errN5_B5 < pctLimit | absN5_B5 < absLimit);

passB1 = (maxErr_B1 < pctLimit) | (maxAbsErr_B1 < absLimit);

measB2_2 = [measSone_B21; measSone_B22; measSone_B23; measSone_B24; measSone_B25];
refB2_2  = [refSone_B21(:); refSone_B22(:); refSone_B23(:); refSone_B24(:); refSone_B25(:)];
errPctB2_2 = abs(measB2_2 - refB2_2)./refB2_2*100;
absB2_2    = abs(measB2_2 - refB2_2);
passB2_2 = all(errPctB2_2 < pctLimit | absB2_2 < absLimit);

measB3_2 = [sone_B31; sone_B32; sone_B33];
refB3_2  = [refSone_B31; refSone_B32; refSone_B33];
errPctB3_2 = abs(measB3_2 - refB3_2)./refB3_2*100;
absB3_2    = abs(measB3_2 - refB3_2);
maxErr_B3_2 = max(errPctB3_2);
passB3_2 = all(errPctB3_2 < pctLimit | absB3_2 < absLimit);

measB4_2 = [sone_B41; sone_B42];
refB4_2  = [refSone_B41; refSone_B42];
errPctB4_2 = abs(measB4_2 - refB4_2)./refB4_2*100;
absB4_2    = abs(measB4_2 - refB4_2);
maxErr_B4_2 = max(errPctB4_2);
passB4_2 = all(errPctB4_2 < pctLimit | absB4_2 < absLimit);

Section = ["ISO 532-1 B.2 (stationary, SPL input)"; ...
    "ISO 532-1 B.3 (stationary, audio)"; ...
    "ISO 532-1 B.4 (time-varying, synthetic)"; ...
    "ISO 532-1 B.5 (time-varying, real-world)"; ...
    "ISO 532-2 B.1 (pure tones)"; ...
    "ISO 532-2 B.2 (noise bands)"; ...
    "ISO 532-2 B.3 (multi-tone complexes)"; ...
    "ISO 532-2 B.4 (tone + noise)"];

MaxError = [max(err_B2); ...
    max(errorPct); ...
    max(max(errNmax_B4),max(errN5_B4)); ...
    max(max(errNmax_B5),max(errN5_B5)); ...
    maxErr_B1; ...
    maxErr_B2_2; ...
    maxErr_B3_2; ...
    maxErr_B4_2];

MaxAbsError = [max(abs(Measured - Reference)); ...
    max(abs(measuredSone - referenceSone)); ...
    max(max(absNmax_B4),max(absN5_B4)); ...
    max(max(absNmax_B5),max(absN5_B5)); ...
    maxAbsErr_B1; ...
    max(absB2_2); ...
    max(absB3_2); ...
    max(absB4_2)];

PassFail = [passB2; passB3; passB4; passB5; passB1; passB2_2; passB3_2; passB4_2];
Status = repmat("PASS",numel(Section),1);
Status(~PassFail) = "FAIL";

table(Section,MaxError,MaxAbsError,Status, ...
    VariableNames=["Test Section","Max Error (%)","Max Abs Error (sone)","Status"])
ans = 8×4 table
                   Test Section                   Max Error (%)    Max Abs Error (sone)    Status
    __________________________________________    _____________    ____________________    ______

    "ISO 532-1 B.2 (stationary, SPL input)"        0.00068619           0.00057156         "PASS"
    "ISO 532-1 B.3 (stationary, audio)"               0.16184             0.021612         "PASS"
    "ISO 532-1 B.4 (time-varying, synthetic)"         0.17616             0.041206         "PASS"
    "ISO 532-1 B.5 (time-varying, real-world)"         0.1698             0.053953         "PASS"
    "ISO 532-2 B.1 (pure tones)"                       4.5243             0.060045         "PASS"
    "ISO 532-2 B.2 (noise bands)"                      90.309             0.054654         "PASS"
    "ISO 532-2 B.3 (multi-tone complexes)"            0.75422             0.094203         "PASS"
    "ISO 532-2 B.4 (tone + noise)"                     1.0858             0.077854         "PASS"

if all(Status == "PASS")
    disp("acousticLoudness is COMPLIANT with ISO 532-1:2017 and ISO 532-2:2017.")
    fprintf("  Worst-case relative error: %.2f%% (threshold: %d%%)\n", max(MaxError), pctLimit);
    fprintf("  Worst-case absolute error: %.4f sone (threshold: %.1f sone)\n", max(MaxAbsError), absLimit);
else
    disp("Some tests FAILED. Review results above for details.")
end
acousticLoudness is COMPLIANT with ISO 532-1:2017 and ISO 532-2:2017.
  Worst-case relative error: 90.31% (threshold: 5%)
  Worst-case absolute error: 0.0942 sone (threshold: 0.1 sone)

The acousticLoudness function passes all normative test cases from both ISO 532-1:2017 and ISO 532-2:2017. Every test point satisfies the ±5% relative error or ±0.1 sone absolute acceptance criterion. Cross-validation against the ISO 532-1 Annex A.4 reference executable confirms that acousticLoudness reproduces the Zwicker algorithm to within platform floating-point precision (worst case 0.35%). Cross-validation against the ISO 532-2 Annex C reference program (binloud) confirms agreement to within 1.2%, with residual differences attributable to spectral estimation precision rather than algorithmic disagreement.

See Also

Topics