@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 +9 -2
- package/dist/index.d.mts +125 -10
- package/dist/index.mjs +60 -18
- package/native/binding/index.d.ts +53 -5
- package/native/binding/index.darwin-arm64.node +0 -0
- package/native/binding/index.darwin-x64.node +0 -0
- package/native/binding/index.linux-arm64-gnu.node +0 -0
- package/native/binding/index.linux-x64-gnu.node +0 -0
- package/native/binding/index.win32-x64-msvc.node +0 -0
- package/package.json +2 -2
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.
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
152
|
-
|
|
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
|
|
217
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
591
|
+
...pinned,
|
|
566
592
|
...device === void 0 ? {} : { device }
|
|
567
593
|
}, deliver);
|
|
568
594
|
}
|
|
569
|
-
this
|
|
595
|
+
this.#output = outputInfo(this.#native.output);
|
|
570
596
|
this.#host = {
|
|
571
597
|
native: this.#native,
|
|
572
|
-
output: this
|
|
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
|
|
711
|
-
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
155
|
-
|
|
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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aeonentertainment/mixer",
|
|
3
|
-
"version": "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.
|
|
29
|
+
"vite-plus": "1.5.1"
|
|
30
30
|
},
|
|
31
31
|
"devEngines": {
|
|
32
32
|
"packageManager": {
|