-
Notifications
You must be signed in to change notification settings - Fork 0
/
Analyser.js
240 lines (211 loc) · 7.23 KB
/
Analyser.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
import Tone from "tone"
import LUFSWorker from "../workers/lufs.worker"
let FFT_SIZE = 2048
let counter = 0
let text
export default {
props: ["audioURL", "sourceNode"],
data() {
return {
runAnalysis: false,
initiated: false,
audioCTX: null,
}
},
methods: {
toggleAnalysis() {
if (!this.runAnalysis) {
this.startAnalysis()
} else {
this.stopAnalysis()
}
},
initContext() {
// Fetch context with Tone
this.audioCTX = Tone.context._context
},
stopAnalysis() {
this.runAnalysis = false
counter = 0
},
startAnalysis() {
this.runAnalysis = true
if (!this.audioURL) {
alert("You need to upload an audio file first!")
return
}
if (!this.initiated) {
this.initContext()
}
// FFT Container Values
const FFTContainer = document.getElementById("FFT")
FFTContainer.width = 1620
FFTContainer.height = 500
FFTContainer.style.width = "810px" // for better quality rendering
FFTContainer.style.height = "250px"
const xAxisLogBins = []
const FFTCtx = FFTContainer.getContext("2d")
const FFTCtxHeight = FFTCtx.canvas.height
const FFTCtxWidth = FFTCtx.canvas.width
const xTicks = [30, 50, 100, 200, 1000, 2000, 4000, 10000]
// Canvas Values
// [px] - padding for the xAxis ticks
const FFTpaddingBottom = 50
// scale up values for quicker movement
const FFTscaler = 4
FFTCtx.font = "30px Avenir"
FFTCtx.fillStyle = "black"
FFTCtx.textAlign = "center"
FFTCtx.strokeStyle = "black"
// Linear Scale X-axis array
const xArray = Array.from({length: FFT_SIZE}, (v, i) => i)
// Log positions of X-Ticks
for (let bin = 0; bin < FFT_SIZE; bin++) {
xAxisLogBins.push(
getXAxisLogScale(FFT_SIZE, FFTCtxWidth, xArray[bin], null)
)
}
// Log Scale X-Axis(Frequency)
for (let freq = 0; freq < xTicks.length; freq++) {
FFTCtx.fillText(
String(xTicks[freq]),
getXAxisLogScale(FFT_SIZE, FFTCtxWidth, null, xTicks[freq]),
FFTCtxHeight
)
}
/* Init LUFS
- Loudness measurement according to the EBU R 128 standard
- Algorithm according to ITU BS.1770 standard
- Recommendation: https://tech.ebu.ch/docs/r/r128.pdf
- Algorithm: https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.1770-4-201510-I!!PDF-E.pdf
- More info: https://en.wikipedia.org/wiki/LKFS
*/
// create the two stage filter process and init meter to fetch values
const waveFormNode = new Tone.Waveform(FFT_SIZE)
const meter = new Tone.Waveform(FFT_SIZE)
const highShelfBvalues = [
1.53512485958697,
-2.69169618940638,
1.19839281085285,
]
const highShelfAValues = [1, -1.69065929318241, 0.73248077421585]
const highShelfFilter = this.audioCTX.createIIRFilter(
highShelfBvalues,
highShelfAValues
)
const highPassBvalues = [1.0, -2.0, 1.0]
const highPassAValues = [1.0, -1.99004745483398, 0.99007225036621]
const highPassFilter = this.audioCTX.createIIRFilter(
highPassBvalues,
highPassAValues
)
// Create FFT
const FFT = new Tone.FFT(FFT_SIZE)
// Connect the graph
Tone.connect(this.sourceNode, FFT)
Tone.connect(FFT, waveFormNode)
Tone.connect(waveFormNode, highShelfFilter)
Tone.connect(highShelfFilter, highPassFilter)
Tone.connect(highPassFilter, meter)
// Define LUFS worker
const lufsWorker = new LUFSWorker()
// Schedule Loop
const scheduleAnalyser = () => {
if (!this.runAnalysis) {
return
}
requestAnimationFrame(scheduleAnalyser)
// Clear the canvas
FFTCtx.clearRect(0, 0, FFTCtxWidth, FFTCtxHeight - FFTpaddingBottom)
// Fetch and draw FFT
const frequencyBinsValue = FFT.getValue()
frequencyBinsValue.forEach((bin, i) => {
FFTCtx.beginPath()
FFTCtx.moveTo(xAxisLogBins[i], FFTCtxHeight - FFTpaddingBottom)
FFTCtx.lineTo(
xAxisLogBins[i],
Math.min(0 - FFTscaler * bin, FFTCtxHeight - FFTpaddingBottom)
)
FFTCtx.stroke()
})
// RMS
const waveFormValue = waveFormNode.getValue()
// Compute average power over the interval
let sumOfSquares = 0
for (let i = 0; i < waveFormValue.length; i++) {
sumOfSquares += waveFormValue[i] ** 2
}
// Normalised RMS - this value is normalised to a 997 Hz Sine Wave at 0dbFS
// More info: https://en.wikipedia.org/wiki/DBFS
const normalisationFactor = 3.01
const RMSPowerDecibels =
20 * Math.log10(Math.sqrt(sumOfSquares / waveFormValue.length)) +
normalisationFactor
displayNumber("RMS", RMSPowerDecibels, counter)
// LUFS
const filteredSignal = meter.getValue()
// Init the worker
if (counter === 0) {
lufsWorker.postMessage({
messageType: "init",
FFT_SIZE: FFT_SIZE,
counter: counter,
FS: this.audioCTX.sampleRate,
})
}
// Fill up the analysis buffer
if (counter !== 0) {
lufsWorker.postMessage({
messageType: "fillBuffer",
filteredSignal: filteredSignal,
counter: counter,
})
}
// Receive back calculation from worker
lufsWorker.onmessage = (e) => {
if (e.data.returnMessageType === "integratedCalculated") {
const integratedLoudness = e.data.integratedLoudness
displayNumber("LUFSIntegrated", integratedLoudness)
}
if (e.data.returnMessageType === "shortTermCalculated") {
const shortTermLoudness = e.data.shortTermLoudness
displayNumber("LUFSShort", shortTermLoudness, counter)
}
}
counter++
}
scheduleAnalyser()
},
},
}
// Convert linear scale to log scale for the canvas
function getXAxisLogScale(FFT_SIZE, canvasWidth, bin, freq) {
const minLog = Math.log10(20)
const maxLog = Math.log10(22050)
// Current frequency corresponding to bin, or passing freq for ticks
if (!freq) {
freq = (bin * 22050) / FFT_SIZE
}
// Bandwidth of the canvas for the frequency range
const bandWidth = canvasWidth / (maxLog - minLog)
// Rounding to avoid decimal pixel values
return Math.round(bandWidth * (Math.log10(freq) - minLog))
}
function displayNumber(id, value, counter) {
const meter = document.getElementById(id + "-level")
if (counter) {
// Slow down the display of numerical values:
// Set a threshold for refreshing the display of the numerical
// value based on the counter (counting per refresh rate) and
// the arbitrary value of 50 defined below
if (counter % 50 === 0) {
text = document.getElementById(id + "-level-text")
text.textContent = value.toFixed(2)
}
meter.value = isFinite(value) ? value : meter.min
} else {
text = document.getElementById(id + "-level-text")
text.textContent = value.toFixed(2)
meter.value = isFinite(value) ? value : meter.min
}
}