buninu-browser 0.7.0 → 0.9.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/AGENTS.md +5 -0
- package/README.md +90 -1
- package/buninu-browser.js +1 -1
- package/frontends/wayland/README.md +35 -2
- package/frontends/wayland/client.js +8 -6
- package/frontends/wayland/clipboard.js +1 -1
- package/frontends/wayland/index.js +1 -1
- package/frontends/wayland/input.js +1 -1
- package/frontends/wayland/resource-io.js +6 -0
- package/frontends/wayland/transport-posix.js +120 -0
- package/frontends/wayland/transport-win32.js +9 -0
- package/frontends/wayland/transport.js +3 -120
- package/frontends/wayland/win32-resources.js +80 -0
- package/frontends/wayland/win32-wire.js +62 -0
- package/lib/audio/biquad.js +25 -0
- package/lib/audio/piano.js +6 -3
- package/lib/audio/playback-buffer.js +5 -5
- package/lib/audio/pulse-client.js +24 -5
- package/lib/audio/web-audio.js +570 -0
- package/lib/browser-dialog.js +116 -0
- package/lib/cdp-server.js +43 -11
- package/lib/file-picker.js +48 -0
- package/lib/headless-shell.js +34 -12
- package/lib/layout/text-layout.js +112 -19
- package/lib/media/mp4-frame-times.js +93 -0
- package/lib/paint/direct-canvas.ts +39 -0
- package/lib/paint/display-list.js +37 -12
- package/lib/paint/raster-readback.js +16 -7
- package/lib/renderer/animation-frames.js +10 -5
- package/lib/renderer/bindings.js +69 -16
- package/lib/renderer/canvas-2d.js +265 -15
- package/lib/renderer/canvas-round-rect.js +36 -0
- package/lib/renderer/css-animations.js +173 -0
- package/lib/renderer/dialog-client.js +30 -0
- package/lib/renderer/dialog-worker.js +35 -0
- package/lib/renderer/eval-environment.js +28 -0
- package/lib/renderer/file-input.js +49 -0
- package/lib/renderer/host.js +33 -4
- package/lib/renderer/html-drag.js +77 -0
- package/lib/renderer/image-loading.js +45 -2
- package/lib/renderer/main.js +4 -0
- package/lib/renderer/module-transform.js +5 -3
- package/lib/renderer/page-builtins.js +59 -0
- package/lib/renderer/page-realm.js +177 -40
- package/lib/renderer/page-renderer.js +397 -49
- package/lib/renderer/piano-input.js +3 -1
- package/lib/renderer/raster-surface.js +18 -0
- package/lib/renderer/script-rewrite.js +165 -9
- package/lib/renderer/video-presentation.js +5 -4
- package/lib/renderer/webgl/clip.js +24 -0
- package/lib/renderer/webgl/context.js +158 -0
- package/lib/renderer/webgl/cpu-fragment.js +211 -0
- package/lib/renderer/webgl/draw-cache.js +22 -0
- package/lib/renderer/webgl/pipeline.js +70 -0
- package/lib/renderer/webgl/shaders.js +58 -0
- package/lib/renderer/webgl/textures.js +43 -0
- package/lib/renderer/window-named-properties.js +46 -0
- package/lib/screenshot-cli.js +4 -7
- package/lib/style/animation-values.js +29 -0
- package/lib/style/computed-style.js +109 -13
- package/lib/style/transform.js +44 -0
- package/lib/window-size.js +10 -0
- package/package.json +1 -1
- package/patches/happy-dom/0007-scope-content-models-to-html-namespace.patch +29 -0
- package/patches/happy-dom/README.md +16 -0
- package/test/animation-frames.test.js +15 -0
- package/test/canvas-2d.test.js +252 -4
- package/test/canvas-frame-pacing.test.js +7 -0
- package/test/cdp-viewers.test.js +36 -0
- package/test/classic-games-patches/0001-fix-game-initialization.patch +177 -0
- package/test/classic-games-patches/README.md +156 -0
- package/test/classic-games-scenarios.js +133 -0
- package/test/clipboard.test.js +16 -0
- package/test/computed-style.test.js +18 -0
- package/test/css-animation-lifecycle.test.js +28 -0
- package/test/css-animations.test.js +83 -0
- package/test/direct-canvas.test.js +39 -0
- package/test/dynamic-inline-handlers.test.js +25 -0
- package/test/eval-declarations.test.js +22 -0
- package/test/file-picker.test.js +26 -0
- package/test/fixtures/canvas-damage.js +23 -0
- package/test/fixtures/canvas-frame-pacing.js +35 -0
- package/test/fixtures/direct-canvas.js +96 -0
- package/test/fixtures/javascript-dialogs.html +8 -0
- package/test/fixtures/media-audio-lifetime.js +65 -0
- package/test/fixtures/renderer-memory.js +7 -5
- package/test/fixtures/video-black-replay.js +8 -6
- package/test/fixtures/video-slow-end.js +39 -0
- package/test/fixtures/web-audio-renderer.js +45 -0
- package/test/flex.test.js +26 -0
- package/test/grid.test.js +13 -0
- package/test/headless-shell.test.js +9 -0
- package/test/headless-window-size.test.js +28 -0
- package/test/html-rcdata.test.js +35 -0
- package/test/image-decode.test.js +18 -0
- package/test/javascript-dialog.test.js +83 -0
- package/test/manual-breakout-paddle.js +42 -0
- package/test/manual-breakout-static.js +35 -0
- package/test/manual-classic-games.js +41 -0
- package/test/manual-clumsy-bird-three-pipes.js +81 -0
- package/test/manual-clumsy-bird.js +52 -0
- package/test/manual-hextris.js +30 -0
- package/test/manual-numbered-music.js +24 -0
- package/test/manual-t-rex-runner.js +49 -0
- package/test/manual-threejs-scene.js +64 -0
- package/test/manual-webgl-benchmark.js +27 -0
- package/test/manual-webgl-cpu.html +26 -0
- package/test/media-audio-lifetime.test.js +8 -0
- package/test/mp4-frame-times.test.js +33 -0
- package/test/page-scripts.test.js +96 -1
- package/test/piano-input.test.js +15 -2
- package/test/piano.test.js +11 -2
- package/test/playback-buffer.test.js +11 -0
- package/test/pointer-drag.test.js +33 -0
- package/test/positioning.test.js +82 -0
- package/test/pulse-client.test.js +51 -2
- package/test/raster-readback.test.js +14 -0
- package/test/script-rewrite.test.js +61 -3
- package/test/video-slow-end.test.js +6 -0
- package/test/viewport-keyboard-overflow.test.js +25 -0
- package/test/wayland-win32-resources.test.js +47 -0
- package/test/wayland-win32-wire.test.js +59 -0
- package/test/web-audio-renderer.test.js +8 -0
- package/test/web-audio.test.js +264 -0
- package/test/webgl-clip.test.js +9 -0
- package/test/webgl-cpu-fragment.test.js +43 -0
- package/test/window-named-properties.test.js +41 -0
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfig.ts +2 -2
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfigContentModelEnum.ts +1 -0
- package/vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts +23 -3
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +1 -1
- package/vendor/land-renderer/LICENSE +21 -0
- package/vendor/land-renderer/README.md +14 -0
- package/vendor/land-renderer/direct-compositor.ts +443 -0
- package/vendor/land-renderer/frame.js +88 -0
- package/vendor/land-renderer/png-fast.js +85 -0
- package/vendor/land-renderer/raw-frames.js +66 -0
- package/vendor/land-renderer/render-types.ts +7 -0
- package/vendor/land-renderer/renderer.ts +134 -0
- package/vendor/land-renderer/snapshot.js +37 -0
- package/vendor/land-renderer/viewport.js +18 -0
- package/vendor/ses/dist/ses.mjs +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// A Windows-only envelope around unchanged Wayland bytes and FD argument order.
|
|
2
|
+
// ACKs keep mapping names alive until the receiver has opened its own handles.
|
|
3
|
+
export class MappingConnection {
|
|
4
|
+
constructor(resources,removed=()=>{}){
|
|
5
|
+
this.resources=resources;this.removed=removed;this.fds=[];this.input=Buffer.alloc(0);this.wire=Buffer.alloc(0);
|
|
6
|
+
this.output=[];this.queuedBytes=0;this.pending=new Map();this.next=1;this.closed=false;
|
|
7
|
+
}
|
|
8
|
+
attach(socket){this.socket=socket;this.flush()}
|
|
9
|
+
packet(meta,data=Buffer.alloc(0)){
|
|
10
|
+
const json=Buffer.from(JSON.stringify(meta)),body=Buffer.alloc(8+json.length+data.length);
|
|
11
|
+
body.writeUInt32LE(4+json.length+data.length);body.writeUInt32LE(json.length,4);json.copy(body,8);data.copy(body,8+json.length);
|
|
12
|
+
if(this.queuedBytes+body.length>4*1024*1024)throw Error('Output queue limit');
|
|
13
|
+
this.output.push(body);this.queuedBytes+=body.length;this.flush();
|
|
14
|
+
}
|
|
15
|
+
send(bytes,fds=[]){
|
|
16
|
+
if(this.closed)return;
|
|
17
|
+
if(this.pending.size>=256)throw Error('Resource ACK queue limit');
|
|
18
|
+
const held=[];
|
|
19
|
+
try{
|
|
20
|
+
for(const fd of fds)held.push(this.resources.sendResource(fd));
|
|
21
|
+
const id=this.next++;if(held.length)this.pending.set(id,held);
|
|
22
|
+
this.packet({version:1,id,resources:held.map(h=>h.descriptor)},bytes);
|
|
23
|
+
}catch(e){for(const h of held)h.release();this.close();throw e}
|
|
24
|
+
}
|
|
25
|
+
flush(){
|
|
26
|
+
try { while(!this.closed&&this.socket&&this.output.length){
|
|
27
|
+
const b=this.output[0],n=this.socket.write(b);if(n<0){this.close();return}if(!n)return;
|
|
28
|
+
this.queuedBytes-=n;if(n<b.length){this.output[0]=b.subarray(n);return}this.output.shift();
|
|
29
|
+
}} catch(error){this.error=error;this.close()}
|
|
30
|
+
}
|
|
31
|
+
receive(bytes){
|
|
32
|
+
try{
|
|
33
|
+
this.wire=Buffer.concat([this.wire,bytes]);
|
|
34
|
+
if(this.wire.length>2*1024*1024)throw Error('Input queue limit');
|
|
35
|
+
while(this.wire.length>=8){
|
|
36
|
+
const size=this.wire.readUInt32LE(),jsonSize=this.wire.readUInt32LE(4);
|
|
37
|
+
if(size<4||size>1024*1024||jsonSize>65536||jsonSize>size-4)throw Error('Invalid envelope');
|
|
38
|
+
if(this.wire.length<size+4)return;
|
|
39
|
+
const meta=JSON.parse(this.wire.subarray(8,8+jsonSize).toString()),data=this.wire.subarray(8+jsonSize,size+4);
|
|
40
|
+
this.wire=this.wire.subarray(size+4);
|
|
41
|
+
if(meta.version!==1)throw Error('Unknown mapping transport version');
|
|
42
|
+
if(meta.ack!==undefined){
|
|
43
|
+
const held=this.pending.get(meta.ack);if(!held||data.length)throw Error('Invalid resource ACK');
|
|
44
|
+
this.pending.delete(meta.ack);for(const h of held)h.release();continue;
|
|
45
|
+
}
|
|
46
|
+
if(!Number.isSafeInteger(meta.id)||!Array.isArray(meta.resources)||meta.resources.length>256)throw Error('Invalid resource packet');
|
|
47
|
+
for(const d of meta.resources)this.fds.push(this.resources.receiveResource(d));
|
|
48
|
+
if(meta.resources.length)this.packet({version:1,ack:meta.id});
|
|
49
|
+
this.input=Buffer.concat([this.input,data]);
|
|
50
|
+
if(this.input.length>1024*1024||this.fds.length>256)throw Error('Wayland queue limit');
|
|
51
|
+
this.onData?.();if(this.closed)return;
|
|
52
|
+
}
|
|
53
|
+
}catch(error){this.error=error;this.close()}
|
|
54
|
+
}
|
|
55
|
+
poll(){this.flush()}
|
|
56
|
+
close(){
|
|
57
|
+
if(this.closed)return;this.closed=true;try{this.socket?.end()}catch{}
|
|
58
|
+
for(const held of this.pending.values())for(const h of held)h.release();this.pending.clear();
|
|
59
|
+
for(const fd of this.fds)this.resources.closeResource(fd);this.fds=[];this.output=[];this.queuedBytes=0;
|
|
60
|
+
try{this.onClose?.()}finally{this.removed()}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Web Audio / Audio EQ Cookbook normalized second-order filter coefficients.
|
|
2
|
+
export function biquadCoefficients(type,frequency,q,gain,rate) {
|
|
3
|
+
const w=2*Math.PI*Math.max(0,Math.min(rate/2,frequency))/rate,c=Math.cos(w),s=Math.sin(w);
|
|
4
|
+
const A=10**(gain/40),alpha=s/(2*Math.max(1e-12,['lowpass','highpass'].includes(type)?10**(q/20):q));
|
|
5
|
+
let b0,b1,b2,a0=1+alpha,a1=-2*c,a2=1-alpha;
|
|
6
|
+
switch(type){
|
|
7
|
+
case 'lowpass':b0=(1-c)/2;b1=1-c;b2=b0;break;
|
|
8
|
+
case 'highpass':b0=(1+c)/2;b1=-(1+c);b2=b0;break;
|
|
9
|
+
case 'bandpass':b0=alpha;b1=0;b2=-alpha;break;
|
|
10
|
+
case 'notch':b0=1;b1=-2*c;b2=1;break;
|
|
11
|
+
case 'allpass':b0=1-alpha;b1=-2*c;b2=1+alpha;break;
|
|
12
|
+
case 'peaking':b0=1+alpha*A;b1=-2*c;b2=1-alpha*A;a0=1+alpha/A;a2=1-alpha/A;break;
|
|
13
|
+
case 'lowshelf':case 'highshelf':{
|
|
14
|
+
const t=Math.sqrt(2*A)*s;
|
|
15
|
+
if(type==='lowshelf'){
|
|
16
|
+
b0=A*((A+1)-(A-1)*c+t);b1=2*A*((A-1)-(A+1)*c);b2=A*((A+1)-(A-1)*c-t);
|
|
17
|
+
a0=(A+1)+(A-1)*c+t;a1=-2*((A-1)+(A+1)*c);a2=(A+1)+(A-1)*c-t;
|
|
18
|
+
}else{
|
|
19
|
+
b0=A*((A+1)+(A-1)*c+t);b1=-2*A*((A-1)+(A+1)*c);b2=A*((A+1)+(A-1)*c-t);
|
|
20
|
+
a0=(A+1)-(A-1)*c+t;a1=2*((A-1)-(A+1)*c);a2=(A+1)-(A-1)*c-t;
|
|
21
|
+
}break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return [b0/a0,b1/a0,b2/a0,a1/a0,a2/a0];
|
|
25
|
+
}
|
package/lib/audio/piano.js
CHANGED
|
@@ -4,8 +4,9 @@ import { floatToS16 } from "./pcm.js";
|
|
|
4
4
|
// is relative to Space = MIDI 60 (C4). Change PIANO_DO alone to transpose
|
|
5
5
|
// every key; the default A4 exactly matches LazyI's original octave.
|
|
6
6
|
// The LazyI key intervals remain fixed in the table.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// Left hand: Escape, `, Tab, CapsLock, ShiftLeft play a ii chord (re la re fa
|
|
8
|
+
// la); AltRight (AltGr) continues . , m (do si la) down to sol. Other control
|
|
9
|
+
// keys (right Shift, Alt, Control, Backspace) are intentionally omitted.
|
|
9
10
|
export const PIANO_KEYS = Object.freeze({
|
|
10
11
|
F1: 29, F2: 36, F3: 41, F4: 45, F5: 48, F6: 53,
|
|
11
12
|
F7: 81, F8: 83, F9: 84, F10: 86, F11: 88, F12: 89,
|
|
@@ -17,6 +18,7 @@ export const PIANO_KEYS = Object.freeze({
|
|
|
17
18
|
k: 64, l: 65, ";": 67, "'": 69, Enter: 71,
|
|
18
19
|
z: 28, x: 35, c: 40, v: 43, b: 47, n: 67, m: 57,
|
|
19
20
|
",": 59, ".": 60, "/": 62, " ": 60,
|
|
21
|
+
Escape: 26, Tab: 38, CapsLock: 41, ShiftLeft: 45, AltRight: 55,
|
|
20
22
|
});
|
|
21
23
|
export const PIANO_DO = 69; // A4, LazyI's original Space pitch.
|
|
22
24
|
|
|
@@ -24,6 +26,7 @@ const CODE_KEYS = {
|
|
|
24
26
|
Backquote: "`", Minus: "-", Equal: "=", BracketLeft: "[", BracketRight: "]",
|
|
25
27
|
Backslash: "\\", Semicolon: ";", Quote: "'", Comma: ",", Period: ".", Slash: "/",
|
|
26
28
|
Space: " ", NumpadEnter: "Enter",
|
|
29
|
+
Escape: "Escape", Tab: "Tab", CapsLock: "CapsLock", ShiftLeft: "ShiftLeft", AltRight: "AltRight",
|
|
27
30
|
};
|
|
28
31
|
const SHIFTED_KEYS = {
|
|
29
32
|
"~": "`", "!": "1", "@": "2", "#": "3", "$": "4", "%": "5", "^": "6",
|
|
@@ -37,7 +40,7 @@ export function pianoNote(key, code = "") {
|
|
|
37
40
|
: /^Digit[0-9]$/.test(code) ? code.slice(5)
|
|
38
41
|
: CODE_KEYS[code] ?? (code.startsWith("F") ? code : null);
|
|
39
42
|
const typed = key.length === 1 ? (SHIFTED_KEYS[key] ?? key.toLowerCase())
|
|
40
|
-
: key === "Space" || key === "Spacebar" ? " " : key;
|
|
43
|
+
: key === "Space" || key === "Spacebar" ? " " : key === "AltGraph" ? "AltRight" : key;
|
|
41
44
|
const note = PIANO_KEYS[physical ?? typed];
|
|
42
45
|
return note === undefined ? null : note + PIANO_DO - 60;
|
|
43
46
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Decoded PCM kept in the controller is independent of the device queue.
|
|
2
2
|
// It can bridge renderer work without increasing pause/seek output latency.
|
|
3
3
|
export class PlaybackBuffer {
|
|
4
|
-
constructor(bytesPerSecond, frameBytes, leadSeconds) {
|
|
4
|
+
constructor(bytesPerSecond, frameBytes, leadSeconds, liveReserveSeconds = 0) {
|
|
5
5
|
this.bytesPerSecond = bytesPerSecond;
|
|
6
6
|
this.frameBytes = frameBytes;
|
|
7
|
-
this.reserve = leadSeconds <= 0.15 ? 0
|
|
8
|
-
this.threshold =
|
|
9
|
-
this.buffering = Boolean(this.
|
|
7
|
+
this.reserve = this.align(bytesPerSecond * (leadSeconds <= 0.15 ? Math.max(0, Math.min(.5, liveReserveSeconds)) : 2));
|
|
8
|
+
this.threshold = leadSeconds <= 0.15 ? 0 : this.align(bytesPerSecond * 0.4);
|
|
9
|
+
this.buffering = Boolean(this.threshold);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
align(bytes) {
|
|
@@ -14,7 +14,7 @@ export class PlaybackBuffer {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
ready(queued, inDevice, finishing) {
|
|
17
|
-
if (!this.
|
|
17
|
+
if (!this.threshold || finishing) return true;
|
|
18
18
|
if (queued < this.frameBytes && inDevice < this.frameBytes) this.buffering = true;
|
|
19
19
|
if (this.buffering && queued >= this.threshold) this.buffering = false;
|
|
20
20
|
return !this.buffering;
|
|
@@ -259,7 +259,7 @@ export class PulseClient {
|
|
|
259
259
|
* `onRequest(bytes)` asks for about that much more audio, paced to real time
|
|
260
260
|
* (see PlaybackStream).
|
|
261
261
|
*/
|
|
262
|
-
async openPlayback({ rate, channels, name = "audio", onRequest = () => {}, onTiming = null, onTimingError = null, leadSeconds = LEAD_SECONDS }) {
|
|
262
|
+
async openPlayback({ rate, channels, name = "audio", onRequest = () => {}, onTiming = null, onTimingError = null, leadSeconds = LEAD_SECONDS, adaptiveLead = false }) {
|
|
263
263
|
const spec = { format: SAMPLE_S16LE, channels, rate };
|
|
264
264
|
const frameBytes = channels * 2;
|
|
265
265
|
const bytesForSeconds = (seconds) => Math.max(frameBytes, Math.round(rate * seconds) * frameBytes);
|
|
@@ -267,7 +267,7 @@ export class PulseClient {
|
|
|
267
267
|
// stream that is deliberately kept only 120 ms ahead would not start until
|
|
268
268
|
// that buffer filled. Request an explicit, short start threshold instead.
|
|
269
269
|
const interactive = leadSeconds <= 0.15;
|
|
270
|
-
const targetLength = bytesForSeconds(interactive ? 0.16 : 0.4);
|
|
270
|
+
const targetLength = bytesForSeconds(adaptiveLead ? 0.55 : interactive ? 0.16 : 0.4);
|
|
271
271
|
const prebuffer = bytesForSeconds(interactive ? 0.04 : 0.08);
|
|
272
272
|
const minimumRequest = bytesForSeconds(0.04);
|
|
273
273
|
const reply = await this.#command(COMMAND.CREATE_PLAYBACK_STREAM, (w) => w
|
|
@@ -279,7 +279,7 @@ export class PulseClient {
|
|
|
279
279
|
const index = reply.u32();
|
|
280
280
|
reply.u32(); // sink input index
|
|
281
281
|
const requested = reply.u32();
|
|
282
|
-
const stream = new PlaybackStream(this, index, spec, onRequest, leadSeconds, onTiming, onTimingError);
|
|
282
|
+
const stream = new PlaybackStream(this, index, spec, onRequest, leadSeconds, onTiming, onTimingError, adaptiveLead);
|
|
283
283
|
this.#streams.set(index, stream);
|
|
284
284
|
stream.grant(requested);
|
|
285
285
|
return stream;
|
|
@@ -338,6 +338,9 @@ export class PulseClient {
|
|
|
338
338
|
* the output. That reserve is discarded on close, not sent to the device.
|
|
339
339
|
*/
|
|
340
340
|
const LEAD_SECONDS = 0.5;
|
|
341
|
+
// Live synthesis keeps a small PCM reserve independent of server credits.
|
|
342
|
+
// Unlike decoded media's two-second reserve, this has no startup threshold.
|
|
343
|
+
const LIVE_RESERVE_SECONDS = 0.32;
|
|
341
344
|
const PACE_MS = 50;
|
|
342
345
|
// Video follows this measured clock. Sharing the 50 ms PCM timer would cap
|
|
343
346
|
// presentation at 20 FPS even when decoding and output can keep up.
|
|
@@ -365,9 +368,11 @@ class PlaybackStream {
|
|
|
365
368
|
#timingPending = false;
|
|
366
369
|
#playedBytes = 0;
|
|
367
370
|
#producerBuffer;
|
|
371
|
+
#adaptiveLead;
|
|
372
|
+
#requestedAt = null;
|
|
368
373
|
killed = false;
|
|
369
374
|
|
|
370
|
-
constructor(client, index, { rate, channels }, onRequest, leadSeconds, onTiming, onTimingError) {
|
|
375
|
+
constructor(client, index, { rate, channels }, onRequest, leadSeconds, onTiming, onTimingError, adaptiveLead) {
|
|
371
376
|
this.#client = client;
|
|
372
377
|
this.index = index;
|
|
373
378
|
// Audio is only ever cut at whole frames (one sample of every channel):
|
|
@@ -377,7 +382,8 @@ class PlaybackStream {
|
|
|
377
382
|
this.#bytesPerSecond = rate * this.#frameBytes;
|
|
378
383
|
this.#onRequest = onRequest;
|
|
379
384
|
this.#leadSeconds = leadSeconds;
|
|
380
|
-
this.#
|
|
385
|
+
this.#adaptiveLead = adaptiveLead;
|
|
386
|
+
this.#producerBuffer = new PlaybackBuffer(this.#bytesPerSecond, this.#frameBytes, leadSeconds, adaptiveLead ? LIVE_RESERVE_SECONDS : 0);
|
|
381
387
|
this.#onTiming = onTiming;
|
|
382
388
|
this.#onTimingError = onTimingError;
|
|
383
389
|
this.#timer = setInterval(() => this.#pace(), PACE_MS);
|
|
@@ -414,7 +420,19 @@ class PlaybackStream {
|
|
|
414
420
|
/** Queues s16le audio from the producer; it goes out as the clock allows. */
|
|
415
421
|
write(bytes) {
|
|
416
422
|
if (this.killed || !bytes.length) return;
|
|
423
|
+
if (this.#adaptiveLead && this.#requestedAt !== null) {
|
|
424
|
+
// Live synthesis runs in the page renderer. Learn its response budget
|
|
425
|
+
// under expensive Canvas/JS work without pre-rendering seconds of future
|
|
426
|
+
// input. The separate controller can play this bounded device queue.
|
|
427
|
+
const responseSeconds=(performance.now()-this.#requestedAt)/1000;
|
|
428
|
+
if(responseSeconds>this.#leadSeconds-0.05)
|
|
429
|
+
this.#leadSeconds=Math.min(0.5,Math.max(this.#leadSeconds,responseSeconds+0.1));
|
|
430
|
+
// Later pieces intentionally yield for page scheduling; measure only
|
|
431
|
+
// the first response, not the time to complete the entire reserve.
|
|
432
|
+
this.#requestedAt=null;
|
|
433
|
+
}
|
|
417
434
|
this.#outstanding = Math.max(0, this.#outstanding - bytes.length);
|
|
435
|
+
if (!this.#outstanding) this.#requestedAt=null;
|
|
418
436
|
this.#queue.push(bytes);
|
|
419
437
|
this.#queuedBytes += bytes.length;
|
|
420
438
|
this.#pace();
|
|
@@ -472,6 +490,7 @@ class PlaybackStream {
|
|
|
472
490
|
const sentNow = inDevice + this.#offset - previousOffset;
|
|
473
491
|
const need = this.#producerBuffer.request(this.#queuedBytes, this.#outstanding, sentNow, immediateNeed);
|
|
474
492
|
if (need >= this.#bytesPerSecond * MIN_REQUEST_SECONDS && !this.finishing) {
|
|
493
|
+
this.#requestedAt ??= performance.now();
|
|
475
494
|
this.#outstanding += need;
|
|
476
495
|
this.#onRequest(need);
|
|
477
496
|
}
|