hypnosound 1.7.2 → 1.8.1

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/.eslintrc CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "parserOptions": {
3
- "ecmaVersion": 2023,
3
+ "ecmaVersion": 2024,
4
4
  "sourceType": "module"
5
5
  },
6
- "plugins": ["prettier"],
7
-
6
+ "plugins": [
7
+ "prettier"
8
+ ],
8
9
  "rules": {
9
10
  "prettier/prettier": "warn"
10
11
  }
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.1",
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)
@@ -1,200 +1,206 @@
1
1
  export const StatTypes = ['normalized', 'mean', 'median', 'standardDeviation', 'zScore', 'min', 'max']
2
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
- }
3
+ const erf = (x) => {
4
+ const a1 = 0.254829592
5
+ const a2 = -0.284496736
6
+ const a3 = 1.421413741
7
+ const a4 = -1.453152027
8
+ const a5 = 1.061405429
9
+ const p = 0.3275911
22
10
 
23
- function removeOldFromMinMaxQueues(oldValue) {
24
- if (minQueue[0] === oldValue) {
25
- minQueue.shift()
26
- }
27
- if (maxQueue[0] === oldValue) {
28
- maxQueue.shift()
29
- }
30
- }
11
+ const sign = x < 0 ? -1 : 1
12
+ x = Math.abs(x)
31
13
 
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
- }
14
+ const t = 1.0 / (1.0 + p * x)
15
+ const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x)
40
16
 
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
- }
17
+ return sign * y
18
+ }
50
19
 
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
- }
20
+ class MonotonicQueue {
21
+ constructor(isMin = true) {
22
+ this.deque = []
23
+ this.compare = isMin ? (a, b) => a > b : (a, b) => a < b
24
+ }
57
25
 
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)
26
+ push(value) {
27
+ while (this.deque.length && this.compare(this.deque[this.deque.length - 1], value)) {
28
+ this.deque.pop()
65
29
  }
30
+ this.deque.push(value)
66
31
  }
67
32
 
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
- }
33
+ remove(value) {
34
+ if (!this.deque.length || this.deque[0] !== value) return
35
+ this.deque.shift()
79
36
  }
80
37
 
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
38
+ peek = () => this.deque[0]
39
+ }
40
+
41
+ const bubbleUp = (heap, isMinHeap) => {
42
+ let index = heap.length - 1
43
+ const value = heap[index]
44
+
45
+ while (index > 0) {
46
+ const parentIdx = Math.floor((index - 1) / 2)
47
+ const shouldSwap = isMinHeap ? heap[index] < heap[parentIdx] : heap[index] > heap[parentIdx]
48
+
49
+ if (!shouldSwap) break
50
+
51
+ heap[index] = heap[parentIdx]
52
+ index = parentIdx
90
53
  }
91
54
 
92
- function sinkDown(heap, isMinHeap) {
93
- let index = 0
94
- let length = heap.length
55
+ heap[index] = value
56
+ }
95
57
 
96
- while (index < length) {
97
- let leftChildIndex = 2 * index + 1
98
- let rightChildIndex = 2 * index + 2
99
- let swapIndex = null
58
+ const getBestChildIndex = (heap, leftChildIndex, rightChildIndex, isMinHeap) => {
59
+ if (rightChildIndex >= heap.length) return leftChildIndex
100
60
 
101
- if (leftChildIndex < length) {
102
- if ((isMinHeap && heap[leftChildIndex] < heap[index]) || (!isMinHeap && heap[leftChildIndex] > heap[index])) {
103
- swapIndex = leftChildIndex
104
- }
105
- }
61
+ const comparator = isMinHeap ? Math.min : Math.max
62
+ return comparator(heap[leftChildIndex], heap[rightChildIndex]) === heap[leftChildIndex] ? leftChildIndex : rightChildIndex
63
+ }
106
64
 
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
- }
65
+ const sinkDown = (heap, isMinHeap) => {
66
+ let index = 0
67
+ const value = heap[0]
68
+ const length = heap.length
115
69
 
116
- if (swapIndex === null) {
117
- break
118
- }
70
+ while (true) {
71
+ const leftChildIndex = 2 * index + 1
72
+ if (leftChildIndex >= length) break
119
73
 
120
- ;[heap[index], heap[swapIndex]] = [heap[swapIndex], heap[index]]
121
- index = swapIndex
122
- }
74
+ const bestChildIndex = getBestChildIndex(heap, leftChildIndex, 2 * index + 2, isMinHeap)
75
+ const shouldSwap = isMinHeap ? heap[bestChildIndex] < value : heap[bestChildIndex] > value
76
+
77
+ if (!shouldSwap) break
78
+
79
+ heap[index] = heap[bestChildIndex]
80
+ index = bestChildIndex
81
+ }
82
+
83
+ heap[index] = value
84
+ }
85
+
86
+ export const makeCalculateStats = (historySize = 500) => {
87
+ const queue = []
88
+ const minQueue = new MonotonicQueue(true)
89
+ const maxQueue = new MonotonicQueue(false)
90
+ const lowerHalf = [] // Max heap
91
+ const upperHalf = [] // Min heap
92
+
93
+ let sum = 0
94
+ let sumOfSquares = 0
95
+
96
+ const getTargetHeap = (value) => {
97
+ if (lowerHalf.length === 0 || value < lowerHalf[0]) return { target: lowerHalf, isMin: false }
98
+ return { target: upperHalf, isMin: true }
99
+ }
100
+
101
+ const addToHeaps = (value) => {
102
+ const heap = getTargetHeap(value)
103
+ heap.target.push(value)
104
+ bubbleUp(heap.target, heap.isMin)
105
+ rebalanceHeaps()
123
106
  }
124
107
 
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)
108
+ const removeNumberFromHeaps = (number) => {
109
+ if (lowerHalf.includes(number)) {
110
+ const index = lowerHalf.indexOf(number)
111
+ lowerHalf[index] = lowerHalf[lowerHalf.length - 1]
112
+ lowerHalf.pop()
113
+ sinkDown(lowerHalf, false)
114
+ } else if (upperHalf.includes(number)) {
115
+ const index = upperHalf.indexOf(number)
116
+ upperHalf[index] = upperHalf[upperHalf.length - 1]
117
+ upperHalf.pop()
118
+ sinkDown(upperHalf, true)
131
119
  }
120
+ rebalanceHeaps()
132
121
  }
133
122
 
134
- function calculateMedian() {
135
- if (lowerHalf.length === upperHalf.length) {
136
- return (lowerHalf[0] + upperHalf[0]) / 2
137
- } else {
138
- return lowerHalf[0]
123
+ const rebalanceHeaps = () => {
124
+ if (lowerHalf.length <= upperHalf.length + 1 && upperHalf.length <= lowerHalf.length) return
125
+
126
+ if (lowerHalf.length > upperHalf.length + 1) {
127
+ const value = extractTop(lowerHalf)
128
+ upperHalf.push(value)
129
+ bubbleUp(upperHalf, true)
130
+ return
139
131
  }
132
+
133
+ const value = extractTop(upperHalf)
134
+ lowerHalf.push(value)
135
+ bubbleUp(lowerHalf, false)
140
136
  }
141
- function erf(x) {
142
- // Constants
143
- const a1 = 0.254829592
144
- const a2 = -0.284496736
145
- const a3 = 1.421413741
146
- const a4 = -1.453152027
147
- const a5 = 1.061405429
148
- const p = 0.3275911
149
-
150
- // Save the sign of x
151
- const sign = x < 0 ? -1 : 1
152
- x = Math.abs(x)
153
-
154
- // A&S formula 7.1.26
155
- const t = 1.0 / (1.0 + p * x)
156
- const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x)
157
-
158
- return sign * y
137
+
138
+ const extractTop = (heap) => {
139
+ if (!heap.length) return null
140
+ const top = heap[0]
141
+ heap[0] = heap[heap.length - 1]
142
+ heap.pop()
143
+ sinkDown(heap, heap === upperHalf)
144
+ return top
159
145
  }
160
146
 
161
- function normalizeZScore(zScore) {
162
- return 0.5 * (1 + erf(zScore / Math.sqrt(2)))
147
+ const calculateMedian = () => {
148
+ if (!lowerHalf.length) return queue[0] || 0
149
+ if (lowerHalf.length === upperHalf.length) return (lowerHalf[0] + upperHalf[0]) / 2
150
+ return lowerHalf[0]
163
151
  }
164
- return function calculateStats(value) {
165
- if (typeof value !== 'number') throw new Error('Input must be a number')
166
152
 
167
- updateMinMaxQueues(value)
168
- addNumberToHeaps(value)
153
+ const calculate = (value) => {
154
+ if (typeof value !== 'number' || isNaN(value)) throw new Error('Input must be a valid number')
169
155
 
156
+ minQueue.push(value)
157
+ maxQueue.push(value)
158
+ addToHeaps(value)
170
159
  queue.push(value)
160
+
171
161
  sum += value
172
162
  sumOfSquares += value * value
173
163
 
174
164
  if (queue.length > historySize) {
175
- let removed = queue.shift()
165
+ const removed = queue.shift()
176
166
  sum -= removed
177
167
  sumOfSquares -= removed * removed
178
- removeOldFromMinMaxQueues(removed)
168
+ minQueue.remove(removed)
169
+ maxQueue.remove(removed)
179
170
  removeNumberFromHeaps(removed)
180
171
  }
181
172
 
182
- let mean = sum / queue.length
183
- let variance = sumOfSquares / queue.length - mean * mean
184
- let min = minQueue.length ? minQueue[0] : 0
185
- let max = maxQueue.length ? maxQueue[0] : 0
186
- let median = calculateMedian()
187
- let normalized = queue.length ? (value - min) / (max - min) : 0
188
- let zScore = variance ? (value - mean) / Math.sqrt(variance) : 0
173
+ const mean = sum / queue.length
174
+ const variance = Math.max(0, sumOfSquares / queue.length - mean * mean)
175
+ const min = minQueue.peek() || 0
176
+ const max = maxQueue.peek() || 0
177
+
178
+ if (max === min) {
179
+ return {
180
+ current: value,
181
+ zScore: 1,
182
+ normalized: 0.5,
183
+ standardDeviation: 0,
184
+ median: value,
185
+ mean,
186
+ min,
187
+ max,
188
+ }
189
+ }
190
+
191
+ const stdDev = Math.sqrt(variance)
189
192
  return {
190
193
  current: value,
191
- zScore: zScore / 2.5,
192
- normalized,
193
- standardDeviation: Math.sqrt(variance),
194
- median,
194
+ zScore: stdDev > 0 ? (value - mean) / (stdDev * 2.5) : 0,
195
+ normalized: (value - min) / (max - min),
196
+ standardDeviation: stdDev,
197
+ median: calculateMedian(),
195
198
  mean,
196
199
  min,
197
200
  max,
198
201
  }
199
202
  }
203
+
204
+ calculate.queue = queue
205
+ return calculate
200
206
  }
@@ -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)