annotate loudness.js @ 471:3a9b869ba7f8 Dev_main

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