@sudobility/music_player 0.1.0 → 0.1.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.
- package/CLAUDE.md +335 -0
- package/dist/core.d.ts +33 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +33 -0
- package/dist/core.js.map +1 -0
- package/dist/engine.d.ts +103 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +2 -0
- package/dist/engine.js.map +1 -0
- package/dist/mocks/index.d.ts +46 -0
- package/dist/mocks/index.d.ts.map +1 -0
- package/dist/mocks/index.js +93 -0
- package/dist/mocks/index.js.map +1 -0
- package/dist/player.d.ts +81 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/player.js +209 -0
- package/dist/player.js.map +1 -0
- package/dist/rn/audio/offline-render.d.ts +29 -0
- package/dist/rn/audio/offline-render.d.ts.map +1 -0
- package/dist/rn/audio/offline-render.js +134 -0
- package/dist/rn/audio/offline-render.js.map +1 -0
- package/dist/rn/index.d.ts +50 -0
- package/dist/rn/index.d.ts.map +1 -0
- package/dist/rn/index.js +39 -0
- package/dist/rn/index.js.map +1 -0
- package/dist/rn/playback/audio-api.d.ts +93 -0
- package/dist/rn/playback/audio-api.d.ts.map +1 -0
- package/dist/rn/playback/audio-api.js +24 -0
- package/dist/rn/playback/audio-api.js.map +1 -0
- package/dist/rn/playback/expression-table.d.ts +22 -0
- package/dist/rn/playback/expression-table.d.ts.map +1 -0
- package/dist/rn/playback/expression-table.js +926 -0
- package/dist/rn/playback/expression-table.js.map +1 -0
- package/dist/rn/playback/expression.d.ts +48 -0
- package/dist/rn/playback/expression.d.ts.map +1 -0
- package/dist/rn/playback/expression.js +120 -0
- package/dist/rn/playback/expression.js.map +1 -0
- package/dist/rn/playback/gm-pack-name.d.ts +40 -0
- package/dist/rn/playback/gm-pack-name.d.ts.map +1 -0
- package/dist/rn/playback/gm-pack-name.js +61 -0
- package/dist/rn/playback/gm-pack-name.js.map +1 -0
- package/dist/rn/playback/pack-library.d.ts +44 -0
- package/dist/rn/playback/pack-library.d.ts.map +1 -0
- package/dist/rn/playback/pack-library.js +85 -0
- package/dist/rn/playback/pack-library.js.map +1 -0
- package/dist/rn/playback/pack-names.json +130 -0
- package/dist/rn/playback/sample-engine.d.ts +216 -0
- package/dist/rn/playback/sample-engine.d.ts.map +1 -0
- package/dist/rn/playback/sample-engine.js +669 -0
- package/dist/rn/playback/sample-engine.js.map +1 -0
- package/dist/rn/playback/sample-pack.d.ts +71 -0
- package/dist/rn/playback/sample-pack.d.ts.map +1 -0
- package/dist/rn/playback/sample-pack.js +112 -0
- package/dist/rn/playback/sample-pack.js.map +1 -0
- package/dist/rn/playback/sustain-loop.d.ts +24 -0
- package/dist/rn/playback/sustain-loop.d.ts.map +1 -0
- package/dist/rn/playback/sustain-loop.js +111 -0
- package/dist/rn/playback/sustain-loop.js.map +1 -0
- package/dist/rn/playback/voice-plan.d.ts +60 -0
- package/dist/rn/playback/voice-plan.d.ts.map +1 -0
- package/dist/rn/playback/voice-plan.js +38 -0
- package/dist/rn/playback/voice-plan.js.map +1 -0
- package/dist/shared/bus.d.ts +73 -0
- package/dist/shared/bus.d.ts.map +1 -0
- package/dist/shared/bus.js +83 -0
- package/dist/shared/bus.js.map +1 -0
- package/dist/shared/midi.d.ts +18 -0
- package/dist/shared/midi.d.ts.map +1 -0
- package/dist/shared/midi.js +25 -0
- package/dist/shared/midi.js.map +1 -0
- package/dist/shared/mix.d.ts +75 -0
- package/dist/shared/mix.d.ts.map +1 -0
- package/dist/shared/mix.js +115 -0
- package/dist/shared/mix.js.map +1 -0
- package/dist/shared/note-queue.d.ts +43 -0
- package/dist/shared/note-queue.d.ts.map +1 -0
- package/dist/shared/note-queue.js +54 -0
- package/dist/shared/note-queue.js.map +1 -0
- package/dist/shared/plan.d.ts +34 -0
- package/dist/shared/plan.d.ts.map +1 -0
- package/dist/shared/plan.js +134 -0
- package/dist/shared/plan.js.map +1 -0
- package/dist/shared/pump-window.d.ts +38 -0
- package/dist/shared/pump-window.d.ts.map +1 -0
- package/dist/shared/pump-window.js +13 -0
- package/dist/shared/pump-window.js.map +1 -0
- package/dist/shared/render-events.d.ts +25 -0
- package/dist/shared/render-events.d.ts.map +1 -0
- package/dist/shared/render-events.js +79 -0
- package/dist/shared/render-events.js.map +1 -0
- package/dist/shared/sounding-set.d.ts +45 -0
- package/dist/shared/sounding-set.d.ts.map +1 -0
- package/dist/shared/sounding-set.js +81 -0
- package/dist/shared/sounding-set.js.map +1 -0
- package/dist/shared/test-plan.d.ts +36 -0
- package/dist/shared/test-plan.d.ts.map +1 -0
- package/dist/shared/test-plan.js +83 -0
- package/dist/shared/test-plan.js.map +1 -0
- package/dist/singleton.d.ts +23 -0
- package/dist/singleton.d.ts.map +1 -0
- package/dist/singleton.js +26 -0
- package/dist/singleton.js.map +1 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/web/audio/offline-synth.d.ts +24 -0
- package/dist/web/audio/offline-synth.d.ts.map +1 -0
- package/dist/web/audio/offline-synth.js +92 -0
- package/dist/web/audio/offline-synth.js.map +1 -0
- package/dist/web/audio/soundfont-render.d.ts +31 -0
- package/dist/web/audio/soundfont-render.d.ts.map +1 -0
- package/dist/web/audio/soundfont-render.js +130 -0
- package/dist/web/audio/soundfont-render.js.map +1 -0
- package/dist/web/audio/synth-types.d.ts +24 -0
- package/dist/web/audio/synth-types.d.ts.map +1 -0
- package/dist/web/audio/synth-types.js +2 -0
- package/dist/web/audio/synth-types.js.map +1 -0
- package/dist/web/index.d.ts +40 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +59 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/playback/channel-allocator.d.ts +54 -0
- package/dist/web/playback/channel-allocator.d.ts.map +1 -0
- package/dist/web/playback/channel-allocator.js +72 -0
- package/dist/web/playback/channel-allocator.js.map +1 -0
- package/dist/web/playback/click.d.ts +9 -0
- package/dist/web/playback/click.d.ts.map +1 -0
- package/dist/web/playback/click.js +61 -0
- package/dist/web/playback/click.js.map +1 -0
- package/dist/web/playback/clock.d.ts +36 -0
- package/dist/web/playback/clock.d.ts.map +1 -0
- package/dist/web/playback/clock.js +64 -0
- package/dist/web/playback/clock.js.map +1 -0
- package/dist/web/playback/governor.d.ts +45 -0
- package/dist/web/playback/governor.d.ts.map +1 -0
- package/dist/web/playback/governor.js +33 -0
- package/dist/web/playback/governor.js.map +1 -0
- package/dist/web/playback/quiet-stub-notices.d.ts +55 -0
- package/dist/web/playback/quiet-stub-notices.d.ts.map +1 -0
- package/dist/web/playback/quiet-stub-notices.js +93 -0
- package/dist/web/playback/quiet-stub-notices.js.map +1 -0
- package/dist/web/playback/soundfont-engine.d.ts +281 -0
- package/dist/web/playback/soundfont-engine.d.ts.map +1 -0
- package/dist/web/playback/soundfont-engine.js +735 -0
- package/dist/web/playback/soundfont-engine.js.map +1 -0
- package/dist/web/playback/soundfont-loader.d.ts +30 -0
- package/dist/web/playback/soundfont-loader.d.ts.map +1 -0
- package/dist/web/playback/soundfont-loader.js +68 -0
- package/dist/web/playback/soundfont-loader.js.map +1 -0
- package/dist/web/playback/synth-host.d.ts +142 -0
- package/dist/web/playback/synth-host.d.ts.map +1 -0
- package/dist/web/playback/synth-host.js +293 -0
- package/dist/web/playback/synth-host.js.map +1 -0
- package/package.json +18 -3
- package/dist/index.d.ts +0 -14
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"click.js","sourceRoot":"","sources":["../../../src/web/playback/click.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,SAAS,GAAG,IAAI,CAAC;AACvB,MAAM,OAAO,GAAG,IAAI,CAAC;AACrB,MAAM,aAAa,GAAG,IAAI,CAAC;AAC3B,wEAAwE;AACxE,MAAM,UAAU,GAAG,IAAI,CAAC;AAUxB,MAAM,UAAU,aAAa,CAC3B,OAAyB,EACzB,WAAsB,EACtB,SAAiB,EACjB,MAAe;IAEf,MAAM,UAAU,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAClC,UAAU,CAAC,SAAS,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;IAC1D,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAE1B,6EAA6E;IAC7E,2BAA2B;IAC3B,MAAM,GAAG,GAAG,SAAS,GAAG,aAAa,CAAC;IACtC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAChD,IAAI,CAAC,IAAI,CAAC,4BAA4B,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEpD,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAC5B,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,UAAU,CAAC,OAAO,GAAG,GAAG,EAAE;QACxB,QAAQ,GAAG,IAAI,CAAC;QAChB,UAAU,CAAC,UAAU,EAAE,CAAC;QACxB,IAAI,CAAC,UAAU,EAAE,CAAC;IACpB,CAAC,CAAC;IAEF,OAAO;QACL,MAAM,EAAE,GAAG;QACX,MAAM,CAAC,IAAY;YACjB,oEAAoE;YACpE,yEAAyE;YACzE,kEAAkE;YAClE,IAAI,QAAQ,IAAI,IAAI,IAAI,GAAG;gBAAE,OAAO;YACpC,qEAAqE;YACrE,wDAAwD;YACxD,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC;YACtC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAClC,6DAA6D;YAC7D,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playback position, owned here rather than by Tone's Transport.
|
|
3
|
+
*
|
|
4
|
+
* `now` is injected: the engine passes the AudioContext's `currentTime`, so
|
|
5
|
+
* position is measured on the very clock the audio is rendered against, and
|
|
6
|
+
* tests pass a variable they control instead of waiting for real time.
|
|
7
|
+
*
|
|
8
|
+
* Everything works by anchoring — a position and the moment it was true —
|
|
9
|
+
* rather than by accumulating deltas. Accumulation drifts, and drift in the
|
|
10
|
+
* playback clock is what silenced tracks in the engine this replaces.
|
|
11
|
+
*/
|
|
12
|
+
export declare class PlaybackClock {
|
|
13
|
+
private readonly now;
|
|
14
|
+
private running;
|
|
15
|
+
private anchorNow;
|
|
16
|
+
private anchorPosition;
|
|
17
|
+
private rate;
|
|
18
|
+
constructor(now: () => number);
|
|
19
|
+
/** Starts, or resumes from wherever it was paused. `fromSeconds` overrides that. */
|
|
20
|
+
start(fromSeconds?: number): void;
|
|
21
|
+
pause(): void;
|
|
22
|
+
stop(): void;
|
|
23
|
+
/** Jumps to `seconds`, keeping whatever running state the clock had. */
|
|
24
|
+
seek(seconds: number): void;
|
|
25
|
+
/**
|
|
26
|
+
* Scales elapsed time — the playback-speed control.
|
|
27
|
+
*
|
|
28
|
+
* Banks the current position first, so a speed change applies from here on
|
|
29
|
+
* rather than retroactively rescaling everything already played, which would
|
|
30
|
+
* jump the caret.
|
|
31
|
+
*/
|
|
32
|
+
setRate(rate: number): void;
|
|
33
|
+
get positionSeconds(): number;
|
|
34
|
+
get isRunning(): boolean;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=clock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../../../src/web/playback/clock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,qBAAa,aAAa;IAMZ,OAAO,CAAC,QAAQ,CAAC,GAAG;IALhC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,IAAI,CAAK;gBAEY,GAAG,EAAE,MAAM,MAAM;IAE9C,oFAAoF;IACpF,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI;IAMjC,KAAK,IAAI,IAAI;IAMb,IAAI,IAAI,IAAI;IAMZ,wEAAwE;IACxE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK3B;;;;;;OAMG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM3B,IAAI,eAAe,IAAI,MAAM,CAG5B;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;CACF"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playback position, owned here rather than by Tone's Transport.
|
|
3
|
+
*
|
|
4
|
+
* `now` is injected: the engine passes the AudioContext's `currentTime`, so
|
|
5
|
+
* position is measured on the very clock the audio is rendered against, and
|
|
6
|
+
* tests pass a variable they control instead of waiting for real time.
|
|
7
|
+
*
|
|
8
|
+
* Everything works by anchoring — a position and the moment it was true —
|
|
9
|
+
* rather than by accumulating deltas. Accumulation drifts, and drift in the
|
|
10
|
+
* playback clock is what silenced tracks in the engine this replaces.
|
|
11
|
+
*/
|
|
12
|
+
export class PlaybackClock {
|
|
13
|
+
now;
|
|
14
|
+
running = false;
|
|
15
|
+
anchorNow = 0;
|
|
16
|
+
anchorPosition = 0;
|
|
17
|
+
rate = 1;
|
|
18
|
+
constructor(now) {
|
|
19
|
+
this.now = now;
|
|
20
|
+
}
|
|
21
|
+
/** Starts, or resumes from wherever it was paused. `fromSeconds` overrides that. */
|
|
22
|
+
start(fromSeconds) {
|
|
23
|
+
this.anchorPosition = fromSeconds ?? this.positionSeconds;
|
|
24
|
+
this.anchorNow = this.now();
|
|
25
|
+
this.running = true;
|
|
26
|
+
}
|
|
27
|
+
pause() {
|
|
28
|
+
if (!this.running)
|
|
29
|
+
return;
|
|
30
|
+
this.anchorPosition = this.positionSeconds;
|
|
31
|
+
this.running = false;
|
|
32
|
+
}
|
|
33
|
+
stop() {
|
|
34
|
+
this.running = false;
|
|
35
|
+
this.anchorPosition = 0;
|
|
36
|
+
this.anchorNow = this.now();
|
|
37
|
+
}
|
|
38
|
+
/** Jumps to `seconds`, keeping whatever running state the clock had. */
|
|
39
|
+
seek(seconds) {
|
|
40
|
+
this.anchorPosition = seconds;
|
|
41
|
+
this.anchorNow = this.now();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Scales elapsed time — the playback-speed control.
|
|
45
|
+
*
|
|
46
|
+
* Banks the current position first, so a speed change applies from here on
|
|
47
|
+
* rather than retroactively rescaling everything already played, which would
|
|
48
|
+
* jump the caret.
|
|
49
|
+
*/
|
|
50
|
+
setRate(rate) {
|
|
51
|
+
this.anchorPosition = this.positionSeconds;
|
|
52
|
+
this.anchorNow = this.now();
|
|
53
|
+
this.rate = rate;
|
|
54
|
+
}
|
|
55
|
+
get positionSeconds() {
|
|
56
|
+
if (!this.running)
|
|
57
|
+
return this.anchorPosition;
|
|
58
|
+
return this.anchorPosition + (this.now() - this.anchorNow) * this.rate;
|
|
59
|
+
}
|
|
60
|
+
get isRunning() {
|
|
61
|
+
return this.running;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=clock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.js","sourceRoot":"","sources":["../../../src/web/playback/clock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,OAAO,aAAa;IAMK;IALrB,OAAO,GAAG,KAAK,CAAC;IAChB,SAAS,GAAG,CAAC,CAAC;IACd,cAAc,GAAG,CAAC,CAAC;IACnB,IAAI,GAAG,CAAC,CAAC;IAEjB,YAA6B,GAAiB;QAAjB,QAAG,GAAH,GAAG,CAAc;IAAG,CAAC;IAElD,oFAAoF;IACpF,KAAK,CAAC,WAAoB;QACxB,IAAI,CAAC,cAAc,GAAG,WAAW,IAAI,IAAI,CAAC,eAAe,CAAC;QAC1D,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;IACtB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAC1B,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,eAAe,CAAC;QAC3C,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;IACvB,CAAC;IAED,IAAI;QACF,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACrB,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC;QACxB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC9B,CAAC;IAED,wEAAwE;IACxE,IAAI,CAAC,OAAe;QAClB,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC;QAC9B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC9B,CAAC;IAED;;;;;;OAMG;IACH,OAAO,CAAC,IAAY;QAClB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,eAAe,CAAC;QAC3C,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,IAAI,eAAe;QACjB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC,cAAc,CAAC;QAC9C,OAAO,IAAI,CAAC,cAAc,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;IACzE,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;CACF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive quality — currently measuring, not acting.
|
|
3
|
+
*
|
|
4
|
+
* This used to step interpolation order down after ten consecutive pump frames
|
|
5
|
+
* more than 100ms late, on the reasoning that a starved pump meant a starved
|
|
6
|
+
* synth. `setInterpolation(0)` renders the same passage in 53% of the time, so
|
|
7
|
+
* the knob is real, and timbre is the right thing to shed first.
|
|
8
|
+
*
|
|
9
|
+
* The scheduling horizon killed the reasoning. The pump no longer delivers
|
|
10
|
+
* notes on time — it tops up a four-second buffer in the worklet, and the
|
|
11
|
+
* synth renders from that buffer on the audio thread. The main thread can now
|
|
12
|
+
* stall for a second with nothing audible happening at all, so lateness here
|
|
13
|
+
* says only that the main thread is busy. Degrading timbre for every listener
|
|
14
|
+
* because a notation redraw took 119ms is a cost with no benefit.
|
|
15
|
+
*
|
|
16
|
+
* So the count is kept and nothing acts on it. It is free, and it is the shape
|
|
17
|
+
* a real audio-thread load signal would take. `AudioWorkletNode` exposes no
|
|
18
|
+
* such signal today, and `AudioContext` offers only latency figures that do not
|
|
19
|
+
* move under voice pressure; when one exists, it plugs in here.
|
|
20
|
+
*
|
|
21
|
+
* Polyphony was to be the other rung. `js-synthesizer` has no runtime setter
|
|
22
|
+
* for it — `ISynthesizer` has `setInterpolation` and `setGain` and nothing else
|
|
23
|
+
* — so the ceiling is chosen once at init in `synth-host.ts` and fluidsynth's
|
|
24
|
+
* own overflow priority does the stealing above it.
|
|
25
|
+
*/
|
|
26
|
+
export type GovernorOptions = {
|
|
27
|
+
onChange?: (interpolationOrder: number) => void;
|
|
28
|
+
};
|
|
29
|
+
export declare class Governor {
|
|
30
|
+
private readonly options;
|
|
31
|
+
private lateFrames;
|
|
32
|
+
constructor(options?: GovernorOptions);
|
|
33
|
+
get interpolation(): number;
|
|
34
|
+
/** Consecutive frames measured late. Diagnostic; nothing acts on it. */
|
|
35
|
+
get stalledFrames(): number;
|
|
36
|
+
/**
|
|
37
|
+
* One pump frame's lateness, in seconds.
|
|
38
|
+
*
|
|
39
|
+
* Counted, never acted on — see this module's doc for why. `options.onChange`
|
|
40
|
+
* is therefore never invoked; it is kept so the caller's wiring survives
|
|
41
|
+
* whatever replaces this signal.
|
|
42
|
+
*/
|
|
43
|
+
record(lateBySeconds: number): void;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=governor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"governor.d.ts","sourceRoot":"","sources":["../../../src/web/playback/governor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,EAAE,CAAC,kBAAkB,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD,CAAC;AAOF,qBAAa,QAAQ;IAGP,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,UAAU,CAAK;gBAEM,OAAO,GAAE,eAAoB;IAE1D,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED,wEAAwE;IACxE,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED;;;;;;OAMG;IACH,MAAM,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI;CAOpC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** fluidsynth's interpolation orders, best first. */
|
|
2
|
+
const LADDER = [7, 4, 1, 0];
|
|
3
|
+
/** How late a pump frame must be to count against the budget. */
|
|
4
|
+
const LATE_THRESHOLD_SECONDS = 0.1;
|
|
5
|
+
export class Governor {
|
|
6
|
+
options;
|
|
7
|
+
lateFrames = 0;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
}
|
|
11
|
+
get interpolation() {
|
|
12
|
+
return LADDER[0];
|
|
13
|
+
}
|
|
14
|
+
/** Consecutive frames measured late. Diagnostic; nothing acts on it. */
|
|
15
|
+
get stalledFrames() {
|
|
16
|
+
return this.lateFrames;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* One pump frame's lateness, in seconds.
|
|
20
|
+
*
|
|
21
|
+
* Counted, never acted on — see this module's doc for why. `options.onChange`
|
|
22
|
+
* is therefore never invoked; it is kept so the caller's wiring survives
|
|
23
|
+
* whatever replaces this signal.
|
|
24
|
+
*/
|
|
25
|
+
record(lateBySeconds) {
|
|
26
|
+
if (lateBySeconds > LATE_THRESHOLD_SECONDS) {
|
|
27
|
+
this.lateFrames += 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.lateFrames = 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=governor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"governor.js","sourceRoot":"","sources":["../../../src/web/playback/governor.ts"],"names":[],"mappings":"AA6BA,qDAAqD;AACrD,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5B,iEAAiE;AACjE,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAEnC,MAAM,OAAO,QAAQ;IAGU;IAFrB,UAAU,GAAG,CAAC,CAAC;IAEvB,YAA6B,UAA2B,EAAE;QAA7B,YAAO,GAAP,OAAO,CAAsB;IAAG,CAAC;IAE9D,IAAI,aAAa;QACf,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;IACnB,CAAC;IAED,wEAAwE;IACxE,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,aAAqB;QAC1B,IAAI,aAAa,GAAG,sBAAsB,EAAE,CAAC;YAC3C,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;IACtB,CAAC;CACF"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Silences libfluidsynth's build-stub notices, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* The WASM build stubs out filesystem calls it does not need and reports each
|
|
5
|
+
* one at *error* level every time a soundfont is loaded:
|
|
6
|
+
*
|
|
7
|
+
* fluidsynth: error: function fluid_stat is a stub, always returning -1
|
|
8
|
+
* fluidsynth: error: function fluid_file_test is a stub, always returning true
|
|
9
|
+
*
|
|
10
|
+
* They are build notices, not failures — audio is verified working with them
|
|
11
|
+
* firing. They also cannot be suppressed through `js-synthesizer`'s
|
|
12
|
+
* `disableLogging`, which controls fluidsynth's *log callback*: these come out
|
|
13
|
+
* of the C code's stderr through emscripten's stdio instead, as the stack shows
|
|
14
|
+
* (`put_char → write → doWritev → _fd_write`).
|
|
15
|
+
*
|
|
16
|
+
* Two different hooks are needed, because the two paths load the module
|
|
17
|
+
* differently.
|
|
18
|
+
*
|
|
19
|
+
* On the **main thread** the build is injected as a classic script, so its
|
|
20
|
+
* `var Module = typeof Module != "undefined" ? Module : {}` sees a global we
|
|
21
|
+
* seeded first, and `Module.printErr` redirects the stream.
|
|
22
|
+
*
|
|
23
|
+
* In an **AudioWorklet** that does not work: `addModule` loads module scripts,
|
|
24
|
+
* so `var Module` is module-scoped and a global seed is invisible. The hook
|
|
25
|
+
* there is the other half of the same line — `Module["printErr"] ||
|
|
26
|
+
* console.error.bind(console)` binds whatever `console.error` *is at module
|
|
27
|
+
* init*. Replacing it in the worklet's own scope beforehand therefore catches
|
|
28
|
+
* the stream, and affects nothing outside that worklet.
|
|
29
|
+
*
|
|
30
|
+
* This matters more than tidiness: fluidsynth calls the stubs once per sample
|
|
31
|
+
* while loading, which measured at over a thousand console writes per font —
|
|
32
|
+
* each capturing a stack, during the load this engine is already slow at.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately a pattern match rather than blanket suppression: anything that
|
|
35
|
+
* is not a stub notice is passed straight through to `console.error`, so a real
|
|
36
|
+
* fluidsynth failure still reaches the console.
|
|
37
|
+
*/
|
|
38
|
+
/** Matches only "function <name> is a stub, ..." — the emscripten build notices. */
|
|
39
|
+
export declare const STUB_NOTICE_PATTERN: RegExp;
|
|
40
|
+
export declare function isStubNotice(text: unknown): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* A module URL that filters the stub notices inside an AudioWorklet.
|
|
43
|
+
*
|
|
44
|
+
* Patches the worklet's own `console.error` rather than seeding `Module`, for
|
|
45
|
+
* the module-scoping reason above. Scoped to the worklet, so the page console
|
|
46
|
+
* is untouched.
|
|
47
|
+
*
|
|
48
|
+
* A blob rather than a hosted file: it is a few lines, and making an app host
|
|
49
|
+
* another asset just to quiet a log would be a poor trade. Returns `null` where
|
|
50
|
+
* blobs are unavailable, in which case the notices simply remain.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createWorkletQuietModuleUrl(): string | null;
|
|
53
|
+
/** Seeds the quiet `Module` on the main thread, before libfluidsynth is injected. */
|
|
54
|
+
export declare function seedMainThreadQuietModule(): void;
|
|
55
|
+
//# sourceMappingURL=quiet-stub-notices.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quiet-stub-notices.d.ts","sourceRoot":"","sources":["../../../src/web/playback/quiet-stub-notices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,oFAAoF;AACpF,eAAO,MAAM,mBAAmB,QAA2B,CAAC;AAE5D,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAEnD;AA0BD;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,IAAI,MAAM,GAAG,IAAI,CAG3D;AAED,qFAAqF;AACrF,wBAAgB,yBAAyB,IAAI,IAAI,CAQhD"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Silences libfluidsynth's build-stub notices, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* The WASM build stubs out filesystem calls it does not need and reports each
|
|
5
|
+
* one at *error* level every time a soundfont is loaded:
|
|
6
|
+
*
|
|
7
|
+
* fluidsynth: error: function fluid_stat is a stub, always returning -1
|
|
8
|
+
* fluidsynth: error: function fluid_file_test is a stub, always returning true
|
|
9
|
+
*
|
|
10
|
+
* They are build notices, not failures — audio is verified working with them
|
|
11
|
+
* firing. They also cannot be suppressed through `js-synthesizer`'s
|
|
12
|
+
* `disableLogging`, which controls fluidsynth's *log callback*: these come out
|
|
13
|
+
* of the C code's stderr through emscripten's stdio instead, as the stack shows
|
|
14
|
+
* (`put_char → write → doWritev → _fd_write`).
|
|
15
|
+
*
|
|
16
|
+
* Two different hooks are needed, because the two paths load the module
|
|
17
|
+
* differently.
|
|
18
|
+
*
|
|
19
|
+
* On the **main thread** the build is injected as a classic script, so its
|
|
20
|
+
* `var Module = typeof Module != "undefined" ? Module : {}` sees a global we
|
|
21
|
+
* seeded first, and `Module.printErr` redirects the stream.
|
|
22
|
+
*
|
|
23
|
+
* In an **AudioWorklet** that does not work: `addModule` loads module scripts,
|
|
24
|
+
* so `var Module` is module-scoped and a global seed is invisible. The hook
|
|
25
|
+
* there is the other half of the same line — `Module["printErr"] ||
|
|
26
|
+
* console.error.bind(console)` binds whatever `console.error` *is at module
|
|
27
|
+
* init*. Replacing it in the worklet's own scope beforehand therefore catches
|
|
28
|
+
* the stream, and affects nothing outside that worklet.
|
|
29
|
+
*
|
|
30
|
+
* This matters more than tidiness: fluidsynth calls the stubs once per sample
|
|
31
|
+
* while loading, which measured at over a thousand console writes per font —
|
|
32
|
+
* each capturing a stack, during the load this engine is already slow at.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately a pattern match rather than blanket suppression: anything that
|
|
35
|
+
* is not a stub notice is passed straight through to `console.error`, so a real
|
|
36
|
+
* fluidsynth failure still reaches the console.
|
|
37
|
+
*/
|
|
38
|
+
/** Matches only "function <name> is a stub, ..." — the emscripten build notices. */
|
|
39
|
+
export const STUB_NOTICE_PATTERN = /function \w+ is a stub/;
|
|
40
|
+
export function isStubNotice(text) {
|
|
41
|
+
return STUB_NOTICE_PATTERN.test(String(text));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The seed, as source, so it can run in a worklet where our modules cannot
|
|
45
|
+
* reach. Kept in sync with `isStubNotice` by construction: both use the same
|
|
46
|
+
* pattern, written once here.
|
|
47
|
+
*/
|
|
48
|
+
function seedSource() {
|
|
49
|
+
// Must run before libfluidsynth: emscripten binds console.error at init.
|
|
50
|
+
return `(function () {
|
|
51
|
+
var pattern = ${STUB_NOTICE_PATTERN.toString()};
|
|
52
|
+
var original = console.error.bind(console);
|
|
53
|
+
console.error = function () {
|
|
54
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
55
|
+
if (typeof arguments[i] === 'string' && pattern.test(arguments[i])) return;
|
|
56
|
+
}
|
|
57
|
+
original.apply(null, arguments);
|
|
58
|
+
};
|
|
59
|
+
globalThis.Module = Object.assign({}, globalThis.Module, {
|
|
60
|
+
printErr: function (text) {
|
|
61
|
+
if (!pattern.test(String(text))) original(text);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
})();`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A module URL that filters the stub notices inside an AudioWorklet.
|
|
68
|
+
*
|
|
69
|
+
* Patches the worklet's own `console.error` rather than seeding `Module`, for
|
|
70
|
+
* the module-scoping reason above. Scoped to the worklet, so the page console
|
|
71
|
+
* is untouched.
|
|
72
|
+
*
|
|
73
|
+
* A blob rather than a hosted file: it is a few lines, and making an app host
|
|
74
|
+
* another asset just to quiet a log would be a poor trade. Returns `null` where
|
|
75
|
+
* blobs are unavailable, in which case the notices simply remain.
|
|
76
|
+
*/
|
|
77
|
+
export function createWorkletQuietModuleUrl() {
|
|
78
|
+
if (typeof Blob === 'undefined' || typeof URL?.createObjectURL !== 'function')
|
|
79
|
+
return null;
|
|
80
|
+
return URL.createObjectURL(new Blob([seedSource()], { type: 'text/javascript' }));
|
|
81
|
+
}
|
|
82
|
+
/** Seeds the quiet `Module` on the main thread, before libfluidsynth is injected. */
|
|
83
|
+
export function seedMainThreadQuietModule() {
|
|
84
|
+
const scope = globalThis;
|
|
85
|
+
scope.Module = {
|
|
86
|
+
...scope.Module,
|
|
87
|
+
printErr: (text) => {
|
|
88
|
+
if (!isStubNotice(text))
|
|
89
|
+
console.error(text);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=quiet-stub-notices.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quiet-stub-notices.js","sourceRoot":"","sources":["../../../src/web/playback/quiet-stub-notices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,oFAAoF;AACpF,MAAM,CAAC,MAAM,mBAAmB,GAAG,wBAAwB,CAAC;AAE5D,MAAM,UAAU,YAAY,CAAC,IAAa;IACxC,OAAO,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;;;GAIG;AACH,SAAS,UAAU;IACjB,yEAAyE;IACzE,OAAO;kBACS,mBAAmB,CAAC,QAAQ,EAAE;;;;;;;;;;;;;MAa1C,CAAC;AACP,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,2BAA2B;IACzC,IAAI,OAAO,IAAI,KAAK,WAAW,IAAI,OAAO,GAAG,EAAE,eAAe,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IAC3F,OAAO,GAAG,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC;AACpF,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,yBAAyB;IACvC,MAAM,KAAK,GAAG,UAA6D,CAAC;IAC5E,KAAK,CAAC,MAAM,GAAG;QACb,GAAG,KAAK,CAAC,MAAM;QACf,QAAQ,EAAE,CAAC,IAAa,EAAE,EAAE;YAC1B,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC;gBAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC/C,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PlaybackEngine` over a worklet-hosted soundfont synth.
|
|
3
|
+
*
|
|
4
|
+
* The shape is: a note queue with a cursor, a clock we own, and a pump that
|
|
5
|
+
* keeps a multi-second horizon of timed notes topped up in the worklet's
|
|
6
|
+
* sequencer. Timing therefore lives on the audio thread, and the pump's only
|
|
7
|
+
* job — refilling a buffer — is one that tolerates being late. Voice
|
|
8
|
+
* allocation and stealing belong to fluidsynth, which is the point: every
|
|
9
|
+
* note-loss bug in the engine this replaces came from our own voice management
|
|
10
|
+
* refusing a note.
|
|
11
|
+
*
|
|
12
|
+
* Everything time-related is injected (`now`, `startPump`, `createContext`,
|
|
13
|
+
* `loadFont`) so the transport can be tested without an audio device or real
|
|
14
|
+
* time. The alternative is an engine verifiable only by ear, which is how the
|
|
15
|
+
* last one accumulated four silent-failure bugs.
|
|
16
|
+
*/
|
|
17
|
+
import type { PlaybackPlan, PlaybackTrack, ScoreRange } from '@sudobility/music_types';
|
|
18
|
+
import type { AuditionVoice, PlaybackEngine, PlaybackObserver } from '../../engine.js';
|
|
19
|
+
import type { LoadProgress } from './soundfont-loader.js';
|
|
20
|
+
/** The slice of `SynthHost` this engine drives, so tests can pass a stub. */
|
|
21
|
+
export type SynthHostLike = {
|
|
22
|
+
init(context: BaseAudioContext, options: {
|
|
23
|
+
fluidsynthModuleUrl: string;
|
|
24
|
+
workletModuleUrl: string;
|
|
25
|
+
soundfont: ArrayBuffer;
|
|
26
|
+
instanceCount: number;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
ensureInstances(count: number): Promise<void>;
|
|
29
|
+
setChannelPercussion(instance: number, channel: number, kit?: number): void;
|
|
30
|
+
programSelect(instance: number, channel: number, program: number): void;
|
|
31
|
+
noteOn(instance: number, channel: number, midi: number, velocity: number): void;
|
|
32
|
+
noteAt(instance: number, channel: number, midi: number, velocity: number, delaySeconds: number, durationSeconds: number): void;
|
|
33
|
+
noteOff(instance: number, channel: number, midi: number): void;
|
|
34
|
+
controlChange(instance: number, channel: number, control: number, value: number): void;
|
|
35
|
+
allSoundOff(): void;
|
|
36
|
+
setInterpolation(order: number): void;
|
|
37
|
+
setMasterVolume(volume: number): void;
|
|
38
|
+
setTrackCount(count: number): void;
|
|
39
|
+
dispose(): void;
|
|
40
|
+
};
|
|
41
|
+
export type SoundfontEngineDeps = {
|
|
42
|
+
host: SynthHostLike;
|
|
43
|
+
moduleUrls: {
|
|
44
|
+
fluidsynth: string;
|
|
45
|
+
worklet: string;
|
|
46
|
+
};
|
|
47
|
+
fontUrl: string;
|
|
48
|
+
loadFont?: (url: string, onProgress?: (progress: LoadProgress) => void) => Promise<ArrayBuffer>;
|
|
49
|
+
createContext?: () => AudioContext;
|
|
50
|
+
/** Defaults to the audio context's own clock, which is what the audio is rendered against. */
|
|
51
|
+
now?: () => number;
|
|
52
|
+
/** Starts the pump and returns a function that stops it. */
|
|
53
|
+
startPump?: (tick: () => void, intervalMs: number) => () => void;
|
|
54
|
+
};
|
|
55
|
+
export declare class SoundfontPlaybackEngine implements PlaybackEngine {
|
|
56
|
+
private readonly queue;
|
|
57
|
+
private readonly governor;
|
|
58
|
+
/** When the pump was last expected to run, for measuring how late it is. */
|
|
59
|
+
private nextPumpDueAt;
|
|
60
|
+
private readonly clock;
|
|
61
|
+
private readonly deps;
|
|
62
|
+
private context;
|
|
63
|
+
private plan;
|
|
64
|
+
/** Replaced on every `load`. The default is 120 BPM at ppq 480, as before. */
|
|
65
|
+
private tempo;
|
|
66
|
+
private tracks;
|
|
67
|
+
private observer;
|
|
68
|
+
private stopPump;
|
|
69
|
+
private readonly sounding;
|
|
70
|
+
/**
|
|
71
|
+
* When the last note of the loaded score finishes.
|
|
72
|
+
*
|
|
73
|
+
* The transport used to know the piece was over by the note queue being
|
|
74
|
+
* exhausted *and* no note-offs being outstanding. Releases now happen in the
|
|
75
|
+
* worklet, so there is nothing outstanding to count — this is what stops
|
|
76
|
+
* playback ending on the last note's attack instead of its release.
|
|
77
|
+
*/
|
|
78
|
+
private lastNoteEndSeconds;
|
|
79
|
+
private loopRange;
|
|
80
|
+
private lastReportedAt;
|
|
81
|
+
private initialized;
|
|
82
|
+
/** In-flight bring-up, so concurrent callers share one attempt. */
|
|
83
|
+
private initializing;
|
|
84
|
+
private loadState;
|
|
85
|
+
/** Counted so a pump that fails every tick reports once, not sixty times a second. */
|
|
86
|
+
private tickFailures;
|
|
87
|
+
/**
|
|
88
|
+
* Where playback should begin once the pump actually runs.
|
|
89
|
+
*
|
|
90
|
+
* The clock is *not* started by `play()`. Starting it there loses the opening
|
|
91
|
+
* of the piece: the first pump tick can be seconds late while the main thread
|
|
92
|
+
* finishes worklet and soundfont setup, and by then the clock says the music
|
|
93
|
+
* is already seconds in — so every note before that point is past its grace
|
|
94
|
+
* window and gets skipped. Measured at 2.3s of lost opening on a real load.
|
|
95
|
+
* Anchoring on the first tick instead means the piece starts where it should,
|
|
96
|
+
* however busy the thread was.
|
|
97
|
+
*/
|
|
98
|
+
private startAtSeconds;
|
|
99
|
+
private metronomeEnabled;
|
|
100
|
+
/** Click positions in ticks, with a cursor, mirroring how notes are drained. */
|
|
101
|
+
private clicks;
|
|
102
|
+
private clickCursor;
|
|
103
|
+
/**
|
|
104
|
+
* Clicks already placed on the audio graph, so the transport can take them
|
|
105
|
+
* back.
|
|
106
|
+
*
|
|
107
|
+
* They are scheduled a whole horizon ahead and do not go through the synth,
|
|
108
|
+
* so `allSoundOff` says nothing to them: pausing used to leave the room
|
|
109
|
+
* ticking for four seconds after the music stopped.
|
|
110
|
+
*/
|
|
111
|
+
private pendingClicks;
|
|
112
|
+
/** Audition voices are held on their own channel so they never disturb a track. */
|
|
113
|
+
private auditionHeld;
|
|
114
|
+
/** How many synths the current score's channel assignment needs. */
|
|
115
|
+
private instanceCount;
|
|
116
|
+
private tempoMultiplier;
|
|
117
|
+
/**
|
|
118
|
+
* The channel auditions borrow — recomputed per score so it is one no track owns.
|
|
119
|
+
*
|
|
120
|
+
* It used to be channel 15 flat. That channel belongs to a track on any score
|
|
121
|
+
* with fifteen pitched parts, so tapping a key selected the audition's program
|
|
122
|
+
* on it and left that part playing the wrong instrument until the next edit
|
|
123
|
+
* reloaded the score.
|
|
124
|
+
*/
|
|
125
|
+
private auditionChannel;
|
|
126
|
+
constructor(deps: SoundfontEngineDeps);
|
|
127
|
+
private now;
|
|
128
|
+
private secondsForTick;
|
|
129
|
+
/**
|
|
130
|
+
* Brings up the audio context and the synth, once.
|
|
131
|
+
*
|
|
132
|
+
* Resuming comes first, and the guard after it is the whole point. A browser
|
|
133
|
+
* starts an `AudioContext` **suspended** unless it was created during a user
|
|
134
|
+
* gesture, and a suspended context never runs its `AudioWorklet`. The synth's
|
|
135
|
+
* font load round-trips through that worklet, so attempting it on a suspended
|
|
136
|
+
* context does not fail — it simply never returns. That is what happened:
|
|
137
|
+
* `load` ran at load time, long before any click, and left a
|
|
138
|
+
* promise pending for good. Every later `play()` awaited the same promise,
|
|
139
|
+
* so the transport sat at "stopped" with nothing in the console to explain it.
|
|
140
|
+
*
|
|
141
|
+
* So when the context cannot be got running, this does the cheap part and
|
|
142
|
+
* returns. `play()` and `noteOn()` are user gestures and will get it running.
|
|
143
|
+
*/
|
|
144
|
+
initialize(): Promise<void>;
|
|
145
|
+
private bringUp;
|
|
146
|
+
/** Only on change, so a progress stream does not become a store-update stream. */
|
|
147
|
+
private reportLoad;
|
|
148
|
+
/** Best effort: without a user gesture behind it this is allowed to fail. */
|
|
149
|
+
private resumeContext;
|
|
150
|
+
/** A stub context in a test has no `state`; only a real suspended one blocks. */
|
|
151
|
+
private contextCanRun;
|
|
152
|
+
load(plan: PlaybackPlan): Promise<void>;
|
|
153
|
+
/**
|
|
154
|
+
* A channel no track owns, searched from the top down because tracks fill
|
|
155
|
+
* from the bottom.
|
|
156
|
+
*
|
|
157
|
+
* Channel 9 is skipped: it is General MIDI's drum channel, so borrowing it
|
|
158
|
+
* would make every audition a drum hit. Percussion auditions get their drum
|
|
159
|
+
* type switched on instead, which is the same route a second drum track takes.
|
|
160
|
+
*
|
|
161
|
+
* Where a score owns every channel of every open instance there is nothing to
|
|
162
|
+
* borrow, and it falls back to sharing the last one — an audition that
|
|
163
|
+
* disturbs one track's program beats a keyboard that makes no sound.
|
|
164
|
+
*/
|
|
165
|
+
private pickAuditionChannel;
|
|
166
|
+
/** Tells the host each track's program, percussion flag, pan and level. */
|
|
167
|
+
private applyPlanToHost;
|
|
168
|
+
/** Volume, mute and solo all resolve to one CC7 value per channel. */
|
|
169
|
+
private applyTrackLevels;
|
|
170
|
+
/**
|
|
171
|
+
* Starts the transport — reporting "playing" only once it is true.
|
|
172
|
+
*
|
|
173
|
+
* The first press of Play has to load a 23MB soundfont and hand it to
|
|
174
|
+
* fluidsynth, which takes seconds. This used to report "playing" up front so
|
|
175
|
+
* the button would not look dead through that wait, on the reasoning that it
|
|
176
|
+
* cost nothing in accuracy: the clock does not start until the pump's first
|
|
177
|
+
* tick, so no part of the piece is skipped however long the load takes.
|
|
178
|
+
*
|
|
179
|
+
* It cost something in accuracy elsewhere. The **caret** does not follow the
|
|
180
|
+
* engine's position while playing — it interpolates, dead-reckoning from the
|
|
181
|
+
* last 30Hz report with elapsed real time, because those reports arrive in
|
|
182
|
+
* clumps. During the load there are no reports at all, so the caret projected
|
|
183
|
+
* forward from a standing start and glided silently through several bars,
|
|
184
|
+
* then snapped back when the music finally began at the beginning. Two
|
|
185
|
+
* separately-sound optimisations that could not both be right.
|
|
186
|
+
*
|
|
187
|
+
* So the transport now says what is true, and `PlaybackLoadState` — which the
|
|
188
|
+
* bar renders as "Preparing instruments 45%" — says why the wait is happening.
|
|
189
|
+
* That indicator did not exist when the early report was written; it is what
|
|
190
|
+
* makes honesty affordable here.
|
|
191
|
+
*
|
|
192
|
+
* Only a cold engine waits. Once the synth is up this reports synchronously,
|
|
193
|
+
* which is every press after the first.
|
|
194
|
+
*/
|
|
195
|
+
play(fromTick?: number): Promise<void>;
|
|
196
|
+
pause(): void;
|
|
197
|
+
stop(): void;
|
|
198
|
+
seek(tick: number): void;
|
|
199
|
+
/**
|
|
200
|
+
* Changes playback speed, rebuilding what is already queued.
|
|
201
|
+
*
|
|
202
|
+
* Every note in the sequencer carries a delay worked out at the speed that
|
|
203
|
+
* was in force when it was posted, and the horizon is four seconds deep — so
|
|
204
|
+
* on its own a speed change moved the caret at once and left the music at the
|
|
205
|
+
* old tempo for a whole horizon, then jumped. Re-seeking where we stand drops
|
|
206
|
+
* that backlog, and the next tick refills it at the new speed. The gap is one
|
|
207
|
+
* pump interval.
|
|
208
|
+
*/
|
|
209
|
+
setTempoMultiplier(multiplier: number): void;
|
|
210
|
+
setLoop(range: ScoreRange | null): void;
|
|
211
|
+
setTrackMute(trackId: string, muted: boolean): void;
|
|
212
|
+
setTrackSolo(trackId: string, solo: boolean): void;
|
|
213
|
+
/**
|
|
214
|
+
* Re-reads the mix off `score` and pushes it, scheduling nothing.
|
|
215
|
+
*
|
|
216
|
+
* `setTrackMute`/`setTrackSolo` covered two of the four mix properties.
|
|
217
|
+
* Volume was read once, in `load`, and pan only in `applyPlanToHost` —
|
|
218
|
+
* so with the playback edit lock in place, where a mix change deliberately
|
|
219
|
+
* does *not* reload, moving a fader mid-playback moved the fader and left the
|
|
220
|
+
* sound where it was.
|
|
221
|
+
*
|
|
222
|
+
* A track the engine does not know is skipped rather than throwing: a mix
|
|
223
|
+
* change cannot add a track (that would be content), so an unknown id means
|
|
224
|
+
* the engine is holding an older plan and the next `load` settles it.
|
|
225
|
+
*/
|
|
226
|
+
applyMix(tracks: readonly PlaybackTrack[]): void;
|
|
227
|
+
setMetronome(enabled: boolean): void;
|
|
228
|
+
setMasterVolume(volume: number): void;
|
|
229
|
+
noteOn(midi: number, voice: AuditionVoice): void;
|
|
230
|
+
noteOff(midi: number): void;
|
|
231
|
+
setObserver(observer: PlaybackObserver | null): void;
|
|
232
|
+
dispose(): void;
|
|
233
|
+
private startPump;
|
|
234
|
+
private endPump;
|
|
235
|
+
private tick;
|
|
236
|
+
private dispatchTick;
|
|
237
|
+
/**
|
|
238
|
+
* The far edge of what this tick may schedule, in ticks.
|
|
239
|
+
*
|
|
240
|
+
* Clamped to just inside a loop, because the wrap only happens on the pump
|
|
241
|
+
* tick *after* the end is reached: without this a note or click a few
|
|
242
|
+
* milliseconds past the loop end was already in the sequencer and sounded
|
|
243
|
+
* before the loop came round, on every pass. The end tick itself belongs to
|
|
244
|
+
* the next pass, where it is the loop's own first beat.
|
|
245
|
+
*/
|
|
246
|
+
private horizonTick;
|
|
247
|
+
/**
|
|
248
|
+
* How late this frame ran against when it was due.
|
|
249
|
+
*
|
|
250
|
+
* Measured on the audio clock rather than wall time, because that is the
|
|
251
|
+
* clock the audio is rendered against and the one that matters if the thread
|
|
252
|
+
* is being starved.
|
|
253
|
+
*/
|
|
254
|
+
private measureLateness;
|
|
255
|
+
/**
|
|
256
|
+
* Clicks due in this window, scheduled ahead at their exact moment.
|
|
257
|
+
*
|
|
258
|
+
* Unlike notes, these can be placed precisely: an oscillator started with
|
|
259
|
+
* `start(at)` sounds at that sample however busy the main thread is, so the
|
|
260
|
+
* click stays steady even when the pump runs late.
|
|
261
|
+
*/
|
|
262
|
+
private pumpMetronome;
|
|
263
|
+
/** Takes back every click still queued — a stop, a seek, or the switch going off. */
|
|
264
|
+
private cancelPendingClicks;
|
|
265
|
+
/** Inverse of `secondsForTick`, for turning the lookahead window back into ticks. */
|
|
266
|
+
private tickForSeconds;
|
|
267
|
+
private get playbackSpeed();
|
|
268
|
+
private applyLoop;
|
|
269
|
+
/**
|
|
270
|
+
* Position and active notes, both at 30Hz.
|
|
271
|
+
*
|
|
272
|
+
* The engine this replaces called `onActiveNotes` on every note-on *and*
|
|
273
|
+
* note-off — some 11,500 store updates for a three-minute piece, on the same
|
|
274
|
+
* thread the scheduler runs on. Coalescing is what keeps that work off the
|
|
275
|
+
* pump's back.
|
|
276
|
+
*/
|
|
277
|
+
private report;
|
|
278
|
+
/** Drops the sounding set and says so, for a seek, stop or teardown. */
|
|
279
|
+
private clearSounding;
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=soundfont-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"soundfont-engine.d.ts","sourceRoot":"","sources":["../../../src/web/playback/soundfont-engine.ts"],"names":[],"mappings":"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"}
|