@aeonentertainment/mixer 0.3.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.3.0/aeonentertainment-mixer-0.3.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.
@@ -63,6 +65,11 @@ await engine.close();
63
65
  card. It opens no device; its own thread renders a block at each deadline on the OS's
64
66
  monotonic clock, and the mix leaves on `rtpReturn`. `measure-clock` prints its timing:
65
67
  `cargo run --release -p devices --bin measure-clock` in `native/`.
68
+ - **The output** is the OS default, unless `openEngine({ output: { device } })` pins one by an id
69
+ from `engine.outputDevices()`. When the default changes, or the output goes away, the song
70
+ moves to the new output after a short gap, a pinned engine moving back when its output
71
+ returns. `engine.output` names the output playing and its latency, and `outputChanged` fires
72
+ after each move with both and the gap.
66
73
  - **The offline engine**, `openEngine({ output: { offline: true } })`, opens no device and
67
74
  renders only when its caller runs `engine.pump(blocks)`. The tests run on it.
68
75
 
package/dist/index.d.mts CHANGED
@@ -18,6 +18,7 @@ declare class Engine$1 {
18
18
  * gets its socket and send task here too.
19
19
  */
20
20
  createMixer(kinds: Array<string>, rtpReturn?: RtpReturnInput | undefined | null): Mixer$1;
21
+ /** The output playing now. After a move the events thread sends the same as `outputChanged`. */
21
22
  get output(): OutputInfo$1;
22
23
  /** The outputs this OS offers, the default marked. Empty on the offline and timer clocks. */
23
24
  outputs(): Array<OutputEntry>;
@@ -72,7 +73,10 @@ declare class Mixer$1 {
72
73
  * recording it for null.
73
74
  */
74
75
  recordSlot(index: number, path?: string | undefined | null): void;
75
- /** 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
+ */
76
80
  stats(): Array<SlotStatsInfo>;
77
81
  /** The return stream's counters, or null for a mixer without one. */
78
82
  returnStats(): RtpReturnStats | null;
@@ -90,11 +94,23 @@ interface BackingInput {
90
94
  /** The armed speed factor, rendered before start. */
91
95
  speed: number;
92
96
  }
97
+ interface CleanupInput {
98
+ enabled: boolean;
99
+ }
93
100
  interface CorrectorInput {
94
101
  enabled: boolean;
95
102
  strength: number;
96
103
  retuneMs: number;
97
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
+ }
98
114
  interface GuideInput {
99
115
  notes: Array<NoteInput>;
100
116
  /** The key's pitch classes as bits, C in bit 0. */
@@ -119,6 +135,8 @@ type NativeEvent = {
119
135
  positionMs: number;
120
136
  hz: number;
121
137
  confidence: number;
138
+ targetMidi?: number;
139
+ errorSemitones: number;
122
140
  } | {
123
141
  kind: 'position';
124
142
  frames: number;
@@ -137,6 +155,10 @@ type NativeEvent = {
137
155
  } | {
138
156
  kind: 'renderFinished';
139
157
  job: number;
158
+ } | {
159
+ kind: 'outputChanged';
160
+ output: OutputInfo$1;
161
+ gapMs: number;
140
162
  };
141
163
  interface NoteInput {
142
164
  midi: number;
@@ -145,11 +167,18 @@ interface NoteInput {
145
167
  }
146
168
  interface OpenOptions {
147
169
  clock: 'offline' | 'timer' | 'device';
148
- /** A device clock's output: an id from `outputs`, or the default output. */
170
+ /** A device clock's output: an id from `outputs` to pin, or none to follow the default output. */
149
171
  device?: string;
150
172
  periodFrames: number;
151
- /** The jitter buffer's playout delay for every `rtp` and `l16` slot. */
152
- 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;
178
+ }
179
+ interface OutputDevice {
180
+ id: string;
181
+ name: string;
153
182
  }
154
183
  interface OutputEntry {
155
184
  id: string;
@@ -160,6 +189,8 @@ interface OutputInfo$1 {
160
189
  sampleRate: number;
161
190
  periodFrames: number;
162
191
  latencyMs: number;
192
+ /** The output playing now; none on the offline clock. */
193
+ device?: OutputDevice;
163
194
  }
164
195
  interface ReverbInput {
165
196
  enabled: boolean;
@@ -188,6 +219,10 @@ interface SlotParamsInput {
188
219
  solo: boolean;
189
220
  corrector: CorrectorInput;
190
221
  reverb: ReverbInput;
222
+ voice: VoiceInput;
223
+ echoCancel: SwitchInput;
224
+ cleanup: CleanupInput;
225
+ howlGuard: SwitchInput;
191
226
  guideOffsetMs: number;
192
227
  }
193
228
  /** `SlotStats` as JavaScript reads it. */
@@ -195,16 +230,29 @@ interface SlotStatsInfo {
195
230
  pushed: number;
196
231
  lost: number;
197
232
  late: number;
233
+ lateAtOpening: number;
198
234
  concealed: number;
199
235
  resyncs: number;
200
236
  jitterMs: number;
201
237
  correctorDelayMs: number;
202
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;
203
243
  }
204
244
  interface StemGainsInput {
205
245
  instrumental: number;
206
246
  guide: number;
207
247
  }
248
+ /** An effect with no knob but on and off. */
249
+ interface SwitchInput {
250
+ enabled: boolean;
251
+ }
252
+ interface VoiceInput {
253
+ enabled: boolean;
254
+ formantSemitones: number;
255
+ }
208
256
  declare const NativeEngine: typeof Engine$1;
209
257
  type NativeEngine = Engine$1;
210
258
  type NativeMixer = Mixer$1;
@@ -213,8 +261,9 @@ type IngestKind = "rtp" | "l16" | "push";
213
261
  type StemKind = "instrumental" | "guideVocals";
214
262
  interface OpenEngineOptions {
215
263
  /**
216
- * A device; `timer` for a box with no sound card, whose own thread renders on the OS's clock
217
- * and whose mix leaves only on a mixer's `rtpReturn`; `offline` for tests, which pump it.
264
+ * A device, pinned by `device` or, left out, following the OS default; `timer` for a box with
265
+ * no sound card, whose own thread renders on the OS's clock and whose mix leaves only on a
266
+ * mixer's `rtpReturn`; `offline` for tests, which pump it.
218
267
  */
219
268
  output: {
220
269
  device?: string;
@@ -226,13 +275,21 @@ interface OpenEngineOptions {
226
275
  };
227
276
  /** Fixed for the engine's life, default 8, at most 16. */
228
277
  slots?: number;
229
- /** 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
+ */
230
282
  playoutDelayMs?: number;
231
283
  }
232
284
  interface OutputInfo {
233
285
  readonly sampleRate: 48000;
234
286
  readonly periodFrames: number;
235
287
  readonly latencyMs: number;
288
+ /** The output playing now, as `outputDevices()` lists it; null on the offline clock. */
289
+ readonly device: {
290
+ readonly id: string;
291
+ readonly name: string;
292
+ } | null;
236
293
  }
237
294
  interface MixerConfig {
238
295
  slots: ReadonlyArray<{
@@ -275,6 +332,33 @@ type SlotEffect = {
275
332
  enabled: boolean;
276
333
  roomSize: number;
277
334
  mix: number;
335
+ } |
336
+ /**
337
+ * A formant move in semitones from the singer's own, -12 to 12, with no change of pitch and no
338
+ * added delay. Decision 0014 has the first voices' settings.
339
+ */
340
+ {
341
+ kind: "voice";
342
+ enabled: boolean;
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;
278
362
  };
279
363
  interface MelodyGuide {
280
364
  notes: ReadonlyArray<{
@@ -289,6 +373,8 @@ interface SlotStats {
289
373
  pushed: number;
290
374
  lost: number;
291
375
  late: number;
376
+ /** Of `late`, the packets that would also have been late had the delay stayed where it opened. */
377
+ lateAtOpening: number;
292
378
  concealed: number;
293
379
  resyncs: number;
294
380
  jitterMs: number;
@@ -299,15 +385,39 @@ interface SlotStats {
299
385
  correctorDelayMs: number;
300
386
  /** The most that delay can reach; 0 with correction off. It holds still, so a budget adds this. */
301
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;
302
403
  }
303
404
  type EngineEvents = {
304
405
  fault: (error: Error) => void;
406
+ /** After every move to another output; `gapMs` is how long the room heard nothing. */
407
+ outputChanged: (output: OutputInfo, gapMs: number) => void;
305
408
  /** The mix after the clipper, reactions included, between songs too. */
306
409
  masterLevel: (rmsDb: number, peakDb: number) => void;
307
410
  };
308
411
  type MixerEvents = {
309
412
  level: (slot: number, rmsDb: number, peakDb: number) => void;
310
- pitch: (slot: number, positionMs: number, hz: number, confidence: number) => void;
413
+ /**
414
+ * Every 10 ms, on the guide's clock. `targetMidi` is the guide note the corrector pulls toward,
415
+ * after the backing's key shift: null with correction off, with no guide, or between notes,
416
+ * where it pulls toward the key. `errorSemitones` is the class it pulls toward less the
417
+ * reading, in the singer's own octave, so positive is flat: 0 with correction off or while
418
+ * unvoiced.
419
+ */
420
+ pitch: (slot: number, positionMs: number, hz: number, confidence: number, targetMidi: number | null, errorSemitones: number) => void;
311
421
  position: (positionMs: number) => void;
312
422
  renderProgress: (fraction: number) => void;
313
423
  backingEnded: () => void;
@@ -332,7 +442,8 @@ declare class Emitter<Events extends Record<keyof Events, Handler>> {
332
442
  }
333
443
  /** What a mixer needs from its engine. Internal to the package. */
334
444
  interface MixerHost extends RenderHost {
335
- readonly output: OutputInfo;
445
+ /** The output playing now; it changes when the engine moves. */
446
+ output(): OutputInfo;
336
447
  /**
337
448
  * Sends a command carrying an id and resolves on its ack with the backing position. `onTimeout`
338
449
  * hears the rejection when no ack comes in time.
@@ -450,10 +561,14 @@ export declare function openEngine(options: OpenEngineOptions): Promise<Engine>;
450
561
  export declare class Engine {
451
562
  #private;
452
563
  readonly capabilities: ReadonlySet<Capability>;
453
- readonly output: OutputInfo;
454
564
  /** Offline engines only: render blocks and return them as interleaved stereo. */
455
565
  readonly pump?: (blocks: number) => Float32Array;
456
566
  constructor(options: OpenEngineOptions);
567
+ /**
568
+ * The output playing now. An engine opened with no `output.device` follows the OS default, and
569
+ * one with a `device` plays there while it exists; `outputChanged` fires after every move.
570
+ */
571
+ get output(): OutputInfo;
457
572
  /** Room-lifetime reaction voices, four at once. False when a fifth is dropped while four play. */
458
573
  readonly reactions: {
459
574
  play: (samples: Float32Array, channels: 1 | 2) => boolean;
package/dist/index.mjs CHANGED
@@ -230,6 +230,13 @@ function defaultSlotParams() {
230
230
  roomSize: 0,
231
231
  mix: 0
232
232
  },
233
+ voice: {
234
+ enabled: false,
235
+ formantSemitones: 0
236
+ },
237
+ echoCancel: { enabled: false },
238
+ cleanup: { enabled: false },
239
+ howlGuard: { enabled: false },
233
240
  guideOffsetMs: 0
234
241
  };
235
242
  }
@@ -305,11 +312,26 @@ var Mixer = class {
305
312
  retuneMs: finiteIn("retuneMs", effect.retuneMs, 0, 1e3)
306
313
  };
307
314
  break;
308
- case "reverb": next.reverb = {
309
- enabled: effect.enabled,
310
- roomSize: finiteIn("roomSize", effect.roomSize, 0, 1),
311
- mix: finiteIn("mix", effect.mix, 0, 1)
312
- };
315
+ case "reverb":
316
+ next.reverb = {
317
+ enabled: effect.enabled,
318
+ roomSize: finiteIn("roomSize", effect.roomSize, 0, 1),
319
+ mix: finiteIn("mix", effect.mix, 0, 1)
320
+ };
321
+ break;
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 };
313
335
  }
314
336
  this.#write(slot, next);
315
337
  }
@@ -393,7 +415,7 @@ var Mixer = class {
393
415
  started,
394
416
  stopping
395
417
  ]);
396
- const { latencyMs } = this.#host.output;
418
+ const { latencyMs } = this.#host.output();
397
419
  return {
398
420
  startedAt: Date.now() + latencyMs,
399
421
  backingOffsetMs: latencyMs,
@@ -488,10 +510,14 @@ var Mixer = class {
488
510
  }
489
511
  };
490
512
  function defaultEffects() {
491
- const { corrector, reverb } = defaultSlotParams();
513
+ const { corrector, reverb, voice, cleanup, echoCancel, howlGuard } = defaultSlotParams();
492
514
  return {
493
515
  corrector,
494
- reverb
516
+ reverb,
517
+ voice,
518
+ cleanup,
519
+ echoCancel,
520
+ howlGuard
495
521
  };
496
522
  }
497
523
  function toGuideInput(guide) {
@@ -522,7 +548,7 @@ var Engine = class {
522
548
  "pause",
523
549
  "level"
524
550
  ]);
525
- output;
551
+ #output;
526
552
  /** How many slots a mixer may have, fixed at open. */
527
553
  #slots;
528
554
  /** Offline engines only: render blocks and return them as interleaved stereo. */
@@ -540,7 +566,7 @@ var Engine = class {
540
566
  #state = "running";
541
567
  constructor(options) {
542
568
  this.#slots = integerIn("slots", options.slots ?? 8, 1, 16);
543
- 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)) };
544
570
  const deliver = (batch) => {
545
571
  this.#deliver(batch);
546
572
  };
@@ -548,13 +574,13 @@ var Engine = class {
548
574
  this.#native = new NativeEngine({
549
575
  clock: "offline",
550
576
  periodFrames: 128,
551
- playoutDelayFrames
577
+ ...pinned
552
578
  }, deliver);
553
579
  this.pump = (blocks) => this.#native.pump(integerIn("blocks", blocks, 1, 1 << 20));
554
580
  } else if ("timer" in options.output) this.#native = new NativeEngine({
555
581
  clock: "timer",
556
582
  periodFrames: 128,
557
- playoutDelayFrames
583
+ ...pinned
558
584
  }, deliver);
559
585
  else {
560
586
  const { device, periodFrames = 128 } = options.output;
@@ -562,14 +588,14 @@ var Engine = class {
562
588
  this.#native = new NativeEngine({
563
589
  clock: "device",
564
590
  periodFrames,
565
- playoutDelayFrames,
591
+ ...pinned,
566
592
  ...device === void 0 ? {} : { device }
567
593
  }, deliver);
568
594
  }
569
- this.output = outputInfo(this.#native.output);
595
+ this.#output = outputInfo(this.#native.output);
570
596
  this.#host = {
571
597
  native: this.#native,
572
- output: this.output,
598
+ output: () => this.#output,
573
599
  command: (send, onTimeout) => this.#command(send, onTimeout),
574
600
  watchRender: (id, onProgress) => new Promise((resolve, reject) => {
575
601
  this.#renders.set(id, {
@@ -586,6 +612,13 @@ var Engine = class {
586
612
  underruns: () => this.#underruns
587
613
  };
588
614
  }
615
+ /**
616
+ * The output playing now. An engine opened with no `output.device` follows the OS default, and
617
+ * one with a `device` plays there while it exists; `outputChanged` fires after every move.
618
+ */
619
+ get output() {
620
+ return this.#output;
621
+ }
589
622
  /** Room-lifetime reaction voices, four at once. False when a fifth is dropped while four play. */
590
623
  reactions = { play: (samples, channels) => {
591
624
  if (channels !== 1 && channels !== 2) throw new RangeError(`channels must be 1 or 2, got ${String(channels)}`);
@@ -654,7 +687,7 @@ var Engine = class {
654
687
  this.#live?.events.emit("level", event.slot, event.rmsDb, event.peakDb);
655
688
  break;
656
689
  case "pitch":
657
- this.#live?.events.emit("pitch", event.slot, event.positionMs, event.hz, event.confidence);
690
+ this.#live?.events.emit("pitch", event.slot, event.positionMs, event.hz, event.confidence, event.targetMidi ?? null, event.errorSemitones);
658
691
  break;
659
692
  case "position":
660
693
  this.#live?.events.emit("position", framesToMs(event.frames));
@@ -677,6 +710,10 @@ var Engine = class {
677
710
  case "renderProgress":
678
711
  this.#renders.get(event.job)?.onProgress(event.fraction);
679
712
  break;
713
+ case "outputChanged":
714
+ this.#output = outputInfo(event.output);
715
+ this.#events.emit("outputChanged", this.#output, event.gapMs);
716
+ break;
680
717
  case "renderFinished": {
681
718
  const render = this.#renders.get(event.job);
682
719
  this.#renders.delete(event.job);
@@ -705,10 +742,15 @@ var Engine = class {
705
742
  };
706
743
  function outputInfo(info) {
707
744
  if (info.sampleRate !== 48e3) throw new Error(`the engine runs at 48 kHz; the addon reports ${info.sampleRate}`);
745
+ const { periodFrames, latencyMs, device } = info;
708
746
  return {
709
747
  sampleRate: 48e3,
710
- periodFrames: info.periodFrames,
711
- latencyMs: info.latencyMs
748
+ periodFrames,
749
+ latencyMs,
750
+ device: device === void 0 ? null : {
751
+ id: device.id,
752
+ name: device.name
753
+ }
712
754
  };
713
755
  }
714
756
  function monoToStereo(mono) {
@@ -30,6 +30,7 @@ export declare class Engine {
30
30
  * gets its socket and send task here too.
31
31
  */
32
32
  createMixer(kinds: Array<string>, rtpReturn?: RtpReturnInput | undefined | null): Mixer
33
+ /** The output playing now. After a move the events thread sends the same as `outputChanged`. */
33
34
  get output(): OutputInfo
34
35
  /** The outputs this OS offers, the default marked. Empty on the offline and timer clocks. */
35
36
  outputs(): Array<OutputEntry>
@@ -85,7 +86,10 @@ export declare class Mixer {
85
86
  * recording it for null.
86
87
  */
87
88
  recordSlot(index: number, path?: string | undefined | null): void
88
- /** 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
+ */
89
93
  stats(): Array<SlotStatsInfo>
90
94
  /** The return stream's counters, or null for a mixer without one. */
91
95
  returnStats(): RtpReturnStats | null
@@ -105,6 +109,10 @@ export interface BackingInput {
105
109
  speed: number
106
110
  }
107
111
 
112
+ export interface CleanupInput {
113
+ enabled: boolean
114
+ }
115
+
108
116
  /** What drives the audio thread. */
109
117
  export declare const enum ClockKind {
110
118
  /** The offline driver, pumped from JavaScript. */
@@ -121,6 +129,16 @@ export interface CorrectorInput {
121
129
  retuneMs: number
122
130
  }
123
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
+
124
142
  export interface GuideInput {
125
143
  notes: Array<NoteInput>
126
144
  /** The key's pitch classes as bits, C in bit 0. */
@@ -131,7 +149,7 @@ export type NativeEvent =
131
149
  | { kind: 'ack'; id: number; positionFrames: number }
132
150
  | { kind: 'level'; slot: number; rmsDb: number; peakDb: number }
133
151
  | { kind: 'masterLevel'; rmsDb: number; peakDb: number }
134
- | { kind: 'pitch'; slot: number; positionMs: number; hz: number; confidence: number }
152
+ | { kind: 'pitch'; slot: number; positionMs: number; hz: number; confidence: number; targetMidi?: number; errorSemitones: number }
135
153
  | { kind: 'position'; frames: number }
136
154
  | { kind: 'backingEnded' }
137
155
  | { kind: 'underrun' }
@@ -139,6 +157,7 @@ export type NativeEvent =
139
157
  | { kind: 'retired' }
140
158
  | { kind: 'renderProgress'; job: number; fraction: number }
141
159
  | { kind: 'renderFinished'; job: number }
160
+ | { kind: 'outputChanged'; output: OutputInfo; gapMs: number }
142
161
 
143
162
  export interface NoteInput {
144
163
  midi: number
@@ -148,11 +167,19 @@ export interface NoteInput {
148
167
 
149
168
  export interface OpenOptions {
150
169
  clock: 'offline' | 'timer' | 'device'
151
- /** A device clock's output: an id from `outputs`, or the default output. */
170
+ /** A device clock's output: an id from `outputs` to pin, or none to follow the default output. */
152
171
  device?: string
153
172
  periodFrames: number
154
- /** The jitter buffer's playout delay for every `rtp` and `l16` slot. */
155
- 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
178
+ }
179
+
180
+ export interface OutputDevice {
181
+ id: string
182
+ name: string
156
183
  }
157
184
 
158
185
  export interface OutputEntry {
@@ -165,6 +192,8 @@ export interface OutputInfo {
165
192
  sampleRate: number
166
193
  periodFrames: number
167
194
  latencyMs: number
195
+ /** The output playing now; none on the offline clock. */
196
+ device?: OutputDevice
168
197
  }
169
198
 
170
199
  export interface ReverbInput {
@@ -197,6 +226,10 @@ export interface SlotParamsInput {
197
226
  solo: boolean
198
227
  corrector: CorrectorInput
199
228
  reverb: ReverbInput
229
+ voice: VoiceInput
230
+ echoCancel: SwitchInput
231
+ cleanup: CleanupInput
232
+ howlGuard: SwitchInput
200
233
  guideOffsetMs: number
201
234
  }
202
235
 
@@ -205,14 +238,29 @@ export interface SlotStatsInfo {
205
238
  pushed: number
206
239
  lost: number
207
240
  late: number
241
+ lateAtOpening: number
208
242
  concealed: number
209
243
  resyncs: number
210
244
  jitterMs: number
211
245
  correctorDelayMs: number
212
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
213
251
  }
214
252
 
215
253
  export interface StemGainsInput {
216
254
  instrumental: number
217
255
  guide: number
218
256
  }
257
+
258
+ /** An effect with no knob but on and off. */
259
+ export interface SwitchInput {
260
+ enabled: boolean
261
+ }
262
+
263
+ export interface VoiceInput {
264
+ enabled: boolean
265
+ formantSemitones: number
266
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aeonentertainment/mixer",
3
- "version": "0.3.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": {