hypnosound 1.7.2 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +4 -3
- package/index.html +70 -19
- package/index.js +15 -29
- package/package.json +1 -1
- package/src/audio/bass.js +5 -3
- package/src/audio/energy.js +10 -4
- package/src/audio/pitchClass.js +6 -6
- package/src/audio/spectralKurtosis.js +7 -3
- package/src/audio/spectralSkew.js +8 -1
- package/src/utils/calculateStats.js +157 -151
- package/window-processor.js +31 -0
package/.eslintrc
CHANGED
package/index.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Audio Capture and Analysis</title>
|
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
7
8
|
</head>
|
|
8
9
|
|
|
9
10
|
<body>
|
|
@@ -17,42 +18,92 @@
|
|
|
17
18
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
18
19
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
|
|
19
20
|
const source = audioContext.createMediaStreamSource(stream)
|
|
21
|
+
|
|
20
22
|
const analyser = audioContext.createAnalyser()
|
|
23
|
+
analyser.fftSize = 32768
|
|
24
|
+
analyser.smoothingTimeConstant = 0.5
|
|
25
|
+
|
|
26
|
+
await audioContext.audioWorklet.addModule('window-processor.js') // Path to your processor file
|
|
27
|
+
const windowNode = new AudioWorkletNode(audioContext, 'window-processor')
|
|
21
28
|
|
|
22
|
-
source.connect(
|
|
23
|
-
analyser
|
|
24
|
-
analyser.smoothingTimeConstant = 0
|
|
29
|
+
source.connect(windowNode)
|
|
30
|
+
windowNode.connect(analyser)
|
|
25
31
|
const bufferLength = analyser.frequencyBinCount
|
|
26
32
|
const dataArray = new Uint8Array(bufferLength)
|
|
27
33
|
|
|
28
|
-
const
|
|
29
|
-
requestAnimationFrame(
|
|
34
|
+
const update = () => {
|
|
35
|
+
requestAnimationFrame(update)
|
|
30
36
|
|
|
31
37
|
analyser.getByteFrequencyData(dataArray)
|
|
32
|
-
|
|
33
38
|
// This is where the magic happens, but be careful what you log...
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
window.audioSignals = {
|
|
40
|
+
energy: a.energy(dataArray),
|
|
41
|
+
spectralCentroid: a.spectralCentroid(dataArray),
|
|
42
|
+
spectralCrest: a.spectralCrest(dataArray),
|
|
43
|
+
spectralEntropy: a.spectralEntropy(dataArray),
|
|
44
|
+
spectralFlux: a.spectralFlux(dataArray),
|
|
45
|
+
spectralKurtosis: a.spectralKurtosis(dataArray),
|
|
46
|
+
spectralRolloff: a.spectralRolloff(dataArray),
|
|
47
|
+
spectralRoughness: a.spectralRoughness(dataArray),
|
|
48
|
+
spectralSkew: a.spectralSkew(dataArray),
|
|
49
|
+
spectralSpread: a.spectralSpread(dataArray),
|
|
45
50
|
bass: a.bass(dataArray),
|
|
46
51
|
mids: a.mids(dataArray),
|
|
47
52
|
treble: a.treble(dataArray),
|
|
48
|
-
|
|
53
|
+
pitchClass: a.pitchClass(dataArray),
|
|
54
|
+
}
|
|
55
|
+
window.fft = dataArray
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
|
|
58
|
+
update()
|
|
52
59
|
} catch (error) {
|
|
53
60
|
console.error('Something went wrong:', error)
|
|
54
61
|
}
|
|
55
62
|
})
|
|
56
63
|
</script>
|
|
64
|
+
<canvas id="myBarChart" width="800" height="400"></canvas>
|
|
65
|
+
<canvas id="fftBarChart" width="800" height="400"></canvas>
|
|
66
|
+
<script>
|
|
67
|
+
var ctx = document.getElementById('myBarChart').getContext('2d')
|
|
68
|
+
var myBarChart = new Chart(ctx, {
|
|
69
|
+
type: 'bar',
|
|
70
|
+
data: {
|
|
71
|
+
labels: [], // Initial empty labels
|
|
72
|
+
datasets: [
|
|
73
|
+
{
|
|
74
|
+
label: 'Audio Data',
|
|
75
|
+
data: [], // Initial empty data
|
|
76
|
+
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
|
77
|
+
borderColor: 'rgba(54, 162, 235, 1)',
|
|
78
|
+
borderWidth: 1,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
options: {
|
|
83
|
+
scales: {
|
|
84
|
+
y: {
|
|
85
|
+
beginAtZero: true,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
responsive: true,
|
|
89
|
+
animation: {
|
|
90
|
+
duration: 0, // Turn off animation for instant updates
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Function to update the chart with a new data object
|
|
96
|
+
function updateChartData(newData) {
|
|
97
|
+
myBarChart.data.labels = Object.keys(newData)
|
|
98
|
+
myBarChart.data.datasets[0].data = Object.values(newData).map(({ value }) => value || 0)
|
|
99
|
+
myBarChart.update()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function render() {
|
|
103
|
+
requestAnimationFrame(render)
|
|
104
|
+
updateChartData(window.audioSignals || {})
|
|
105
|
+
}
|
|
106
|
+
render()
|
|
107
|
+
</script>
|
|
57
108
|
</body>
|
|
58
109
|
</html>
|
package/index.js
CHANGED
|
@@ -51,96 +51,82 @@ class AudioProcessor {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
energy = (fft) => {
|
|
54
|
-
const
|
|
55
|
-
const value = energy(windowedFft)
|
|
54
|
+
const value = energy(fft)
|
|
56
55
|
const stats = this.statCalculators.energy(value)
|
|
57
56
|
return { value, stats }
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
spectralCentroid = (fft) => {
|
|
61
|
-
const
|
|
62
|
-
const value = spectralCentroid(applyKaiserWindow(windowedFft))
|
|
60
|
+
const value = spectralCentroid(applyKaiserWindow(fft))
|
|
63
61
|
const stats = this.statCalculators.spectralCentroid(value)
|
|
64
62
|
return { value, stats }
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
spectralCrest = (fft) => {
|
|
68
|
-
const
|
|
69
|
-
const value = spectralCrest(windowedFft)
|
|
66
|
+
const value = spectralCrest(fft)
|
|
70
67
|
const stats = this.statCalculators.spectralCentroid(value)
|
|
71
68
|
return { value, stats }
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
spectralEntropy = (fft) => {
|
|
75
|
-
const
|
|
76
|
-
const value = spectralEntropy(windowedFft)
|
|
72
|
+
const value = spectralEntropy(fft)
|
|
77
73
|
const stats = this.statCalculators.spectralEntropy(value)
|
|
78
74
|
return { value, stats }
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
spectralFlux = (fft) => {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
this.previousValue.spectralFlux = new Uint8Array(windowedFft)
|
|
78
|
+
const value = spectralFlux(fft, this.previousValue.spectralFlux)
|
|
79
|
+
this.previousValue.spectralFlux = new Uint8Array(fft)
|
|
85
80
|
const stats = this.statCalculators.spectralFlux(value)
|
|
86
81
|
return { value, stats }
|
|
87
82
|
}
|
|
88
83
|
spectralKurtosis = (fft) => {
|
|
89
|
-
const
|
|
90
|
-
const value = spectralKurtosis(windowedFft)
|
|
84
|
+
const value = spectralKurtosis(fft)
|
|
91
85
|
const stats = this.statCalculators.spectralKurtosis(value)
|
|
92
86
|
return { value, stats }
|
|
93
87
|
}
|
|
94
88
|
|
|
95
89
|
spectralRolloff = (fft) => {
|
|
96
|
-
const
|
|
97
|
-
const value = spectralRolloff(windowedFft)
|
|
90
|
+
const value = spectralRolloff(fft)
|
|
98
91
|
const stats = this.statCalculators.spectralRolloff(value)
|
|
99
92
|
return { value, stats }
|
|
100
93
|
}
|
|
101
94
|
|
|
102
95
|
spectralRoughness = (fft) => {
|
|
103
|
-
const
|
|
104
|
-
const value = spectralRoughness(windowedFft)
|
|
96
|
+
const value = spectralRoughness(fft)
|
|
105
97
|
const stats = this.statCalculators.spectralRoughness(value)
|
|
106
98
|
return { value, stats }
|
|
107
99
|
}
|
|
108
100
|
|
|
109
101
|
spectralSkew = (fft) => {
|
|
110
|
-
const
|
|
111
|
-
const value = spectralSkew(windowedFft)
|
|
102
|
+
const value = spectralSkew(fft)
|
|
112
103
|
const stats = this.statCalculators.spectralSkew(value)
|
|
113
104
|
return { value, stats }
|
|
114
105
|
}
|
|
115
106
|
|
|
116
107
|
spectralSpread = (fft) => {
|
|
117
|
-
const
|
|
118
|
-
const value = spectralSpread(windowedFft)
|
|
108
|
+
const value = spectralSpread(fft)
|
|
119
109
|
const stats = this.statCalculators.spectralSpread(value)
|
|
120
110
|
return { value, stats }
|
|
121
111
|
}
|
|
122
112
|
|
|
123
113
|
pitchClass = (fft) => {
|
|
124
|
-
const
|
|
125
|
-
const value = pitchClass(windowedFft)
|
|
114
|
+
const value = pitchClass(fft)
|
|
126
115
|
const stats = this.statCalculators.pitchClass(value)
|
|
127
116
|
return { value, stats }
|
|
128
117
|
}
|
|
129
118
|
bass = (fft) => {
|
|
130
|
-
const
|
|
131
|
-
const value = bass(windowedFft)
|
|
119
|
+
const value = bass(fft)
|
|
132
120
|
const stats = this.statCalculators.bass(value)
|
|
133
121
|
return { value, stats }
|
|
134
122
|
}
|
|
135
123
|
treble = (fft) => {
|
|
136
|
-
const
|
|
137
|
-
const value = treble(windowedFft)
|
|
124
|
+
const value = treble(fft)
|
|
138
125
|
const stats = this.statCalculators.treble(value)
|
|
139
126
|
return { value, stats }
|
|
140
127
|
}
|
|
141
128
|
mids = (fft) => {
|
|
142
|
-
const
|
|
143
|
-
const value = mids(windowedFft)
|
|
129
|
+
const value = mids(fft)
|
|
144
130
|
const stats = this.statCalculators.mids(value)
|
|
145
131
|
return { value, stats }
|
|
146
132
|
}
|
package/package.json
CHANGED
package/src/audio/bass.js
CHANGED
|
@@ -15,7 +15,8 @@ function calculateBassPower(fft, sampleRate, totalSamples) {
|
|
|
15
15
|
|
|
16
16
|
for (let i = 0; i < fft.length; i++) {
|
|
17
17
|
let frequency = i * frequencyResolution
|
|
18
|
-
|
|
18
|
+
// Normalize each FFT value from 0 to 1 (assuming Uint8Array values 0-255)
|
|
19
|
+
let magnitude = fft[i] / 255
|
|
19
20
|
let power = magnitude * magnitude
|
|
20
21
|
|
|
21
22
|
// Accumulate max energy for normalization
|
|
@@ -26,7 +27,8 @@ function calculateBassPower(fft, sampleRate, totalSamples) {
|
|
|
26
27
|
bassEnergy += power
|
|
27
28
|
}
|
|
28
29
|
}
|
|
30
|
+
|
|
29
31
|
// Normalize bass energy from 0 to 1
|
|
30
|
-
let normalizedBassPower = bassEnergy / maxEnergy
|
|
31
|
-
return isNaN(normalizedBassPower) ? 0 : normalizedBassPower
|
|
32
|
+
let normalizedBassPower = maxEnergy > 0 ? bassEnergy / maxEnergy : 0
|
|
33
|
+
return isNaN(normalizedBassPower) ? 0 : normalizedBassPower
|
|
32
34
|
}
|
package/src/audio/energy.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
export default function energy(fft) {
|
|
2
|
-
return calculateFFTEnergy(fft)
|
|
2
|
+
return calculateFFTEnergy(fft) / 1000
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
function calculateFFTEnergy(currentSignal) {
|
|
6
6
|
let energy = 0
|
|
7
|
+
const maxPossibleValue = 1 // This should be 1 if your data is normalized between 0 and 1
|
|
8
|
+
const maxPossibleEnergy = currentSignal.length // Total samples if each sample was at maximum value
|
|
9
|
+
|
|
7
10
|
for (let i = 0; i < currentSignal.length; i++) {
|
|
8
|
-
let normalizedValue = currentSignal[i] /
|
|
9
|
-
energy += normalizedValue * normalizedValue
|
|
11
|
+
let normalizedValue = currentSignal[i] / maxPossibleValue // Normalize each FFT value
|
|
12
|
+
energy += normalizedValue * normalizedValue // Sum the squares of the normalized values
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
// Normalize the computed energy by the number of samples since each sample's max energy would be 1 if maxPossibleValue is 1
|
|
16
|
+
energy = energy / currentSignal.length
|
|
17
|
+
|
|
18
|
+
return energy
|
|
13
19
|
}
|
package/src/audio/pitchClass.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export default function pitchClass(fft) {
|
|
1
|
+
export default function pitchClass(fft, sampleRate = 44100) {
|
|
2
2
|
// Constants for the FFT processing
|
|
3
|
-
const
|
|
4
|
-
const fftSize = fft.length // This is an example, adjust based on your FFT setup
|
|
3
|
+
const fftSize = fft.length
|
|
5
4
|
const freqResolution = sampleRate / fftSize
|
|
6
5
|
|
|
7
6
|
// Finding the dominant frequency in the FFT data
|
|
8
7
|
let maxIndex = 0
|
|
9
8
|
let maxValue = 0
|
|
10
|
-
for (let i =
|
|
9
|
+
for (let i = 1; i < fft.length; i++) {
|
|
10
|
+
// start from 1 to skip DC offset
|
|
11
11
|
if (fft[i] > maxValue) {
|
|
12
12
|
maxValue = fft[i]
|
|
13
13
|
maxIndex = i
|
|
@@ -15,9 +15,9 @@ export default function pitchClass(fft) {
|
|
|
15
15
|
}
|
|
16
16
|
const dominantFreq = maxIndex * freqResolution
|
|
17
17
|
|
|
18
|
-
// Convert to MIDI note then to pitchClass
|
|
18
|
+
// Convert to MIDI note then to pitchClass
|
|
19
19
|
const midiNote = 69 + 12 * Math.log2(dominantFreq / 440)
|
|
20
|
-
const pitchClass = midiNote % 12
|
|
20
|
+
const pitchClass = Math.round(midiNote) % 12 // round to reduce minor fluctuation effects
|
|
21
21
|
|
|
22
22
|
// Normalize to a 0-1 range
|
|
23
23
|
const normalizedpitchClass = pitchClass / 12
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import mu from '../utils/mu.js'
|
|
2
|
+
|
|
2
3
|
export default function spectralKurtosis(fft) {
|
|
3
|
-
const
|
|
4
|
-
|
|
4
|
+
const computed = calculateSpectralKurtosis(fft) // Process FFT data
|
|
5
|
+
// Normalize using a logistic function
|
|
6
|
+
const k = 0.05 // Adjust k based on the expected range of kurtosis values
|
|
7
|
+
const value = 1 / (1 + Math.exp(-k * computed))
|
|
8
|
+
return value
|
|
5
9
|
}
|
|
6
10
|
|
|
7
11
|
function calculateSpectralKurtosis(fftData) {
|
|
@@ -15,7 +19,7 @@ function calculateSpectralKurtosis(fftData) {
|
|
|
15
19
|
}
|
|
16
20
|
fourthMoment /= fftData.length
|
|
17
21
|
|
|
18
|
-
// Add a small epsilon to the denominator to prevent
|
|
22
|
+
// Add a small epsilon to the denominator to prevent division by a very small number
|
|
19
23
|
const epsilon = 1e-7 // Adjust epsilon based on the scale of your data
|
|
20
24
|
const kurtosis = variance ? fourthMoment / Math.pow(variance + epsilon, 2) - 3 : 0
|
|
21
25
|
return kurtosis
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
export default function spectralSkew(fft) {
|
|
2
2
|
const computed = calculateSpectralSkewness(fft) || 0 // Process FFT data
|
|
3
|
-
|
|
3
|
+
// Adjust the steepness factor (k) based on expected skewness range
|
|
4
|
+
const k = 0.05 // Decrease this value if skewness values are large
|
|
5
|
+
// Apply a logistic function to map the result to (0, 1)
|
|
6
|
+
const value = 1 / (1 + Math.exp(-k * computed))
|
|
4
7
|
return value
|
|
5
8
|
}
|
|
6
9
|
|
|
7
10
|
function calculateSpectralSkewness(fftData) {
|
|
11
|
+
if (fftData.length === 0) {
|
|
12
|
+
return 0 // Guard against division by zero if fftData is empty
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
const mean = fftData.reduce((sum, val) => sum + val, 0) / fftData.length
|
|
9
16
|
const variance = fftData.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / fftData.length
|
|
10
17
|
const standardDeviation = Math.sqrt(variance)
|
|
@@ -1,200 +1,206 @@
|
|
|
1
1
|
export const StatTypes = ['normalized', 'mean', 'median', 'standardDeviation', 'zScore', 'min', 'max']
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
let upperHalf = [] // Min heap
|
|
11
|
-
|
|
12
|
-
function updateMinMaxQueues(value) {
|
|
13
|
-
while (minQueue.length && minQueue[minQueue.length - 1] > value) {
|
|
14
|
-
minQueue.pop()
|
|
15
|
-
}
|
|
16
|
-
while (maxQueue.length && maxQueue[maxQueue.length - 1] < value) {
|
|
17
|
-
maxQueue.pop()
|
|
18
|
-
}
|
|
19
|
-
minQueue.push(value)
|
|
20
|
-
maxQueue.push(value)
|
|
21
|
-
}
|
|
3
|
+
const erf = (x) => {
|
|
4
|
+
const a1 = 0.254829592
|
|
5
|
+
const a2 = -0.284496736
|
|
6
|
+
const a3 = 1.421413741
|
|
7
|
+
const a4 = -1.453152027
|
|
8
|
+
const a5 = 1.061405429
|
|
9
|
+
const p = 0.3275911
|
|
22
10
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
minQueue.shift()
|
|
26
|
-
}
|
|
27
|
-
if (maxQueue[0] === oldValue) {
|
|
28
|
-
maxQueue.shift()
|
|
29
|
-
}
|
|
30
|
-
}
|
|
11
|
+
const sign = x < 0 ? -1 : 1
|
|
12
|
+
x = Math.abs(x)
|
|
31
13
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
lowerHalf.push(number)
|
|
35
|
-
bubbleUp(lowerHalf, false)
|
|
36
|
-
} else {
|
|
37
|
-
upperHalf.push(number)
|
|
38
|
-
bubbleUp(upperHalf, true)
|
|
39
|
-
}
|
|
14
|
+
const t = 1.0 / (1.0 + p * x)
|
|
15
|
+
const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x)
|
|
40
16
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
upperHalf.push(extractTop(lowerHalf, false))
|
|
44
|
-
bubbleUp(upperHalf, true)
|
|
45
|
-
} else if (upperHalf.length > lowerHalf.length) {
|
|
46
|
-
lowerHalf.push(extractTop(upperHalf, true))
|
|
47
|
-
bubbleUp(lowerHalf, false)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
17
|
+
return sign * y
|
|
18
|
+
}
|
|
50
19
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
20
|
+
class MonotonicQueue {
|
|
21
|
+
constructor(isMin = true) {
|
|
22
|
+
this.deque = []
|
|
23
|
+
this.compare = isMin ? (a, b) => a > b : (a, b) => a < b
|
|
24
|
+
}
|
|
57
25
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
bubbleUp(upperHalf, true)
|
|
62
|
-
} else if (upperHalf.length > lowerHalf.length) {
|
|
63
|
-
lowerHalf.push(extractTop(upperHalf, true))
|
|
64
|
-
bubbleUp(lowerHalf, false)
|
|
26
|
+
push(value) {
|
|
27
|
+
while (this.deque.length && this.compare(this.deque[this.deque.length - 1], value)) {
|
|
28
|
+
this.deque.pop()
|
|
65
29
|
}
|
|
30
|
+
this.deque.push(value)
|
|
66
31
|
}
|
|
67
32
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
let parentIdx = Math.floor((index - 1) / 2)
|
|
72
|
-
if ((isMinHeap && heap[index] < heap[parentIdx]) || (!isMinHeap && heap[index] > heap[parentIdx])) {
|
|
73
|
-
;[heap[index], heap[parentIdx]] = [heap[parentIdx], heap[index]]
|
|
74
|
-
index = parentIdx
|
|
75
|
-
} else {
|
|
76
|
-
break
|
|
77
|
-
}
|
|
78
|
-
}
|
|
33
|
+
remove(value) {
|
|
34
|
+
if (!this.deque.length || this.deque[0] !== value) return
|
|
35
|
+
this.deque.shift()
|
|
79
36
|
}
|
|
80
37
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
38
|
+
peek = () => this.deque[0]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const bubbleUp = (heap, isMinHeap) => {
|
|
42
|
+
let index = heap.length - 1
|
|
43
|
+
const value = heap[index]
|
|
44
|
+
|
|
45
|
+
while (index > 0) {
|
|
46
|
+
const parentIdx = Math.floor((index - 1) / 2)
|
|
47
|
+
const shouldSwap = isMinHeap ? heap[index] < heap[parentIdx] : heap[index] > heap[parentIdx]
|
|
48
|
+
|
|
49
|
+
if (!shouldSwap) break
|
|
50
|
+
|
|
51
|
+
heap[index] = heap[parentIdx]
|
|
52
|
+
index = parentIdx
|
|
90
53
|
}
|
|
91
54
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
let length = heap.length
|
|
55
|
+
heap[index] = value
|
|
56
|
+
}
|
|
95
57
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
let rightChildIndex = 2 * index + 2
|
|
99
|
-
let swapIndex = null
|
|
58
|
+
const getBestChildIndex = (heap, leftChildIndex, rightChildIndex, isMinHeap) => {
|
|
59
|
+
if (rightChildIndex >= heap.length) return leftChildIndex
|
|
100
60
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
}
|
|
61
|
+
const comparator = isMinHeap ? Math.min : Math.max
|
|
62
|
+
return comparator(heap[leftChildIndex], heap[rightChildIndex]) === heap[leftChildIndex] ? leftChildIndex : rightChildIndex
|
|
63
|
+
}
|
|
106
64
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
) {
|
|
112
|
-
swapIndex = rightChildIndex
|
|
113
|
-
}
|
|
114
|
-
}
|
|
65
|
+
const sinkDown = (heap, isMinHeap) => {
|
|
66
|
+
let index = 0
|
|
67
|
+
const value = heap[0]
|
|
68
|
+
const length = heap.length
|
|
115
69
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
70
|
+
while (true) {
|
|
71
|
+
const leftChildIndex = 2 * index + 1
|
|
72
|
+
if (leftChildIndex >= length) break
|
|
119
73
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
74
|
+
const bestChildIndex = getBestChildIndex(heap, leftChildIndex, 2 * index + 2, isMinHeap)
|
|
75
|
+
const shouldSwap = isMinHeap ? heap[bestChildIndex] < value : heap[bestChildIndex] > value
|
|
76
|
+
|
|
77
|
+
if (!shouldSwap) break
|
|
78
|
+
|
|
79
|
+
heap[index] = heap[bestChildIndex]
|
|
80
|
+
index = bestChildIndex
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
heap[index] = value
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const makeCalculateStats = (historySize = 500) => {
|
|
87
|
+
const queue = []
|
|
88
|
+
const minQueue = new MonotonicQueue(true)
|
|
89
|
+
const maxQueue = new MonotonicQueue(false)
|
|
90
|
+
const lowerHalf = [] // Max heap
|
|
91
|
+
const upperHalf = [] // Min heap
|
|
92
|
+
|
|
93
|
+
let sum = 0
|
|
94
|
+
let sumOfSquares = 0
|
|
95
|
+
|
|
96
|
+
const getTargetHeap = (value) => {
|
|
97
|
+
if (lowerHalf.length === 0 || value < lowerHalf[0]) return { target: lowerHalf, isMin: false }
|
|
98
|
+
return { target: upperHalf, isMin: true }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const addToHeaps = (value) => {
|
|
102
|
+
const heap = getTargetHeap(value)
|
|
103
|
+
heap.target.push(value)
|
|
104
|
+
bubbleUp(heap.target, heap.isMin)
|
|
105
|
+
rebalanceHeaps()
|
|
123
106
|
}
|
|
124
107
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
sinkDown(
|
|
108
|
+
const removeNumberFromHeaps = (number) => {
|
|
109
|
+
if (lowerHalf.includes(number)) {
|
|
110
|
+
const index = lowerHalf.indexOf(number)
|
|
111
|
+
lowerHalf[index] = lowerHalf[lowerHalf.length - 1]
|
|
112
|
+
lowerHalf.pop()
|
|
113
|
+
sinkDown(lowerHalf, false)
|
|
114
|
+
} else if (upperHalf.includes(number)) {
|
|
115
|
+
const index = upperHalf.indexOf(number)
|
|
116
|
+
upperHalf[index] = upperHalf[upperHalf.length - 1]
|
|
117
|
+
upperHalf.pop()
|
|
118
|
+
sinkDown(upperHalf, true)
|
|
131
119
|
}
|
|
120
|
+
rebalanceHeaps()
|
|
132
121
|
}
|
|
133
122
|
|
|
134
|
-
|
|
135
|
-
if (lowerHalf.length
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
123
|
+
const rebalanceHeaps = () => {
|
|
124
|
+
if (lowerHalf.length <= upperHalf.length + 1 && upperHalf.length <= lowerHalf.length) return
|
|
125
|
+
|
|
126
|
+
if (lowerHalf.length > upperHalf.length + 1) {
|
|
127
|
+
const value = extractTop(lowerHalf)
|
|
128
|
+
upperHalf.push(value)
|
|
129
|
+
bubbleUp(upperHalf, true)
|
|
130
|
+
return
|
|
139
131
|
}
|
|
132
|
+
|
|
133
|
+
const value = extractTop(upperHalf)
|
|
134
|
+
lowerHalf.push(value)
|
|
135
|
+
bubbleUp(lowerHalf, false)
|
|
140
136
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// Save the sign of x
|
|
151
|
-
const sign = x < 0 ? -1 : 1
|
|
152
|
-
x = Math.abs(x)
|
|
153
|
-
|
|
154
|
-
// A&S formula 7.1.26
|
|
155
|
-
const t = 1.0 / (1.0 + p * x)
|
|
156
|
-
const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x)
|
|
157
|
-
|
|
158
|
-
return sign * y
|
|
137
|
+
|
|
138
|
+
const extractTop = (heap) => {
|
|
139
|
+
if (!heap.length) return null
|
|
140
|
+
const top = heap[0]
|
|
141
|
+
heap[0] = heap[heap.length - 1]
|
|
142
|
+
heap.pop()
|
|
143
|
+
sinkDown(heap, heap === upperHalf)
|
|
144
|
+
return top
|
|
159
145
|
}
|
|
160
146
|
|
|
161
|
-
|
|
162
|
-
|
|
147
|
+
const calculateMedian = () => {
|
|
148
|
+
if (!lowerHalf.length) return queue[0] || 0
|
|
149
|
+
if (lowerHalf.length === upperHalf.length) return (lowerHalf[0] + upperHalf[0]) / 2
|
|
150
|
+
return lowerHalf[0]
|
|
163
151
|
}
|
|
164
|
-
return function calculateStats(value) {
|
|
165
|
-
if (typeof value !== 'number') throw new Error('Input must be a number')
|
|
166
152
|
|
|
167
|
-
|
|
168
|
-
|
|
153
|
+
const calculate = (value) => {
|
|
154
|
+
if (typeof value !== 'number' || isNaN(value)) throw new Error('Input must be a valid number')
|
|
169
155
|
|
|
156
|
+
minQueue.push(value)
|
|
157
|
+
maxQueue.push(value)
|
|
158
|
+
addToHeaps(value)
|
|
170
159
|
queue.push(value)
|
|
160
|
+
|
|
171
161
|
sum += value
|
|
172
162
|
sumOfSquares += value * value
|
|
173
163
|
|
|
174
164
|
if (queue.length > historySize) {
|
|
175
|
-
|
|
165
|
+
const removed = queue.shift()
|
|
176
166
|
sum -= removed
|
|
177
167
|
sumOfSquares -= removed * removed
|
|
178
|
-
|
|
168
|
+
minQueue.remove(removed)
|
|
169
|
+
maxQueue.remove(removed)
|
|
179
170
|
removeNumberFromHeaps(removed)
|
|
180
171
|
}
|
|
181
172
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
173
|
+
const mean = sum / queue.length
|
|
174
|
+
const variance = Math.max(0, sumOfSquares / queue.length - mean * mean)
|
|
175
|
+
const min = minQueue.peek() || 0
|
|
176
|
+
const max = maxQueue.peek() || 0
|
|
177
|
+
|
|
178
|
+
if (max === min) {
|
|
179
|
+
return {
|
|
180
|
+
current: value,
|
|
181
|
+
zScore: 1,
|
|
182
|
+
normalized: 0.5,
|
|
183
|
+
standardDeviation: 0,
|
|
184
|
+
median: value,
|
|
185
|
+
mean,
|
|
186
|
+
min,
|
|
187
|
+
max,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const stdDev = Math.sqrt(variance)
|
|
189
192
|
return {
|
|
190
193
|
current: value,
|
|
191
|
-
zScore:
|
|
192
|
-
normalized,
|
|
193
|
-
standardDeviation:
|
|
194
|
-
median,
|
|
194
|
+
zScore: stdDev > 0 ? (value - mean) / (stdDev * 2.5) : 0,
|
|
195
|
+
normalized: (value - min) / (max - min),
|
|
196
|
+
standardDeviation: stdDev,
|
|
197
|
+
median: calculateMedian(),
|
|
195
198
|
mean,
|
|
196
199
|
min,
|
|
197
200
|
max,
|
|
198
201
|
}
|
|
199
202
|
}
|
|
203
|
+
|
|
204
|
+
calculate.queue = queue
|
|
205
|
+
return calculate
|
|
200
206
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class WindowProcessor extends AudioWorkletProcessor {
|
|
2
|
+
static get parameterDescriptors() {
|
|
3
|
+
return [] // Add any parameters you might need, like window type
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
super()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
process(inputs, outputs, parameters) {
|
|
11
|
+
const input = inputs[0]
|
|
12
|
+
const output = outputs[0]
|
|
13
|
+
|
|
14
|
+
if (input.length > 0) {
|
|
15
|
+
const windowLength = input[0].length
|
|
16
|
+
for (let channel = 0; channel < input.length; channel++) {
|
|
17
|
+
const inputChannel = input[channel]
|
|
18
|
+
const outputChannel = output[channel]
|
|
19
|
+
for (let i = 0; i < windowLength; i++) {
|
|
20
|
+
// Apply a Hanning window as an example
|
|
21
|
+
const windowCoeff = 0.5 * (1 - Math.cos((2 * Math.PI * i) / (windowLength - 1)))
|
|
22
|
+
outputChannel[i] = inputChannel[i] * windowCoeff
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return true // Keep processor alive
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
registerProcessor('window-processor', WindowProcessor)
|