@crewhaus/vad-engine 0.1.4 → 0.1.6
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.ts +50 -0
- package/dist/index.js +81 -0
- package/package.json +8 -5
- package/src/index.test.ts +0 -124
- package/src/index.ts +0 -107
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog R16 `vad-engine` — Section 24 VOICE.
|
|
3
|
+
*
|
|
4
|
+
* Voice activity detection. WebRTC-VAD-style heuristic over PCM 16-bit
|
|
5
|
+
* frames: a frame is "speech" when BOTH (a) RMS energy crosses an
|
|
6
|
+
* aggressiveness-tuned threshold and (b) zero-crossing-rate sits in a
|
|
7
|
+
* speech-shaped band. Either signal alone produces a "transitioning"
|
|
8
|
+
* verdict, used by `barge-in-controller` to apply hysteresis (don't
|
|
9
|
+
* bargle in on a 50ms cough).
|
|
10
|
+
*
|
|
11
|
+
* Frame size is conventional: 30ms at the input sample rate. At 24kHz
|
|
12
|
+
* mono that's 720 samples; the detector accepts ANY length but reports
|
|
13
|
+
* the verdict as if it were a single frame, so callers framing at
|
|
14
|
+
* higher granularity will simply get richer output.
|
|
15
|
+
*
|
|
16
|
+
* Aggressiveness:
|
|
17
|
+
* 0 — most permissive (high speech recall, low false positives)
|
|
18
|
+
* 1 — default
|
|
19
|
+
* 2 — moderately aggressive (ignores low-amplitude background)
|
|
20
|
+
* 3 — most aggressive (rejects most non-vocal audio)
|
|
21
|
+
*
|
|
22
|
+
* No external ML deps: pure heuristic. Adequate for barge-in and as a
|
|
23
|
+
* pre-filter for paid VAD models. `T9` random-noise stability test
|
|
24
|
+
* proves a 10-second white-noise stream produces zero "speech" verdicts
|
|
25
|
+
* at aggressiveness 2+.
|
|
26
|
+
*/
|
|
27
|
+
export type Aggressiveness = 0 | 1 | 2 | 3;
|
|
28
|
+
export type VadVerdict = "speech" | "silence" | "transitioning";
|
|
29
|
+
export type DetectorOptions = {
|
|
30
|
+
readonly aggressiveness?: Aggressiveness;
|
|
31
|
+
/**
|
|
32
|
+
* Sample rate of incoming PCM (Hz). RESERVED — currently has no effect on
|
|
33
|
+
* detection: the energy thresholds and zero-crossing band below are
|
|
34
|
+
* sample-rate-independent, and the detector does NOT resample. The option is
|
|
35
|
+
* accepted for forward compatibility (a planned band-pass filter stage). Pass
|
|
36
|
+
* it for clarity if you like, but do not infer any resampling behaviour from
|
|
37
|
+
* it.
|
|
38
|
+
*/
|
|
39
|
+
readonly sampleRate?: number;
|
|
40
|
+
};
|
|
41
|
+
export interface VadDetector {
|
|
42
|
+
detect(frame: Int16Array): VadVerdict;
|
|
43
|
+
readonly aggressiveness: Aggressiveness;
|
|
44
|
+
}
|
|
45
|
+
export declare function createVadDetector(opts?: DetectorOptions): VadDetector;
|
|
46
|
+
/**
|
|
47
|
+
* Helper: split a long PCM buffer into 30ms frames at `sampleRate`. Tail
|
|
48
|
+
* shorter than one frame is dropped (caller can decide to pad).
|
|
49
|
+
*/
|
|
50
|
+
export declare function frames30ms(pcm: Int16Array, sampleRate?: number): Int16Array[];
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog R16 `vad-engine` — Section 24 VOICE.
|
|
3
|
+
*
|
|
4
|
+
* Voice activity detection. WebRTC-VAD-style heuristic over PCM 16-bit
|
|
5
|
+
* frames: a frame is "speech" when BOTH (a) RMS energy crosses an
|
|
6
|
+
* aggressiveness-tuned threshold and (b) zero-crossing-rate sits in a
|
|
7
|
+
* speech-shaped band. Either signal alone produces a "transitioning"
|
|
8
|
+
* verdict, used by `barge-in-controller` to apply hysteresis (don't
|
|
9
|
+
* bargle in on a 50ms cough).
|
|
10
|
+
*
|
|
11
|
+
* Frame size is conventional: 30ms at the input sample rate. At 24kHz
|
|
12
|
+
* mono that's 720 samples; the detector accepts ANY length but reports
|
|
13
|
+
* the verdict as if it were a single frame, so callers framing at
|
|
14
|
+
* higher granularity will simply get richer output.
|
|
15
|
+
*
|
|
16
|
+
* Aggressiveness:
|
|
17
|
+
* 0 — most permissive (high speech recall, low false positives)
|
|
18
|
+
* 1 — default
|
|
19
|
+
* 2 — moderately aggressive (ignores low-amplitude background)
|
|
20
|
+
* 3 — most aggressive (rejects most non-vocal audio)
|
|
21
|
+
*
|
|
22
|
+
* No external ML deps: pure heuristic. Adequate for barge-in and as a
|
|
23
|
+
* pre-filter for paid VAD models. `T9` random-noise stability test
|
|
24
|
+
* proves a 10-second white-noise stream produces zero "speech" verdicts
|
|
25
|
+
* at aggressiveness 2+.
|
|
26
|
+
*/
|
|
27
|
+
const ENERGY_THRESHOLDS = {
|
|
28
|
+
0: 280,
|
|
29
|
+
1: 540,
|
|
30
|
+
2: 900,
|
|
31
|
+
3: 1500,
|
|
32
|
+
};
|
|
33
|
+
// Speech-shaped ZCR band (per 1k samples). Empirical for 24kHz speech
|
|
34
|
+
// vs ambient noise vs single-frequency tones. The lower bound rejects
|
|
35
|
+
// pure low-frequency tones (e.g. 60Hz mains hum); the upper bound
|
|
36
|
+
// rejects white noise / high-frequency hiss.
|
|
37
|
+
const ZCR_LOW_PER_1K = 15;
|
|
38
|
+
const ZCR_HIGH_PER_1K = 220;
|
|
39
|
+
export function createVadDetector(opts = {}) {
|
|
40
|
+
const aggressiveness = opts.aggressiveness ?? 1;
|
|
41
|
+
void opts.sampleRate; // currently unused; reserved for future bandpass filter
|
|
42
|
+
return {
|
|
43
|
+
aggressiveness,
|
|
44
|
+
detect(frame) {
|
|
45
|
+
if (frame.length === 0)
|
|
46
|
+
return "silence";
|
|
47
|
+
let sumSq = 0;
|
|
48
|
+
let zeroCrossings = 0;
|
|
49
|
+
let prev = frame[0] ?? 0;
|
|
50
|
+
for (let i = 0; i < frame.length; i++) {
|
|
51
|
+
const v = frame[i] ?? 0;
|
|
52
|
+
sumSq += v * v;
|
|
53
|
+
if (i > 0 && Math.sign(v) !== Math.sign(prev) && (v !== 0 || prev !== 0)) {
|
|
54
|
+
zeroCrossings += 1;
|
|
55
|
+
}
|
|
56
|
+
prev = v;
|
|
57
|
+
}
|
|
58
|
+
const rms = Math.sqrt(sumSq / frame.length);
|
|
59
|
+
const zcrPer1k = (zeroCrossings * 1000) / frame.length;
|
|
60
|
+
const energyOk = rms >= ENERGY_THRESHOLDS[aggressiveness];
|
|
61
|
+
const zcrInBand = zcrPer1k >= ZCR_LOW_PER_1K && zcrPer1k <= ZCR_HIGH_PER_1K;
|
|
62
|
+
if (energyOk && zcrInBand)
|
|
63
|
+
return "speech";
|
|
64
|
+
if (!energyOk && !zcrInBand)
|
|
65
|
+
return "silence";
|
|
66
|
+
return "transitioning";
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Helper: split a long PCM buffer into 30ms frames at `sampleRate`. Tail
|
|
72
|
+
* shorter than one frame is dropped (caller can decide to pad).
|
|
73
|
+
*/
|
|
74
|
+
export function frames30ms(pcm, sampleRate = 24_000) {
|
|
75
|
+
const n = Math.floor(sampleRate * 0.03);
|
|
76
|
+
const out = [];
|
|
77
|
+
for (let i = 0; i + n <= pcm.length; i += n) {
|
|
78
|
+
out.push(pcm.subarray(i, i + n));
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/vad-engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Voice activity detection — energy + ZCR heuristic over PCM 16-bit frames (Section 24 VOICE)",
|
|
6
|
-
"main": "
|
|
7
|
-
"types": "
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
10
13
|
},
|
|
11
14
|
"scripts": {
|
|
12
15
|
"test": "bun test src"
|
|
@@ -29,5 +32,5 @@
|
|
|
29
32
|
"publishConfig": {
|
|
30
33
|
"access": "public"
|
|
31
34
|
},
|
|
32
|
-
"files": ["
|
|
35
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
33
36
|
}
|
package/src/index.test.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { createVadDetector, frames30ms } from "./index.js";
|
|
3
|
-
|
|
4
|
-
function silence(n: number): Int16Array {
|
|
5
|
-
return new Int16Array(n);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function whiteNoise(n: number, amplitude: number, seed = 1): Int16Array {
|
|
9
|
-
// Linear-congruential PRNG so tests are deterministic.
|
|
10
|
-
let s = seed;
|
|
11
|
-
const out = new Int16Array(n);
|
|
12
|
-
for (let i = 0; i < n; i++) {
|
|
13
|
-
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
|
14
|
-
const r = ((s % 1000) - 500) / 500; // [-1, 1)
|
|
15
|
-
out[i] = Math.round(r * amplitude);
|
|
16
|
-
}
|
|
17
|
-
return out;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function tone(n: number, freqHz: number, sampleRate: number, amplitude: number): Int16Array {
|
|
21
|
-
const out = new Int16Array(n);
|
|
22
|
-
for (let i = 0; i < n; i++) {
|
|
23
|
-
out[i] = Math.round(Math.sin((2 * Math.PI * freqHz * i) / sampleRate) * amplitude);
|
|
24
|
-
}
|
|
25
|
-
return out;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Speech-like signal: a sum of two formants (F1 ≈ 500Hz, F2 ≈ 1500Hz)
|
|
30
|
-
* with amplitude modulation to approximate voicing. Enough to push both
|
|
31
|
-
* energy and ZCR into the speech band for aggressiveness ≤ 2.
|
|
32
|
-
*/
|
|
33
|
-
function speechLike(n: number, sampleRate: number, amplitude: number): Int16Array {
|
|
34
|
-
const out = new Int16Array(n);
|
|
35
|
-
for (let i = 0; i < n; i++) {
|
|
36
|
-
const env = 0.6 + 0.4 * Math.sin((2 * Math.PI * 5 * i) / sampleRate);
|
|
37
|
-
const sig =
|
|
38
|
-
0.7 * Math.sin((2 * Math.PI * 500 * i) / sampleRate) +
|
|
39
|
-
0.3 * Math.sin((2 * Math.PI * 1500 * i) / sampleRate);
|
|
40
|
-
out[i] = Math.round(env * sig * amplitude);
|
|
41
|
-
}
|
|
42
|
-
return out;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
describe("createVadDetector — energy + ZCR heuristic (T1)", () => {
|
|
46
|
-
test("silent frame → silence at every aggressiveness", () => {
|
|
47
|
-
for (const a of [0, 1, 2, 3] as const) {
|
|
48
|
-
const det = createVadDetector({ aggressiveness: a });
|
|
49
|
-
expect(det.detect(silence(720))).toBe("silence");
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("speech-like signal → speech at aggressiveness 0..2", () => {
|
|
54
|
-
const f = speechLike(720, 24_000, 8000);
|
|
55
|
-
for (const a of [0, 1, 2] as const) {
|
|
56
|
-
const det = createVadDetector({ aggressiveness: a });
|
|
57
|
-
expect(det.detect(f)).toBe("speech");
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("aggressiveness=3 ignores low-amplitude speech-shaped signal", () => {
|
|
62
|
-
const det = createVadDetector({ aggressiveness: 3 });
|
|
63
|
-
const f = speechLike(720, 24_000, 1500);
|
|
64
|
-
// Low energy at aggressiveness 3: should NOT be classified as speech.
|
|
65
|
-
expect(det.detect(f)).not.toBe("speech");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("low-energy white noise → silence (T9 noise rejection at aggressiveness ≥ 2)", () => {
|
|
69
|
-
const det = createVadDetector({ aggressiveness: 2 });
|
|
70
|
-
const f = whiteNoise(720, 250);
|
|
71
|
-
// Low-amplitude noise → energy below threshold.
|
|
72
|
-
expect(det.detect(f)).toBe("silence");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("loud white noise → not speech (ZCR too high)", () => {
|
|
76
|
-
// White noise has ZCR much higher than the speech band.
|
|
77
|
-
const det = createVadDetector({ aggressiveness: 1 });
|
|
78
|
-
const f = whiteNoise(720, 12000);
|
|
79
|
-
// High energy but ZCR way out of band → transitioning, NOT speech.
|
|
80
|
-
expect(det.detect(f)).not.toBe("speech");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("pure tone at 60Hz → not speech (ZCR too low)", () => {
|
|
84
|
-
const det = createVadDetector({ aggressiveness: 1 });
|
|
85
|
-
const f = tone(720, 60, 24_000, 8000);
|
|
86
|
-
// Energy passes but ZCR is too low → not speech.
|
|
87
|
-
expect(det.detect(f)).not.toBe("speech");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("empty frame → silence", () => {
|
|
91
|
-
const det = createVadDetector({ aggressiveness: 1 });
|
|
92
|
-
expect(det.detect(new Int16Array(0))).toBe("silence");
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe("frames30ms helper", () => {
|
|
97
|
-
test("splits a 2-second PCM buffer into 66 30ms frames at 24kHz", () => {
|
|
98
|
-
const pcm = new Int16Array(24_000 * 2);
|
|
99
|
-
const fs = frames30ms(pcm, 24_000);
|
|
100
|
-
expect(fs.length).toBe(66); // floor(48000 / 720) = 66
|
|
101
|
-
expect(fs[0]?.length).toBe(720);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("drops a partial tail shorter than one frame", () => {
|
|
105
|
-
const pcm = new Int16Array(720 + 100);
|
|
106
|
-
const fs = frames30ms(pcm, 24_000);
|
|
107
|
-
expect(fs.length).toBe(1);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe("T9 noise-injection stability — random noise at aggressiveness 2+ never reports speech", () => {
|
|
112
|
-
test("10-second white-noise stream → 0 speech verdicts at aggressiveness 2", () => {
|
|
113
|
-
const sr = 24_000;
|
|
114
|
-
const det = createVadDetector({ aggressiveness: 2 });
|
|
115
|
-
const pcm = whiteNoise(sr * 10, 6000, 42);
|
|
116
|
-
const fs = frames30ms(pcm, sr);
|
|
117
|
-
const counts = { speech: 0, silence: 0, transitioning: 0 };
|
|
118
|
-
for (const f of fs) {
|
|
119
|
-
counts[det.detect(f)] += 1;
|
|
120
|
-
}
|
|
121
|
-
expect(counts.speech).toBe(0);
|
|
122
|
-
expect(counts.silence + counts.transitioning).toBe(fs.length);
|
|
123
|
-
});
|
|
124
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Catalog R16 `vad-engine` — Section 24 VOICE.
|
|
3
|
-
*
|
|
4
|
-
* Voice activity detection. WebRTC-VAD-style heuristic over PCM 16-bit
|
|
5
|
-
* frames: a frame is "speech" when BOTH (a) RMS energy crosses an
|
|
6
|
-
* aggressiveness-tuned threshold and (b) zero-crossing-rate sits in a
|
|
7
|
-
* speech-shaped band. Either signal alone produces a "transitioning"
|
|
8
|
-
* verdict, used by `barge-in-controller` to apply hysteresis (don't
|
|
9
|
-
* bargle in on a 50ms cough).
|
|
10
|
-
*
|
|
11
|
-
* Frame size is conventional: 30ms at the input sample rate. At 24kHz
|
|
12
|
-
* mono that's 720 samples; the detector accepts ANY length but reports
|
|
13
|
-
* the verdict as if it were a single frame, so callers framing at
|
|
14
|
-
* higher granularity will simply get richer output.
|
|
15
|
-
*
|
|
16
|
-
* Aggressiveness:
|
|
17
|
-
* 0 — most permissive (high speech recall, low false positives)
|
|
18
|
-
* 1 — default
|
|
19
|
-
* 2 — moderately aggressive (ignores low-amplitude background)
|
|
20
|
-
* 3 — most aggressive (rejects most non-vocal audio)
|
|
21
|
-
*
|
|
22
|
-
* No external ML deps: pure heuristic. Adequate for barge-in and as a
|
|
23
|
-
* pre-filter for paid VAD models. `T9` random-noise stability test
|
|
24
|
-
* proves a 10-second white-noise stream produces zero "speech" verdicts
|
|
25
|
-
* at aggressiveness 2+.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
export type Aggressiveness = 0 | 1 | 2 | 3;
|
|
29
|
-
|
|
30
|
-
export type VadVerdict = "speech" | "silence" | "transitioning";
|
|
31
|
-
|
|
32
|
-
export type DetectorOptions = {
|
|
33
|
-
readonly aggressiveness?: Aggressiveness;
|
|
34
|
-
/**
|
|
35
|
-
* Sample rate of incoming PCM (Hz). RESERVED — currently has no effect on
|
|
36
|
-
* detection: the energy thresholds and zero-crossing band below are
|
|
37
|
-
* sample-rate-independent, and the detector does NOT resample. The option is
|
|
38
|
-
* accepted for forward compatibility (a planned band-pass filter stage). Pass
|
|
39
|
-
* it for clarity if you like, but do not infer any resampling behaviour from
|
|
40
|
-
* it.
|
|
41
|
-
*/
|
|
42
|
-
readonly sampleRate?: number;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const ENERGY_THRESHOLDS: Record<Aggressiveness, number> = {
|
|
46
|
-
0: 280,
|
|
47
|
-
1: 540,
|
|
48
|
-
2: 900,
|
|
49
|
-
3: 1500,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// Speech-shaped ZCR band (per 1k samples). Empirical for 24kHz speech
|
|
53
|
-
// vs ambient noise vs single-frequency tones. The lower bound rejects
|
|
54
|
-
// pure low-frequency tones (e.g. 60Hz mains hum); the upper bound
|
|
55
|
-
// rejects white noise / high-frequency hiss.
|
|
56
|
-
const ZCR_LOW_PER_1K = 15;
|
|
57
|
-
const ZCR_HIGH_PER_1K = 220;
|
|
58
|
-
|
|
59
|
-
export interface VadDetector {
|
|
60
|
-
detect(frame: Int16Array): VadVerdict;
|
|
61
|
-
readonly aggressiveness: Aggressiveness;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function createVadDetector(opts: DetectorOptions = {}): VadDetector {
|
|
65
|
-
const aggressiveness = opts.aggressiveness ?? 1;
|
|
66
|
-
void opts.sampleRate; // currently unused; reserved for future bandpass filter
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
aggressiveness,
|
|
70
|
-
detect(frame: Int16Array): VadVerdict {
|
|
71
|
-
if (frame.length === 0) return "silence";
|
|
72
|
-
let sumSq = 0;
|
|
73
|
-
let zeroCrossings = 0;
|
|
74
|
-
let prev = frame[0] ?? 0;
|
|
75
|
-
for (let i = 0; i < frame.length; i++) {
|
|
76
|
-
const v = frame[i] ?? 0;
|
|
77
|
-
sumSq += v * v;
|
|
78
|
-
if (i > 0 && Math.sign(v) !== Math.sign(prev) && (v !== 0 || prev !== 0)) {
|
|
79
|
-
zeroCrossings += 1;
|
|
80
|
-
}
|
|
81
|
-
prev = v;
|
|
82
|
-
}
|
|
83
|
-
const rms = Math.sqrt(sumSq / frame.length);
|
|
84
|
-
const zcrPer1k = (zeroCrossings * 1000) / frame.length;
|
|
85
|
-
|
|
86
|
-
const energyOk = rms >= ENERGY_THRESHOLDS[aggressiveness];
|
|
87
|
-
const zcrInBand = zcrPer1k >= ZCR_LOW_PER_1K && zcrPer1k <= ZCR_HIGH_PER_1K;
|
|
88
|
-
|
|
89
|
-
if (energyOk && zcrInBand) return "speech";
|
|
90
|
-
if (!energyOk && !zcrInBand) return "silence";
|
|
91
|
-
return "transitioning";
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Helper: split a long PCM buffer into 30ms frames at `sampleRate`. Tail
|
|
98
|
-
* shorter than one frame is dropped (caller can decide to pad).
|
|
99
|
-
*/
|
|
100
|
-
export function frames30ms(pcm: Int16Array, sampleRate = 24_000): Int16Array[] {
|
|
101
|
-
const n = Math.floor(sampleRate * 0.03);
|
|
102
|
-
const out: Int16Array[] = [];
|
|
103
|
-
for (let i = 0; i + n <= pcm.length; i += n) {
|
|
104
|
-
out.push(pcm.subarray(i, i + n));
|
|
105
|
-
}
|
|
106
|
-
return out;
|
|
107
|
-
}
|