hypnosound 1.0.0

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/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ node --version
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 hypnodroid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # hypnosound
2
+ A little library for analyzing audio
package/cmd.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import AudioProcessor from './index.js'
3
+ console.log(AudioProcessor)
package/index.html ADDED
@@ -0,0 +1,54 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Audio Capture and Analysis</title>
7
+ </head>
8
+ <body>
9
+ <button id="start">Start Listening</button>
10
+ <script type="module">
11
+ import AudioProcessor from './index.js'
12
+ document.getElementById('start').addEventListener('click', async () => {
13
+ const a = new AudioProcessor()
14
+ try {
15
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
16
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
17
+ const source = audioContext.createMediaStreamSource(stream);
18
+ const analyser = audioContext.createAnalyser();
19
+
20
+ source.connect(analyser);
21
+ analyser.fftSize = 32768; // Or whatever size you need
22
+
23
+ const bufferLength = analyser.frequencyBinCount;
24
+ const dataArray = new Uint8Array(bufferLength);
25
+
26
+ const draw = () => {
27
+ requestAnimationFrame(draw);
28
+
29
+ analyser.getByteFrequencyData(dataArray);
30
+
31
+ // This is where the magic happens, but be careful what you log...
32
+ console.log({
33
+ energy: a.energy(dataArray),
34
+ spectralCentroid: a.spectralCentroid(dataArray),
35
+ spectralCrest: a.spectralCrest(dataArray),
36
+ spectralEntropy: a.spectralEntropy(dataArray),
37
+ spectralFlux: a.spectralFlux(dataArray),
38
+ spectralKurtosis: a.spectralKurtosis(dataArray),
39
+ spectralRolloff: a.spectralRolloff(dataArray),
40
+ spectralRoughness: a.spectralRoughness(dataArray),
41
+ spectralSkew: a.spectralSkew(dataArray),
42
+ spectralSpread: a.spectralSpread(dataArray),
43
+ });
44
+ // console.log(a.spectralCentroid(dataArray).value);
45
+ };
46
+
47
+ draw();
48
+ } catch (error) {
49
+ console.error('Something went wrong:', error);
50
+ }
51
+ });
52
+ </script>
53
+ </body>
54
+ </html>
package/index.js ADDED
@@ -0,0 +1,111 @@
1
+ import { makeCalculateStats } from "./src/utils/calculateStats.js";
2
+
3
+ import energy from "./src/audio/energy.js";
4
+ import spectralCentroid from "./src/audio/spectralCentroid.js";
5
+ import spectralCrest from "./src/audio/spectralCrest.js";
6
+ import spectralEntropy from "./src/audio/spectralEntropy.js";
7
+ import spectralFlux from "./src/audio/spectralFlux.js";
8
+ import spectralKurtosis from "./src/audio/spectralKurtosis.js";
9
+ import spectralRolloff from "./src/audio/spectralRolloff.js";
10
+ import spectralRoughness from "./src/audio/spectralRoughness.js";
11
+ import spectralSkew from "./src/audio/spectralSkew.js";
12
+ import spectralSpread from "./src/audio/spectralSpread.js";
13
+
14
+ class AudioProcessor {
15
+ constructor() {
16
+ this.statCalculators = {};
17
+ this.previousValue = {};
18
+ this.analyzers = {};
19
+
20
+ this.statCalculators.energy = makeCalculateStats();
21
+ this.previousValue.energy = 0;
22
+
23
+ this.statCalculators.spectralCentroid = makeCalculateStats();
24
+ this.previousValue.spectralCentroid = 0;
25
+
26
+ this.statCalculators.spectralCrest = makeCalculateStats();
27
+ this.previousValue.spectralCrest = 0;
28
+
29
+ this.statCalculators.spectralEntropy = makeCalculateStats();
30
+ this.previousValue.spectralEntropy = 0;
31
+
32
+ this.statCalculators.spectralFlux = makeCalculateStats();
33
+ this.previousValue.spectralFlux = null;
34
+
35
+ this.statCalculators.spectralKurtosis = makeCalculateStats();
36
+ this.previousValue.spectralKurtosis = 0;
37
+
38
+ this.statCalculators.spectralRolloff = makeCalculateStats();
39
+ this.previousValue.spectralRolloff = 0;
40
+
41
+ this.statCalculators.spectralSkew = makeCalculateStats();
42
+ this.previousValue.spectralSkew = 0;
43
+
44
+ this.statCalculators.spectralRoughness = makeCalculateStats();
45
+ this.previousValue.spectralRoughness = 0;
46
+
47
+ this.statCalculators.spectralSpread = makeCalculateStats();
48
+ this.previousValue.spectralSpread = 0;
49
+ }
50
+
51
+ energy = (fft) => {
52
+ const { value, stats } = energy(this.previousValue.energy, this.statCalculators.energy, fft);
53
+ this.previousValue.energy = value;
54
+ return { value, stats };
55
+ };
56
+
57
+ spectralCentroid = (fft) => {
58
+ const { value, stats } = spectralCentroid(this.previousValue.spectralCentroid, this.statCalculators.spectralCentroid, fft);
59
+ this.previousValue.spectralCentroid = value;
60
+ return { value, stats };
61
+ };
62
+
63
+ spectralCrest = (fft) => {
64
+ const { value, stats } = spectralCrest(this.previousValue.spectralCrest, this.statCalculators.spectralCrest, fft);
65
+ this.previousValue.spectralCrest = value;
66
+ return { value, stats };
67
+ };
68
+
69
+ spectralEntropy = (fft) => {
70
+ const { value, stats } = spectralEntropy(this.previousValue.spectralEntropy, this.statCalculators.spectralEntropy, fft);
71
+ this.previousValue.spectralEntropy = value;
72
+ return { value, stats };
73
+ };
74
+
75
+ spectralFlux = (fft) => {
76
+ const { value, stats } = spectralFlux(this.previousValue.spectralFlux, this.statCalculators.spectralFlux, fft);
77
+ this.previousValue.spectralFlux = new Uint8Array(fft);
78
+ return { value, stats };
79
+ };
80
+ spectralKurtosis = (fft) => {
81
+ const { value, stats } = spectralKurtosis(this.previousValue.spectralKurtosis, this.statCalculators.spectralKurtosis, fft);
82
+ this.previousValue.spectralKurtosis = value;
83
+ return { value, stats };
84
+ };
85
+
86
+ spectralRolloff = (fft) => {
87
+ const { value, stats } = spectralRolloff(this.previousValue.spectralRolloff, this.statCalculators.spectralRolloff, fft);
88
+ this.previousValue.spectralRolloff = value;
89
+ return { value, stats };
90
+ };
91
+
92
+ spectralRoughness = (fft) => {
93
+ const { value, stats } = spectralRoughness(this.previousValue.spectralRoughness, this.statCalculators.spectralRoughness, fft);
94
+ this.previousValue.spectralRoughness = value;
95
+ return { value, stats };
96
+ };
97
+
98
+ spectralSkew = (fft) => {
99
+ const { value, stats } = spectralSkew(this.previousValue.spectralSkew, this.statCalculators.spectralSkew, fft);
100
+ this.previousValue.spectralSkew = value;
101
+ return { value, stats };
102
+ };
103
+
104
+ spectralSpread = (fft) => {
105
+ const { value, stats } = spectralSpread(this.previousValue.spectralSpread, this.statCalculators.spectralSpread, fft);
106
+ this.previousValue.spectralSpread = value;
107
+ return { value, stats };
108
+ };
109
+ }
110
+ export default AudioProcessor;
111
+ export {energy, spectralCentroid, spectralCrest, spectralEntropy, spectralFlux, spectralKurtosis, spectralRolloff, spectralRoughness, spectralSkew, spectralSpread};
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "hypnosound",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "description": "A small library for analyzing audio",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1",
9
+ "start": "live-server ."
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/hypnodroid/hypnosound.git"
14
+ },
15
+ "keywords": [
16
+ "Audio",
17
+ "sound",
18
+ "music"
19
+ ],
20
+ "author": "redaphid <iam@aaronherres.com>",
21
+ "license": "MIT",
22
+ "bugs": {
23
+ "url": "https://github.com/hypnodroid/hypnosound/issues"
24
+ },
25
+ "homepage": "https://github.com/hypnodroid/hypnosound#readme",
26
+ "devDependencies": {
27
+ "live-server": "^1.2.2"
28
+ }
29
+ }
@@ -0,0 +1,16 @@
1
+
2
+ export default function energy(prevValue,statCalculator, fft) {
3
+ const value = calculateFFTEnergy(fft)
4
+ const stats = statCalculator(value)
5
+ return { value, stats }
6
+ }
7
+
8
+ function calculateFFTEnergy(currentSignal) {
9
+ let energy = 0
10
+ for (let i = 0; i < currentSignal.length; i++) {
11
+ let normalizedValue = currentSignal[i] / currentSignal.length
12
+ energy += normalizedValue * normalizedValue
13
+ }
14
+
15
+ return energy * 10
16
+ }
@@ -0,0 +1,11 @@
1
+ export * as energy from './energy'
2
+ export * as spectralCentroid from './spectralCentroid'
3
+ export * as spectralCrest from './spectralCrest'
4
+ export * as spectralEntropy from './spectralEntropy'
5
+ export * as spectralFlux from './spectralFlux'
6
+ export * as spectralKurtosis from './spectralKurtosis'
7
+ export * as spectralRolloff from './spectralRolloff'
8
+ export * as spectralRoughness from './spectralRoughness'
9
+ export * as spectralSkew from './spectralSkew'
10
+ export * as spectralSpread from './spectralSpread'
11
+
@@ -0,0 +1,33 @@
1
+ export default function spectralCentroid(prevValue,statCalculator, fft) {
2
+ let computed = calculateSpectralCentroid(fft) // Process FFT data
3
+ computed *= 1.5
4
+ const value = computed
5
+ const stats = statCalculator(value)
6
+ return { value, stats }
7
+ }
8
+
9
+ function mu(i, amplitudeSpect) {
10
+ let numerator = 0
11
+ let denominator = 0
12
+
13
+ for (let k = 0; k < amplitudeSpect.length; k++) {
14
+ numerator += Math.pow(k, i) * Math.abs(amplitudeSpect[k])
15
+ denominator += amplitudeSpect[k]
16
+ }
17
+
18
+ if (denominator === 0) return null // Prevent division by zero
19
+ return numerator / denominator
20
+ }
21
+
22
+
23
+ function calculateSpectralCentroid(ampSpectrum) {
24
+ const centroid = mu(1, ampSpectrum)
25
+ if (centroid === null) return null
26
+
27
+ // Maximum centroid occurs when all energy is at the highest frequency bin
28
+ const maxCentroid = mu(
29
+ 1,
30
+ ampSpectrum.map((val, index) => (index === ampSpectrum.length - 1 ? 1 : 0)),
31
+ )
32
+ return centroid / maxCentroid // Normalize the centroid
33
+ }
@@ -0,0 +1,19 @@
1
+ export default function spectralCrest(prevValue,statCalculator, fft) {
2
+ let computed = calculateSpectralCrest(fft) // Process FFT data
3
+ const value = computed * 100
4
+ const stats = statCalculator(value)
5
+ return { value, stats }
6
+ }
7
+
8
+ function calculateSpectralCrest(fftData) {
9
+ // Find the maximum amplitude in the spectrum
10
+ const maxAmplitude = Math.max(...fftData)
11
+
12
+ // Calculate the sum of all amplitudes
13
+ const sumAmplitudes = fftData.reduce((sum, amplitude) => sum + amplitude, 0)
14
+
15
+ // Calculate the Spectral Crest
16
+ const spectralCrest = sumAmplitudes !== 0 ? maxAmplitude / sumAmplitudes : 0
17
+
18
+ return spectralCrest
19
+ }
@@ -0,0 +1,31 @@
1
+ export default function spectralEntropy(prevValue,statCalculator, fft) {
2
+ let computed = calculateSpectralEntropy(fft) // Process FFT data
3
+ const value = computed
4
+ const stats = statCalculator(value)
5
+ return { value, stats }
6
+ }
7
+
8
+ function toPowerSpectrum(fftData) {
9
+ return fftData.map((amplitude) => Math.pow(amplitude, 2))
10
+ }
11
+
12
+ function calculateSpectralEntropy(fftData) {
13
+ const powerSpectrum = toPowerSpectrum(fftData)
14
+ // Normalize the power spectrum to create a probability distribution
15
+ const totalPower = powerSpectrum.reduce((sum, val) => sum + val, 0)
16
+ if(totalPower === 0) return 0
17
+ const probabilityDistribution = new Float32Array(powerSpectrum.length)
18
+ for (let i = 0; i < powerSpectrum.length; i++) {
19
+ probabilityDistribution[i] = powerSpectrum[i] / totalPower
20
+ }
21
+
22
+ const entropy = probabilityDistribution.reduce((sum, prob) => {
23
+ if (prob > 0) {
24
+ const logProb = Math.log(prob);
25
+ return sum - prob * logProb;
26
+ } else {
27
+ return sum;
28
+ }
29
+ }, 0);
30
+ return entropy / Math.log(probabilityDistribution.length)
31
+ }
@@ -0,0 +1,19 @@
1
+ export default function spectralFlux(prevValue, statCalculator, fft) {
2
+ const value = calculateSpectralFlux(fft, prevValue)
3
+ const stats = statCalculator(value)
4
+ return { value, stats }
5
+ }
6
+
7
+ function calculateSpectralFlux(currentSignal, previousSignal) {
8
+
9
+ if (!previousSignal) {
10
+ previousSignal = new Float32Array(currentSignal.length)
11
+ }
12
+
13
+ let sf = 0
14
+ for (let i = 0; i < currentSignal.length; i++) {
15
+ const diff = Math.abs(currentSignal[i]) - Math.abs(previousSignal[i])
16
+ sf += (diff + Math.abs(diff)) / 2
17
+ }
18
+ return sf / 30_000
19
+ }
@@ -0,0 +1,36 @@
1
+ export default function spectralKurtosis(prevValue,statCalculator, fft) {
2
+ let computed = calculateSpectralKurtosis(fft) // Process FFT data
3
+ const value = computed * 100
4
+ const stats = statCalculator(value)
5
+ return { value, stats }
6
+ }
7
+
8
+ function mu(i, amplitudeSpect) {
9
+ let numerator = 0
10
+ let denominator = 0
11
+
12
+ for (let k = 0; k < amplitudeSpect.length; k++) {
13
+ numerator += Math.pow(k, i) * Math.abs(amplitudeSpect[k])
14
+ denominator += amplitudeSpect[k]
15
+ }
16
+
17
+ if (denominator === 0) return null // Prevent division by zero
18
+ return numerator / denominator
19
+ }
20
+ function calculateSpectralKurtosis(fftData) {
21
+ const mean = mu(1, fftData)
22
+ const secondMoment = mu(2, fftData)
23
+ const variance = secondMoment - Math.pow(mean, 2)
24
+
25
+ let fourthMoment = 0
26
+ for (let i = 0; i < fftData.length; i++) {
27
+ fourthMoment += Math.pow(fftData[i] - mean, 4)
28
+ }
29
+ fourthMoment /= fftData.length
30
+
31
+ // Add a small epsilon to the denominator to prevent gigantic scores when variance is very small
32
+ const epsilon = 1e-7 // Adjust epsilon based on the scale of your data
33
+ const kurtosis = variance ? fourthMoment / Math.pow(variance + epsilon, 2) - 3 : 0
34
+ if (kurtosis > 3000) return 0
35
+ return kurtosis / 3000
36
+ }
@@ -0,0 +1,21 @@
1
+ export default function spectralRolloff(prevValue, statCalculator, fft) {
2
+ const value = calculateSpectralRolloff(fft) // Compute Spectral Rolloff
3
+ const stats = statCalculator(value)
4
+ return { value, stats }
5
+ }
6
+
7
+ // Calculate Spectral Rolloff
8
+ function calculateSpectralRolloff(fftData, threshold = 0.85) {
9
+ let totalEnergy = fftData.reduce((acc, val) => acc + val, 0)
10
+ let energyThreshold = totalEnergy * threshold
11
+ let cumulativeEnergy = 0
12
+
13
+ for (let i = 0; i < fftData.length; i++) {
14
+ cumulativeEnergy += fftData[i]
15
+ if (cumulativeEnergy >= energyThreshold) {
16
+ return i / fftData.length // Normalized rolloff frequency
17
+ }
18
+ }
19
+
20
+ return 0 // In case the threshold is not met
21
+ }
@@ -0,0 +1,18 @@
1
+ export default function spectralRoughness(prevValue, statCalculator, fft) {
2
+ let computed = calculateSpectralRoughness(fft) // Process FFT data
3
+ const value = computed/100_000
4
+ const stats = statCalculator(value)
5
+ return { value, stats }
6
+ }
7
+
8
+ function calculateSpectralRoughness(fftData) {
9
+ let roughness = 0
10
+
11
+ for (let i = 1; i < fftData.length; i++) {
12
+ // Calculate the difference in amplitude between adjacent frequency bins
13
+ let diff = Math.abs(fftData[i] - fftData[i - 1])
14
+ roughness += diff
15
+ }
16
+
17
+ return roughness
18
+ }
@@ -0,0 +1,33 @@
1
+ export default function spectralSkew(prevValue, statCalculator, fft) {
2
+ const computed = calculateSpectralSkewness(fft) || 0 // Process FFT data
3
+ const value = computed / 10.
4
+ const stats = statCalculator(value)
5
+ return { value, stats }
6
+ }
7
+
8
+ function mu(i, amplitudeSpect) {
9
+ let numerator = 0
10
+ let denominator = 0
11
+
12
+ for (let k = 0; k < amplitudeSpect.length; k++) {
13
+ numerator += Math.pow(k, i) * Math.abs(amplitudeSpect[k])
14
+ denominator += amplitudeSpect[k]
15
+ }
16
+
17
+ if (denominator === 0) return null // Prevent division by zero
18
+ return numerator / denominator
19
+ }
20
+
21
+ function calculateSpectralSkewness(fftData) {
22
+ const mean = fftData.reduce((sum, val) => sum + val, 0) / fftData.length
23
+ const variance = fftData.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / fftData.length
24
+ const standardDeviation = Math.sqrt(variance)
25
+
26
+ let skewness = 0
27
+ if (standardDeviation !== 0) {
28
+ skewness = fftData.reduce((sum, val) => sum + Math.pow(val - mean, 3), 0) / fftData.length
29
+ skewness /= Math.pow(standardDeviation, 3)
30
+ }
31
+
32
+ return skewness
33
+ }
@@ -0,0 +1,40 @@
1
+ export default function spectralSpread(prevValue, statCalculator, fft) {
2
+ let computed = calculateSpectralSpread(fft) // Process FFT data
3
+ const value = computed
4
+ const stats = statCalculator(value)
5
+ return { value, stats }
6
+ }
7
+
8
+ function mu(i, amplitudeSpect) {
9
+ let numerator = 0
10
+ let denominator = 0
11
+
12
+ for (let k = 0; k < amplitudeSpect.length; k++) {
13
+ numerator += Math.pow(k, i) * Math.abs(amplitudeSpect[k])
14
+ denominator += amplitudeSpect[k]
15
+ }
16
+
17
+ if (denominator === 0) return 0 // Prevent division by zero
18
+ return numerator / denominator
19
+ }
20
+
21
+ function calculateMaxSpread(fftSize) {
22
+ // Create a spectrum with energy at the two extremes
23
+ const extremeSpectrum = new Array(fftSize).fill(0)
24
+ extremeSpectrum[0] = 1 // Energy at the lowest frequency
25
+ extremeSpectrum[fftSize - 1] = 1 // Energy at the highest frequency
26
+
27
+ const meanFrequency = mu(1, extremeSpectrum)
28
+ const secondMoment = mu(2, extremeSpectrum)
29
+
30
+ return Math.sqrt(secondMoment - Math.pow(meanFrequency, 2))
31
+ }
32
+
33
+ function calculateSpectralSpread(fftData) {
34
+ const meanFrequency = mu(1, fftData)
35
+ const secondMoment = mu(2, fftData)
36
+ const maxSpread = calculateMaxSpread(fftData.length)
37
+ const spread = Math.sqrt(secondMoment - Math.pow(meanFrequency, 2))
38
+ // Normalize the spread
39
+ return spread / maxSpread
40
+ }
@@ -0,0 +1,194 @@
1
+ export const StatTypes = ['normalized', 'mean', 'median', 'standardDeviation', 'zScore', 'min', 'max']
2
+
3
+ export function makeCalculateStats(historySize = 500) {
4
+ let queue = []
5
+ let sum = 0
6
+ let sumOfSquares = 0
7
+ let minQueue = []
8
+ let maxQueue = []
9
+ let lowerHalf = [] // Max heap
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
+ }
22
+
23
+ function removeOldFromMinMaxQueues(oldValue) {
24
+ if (minQueue[0] === oldValue) {
25
+ minQueue.shift()
26
+ }
27
+ if (maxQueue[0] === oldValue) {
28
+ maxQueue.shift()
29
+ }
30
+ }
31
+
32
+ function addNumberToHeaps(number) {
33
+ if (lowerHalf.length === 0 || number < lowerHalf[0]) {
34
+ lowerHalf.push(number)
35
+ bubbleUp(lowerHalf, false)
36
+ } else {
37
+ upperHalf.push(number)
38
+ bubbleUp(upperHalf, true)
39
+ }
40
+
41
+ // Rebalance heaps
42
+ if (lowerHalf.length > upperHalf.length + 1) {
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
+ }
50
+
51
+ function removeNumberFromHeaps(number) {
52
+ if (lowerHalf.includes(number)) {
53
+ removeNumber(lowerHalf, number, false)
54
+ } else if (upperHalf.includes(number)) {
55
+ removeNumber(upperHalf, number, true)
56
+ }
57
+
58
+ // Rebalance heaps
59
+ if (lowerHalf.length > upperHalf.length + 1) {
60
+ upperHalf.push(extractTop(lowerHalf, false))
61
+ bubbleUp(upperHalf, true)
62
+ } else if (upperHalf.length > lowerHalf.length) {
63
+ lowerHalf.push(extractTop(upperHalf, true))
64
+ bubbleUp(lowerHalf, false)
65
+ }
66
+ }
67
+
68
+ function bubbleUp(heap, isMinHeap) {
69
+ let index = heap.length - 1
70
+ while (index > 0) {
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
+ }
79
+ }
80
+
81
+ function extractTop(heap, isMinHeap) {
82
+ if (heap.length === 0) {
83
+ return null
84
+ }
85
+ let top = heap[0]
86
+ heap[0] = heap[heap.length - 1]
87
+ heap.pop()
88
+ sinkDown(heap, isMinHeap)
89
+ return top
90
+ }
91
+
92
+ function sinkDown(heap, isMinHeap) {
93
+ let index = 0
94
+ let length = heap.length
95
+
96
+ while (index < length) {
97
+ let leftChildIndex = 2 * index + 1
98
+ let rightChildIndex = 2 * index + 2
99
+ let swapIndex = null
100
+
101
+ if (leftChildIndex < length) {
102
+ if ((isMinHeap && heap[leftChildIndex] < heap[index]) || (!isMinHeap && heap[leftChildIndex] > heap[index])) {
103
+ swapIndex = leftChildIndex
104
+ }
105
+ }
106
+
107
+ if (rightChildIndex < length) {
108
+ if (
109
+ (isMinHeap && heap[rightChildIndex] < (swapIndex === null ? heap[index] : heap[leftChildIndex])) ||
110
+ (!isMinHeap && heap[rightChildIndex] > (swapIndex === null ? heap[index] : heap[leftChildIndex]))
111
+ ) {
112
+ swapIndex = rightChildIndex
113
+ }
114
+ }
115
+
116
+ if (swapIndex === null) {
117
+ break
118
+ }
119
+
120
+ ;[heap[index], heap[swapIndex]] = [heap[swapIndex], heap[index]]
121
+ index = swapIndex
122
+ }
123
+ }
124
+
125
+ function removeNumber(heap, number, isMinHeap) {
126
+ let index = heap.indexOf(number)
127
+ if (index !== -1) {
128
+ heap[index] = heap[heap.length - 1]
129
+ heap.pop()
130
+ sinkDown(heap, isMinHeap)
131
+ }
132
+ }
133
+
134
+ function calculateMedian() {
135
+ if (lowerHalf.length === upperHalf.length) {
136
+ return (lowerHalf[0] + upperHalf[0]) / 2
137
+ } else {
138
+ return lowerHalf[0]
139
+ }
140
+ }
141
+
142
+ function calculateMAD(median) {
143
+ let deviations = queue.map((value) => Math.abs(value - median))
144
+ let mad = medianAbsoluteDeviation(deviations)
145
+ return mad
146
+ }
147
+
148
+ function medianAbsoluteDeviation(values) {
149
+ if (values.length === 0) {
150
+ return 0
151
+ }
152
+ let median = calculateMedian(values)
153
+ let absoluteDeviations = values.map((value) => Math.abs(value - median))
154
+ let medianAbsoluteDeviation = calculateMedian(absoluteDeviations)
155
+ return medianAbsoluteDeviation
156
+ }
157
+
158
+ return function calculateStats(value) {
159
+ if (typeof value !== 'number') throw new Error('Input must be a number')
160
+
161
+ updateMinMaxQueues(value)
162
+ addNumberToHeaps(value)
163
+
164
+ queue.push(value)
165
+ sum += value
166
+ sumOfSquares += value * value
167
+
168
+ if (queue.length > historySize) {
169
+ let removed = queue.shift()
170
+ sum -= removed
171
+ sumOfSquares -= removed * removed
172
+ removeOldFromMinMaxQueues(removed)
173
+ removeNumberFromHeaps(removed)
174
+ }
175
+
176
+ let mean = sum / queue.length
177
+ let variance = sumOfSquares / queue.length - mean * mean
178
+ let min = minQueue.length ? minQueue[0] : Infinity
179
+ let max = maxQueue.length ? maxQueue[0] : -Infinity
180
+ let median = calculateMedian()
181
+ let mad = calculateMAD(median)
182
+
183
+ return {
184
+ current: value,
185
+ zScore: (variance ? (value - mean) / Math.sqrt(variance) : 0) / 6,
186
+ normalized: mad, // Using MAD normalization as 'normalized'
187
+ standardDeviation: Math.sqrt(variance),
188
+ median,
189
+ mean,
190
+ min,
191
+ max,
192
+ }
193
+ }
194
+ }