sona-ai-voice 0.2.3 → 0.2.4
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/package.json +4 -2
- package/packages/cli/dist/audio-capture.js +17 -26
- package/packages/cli/dist/audio-playback.js +36 -49
- package/packages/cli/dist/audio-utils.js +7 -17
- package/packages/cli/dist/echo-canceller.js +26 -49
- package/packages/cli/dist/gui-server.js +17 -24
- package/packages/cli/dist/index.js +14 -18
- package/packages/cli/dist/particle-ui.js +46 -88
- package/packages/cli/dist/setup.js +19 -44
- package/packages/cli/dist/tools/codebase-analyzer.js +24 -43
- package/packages/cli/dist/tools/codebase-context.js +29 -54
- package/packages/cli/dist/tools/tool-executor.js +20 -47
- package/packages/cli/dist/tools/web-search.js +14 -30
- package/packages/cli/dist/voice-conversation.js +64 -82
- package/packages/realtime/dist/index.js +1 -4
- package/packages/realtime/dist/session.js +22 -54
- package/packages/shared/dist/config.js +36 -40
- package/packages/shared/dist/constants.js +16 -19
- package/packages/shared/dist/index.js +1 -4
- package/packages/shared/dist/types.js +7 -11
- package/packages/audio/package.json +0 -16
- package/packages/cache/package.json +0 -22
- package/packages/cli/package.json +0 -31
- package/packages/indexer/package.json +0 -22
- package/packages/realtime/package.json +0 -22
- package/packages/shared/package.json +0 -22
- package/packages/terminal/package.json +0 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sona-ai-voice",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Voice-to-voice AI CLI companion powered by OpenAI Realtime API",
|
|
5
5
|
"author": "zerish",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"sona": "packages/cli/dist/index.js"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
|
+
"README.md",
|
|
24
25
|
"packages/*/dist/**/*",
|
|
25
|
-
"packages/*/package.json",
|
|
26
26
|
"packages/cli/src/gui/**/*",
|
|
27
27
|
"bin/**/*"
|
|
28
28
|
],
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"lint": "eslint packages --ext .ts,.tsx",
|
|
39
39
|
"format": "prettier --write \"packages/**/*.{ts,tsx,json,md}\"",
|
|
40
40
|
"cache:clear": "rm -rf node_modules/.cache .sona-cache",
|
|
41
|
+
"prepack": "node scripts/prepack.js",
|
|
42
|
+
"postpack": "node scripts/postpack.js",
|
|
41
43
|
"prepublishOnly": "npm run build"
|
|
42
44
|
},
|
|
43
45
|
"dependencies": {
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
* Audio Capture Module
|
|
4
|
-
* Mic capture with pause/resume for echo cancellation
|
|
5
|
-
*/
|
|
2
|
+
|
|
6
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
4
|
exports.AudioCapture = void 0;
|
|
8
5
|
const child_process_1 = require("child_process");
|
|
@@ -30,14 +27,14 @@ class AudioCapture extends events_1.EventEmitter {
|
|
|
30
27
|
this.micProcess = null;
|
|
31
28
|
}
|
|
32
29
|
this.micProcess = (0, child_process_1.spawn)('sox', [
|
|
33
|
-
'-d',
|
|
34
|
-
'-t', 'raw',
|
|
35
|
-
'-b', '16',
|
|
36
|
-
'-e', 'signed-integer',
|
|
37
|
-
'-r', '24000',
|
|
38
|
-
'-c', '1',
|
|
39
|
-
'-q',
|
|
40
|
-
'-',
|
|
30
|
+
'-d',
|
|
31
|
+
'-t', 'raw',
|
|
32
|
+
'-b', '16',
|
|
33
|
+
'-e', 'signed-integer',
|
|
34
|
+
'-r', '24000',
|
|
35
|
+
'-c', '1',
|
|
36
|
+
'-q',
|
|
37
|
+
'-',
|
|
41
38
|
]);
|
|
42
39
|
if (!this.micProcess.stdout) {
|
|
43
40
|
this.emit('error', new Error('Failed to start microphone'));
|
|
@@ -48,12 +45,12 @@ class AudioCapture extends events_1.EventEmitter {
|
|
|
48
45
|
this.emit('data', chunk);
|
|
49
46
|
}
|
|
50
47
|
});
|
|
51
|
-
|
|
48
|
+
|
|
52
49
|
let stderrOutput = '';
|
|
53
50
|
if (this.micProcess.stderr) {
|
|
54
51
|
this.micProcess.stderr.on('data', (chunk) => {
|
|
55
52
|
stderrOutput += chunk.toString();
|
|
56
|
-
|
|
53
|
+
|
|
57
54
|
if (stderrOutput.includes('ALSA') ||
|
|
58
55
|
stderrOutput.includes('no default audio device') ||
|
|
59
56
|
stderrOutput.includes('Unknown PCM') ||
|
|
@@ -64,9 +61,9 @@ class AudioCapture extends events_1.EventEmitter {
|
|
|
64
61
|
});
|
|
65
62
|
}
|
|
66
63
|
this.micProcess.on('error', (error) => {
|
|
67
|
-
|
|
64
|
+
|
|
68
65
|
if (error.code === 'ENOENT') {
|
|
69
|
-
|
|
66
|
+
|
|
70
67
|
if (!this.isRecording) {
|
|
71
68
|
this.emit('error', new Error('Audio capture tool (sox) not found. Please install sox.'));
|
|
72
69
|
}
|
|
@@ -81,15 +78,12 @@ class AudioCapture extends events_1.EventEmitter {
|
|
|
81
78
|
this.isPaused = false;
|
|
82
79
|
this.emit('started');
|
|
83
80
|
}
|
|
84
|
-
|
|
85
|
-
* Pause audio capture (for echo cancellation during playback)
|
|
86
|
-
* Kills the mic process to ensure no audio leaks through
|
|
87
|
-
*/
|
|
81
|
+
|
|
88
82
|
pause() {
|
|
89
83
|
if (!this.isRecording || this.isPaused)
|
|
90
84
|
return;
|
|
91
85
|
this.isPaused = true;
|
|
92
|
-
|
|
86
|
+
|
|
93
87
|
if (this.micProcess) {
|
|
94
88
|
try {
|
|
95
89
|
this.micProcess.kill('SIGKILL');
|
|
@@ -98,14 +92,12 @@ class AudioCapture extends events_1.EventEmitter {
|
|
|
98
92
|
this.micProcess = null;
|
|
99
93
|
}
|
|
100
94
|
}
|
|
101
|
-
|
|
102
|
-
* Resume audio capture after playback ends
|
|
103
|
-
*/
|
|
95
|
+
|
|
104
96
|
resume() {
|
|
105
97
|
if (!this.isPaused)
|
|
106
98
|
return;
|
|
107
99
|
this.isPaused = false;
|
|
108
|
-
|
|
100
|
+
|
|
109
101
|
this.spawnMic();
|
|
110
102
|
}
|
|
111
103
|
stop() {
|
|
@@ -136,4 +128,3 @@ class AudioCapture extends events_1.EventEmitter {
|
|
|
136
128
|
}
|
|
137
129
|
}
|
|
138
130
|
exports.AudioCapture = AudioCapture;
|
|
139
|
-
//# sourceMappingURL=audio-capture.js.map
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
* Audio Playback Module
|
|
4
|
-
* Smooth speaker output using sox with buffering to prevent audio cracks
|
|
5
|
-
*/
|
|
2
|
+
|
|
6
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
4
|
exports.AudioPlayback = void 0;
|
|
8
5
|
const child_process_1 = require("child_process");
|
|
@@ -11,7 +8,7 @@ class AudioPlayback extends events_1.EventEmitter {
|
|
|
11
8
|
playProcess = null;
|
|
12
9
|
isWritable = false;
|
|
13
10
|
config;
|
|
14
|
-
|
|
11
|
+
|
|
15
12
|
audioBuffer = [];
|
|
16
13
|
isPlaying = false;
|
|
17
14
|
totalBytesQueued = 0;
|
|
@@ -26,7 +23,7 @@ class AudioPlayback extends events_1.EventEmitter {
|
|
|
26
23
|
}
|
|
27
24
|
spawnProcess() {
|
|
28
25
|
if (this.playProcess) {
|
|
29
|
-
return;
|
|
26
|
+
return;
|
|
30
27
|
}
|
|
31
28
|
this.playProcess = (0, child_process_1.spawn)('play', [
|
|
32
29
|
'-t', 'raw',
|
|
@@ -35,26 +32,26 @@ class AudioPlayback extends events_1.EventEmitter {
|
|
|
35
32
|
'-r', String(this.config.sampleRate),
|
|
36
33
|
'-c', '1',
|
|
37
34
|
'-q',
|
|
38
|
-
'-',
|
|
35
|
+
'-',
|
|
39
36
|
], {
|
|
40
|
-
stdio: ['pipe', 'ignore', 'ignore']
|
|
37
|
+
stdio: ['pipe', 'ignore', 'ignore']
|
|
41
38
|
});
|
|
42
39
|
if (!this.playProcess.stdin) {
|
|
43
40
|
this.emit('error', new Error('Failed to start speaker'));
|
|
44
41
|
return;
|
|
45
42
|
}
|
|
46
|
-
|
|
43
|
+
|
|
47
44
|
this.playProcess.stdin.on('error', (error) => {
|
|
48
45
|
if (error.code !== 'EPIPE' && error.code !== 'ERR_STREAM_DESTROYED') {
|
|
49
46
|
this.emit('error', error);
|
|
50
47
|
}
|
|
51
48
|
});
|
|
52
|
-
|
|
49
|
+
|
|
53
50
|
let stderrOutput = '';
|
|
54
51
|
if (this.playProcess.stderr) {
|
|
55
52
|
this.playProcess.stderr.on('data', (chunk) => {
|
|
56
53
|
stderrOutput += chunk.toString();
|
|
57
|
-
|
|
54
|
+
|
|
58
55
|
if (stderrOutput.includes('ALSA') || stderrOutput.includes('no default audio device') || stderrOutput.includes('Unknown PCM')) {
|
|
59
56
|
this.emit('error', new Error('Audio device not available. WSL does not support direct audio access.'));
|
|
60
57
|
this.playProcess?.kill();
|
|
@@ -62,9 +59,9 @@ class AudioPlayback extends events_1.EventEmitter {
|
|
|
62
59
|
});
|
|
63
60
|
}
|
|
64
61
|
this.playProcess.on('error', (error) => {
|
|
65
|
-
|
|
62
|
+
|
|
66
63
|
if (error.code === 'ENOENT') {
|
|
67
|
-
|
|
64
|
+
|
|
68
65
|
if (!this.isPlaying) {
|
|
69
66
|
this.emit('error', new Error('Audio playback tool (play) not found. Please install sox.'));
|
|
70
67
|
}
|
|
@@ -74,20 +71,20 @@ class AudioPlayback extends events_1.EventEmitter {
|
|
|
74
71
|
});
|
|
75
72
|
this.playProcess.on('exit', (code) => {
|
|
76
73
|
if (code !== 0 && code !== null && stderrOutput.includes('ALSA')) {
|
|
77
|
-
|
|
74
|
+
|
|
78
75
|
this.emit('error', new Error('Audio device not available. WSL does not support direct audio access.'));
|
|
79
76
|
}
|
|
80
77
|
});
|
|
81
78
|
this.playProcess.on('close', () => {
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
|
|
80
|
+
|
|
84
81
|
if (!this.finishRequested) {
|
|
85
82
|
this.playProcess = null;
|
|
86
83
|
this.isWritable = false;
|
|
87
84
|
this.spawnProcess();
|
|
88
85
|
}
|
|
89
86
|
else {
|
|
90
|
-
|
|
87
|
+
|
|
91
88
|
this.playProcess = null;
|
|
92
89
|
this.isWritable = false;
|
|
93
90
|
}
|
|
@@ -95,16 +92,14 @@ class AudioPlayback extends events_1.EventEmitter {
|
|
|
95
92
|
this.isWritable = true;
|
|
96
93
|
this.emit('started');
|
|
97
94
|
}
|
|
98
|
-
|
|
99
|
-
* Queue audio for playback
|
|
100
|
-
*/
|
|
95
|
+
|
|
101
96
|
play(audioBuffer) {
|
|
102
97
|
if (this.finishRequested) {
|
|
103
|
-
return;
|
|
98
|
+
return;
|
|
104
99
|
}
|
|
105
100
|
this.totalBytesQueued += audioBuffer.length;
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
|
|
102
|
+
|
|
108
103
|
if (this.playProcess?.stdin && this.isWritable) {
|
|
109
104
|
try {
|
|
110
105
|
this.playProcess.stdin.write(audioBuffer);
|
|
@@ -115,10 +110,7 @@ class AudioPlayback extends events_1.EventEmitter {
|
|
|
115
110
|
}
|
|
116
111
|
this.isPlaying = true;
|
|
117
112
|
}
|
|
118
|
-
|
|
119
|
-
* Signal that we're done sending audio for this response.
|
|
120
|
-
* Closes stdin to signal end of stream, then waits for all audio to play.
|
|
121
|
-
*/
|
|
113
|
+
|
|
122
114
|
finishPlayback() {
|
|
123
115
|
if (this.totalBytesQueued === 0) {
|
|
124
116
|
this.resetForNextResponse();
|
|
@@ -126,54 +118,50 @@ class AudioPlayback extends events_1.EventEmitter {
|
|
|
126
118
|
return;
|
|
127
119
|
}
|
|
128
120
|
this.finishRequested = true;
|
|
129
|
-
|
|
130
|
-
|
|
121
|
+
|
|
122
|
+
|
|
131
123
|
if (this.playProcess?.stdin && this.isWritable) {
|
|
132
124
|
try {
|
|
133
125
|
this.playProcess.stdin.end();
|
|
134
126
|
this.isWritable = false;
|
|
135
127
|
}
|
|
136
128
|
catch {
|
|
137
|
-
|
|
129
|
+
|
|
138
130
|
}
|
|
139
131
|
}
|
|
140
|
-
|
|
141
|
-
|
|
132
|
+
|
|
133
|
+
|
|
142
134
|
const durationMs = (this.totalBytesQueued / (this.config.sampleRate * 2)) * 1000;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const waitTime = durationMs + 200;
|
|
146
|
-
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
const waitTime = durationMs + 200;
|
|
138
|
+
|
|
147
139
|
this.drainTimeout = setTimeout(() => {
|
|
148
140
|
this.drainTimeout = null;
|
|
149
|
-
|
|
141
|
+
|
|
150
142
|
this.spawnProcess();
|
|
151
143
|
this.resetForNextResponse();
|
|
152
144
|
this.emit('finished');
|
|
153
145
|
}, waitTime);
|
|
154
146
|
}
|
|
155
|
-
|
|
156
|
-
* Reset state for next response
|
|
157
|
-
*/
|
|
147
|
+
|
|
158
148
|
resetForNextResponse() {
|
|
159
149
|
this.audioBuffer = [];
|
|
160
150
|
this.totalBytesQueued = 0;
|
|
161
151
|
this.isPlaying = false;
|
|
162
152
|
this.finishRequested = false;
|
|
163
|
-
|
|
164
|
-
|
|
153
|
+
|
|
154
|
+
|
|
165
155
|
}
|
|
166
|
-
|
|
167
|
-
* Immediately stop playback (for barge-in)
|
|
168
|
-
*/
|
|
156
|
+
|
|
169
157
|
stop() {
|
|
170
158
|
if (this.drainTimeout) {
|
|
171
159
|
clearTimeout(this.drainTimeout);
|
|
172
160
|
this.drainTimeout = null;
|
|
173
161
|
}
|
|
174
|
-
|
|
162
|
+
|
|
175
163
|
this.finishRequested = false;
|
|
176
|
-
|
|
164
|
+
|
|
177
165
|
if (this.playProcess) {
|
|
178
166
|
try {
|
|
179
167
|
this.playProcess.kill('SIGKILL');
|
|
@@ -185,7 +173,7 @@ class AudioPlayback extends events_1.EventEmitter {
|
|
|
185
173
|
this.totalBytesQueued = 0;
|
|
186
174
|
this.isPlaying = false;
|
|
187
175
|
this.isWritable = false;
|
|
188
|
-
|
|
176
|
+
|
|
189
177
|
this.spawnProcess();
|
|
190
178
|
this.emit('stopped');
|
|
191
179
|
}
|
|
@@ -200,4 +188,3 @@ class AudioPlayback extends events_1.EventEmitter {
|
|
|
200
188
|
}
|
|
201
189
|
}
|
|
202
190
|
exports.AudioPlayback = AudioPlayback;
|
|
203
|
-
//# sourceMappingURL=audio-playback.js.map
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
* Cross-platform audio command detection
|
|
4
|
-
* Detects available audio tools on different platforms
|
|
5
|
-
*/
|
|
2
|
+
|
|
6
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
4
|
exports.detectAudioCommands = detectAudioCommands;
|
|
8
5
|
exports.getAudioInstallInstructions = getAudioInstallInstructions;
|
|
9
6
|
exports.getAudioError = getAudioError;
|
|
10
7
|
const os_1 = require("os");
|
|
11
8
|
const child_process_1 = require("child_process");
|
|
12
|
-
|
|
13
|
-
* Detect available audio commands for the current platform
|
|
14
|
-
*/
|
|
9
|
+
|
|
15
10
|
function detectAudioCommands() {
|
|
16
11
|
const os = (0, os_1.platform)();
|
|
17
12
|
if (os === 'win32') {
|
|
18
|
-
|
|
13
|
+
|
|
19
14
|
try {
|
|
20
15
|
(0, child_process_1.execSync)('sox --version', { stdio: 'ignore' });
|
|
21
16
|
(0, child_process_1.execSync)('play --version', { stdio: 'ignore' });
|
|
@@ -27,7 +22,7 @@ function detectAudioCommands() {
|
|
|
27
22
|
};
|
|
28
23
|
}
|
|
29
24
|
catch {
|
|
30
|
-
|
|
25
|
+
|
|
31
26
|
try {
|
|
32
27
|
(0, child_process_1.execSync)('ffmpeg -version', { stdio: 'ignore' });
|
|
33
28
|
return {
|
|
@@ -48,7 +43,7 @@ function detectAudioCommands() {
|
|
|
48
43
|
}
|
|
49
44
|
}
|
|
50
45
|
else {
|
|
51
|
-
|
|
46
|
+
|
|
52
47
|
try {
|
|
53
48
|
(0, child_process_1.execSync)('sox --version', { stdio: 'ignore' });
|
|
54
49
|
(0, child_process_1.execSync)('play --version', { stdio: 'ignore' });
|
|
@@ -69,9 +64,7 @@ function detectAudioCommands() {
|
|
|
69
64
|
}
|
|
70
65
|
}
|
|
71
66
|
}
|
|
72
|
-
|
|
73
|
-
* Get platform-specific installation instructions
|
|
74
|
-
*/
|
|
67
|
+
|
|
75
68
|
function getAudioInstallInstructions(commands) {
|
|
76
69
|
if (commands.available) {
|
|
77
70
|
return '';
|
|
@@ -86,9 +79,7 @@ function getAudioInstallInstructions(commands) {
|
|
|
86
79
|
return 'Audio tools not found. Install sox: sudo apt-get install sox libsox-fmt-all';
|
|
87
80
|
}
|
|
88
81
|
}
|
|
89
|
-
|
|
90
|
-
* Get audio command error message
|
|
91
|
-
*/
|
|
82
|
+
|
|
92
83
|
function getAudioError(commands) {
|
|
93
84
|
if (commands.available) {
|
|
94
85
|
return '';
|
|
@@ -96,4 +87,3 @@ function getAudioError(commands) {
|
|
|
96
87
|
const instructions = getAudioInstallInstructions(commands);
|
|
97
88
|
return `Audio tools (${commands.record}/${commands.play}) are not available.\n${instructions}`;
|
|
98
89
|
}
|
|
99
|
-
//# sourceMappingURL=audio-utils.js.map
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
* Echo Canceller Module
|
|
4
|
-
*
|
|
5
|
-
* Uses audio fingerprinting and correlation to detect when the mic
|
|
6
|
-
* is picking up what we're playing through the speakers.
|
|
7
|
-
*
|
|
8
|
-
* This is the "last line of defense" when hardware AEC isn't available.
|
|
9
|
-
*/
|
|
2
|
+
|
|
10
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
4
|
exports.EchoCanceller = void 0;
|
|
12
5
|
class EchoCanceller {
|
|
@@ -15,26 +8,24 @@ class EchoCanceller {
|
|
|
15
8
|
isPlayingAudio = false;
|
|
16
9
|
playbackEndTime = 0;
|
|
17
10
|
config;
|
|
18
|
-
|
|
11
|
+
|
|
19
12
|
recentPlaybackEnergy = 0;
|
|
20
13
|
constructor(config = {}) {
|
|
21
14
|
this.config = {
|
|
22
15
|
sampleRate: 24000,
|
|
23
|
-
bufferDurationMs: 1500,
|
|
24
|
-
correlationThreshold: 0.35,
|
|
25
|
-
energyThreshold: 0.01,
|
|
16
|
+
bufferDurationMs: 1500,
|
|
17
|
+
correlationThreshold: 0.35,
|
|
18
|
+
energyThreshold: 0.01,
|
|
26
19
|
...config,
|
|
27
20
|
};
|
|
28
21
|
const bufferSize = Math.floor(this.config.sampleRate * this.config.bufferDurationMs / 1000);
|
|
29
22
|
this.playbackBuffer = new Float32Array(bufferSize);
|
|
30
23
|
}
|
|
31
|
-
|
|
32
|
-
* Call this whenever we send audio to the speakers
|
|
33
|
-
*/
|
|
24
|
+
|
|
34
25
|
feedPlayback(audioBuffer) {
|
|
35
26
|
this.isPlayingAudio = true;
|
|
36
27
|
this.playbackEndTime = Date.now();
|
|
37
|
-
|
|
28
|
+
|
|
38
29
|
const samples = this.pcm16ToFloat(audioBuffer);
|
|
39
30
|
for (let i = 0; i < samples.length; i++) {
|
|
40
31
|
const pos = this.playbackWritePos;
|
|
@@ -43,60 +34,53 @@ class EchoCanceller {
|
|
|
43
34
|
}
|
|
44
35
|
this.playbackWritePos = (this.playbackWritePos + 1) % this.playbackBuffer.length;
|
|
45
36
|
}
|
|
46
|
-
|
|
37
|
+
|
|
47
38
|
this.recentPlaybackEnergy = this.calculateEnergy(samples);
|
|
48
39
|
}
|
|
49
|
-
|
|
50
|
-
* Mark playback as stopped
|
|
51
|
-
*/
|
|
40
|
+
|
|
52
41
|
markPlaybackStopped() {
|
|
53
42
|
this.isPlayingAudio = false;
|
|
54
43
|
this.playbackEndTime = Date.now();
|
|
55
44
|
}
|
|
56
|
-
|
|
57
|
-
* Check if mic audio is likely echo from our playback
|
|
58
|
-
* Returns true if we should DROP this audio (it's echo)
|
|
59
|
-
*/
|
|
45
|
+
|
|
60
46
|
isEcho(micBuffer) {
|
|
61
|
-
|
|
47
|
+
|
|
62
48
|
const timeSincePlayback = Date.now() - this.playbackEndTime;
|
|
63
49
|
if (timeSincePlayback > 2000) {
|
|
64
50
|
return false;
|
|
65
51
|
}
|
|
66
52
|
const micSamples = this.pcm16ToFloat(micBuffer);
|
|
67
53
|
const micEnergy = this.calculateEnergy(micSamples);
|
|
68
|
-
|
|
54
|
+
|
|
69
55
|
if (micEnergy < this.config.energyThreshold) {
|
|
70
56
|
return false;
|
|
71
57
|
}
|
|
72
|
-
|
|
58
|
+
|
|
73
59
|
if (this.isPlayingAudio) {
|
|
74
|
-
|
|
75
|
-
|
|
60
|
+
|
|
61
|
+
|
|
76
62
|
if (micEnergy < this.recentPlaybackEnergy * 3) {
|
|
77
|
-
return true;
|
|
63
|
+
return true;
|
|
78
64
|
}
|
|
79
65
|
}
|
|
80
|
-
|
|
66
|
+
|
|
81
67
|
const correlation = this.crossCorrelate(micSamples);
|
|
82
68
|
if (correlation > this.config.correlationThreshold) {
|
|
83
|
-
return true;
|
|
69
|
+
return true;
|
|
84
70
|
}
|
|
85
71
|
return false;
|
|
86
72
|
}
|
|
87
|
-
|
|
88
|
-
* Calculate cross-correlation between mic audio and playback buffer
|
|
89
|
-
*/
|
|
73
|
+
|
|
90
74
|
crossCorrelate(micSamples) {
|
|
91
75
|
if (micSamples.length === 0)
|
|
92
76
|
return 0;
|
|
93
77
|
const playbackLen = this.playbackBuffer.length;
|
|
94
78
|
const micLen = micSamples.length;
|
|
95
|
-
|
|
79
|
+
|
|
96
80
|
let maxCorrelation = 0;
|
|
97
|
-
|
|
81
|
+
|
|
98
82
|
const maxLag = Math.min(12000, playbackLen - micLen);
|
|
99
|
-
const lagStep = 100;
|
|
83
|
+
const lagStep = 100;
|
|
100
84
|
for (let lag = 0; lag < maxLag; lag += lagStep) {
|
|
101
85
|
let sum = 0;
|
|
102
86
|
let playbackSum = 0;
|
|
@@ -111,7 +95,7 @@ class EchoCanceller {
|
|
|
111
95
|
micSum += micSample * micSample;
|
|
112
96
|
}
|
|
113
97
|
}
|
|
114
|
-
|
|
98
|
+
|
|
115
99
|
const denom = Math.sqrt(playbackSum * micSum);
|
|
116
100
|
if (denom > 0) {
|
|
117
101
|
const correlation = Math.abs(sum / denom);
|
|
@@ -120,9 +104,7 @@ class EchoCanceller {
|
|
|
120
104
|
}
|
|
121
105
|
return maxCorrelation;
|
|
122
106
|
}
|
|
123
|
-
|
|
124
|
-
* Calculate RMS energy of samples
|
|
125
|
-
*/
|
|
107
|
+
|
|
126
108
|
calculateEnergy(samples) {
|
|
127
109
|
if (samples.length === 0)
|
|
128
110
|
return 0;
|
|
@@ -135,9 +117,7 @@ class EchoCanceller {
|
|
|
135
117
|
}
|
|
136
118
|
return Math.sqrt(sum / samples.length);
|
|
137
119
|
}
|
|
138
|
-
|
|
139
|
-
* Convert PCM16 buffer to float samples
|
|
140
|
-
*/
|
|
120
|
+
|
|
141
121
|
pcm16ToFloat(buffer) {
|
|
142
122
|
const samples = new Float32Array(buffer.length / 2);
|
|
143
123
|
for (let i = 0; i < samples.length; i++) {
|
|
@@ -145,9 +125,7 @@ class EchoCanceller {
|
|
|
145
125
|
}
|
|
146
126
|
return samples;
|
|
147
127
|
}
|
|
148
|
-
|
|
149
|
-
* Clear the playback buffer (e.g., on new session)
|
|
150
|
-
*/
|
|
128
|
+
|
|
151
129
|
reset() {
|
|
152
130
|
this.playbackBuffer.fill(0);
|
|
153
131
|
this.playbackWritePos = 0;
|
|
@@ -156,4 +134,3 @@ class EchoCanceller {
|
|
|
156
134
|
}
|
|
157
135
|
}
|
|
158
136
|
exports.EchoCanceller = EchoCanceller;
|
|
159
|
-
//# sourceMappingURL=echo-canceller.js.map
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
* GUI Server - Serves particle visualization and broadcasts state updates
|
|
4
|
-
*/
|
|
2
|
+
|
|
5
3
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
5
|
};
|
|
@@ -29,7 +27,7 @@ class GUIServer extends events_1.EventEmitter {
|
|
|
29
27
|
}
|
|
30
28
|
async start() {
|
|
31
29
|
return new Promise((resolve, reject) => {
|
|
32
|
-
|
|
30
|
+
|
|
33
31
|
this.httpServer = (0, http_1.createServer)((req, res) => {
|
|
34
32
|
if (req.url === '/' || req.url?.startsWith('/?')) {
|
|
35
33
|
try {
|
|
@@ -48,11 +46,11 @@ class GUIServer extends events_1.EventEmitter {
|
|
|
48
46
|
res.end('Not found');
|
|
49
47
|
}
|
|
50
48
|
});
|
|
51
|
-
|
|
49
|
+
|
|
52
50
|
this.wss = new ws_1.WebSocketServer({ server: this.httpServer });
|
|
53
51
|
this.wss.on('connection', (ws) => {
|
|
54
52
|
this.clients.add(ws);
|
|
55
|
-
|
|
53
|
+
|
|
56
54
|
ws.send(JSON.stringify({ type: 'state', state: this.currentState }));
|
|
57
55
|
ws.on('close', () => {
|
|
58
56
|
this.clients.delete(ws);
|
|
@@ -69,7 +67,7 @@ class GUIServer extends events_1.EventEmitter {
|
|
|
69
67
|
});
|
|
70
68
|
this.httpServer.on('error', (err) => {
|
|
71
69
|
if (err.code === 'EADDRINUSE') {
|
|
72
|
-
|
|
70
|
+
|
|
73
71
|
this.port++;
|
|
74
72
|
this.httpServer?.close();
|
|
75
73
|
this.start().then(resolve).catch(reject);
|
|
@@ -93,45 +91,41 @@ class GUIServer extends events_1.EventEmitter {
|
|
|
93
91
|
command = 'cmd';
|
|
94
92
|
args = ['/c', 'start', url];
|
|
95
93
|
break;
|
|
96
|
-
default:
|
|
94
|
+
default:
|
|
97
95
|
command = 'xdg-open';
|
|
98
96
|
args = [url];
|
|
99
97
|
break;
|
|
100
98
|
}
|
|
101
|
-
|
|
99
|
+
|
|
102
100
|
try {
|
|
103
101
|
const proc = (0, child_process_1.spawn)(command, args, {
|
|
104
102
|
detached: true,
|
|
105
103
|
stdio: 'ignore'
|
|
106
104
|
});
|
|
107
|
-
|
|
105
|
+
|
|
108
106
|
proc.on('error', (error) => {
|
|
109
|
-
|
|
107
|
+
|
|
110
108
|
if (error.code === 'ENOENT') {
|
|
111
|
-
|
|
109
|
+
|
|
112
110
|
console.log(chalk_1.default.gray(`\nGUI available at: http://localhost:${this.port}`));
|
|
113
111
|
console.log(chalk_1.default.gray('(Browser not auto-opened - open manually if needed)'));
|
|
114
112
|
}
|
|
115
|
-
|
|
113
|
+
|
|
116
114
|
});
|
|
117
|
-
|
|
115
|
+
|
|
118
116
|
proc.unref();
|
|
119
117
|
}
|
|
120
118
|
catch (error) {
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
|
|
120
|
+
|
|
123
121
|
}
|
|
124
122
|
}
|
|
125
|
-
|
|
126
|
-
* Broadcast state change to all connected clients
|
|
127
|
-
*/
|
|
123
|
+
|
|
128
124
|
setState(state) {
|
|
129
125
|
this.currentState = state;
|
|
130
126
|
this.broadcast({ type: 'state', state });
|
|
131
127
|
}
|
|
132
|
-
|
|
133
|
-
* Broadcast audio level to all connected clients
|
|
134
|
-
*/
|
|
128
|
+
|
|
135
129
|
setAudioLevel(level) {
|
|
136
130
|
this.broadcast({ type: 'audio', level: Math.min(1, Math.max(0, level)) });
|
|
137
131
|
}
|
|
@@ -143,7 +137,7 @@ class GUIServer extends events_1.EventEmitter {
|
|
|
143
137
|
client.send(message);
|
|
144
138
|
}
|
|
145
139
|
catch {
|
|
146
|
-
|
|
140
|
+
|
|
147
141
|
}
|
|
148
142
|
}
|
|
149
143
|
}
|
|
@@ -169,4 +163,3 @@ class GUIServer extends events_1.EventEmitter {
|
|
|
169
163
|
}
|
|
170
164
|
}
|
|
171
165
|
exports.GUIServer = GUIServer;
|
|
172
|
-
//# sourceMappingURL=gui-server.js.map
|