nickjillings@1318
|
1 /**
|
nickjillings@1318
|
2 * loundess.js
|
nickjillings@1318
|
3 * Loudness module for the Web Audio Evaluation Toolbox
|
nickjillings@1318
|
4 * Allows for automatic calculation of loudness of Web Audio API Buffer objects,
|
nickjillings@1318
|
5 * return gain values to correct for a target loudness or match loudness between
|
nickjillings@1318
|
6 * multiple objects
|
nickjillings@1318
|
7 */
|
nickjillings@1318
|
8
|
nickjillings@1318
|
9 var interval_cal_loudness_event = null;
|
nickjillings@1318
|
10
|
nickjillings@1318
|
11 if (typeof OfflineAudioContext == "undefined"){
|
nickjillings@1318
|
12 var OfflineAudioContext = webkitOfflineAudioContext;
|
nickjillings@1318
|
13 }
|
nickjillings@1318
|
14
|
nickjillings@1318
|
15 function calculateLoudness(buffer, timescale, target, offlineContext)
|
nickjillings@1318
|
16 {
|
nickjillings@1318
|
17 // This function returns the EBU R 128 specification loudness model and sets the linear gain required to match -23 LUFS
|
nickjillings@1318
|
18 // buffer -> Web Audio API Buffer object
|
nickjillings@1318
|
19 // timescale -> M or Momentary (returns Array), S or Short (returns Array),
|
nickjillings@1318
|
20 // I or Integrated (default, returns number)
|
nickjillings@1318
|
21 // target -> default is -23 LUFS but can be any LUFS measurement.
|
nickjillings@1318
|
22
|
nickjillings@1318
|
23 if (buffer == undefined)
|
nickjillings@1318
|
24 {
|
nickjillings@1318
|
25 return 0;
|
nickjillings@1318
|
26 }
|
nickjillings@1318
|
27 if (timescale == undefined)
|
nickjillings@1318
|
28 {
|
nickjillings@1318
|
29 timescale = "I";
|
nickjillings@1318
|
30 }
|
nickjillings@1318
|
31 if (target == undefined)
|
nickjillings@1318
|
32 {
|
nickjillings@1318
|
33 target = -23;
|
nickjillings@1318
|
34 }
|
nickjillings@1318
|
35 if (offlineContext == undefined)
|
nickjillings@1318
|
36 {
|
nickjillings@1318
|
37 offlineContext = new OfflineAudioContext(buffer.numberOfChannels, buffer.length, buffer.sampleRate);
|
nickjillings@1318
|
38 }
|
nickjillings@1318
|
39 // Create the required filters
|
nickjillings@1318
|
40 var KFilter = offlineContext.createBiquadFilter();
|
nickjillings@1318
|
41 KFilter.type = "highshelf";
|
nickjillings@1318
|
42 KFilter.gain.value = 4;
|
nickjillings@1318
|
43 KFilter.frequency.value = 1480;
|
nickjillings@1318
|
44
|
nickjillings@1318
|
45 var HPFilter = offlineContext.createBiquadFilter();
|
nickjillings@1318
|
46 HPFilter.type = "highpass";
|
nickjillings@1318
|
47 HPFilter.Q.value = 0.707;
|
nickjillings@1318
|
48 HPFilter.frequency.value = 60;
|
nickjillings@1318
|
49 // copy Data into the process buffer
|
nickjillings@1318
|
50 var processSource = offlineContext.createBufferSource();
|
nickjillings@1318
|
51 processSource.buffer = buffer;
|
nickjillings@1318
|
52
|
nickjillings@1318
|
53 processSource.connect(KFilter);
|
nickjillings@1318
|
54 KFilter.connect(HPFilter);
|
nickjillings@1318
|
55 HPFilter.connect(offlineContext.destination);
|
nickjillings@1318
|
56 processSource.start();
|
nickjillings@1318
|
57 offlineContext.oncomplete = function(renderedBuffer) {
|
nickjillings@1318
|
58 // Have the renderedBuffer information, now continue processing
|
nickjillings@1318
|
59 if (typeof renderedBuffer.renderedBuffer == 'object') {
|
nickjillings@1318
|
60 renderedBuffer = renderedBuffer.renderedBuffer;
|
nickjillings@1318
|
61 }
|
nickjillings@1318
|
62 switch(timescale)
|
nickjillings@1318
|
63 {
|
nickjillings@1318
|
64 case "I":
|
nickjillings@1318
|
65 var blockEnergy = calculateProcessedLoudness(renderedBuffer, 400, 0.75);
|
nickjillings@1318
|
66 // Apply the absolute gate
|
nickjillings@1318
|
67 var loudness = calculateLoudnessFromChannelBlocks(blockEnergy);
|
nickjillings@1318
|
68 var absgatedEnergy = new Array(blockEnergy.length);
|
nickjillings@1318
|
69 for (var c=0; c<blockEnergy.length; c++)
|
nickjillings@1318
|
70 {
|
nickjillings@1318
|
71 absgatedEnergy[c] = [];
|
nickjillings@1318
|
72 }
|
nickjillings@1318
|
73 for (var i=0; i<loudness.length; i++)
|
nickjillings@1318
|
74 {
|
nickjillings@1318
|
75 if (loudness[i] >= -70)
|
nickjillings@1318
|
76 {
|
nickjillings@1318
|
77 for (var c=0; c<blockEnergy.length; c++)
|
nickjillings@1318
|
78 {
|
nickjillings@1318
|
79 absgatedEnergy[c].push(blockEnergy[c][i]);
|
nickjillings@1318
|
80 }
|
nickjillings@1318
|
81 }
|
nickjillings@1318
|
82 }
|
nickjillings@1318
|
83 var overallAbsLoudness = calculateOverallLoudnessFromChannelBlocks(absgatedEnergy);
|
nickjillings@1318
|
84
|
nickjillings@1318
|
85 //applying the relative gate 8 dB down from overallAbsLoudness
|
nickjillings@1318
|
86 var relGateLevel = overallAbsLoudness - 8;
|
nickjillings@1318
|
87 var relgateEnergy = new Array(blockEnergy.length);
|
nickjillings@1318
|
88 for (var c=0; c<blockEnergy.length; c++)
|
nickjillings@1318
|
89 {
|
nickjillings@1318
|
90 relgateEnergy[c] = [];
|
nickjillings@1318
|
91 }
|
nickjillings@1318
|
92 for (var i=0; i<loudness.length; i++)
|
nickjillings@1318
|
93 {
|
nickjillings@1318
|
94 if (loudness[i] >= relGateLevel)
|
nickjillings@1318
|
95 {
|
nickjillings@1318
|
96 for (var c=0; c<blockEnergy.length; c++)
|
nickjillings@1318
|
97 {
|
nickjillings@1318
|
98 relgateEnergy[c].push(blockEnergy[c][i]);
|
nickjillings@1318
|
99 }
|
nickjillings@1318
|
100 }
|
nickjillings@1318
|
101 }
|
nickjillings@1318
|
102 var overallRelLoudness = calculateOverallLoudnessFromChannelBlocks(relgateEnergy);
|
nickjillings@1318
|
103 buffer.lufs = overallRelLoudness;
|
nickjillings@1318
|
104 }
|
nickjillings@1318
|
105 };
|
nickjillings@1318
|
106 offlineContext.startRendering();
|
nickjillings@1318
|
107 }
|
nickjillings@1318
|
108
|
nickjillings@1318
|
109 function calculateProcessedLoudness(buffer, winDur, overlap)
|
nickjillings@1318
|
110 {
|
nickjillings@1318
|
111 // Buffer Web Audio buffer node
|
nickjillings@1318
|
112 // winDur Window Duration in milliseconds
|
nickjillings@1318
|
113 // overlap Window overlap as normalised (0.5 = 50% overlap);
|
nickjillings@1318
|
114 if (buffer == undefined)
|
nickjillings@1318
|
115 {
|
nickjillings@1318
|
116 return 0;
|
nickjillings@1318
|
117 }
|
nickjillings@1318
|
118 if (winDur == undefined)
|
nickjillings@1318
|
119 {
|
nickjillings@1318
|
120 winDur = 400;
|
nickjillings@1318
|
121 }
|
nickjillings@1318
|
122 if (overlap == undefined)
|
nickjillings@1318
|
123 {
|
nickjillings@1318
|
124 overlap = 0.5;
|
nickjillings@1318
|
125 }
|
nickjillings@1318
|
126 var winSize = buffer.sampleRate*winDur/1000;
|
nickjillings@1318
|
127 var olapSize = (1-overlap)*winSize;
|
nickjillings@1318
|
128 var numberOfFrames = Math.floor(buffer.length/olapSize - winSize/olapSize + 1);
|
nickjillings@1318
|
129 var blockEnergy = new Array(buffer.numberOfChannels);
|
nickjillings@1318
|
130 for (var channel = 0; channel < buffer.numberOfChannels; channel++)
|
nickjillings@1318
|
131 {
|
nickjillings@1318
|
132 blockEnergy[channel] = new Float32Array(numberOfFrames);
|
nickjillings@1318
|
133 var data = buffer.getChannelData(channel);
|
nickjillings@1318
|
134 for (var i=0; i<numberOfFrames; i++)
|
nickjillings@1318
|
135 {
|
nickjillings@1318
|
136 var sigma = 0;
|
nickjillings@1318
|
137 for (var n=i*olapSize; n < i*olapSize+winSize; n++)
|
nickjillings@1318
|
138 {
|
nickjillings@1318
|
139 sigma += Math.pow(data[n],2);
|
nickjillings@1318
|
140 }
|
nickjillings@1318
|
141 blockEnergy[channel][i] = sigma/winSize;
|
nickjillings@1318
|
142 }
|
nickjillings@1318
|
143 }
|
nickjillings@1318
|
144 return blockEnergy;
|
nickjillings@1318
|
145 }
|
nickjillings@1318
|
146 function calculateLoudnessFromChannelBlocks(blockEnergy)
|
nickjillings@1318
|
147 {
|
nickjillings@1318
|
148 // Loudness
|
nickjillings@1318
|
149 var loudness = new Float32Array(blockEnergy[0].length);
|
nickjillings@1318
|
150 for (var i=0; i<blockEnergy[0].length; i++)
|
nickjillings@1318
|
151 {
|
nickjillings@1318
|
152 var sigma = 0;
|
nickjillings@1318
|
153 for (var channel = 0; channel < blockEnergy.length; channel++)
|
nickjillings@1318
|
154 {
|
nickjillings@1318
|
155 var G = 1.0;
|
nickjillings@1318
|
156 if (channel >= 4) {G = 1.41;}
|
nickjillings@1318
|
157 sigma += blockEnergy[channel][i]*G;
|
nickjillings@1318
|
158 }
|
nickjillings@1318
|
159 loudness[i] = -0.691 + 10*Math.log10(sigma);
|
nickjillings@1318
|
160 }
|
nickjillings@1318
|
161 return loudness;
|
nickjillings@1318
|
162 }
|
nickjillings@1318
|
163 function calculateOverallLoudnessFromChannelBlocks(blockEnergy)
|
nickjillings@1318
|
164 {
|
nickjillings@1318
|
165 // Loudness
|
nickjillings@1318
|
166 var summation = 0;
|
nickjillings@1318
|
167 for (var channel = 0; channel < blockEnergy.length; channel++)
|
nickjillings@1318
|
168 {
|
nickjillings@1318
|
169 var G = 1.0;
|
nickjillings@1318
|
170 if (channel >= 4) {G = 1.41;}
|
nickjillings@1318
|
171 var sigma = 0;
|
nickjillings@1318
|
172 for (var i=0; i<blockEnergy[0].length; i++)
|
nickjillings@1318
|
173 {
|
nickjillings@1318
|
174 blockEnergy[channel][i] *= G;
|
nickjillings@1318
|
175 sigma += blockEnergy[channel][i];
|
nickjillings@1318
|
176 }
|
nickjillings@1318
|
177 sigma /= blockEnergy.length;
|
nickjillings@1318
|
178 summation+= sigma;
|
nickjillings@1318
|
179 }
|
nickjillings@1318
|
180 return -0.691 + 10*Math.log10(summation);;
|
nickjillings@1318
|
181 }
|