hypnosound 1.8.1 → 1.10.2
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/.github/workflows/publish.yml +44 -0
- package/index.js +23 -147
- package/package.json +4 -10
- package/scripts/generate-barrel.mjs +26 -0
- package/src/audio/index.js +30 -14
- package/src/utils/calculateStats.js +47 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths: [package.json]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: 20
|
|
20
|
+
registry-url: https://registry.npmjs.org
|
|
21
|
+
|
|
22
|
+
- name: Update npm
|
|
23
|
+
run: npm install -g npm@latest
|
|
24
|
+
|
|
25
|
+
- name: Check if version changed
|
|
26
|
+
id: version-check
|
|
27
|
+
run: |
|
|
28
|
+
LOCAL_VERSION=$(node -p "require('./package.json').version")
|
|
29
|
+
NPM_VERSION=$(npm view hypnosound version 2>/dev/null || echo "0.0.0")
|
|
30
|
+
echo "local=$LOCAL_VERSION" >> "$GITHUB_OUTPUT"
|
|
31
|
+
echo "npm=$NPM_VERSION" >> "$GITHUB_OUTPUT"
|
|
32
|
+
if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
|
|
33
|
+
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
34
|
+
else
|
|
35
|
+
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
- name: Publish
|
|
39
|
+
if: steps.version-check.outputs.changed == 'true'
|
|
40
|
+
run: npm publish --provenance --access public
|
|
41
|
+
|
|
42
|
+
- name: Skip
|
|
43
|
+
if: steps.version-check.outputs.changed == 'false'
|
|
44
|
+
run: echo "Version ${{ steps.version-check.outputs.local }} already published, skipping"
|
package/index.js
CHANGED
|
@@ -1,153 +1,29 @@
|
|
|
1
1
|
import { StatTypes, makeCalculateStats } from './src/utils/calculateStats.js'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
import spectralFlux from './src/audio/spectralFlux.js'
|
|
8
|
-
import spectralKurtosis from './src/audio/spectralKurtosis.js'
|
|
9
|
-
import spectralRolloff from './src/audio/spectralRolloff.js'
|
|
10
|
-
import spectralRoughness from './src/audio/spectralRoughness.js'
|
|
11
|
-
import spectralSkew from './src/audio/spectralSkew.js'
|
|
12
|
-
import spectralSpread from './src/audio/spectralSpread.js'
|
|
13
|
-
import pitchClass from './src/audio/pitchClass.js'
|
|
14
|
-
import bass from './src/audio/bass.js'
|
|
15
|
-
import treble from './src/audio/treble.js'
|
|
16
|
-
import mids from './src/audio/mids.js'
|
|
2
|
+
export * from './src/audio/index.js'
|
|
3
|
+
export { applyKaiserWindow } from './src/utils/applyKaiserWindow.js'
|
|
4
|
+
export { StatTypes, makeCalculateStats } from './src/utils/calculateStats.js'
|
|
5
|
+
import * as audio from './src/audio/index.js'
|
|
6
|
+
|
|
17
7
|
class AudioProcessor {
|
|
18
8
|
constructor() {
|
|
19
|
-
|
|
20
|
-
this.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.statCalculators.spectralSkew = makeCalculateStats()
|
|
39
|
-
|
|
40
|
-
this.statCalculators.spectralRoughness = makeCalculateStats()
|
|
41
|
-
|
|
42
|
-
this.statCalculators.spectralSpread = makeCalculateStats()
|
|
43
|
-
|
|
44
|
-
this.statCalculators.pitchClass = makeCalculateStats()
|
|
45
|
-
|
|
46
|
-
this.statCalculators.bass = makeCalculateStats()
|
|
47
|
-
|
|
48
|
-
this.statCalculators.treble = makeCalculateStats()
|
|
49
|
-
|
|
50
|
-
this.statCalculators.mids = makeCalculateStats()
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
energy = (fft) => {
|
|
54
|
-
const value = energy(fft)
|
|
55
|
-
const stats = this.statCalculators.energy(value)
|
|
56
|
-
return { value, stats }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
spectralCentroid = (fft) => {
|
|
60
|
-
const value = spectralCentroid(applyKaiserWindow(fft))
|
|
61
|
-
const stats = this.statCalculators.spectralCentroid(value)
|
|
62
|
-
return { value, stats }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
spectralCrest = (fft) => {
|
|
66
|
-
const value = spectralCrest(fft)
|
|
67
|
-
const stats = this.statCalculators.spectralCentroid(value)
|
|
68
|
-
return { value, stats }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
spectralEntropy = (fft) => {
|
|
72
|
-
const value = spectralEntropy(fft)
|
|
73
|
-
const stats = this.statCalculators.spectralEntropy(value)
|
|
74
|
-
return { value, stats }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
spectralFlux = (fft) => {
|
|
78
|
-
const value = spectralFlux(fft, this.previousValue.spectralFlux)
|
|
79
|
-
this.previousValue.spectralFlux = new Uint8Array(fft)
|
|
80
|
-
const stats = this.statCalculators.spectralFlux(value)
|
|
81
|
-
return { value, stats }
|
|
82
|
-
}
|
|
83
|
-
spectralKurtosis = (fft) => {
|
|
84
|
-
const value = spectralKurtosis(fft)
|
|
85
|
-
const stats = this.statCalculators.spectralKurtosis(value)
|
|
86
|
-
return { value, stats }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
spectralRolloff = (fft) => {
|
|
90
|
-
const value = spectralRolloff(fft)
|
|
91
|
-
const stats = this.statCalculators.spectralRolloff(value)
|
|
92
|
-
return { value, stats }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
spectralRoughness = (fft) => {
|
|
96
|
-
const value = spectralRoughness(fft)
|
|
97
|
-
const stats = this.statCalculators.spectralRoughness(value)
|
|
98
|
-
return { value, stats }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
spectralSkew = (fft) => {
|
|
102
|
-
const value = spectralSkew(fft)
|
|
103
|
-
const stats = this.statCalculators.spectralSkew(value)
|
|
104
|
-
return { value, stats }
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
spectralSpread = (fft) => {
|
|
108
|
-
const value = spectralSpread(fft)
|
|
109
|
-
const stats = this.statCalculators.spectralSpread(value)
|
|
110
|
-
return { value, stats }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
pitchClass = (fft) => {
|
|
114
|
-
const value = pitchClass(fft)
|
|
115
|
-
const stats = this.statCalculators.pitchClass(value)
|
|
116
|
-
return { value, stats }
|
|
117
|
-
}
|
|
118
|
-
bass = (fft) => {
|
|
119
|
-
const value = bass(fft)
|
|
120
|
-
const stats = this.statCalculators.bass(value)
|
|
121
|
-
return { value, stats }
|
|
122
|
-
}
|
|
123
|
-
treble = (fft) => {
|
|
124
|
-
const value = treble(fft)
|
|
125
|
-
const stats = this.statCalculators.treble(value)
|
|
126
|
-
return { value, stats }
|
|
127
|
-
}
|
|
128
|
-
mids = (fft) => {
|
|
129
|
-
const value = mids(fft)
|
|
130
|
-
const stats = this.statCalculators.mids(value)
|
|
131
|
-
return { value, stats }
|
|
9
|
+
const { AudioFeatures } = audio
|
|
10
|
+
this.state = AudioFeatures.reduce((acc, feature) => {
|
|
11
|
+
acc[feature] = {
|
|
12
|
+
analyzer: audio[feature],
|
|
13
|
+
statCalculator: makeCalculateStats(),
|
|
14
|
+
}
|
|
15
|
+
return acc
|
|
16
|
+
}, {})
|
|
17
|
+
for (const feature of AudioFeatures) {
|
|
18
|
+
this[feature] = (fft) => {
|
|
19
|
+
const { previousValue = 0, statCalculator, analyzer } = this.state[feature]
|
|
20
|
+
const value = analyzer(fft, previousValue)
|
|
21
|
+
this.state[feature].previousValue = value
|
|
22
|
+
this.state[feature].statCalculator = statCalculator
|
|
23
|
+
this.state[feature].stats = statCalculator(value, previousValue)
|
|
24
|
+
return { value, ...this.state[feature] }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
132
27
|
}
|
|
133
28
|
}
|
|
134
29
|
export default AudioProcessor
|
|
135
|
-
export {
|
|
136
|
-
energy,
|
|
137
|
-
spectralCentroid,
|
|
138
|
-
spectralCrest,
|
|
139
|
-
spectralEntropy,
|
|
140
|
-
spectralFlux,
|
|
141
|
-
spectralKurtosis,
|
|
142
|
-
spectralRolloff,
|
|
143
|
-
spectralRoughness,
|
|
144
|
-
spectralSkew,
|
|
145
|
-
spectralSpread,
|
|
146
|
-
pitchClass,
|
|
147
|
-
bass,
|
|
148
|
-
mids,
|
|
149
|
-
treble,
|
|
150
|
-
makeCalculateStats,
|
|
151
|
-
StatTypes,
|
|
152
|
-
applyKaiserWindow,
|
|
153
|
-
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypnosound",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.10.2",
|
|
5
5
|
"description": "A small library for analyzing audio",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"format": "eslint --fix ."
|
|
8
|
+
"start": "python -m http.server 1337",
|
|
9
|
+
"build": "bun scripts/generate-barrel.mjs"
|
|
11
10
|
},
|
|
12
11
|
"repository": {
|
|
13
12
|
"type": "git",
|
|
@@ -23,10 +22,5 @@
|
|
|
23
22
|
"bugs": {
|
|
24
23
|
"url": "https://github.com/hypnodroid/hypnosound/issues"
|
|
25
24
|
},
|
|
26
|
-
"homepage": "https://github.com/hypnodroid/hypnosound#readme"
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"eslint": "^8.57.0",
|
|
29
|
-
"eslint-plugin-prettier": "^5.1.3",
|
|
30
|
-
"live-server": "^1.2.2"
|
|
31
|
-
}
|
|
25
|
+
"homepage": "https://github.com/hypnodroid/hypnosound#readme"
|
|
32
26
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { readdir } from 'node:fs/promises'
|
|
3
|
+
import { basename } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const main = async (dir = './src/audio', out = './src/audio') => {
|
|
6
|
+
const functionNamesAndPaths = (await readdir(dir, { recursive: true }))
|
|
7
|
+
.filter((file) => file.endsWith('.js'))
|
|
8
|
+
.filter((file) => file !== 'index.js')
|
|
9
|
+
.map((file) => file.replace(dir + '/', ''))
|
|
10
|
+
.map((file) => file.replace(/\\/g, '/'))
|
|
11
|
+
.map((file) => {
|
|
12
|
+
const name = basename(file, '.js')
|
|
13
|
+
const path = file.replace(/\\/g, '/')
|
|
14
|
+
return { name, path }
|
|
15
|
+
})
|
|
16
|
+
Bun.write(`${out}/index.js`, exportTemplate(functionNamesAndPaths))
|
|
17
|
+
}
|
|
18
|
+
const exportTemplate = (functionNamesAndPaths) => {
|
|
19
|
+
return `${functionNamesAndPaths.map((fn) => `export { default as ${fn.name} } from "./${fn.path}"`).join('\n')}
|
|
20
|
+
export const AudioFeatures = ${JSON.stringify(
|
|
21
|
+
functionNamesAndPaths.map(({ name }) => name),
|
|
22
|
+
null,
|
|
23
|
+
2,
|
|
24
|
+
)}`.trim()
|
|
25
|
+
}
|
|
26
|
+
main()
|
package/src/audio/index.js
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
export
|
|
10
|
-
export
|
|
11
|
-
export
|
|
12
|
-
export
|
|
13
|
-
export
|
|
14
|
-
export
|
|
1
|
+
export { default as bass } from "./bass.js"
|
|
2
|
+
export { default as energy } from "./energy.js"
|
|
3
|
+
export { default as mids } from "./mids.js"
|
|
4
|
+
export { default as pitchClass } from "./pitchClass.js"
|
|
5
|
+
export { default as spectralCentroid } from "./spectralCentroid.js"
|
|
6
|
+
export { default as spectralCrest } from "./spectralCrest.js"
|
|
7
|
+
export { default as spectralEntropy } from "./spectralEntropy.js"
|
|
8
|
+
export { default as spectralFlux } from "./spectralFlux.js"
|
|
9
|
+
export { default as spectralKurtosis } from "./spectralKurtosis.js"
|
|
10
|
+
export { default as spectralRolloff } from "./spectralRolloff.js"
|
|
11
|
+
export { default as spectralRoughness } from "./spectralRoughness.js"
|
|
12
|
+
export { default as spectralSkew } from "./spectralSkew.js"
|
|
13
|
+
export { default as spectralSpread } from "./spectralSpread.js"
|
|
14
|
+
export { default as treble } from "./treble.js"
|
|
15
|
+
export const AudioFeatures = [
|
|
16
|
+
"bass",
|
|
17
|
+
"energy",
|
|
18
|
+
"mids",
|
|
19
|
+
"pitchClass",
|
|
20
|
+
"spectralCentroid",
|
|
21
|
+
"spectralCrest",
|
|
22
|
+
"spectralEntropy",
|
|
23
|
+
"spectralFlux",
|
|
24
|
+
"spectralKurtosis",
|
|
25
|
+
"spectralRolloff",
|
|
26
|
+
"spectralRoughness",
|
|
27
|
+
"spectralSkew",
|
|
28
|
+
"spectralSpread",
|
|
29
|
+
"treble"
|
|
30
|
+
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const StatTypes = ['normalized', 'mean', 'median', 'standardDeviation', 'zScore', 'min', 'max']
|
|
1
|
+
export const StatTypes = ['normalized', 'mean', 'median', 'standardDeviation', 'zScore', 'min', 'max', 'slope', 'intercept', 'rSquared']
|
|
2
2
|
|
|
3
3
|
const erf = (x) => {
|
|
4
4
|
const a1 = 0.254829592
|
|
@@ -150,6 +150,44 @@ export const makeCalculateStats = (historySize = 500) => {
|
|
|
150
150
|
return lowerHalf[0]
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// Calculate linear regression using least squares method
|
|
154
|
+
// Uses indices 0 to n-1 as x values, queue values as y values
|
|
155
|
+
const calculateLinearRegression = (mean) => {
|
|
156
|
+
const n = queue.length
|
|
157
|
+
if (n < 2) return { slope: 0, intercept: mean, rSquared: 0 }
|
|
158
|
+
|
|
159
|
+
// For x = 0, 1, 2, ..., n-1:
|
|
160
|
+
// sumX = n*(n-1)/2
|
|
161
|
+
// sumXX = n*(n-1)*(2n-1)/6
|
|
162
|
+
const sumX = (n * (n - 1)) / 2
|
|
163
|
+
const sumXX = (n * (n - 1) * (2 * n - 1)) / 6
|
|
164
|
+
|
|
165
|
+
// Calculate sumXY = sum of (i * queue[i])
|
|
166
|
+
let sumXYCalc = 0
|
|
167
|
+
for (let i = 0; i < n; i++) {
|
|
168
|
+
sumXYCalc += i * queue[i]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const denominator = n * sumXX - sumX * sumX
|
|
172
|
+
if (denominator === 0) return { slope: 0, intercept: mean, rSquared: 0 }
|
|
173
|
+
|
|
174
|
+
const slope = (n * sumXYCalc - sumX * sum) / denominator
|
|
175
|
+
const intercept = (sum - slope * sumX) / n
|
|
176
|
+
|
|
177
|
+
// Calculate R-squared (coefficient of determination)
|
|
178
|
+
let ssRes = 0 // Sum of squared residuals
|
|
179
|
+
let ssTot = 0 // Total sum of squares
|
|
180
|
+
for (let i = 0; i < n; i++) {
|
|
181
|
+
const predicted = slope * i + intercept
|
|
182
|
+
ssRes += (queue[i] - predicted) ** 2
|
|
183
|
+
ssTot += (queue[i] - mean) ** 2
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const rSquared = ssTot === 0 ? 1 : 1 - ssRes / ssTot
|
|
187
|
+
|
|
188
|
+
return { slope, intercept, rSquared }
|
|
189
|
+
}
|
|
190
|
+
|
|
153
191
|
const calculate = (value) => {
|
|
154
192
|
if (typeof value !== 'number' || isNaN(value)) throw new Error('Input must be a valid number')
|
|
155
193
|
|
|
@@ -175,6 +213,8 @@ export const makeCalculateStats = (historySize = 500) => {
|
|
|
175
213
|
const min = minQueue.peek() || 0
|
|
176
214
|
const max = maxQueue.peek() || 0
|
|
177
215
|
|
|
216
|
+
const regression = calculateLinearRegression(mean)
|
|
217
|
+
|
|
178
218
|
if (max === min) {
|
|
179
219
|
return {
|
|
180
220
|
current: value,
|
|
@@ -185,6 +225,9 @@ export const makeCalculateStats = (historySize = 500) => {
|
|
|
185
225
|
mean,
|
|
186
226
|
min,
|
|
187
227
|
max,
|
|
228
|
+
slope: regression.slope,
|
|
229
|
+
intercept: regression.intercept,
|
|
230
|
+
rSquared: regression.rSquared,
|
|
188
231
|
}
|
|
189
232
|
}
|
|
190
233
|
|
|
@@ -198,6 +241,9 @@ export const makeCalculateStats = (historySize = 500) => {
|
|
|
198
241
|
mean,
|
|
199
242
|
min,
|
|
200
243
|
max,
|
|
244
|
+
slope: regression.slope,
|
|
245
|
+
intercept: regression.intercept,
|
|
246
|
+
rSquared: regression.rSquared,
|
|
201
247
|
}
|
|
202
248
|
}
|
|
203
249
|
|