hypnosound 1.2.0 → 1.3.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/README.md +56 -1
- package/index.html +17 -16
- package/index.js +32 -13
- package/package.json +1 -1
- package/src/audio/index.js +1 -0
- package/src/audio/pitchClass.js +26 -0
- package/src/audio/spectralCentroid.js +18 -10
- package/src/utils/applyKaiserWindow.js +31 -0
- package/cmd.js +0 -3
package/README.md
CHANGED
|
@@ -1,2 +1,57 @@
|
|
|
1
1
|
# hypnosound
|
|
2
|
-
A little library for
|
|
2
|
+
A little library for extracting audio features, and optionally applying statistics to them.
|
|
3
|
+
|
|
4
|
+
## Usage
|
|
5
|
+
Check out [index.html](./index.html) for a simple example. You can run it via `npm run start`.
|
|
6
|
+
|
|
7
|
+
You can either use the AudioProcessor, which maintains state and calculates the statistics for you, or use of the functions directly in a functional way. Everything can be used functionally except for spectralFlux, which requires state.
|
|
8
|
+
|
|
9
|
+
### AudioProcessor
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
import AudioProcessor from 'hypnosound';
|
|
13
|
+
const a = new AudioProcessor();
|
|
14
|
+
console.log({
|
|
15
|
+
energy: a.energy(fft),
|
|
16
|
+
spectralCentroid: a.spectralCentroid(fft),
|
|
17
|
+
spectralCrest: a.spectralCrest(fft),
|
|
18
|
+
spectralEntropy: a.spectralEntropy(fft),
|
|
19
|
+
spectralFlux: a.spectralFlux(fft),
|
|
20
|
+
spectralKurtosis: a.spectralKurtosis(fft),
|
|
21
|
+
spectralRolloff: a.spectralRolloff(fft),
|
|
22
|
+
spectralRoughness: a.spectralRoughness(fft),
|
|
23
|
+
spectralSkew: a.spectralSkew(fft),
|
|
24
|
+
spectralSpread: a.spectralSpread(fft),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Each audio feature comes with statistics, which are calculated automatically. You can access them like so:
|
|
30
|
+
```javascript
|
|
31
|
+
const {value, stats} = a.energy(fft)
|
|
32
|
+
console.log(`the current value for energy is ${value}`);
|
|
33
|
+
console.log(`here are some stats: zScore: ${stats.zScore}, normalized: ${stats.normalized}, standardDeviation: ${stats.standardDeviation}, median: ${stats.median}, mean: ${stats.mean}, min: ${stats.min}, max: ${stats.max}`);
|
|
34
|
+
```
|
|
35
|
+
⚠️ __Warning: Each call to a function will update the statistics for that feature. so I'd recommend saving the result of the function call to a variable and then use that__
|
|
36
|
+
|
|
37
|
+
### Functional
|
|
38
|
+
```javascript
|
|
39
|
+
import {energy} from 'hypnosound'; // or any other audio feature EXCEPT spectralFlux
|
|
40
|
+
console.log(energy(fft)); // returns the instantaneous energy value.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
You may want to calculate statistics for the audio features on your own, but still use the functional style.
|
|
44
|
+
Since statistics require state, this must be managed outside the function in purely functional mode.
|
|
45
|
+
Here's an example of how you might do that:
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
import { makeCalculateStats, spectralCentroid } from 'hypnosound'
|
|
49
|
+
const calculateStats = makeCalculateStats()
|
|
50
|
+
|
|
51
|
+
const value = spectralCentroid(fft)
|
|
52
|
+
const stats = calculateStats(value) // WARNING: each call to calculateStats will update the state.
|
|
53
|
+
|
|
54
|
+
console.log({value, stats})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
|
package/index.html
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
<button id="start">Start Listening</button>
|
|
10
10
|
<script type="module">
|
|
11
11
|
import AudioProcessor from './index.js'
|
|
12
|
-
document.getElementById('start')
|
|
12
|
+
const button = document.getElementById('start')
|
|
13
|
+
button.addEventListener('click', async () => {
|
|
13
14
|
const a = new AudioProcessor()
|
|
14
15
|
try {
|
|
15
16
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
@@ -18,8 +19,8 @@
|
|
|
18
19
|
const analyser = audioContext.createAnalyser();
|
|
19
20
|
|
|
20
21
|
source.connect(analyser);
|
|
21
|
-
analyser.fftSize = 32768; // Or whatever size you need
|
|
22
|
-
|
|
22
|
+
analyser.fftSize = 32768/2; // Or whatever size you need
|
|
23
|
+
analyser.smoothingTimeConstant = 0
|
|
23
24
|
const bufferLength = analyser.frequencyBinCount;
|
|
24
25
|
const dataArray = new Uint8Array(bufferLength);
|
|
25
26
|
|
|
@@ -29,19 +30,19 @@
|
|
|
29
30
|
analyser.getByteFrequencyData(dataArray);
|
|
30
31
|
|
|
31
32
|
// This is where the magic happens, but be careful what you log...
|
|
32
|
-
console.log(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
console.log(
|
|
34
|
+
// energy: a.energy(dataArray),
|
|
35
|
+
// spectralCentroid: a.spectralCentroid(dataArray),
|
|
36
|
+
// spectralCrest: a.spectralCrest(dataArray),
|
|
37
|
+
// spectralEntropy: a.spectralEntropy(dataArray),
|
|
38
|
+
// spectralFlux: a.spectralFlux(dataArray),
|
|
39
|
+
// spectralKurtosis: a.spectralKurtosis(dataArray),
|
|
40
|
+
// spectralRolloff: a.spectralRolloff(dataArray),
|
|
41
|
+
// spectralRoughness: a.spectralRoughness(dataArray),
|
|
42
|
+
// spectralSkew: a.spectralSkew(dataArray),
|
|
43
|
+
// spectralSpread: a.spectralSpread(dataArray),
|
|
44
|
+
a.pitchClass(dataArray),
|
|
45
|
+
);
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
draw();
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { makeCalculateStats } from './src/utils/calculateStats.js'
|
|
2
|
-
|
|
2
|
+
import {applyKaiserWindow} from './src/utils/applyKaiserWindow.js'
|
|
3
3
|
import energy from './src/audio/energy.js'
|
|
4
4
|
import spectralCentroid from './src/audio/spectralCentroid.js'
|
|
5
5
|
import spectralCrest from './src/audio/spectralCrest.js'
|
|
@@ -10,7 +10,7 @@ import spectralRolloff from './src/audio/spectralRolloff.js'
|
|
|
10
10
|
import spectralRoughness from './src/audio/spectralRoughness.js'
|
|
11
11
|
import spectralSkew from './src/audio/spectralSkew.js'
|
|
12
12
|
import spectralSpread from './src/audio/spectralSpread.js'
|
|
13
|
-
|
|
13
|
+
import pitchClass from './src/audio/pitchClass.js'
|
|
14
14
|
class AudioProcessor {
|
|
15
15
|
constructor() {
|
|
16
16
|
// aah, state management
|
|
@@ -37,67 +37,86 @@ class AudioProcessor {
|
|
|
37
37
|
this.statCalculators.spectralRoughness = makeCalculateStats()
|
|
38
38
|
|
|
39
39
|
this.statCalculators.spectralSpread = makeCalculateStats()
|
|
40
|
+
|
|
41
|
+
this.statCalculators.pitchClass = makeCalculateStats()
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
energy = (fft) => {
|
|
43
|
-
const
|
|
45
|
+
const windowedFft = applyKaiserWindow(fft)
|
|
46
|
+
const value = energy(windowedFft)
|
|
44
47
|
const stats = this.statCalculators.energy(value)
|
|
45
48
|
return { value, stats }
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
spectralCentroid = (fft) => {
|
|
49
|
-
const
|
|
52
|
+
const windowedFft = applyKaiserWindow(fft)
|
|
53
|
+
const value = spectralCentroid(applyKaiserWindow(windowedFft))
|
|
50
54
|
const stats = this.statCalculators.spectralCentroid(value)
|
|
51
55
|
return { value, stats }
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
spectralCrest = (fft) => {
|
|
55
|
-
const
|
|
59
|
+
const windowedFft = applyKaiserWindow(fft)
|
|
60
|
+
const value = spectralCrest(windowedFft)
|
|
56
61
|
const stats = this.statCalculators.spectralCentroid(value)
|
|
57
62
|
return { value, stats }
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
spectralEntropy = (fft) => {
|
|
61
|
-
const
|
|
66
|
+
const windowedFft = applyKaiserWindow(fft)
|
|
67
|
+
const value = spectralEntropy(windowedFft)
|
|
62
68
|
const stats = this.statCalculators.spectralEntropy(value)
|
|
63
69
|
return { value, stats }
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
spectralFlux = (fft) => {
|
|
67
|
-
const
|
|
68
|
-
this.previousValue.spectralFlux
|
|
73
|
+
const windowedFft = applyKaiserWindow(fft)
|
|
74
|
+
const value = spectralFlux(windowedFft, this.previousValue.spectralFlux)
|
|
75
|
+
this.previousValue.spectralFlux = new Uint8Array(windowedFft)
|
|
69
76
|
const stats = this.statCalculators.spectralFlux(value)
|
|
70
77
|
return { value, stats }
|
|
71
78
|
}
|
|
72
79
|
spectralKurtosis = (fft) => {
|
|
73
|
-
const
|
|
80
|
+
const windowedFft = applyKaiserWindow(fft)
|
|
81
|
+
const value = spectralKurtosis(windowedFft)
|
|
74
82
|
const stats = this.statCalculators.spectralKurtosis(value)
|
|
75
83
|
return { value, stats }
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
spectralRolloff = (fft) => {
|
|
79
|
-
const
|
|
87
|
+
const windowedFft = applyKaiserWindow(fft)
|
|
88
|
+
const value = spectralRolloff(windowedFft)
|
|
80
89
|
const stats = this.statCalculators.spectralRolloff(value)
|
|
81
90
|
return { value, stats }
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
spectralRoughness = (fft) => {
|
|
85
|
-
const
|
|
94
|
+
const windowedFft = applyKaiserWindow(fft)
|
|
95
|
+
const value = spectralRoughness(windowedFft)
|
|
86
96
|
const stats = this.statCalculators.spectralRoughness(value)
|
|
87
97
|
return { value, stats }
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
spectralSkew = (fft) => {
|
|
91
|
-
const
|
|
101
|
+
const windowedFft = applyKaiserWindow(fft)
|
|
102
|
+
const value = spectralSkew(windowedFft)
|
|
92
103
|
const stats = this.statCalculators.spectralSkew(value)
|
|
93
104
|
return { value, stats }
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
spectralSpread = (fft) => {
|
|
97
|
-
const
|
|
108
|
+
const windowedFft = applyKaiserWindow(fft)
|
|
109
|
+
const value = spectralSpread(windowedFft)
|
|
98
110
|
const stats = this.statCalculators.spectralSpread(value)
|
|
99
111
|
return { value, stats }
|
|
100
112
|
}
|
|
113
|
+
|
|
114
|
+
pitchClass = (fft) => {
|
|
115
|
+
const windowedFft = applyKaiserWindow(fft)
|
|
116
|
+
const value = pitchClass(windowedFft)
|
|
117
|
+
const stats = this.statCalculators.pitchClass(value)
|
|
118
|
+
return { value, stats }
|
|
119
|
+
}
|
|
101
120
|
}
|
|
102
121
|
export default AudioProcessor
|
|
103
122
|
export {
|
package/package.json
CHANGED
package/src/audio/index.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export default function pitchClass(fft) {
|
|
2
|
+
// Constants for the FFT processing
|
|
3
|
+
const sampleRate = 44100; // This could vary
|
|
4
|
+
const fftSize = fft.length; // This is an example, adjust based on your FFT setup
|
|
5
|
+
const freqResolution = sampleRate / fftSize;
|
|
6
|
+
|
|
7
|
+
// Finding the dominant frequency in the FFT data
|
|
8
|
+
let maxIndex = 0;
|
|
9
|
+
let maxValue = 0;
|
|
10
|
+
for (let i = 0; i < fft.length; i++) {
|
|
11
|
+
if (fft[i] > maxValue) {
|
|
12
|
+
maxValue = fft[i];
|
|
13
|
+
maxIndex = i;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const dominantFreq = maxIndex * freqResolution;
|
|
17
|
+
|
|
18
|
+
// Convert to MIDI note then to pitchClass class
|
|
19
|
+
const midiNote = 69 + 12 * Math.log2(dominantFreq / 440);
|
|
20
|
+
const pitchClass = midiNote % 12;
|
|
21
|
+
|
|
22
|
+
// Normalize to a 0-1 range
|
|
23
|
+
const normalizedpitchClass = pitchClass / 12;
|
|
24
|
+
|
|
25
|
+
return normalizedpitchClass;
|
|
26
|
+
}
|
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
import mu from '../utils/mu.js'
|
|
2
1
|
export default function spectralCentroid(fft) {
|
|
3
2
|
const computed = calculateSpectralCentroid(fft) // Process FFT data
|
|
4
3
|
return computed * 1.5
|
|
5
4
|
}
|
|
6
|
-
|
|
7
5
|
function calculateSpectralCentroid(ampSpectrum) {
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
if (!ampSpectrum.length) return null // Early exit if the spectrum is empty
|
|
7
|
+
|
|
8
|
+
let numerator = 0
|
|
9
|
+
let denominator = 0
|
|
10
|
+
|
|
11
|
+
// Calculate the weighted sum (numerator) and the sum of the amplitudes (denominator)
|
|
12
|
+
ampSpectrum.forEach((amplitude, index) => {
|
|
13
|
+
numerator += index * amplitude
|
|
14
|
+
denominator += amplitude
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// Avoid dividing by zero
|
|
18
|
+
if (denominator === 0) return null
|
|
19
|
+
|
|
20
|
+
const centroidIndex = numerator / denominator
|
|
21
|
+
// Normalize the centroid index to be between 0 and 1
|
|
22
|
+
const normalizedCentroid = centroidIndex / (ampSpectrum.length - 1)
|
|
10
23
|
|
|
11
|
-
|
|
12
|
-
const maxCentroid = mu(
|
|
13
|
-
1,
|
|
14
|
-
ampSpectrum.map((val, index) => (index === ampSpectrum.length - 1 ? 1 : 0)),
|
|
15
|
-
)
|
|
16
|
-
return centroid / maxCentroid // Normalize the centroid
|
|
24
|
+
return normalizedCentroid
|
|
17
25
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function applyKaiserWindow(audioBuffer, beta = 5.658) {
|
|
2
|
+
// Beta default based on common use
|
|
3
|
+
const N = audioBuffer.length
|
|
4
|
+
const windowedBuffer = new Float32Array(N)
|
|
5
|
+
const I0Beta = I0(beta) // Calculate the zeroth order modified Bessel function of the first kind for beta
|
|
6
|
+
|
|
7
|
+
for (let n = 0; n < N; n++) {
|
|
8
|
+
const windowValue = I0(beta * Math.sqrt(1 - Math.pow((2 * n) / (N - 1) - 1, 2))) / I0Beta
|
|
9
|
+
windowedBuffer[n] = audioBuffer[n] * windowValue
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return windowedBuffer
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Calculate the zeroth order modified Bessel function of the first kind
|
|
16
|
+
// This approximation is suitable for the window function calculation
|
|
17
|
+
function I0(x) {
|
|
18
|
+
let sum = 1.0
|
|
19
|
+
let y = x / 2.0
|
|
20
|
+
let term = 1.0
|
|
21
|
+
let k = 1
|
|
22
|
+
|
|
23
|
+
while (term > 1e-6 * sum) {
|
|
24
|
+
// Continue until the added value is insignificant
|
|
25
|
+
term *= (y / k) ** 2
|
|
26
|
+
sum += term
|
|
27
|
+
k++
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return sum
|
|
31
|
+
}
|
package/cmd.js
DELETED