accelr8 3.1.1 → 3.2.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.
@@ -1175,8 +1175,48 @@ function microLine(w, engine, frame2, info) {
1175
1175
 
1176
1176
  // src/voice.ts
1177
1177
  import { spawn, spawnSync } from "node:child_process";
1178
+ import fs2 from "node:fs";
1179
+ import path2 from "node:path";
1180
+
1181
+ // src/voicelog.ts
1178
1182
  import fs from "node:fs";
1183
+ import os from "node:os";
1179
1184
  import path from "node:path";
1185
+ var logPath = null;
1186
+ var enabled = true;
1187
+ function file() {
1188
+ if (!logPath) {
1189
+ const dir = path.join(os.homedir(), ".accelr8");
1190
+ try {
1191
+ fs.mkdirSync(dir, { recursive: true });
1192
+ } catch {
1193
+ }
1194
+ logPath = path.join(dir, "voice.log");
1195
+ }
1196
+ return logPath;
1197
+ }
1198
+ function stamp(nowMs) {
1199
+ const d = new Date(nowMs);
1200
+ const p = (n, w = 2) => String(n).padStart(w, "0");
1201
+ return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
1202
+ }
1203
+ function vlog(nowMs, stage, msg, data) {
1204
+ if (!enabled) return;
1205
+ try {
1206
+ const extra = data ? " " + Object.entries(data).map(([k, v]) => `${k}=${String(v).slice(0, 80)}`).join(" ") : "";
1207
+ fs.appendFileSync(file(), `${stamp(nowMs)} [${stage}] ${msg}${extra}
1208
+ `);
1209
+ } catch {
1210
+ }
1211
+ }
1212
+ function vlogSession(nowMs, header) {
1213
+ vlog(nowMs, "session", "\u2500\u2500\u2500\u2500 voice session \u2500\u2500\u2500\u2500", header);
1214
+ }
1215
+ function voiceLogPath() {
1216
+ return file();
1217
+ }
1218
+
1219
+ // src/voice.ts
1180
1220
  var SAMPLE_RATE = 16e3;
1181
1221
  var CHUNK_BYTES = 1280;
1182
1222
  var KILL_GRACE_MS = 300;
@@ -1235,13 +1275,13 @@ function detectVoiceTools() {
1235
1275
  }
1236
1276
  function which(bin) {
1237
1277
  const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
1238
- for (const dir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
1278
+ for (const dir of (process.env.PATH ?? "").split(path2.delimiter).filter(Boolean)) {
1239
1279
  for (const ext of exts) {
1240
- const candidate = path.join(dir, bin + ext);
1280
+ const candidate = path2.join(dir, bin + ext);
1241
1281
  try {
1242
- if (!fs.existsSync(candidate)) continue;
1243
- fs.accessSync(candidate, fs.constants.X_OK);
1244
- if (fs.statSync(candidate).isFile()) return candidate;
1282
+ if (!fs2.existsSync(candidate)) continue;
1283
+ fs2.accessSync(candidate, fs2.constants.X_OK);
1284
+ if (fs2.statSync(candidate).isFile()) return candidate;
1245
1285
  } catch {
1246
1286
  }
1247
1287
  }
@@ -1309,12 +1349,15 @@ var VoiceCapture = class {
1309
1349
  try {
1310
1350
  child = spawn(this.tools.ffmpegPath, args, { stdio: ["ignore", "pipe", "pipe"] });
1311
1351
  } catch (e) {
1352
+ vlog(Date.now(), "capture", "ffmpeg spawn threw", { backend, err: errMsg(e) });
1312
1353
  queueMicrotask(() => opts.onError("mic capture failed to start: " + errMsg(e)));
1313
1354
  return null;
1314
1355
  }
1356
+ vlog(Date.now(), "capture", "ffmpeg spawned", { backend, device: opts.device ?? "default", args: args.join(" ") });
1315
1357
  const s = { child, onChunk: opts.onChunk, buf: Buffer.alloc(0), stderrTail: "", produced: false, stopped: false };
1316
1358
  child.stdout?.on("data", (chunk) => {
1317
1359
  if (s.stopped) return;
1360
+ if (!s.produced) vlog(Date.now(), "capture", "first audio bytes from mic", { backend, bytes: chunk.length });
1318
1361
  s.produced = true;
1319
1362
  s.buf = s.buf.length ? Buffer.concat([s.buf, chunk]) : chunk;
1320
1363
  const whole = Math.floor(s.buf.length / CHUNK_BYTES) * CHUNK_BYTES;
@@ -1329,11 +1372,14 @@ var VoiceCapture = class {
1329
1372
  child.on("error", (e) => {
1330
1373
  if (s.stopped || this.session !== s) return;
1331
1374
  this.session = null;
1375
+ vlog(Date.now(), "capture", "ffmpeg error", { backend, code: e.code, produced: s.produced, stderr: s.stderrTail.trim() });
1332
1376
  opts.onError(e.code === "ENOENT" ? "ffmpeg not found" : "mic capture error: " + errMsg(e));
1333
1377
  });
1334
1378
  child.on("close", (code) => {
1335
1379
  if (s.stopped || this.session !== s) return;
1380
+ vlog(Date.now(), "capture", "ffmpeg exited", { backend, code, producedAudio: s.produced, stderr: s.stderrTail.trim() });
1336
1381
  if (backend === "primary" && process.platform === "linux" && !s.produced && code !== 0) {
1382
+ vlog(Date.now(), "capture", "alsa failed, retrying on pulse");
1337
1383
  const pulse = captureArgs("linux", opts.device).map((a) => a === "alsa" ? "pulse" : a);
1338
1384
  this.session = this.spawnSession(opts, pulse, "pulse");
1339
1385
  return;
@@ -1438,22 +1484,32 @@ var VoicePlayback = class {
1438
1484
  this.players.clear();
1439
1485
  }
1440
1486
  spawnPlayer(speakerId) {
1487
+ const args = playbackArgs(this.tools.ffplayPath);
1441
1488
  let child;
1442
1489
  try {
1443
- child = spawn(this.tools.ffplayPath, playbackArgs(this.tools.ffplayPath), { stdio: ["pipe", "ignore", "ignore"] });
1444
- } catch {
1490
+ child = spawn(this.tools.ffplayPath, args, { stdio: ["pipe", "ignore", "pipe"] });
1491
+ } catch (e) {
1492
+ vlog(Date.now(), "playback", "ffplay spawn threw", { speaker: speakerId, err: errMsg(e) });
1445
1493
  return null;
1446
1494
  }
1495
+ vlog(Date.now(), "playback", "ffplay spawned", { speaker: speakerId, args: args.join(" ") });
1447
1496
  const p = { child, alive: true, idleTimer: null, pre: [], flowing: false };
1497
+ let stderrTail = "";
1498
+ child.stderr?.setEncoding("utf8");
1499
+ child.stderr?.on("data", (c) => {
1500
+ stderrTail = (stderrTail + c).slice(-STDERR_TAIL);
1501
+ });
1448
1502
  child.stdin?.on("error", () => {
1449
1503
  p.alive = false;
1450
1504
  });
1451
- child.on("error", () => {
1505
+ child.on("error", (e) => {
1452
1506
  p.alive = false;
1507
+ vlog(Date.now(), "playback", "ffplay error", { speaker: speakerId, code: e.code, msg: errMsg(e) });
1453
1508
  if (this.players.get(speakerId) === p) this.reap(speakerId, p);
1454
1509
  });
1455
- child.on("close", () => {
1510
+ child.on("close", (code) => {
1456
1511
  p.alive = false;
1512
+ vlog(Date.now(), "playback", "ffplay exited", { speaker: speakerId, code, stderr: stderrTail.trim() });
1457
1513
  if (this.players.get(speakerId) === p) {
1458
1514
  if (p.idleTimer) {
1459
1515
  clearTimeout(p.idleTimer);
@@ -1937,6 +1993,8 @@ var voiceCapture = null;
1937
1993
  var voicePlayback = null;
1938
1994
  var voiceDevice = process.env.ACCELR8_MIC || void 0;
1939
1995
  var voiceRetries = 0;
1996
+ var voiceSent = 0;
1997
+ var voiceRecv = /* @__PURE__ */ new Map();
1940
1998
  function autoPickMic() {
1941
1999
  if (process.platform === "darwin") {
1942
2000
  const mics = listMicDevices();
@@ -1958,7 +2016,11 @@ function startCapture() {
1958
2016
  device: voiceDevice,
1959
2017
  onChunk: (b64) => {
1960
2018
  voiceRetries = 0;
1961
- if (voiceTransmit) sendMsg({ t: "voiceFrame", data: b64 });
2019
+ if (!voiceTransmit) return;
2020
+ voiceSent++;
2021
+ if (voiceSent === 1) vlog(Date.now(), "transmit", "first frame sent to server");
2022
+ else if (voiceSent % 125 === 0) vlog(Date.now(), "transmit", "still sending", { frames: voiceSent });
2023
+ sendMsg({ t: "voiceFrame", data: b64 });
1962
2024
  },
1963
2025
  onError: (m) => {
1964
2026
  if (!voiceTransmit) return;
@@ -1991,12 +2053,25 @@ function toggleVoice() {
1991
2053
  voiceCapture = new VoiceCapture(voiceTools);
1992
2054
  voiceOn = true;
1993
2055
  voiceTransmit = false;
2056
+ voiceSent = 0;
2057
+ voiceRecv.clear();
1994
2058
  if (voiceDevice === void 0 && voiceTools.capture) voiceDevice = autoPickMic();
2059
+ vlogSession(Date.now(), {
2060
+ platform: process.platform,
2061
+ version: PROTO,
2062
+ name: me.name,
2063
+ ffmpeg: voiceTools.ffmpegPath ?? "MISSING",
2064
+ ffplay: voiceTools.ffplayPath ?? "MISSING",
2065
+ capture: voiceTools.capture,
2066
+ playback: voiceTools.playback,
2067
+ mic: voiceDevice ?? "default"
2068
+ });
1995
2069
  sendMsg({ t: "voiceJoin" });
1996
2070
  state.status = voiceTools.capture ? "voice on \xB7 /talk (or ctrl-g) to speak" : "voice on \xB7 listen only (no mic found)";
1997
2071
  }
1998
2072
  function leaveVoice() {
1999
2073
  if (!voiceOn) return;
2074
+ vlog(Date.now(), "session", "leaving voice", { sent: voiceSent, recvSpeakers: voiceRecv.size, recvTotal: [...voiceRecv.values()].reduce((a, b) => a + b, 0) });
2000
2075
  voiceCapture?.stop();
2001
2076
  voicePlayback?.stopAll();
2002
2077
  voiceCapture = null;
@@ -2452,7 +2527,12 @@ function handleServer(msg) {
2452
2527
  state.voicePeers = Array.isArray(msg.peers) ? msg.peers : [];
2453
2528
  break;
2454
2529
  case "voiceFrame":
2455
- if (voiceOn) voicePlayback?.feed(msg.sid, msg.data);
2530
+ if (voiceOn) {
2531
+ const n = (voiceRecv.get(msg.sid) ?? 0) + 1;
2532
+ voiceRecv.set(msg.sid, n);
2533
+ if (n === 1) vlog(Date.now(), "receive", "first frame received from a peer", { sid: msg.sid.slice(0, 8) });
2534
+ voicePlayback?.feed(msg.sid, msg.data);
2535
+ }
2456
2536
  break;
2457
2537
  case "error": {
2458
2538
  if (msg.code === "chatRate" || msg.code === "msgTooLarge") {
@@ -3947,6 +4027,17 @@ function slashCommand(text) {
3947
4027
  toggleVoice();
3948
4028
  } else if (cmd === "/talk" || cmd === "/mute") {
3949
4029
  toggleTalk();
4030
+ } else if (cmd === "/voice debug") {
4031
+ const recvTotal = [...voiceRecv.values()].reduce((a, b) => a + b, 0);
4032
+ sidecarBlock = { title: "voice debug", at: Date.now(), lines: [
4033
+ `on: ${voiceOn} \xB7 live: ${voiceTransmit}`,
4034
+ `tools: ffmpeg ${voiceTools?.ffmpegPath ? "ok" : "MISSING"} \xB7 ffplay ${voiceTools?.ffplayPath ? "ok" : "MISSING"}`,
4035
+ `mic: ${voiceDevice ?? "default"} \xB7 peers: ${state.voicePeers.length}`,
4036
+ `sent: ${voiceSent} \xB7 received: ${recvTotal} from ${voiceRecv.size}`,
4037
+ `log: ${voiceLogPath()}`,
4038
+ "send me that file if voice fails"
4039
+ ] };
4040
+ state.status = "voice debug \xB7 see the panel \xB7 log at ~/.accelr8/voice.log";
3950
4041
  } else if (cmd === "/voice mics" || cmd === "/mics") {
3951
4042
  const mics = listMicDevices();
3952
4043
  sidecarBlock = { title: "microphones", at: Date.now(), lines: mics.length ? mics.map((m) => `${m.index} ${m.name}${voiceDevice === m.index ? " \xB7 selected" : ""}`).concat("use: /voice mic <index>") : ["none found (need ffmpeg)"] };