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 CHANGED
@@ -1,2 +1,57 @@
1
1
  # hypnosound
2
- A little library for analyzing audio
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').addEventListener('click', async () => {
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
- 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);
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 value = energy(fft)
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 value = spectralCentroid(fft)
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 value = spectralCrest(fft)
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 value = spectralEntropy(fft)
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 value = spectralFlux(fft, this.previousValue.spectralFlux)
68
- this.previousValue.spectralFlux = new Uint8Array(fft)
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 value = spectralKurtosis(fft)
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 value = spectralRolloff(fft)
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 value = spectralRoughness(fft)
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 value = spectralSkew(fft)
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 value = spectralSpread(fft)
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hypnosound",
3
3
  "type": "module",
4
- "version": "1.2.0",
4
+ "version": "1.3.0",
5
5
  "description": "A small library for analyzing audio",
6
6
  "main": "index.js",
7
7
  "scripts": {
@@ -8,3 +8,4 @@ export * as spectralRolloff from './spectralRolloff'
8
8
  export * as spectralRoughness from './spectralRoughness'
9
9
  export * as spectralSkew from './spectralSkew'
10
10
  export * as spectralSpread from './spectralSpread'
11
+ export * as pitchClass from './pitchClass'
@@ -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
- const centroid = mu(1, ampSpectrum)
9
- if (centroid === null) return null
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
- // Maximum centroid occurs when all energy is at the highest frequency bin
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
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- import AudioProcessor from './index.js'
3
- console.log(AudioProcessor)