@iam-protocol/pulse-sdk 0.2.5 → 0.3.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/dist/index.d.mts +52 -2
- package/dist/index.d.ts +52 -2
- package/dist/index.js +546 -68
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +541 -68
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/config.ts +1 -1
- package/src/extraction/kinematic.ts +171 -1
- package/src/extraction/lpc.ts +215 -0
- package/src/extraction/speaker.ts +361 -0
- package/src/hashing/simhash.ts +1 -1
- package/src/index.ts +2 -0
- package/src/pulse.ts +16 -5
- package/test/integration.test.ts +2 -2
- package/src/extraction/mfcc.ts +0 -113
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iam-protocol/pulse-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Client-side SDK for IAM Protocol — sensor capture, TBH generation, ZK proof construction",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"circomlibjs": "^0.1.7",
|
|
23
23
|
"meyda": "^5.6.3",
|
|
24
|
+
"pitchfinder": "^2.3.4",
|
|
24
25
|
"snarkjs": "^0.7.6"
|
|
25
26
|
},
|
|
26
27
|
"peerDependencies": {
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MotionSample, TouchSample } from "../sensor/types";
|
|
2
|
-
import { condense, variance } from "./statistics";
|
|
2
|
+
import { condense, variance, entropy } from "./statistics";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Extract kinematic features from motion (IMU) data.
|
|
@@ -122,3 +122,173 @@ function derivative(values: number[]): number[] {
|
|
|
122
122
|
}
|
|
123
123
|
return d;
|
|
124
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Extract mouse dynamics features as a desktop replacement for motion sensor data.
|
|
128
|
+
* Captures behavioral patterns from mouse/pointer movement that are user-specific:
|
|
129
|
+
* path curvature, speed patterns, micro-corrections, pause behavior.
|
|
130
|
+
*
|
|
131
|
+
* Returns: 54 values (matches motion feature dimension for consistent SimHash input)
|
|
132
|
+
*/
|
|
133
|
+
export function extractMouseDynamics(samples: TouchSample[]): number[] {
|
|
134
|
+
if (samples.length < 10) return new Array(54).fill(0);
|
|
135
|
+
|
|
136
|
+
const x = samples.map((s) => s.x);
|
|
137
|
+
const y = samples.map((s) => s.y);
|
|
138
|
+
const pressure = samples.map((s) => s.pressure);
|
|
139
|
+
const area = samples.map((s) => s.width * s.height);
|
|
140
|
+
|
|
141
|
+
// Velocity
|
|
142
|
+
const vx = derivative(x);
|
|
143
|
+
const vy = derivative(y);
|
|
144
|
+
const speed = vx.map((dx, i) => Math.sqrt(dx * dx + (vy[i] ?? 0) * (vy[i] ?? 0)));
|
|
145
|
+
|
|
146
|
+
// Acceleration
|
|
147
|
+
const accX = derivative(vx);
|
|
148
|
+
const accY = derivative(vy);
|
|
149
|
+
const acc = accX.map((ax, i) => Math.sqrt(ax * ax + (accY[i] ?? 0) * (accY[i] ?? 0)));
|
|
150
|
+
|
|
151
|
+
// Jerk (derivative of acceleration)
|
|
152
|
+
const jerkX = derivative(accX);
|
|
153
|
+
const jerkY = derivative(accY);
|
|
154
|
+
const jerk = jerkX.map((jx, i) => Math.sqrt(jx * jx + (jerkY[i] ?? 0) * (jerkY[i] ?? 0)));
|
|
155
|
+
|
|
156
|
+
// Path curvature: angle change between consecutive movement vectors
|
|
157
|
+
const curvatures: number[] = [];
|
|
158
|
+
for (let i = 1; i < vx.length; i++) {
|
|
159
|
+
const angle1 = Math.atan2(vy[i - 1] ?? 0, vx[i - 1] ?? 0);
|
|
160
|
+
const angle2 = Math.atan2(vy[i] ?? 0, vx[i] ?? 0);
|
|
161
|
+
let diff = angle2 - angle1;
|
|
162
|
+
while (diff > Math.PI) diff -= 2 * Math.PI;
|
|
163
|
+
while (diff < -Math.PI) diff += 2 * Math.PI;
|
|
164
|
+
curvatures.push(Math.abs(diff));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Movement directions for directional entropy
|
|
168
|
+
const directions = vx.map((dx, i) => Math.atan2(vy[i] ?? 0, dx));
|
|
169
|
+
|
|
170
|
+
// Micro-corrections: direction reversals
|
|
171
|
+
let reversals = 0;
|
|
172
|
+
for (let i = 2; i < directions.length; i++) {
|
|
173
|
+
const d1 = directions[i - 1]! - directions[i - 2]!;
|
|
174
|
+
const d2 = directions[i]! - directions[i - 1]!;
|
|
175
|
+
if (d1 * d2 < 0) reversals++;
|
|
176
|
+
}
|
|
177
|
+
const reversalRate = directions.length > 2 ? reversals / (directions.length - 2) : 0;
|
|
178
|
+
const reversalMagnitude = curvatures.length > 0
|
|
179
|
+
? curvatures.reduce((a, b) => a + b, 0) / curvatures.length
|
|
180
|
+
: 0;
|
|
181
|
+
|
|
182
|
+
// Pause detection: frames where speed is near zero
|
|
183
|
+
const speedThreshold = 0.5;
|
|
184
|
+
const pauseFrames = speed.filter((s) => s < speedThreshold).length;
|
|
185
|
+
const pauseRatio = speed.length > 0 ? pauseFrames / speed.length : 0;
|
|
186
|
+
|
|
187
|
+
// Path efficiency: straight-line distance / total path length
|
|
188
|
+
const totalPathLength = speed.reduce((a, b) => a + b, 0);
|
|
189
|
+
const straightLine = Math.sqrt(
|
|
190
|
+
(x[x.length - 1]! - x[0]!) ** 2 + (y[y.length - 1]! - y[0]!) ** 2
|
|
191
|
+
);
|
|
192
|
+
const pathEfficiency = totalPathLength > 0 ? straightLine / totalPathLength : 0;
|
|
193
|
+
|
|
194
|
+
// Movement durations between pauses
|
|
195
|
+
const movementDurations: number[] = [];
|
|
196
|
+
let currentDuration = 0;
|
|
197
|
+
for (const s of speed) {
|
|
198
|
+
if (s >= speedThreshold) {
|
|
199
|
+
currentDuration++;
|
|
200
|
+
} else if (currentDuration > 0) {
|
|
201
|
+
movementDurations.push(currentDuration);
|
|
202
|
+
currentDuration = 0;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (currentDuration > 0) movementDurations.push(currentDuration);
|
|
206
|
+
|
|
207
|
+
// Segment lengths between direction changes
|
|
208
|
+
const segmentLengths: number[] = [];
|
|
209
|
+
let segLen = 0;
|
|
210
|
+
for (let i = 1; i < directions.length; i++) {
|
|
211
|
+
segLen += speed[i] ?? 0;
|
|
212
|
+
const angleDiff = Math.abs(directions[i]! - directions[i - 1]!);
|
|
213
|
+
if (angleDiff > Math.PI / 4) {
|
|
214
|
+
segmentLengths.push(segLen);
|
|
215
|
+
segLen = 0;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (segLen > 0) segmentLengths.push(segLen);
|
|
219
|
+
|
|
220
|
+
// Windowed jitter variance of speed
|
|
221
|
+
const windowSize = Math.max(5, Math.floor(speed.length / 4));
|
|
222
|
+
const windowVariances: number[] = [];
|
|
223
|
+
for (let i = 0; i + windowSize <= speed.length; i += windowSize) {
|
|
224
|
+
const window = speed.slice(i, i + windowSize);
|
|
225
|
+
windowVariances.push(variance(window));
|
|
226
|
+
}
|
|
227
|
+
const speedJitter = windowVariances.length > 1 ? variance(windowVariances) : 0;
|
|
228
|
+
|
|
229
|
+
// Path length normalized by capture duration
|
|
230
|
+
const duration = samples.length > 1
|
|
231
|
+
? (samples[samples.length - 1]!.timestamp - samples[0]!.timestamp) / 1000
|
|
232
|
+
: 1;
|
|
233
|
+
const normalizedPathLength = totalPathLength / Math.max(duration, 0.001);
|
|
234
|
+
|
|
235
|
+
// Angle autocorrelation at lags 1, 2, 3
|
|
236
|
+
const angleAutoCorr: number[] = [];
|
|
237
|
+
for (let lag = 1; lag <= 3; lag++) {
|
|
238
|
+
if (directions.length <= lag) {
|
|
239
|
+
angleAutoCorr.push(0);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const n = directions.length - lag;
|
|
243
|
+
const meanDir = directions.reduce((a, b) => a + b, 0) / directions.length;
|
|
244
|
+
let num = 0;
|
|
245
|
+
let den = 0;
|
|
246
|
+
for (let i = 0; i < n; i++) {
|
|
247
|
+
num += (directions[i]! - meanDir) * (directions[i + lag]! - meanDir);
|
|
248
|
+
den += (directions[i]! - meanDir) ** 2;
|
|
249
|
+
}
|
|
250
|
+
angleAutoCorr.push(den > 0 ? num / den : 0);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Assemble 54 features
|
|
254
|
+
const curvatureStats = condense(curvatures); // 4
|
|
255
|
+
const dirEntropy = entropy(directions, 16); // 1
|
|
256
|
+
const speedStats = condense(speed); // 4
|
|
257
|
+
const accStats = condense(acc); // 4
|
|
258
|
+
// micro-corrections: reversalRate + reversalMagnitude // 2
|
|
259
|
+
// pauseRatio // 1
|
|
260
|
+
// pathEfficiency // 1
|
|
261
|
+
// speedJitter // 1
|
|
262
|
+
const jerkStats = condense(jerk); // 4
|
|
263
|
+
const vxStats = condense(vx); // 4
|
|
264
|
+
const vyStats = condense(vy); // 4
|
|
265
|
+
const accXStats = condense(accX); // 4
|
|
266
|
+
const accYStats = condense(accY); // 4
|
|
267
|
+
const pressureStats = condense(pressure); // 4
|
|
268
|
+
const moveDurStats = condense(movementDurations); // 4
|
|
269
|
+
const segLenStats = condense(segmentLengths); // 4
|
|
270
|
+
// angleAutoCorr[0..2] // 3
|
|
271
|
+
// normalizedPathLength // 1
|
|
272
|
+
// Total: 4+1+4+4+2+1+1+1+4+4+4+4+4+4+4+4+3+1 = 54
|
|
273
|
+
|
|
274
|
+
return [
|
|
275
|
+
curvatureStats.mean, curvatureStats.variance, curvatureStats.skewness, curvatureStats.kurtosis,
|
|
276
|
+
dirEntropy,
|
|
277
|
+
speedStats.mean, speedStats.variance, speedStats.skewness, speedStats.kurtosis,
|
|
278
|
+
accStats.mean, accStats.variance, accStats.skewness, accStats.kurtosis,
|
|
279
|
+
reversalRate, reversalMagnitude,
|
|
280
|
+
pauseRatio,
|
|
281
|
+
pathEfficiency,
|
|
282
|
+
speedJitter,
|
|
283
|
+
jerkStats.mean, jerkStats.variance, jerkStats.skewness, jerkStats.kurtosis,
|
|
284
|
+
vxStats.mean, vxStats.variance, vxStats.skewness, vxStats.kurtosis,
|
|
285
|
+
vyStats.mean, vyStats.variance, vyStats.skewness, vyStats.kurtosis,
|
|
286
|
+
accXStats.mean, accXStats.variance, accXStats.skewness, accXStats.kurtosis,
|
|
287
|
+
accYStats.mean, accYStats.variance, accYStats.skewness, accYStats.kurtosis,
|
|
288
|
+
pressureStats.mean, pressureStats.variance, pressureStats.skewness, pressureStats.kurtosis,
|
|
289
|
+
moveDurStats.mean, moveDurStats.variance, moveDurStats.skewness, moveDurStats.kurtosis,
|
|
290
|
+
segLenStats.mean, segLenStats.variance, segLenStats.skewness, segLenStats.kurtosis,
|
|
291
|
+
angleAutoCorr[0] ?? 0, angleAutoCorr[1] ?? 0, angleAutoCorr[2] ?? 0,
|
|
292
|
+
normalizedPathLength,
|
|
293
|
+
];
|
|
294
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Predictive Coding (LPC) for formant detection.
|
|
3
|
+
*
|
|
4
|
+
* Implements Levinson-Durbin recursion for LPC coefficient computation
|
|
5
|
+
* and polynomial root-finding for formant frequency estimation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compute autocorrelation of a signal for lags 0..order.
|
|
10
|
+
*/
|
|
11
|
+
function autocorrelate(signal: Float32Array, order: number): number[] {
|
|
12
|
+
const r: number[] = [];
|
|
13
|
+
for (let lag = 0; lag <= order; lag++) {
|
|
14
|
+
let sum = 0;
|
|
15
|
+
for (let i = 0; i < signal.length - lag; i++) {
|
|
16
|
+
sum += signal[i]! * signal[i + lag]!;
|
|
17
|
+
}
|
|
18
|
+
r.push(sum);
|
|
19
|
+
}
|
|
20
|
+
return r;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Levinson-Durbin recursion to compute LPC coefficients from autocorrelation.
|
|
25
|
+
* Returns the LPC coefficients a[1..order] (a[0] is implicitly 1).
|
|
26
|
+
*/
|
|
27
|
+
function levinsonDurbin(r: number[], order: number): number[] {
|
|
28
|
+
const a: number[] = new Array(order + 1).fill(0);
|
|
29
|
+
const aTemp: number[] = new Array(order + 1).fill(0);
|
|
30
|
+
a[0] = 1;
|
|
31
|
+
|
|
32
|
+
let error = r[0]!;
|
|
33
|
+
if (error === 0) return new Array(order).fill(0);
|
|
34
|
+
|
|
35
|
+
for (let i = 1; i <= order; i++) {
|
|
36
|
+
let lambda = 0;
|
|
37
|
+
for (let j = 1; j < i; j++) {
|
|
38
|
+
lambda += a[j]! * r[i - j]!;
|
|
39
|
+
}
|
|
40
|
+
lambda = -(r[i]! + lambda) / error;
|
|
41
|
+
|
|
42
|
+
for (let j = 1; j < i; j++) {
|
|
43
|
+
aTemp[j] = a[j]! + lambda * a[i - j]!;
|
|
44
|
+
}
|
|
45
|
+
aTemp[i] = lambda;
|
|
46
|
+
|
|
47
|
+
for (let j = 1; j <= i; j++) {
|
|
48
|
+
a[j] = aTemp[j]!;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
error *= 1 - lambda * lambda;
|
|
52
|
+
if (error <= 0) break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return a.slice(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Find roots of a polynomial using the Durand-Kerner method.
|
|
60
|
+
* The polynomial is 1 + a[0]*z^-1 + a[1]*z^-2 + ... + a[n-1]*z^-n
|
|
61
|
+
* which is equivalent to z^n + a[0]*z^(n-1) + ... + a[n-1] = 0.
|
|
62
|
+
*
|
|
63
|
+
* Returns complex roots as [real, imag] pairs.
|
|
64
|
+
*/
|
|
65
|
+
function findRoots(coefficients: number[], maxIterations: number = 50): [number, number][] {
|
|
66
|
+
const n = coefficients.length;
|
|
67
|
+
if (n === 0) return [];
|
|
68
|
+
|
|
69
|
+
// Initial guesses: points on a circle of radius 0.9
|
|
70
|
+
const roots: [number, number][] = [];
|
|
71
|
+
for (let i = 0; i < n; i++) {
|
|
72
|
+
const angle = (2 * Math.PI * i) / n + 0.1;
|
|
73
|
+
roots.push([0.9 * Math.cos(angle), 0.9 * Math.sin(angle)]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
77
|
+
let maxShift = 0;
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < n; i++) {
|
|
80
|
+
// Evaluate polynomial at roots[i]: z^n + a[0]*z^(n-1) + ... + a[n-1]
|
|
81
|
+
let pReal = 1;
|
|
82
|
+
let pImag = 0;
|
|
83
|
+
let zPowReal = 1;
|
|
84
|
+
let zPowImag = 0;
|
|
85
|
+
|
|
86
|
+
// Compute z^n by repeated multiplication
|
|
87
|
+
const [rr, ri] = roots[i]!;
|
|
88
|
+
let curReal = 1;
|
|
89
|
+
let curImag = 0;
|
|
90
|
+
|
|
91
|
+
// Evaluate as: z^n + sum(a[k] * z^(n-1-k))
|
|
92
|
+
// Start with z^n
|
|
93
|
+
let znReal = 1;
|
|
94
|
+
let znImag = 0;
|
|
95
|
+
for (let k = 0; k < n; k++) {
|
|
96
|
+
const newReal = znReal * rr - znImag * ri;
|
|
97
|
+
const newImag = znReal * ri + znImag * rr;
|
|
98
|
+
znReal = newReal;
|
|
99
|
+
znImag = newImag;
|
|
100
|
+
}
|
|
101
|
+
pReal = znReal;
|
|
102
|
+
pImag = znImag;
|
|
103
|
+
|
|
104
|
+
// Add coefficient terms: a[k] * z^(n-1-k)
|
|
105
|
+
zPowReal = 1;
|
|
106
|
+
zPowImag = 0;
|
|
107
|
+
for (let k = n - 1; k >= 0; k--) {
|
|
108
|
+
pReal += coefficients[k]! * zPowReal;
|
|
109
|
+
pImag += coefficients[k]! * zPowImag;
|
|
110
|
+
const newReal = zPowReal * rr - zPowImag * ri;
|
|
111
|
+
const newImag = zPowReal * ri + zPowImag * rr;
|
|
112
|
+
zPowReal = newReal;
|
|
113
|
+
zPowImag = newImag;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Compute product of (roots[i] - roots[j]) for j != i
|
|
117
|
+
let denomReal = 1;
|
|
118
|
+
let denomImag = 0;
|
|
119
|
+
for (let j = 0; j < n; j++) {
|
|
120
|
+
if (j === i) continue;
|
|
121
|
+
const diffReal = rr - roots[j]![0];
|
|
122
|
+
const diffImag = ri - roots[j]![1];
|
|
123
|
+
const newReal = denomReal * diffReal - denomImag * diffImag;
|
|
124
|
+
const newImag = denomReal * diffImag + denomImag * diffReal;
|
|
125
|
+
denomReal = newReal;
|
|
126
|
+
denomImag = newImag;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Divide p / denom
|
|
130
|
+
const denomMag2 = denomReal * denomReal + denomImag * denomImag;
|
|
131
|
+
if (denomMag2 < 1e-30) continue;
|
|
132
|
+
|
|
133
|
+
const shiftReal = (pReal * denomReal + pImag * denomImag) / denomMag2;
|
|
134
|
+
const shiftImag = (pImag * denomReal - pReal * denomImag) / denomMag2;
|
|
135
|
+
|
|
136
|
+
roots[i] = [rr - shiftReal, ri - shiftImag];
|
|
137
|
+
maxShift = Math.max(maxShift, Math.sqrt(shiftReal * shiftReal + shiftImag * shiftImag));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (maxShift < 1e-10) break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return roots;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Extract formant frequencies (F1, F2, F3) from a single audio frame.
|
|
148
|
+
* Returns [F1, F2, F3] in Hz, or null if extraction fails.
|
|
149
|
+
*/
|
|
150
|
+
function extractFormants(
|
|
151
|
+
frame: Float32Array,
|
|
152
|
+
sampleRate: number,
|
|
153
|
+
lpcOrder: number = 12
|
|
154
|
+
): [number, number, number] | null {
|
|
155
|
+
const r = autocorrelate(frame, lpcOrder);
|
|
156
|
+
const coeffs = levinsonDurbin(r, lpcOrder);
|
|
157
|
+
|
|
158
|
+
const roots = findRoots(coeffs);
|
|
159
|
+
|
|
160
|
+
// Convert roots to frequencies, keep only positive-frequency roots
|
|
161
|
+
const formantCandidates: number[] = [];
|
|
162
|
+
|
|
163
|
+
for (const [real, imag] of roots) {
|
|
164
|
+
if (imag <= 0) continue; // Keep only positive-frequency roots
|
|
165
|
+
|
|
166
|
+
const freq = (Math.atan2(imag, real) / (2 * Math.PI)) * sampleRate;
|
|
167
|
+
const bandwidth = (-sampleRate / (2 * Math.PI)) * Math.log(Math.sqrt(real * real + imag * imag));
|
|
168
|
+
|
|
169
|
+
// Filter: formants are in 200-5000Hz range with reasonable bandwidth
|
|
170
|
+
if (freq > 200 && freq < 5000 && bandwidth < 500) {
|
|
171
|
+
formantCandidates.push(freq);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
formantCandidates.sort((a, b) => a - b);
|
|
176
|
+
|
|
177
|
+
if (formantCandidates.length < 3) return null;
|
|
178
|
+
|
|
179
|
+
return [formantCandidates[0]!, formantCandidates[1]!, formantCandidates[2]!];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Extract formant ratio time series (F1/F2 and F2/F3) from audio.
|
|
184
|
+
* Returns { f1f2: number[], f2f3: number[] } — one ratio per frame where formants were detected.
|
|
185
|
+
*/
|
|
186
|
+
export function extractFormantRatios(
|
|
187
|
+
samples: Float32Array,
|
|
188
|
+
sampleRate: number,
|
|
189
|
+
frameSize: number,
|
|
190
|
+
hopSize: number
|
|
191
|
+
): { f1f2: number[]; f2f3: number[] } {
|
|
192
|
+
const f1f2: number[] = [];
|
|
193
|
+
const f2f3: number[] = [];
|
|
194
|
+
const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < numFrames; i++) {
|
|
197
|
+
const start = i * hopSize;
|
|
198
|
+
const frame = samples.slice(start, start + frameSize);
|
|
199
|
+
|
|
200
|
+
// Apply Hamming window
|
|
201
|
+
const windowed = new Float32Array(frameSize);
|
|
202
|
+
for (let j = 0; j < frameSize; j++) {
|
|
203
|
+
windowed[j] = (frame[j] ?? 0) * (0.54 - 0.46 * Math.cos((2 * Math.PI * j) / (frameSize - 1)));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const formants = extractFormants(windowed, sampleRate);
|
|
207
|
+
if (formants) {
|
|
208
|
+
const [f1, f2, f3] = formants;
|
|
209
|
+
if (f2 > 0) f1f2.push(f1 / f2);
|
|
210
|
+
if (f3 > 0) f2f3.push(f2 / f3);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { f1f2, f2f3 };
|
|
215
|
+
}
|