@aeonentertainment/mixer 0.3.0 → 0.4.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 +6 -1
- package/dist/index.d.mts +53 -6
- package/dist/index.mjs +39 -12
- package/native/binding/index.d.ts +17 -2
- 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 +1 -1
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.4.0/aeonentertainment-mixer-0.4.0.tgz
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Use
|
|
@@ -63,6 +63,11 @@ await engine.close();
|
|
|
63
63
|
card. It opens no device; its own thread renders a block at each deadline on the OS's
|
|
64
64
|
monotonic clock, and the mix leaves on `rtpReturn`. `measure-clock` prints its timing:
|
|
65
65
|
`cargo run --release -p devices --bin measure-clock` in `native/`.
|
|
66
|
+
- **The output** is the OS default, unless `openEngine({ output: { device } })` pins one by an id
|
|
67
|
+
from `engine.outputDevices()`. When the default changes, or the output goes away, the song
|
|
68
|
+
moves to the new output after a short gap, a pinned engine moving back when its output
|
|
69
|
+
returns. `engine.output` names the output playing and its latency, and `outputChanged` fires
|
|
70
|
+
after each move with both and the gap.
|
|
66
71
|
- **The offline engine**, `openEngine({ output: { offline: true } })`, opens no device and
|
|
67
72
|
renders only when its caller runs `engine.pump(blocks)`. The tests run on it.
|
|
68
73
|
|
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>;
|
|
@@ -119,6 +120,8 @@ type NativeEvent = {
|
|
|
119
120
|
positionMs: number;
|
|
120
121
|
hz: number;
|
|
121
122
|
confidence: number;
|
|
123
|
+
targetMidi?: number;
|
|
124
|
+
errorSemitones: number;
|
|
122
125
|
} | {
|
|
123
126
|
kind: 'position';
|
|
124
127
|
frames: number;
|
|
@@ -137,6 +140,10 @@ type NativeEvent = {
|
|
|
137
140
|
} | {
|
|
138
141
|
kind: 'renderFinished';
|
|
139
142
|
job: number;
|
|
143
|
+
} | {
|
|
144
|
+
kind: 'outputChanged';
|
|
145
|
+
output: OutputInfo$1;
|
|
146
|
+
gapMs: number;
|
|
140
147
|
};
|
|
141
148
|
interface NoteInput {
|
|
142
149
|
midi: number;
|
|
@@ -145,12 +152,16 @@ interface NoteInput {
|
|
|
145
152
|
}
|
|
146
153
|
interface OpenOptions {
|
|
147
154
|
clock: 'offline' | 'timer' | 'device';
|
|
148
|
-
/** A device clock's output: an id from `outputs
|
|
155
|
+
/** A device clock's output: an id from `outputs` to pin, or none to follow the default output. */
|
|
149
156
|
device?: string;
|
|
150
157
|
periodFrames: number;
|
|
151
158
|
/** The jitter buffer's playout delay for every `rtp` and `l16` slot. */
|
|
152
159
|
playoutDelayFrames: number;
|
|
153
160
|
}
|
|
161
|
+
interface OutputDevice {
|
|
162
|
+
id: string;
|
|
163
|
+
name: string;
|
|
164
|
+
}
|
|
154
165
|
interface OutputEntry {
|
|
155
166
|
id: string;
|
|
156
167
|
name: string;
|
|
@@ -160,6 +171,8 @@ interface OutputInfo$1 {
|
|
|
160
171
|
sampleRate: number;
|
|
161
172
|
periodFrames: number;
|
|
162
173
|
latencyMs: number;
|
|
174
|
+
/** The output playing now; none on the offline clock. */
|
|
175
|
+
device?: OutputDevice;
|
|
163
176
|
}
|
|
164
177
|
interface ReverbInput {
|
|
165
178
|
enabled: boolean;
|
|
@@ -188,6 +201,7 @@ interface SlotParamsInput {
|
|
|
188
201
|
solo: boolean;
|
|
189
202
|
corrector: CorrectorInput;
|
|
190
203
|
reverb: ReverbInput;
|
|
204
|
+
voice: VoiceInput;
|
|
191
205
|
guideOffsetMs: number;
|
|
192
206
|
}
|
|
193
207
|
/** `SlotStats` as JavaScript reads it. */
|
|
@@ -205,6 +219,10 @@ interface StemGainsInput {
|
|
|
205
219
|
instrumental: number;
|
|
206
220
|
guide: number;
|
|
207
221
|
}
|
|
222
|
+
interface VoiceInput {
|
|
223
|
+
enabled: boolean;
|
|
224
|
+
formantSemitones: number;
|
|
225
|
+
}
|
|
208
226
|
declare const NativeEngine: typeof Engine$1;
|
|
209
227
|
type NativeEngine = Engine$1;
|
|
210
228
|
type NativeMixer = Mixer$1;
|
|
@@ -213,8 +231,9 @@ type IngestKind = "rtp" | "l16" | "push";
|
|
|
213
231
|
type StemKind = "instrumental" | "guideVocals";
|
|
214
232
|
interface OpenEngineOptions {
|
|
215
233
|
/**
|
|
216
|
-
* A device
|
|
217
|
-
*
|
|
234
|
+
* A device, pinned by `device` or, left out, following the OS default; `timer` for a box with
|
|
235
|
+
* no sound card, whose own thread renders on the OS's clock and whose mix leaves only on a
|
|
236
|
+
* mixer's `rtpReturn`; `offline` for tests, which pump it.
|
|
218
237
|
*/
|
|
219
238
|
output: {
|
|
220
239
|
device?: string;
|
|
@@ -233,6 +252,11 @@ interface OutputInfo {
|
|
|
233
252
|
readonly sampleRate: 48000;
|
|
234
253
|
readonly periodFrames: number;
|
|
235
254
|
readonly latencyMs: number;
|
|
255
|
+
/** The output playing now, as `outputDevices()` lists it; null on the offline clock. */
|
|
256
|
+
readonly device: {
|
|
257
|
+
readonly id: string;
|
|
258
|
+
readonly name: string;
|
|
259
|
+
} | null;
|
|
236
260
|
}
|
|
237
261
|
interface MixerConfig {
|
|
238
262
|
slots: ReadonlyArray<{
|
|
@@ -275,6 +299,15 @@ type SlotEffect = {
|
|
|
275
299
|
enabled: boolean;
|
|
276
300
|
roomSize: number;
|
|
277
301
|
mix: number;
|
|
302
|
+
} |
|
|
303
|
+
/**
|
|
304
|
+
* A formant move in semitones from the singer's own, -12 to 12, with no change of pitch and no
|
|
305
|
+
* added delay. Decision 0014 has the first voices' settings.
|
|
306
|
+
*/
|
|
307
|
+
{
|
|
308
|
+
kind: "voice";
|
|
309
|
+
enabled: boolean;
|
|
310
|
+
formantSemitones: number;
|
|
278
311
|
};
|
|
279
312
|
interface MelodyGuide {
|
|
280
313
|
notes: ReadonlyArray<{
|
|
@@ -302,12 +335,21 @@ interface SlotStats {
|
|
|
302
335
|
}
|
|
303
336
|
type EngineEvents = {
|
|
304
337
|
fault: (error: Error) => void;
|
|
338
|
+
/** After every move to another output; `gapMs` is how long the room heard nothing. */
|
|
339
|
+
outputChanged: (output: OutputInfo, gapMs: number) => void;
|
|
305
340
|
/** The mix after the clipper, reactions included, between songs too. */
|
|
306
341
|
masterLevel: (rmsDb: number, peakDb: number) => void;
|
|
307
342
|
};
|
|
308
343
|
type MixerEvents = {
|
|
309
344
|
level: (slot: number, rmsDb: number, peakDb: number) => void;
|
|
310
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Every 10 ms, on the guide's clock. `targetMidi` is the guide note the corrector pulls toward,
|
|
347
|
+
* after the backing's key shift: null with correction off, with no guide, or between notes,
|
|
348
|
+
* where it pulls toward the key. `errorSemitones` is the class it pulls toward less the
|
|
349
|
+
* reading, in the singer's own octave, so positive is flat: 0 with correction off or while
|
|
350
|
+
* unvoiced.
|
|
351
|
+
*/
|
|
352
|
+
pitch: (slot: number, positionMs: number, hz: number, confidence: number, targetMidi: number | null, errorSemitones: number) => void;
|
|
311
353
|
position: (positionMs: number) => void;
|
|
312
354
|
renderProgress: (fraction: number) => void;
|
|
313
355
|
backingEnded: () => void;
|
|
@@ -332,7 +374,8 @@ declare class Emitter<Events extends Record<keyof Events, Handler>> {
|
|
|
332
374
|
}
|
|
333
375
|
/** What a mixer needs from its engine. Internal to the package. */
|
|
334
376
|
interface MixerHost extends RenderHost {
|
|
335
|
-
|
|
377
|
+
/** The output playing now; it changes when the engine moves. */
|
|
378
|
+
output(): OutputInfo;
|
|
336
379
|
/**
|
|
337
380
|
* Sends a command carrying an id and resolves on its ack with the backing position. `onTimeout`
|
|
338
381
|
* hears the rejection when no ack comes in time.
|
|
@@ -450,10 +493,14 @@ export declare function openEngine(options: OpenEngineOptions): Promise<Engine>;
|
|
|
450
493
|
export declare class Engine {
|
|
451
494
|
#private;
|
|
452
495
|
readonly capabilities: ReadonlySet<Capability>;
|
|
453
|
-
readonly output: OutputInfo;
|
|
454
496
|
/** Offline engines only: render blocks and return them as interleaved stereo. */
|
|
455
497
|
readonly pump?: (blocks: number) => Float32Array;
|
|
456
498
|
constructor(options: OpenEngineOptions);
|
|
499
|
+
/**
|
|
500
|
+
* The output playing now. An engine opened with no `output.device` follows the OS default, and
|
|
501
|
+
* one with a `device` plays there while it exists; `outputChanged` fires after every move.
|
|
502
|
+
*/
|
|
503
|
+
get output(): OutputInfo;
|
|
457
504
|
/** Room-lifetime reaction voices, four at once. False when a fifth is dropped while four play. */
|
|
458
505
|
readonly reactions: {
|
|
459
506
|
play: (samples: Float32Array, channels: 1 | 2) => boolean;
|
package/dist/index.mjs
CHANGED
|
@@ -230,6 +230,10 @@ function defaultSlotParams() {
|
|
|
230
230
|
roomSize: 0,
|
|
231
231
|
mix: 0
|
|
232
232
|
},
|
|
233
|
+
voice: {
|
|
234
|
+
enabled: false,
|
|
235
|
+
formantSemitones: 0
|
|
236
|
+
},
|
|
233
237
|
guideOffsetMs: 0
|
|
234
238
|
};
|
|
235
239
|
}
|
|
@@ -305,10 +309,16 @@ var Mixer = class {
|
|
|
305
309
|
retuneMs: finiteIn("retuneMs", effect.retuneMs, 0, 1e3)
|
|
306
310
|
};
|
|
307
311
|
break;
|
|
308
|
-
case "reverb":
|
|
312
|
+
case "reverb":
|
|
313
|
+
next.reverb = {
|
|
314
|
+
enabled: effect.enabled,
|
|
315
|
+
roomSize: finiteIn("roomSize", effect.roomSize, 0, 1),
|
|
316
|
+
mix: finiteIn("mix", effect.mix, 0, 1)
|
|
317
|
+
};
|
|
318
|
+
break;
|
|
319
|
+
case "voice": next.voice = {
|
|
309
320
|
enabled: effect.enabled,
|
|
310
|
-
|
|
311
|
-
mix: finiteIn("mix", effect.mix, 0, 1)
|
|
321
|
+
formantSemitones: finiteIn("formantSemitones", effect.formantSemitones, -12, 12)
|
|
312
322
|
};
|
|
313
323
|
}
|
|
314
324
|
this.#write(slot, next);
|
|
@@ -393,7 +403,7 @@ var Mixer = class {
|
|
|
393
403
|
started,
|
|
394
404
|
stopping
|
|
395
405
|
]);
|
|
396
|
-
const { latencyMs } = this.#host.output;
|
|
406
|
+
const { latencyMs } = this.#host.output();
|
|
397
407
|
return {
|
|
398
408
|
startedAt: Date.now() + latencyMs,
|
|
399
409
|
backingOffsetMs: latencyMs,
|
|
@@ -488,10 +498,11 @@ var Mixer = class {
|
|
|
488
498
|
}
|
|
489
499
|
};
|
|
490
500
|
function defaultEffects() {
|
|
491
|
-
const { corrector, reverb } = defaultSlotParams();
|
|
501
|
+
const { corrector, reverb, voice } = defaultSlotParams();
|
|
492
502
|
return {
|
|
493
503
|
corrector,
|
|
494
|
-
reverb
|
|
504
|
+
reverb,
|
|
505
|
+
voice
|
|
495
506
|
};
|
|
496
507
|
}
|
|
497
508
|
function toGuideInput(guide) {
|
|
@@ -522,7 +533,7 @@ var Engine = class {
|
|
|
522
533
|
"pause",
|
|
523
534
|
"level"
|
|
524
535
|
]);
|
|
525
|
-
output;
|
|
536
|
+
#output;
|
|
526
537
|
/** How many slots a mixer may have, fixed at open. */
|
|
527
538
|
#slots;
|
|
528
539
|
/** Offline engines only: render blocks and return them as interleaved stereo. */
|
|
@@ -566,10 +577,10 @@ var Engine = class {
|
|
|
566
577
|
...device === void 0 ? {} : { device }
|
|
567
578
|
}, deliver);
|
|
568
579
|
}
|
|
569
|
-
this
|
|
580
|
+
this.#output = outputInfo(this.#native.output);
|
|
570
581
|
this.#host = {
|
|
571
582
|
native: this.#native,
|
|
572
|
-
output: this
|
|
583
|
+
output: () => this.#output,
|
|
573
584
|
command: (send, onTimeout) => this.#command(send, onTimeout),
|
|
574
585
|
watchRender: (id, onProgress) => new Promise((resolve, reject) => {
|
|
575
586
|
this.#renders.set(id, {
|
|
@@ -586,6 +597,13 @@ var Engine = class {
|
|
|
586
597
|
underruns: () => this.#underruns
|
|
587
598
|
};
|
|
588
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* The output playing now. An engine opened with no `output.device` follows the OS default, and
|
|
602
|
+
* one with a `device` plays there while it exists; `outputChanged` fires after every move.
|
|
603
|
+
*/
|
|
604
|
+
get output() {
|
|
605
|
+
return this.#output;
|
|
606
|
+
}
|
|
589
607
|
/** Room-lifetime reaction voices, four at once. False when a fifth is dropped while four play. */
|
|
590
608
|
reactions = { play: (samples, channels) => {
|
|
591
609
|
if (channels !== 1 && channels !== 2) throw new RangeError(`channels must be 1 or 2, got ${String(channels)}`);
|
|
@@ -654,7 +672,7 @@ var Engine = class {
|
|
|
654
672
|
this.#live?.events.emit("level", event.slot, event.rmsDb, event.peakDb);
|
|
655
673
|
break;
|
|
656
674
|
case "pitch":
|
|
657
|
-
this.#live?.events.emit("pitch", event.slot, event.positionMs, event.hz, event.confidence);
|
|
675
|
+
this.#live?.events.emit("pitch", event.slot, event.positionMs, event.hz, event.confidence, event.targetMidi ?? null, event.errorSemitones);
|
|
658
676
|
break;
|
|
659
677
|
case "position":
|
|
660
678
|
this.#live?.events.emit("position", framesToMs(event.frames));
|
|
@@ -677,6 +695,10 @@ var Engine = class {
|
|
|
677
695
|
case "renderProgress":
|
|
678
696
|
this.#renders.get(event.job)?.onProgress(event.fraction);
|
|
679
697
|
break;
|
|
698
|
+
case "outputChanged":
|
|
699
|
+
this.#output = outputInfo(event.output);
|
|
700
|
+
this.#events.emit("outputChanged", this.#output, event.gapMs);
|
|
701
|
+
break;
|
|
680
702
|
case "renderFinished": {
|
|
681
703
|
const render = this.#renders.get(event.job);
|
|
682
704
|
this.#renders.delete(event.job);
|
|
@@ -705,10 +727,15 @@ var Engine = class {
|
|
|
705
727
|
};
|
|
706
728
|
function outputInfo(info) {
|
|
707
729
|
if (info.sampleRate !== 48e3) throw new Error(`the engine runs at 48 kHz; the addon reports ${info.sampleRate}`);
|
|
730
|
+
const { periodFrames, latencyMs, device } = info;
|
|
708
731
|
return {
|
|
709
732
|
sampleRate: 48e3,
|
|
710
|
-
periodFrames
|
|
711
|
-
latencyMs
|
|
733
|
+
periodFrames,
|
|
734
|
+
latencyMs,
|
|
735
|
+
device: device === void 0 ? null : {
|
|
736
|
+
id: device.id,
|
|
737
|
+
name: device.name
|
|
738
|
+
}
|
|
712
739
|
};
|
|
713
740
|
}
|
|
714
741
|
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>
|
|
@@ -131,7 +132,7 @@ export type NativeEvent =
|
|
|
131
132
|
| { kind: 'ack'; id: number; positionFrames: number }
|
|
132
133
|
| { kind: 'level'; slot: number; rmsDb: number; peakDb: number }
|
|
133
134
|
| { kind: 'masterLevel'; rmsDb: number; peakDb: number }
|
|
134
|
-
| { kind: 'pitch'; slot: number; positionMs: number; hz: number; confidence: number }
|
|
135
|
+
| { kind: 'pitch'; slot: number; positionMs: number; hz: number; confidence: number; targetMidi?: number; errorSemitones: number }
|
|
135
136
|
| { kind: 'position'; frames: number }
|
|
136
137
|
| { kind: 'backingEnded' }
|
|
137
138
|
| { kind: 'underrun' }
|
|
@@ -139,6 +140,7 @@ export type NativeEvent =
|
|
|
139
140
|
| { kind: 'retired' }
|
|
140
141
|
| { kind: 'renderProgress'; job: number; fraction: number }
|
|
141
142
|
| { kind: 'renderFinished'; job: number }
|
|
143
|
+
| { kind: 'outputChanged'; output: OutputInfo; gapMs: number }
|
|
142
144
|
|
|
143
145
|
export interface NoteInput {
|
|
144
146
|
midi: number
|
|
@@ -148,13 +150,18 @@ export interface NoteInput {
|
|
|
148
150
|
|
|
149
151
|
export interface OpenOptions {
|
|
150
152
|
clock: 'offline' | 'timer' | 'device'
|
|
151
|
-
/** A device clock's output: an id from `outputs
|
|
153
|
+
/** A device clock's output: an id from `outputs` to pin, or none to follow the default output. */
|
|
152
154
|
device?: string
|
|
153
155
|
periodFrames: number
|
|
154
156
|
/** The jitter buffer's playout delay for every `rtp` and `l16` slot. */
|
|
155
157
|
playoutDelayFrames: number
|
|
156
158
|
}
|
|
157
159
|
|
|
160
|
+
export interface OutputDevice {
|
|
161
|
+
id: string
|
|
162
|
+
name: string
|
|
163
|
+
}
|
|
164
|
+
|
|
158
165
|
export interface OutputEntry {
|
|
159
166
|
id: string
|
|
160
167
|
name: string
|
|
@@ -165,6 +172,8 @@ export interface OutputInfo {
|
|
|
165
172
|
sampleRate: number
|
|
166
173
|
periodFrames: number
|
|
167
174
|
latencyMs: number
|
|
175
|
+
/** The output playing now; none on the offline clock. */
|
|
176
|
+
device?: OutputDevice
|
|
168
177
|
}
|
|
169
178
|
|
|
170
179
|
export interface ReverbInput {
|
|
@@ -197,6 +206,7 @@ export interface SlotParamsInput {
|
|
|
197
206
|
solo: boolean
|
|
198
207
|
corrector: CorrectorInput
|
|
199
208
|
reverb: ReverbInput
|
|
209
|
+
voice: VoiceInput
|
|
200
210
|
guideOffsetMs: number
|
|
201
211
|
}
|
|
202
212
|
|
|
@@ -216,3 +226,8 @@ export interface StemGainsInput {
|
|
|
216
226
|
instrumental: number
|
|
217
227
|
guide: number
|
|
218
228
|
}
|
|
229
|
+
|
|
230
|
+
export interface VoiceInput {
|
|
231
|
+
enabled: boolean
|
|
232
|
+
formantSemitones: number
|
|
233
|
+
}
|
|
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.4.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"
|