accelr8 3.8.1 → 3.8.2

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.
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  DEFAULT_SERVER,
9
9
  VERSION
10
- } from "./chunk-UTWXIX5N.js";
10
+ } from "./chunk-7JSMON7M.js";
11
11
 
12
12
  // src/radio/app.ts
13
13
  import tk from "terminal-kit";
@@ -318,7 +318,7 @@ var matrix = {
318
318
  frame(tMs, w, h) {
319
319
  const out = new Array(w * h);
320
320
  const t = tMs / 1e3;
321
- const CH = "01<>+*=~^:;|/\\ACELR8#$%&?!";
321
+ const CH2 = "01<>+*=~^:;|/\\ACELR8#$%&?!";
322
322
  for (let x = 0; x < w; x++) {
323
323
  let h0 = 0, l0 = 1, h1 = 0, l1 = 1;
324
324
  for (let d = 0; d < 2; d++) {
@@ -345,7 +345,7 @@ var matrix = {
345
345
  continue;
346
346
  }
347
347
  const rate = 1.5 + 4 * hash2(x, y);
348
- const ch = CH[Math.floor(hashU(x * 31 + y * 57 + Math.floor(t * rate) * 7919) * CH.length)];
348
+ const ch = CH2[Math.floor(hashU(x * 31 + y * 57 + Math.floor(t * rate) * 7919) * CH2.length)];
349
349
  const isHead = q0 > 0 && d0 < 1 || q1 > 0 && d1 < 1;
350
350
  out[y * w + x] = isHead ? { ch, color: col(200 + 55 * q, 252, 252) } : { ch, color: col(18 + 40 * q, 55 + 155 * q, 60 + 195 * q) };
351
351
  }
@@ -5345,123 +5345,183 @@ function renderOverlay(ctx, opts) {
5345
5345
  }
5346
5346
 
5347
5347
  // src/radio/audio.ts
5348
- import { spawn as spawn2 } from "node:child_process";
5348
+ import { spawn as spawn2, spawnSync } from "node:child_process";
5349
+ var SR = 44100;
5350
+ var CH = 2;
5349
5351
  function nextAudioAction(sched, serverNowMs, mem, driftBudgetMs = 1500) {
5350
5352
  const pos = nowPlaying(sched, serverNowMs);
5351
5353
  if (!pos || !pos.item.url) return { kind: "none" };
5352
- const trackChanged = pos.item.id !== mem.playingTrackId;
5353
- if (trackChanged || !mem.procAlive) {
5354
- return { kind: "play", track: pos.item, offsetMs: pos.offsetMs };
5354
+ if (pos.item.id !== mem.playingTrackId || !mem.procAlive) return { kind: "play", track: pos.item, offsetMs: pos.offsetMs };
5355
+ const implied = mem.spawnedOffsetMs + (serverNowMs - mem.spawnedAtServerMs);
5356
+ if (Math.abs(implied - pos.offsetMs) > driftBudgetMs) return { kind: "play", track: pos.item, offsetMs: pos.offsetMs };
5357
+ return { kind: "none" };
5358
+ }
5359
+ function applyGain(buf, fromGain, target, rampPerSample = 1 / 1400) {
5360
+ let g = fromGain;
5361
+ for (let i = 0; i + 1 < buf.length; i += 2) {
5362
+ if (g < target) g = g + rampPerSample > target ? target : g + rampPerSample;
5363
+ else if (g > target) g = g - rampPerSample < target ? target : g - rampPerSample;
5364
+ let s = Math.round(buf.readInt16LE(i) * g);
5365
+ if (s > 32767) s = 32767;
5366
+ else if (s < -32768) s = -32768;
5367
+ buf.writeInt16LE(s, i);
5355
5368
  }
5356
- const impliedOffset = mem.spawnedOffsetMs + (serverNowMs - mem.spawnedAtServerMs);
5357
- if (Math.abs(impliedOffset - pos.offsetMs) > driftBudgetMs) {
5358
- return { kind: "play", track: pos.item, offsetMs: pos.offsetMs };
5369
+ return g;
5370
+ }
5371
+ function decoderArgs(url, offsetMs) {
5372
+ const args = ["-hide_banner", "-loglevel", "error", "-re"];
5373
+ if (offsetMs > 0) args.push("-ss", (offsetMs / 1e3).toFixed(3));
5374
+ args.push("-i", url, "-f", "s16le", "-ar", String(SR), "-ac", String(CH), "-");
5375
+ return args;
5376
+ }
5377
+ var chFlagCache = null;
5378
+ function ffplayStereoFlag(ffplayPath) {
5379
+ if (chFlagCache) return chFlagCache;
5380
+ let modern = true;
5381
+ try {
5382
+ const out = spawnSync(ffplayPath, ["-hide_banner", "-version"], { encoding: "utf8", timeout: 3e3 });
5383
+ const m = ((out.stdout || "") + (out.stderr || "")).match(/version\s+n?(\d+)\.(\d+)/i);
5384
+ if (m) {
5385
+ const major = +m[1], minor = +m[2];
5386
+ modern = major > 5 || major === 5 && minor >= 1;
5387
+ }
5388
+ } catch {
5359
5389
  }
5360
- return { kind: "none" };
5390
+ chFlagCache = modern ? ["-ch_layout", "stereo"] : ["-ac", String(CH)];
5391
+ return chFlagCache;
5361
5392
  }
5362
- function ffplayArgs(url, offsetMs, volume = 70) {
5363
- const args = [
5393
+ function sinkArgs(ffplayPath) {
5394
+ return [
5364
5395
  "-hide_banner",
5365
5396
  "-loglevel",
5366
5397
  "error",
5367
5398
  "-nodisp",
5368
- "-autoexit",
5369
- "-volume",
5370
- String(Math.max(0, Math.min(100, Math.round(volume))))
5399
+ "-sync",
5400
+ "audio",
5401
+ "-infbuf",
5402
+ "-f",
5403
+ "s16le",
5404
+ "-ar",
5405
+ String(SR),
5406
+ ...ffplayStereoFlag(ffplayPath),
5407
+ "-i",
5408
+ "-"
5371
5409
  ];
5372
- if (offsetMs > 0) args.push("-ss", (offsetMs / 1e3).toFixed(3));
5373
- args.push("-i", url);
5374
- return args;
5375
5410
  }
5376
5411
  var SyncedAudio = class {
5377
- proc = null;
5412
+ sink = null;
5413
+ decoder = null;
5378
5414
  mem = { playingTrackId: null, spawnedAtServerMs: 0, spawnedOffsetMs: 0, procAlive: false };
5379
5415
  opts = null;
5380
5416
  timer = null;
5381
- vol = 70;
5382
- // 0..100
5417
+ gain = 0.7;
5418
+ // current (ramping) gain
5419
+ target = 0.7;
5420
+ // desired volume (0..1)
5383
5421
  muted = false;
5384
5422
  start(opts) {
5385
5423
  this.stop();
5386
5424
  this.opts = opts;
5425
+ this.startSink();
5387
5426
  this.tick();
5388
5427
  this.timer = setInterval(() => this.tick(), 1e3);
5389
5428
  }
5390
- /** Swap in a new schedule (worker pushed a new playlist) without a restart. */
5391
5429
  setSchedule(sched) {
5392
5430
  if (this.opts) this.opts.schedule = sched;
5393
5431
  }
5394
5432
  get volume() {
5395
- return this.vol;
5433
+ return Math.round(this.target * 100);
5396
5434
  }
5397
5435
  get isMuted() {
5398
5436
  return this.muted;
5399
5437
  }
5400
- /** Set volume 0..100. Respawns the current track at its live offset so the
5401
- * change is audible immediately (unless muted). */
5438
+ get nowTrackId() {
5439
+ return this.mem.playingTrackId;
5440
+ }
5441
+ /** Smooth: just move the target; the per-chunk gain ramp glides to it. */
5402
5442
  setVolume(v) {
5403
- this.vol = Math.max(0, Math.min(100, Math.round(v)));
5404
- if (!this.muted) this.respawnCurrent();
5443
+ this.target = Math.max(0, Math.min(100, Math.round(v))) / 100;
5444
+ this.muted = false;
5405
5445
  }
5406
- /** Toggle mute. Muting stops the process (no CPU); unmuting resyncs + resumes. */
5407
5446
  toggleMute() {
5408
5447
  this.muted = !this.muted;
5409
- if (this.muted) {
5410
- this.killProc();
5411
- this.mem.playingTrackId = null;
5412
- this.mem.procAlive = false;
5413
- } else this.tick();
5414
5448
  return this.muted;
5415
5449
  }
5416
- respawnCurrent() {
5450
+ // gain ramps to 0 / back, no cut
5451
+ effTarget() {
5452
+ return this.muted ? 0 : this.target;
5453
+ }
5454
+ startSink() {
5417
5455
  if (!this.opts) return;
5418
- const pos = nowPlaying(this.opts.schedule, this.opts.serverNow());
5419
- if (pos && pos.item.url) this.spawn(pos.item, pos.offsetMs);
5456
+ const sink = spawn2(this.opts.ffplayPath, sinkArgs(this.opts.ffplayPath), { stdio: ["pipe", "ignore", "pipe"] });
5457
+ this.sink = sink;
5458
+ sink.stdin?.on("error", () => {
5459
+ });
5460
+ sink.stdin?.on("drain", () => this.decoder?.stdout?.resume());
5461
+ let err = "";
5462
+ sink.stderr?.on("data", (d) => {
5463
+ err = (err + d.toString()).slice(-200);
5464
+ });
5465
+ sink.on("exit", () => {
5466
+ if (this.sink !== sink) return;
5467
+ this.sink = null;
5468
+ if (err) this.opts?.onError?.(err.split("\n").pop() || err);
5469
+ if (this.opts) {
5470
+ this.startSink();
5471
+ this.mem.procAlive = false;
5472
+ }
5473
+ });
5420
5474
  }
5421
5475
  tick() {
5422
- if (!this.opts || this.muted) return;
5476
+ if (!this.opts) return;
5477
+ if (!this.sink) this.startSink();
5423
5478
  const action = nextAudioAction(this.opts.schedule, this.opts.serverNow(), this.mem);
5424
- if (action.kind === "play") this.spawn(action.track, action.offsetMs);
5479
+ if (action.kind === "play") this.spawnDecoder(action.track, action.offsetMs);
5425
5480
  }
5426
- spawn(track, offsetMs) {
5481
+ spawnDecoder(track, offsetMs) {
5427
5482
  if (!this.opts) return;
5428
- this.killProc();
5429
- const proc = spawn2(this.opts.ffplayPath, ffplayArgs(track.url, offsetMs, this.vol), { stdio: ["ignore", "ignore", "pipe"] });
5430
- this.proc = proc;
5483
+ this.killDecoder();
5484
+ const dec = spawn2(this.opts.ffmpegPath, decoderArgs(track.url, offsetMs), { stdio: ["ignore", "pipe", "ignore"] });
5485
+ this.decoder = dec;
5431
5486
  this.mem = { playingTrackId: track.id, spawnedAtServerMs: this.opts.serverNow(), spawnedOffsetMs: offsetMs, procAlive: true };
5432
5487
  this.opts.onTrack?.(track);
5433
- let err = "";
5434
- proc.stderr?.on("data", (d) => {
5435
- err = (err + d.toString()).slice(-200);
5488
+ dec.stdout?.on("data", (chunk) => {
5489
+ if (this.decoder !== dec) return;
5490
+ this.gain = applyGain(chunk, this.gain, this.effTarget());
5491
+ const sink = this.sink;
5492
+ if (!sink || !sink.stdin || sink.stdin.destroyed) return;
5493
+ const ok = sink.stdin.write(chunk);
5494
+ if (ok === false) dec.stdout?.pause();
5436
5495
  });
5437
- proc.on("error", (e) => {
5438
- this.mem.procAlive = false;
5439
- this.opts?.onError?.(e.message);
5496
+ dec.on("error", () => {
5497
+ if (this.decoder === dec) this.mem.procAlive = false;
5440
5498
  });
5441
- proc.on("exit", () => {
5442
- if (this.proc !== proc) return;
5443
- this.mem.procAlive = false;
5444
- if (err) this.opts?.onError?.(err.split("\n").pop() || err);
5499
+ dec.on("exit", () => {
5500
+ if (this.decoder === dec) this.mem.procAlive = false;
5445
5501
  });
5446
5502
  }
5447
- killProc() {
5448
- if (this.proc) {
5503
+ killDecoder() {
5504
+ if (this.decoder) {
5449
5505
  try {
5450
- this.proc.kill("SIGKILL");
5506
+ this.decoder.kill("SIGKILL");
5451
5507
  } catch {
5452
5508
  }
5453
- this.proc = null;
5509
+ this.decoder = null;
5454
5510
  }
5455
5511
  }
5456
- get nowTrackId() {
5457
- return this.mem.playingTrackId;
5458
- }
5459
5512
  stop() {
5460
5513
  if (this.timer) {
5461
5514
  clearInterval(this.timer);
5462
5515
  this.timer = null;
5463
5516
  }
5464
- this.killProc();
5517
+ this.killDecoder();
5518
+ if (this.sink) {
5519
+ try {
5520
+ this.sink.kill("SIGKILL");
5521
+ } catch {
5522
+ }
5523
+ this.sink = null;
5524
+ }
5465
5525
  this.mem = { playingTrackId: null, spawnedAtServerMs: 0, spawnedOffsetMs: 0, procAlive: false };
5466
5526
  }
5467
5527
  };
@@ -5606,9 +5666,9 @@ function startBackground(opts) {
5606
5666
  } else {
5607
5667
  visuals = new Showrunner(ALL_SCENES);
5608
5668
  }
5609
- if (tools.ffplayPath) {
5669
+ if (tools.ffmpegPath && tools.ffplayPath) {
5610
5670
  audio = new SyncedAudio();
5611
- audio.start({ ffplayPath: tools.ffplayPath, schedule: state.schedule, serverNow });
5671
+ audio.start({ ffplayPath: tools.ffplayPath, ffmpegPath: tools.ffmpegPath, schedule: state.schedule, serverNow });
5612
5672
  }
5613
5673
  }
5614
5674
  function makeScreen() {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/protocol.ts
4
- var VERSION = true ? "3.8.1" : "3.8.1";
4
+ var VERSION = true ? "3.8.2" : "3.8.2";
5
5
  var PROTO = 3;
6
6
  var PORT = 8787;
7
7
  var DEFAULT_SERVER = "wss://accelr8-canvas.patonchain.workers.dev";
@@ -32,7 +32,7 @@ import {
32
32
  VERSION,
33
33
  decode,
34
34
  encode
35
- } from "./chunk-UTWXIX5N.js";
35
+ } from "./chunk-7JSMON7M.js";
36
36
 
37
37
  // src/client.ts
38
38
  import tk from "terminal-kit";
@@ -6,7 +6,7 @@ import {
6
6
  LIMITS,
7
7
  PORT,
8
8
  PROTO
9
- } from "./chunk-UTWXIX5N.js";
9
+ } from "./chunk-7JSMON7M.js";
10
10
 
11
11
  // src/server.ts
12
12
  import { WebSocketServer, WebSocket } from "ws";
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  DEFAULT_SERVER,
4
4
  PORT,
5
5
  VERSION
6
- } from "./chunks/chunk-UTWXIX5N.js";
6
+ } from "./chunks/chunk-7JSMON7M.js";
7
7
 
8
8
  // src/cli.ts
9
9
  var HELP = `
@@ -53,7 +53,7 @@ async function main() {
53
53
  console.error(`invalid port: ${argVal(args, "--port")}`);
54
54
  process.exit(1);
55
55
  }
56
- const { runServer } = await import("./chunks/server-V56IHFUH.js");
56
+ const { runServer } = await import("./chunks/server-IRLRECZ7.js");
57
57
  runServer(port);
58
58
  return;
59
59
  }
@@ -67,7 +67,7 @@ async function main() {
67
67
  console.error("accelr8 radio needs an interactive terminal (TTY).");
68
68
  process.exit(1);
69
69
  }
70
- const { runRadio } = await import("./chunks/app-7YGYMFSZ.js");
70
+ const { runRadio } = await import("./chunks/app-SYLCS3QX.js");
71
71
  runRadio({
72
72
  name: argVal(args, "--name"),
73
73
  server: argVal(args, "--server"),
@@ -90,7 +90,7 @@ async function main() {
90
90
  const { cachedUpdate, refreshUpdateCache } = await import("./chunks/update-UEPNGB6Y.js");
91
91
  refreshUpdateCache();
92
92
  const update = cachedUpdate(VERSION);
93
- const { runClient } = await import("./chunks/client-3RLQY3ZB.js");
93
+ const { runClient } = await import("./chunks/client-BKQLCKVT.js");
94
94
  runClient({
95
95
  name: argVal(args, "--name"),
96
96
  server: argVal(args, "--server"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "accelr8",
3
- "version": "3.8.1",
3
+ "version": "3.8.2",
4
4
  "description": "Collaborative ascii canvas in your terminal. Real multiplayer, live cursors, mouse-driven TUI.",
5
5
  "type": "module",
6
6
  "license": "ISC",