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 +70 -19
- package/index.js +15 -29
- package/package.json +1 -1
- package/src/audio/bass.js +5 -3
- package/src/audio/energy.js +10 -4
- package/src/audio/pitchClass.js +6 -6
- package/src/audio/spectralKurtosis.js +7 -3
- package/src/audio/spectralSkew.js +8 -1
- package/window-processor.js +31 -0
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(
|
|
23
|
-
analyser
|
|
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
|
|
29
|
-
requestAnimationFrame(
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
83
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
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
|
-
|
|
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
|
|
32
|
+
let normalizedBassPower = maxEnergy > 0 ? bassEnergy / maxEnergy : 0
|
|
33
|
+
return isNaN(normalizedBassPower) ? 0 : normalizedBassPower
|
|
32
34
|
}
|
package/src/audio/energy.js
CHANGED
|
@@ -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] /
|
|
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
|
-
|
|
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
|
}
|
package/src/audio/pitchClass.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
|
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
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
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)
|