@aeonentertainment/mixer 0.4.0 → 0.5.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/README.md CHANGED
@@ -15,7 +15,7 @@ Each [release](https://github.com/aeonentertainment/aeon-mixer/releases) carries
15
15
  Depend on its URL, and the lockfile pins its hash:
16
16
 
17
17
  ```bash
18
- npm install https://github.com/aeonentertainment/aeon-mixer/releases/download/v0.4.0/aeonentertainment-mixer-0.4.0.tgz
18
+ npm install https://github.com/aeonentertainment/aeon-mixer/releases/download/v0.5.0/aeonentertainment-mixer-0.5.0.tgz
19
19
  ```
20
20
 
21
21
  ## Use
@@ -52,7 +52,9 @@ await engine.close();
52
52
  takes Opus over UDP from a phone or mediasoup, `l16` takes raw PCM over loopback from a house
53
53
  mic, and `push` takes PCM the host hands to `mixer.push`. Each slot has gain, pan, mute, solo,
54
54
  reverb, a pitch reading, a melody guide and pitch correction toward it, whose delay `stats()`
55
- reports.
55
+ reports. A slot can also take the room's echo of the mix out of its input and duck itself when
56
+ its loop through the speakers starts to howl; both add no delay, and `stats()` reports what
57
+ they found.
56
58
  - **The backing** is any format Symphonia decodes, resampled to 48 kHz. The armed key and speed
57
59
  are rendered before `start` resolves. Live, it supports seek, pause, resume, a key change and
58
60
  stem gains, and `start`, `seek` and `resume` report the position the lyric clock follows.
package/dist/index.d.mts CHANGED
@@ -73,7 +73,10 @@ declare class Mixer$1 {
73
73
  * recording it for null.
74
74
  */
75
75
  recordSlot(index: number, path?: string | undefined | null): void;
76
- /** Every slot's ingest counters and corrector delay, as the audio thread last published them. */
76
+ /**
77
+ * Every slot's ingest counters, corrector delay and echo findings, as the audio thread last
78
+ * published them.
79
+ */
77
80
  stats(): Array<SlotStatsInfo>;
78
81
  /** The return stream's counters, or null for a mixer without one. */
79
82
  returnStats(): RtpReturnStats | null;
@@ -91,11 +94,23 @@ interface BackingInput {
91
94
  /** The armed speed factor, rendered before start. */
92
95
  speed: number;
93
96
  }
97
+ interface CleanupInput {
98
+ enabled: boolean;
99
+ }
94
100
  interface CorrectorInput {
95
101
  enabled: boolean;
96
102
  strength: number;
97
103
  retuneMs: number;
98
104
  }
105
+ /** What a slot's echo canceller has found, as JavaScript reads it. */
106
+ interface EchoInfo {
107
+ /** From the master being rendered to its echo reaching the slot. */
108
+ pathMs: number;
109
+ /** How far under the master the echo arrives, over the last second. */
110
+ bleedDb: number;
111
+ /** How much quieter the slot came out than it went in, over the last second. */
112
+ removedDb: number;
113
+ }
99
114
  interface GuideInput {
100
115
  notes: Array<NoteInput>;
101
116
  /** The key's pitch classes as bits, C in bit 0. */
@@ -155,8 +170,11 @@ interface OpenOptions {
155
170
  /** A device clock's output: an id from `outputs` to pin, or none to follow the default output. */
156
171
  device?: string;
157
172
  periodFrames: number;
158
- /** The jitter buffer's playout delay for every `rtp` and `l16` slot. */
159
- playoutDelayFrames: number;
173
+ /**
174
+ * Pins the jitter buffer's playout delay for every `rtp` and `l16` slot. Without it, each
175
+ * stream chooses its own.
176
+ */
177
+ playoutDelayFrames?: number;
160
178
  }
161
179
  interface OutputDevice {
162
180
  id: string;
@@ -202,6 +220,9 @@ interface SlotParamsInput {
202
220
  corrector: CorrectorInput;
203
221
  reverb: ReverbInput;
204
222
  voice: VoiceInput;
223
+ echoCancel: SwitchInput;
224
+ cleanup: CleanupInput;
225
+ howlGuard: SwitchInput;
205
226
  guideOffsetMs: number;
206
227
  }
207
228
  /** `SlotStats` as JavaScript reads it. */
@@ -209,16 +230,25 @@ interface SlotStatsInfo {
209
230
  pushed: number;
210
231
  lost: number;
211
232
  late: number;
233
+ lateAtOpening: number;
212
234
  concealed: number;
213
235
  resyncs: number;
214
236
  jitterMs: number;
215
237
  correctorDelayMs: number;
216
238
  correctorMaxDelayMs: number;
239
+ /** Null while the canceller is off or has yet to find and learn the room's echo. */
240
+ echo: EchoInfo | null;
241
+ playoutDelayMs: number;
242
+ howlCatches: number;
217
243
  }
218
244
  interface StemGainsInput {
219
245
  instrumental: number;
220
246
  guide: number;
221
247
  }
248
+ /** An effect with no knob but on and off. */
249
+ interface SwitchInput {
250
+ enabled: boolean;
251
+ }
222
252
  interface VoiceInput {
223
253
  enabled: boolean;
224
254
  formantSemitones: number;
@@ -245,7 +275,10 @@ interface OpenEngineOptions {
245
275
  };
246
276
  /** Fixed for the engine's life, default 8, at most 16. */
247
277
  slots?: number;
248
- /** The ingest jitter buffer, default 15. */
278
+ /**
279
+ * Pins the ingest jitter buffer to this delay. Leave it out and each slot chooses its own from
280
+ * what its stream shows, opening at 15; `stats()` reports where each one is.
281
+ */
249
282
  playoutDelayMs?: number;
250
283
  }
251
284
  interface OutputInfo {
@@ -308,6 +341,24 @@ type SlotEffect = {
308
341
  kind: "voice";
309
342
  enabled: boolean;
310
343
  formantSemitones: number;
344
+ } |
345
+ /** Takes the room's echo of the mix out of the slot's input, with no added delay. */
346
+ {
347
+ kind: "echo-cancel";
348
+ enabled: boolean;
349
+ } |
350
+ /**
351
+ * A high-pass and gate ahead of the corrector and a compressor after the meter. None reads
352
+ * ahead or buffers; decision 0012 has their numbers and costs.
353
+ */
354
+ {
355
+ kind: "cleanup";
356
+ enabled: boolean;
357
+ } |
358
+ /** Ducks the slot when its loop through the speakers starts to howl, with no added delay. */
359
+ {
360
+ kind: "howl-guard";
361
+ enabled: boolean;
311
362
  };
312
363
  interface MelodyGuide {
313
364
  notes: ReadonlyArray<{
@@ -322,6 +373,8 @@ interface SlotStats {
322
373
  pushed: number;
323
374
  lost: number;
324
375
  late: number;
376
+ /** Of `late`, the packets that would also have been late had the delay stayed where it opened. */
377
+ lateAtOpening: number;
325
378
  concealed: number;
326
379
  resyncs: number;
327
380
  jitterMs: number;
@@ -332,6 +385,21 @@ interface SlotStats {
332
385
  correctorDelayMs: number;
333
386
  /** The most that delay can reach; 0 with correction off. It holds still, so a budget adds this. */
334
387
  correctorMaxDelayMs: number;
388
+ /**
389
+ * What the echo canceller found: `pathMs` from the mix being rendered to its echo reaching the
390
+ * slot, `bleedDb` how far under the mix that echo arrives, `removedDb` how much quieter the slot
391
+ * came out than it went in, both over the last second. Null while it is off, or until it has
392
+ * found the echo and learned it.
393
+ */
394
+ echo: {
395
+ pathMs: number;
396
+ bleedDb: number;
397
+ removedDb: number;
398
+ } | null;
399
+ /** The delay the stream's earliest packets wait now; zero for a `push` slot. */
400
+ playoutDelayMs: number;
401
+ /** How often the howl guard has ducked this slot. */
402
+ howlCatches: number;
335
403
  }
336
404
  type EngineEvents = {
337
405
  fault: (error: Error) => void;
package/dist/index.mjs CHANGED
@@ -234,6 +234,9 @@ function defaultSlotParams() {
234
234
  enabled: false,
235
235
  formantSemitones: 0
236
236
  },
237
+ echoCancel: { enabled: false },
238
+ cleanup: { enabled: false },
239
+ howlGuard: { enabled: false },
237
240
  guideOffsetMs: 0
238
241
  };
239
242
  }
@@ -316,10 +319,19 @@ var Mixer = class {
316
319
  mix: finiteIn("mix", effect.mix, 0, 1)
317
320
  };
318
321
  break;
319
- case "voice": next.voice = {
320
- enabled: effect.enabled,
321
- formantSemitones: finiteIn("formantSemitones", effect.formantSemitones, -12, 12)
322
- };
322
+ case "voice":
323
+ next.voice = {
324
+ enabled: effect.enabled,
325
+ formantSemitones: finiteIn("formantSemitones", effect.formantSemitones, -12, 12)
326
+ };
327
+ break;
328
+ case "echo-cancel":
329
+ next.echoCancel = { enabled: effect.enabled };
330
+ break;
331
+ case "cleanup":
332
+ next.cleanup = { enabled: effect.enabled };
333
+ break;
334
+ case "howl-guard": next.howlGuard = { enabled: effect.enabled };
323
335
  }
324
336
  this.#write(slot, next);
325
337
  }
@@ -498,11 +510,14 @@ var Mixer = class {
498
510
  }
499
511
  };
500
512
  function defaultEffects() {
501
- const { corrector, reverb, voice } = defaultSlotParams();
513
+ const { corrector, reverb, voice, cleanup, echoCancel, howlGuard } = defaultSlotParams();
502
514
  return {
503
515
  corrector,
504
516
  reverb,
505
- voice
517
+ voice,
518
+ cleanup,
519
+ echoCancel,
520
+ howlGuard
506
521
  };
507
522
  }
508
523
  function toGuideInput(guide) {
@@ -551,7 +566,7 @@ var Engine = class {
551
566
  #state = "running";
552
567
  constructor(options) {
553
568
  this.#slots = integerIn("slots", options.slots ?? 8, 1, 16);
554
- const playoutDelayFrames = msToFrames(finiteIn("playoutDelayMs", options.playoutDelayMs ?? 15, 5, 100));
569
+ const pinned = options.playoutDelayMs === void 0 ? {} : { playoutDelayFrames: msToFrames(finiteIn("playoutDelayMs", options.playoutDelayMs, 5, 100)) };
555
570
  const deliver = (batch) => {
556
571
  this.#deliver(batch);
557
572
  };
@@ -559,13 +574,13 @@ var Engine = class {
559
574
  this.#native = new NativeEngine({
560
575
  clock: "offline",
561
576
  periodFrames: 128,
562
- playoutDelayFrames
577
+ ...pinned
563
578
  }, deliver);
564
579
  this.pump = (blocks) => this.#native.pump(integerIn("blocks", blocks, 1, 1 << 20));
565
580
  } else if ("timer" in options.output) this.#native = new NativeEngine({
566
581
  clock: "timer",
567
582
  periodFrames: 128,
568
- playoutDelayFrames
583
+ ...pinned
569
584
  }, deliver);
570
585
  else {
571
586
  const { device, periodFrames = 128 } = options.output;
@@ -573,7 +588,7 @@ var Engine = class {
573
588
  this.#native = new NativeEngine({
574
589
  clock: "device",
575
590
  periodFrames,
576
- playoutDelayFrames,
591
+ ...pinned,
577
592
  ...device === void 0 ? {} : { device }
578
593
  }, deliver);
579
594
  }
@@ -86,7 +86,10 @@ export declare class Mixer {
86
86
  * recording it for null.
87
87
  */
88
88
  recordSlot(index: number, path?: string | undefined | null): void
89
- /** Every slot's ingest counters and corrector delay, as the audio thread last published them. */
89
+ /**
90
+ * Every slot's ingest counters, corrector delay and echo findings, as the audio thread last
91
+ * published them.
92
+ */
90
93
  stats(): Array<SlotStatsInfo>
91
94
  /** The return stream's counters, or null for a mixer without one. */
92
95
  returnStats(): RtpReturnStats | null
@@ -106,6 +109,10 @@ export interface BackingInput {
106
109
  speed: number
107
110
  }
108
111
 
112
+ export interface CleanupInput {
113
+ enabled: boolean
114
+ }
115
+
109
116
  /** What drives the audio thread. */
110
117
  export declare const enum ClockKind {
111
118
  /** The offline driver, pumped from JavaScript. */
@@ -122,6 +129,16 @@ export interface CorrectorInput {
122
129
  retuneMs: number
123
130
  }
124
131
 
132
+ /** What a slot's echo canceller has found, as JavaScript reads it. */
133
+ export interface EchoInfo {
134
+ /** From the master being rendered to its echo reaching the slot. */
135
+ pathMs: number
136
+ /** How far under the master the echo arrives, over the last second. */
137
+ bleedDb: number
138
+ /** How much quieter the slot came out than it went in, over the last second. */
139
+ removedDb: number
140
+ }
141
+
125
142
  export interface GuideInput {
126
143
  notes: Array<NoteInput>
127
144
  /** The key's pitch classes as bits, C in bit 0. */
@@ -153,8 +170,11 @@ export interface OpenOptions {
153
170
  /** A device clock's output: an id from `outputs` to pin, or none to follow the default output. */
154
171
  device?: string
155
172
  periodFrames: number
156
- /** The jitter buffer's playout delay for every `rtp` and `l16` slot. */
157
- playoutDelayFrames: number
173
+ /**
174
+ * Pins the jitter buffer's playout delay for every `rtp` and `l16` slot. Without it, each
175
+ * stream chooses its own.
176
+ */
177
+ playoutDelayFrames?: number
158
178
  }
159
179
 
160
180
  export interface OutputDevice {
@@ -207,6 +227,9 @@ export interface SlotParamsInput {
207
227
  corrector: CorrectorInput
208
228
  reverb: ReverbInput
209
229
  voice: VoiceInput
230
+ echoCancel: SwitchInput
231
+ cleanup: CleanupInput
232
+ howlGuard: SwitchInput
210
233
  guideOffsetMs: number
211
234
  }
212
235
 
@@ -215,11 +238,16 @@ export interface SlotStatsInfo {
215
238
  pushed: number
216
239
  lost: number
217
240
  late: number
241
+ lateAtOpening: number
218
242
  concealed: number
219
243
  resyncs: number
220
244
  jitterMs: number
221
245
  correctorDelayMs: number
222
246
  correctorMaxDelayMs: number
247
+ /** Null while the canceller is off or has yet to find and learn the room's echo. */
248
+ echo: EchoInfo | null
249
+ playoutDelayMs: number
250
+ howlCatches: number
223
251
  }
224
252
 
225
253
  export interface StemGainsInput {
@@ -227,6 +255,11 @@ export interface StemGainsInput {
227
255
  guide: number
228
256
  }
229
257
 
258
+ /** An effect with no knob but on and off. */
259
+ export interface SwitchInput {
260
+ enabled: boolean
261
+ }
262
+
230
263
  export interface VoiceInput {
231
264
  enabled: boolean
232
265
  formantSemitones: number
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aeonentertainment/mixer",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Low-latency LAN audio mixer for Aeon Karaoke: phone and house mics, a backing track, one mix to the room.",
5
5
  "bugs": {
6
6
  "url": "https://github.com/aeonentertainment/aeon-mixer/issues"
@@ -26,7 +26,7 @@
26
26
  "@napi-rs/cli": "^3.10.9",
27
27
  "@types/node": "^26.6.6",
28
28
  "typescript": "^7.0.2",
29
- "vite-plus": "1.4.0"
29
+ "vite-plus": "1.5.1"
30
30
  },
31
31
  "devEngines": {
32
32
  "packageManager": {