annotate loudness.js @ 793:261d92ea87e1

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