LoudnessModel functions for working with phons, sones and masking


Phon values are obtained from equal loudness contours which measure the sensitivity of the (average) human ear to loudness as a function of frequency. There are 11 such contours for different intensity levels and the calculation interpolates within them. 


Sones are units of so called 'specific loudness', the difference with respect to phons being that they are a linear, not a logarithmic scale. They reflect the linear behaviour of loudness impression in that a perceived doubling is the double of their value, corresponding to an increase of 10 phons. Dynamic levels ppp, pp, p, mf, f, ff, and fff correspond to loudnesses 1, 2, 4, 8, 16, 32, and 64 sones (40, 50, 60, 70, 80, 90 and 100 phons). 


Masking is the perceptual phenomena whereby a louder sound masks (prevents us from hearing) an otherwise fainter sound. The basilar membrane in the cochlea operates as a bank of filters, each having a limited resolving power corresponding to a critical band. Masking happens when two partials stimulate the same critical band. The formulas for calculating this have been derived from experimental data.


ISO 226:2003 contours and interpolation code kindly shared by Nick Collins. 

Masking model implemented from the article: Applying Psychoacoustics in Composition: "Harmonic" Progressions of "Nonharmonic" Sonorities, by Richard Parncutt. Perspectives of New Music, 32:2, 1994.


See also: Dissonance a class that deals with auditory roughness.


Class Methods

No instances are created from this class. It just holds the functions and data to calculate loudness countours and masking. They return floats and arrays of floats. 


*calc(freq, spl)

Returns the phon value for a certain frequency in Hz and intensity in dB SPL

// examples

LoudnessModel.calc(1000, 100); //at 1000Hz phons and dB spl are the same

LoudnessModel.calc(50,100); // at 50Hz the perception of those 100dB is 23dB lower

LoudnessModel.calc(30,100); // and falls drastically by 30Hz

LoudnessModel.calc(8000,100); // also lower at 8000Hz

However, there is a convinience method in the additions to SimpleNumber and Sequenceable Collection that simplifies this:

asPhon(freq, spl, calib = 0) (a method of SimpleNumber and SequenceableCollection)

The method is made to work for numbers, pairs, and arrays of pairs: 

50.asPhon(100);

[8000, 100].asPhon; 

[[110, 80], [440, 80], [880, 80], [1720,80]].asPhon;

Calib is used for calibration when the values for intensity have been converted directly from amplitudes: 

f = Array.series(5, 100, 100);

//ampdb will be relative to 0 dBFS (dB Full Scale), not to 'absolute' dB spl:

a = Array.fill(5, {|i| (i+1).reciprocal}).ampdb; // 1/n amp array in dB

[f, a].flop.asPhon(80).round(0.01); // calibrate so that 80dB spl = 0 dBFS

// -> [ 61.45, 65.28, 65.72, 65.28, 64.65 ]


To convert from amplitudes to sones, an additional step is needed to go from phons to sones, using  SimpleNumber:phonToSone (also included is the inverse function soneToPhon). There is a convenience method that behaves in the same way as asPhon:


asSone(freq, spl, calib = 0) (a method of SimpleNumber and SequenceableCollection)

Calib works in the same way as in asPhon.


1000.asSone(100); // fff of a 1000Hz sine is 64 sones

[1000, 40].asSone;// the range ppp-fff corresponds to the range 40-100dB spl (at 1000Hz)

[[110, 80], [440, 80], [880, 80], [1720,80]].asSone;

f = Array.series(5, 100, 100);

//ampdb will be relative to 0 dBFS (dB Full Scale), not to 'absolute' dB spl:

a = Array.fill(5, {|i| (i+1).reciprocal}).ampdb; 

[f, a].flop.asSone(100).round(0.01); // calibrate so that 100dB spl = 0 dBFS

// -> [ 33.33, 32.25, 29.15, 26.37, 24.04 ]


Methods for masking analysis:

For most applications you will only need to use *compensateMasking which returns the amplitudes of partials compensated to account for this effect. The other methods go through the process of masking calculation and call each other progressively.  *audibleLevel and *audibility are usefull for knowing how many dB or in what proportion do partials mask each other. 

They are presented from the highest to the lowest level of processing: 

*compensateMasking(partials, levels, grad = 12)

This method handles all the details of masking analysis. It calculates the audibility and uses it as weights for compensating the loudness of each partial in arrays partials and levels. It returns the dB spl values for each of the partials. 

// Example: 

f = [ [400, 90], [500, 80], [700, 75] ]; // pairs of freqs, levels

p = f.flop; // express them as separate arrays of freqs and levels

m = LoudnessModel.compensateMasking(p[0], p[1]); // -> [ 90, 72.335687836532, 73.853070085656 ]

[p[0], m].flop.asSone.round(0.01); // -> [ 30.8, 8.76, 10.52 ] results in sones

f.asSone.round(0.01); 

// -> [ 30.8, 15.42, 11.42 ] (the second partial is half as loud after masking)


Convenience method for arrays of pairs [freq, spl]:

f.compensateMasking; //work directly on array f, no need to flop it

[p[0], f.compensateMasking].flop.asSone.round(0.01); 


*audibility(f, a, grad = 12)

Gives the audibility (also called 'spectral pitch weight') for arrays of partials and levels. It is a value from 0.0 to 1.0, a weight of how audible a partial is after masking.

// Example: 

f = [ [400, 90], [500, 80], [700, 75] ]; // pairs of freqs, levels

p = f.flop; // express them as separate arrays of freqs and levels

LoudnessModel.audibility(p[0], p[1]);


*audibleLevel(partials, levels, grad = 12)

Gives as a result the audible level for arrays partials and levels. 

// Example (follows from previous):

LoudnessModel.audibleLevel(p[0], p[1]);


*maskingSum(partials, levels, grad = 12)

Calculates the overal masking level for arrays of partials (in Hz) and levels (in dB spl). 


// Example (following the previous ones): 

LoudnessModel.maskingSum(p[0], p[1]);

// -> [ 63.468028014719, 73.693756009561, 55.533500735127 ]


*partialMasking(p, p2, a2, grad = 12)

Where p is the maskee, p2 the masker and a2 the level of the masker. p1 and p2 should be in erb units (Equivalent Rectangular Bandwidth, use SimpleNumber:hzToErb) and a2 in dB spl; grad is the gradient of the masking pattern of a single pure tone, it is derived from experimental data and normally should be in the range of 12 to 18dB per critical band (12 meaning more masking than 18).

// Example: the extent to which two sine tones of 400 and 500 Hz with levels of 

// 50 and 60 dB respectively mask each other:

LoudnessModel.partialMasking(400.hzToErb, 500.hzToErb, 60); // -> 43.27..

LoudnessModel.partialMasking(500.hzToErb, 400.hzToErb, 50); // -> 33.27..

// this means that the higher component masks the lower by 43dB, its audible level

// is 50 - 43 = 7dB (it is only weakly audible). Conversely, the degree to which the lower

// component masks the upper one is 60 - 33 = 27dB, considerably higher. 


...


LoudnessModel: (2007), juan sebastián lach lau http://web.me.com/jslach/