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 +1 -0
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/cmd.js +3 -0
- package/index.html +54 -0
- package/index.js +111 -0
- package/package.json +29 -0
- package/src/audio/energy.js +16 -0
- package/src/audio/index.js +11 -0
- package/src/audio/spectralCentroid.js +33 -0
- package/src/audio/spectralCrest.js +19 -0
- package/src/audio/spectralEntropy.js +31 -0
- package/src/audio/spectralFlux.js +19 -0
- package/src/audio/spectralKurtosis.js +36 -0
- package/src/audio/spectralRolloff.js +21 -0
- package/src/audio/spectralRoughness.js +18 -0
- package/src/audio/spectralSkew.js +33 -0
- package/src/audio/spectralSpread.js +40 -0
- package/src/utils/calculateStats.js +194 -0
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
package/cmd.js
ADDED
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
|
+
}
|