hypnosound 1.7.2 → 1.8.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/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(analyser)
23
- analyser.fftSize = 32768 / 2 // Or whatever size you need
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 draw = () => {
29
- requestAnimationFrame(draw)
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
- console.log({
35
- // energy: a.energy(dataArray),
36
- // spectralCentroid: a.spectralCentroid(dataArray),
37
- // spectralCrest: a.spectralCrest(dataArray),
38
- // spectralEntropy: a.spectralEntropy(dataArray),
39
- // spectralFlux: a.spectralFlux(dataArray),
40
- // spectralKurtosis: a.spectralKurtosis(dataArray),
41
- // spectralRolloff: a.spectralRolloff(dataArray),
42
- // spectralRoughness: a.spectralRoughness(dataArray),
43
- // spectralSkew: a.spectralSkew(dataArray),
44
- // spectralSpread: a.spectralSpread(dataArray),
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
- draw()
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 windowedFft = applyKaiserWindow(fft)
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 windowedFft = applyKaiserWindow(fft)
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 windowedFft = applyKaiserWindow(fft)
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 windowedFft = applyKaiserWindow(fft)
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 windowedFft = applyKaiserWindow(fft)
83
- const value = spectralFlux(windowedFft, this.previousValue.spectralFlux)
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 windowedFft = applyKaiserWindow(fft)
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 windowedFft = applyKaiserWindow(fft)
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 windowedFft = applyKaiserWindow(fft)
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 windowedFft = applyKaiserWindow(fft)
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 windowedFft = applyKaiserWindow(fft)
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 windowedFft = applyKaiserWindow(fft)
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 windowedFft = applyKaiserWindow(fft)
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 windowedFft = applyKaiserWindow(fft)
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 windowedFft = applyKaiserWindow(fft)
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hypnosound",
3
3
  "type": "module",
4
- "version": "1.7.2",
4
+ "version": "1.8.0",
5
5
  "description": "A small library for analyzing audio",
6
6
  "main": "index.js",
7
7
  "scripts": {
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
- let magnitude = Math.abs(fft[i]) / totalSamples
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 // Scale by 10 if needed, similar to your original function
32
+ let normalizedBassPower = maxEnergy > 0 ? bassEnergy / maxEnergy : 0
33
+ return isNaN(normalizedBassPower) ? 0 : normalizedBassPower
32
34
  }
@@ -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] / currentSignal.length
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
- return energy * 2
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
  }
@@ -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 sampleRate = 44100 // This could vary
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 = 0; i < fft.length; 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 class
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 value = calculateSpectralKurtosis(fft) // Process FFT data
4
- return value / 1000
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 gigantic scores when variance is very small
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
- const value = computed / 10
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)
@@ -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)