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.
@@ -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
- import { applyKaiserWindow } from './src/utils/applyKaiserWindow.js'
3
- import energy from './src/audio/energy.js'
4
- import spectralCentroid from './src/audio/spectralCentroid.js'
5
- import spectralCrest from './src/audio/spectralCrest.js'
6
- import spectralEntropy from './src/audio/spectralEntropy.js'
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
- // aah, state management
20
- this.statCalculators = {}
21
- this.previousValue = {}
22
-
23
- this.statCalculators.energy = makeCalculateStats()
24
-
25
- this.statCalculators.spectralCentroid = makeCalculateStats()
26
-
27
- this.statCalculators.spectralCrest = makeCalculateStats()
28
-
29
- this.statCalculators.spectralEntropy = makeCalculateStats()
30
-
31
- this.statCalculators.spectralFlux = makeCalculateStats()
32
- this.previousValue.spectralFlux = null
33
-
34
- this.statCalculators.spectralKurtosis = makeCalculateStats()
35
-
36
- this.statCalculators.spectralRolloff = makeCalculateStats()
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.8.1",
4
+ "version": "1.10.2",
5
5
  "description": "A small library for analyzing audio",
6
6
  "main": "index.js",
7
7
  "scripts": {
8
- "test": "echo \"Error: no test specified\" && exit 1",
9
- "start": "live-server .",
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()
@@ -1,14 +1,30 @@
1
- export * as energy from './energy'
2
- export * as spectralCentroid from './spectralCentroid'
3
- export * as spectralCrest from './spectralCrest'
4
- export * as spectralEntropy from './spectralEntropy'
5
- export * as spectralFlux from './spectralFlux'
6
- export * as spectralKurtosis from './spectralKurtosis'
7
- export * as spectralRolloff from './spectralRolloff'
8
- export * as spectralRoughness from './spectralRoughness'
9
- export * as spectralSkew from './spectralSkew'
10
- export * as spectralSpread from './spectralSpread'
11
- export * as pitchClass from './pitchClass'
12
- export * as bass from './bass'
13
- export * as treble from './treble'
14
- export * as mids from './mids'
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