auditok 0.1.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/LICENSE +21 -0
- package/README.md +236 -0
- package/dist/browser/index.d.ts +44 -0
- package/dist/browser/index.js +118 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/node/ffmpeg.d.ts +47 -0
- package/dist/node/ffmpeg.js +117 -0
- package/dist/node/index.d.ts +32 -0
- package/dist/node/index.js +54 -0
- package/dist/region.d.ts +31 -0
- package/dist/region.js +81 -0
- package/dist/signal.d.ts +63 -0
- package/dist/signal.js +240 -0
- package/dist/split.d.ts +121 -0
- package/dist/split.js +414 -0
- package/dist/tokenizer.d.ts +82 -0
- package/dist/tokenizer.js +312 -0
- package/dist/wav.d.ts +23 -0
- package/dist/wav.js +141 -0
- package/package.json +71 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamTokenizer: the event-shaping state machine at the heart of
|
|
3
|
+
* auditok, ported line for line from the Python implementation. It is
|
|
4
|
+
* generic over the frame type — frames can be analysis windows of
|
|
5
|
+
* audio, characters, booleans from another VAD's frame decisions —
|
|
6
|
+
* and turns per-frame valid/invalid decisions into tokens with
|
|
7
|
+
* min/max length and silence semantics.
|
|
8
|
+
*/
|
|
9
|
+
const SILENCE = 0;
|
|
10
|
+
const POSSIBLE_SILENCE = 1;
|
|
11
|
+
const POSSIBLE_NOISE = 2;
|
|
12
|
+
const NOISE = 3;
|
|
13
|
+
const TRAILING_COLLECTION = 4;
|
|
14
|
+
export class StreamTokenizer {
|
|
15
|
+
constructor(validator, options) {
|
|
16
|
+
this.state = SILENCE;
|
|
17
|
+
this.data = [];
|
|
18
|
+
this.leadingBuffer = [];
|
|
19
|
+
this.contiguousToken = false;
|
|
20
|
+
this.initCount = 0;
|
|
21
|
+
this.silenceLength = 0;
|
|
22
|
+
this.startFrame = 0;
|
|
23
|
+
this.currentFrame = -1;
|
|
24
|
+
if (typeof validator === "function") {
|
|
25
|
+
this.isValid = validator;
|
|
26
|
+
}
|
|
27
|
+
else if (validator && typeof validator.isValid === "function") {
|
|
28
|
+
this.isValid = validator.isValid.bind(validator);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
throw new TypeError("'validator' must be a function or an object with an isValid method");
|
|
32
|
+
}
|
|
33
|
+
const { minLength, maxLength, maxContinuousSilence, maxLeadingSilence = 0, maxTrailingSilence = null, strictMinLength = false, initMin = 0, initMaxSilence = 0, } = options;
|
|
34
|
+
if (maxLength <= 0) {
|
|
35
|
+
throw new RangeError(`'maxLength' must be > 0 (value=${maxLength})`);
|
|
36
|
+
}
|
|
37
|
+
if (minLength <= 0 || minLength > maxLength) {
|
|
38
|
+
throw new RangeError(`'minLength' must be > 0 and <= 'maxLength' (value=${minLength})`);
|
|
39
|
+
}
|
|
40
|
+
if (maxContinuousSilence >= maxLength) {
|
|
41
|
+
throw new RangeError(`'maxContinuousSilence' must be < 'maxLength' ` +
|
|
42
|
+
`(value=${maxContinuousSilence})`);
|
|
43
|
+
}
|
|
44
|
+
if (initMin >= maxLength) {
|
|
45
|
+
throw new RangeError(`'initMin' must be < 'maxLength' (value=${initMin})`);
|
|
46
|
+
}
|
|
47
|
+
this.minLength = minLength;
|
|
48
|
+
this.maxLength = maxLength;
|
|
49
|
+
this.maxContinuousSilence = maxContinuousSilence;
|
|
50
|
+
this.maxLeadingSilence = maxLeadingSilence;
|
|
51
|
+
this.maxTrailingSilence = maxTrailingSilence;
|
|
52
|
+
this.strictMinLength = strictMinLength;
|
|
53
|
+
this.initMin = initMin;
|
|
54
|
+
this.initMaxSilence = initMaxSilence;
|
|
55
|
+
this.needsTrailingExtension =
|
|
56
|
+
maxTrailingSilence !== null &&
|
|
57
|
+
maxTrailingSilence > maxContinuousSilence;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Tokenize `source` frame by frame, yielding tokens as soon as their
|
|
61
|
+
* end is decided. Accepts both sync and async iterables.
|
|
62
|
+
*/
|
|
63
|
+
async *tokenize(source) {
|
|
64
|
+
this.reinitialize();
|
|
65
|
+
for await (const frame of source) {
|
|
66
|
+
this.currentFrame += 1;
|
|
67
|
+
const token = this.process(frame);
|
|
68
|
+
if (token !== undefined) {
|
|
69
|
+
yield token;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
this.currentFrame += 1;
|
|
73
|
+
const token = this.postProcess();
|
|
74
|
+
if (token !== undefined) {
|
|
75
|
+
yield token;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Convenience wrapper collecting all tokens into an array. */
|
|
79
|
+
async tokenizeAll(source) {
|
|
80
|
+
const tokens = [];
|
|
81
|
+
for await (const token of this.tokenize(source)) {
|
|
82
|
+
tokens.push(token);
|
|
83
|
+
}
|
|
84
|
+
return tokens;
|
|
85
|
+
}
|
|
86
|
+
reinitialize() {
|
|
87
|
+
this.contiguousToken = false;
|
|
88
|
+
this.data = [];
|
|
89
|
+
this.state = SILENCE;
|
|
90
|
+
this.currentFrame = -1;
|
|
91
|
+
this.initCount = 0;
|
|
92
|
+
this.silenceLength = 0;
|
|
93
|
+
this.startFrame = 0;
|
|
94
|
+
this.leadingBuffer = [];
|
|
95
|
+
}
|
|
96
|
+
pushLeading(frame) {
|
|
97
|
+
if (this.maxLeadingSilence <= 0) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.leadingBuffer.push(frame);
|
|
101
|
+
if (this.leadingBuffer.length > this.maxLeadingSilence) {
|
|
102
|
+
this.leadingBuffer.shift();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
startToken(frame) {
|
|
106
|
+
const leading = this.leadingBuffer;
|
|
107
|
+
this.leadingBuffer = [];
|
|
108
|
+
this.initCount = 1;
|
|
109
|
+
this.silenceLength = 0;
|
|
110
|
+
this.startFrame = this.currentFrame - leading.length;
|
|
111
|
+
this.data = [...leading, frame];
|
|
112
|
+
}
|
|
113
|
+
process(frame) {
|
|
114
|
+
const frameIsValid = this.isValid(frame);
|
|
115
|
+
if (this.state === SILENCE) {
|
|
116
|
+
if (frameIsValid) {
|
|
117
|
+
// seems we got a valid frame after a silence
|
|
118
|
+
this.startToken(frame);
|
|
119
|
+
if (this.initCount >= this.initMin) {
|
|
120
|
+
this.state = NOISE;
|
|
121
|
+
if (this.data.length >= this.maxLength) {
|
|
122
|
+
return this.processEndOfDetection(true);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.state = POSSIBLE_NOISE;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
this.pushLeading(frame);
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
if (this.state === POSSIBLE_NOISE) {
|
|
135
|
+
if (frameIsValid) {
|
|
136
|
+
this.silenceLength = 0;
|
|
137
|
+
this.initCount += 1;
|
|
138
|
+
this.data.push(frame);
|
|
139
|
+
if (this.initCount >= this.initMin) {
|
|
140
|
+
this.state = NOISE;
|
|
141
|
+
if (this.data.length >= this.maxLength) {
|
|
142
|
+
return this.processEndOfDetection(true);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
this.silenceLength += 1;
|
|
148
|
+
if (this.silenceLength > this.initMaxSilence ||
|
|
149
|
+
this.data.length + 1 >= this.maxLength) {
|
|
150
|
+
// either initMaxSilence or maxLength is reached before
|
|
151
|
+
// initCount, back to silence
|
|
152
|
+
this.data = [];
|
|
153
|
+
this.state = SILENCE;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this.data.push(frame);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
if (this.state === NOISE) {
|
|
162
|
+
if (frameIsValid) {
|
|
163
|
+
this.data.push(frame);
|
|
164
|
+
if (this.data.length >= this.maxLength) {
|
|
165
|
+
return this.processEndOfDetection(true);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (this.maxContinuousSilence <= 0) {
|
|
169
|
+
if (this.needsTrailingExtension) {
|
|
170
|
+
this.data.push(frame);
|
|
171
|
+
this.silenceLength = 1;
|
|
172
|
+
this.state = TRAILING_COLLECTION;
|
|
173
|
+
if (this.data.length >= this.maxLength) {
|
|
174
|
+
this.state = SILENCE;
|
|
175
|
+
return this.processEndOfDetection();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
this.state = SILENCE;
|
|
180
|
+
const token = this.processEndOfDetection();
|
|
181
|
+
this.pushLeading(frame);
|
|
182
|
+
return token;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// this is the first silent frame following a valid one and it
|
|
187
|
+
// is tolerated
|
|
188
|
+
this.silenceLength = 1;
|
|
189
|
+
this.data.push(frame);
|
|
190
|
+
this.state = POSSIBLE_SILENCE;
|
|
191
|
+
if (this.data.length === this.maxLength) {
|
|
192
|
+
return this.processEndOfDetection(true);
|
|
193
|
+
// don't reset silenceLength because we still need to know
|
|
194
|
+
// the total number of silent frames
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
if (this.state === POSSIBLE_SILENCE) {
|
|
200
|
+
if (frameIsValid) {
|
|
201
|
+
this.data.push(frame);
|
|
202
|
+
this.silenceLength = 0;
|
|
203
|
+
this.state = NOISE;
|
|
204
|
+
if (this.data.length >= this.maxLength) {
|
|
205
|
+
return this.processEndOfDetection(true);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
if (this.silenceLength >= this.maxContinuousSilence) {
|
|
210
|
+
if (this.needsTrailingExtension &&
|
|
211
|
+
this.silenceLength < this.data.length) {
|
|
212
|
+
// continue collecting trailing beyond maxContinuousSilence
|
|
213
|
+
this.data.push(frame);
|
|
214
|
+
this.silenceLength += 1;
|
|
215
|
+
this.state = TRAILING_COLLECTION;
|
|
216
|
+
if (this.silenceLength >= this.maxTrailingSilence ||
|
|
217
|
+
this.data.length >= this.maxLength) {
|
|
218
|
+
this.state = SILENCE;
|
|
219
|
+
return this.processEndOfDetection();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
this.state = SILENCE;
|
|
224
|
+
if (this.silenceLength < this.data.length) {
|
|
225
|
+
const token = this.processEndOfDetection();
|
|
226
|
+
this.pushLeading(frame);
|
|
227
|
+
return token;
|
|
228
|
+
}
|
|
229
|
+
this.data = [];
|
|
230
|
+
this.silenceLength = 0;
|
|
231
|
+
this.pushLeading(frame);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
this.data.push(frame);
|
|
236
|
+
this.silenceLength += 1;
|
|
237
|
+
if (this.data.length >= this.maxLength) {
|
|
238
|
+
return this.processEndOfDetection(true);
|
|
239
|
+
// don't reset silenceLength because we still need to know
|
|
240
|
+
// the total number of silent frames
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
// state === TRAILING_COLLECTION
|
|
247
|
+
if (frameIsValid) {
|
|
248
|
+
// event is over: deliver with collected trailing, then start a
|
|
249
|
+
// new token from this valid frame
|
|
250
|
+
const token = this.processEndOfDetection();
|
|
251
|
+
this.startToken(frame);
|
|
252
|
+
this.state = this.initCount >= this.initMin ? NOISE : POSSIBLE_NOISE;
|
|
253
|
+
return token;
|
|
254
|
+
}
|
|
255
|
+
this.data.push(frame);
|
|
256
|
+
this.silenceLength += 1;
|
|
257
|
+
if (this.silenceLength >= this.maxTrailingSilence ||
|
|
258
|
+
this.data.length >= this.maxLength) {
|
|
259
|
+
this.state = SILENCE;
|
|
260
|
+
return this.processEndOfDetection();
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
postProcess() {
|
|
265
|
+
if (this.state === NOISE ||
|
|
266
|
+
this.state === POSSIBLE_SILENCE ||
|
|
267
|
+
this.state === TRAILING_COLLECTION) {
|
|
268
|
+
if (this.data.length > 0 && this.data.length > this.silenceLength) {
|
|
269
|
+
return this.processEndOfDetection();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
processEndOfDetection(truncated = false) {
|
|
275
|
+
if (!truncated &&
|
|
276
|
+
this.maxTrailingSilence !== null &&
|
|
277
|
+
this.silenceLength > this.maxTrailingSilence) {
|
|
278
|
+
// Trim trailing silence beyond the allowed amount. Happens if
|
|
279
|
+
// maxContinuousSilence is reached or maxLength is reached at a
|
|
280
|
+
// silent frame. Trimmed frames are seeded into the leading
|
|
281
|
+
// buffer so they can become leading silence for the next token.
|
|
282
|
+
const excess = this.silenceLength - this.maxTrailingSilence;
|
|
283
|
+
const trimStart = Math.max(0, this.data.length - excess);
|
|
284
|
+
const trimmed = this.data.splice(trimStart);
|
|
285
|
+
for (const frame of trimmed) {
|
|
286
|
+
this.pushLeading(frame);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (this.data.length >= this.minLength ||
|
|
290
|
+
(this.data.length > 0 && !this.strictMinLength && this.contiguousToken)) {
|
|
291
|
+
const token = {
|
|
292
|
+
frames: this.data,
|
|
293
|
+
start: this.startFrame,
|
|
294
|
+
end: this.startFrame + this.data.length - 1,
|
|
295
|
+
};
|
|
296
|
+
this.data = [];
|
|
297
|
+
if (truncated) {
|
|
298
|
+
// next token (if any) will start at currentFrame + 1 and is
|
|
299
|
+
// contiguous with the just delivered one
|
|
300
|
+
this.startFrame = this.currentFrame + 1;
|
|
301
|
+
this.contiguousToken = true;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
this.contiguousToken = false;
|
|
305
|
+
}
|
|
306
|
+
return token;
|
|
307
|
+
}
|
|
308
|
+
this.contiguousToken = false;
|
|
309
|
+
this.data = [];
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
}
|
package/dist/wav.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-free WAV (RIFF) codec: works in Node and in the browser
|
|
3
|
+
* without an AudioContext. Decodes PCM 8/16/24/32-bit integer and
|
|
4
|
+
* 32-bit IEEE float (including WAVE_FORMAT_EXTENSIBLE); encodes 16-bit
|
|
5
|
+
* PCM or 32-bit float.
|
|
6
|
+
*/
|
|
7
|
+
export interface DecodedAudio {
|
|
8
|
+
channelData: Float32Array[];
|
|
9
|
+
sampleRate: number;
|
|
10
|
+
}
|
|
11
|
+
/** True if `data` starts with a RIFF/WAVE header. */
|
|
12
|
+
export declare function isWav(data: Uint8Array | ArrayBuffer): boolean;
|
|
13
|
+
/** Decode a WAV file into planar Float32Array channels in [-1, 1]. */
|
|
14
|
+
export declare function decodeWav(data: Uint8Array | ArrayBuffer): DecodedAudio;
|
|
15
|
+
export interface WavEncodeOptions {
|
|
16
|
+
/** 16 (PCM, default) or "float32" (IEEE float). */
|
|
17
|
+
bitDepth?: 16 | "float32";
|
|
18
|
+
}
|
|
19
|
+
/** Encode planar Float32Array channels as a WAV file. */
|
|
20
|
+
export declare function encodeWav(audio: {
|
|
21
|
+
channelData: readonly Float32Array[];
|
|
22
|
+
sampleRate: number;
|
|
23
|
+
}, options?: WavEncodeOptions): Uint8Array;
|
package/dist/wav.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-free WAV (RIFF) codec: works in Node and in the browser
|
|
3
|
+
* without an AudioContext. Decodes PCM 8/16/24/32-bit integer and
|
|
4
|
+
* 32-bit IEEE float (including WAVE_FORMAT_EXTENSIBLE); encodes 16-bit
|
|
5
|
+
* PCM or 32-bit float.
|
|
6
|
+
*/
|
|
7
|
+
const WAVE_FORMAT_PCM = 1;
|
|
8
|
+
const WAVE_FORMAT_IEEE_FLOAT = 3;
|
|
9
|
+
const WAVE_FORMAT_EXTENSIBLE = 0xfffe;
|
|
10
|
+
function toDataView(data) {
|
|
11
|
+
if (data instanceof ArrayBuffer) {
|
|
12
|
+
return new DataView(data);
|
|
13
|
+
}
|
|
14
|
+
return new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
15
|
+
}
|
|
16
|
+
/** True if `data` starts with a RIFF/WAVE header. */
|
|
17
|
+
export function isWav(data) {
|
|
18
|
+
const view = toDataView(data);
|
|
19
|
+
return (view.byteLength >= 12 &&
|
|
20
|
+
view.getUint32(0, false) === 0x52494646 && // "RIFF"
|
|
21
|
+
view.getUint32(8, false) === 0x57415645 // "WAVE"
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
/** Decode a WAV file into planar Float32Array channels in [-1, 1]. */
|
|
25
|
+
export function decodeWav(data) {
|
|
26
|
+
const view = toDataView(data);
|
|
27
|
+
if (!isWav(data)) {
|
|
28
|
+
throw new Error("not a RIFF/WAVE file");
|
|
29
|
+
}
|
|
30
|
+
let format = 0;
|
|
31
|
+
let channels = 0;
|
|
32
|
+
let sampleRate = 0;
|
|
33
|
+
let bitsPerSample = 0;
|
|
34
|
+
let dataOffset = -1;
|
|
35
|
+
let dataLength = 0;
|
|
36
|
+
let offset = 12;
|
|
37
|
+
while (offset + 8 <= view.byteLength) {
|
|
38
|
+
const chunkId = view.getUint32(offset, false);
|
|
39
|
+
const chunkSize = view.getUint32(offset + 4, true);
|
|
40
|
+
const body = offset + 8;
|
|
41
|
+
if (chunkId === 0x666d7420) {
|
|
42
|
+
// "fmt "
|
|
43
|
+
format = view.getUint16(body, true);
|
|
44
|
+
channels = view.getUint16(body + 2, true);
|
|
45
|
+
sampleRate = view.getUint32(body + 4, true);
|
|
46
|
+
bitsPerSample = view.getUint16(body + 14, true);
|
|
47
|
+
if (format === WAVE_FORMAT_EXTENSIBLE && chunkSize >= 40) {
|
|
48
|
+
// sub-format GUID starts with the actual format code
|
|
49
|
+
format = view.getUint16(body + 24, true);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (chunkId === 0x64617461) {
|
|
53
|
+
// "data"
|
|
54
|
+
dataOffset = body;
|
|
55
|
+
dataLength = Math.min(chunkSize, view.byteLength - body);
|
|
56
|
+
}
|
|
57
|
+
offset = body + chunkSize + (chunkSize % 2); // chunks are word-aligned
|
|
58
|
+
}
|
|
59
|
+
if (channels === 0 || sampleRate === 0) {
|
|
60
|
+
throw new Error("invalid WAV file: no fmt chunk");
|
|
61
|
+
}
|
|
62
|
+
if (dataOffset < 0) {
|
|
63
|
+
throw new Error("invalid WAV file: no data chunk");
|
|
64
|
+
}
|
|
65
|
+
const bytesPerSample = bitsPerSample / 8;
|
|
66
|
+
if (!((format === WAVE_FORMAT_PCM && [8, 16, 24, 32].includes(bitsPerSample)) ||
|
|
67
|
+
(format === WAVE_FORMAT_IEEE_FLOAT && bitsPerSample === 32))) {
|
|
68
|
+
throw new Error(`unsupported WAV format: format code ${format}, ` +
|
|
69
|
+
`${bitsPerSample} bits per sample`);
|
|
70
|
+
}
|
|
71
|
+
const frameCount = Math.floor(dataLength / (bytesPerSample * channels));
|
|
72
|
+
const channelData = Array.from({ length: channels }, () => new Float32Array(frameCount));
|
|
73
|
+
for (let i = 0; i < frameCount; i++) {
|
|
74
|
+
for (let c = 0; c < channels; c++) {
|
|
75
|
+
const at = dataOffset + (i * channels + c) * bytesPerSample;
|
|
76
|
+
let value;
|
|
77
|
+
if (format === WAVE_FORMAT_IEEE_FLOAT) {
|
|
78
|
+
value = view.getFloat32(at, true);
|
|
79
|
+
}
|
|
80
|
+
else if (bitsPerSample === 8) {
|
|
81
|
+
value = (view.getUint8(at) - 128) / 128;
|
|
82
|
+
}
|
|
83
|
+
else if (bitsPerSample === 16) {
|
|
84
|
+
value = view.getInt16(at, true) / 32768;
|
|
85
|
+
}
|
|
86
|
+
else if (bitsPerSample === 24) {
|
|
87
|
+
const raw = view.getUint8(at) |
|
|
88
|
+
(view.getUint8(at + 1) << 8) |
|
|
89
|
+
(view.getInt8(at + 2) << 16);
|
|
90
|
+
value = raw / 8388608;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
value = view.getInt32(at, true) / 2147483648;
|
|
94
|
+
}
|
|
95
|
+
channelData[c][i] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { channelData, sampleRate };
|
|
99
|
+
}
|
|
100
|
+
/** Encode planar Float32Array channels as a WAV file. */
|
|
101
|
+
export function encodeWav(audio, options = {}) {
|
|
102
|
+
const { channelData, sampleRate } = audio;
|
|
103
|
+
const bitDepth = options.bitDepth ?? 16;
|
|
104
|
+
const float = bitDepth === "float32";
|
|
105
|
+
const bytesPerSample = float ? 4 : 2;
|
|
106
|
+
const channels = channelData.length;
|
|
107
|
+
const frameCount = channelData[0]?.length ?? 0;
|
|
108
|
+
const dataSize = frameCount * channels * bytesPerSample;
|
|
109
|
+
const buffer = new ArrayBuffer(44 + dataSize);
|
|
110
|
+
const view = new DataView(buffer);
|
|
111
|
+
view.setUint32(0, 0x52494646, false); // "RIFF"
|
|
112
|
+
view.setUint32(4, 36 + dataSize, true);
|
|
113
|
+
view.setUint32(8, 0x57415645, false); // "WAVE"
|
|
114
|
+
view.setUint32(12, 0x666d7420, false); // "fmt "
|
|
115
|
+
view.setUint32(16, 16, true);
|
|
116
|
+
view.setUint16(20, float ? WAVE_FORMAT_IEEE_FLOAT : WAVE_FORMAT_PCM, true);
|
|
117
|
+
view.setUint16(22, channels, true);
|
|
118
|
+
view.setUint32(24, sampleRate, true);
|
|
119
|
+
view.setUint32(28, sampleRate * channels * bytesPerSample, true);
|
|
120
|
+
view.setUint16(32, channels * bytesPerSample, true);
|
|
121
|
+
view.setUint16(34, float ? 32 : 16, true);
|
|
122
|
+
view.setUint32(36, 0x64617461, false); // "data"
|
|
123
|
+
view.setUint32(40, dataSize, true);
|
|
124
|
+
let at = 44;
|
|
125
|
+
for (let i = 0; i < frameCount; i++) {
|
|
126
|
+
for (let c = 0; c < channels; c++) {
|
|
127
|
+
const value = channelData[c][i];
|
|
128
|
+
if (float) {
|
|
129
|
+
view.setFloat32(at, value, true);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// symmetric scale, clamped to the int16 range: values on the
|
|
133
|
+
// int16 grid (v / 32768) roundtrip exactly
|
|
134
|
+
const scaled = Math.round(value * 32768);
|
|
135
|
+
view.setInt16(at, Math.max(-32768, Math.min(32767, scaled)), true);
|
|
136
|
+
}
|
|
137
|
+
at += bytesPerSample;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return new Uint8Array(buffer);
|
|
141
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auditok",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Audio activity detection and segmentation: turn audio into events with min/max duration and silence semantics. JavaScript port of the Python auditok package. Zero dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"audio",
|
|
7
|
+
"vad",
|
|
8
|
+
"voice-activity-detection",
|
|
9
|
+
"audio-activity-detection",
|
|
10
|
+
"segmentation",
|
|
11
|
+
"silence-detection",
|
|
12
|
+
"silence-removal",
|
|
13
|
+
"speech",
|
|
14
|
+
"tokenizer",
|
|
15
|
+
"webaudio"
|
|
16
|
+
],
|
|
17
|
+
"author": "Amine Sehili <amine.sehili@gmail.com>",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/amsehili/auditok.js.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/amsehili/auditok.js",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/amsehili/auditok.js/issues"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"browser": {
|
|
35
|
+
"types": "./dist/browser/index.d.ts",
|
|
36
|
+
"default": "./dist/browser/index.js"
|
|
37
|
+
},
|
|
38
|
+
"node": {
|
|
39
|
+
"types": "./dist/node/index.d.ts",
|
|
40
|
+
"default": "./dist/node/index.js"
|
|
41
|
+
},
|
|
42
|
+
"default": {
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"default": "./dist/index.js"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"./core": {
|
|
48
|
+
"types": "./dist/index.d.ts",
|
|
49
|
+
"default": "./dist/index.js"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"types": "./dist/index.d.ts",
|
|
53
|
+
"files": [
|
|
54
|
+
"dist",
|
|
55
|
+
"README.md",
|
|
56
|
+
"LICENSE"
|
|
57
|
+
],
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsc -p tsconfig.json",
|
|
60
|
+
"build:webrtcvad": "tsc -p webrtcvad/tsconfig.json",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"demo:lib": "npm run build && npm run build:webrtcvad && rm -rf docs/lib && mkdir -p docs/lib/browser docs/lib/webrtcvad && cp dist/*.js docs/lib/ && cp dist/browser/*.js docs/lib/browser/ && cp webrtcvad/dist/*.js docs/lib/webrtcvad/",
|
|
63
|
+
"demo:serve": "npx serve docs",
|
|
64
|
+
"prepublishOnly": "npm run build && npm test"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@types/node": "^18.19.0",
|
|
68
|
+
"typescript": "^5.5.0",
|
|
69
|
+
"vitest": "^3.2.7"
|
|
70
|
+
}
|
|
71
|
+
}
|