buninu-browser 0.6.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 +94 -4
- 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/index.js +10 -6
- 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 +128 -72
- 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 +79 -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/index.test.js +1 -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,570 @@
|
|
|
1
|
+
import {biquadCoefficients} from './biquad.js';
|
|
2
|
+
import {floatToS16} from './pcm.js';
|
|
3
|
+
|
|
4
|
+
// A software Web Audio graph. Each running context shares one existing PCM
|
|
5
|
+
// output stream, regardless of how many oscillators or gain nodes it owns.
|
|
6
|
+
const QUANTUM = 128;
|
|
7
|
+
// Immutable by convention: nodes must allocate before modifying silence.
|
|
8
|
+
const SILENCE = new Float32Array(QUANTUM);
|
|
9
|
+
// Flush inaudible float32 subnormal filter state so a silent IIR tail can
|
|
10
|
+
// become inactive instead of keeping its whole graph processing forever.
|
|
11
|
+
const MIN_NORMAL_FLOAT32 = 2 ** -126;
|
|
12
|
+
const TAU = 2 * Math.PI;
|
|
13
|
+
|
|
14
|
+
// Backward graph edges do not own inactive, unreferenced input nodes. Running
|
|
15
|
+
// sources own their forward paths, and the context owns filters with live
|
|
16
|
+
// tails until those tails decay. Author-held nodes retain their connections.
|
|
17
|
+
class InputConnections {
|
|
18
|
+
refs = new Set();
|
|
19
|
+
byNode = new WeakMap();
|
|
20
|
+
add(node) {
|
|
21
|
+
if(!this.byNode.has(node)){const ref=new WeakRef(node);this.byNode.set(node,ref);this.refs.add(ref);}
|
|
22
|
+
}
|
|
23
|
+
delete(node) {
|
|
24
|
+
const ref=this.byNode.get(node);this.byNode.delete(node);
|
|
25
|
+
return ref?this.refs.delete(ref):false;
|
|
26
|
+
}
|
|
27
|
+
*[Symbol.iterator]() {
|
|
28
|
+
for(const ref of this.refs){const node=ref.deref();if(node)yield node;else this.refs.delete(ref);}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const finite = value => {
|
|
32
|
+
const number = Number(value);
|
|
33
|
+
if (!Number.isFinite(number)) throw new TypeError('A finite number is required');
|
|
34
|
+
return number;
|
|
35
|
+
};
|
|
36
|
+
const timeValue = value => {
|
|
37
|
+
const time = finite(value);
|
|
38
|
+
if (time < 0) throw new RangeError('Time must not be negative');
|
|
39
|
+
return time;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function createWebAudioScope({audio = null, EventTarget = globalThis.EventTarget,
|
|
43
|
+
Event = globalThis.Event, DOMException = globalThis.DOMException,
|
|
44
|
+
decodeAudio = null, schedule = fn => setTimeout(fn, 0), reportError = () => {}, realtime = true} = {}) {
|
|
45
|
+
const contexts = new WeakMap(), nodes = new WeakMap(), params = new WeakMap(), buffers = new WeakMap();
|
|
46
|
+
const live = new Set(), internal = {};
|
|
47
|
+
let disposed = false;
|
|
48
|
+
const get = (map, object) => {
|
|
49
|
+
const state = map.get(object);
|
|
50
|
+
if (!state) throw new TypeError('Illegal invocation');
|
|
51
|
+
return state;
|
|
52
|
+
};
|
|
53
|
+
const contextOf = object => get(contexts, object);
|
|
54
|
+
const nodeOf = object => get(nodes, object);
|
|
55
|
+
const paramOf = object => get(params, object);
|
|
56
|
+
const invalid = message => new DOMException(message, 'InvalidStateError');
|
|
57
|
+
const clock = context => {const state=contextOf(context);return state.frame/state.rate;};
|
|
58
|
+
const notify = (target, type) => schedule(() => {
|
|
59
|
+
if (!disposed) target.dispatchEvent(new Event(type));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function parameterValue(state, time) {
|
|
63
|
+
// Rendering asks for monotonically increasing sample times. Retain the
|
|
64
|
+
// automation cursor so a long score does not rescan every previous note
|
|
65
|
+
// for every sample. Edits and backward queries restart that cursor.
|
|
66
|
+
let cursor=state.cursor;
|
|
67
|
+
if (!cursor || time<cursor.time) cursor={index:0,value:state.initial,anchor:0,target:null,time:-Infinity};
|
|
68
|
+
let {index,value,anchor,target}=cursor;
|
|
69
|
+
const at=end=>target ? target.value+(value-target.value)*Math.exp(-(end-anchor)/target.constant) : value;
|
|
70
|
+
for(;index<state.events.length;index++) {
|
|
71
|
+
const event=state.events[index];
|
|
72
|
+
if(time<event.time)break;
|
|
73
|
+
value=at(event.time);anchor=event.time;
|
|
74
|
+
target=event.kind==='target'&&event.constant>0?event:null;
|
|
75
|
+
if(!target)value=event.value;
|
|
76
|
+
}
|
|
77
|
+
Object.assign(cursor,{index,value,anchor,target,time});state.cursor=cursor;
|
|
78
|
+
const next=state.events[index];
|
|
79
|
+
if(next && next.time>anchor){
|
|
80
|
+
const fraction=Math.max(0,(time-anchor)/(next.time-anchor));
|
|
81
|
+
if(next.kind==='linear')return value+(next.value-value)*fraction;
|
|
82
|
+
if(next.kind==='exponential'&&value*next.value>0)return value*(next.value/value)**fraction;
|
|
83
|
+
}
|
|
84
|
+
return at(time);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const bufferOf = object => get(buffers, object);
|
|
88
|
+
class AudioBuffer {
|
|
89
|
+
constructor({numberOfChannels=1,length,sampleRate}={}) {
|
|
90
|
+
const channels=Number(numberOfChannels),rate=finite(sampleRate);length=Math.trunc(Number(length));
|
|
91
|
+
if(!Number.isInteger(channels)||channels<1||channels>32||!Number.isInteger(length)||length<1||channels*length>16777216||rate<8000||rate>192000)
|
|
92
|
+
throw new DOMException('Unsupported audio buffer size or sample rate','NotSupportedError');
|
|
93
|
+
buffers.set(this,{rate,length,data:Array.from({length:channels},()=>new Float32Array(length))});
|
|
94
|
+
}
|
|
95
|
+
get sampleRate(){return bufferOf(this).rate;}
|
|
96
|
+
get length(){return bufferOf(this).length;}
|
|
97
|
+
get duration(){const b=bufferOf(this);return b.length/b.rate;}
|
|
98
|
+
get numberOfChannels(){return bufferOf(this).data.length;}
|
|
99
|
+
getChannelData(channel){const b=bufferOf(this);channel=Number(channel)>>>0;if(channel>=b.data.length)throw new DOMException('Invalid audio channel','IndexSizeError');return b.data[channel];}
|
|
100
|
+
copyFromChannel(destination,channel,start=0){const data=this.getChannelData(channel);start=Number(start)>>>0;if(!(destination instanceof Float32Array))throw new TypeError('Float32Array required');destination.set(data.subarray(start,start+destination.length));}
|
|
101
|
+
copyToChannel(source,channel,start=0){const data=this.getChannelData(channel);start=Number(start)>>>0;if(!(source instanceof Float32Array))throw new TypeError('Float32Array required');if(start<data.length)data.set(source.subarray(0,data.length-start),start);}
|
|
102
|
+
}
|
|
103
|
+
async function decodeBuffer(input, rate) {
|
|
104
|
+
if(!decodeAudio)throw new DOMException('Audio decoding unavailable','EncodingError');
|
|
105
|
+
let decoder;
|
|
106
|
+
try{
|
|
107
|
+
decoder=await decodeAudio(input);
|
|
108
|
+
const channels=decoder.channels,sourceRate=decoder.sampleRate,chunks=[];let count=0;
|
|
109
|
+
if(!Number.isInteger(channels)||channels<1||channels>32||!(sourceRate>0))throw Error('Invalid audio stream');
|
|
110
|
+
for(let chunk;(chunk=decoder.nextSamples())!==null;){
|
|
111
|
+
if(!chunk.length)break;
|
|
112
|
+
count+=chunk.length;
|
|
113
|
+
if(count>16777216||count/channels/sourceRate*rate*channels>16777216)throw Error('Decoded audio exceeds buffer limit');
|
|
114
|
+
chunks.push(chunk.slice());
|
|
115
|
+
}
|
|
116
|
+
if(!count||count%channels)throw Error('No complete audio samples');
|
|
117
|
+
const samples=new Float32Array(count);let at=0;for(const chunk of chunks){samples.set(chunk,at);at+=chunk.length;}
|
|
118
|
+
const frames=count/channels,length=Math.max(1,Math.round(frames*rate/sourceRate));
|
|
119
|
+
const buffer=new AudioBuffer({numberOfChannels:channels,length,sampleRate:rate}),data=bufferOf(buffer).data;
|
|
120
|
+
for(let i=0;i<length;i++){
|
|
121
|
+
const pos=Math.min(frames-1,i*sourceRate/rate),a=Math.floor(pos),b=Math.min(frames-1,a+1),f=pos-a;
|
|
122
|
+
for(let c=0;c<channels;c++)data[c][i]=samples[a*channels+c]*(1-f)+samples[b*channels+c]*f;
|
|
123
|
+
}
|
|
124
|
+
return buffer;
|
|
125
|
+
}catch(error){throw new DOMException('Cannot decode audio: '+error.message,'EncodingError');}
|
|
126
|
+
finally{decoder?.close();}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
class AudioParam {
|
|
130
|
+
constructor(context, value, min, max, token) {
|
|
131
|
+
if (token !== internal) throw new TypeError('Illegal constructor');
|
|
132
|
+
params.set(this, {context, initial:value, defaultValue:value, min, max, events:[], inputs:new InputConnections(), automationRate:'a-rate'});
|
|
133
|
+
}
|
|
134
|
+
get value() {const state=paramOf(this);return parameterValue(state, clock(state.context));}
|
|
135
|
+
set value(value) {this.setValueAtTime(value, clock(paramOf(this).context));}
|
|
136
|
+
get defaultValue() {return paramOf(this).defaultValue;}
|
|
137
|
+
get minValue() {return paramOf(this).min;}
|
|
138
|
+
get maxValue() {return paramOf(this).max;}
|
|
139
|
+
get automationRate() {return paramOf(this).automationRate;}
|
|
140
|
+
set automationRate(value) {
|
|
141
|
+
if (!['a-rate','k-rate'].includes(value)) throw new TypeError('Invalid automation rate');
|
|
142
|
+
paramOf(this).automationRate=value;
|
|
143
|
+
}
|
|
144
|
+
setValueAtTime(value, time) {return addEvent(this, 'set', value, time);}
|
|
145
|
+
linearRampToValueAtTime(value, time) {return addEvent(this, 'linear', value, time);}
|
|
146
|
+
exponentialRampToValueAtTime(value, time) {
|
|
147
|
+
if (finite(value) === 0) throw new RangeError('An exponential ramp cannot target zero');
|
|
148
|
+
return addEvent(this, 'exponential', value, time);
|
|
149
|
+
}
|
|
150
|
+
setTargetAtTime(value, time, constant) {
|
|
151
|
+
constant=finite(constant);
|
|
152
|
+
if (constant < 0) throw new RangeError('Time constant must not be negative');
|
|
153
|
+
return addEvent(this, 'target', value, time, constant);
|
|
154
|
+
}
|
|
155
|
+
cancelScheduledValues(time) {
|
|
156
|
+
time=timeValue(time);
|
|
157
|
+
const state=paramOf(this);
|
|
158
|
+
state.events=state.events.filter(event=>event.time<time);
|
|
159
|
+
state.cursor=null;
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
cancelAndHoldAtTime(time) {
|
|
163
|
+
time=timeValue(time);
|
|
164
|
+
const state=paramOf(this), value=parameterValue(state,time);
|
|
165
|
+
const next=state.events.find(event=>event.time>=time);
|
|
166
|
+
this.cancelScheduledValues(time);
|
|
167
|
+
// Keep the part of a ramp before the cancellation point, then hold.
|
|
168
|
+
if (next && ['linear','exponential'].includes(next.kind)) addEvent(this,next.kind,value,time);
|
|
169
|
+
return this.setValueAtTime(value,time);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function addEvent(param, kind, value, time, constant) {
|
|
174
|
+
const state=paramOf(param);
|
|
175
|
+
value=Math.max(state.min,Math.min(state.max,finite(value)));
|
|
176
|
+
time=timeValue(time);
|
|
177
|
+
const event={kind,value,time,constant};
|
|
178
|
+
const existing=state.events.findIndex(item=>item.kind===kind&&item.time===time);
|
|
179
|
+
if (existing>=0) state.events.splice(existing,1);
|
|
180
|
+
state.events.push(event);
|
|
181
|
+
state.events.sort((a,b)=>a.time-b.time);state.cursor=null;
|
|
182
|
+
return param;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function disconnectOutputs(node) {
|
|
186
|
+
const state=nodeOf(node);
|
|
187
|
+
for(const target of state.outputs)(nodes.get(target)??params.get(target)).inputs.delete(node);
|
|
188
|
+
state.outputs.clear();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
class AudioNode extends EventTarget {
|
|
192
|
+
constructor(context, kind, token) {
|
|
193
|
+
super();
|
|
194
|
+
if (token !== internal) throw new TypeError('Illegal constructor');
|
|
195
|
+
contextOf(context);
|
|
196
|
+
nodes.set(this,{context,kind,inputs:new InputConnections(),outputs:new Set(),channelCount:2,channelCountMode:'max',channelInterpretation:'speakers',handlers:new Map()});
|
|
197
|
+
}
|
|
198
|
+
get context() {return nodeOf(this).context;}
|
|
199
|
+
get numberOfInputs() {return ['oscillator','buffer'].includes(nodeOf(this).kind)?0:1;}
|
|
200
|
+
get numberOfOutputs() {return nodeOf(this).kind==='destination'?0:1;}
|
|
201
|
+
get channelCount() {return nodeOf(this).channelCount;}
|
|
202
|
+
set channelCount(value) {
|
|
203
|
+
value=Number(value);
|
|
204
|
+
if (!Number.isInteger(value)||value<1||value>2) throw new DOMException('Only mono and stereo are supported','NotSupportedError');
|
|
205
|
+
const state=nodeOf(this);
|
|
206
|
+
if (state.kind==='destination' && value!==2) throw new DOMException('The output is stereo','NotSupportedError');
|
|
207
|
+
state.channelCount=value;
|
|
208
|
+
}
|
|
209
|
+
get channelCountMode() {return nodeOf(this).channelCountMode;}
|
|
210
|
+
set channelCountMode(value) {
|
|
211
|
+
if (!['max','clamped-max','explicit'].includes(value)) throw new TypeError('Invalid channel count mode');
|
|
212
|
+
nodeOf(this).channelCountMode=value;
|
|
213
|
+
}
|
|
214
|
+
get channelInterpretation() {return nodeOf(this).channelInterpretation;}
|
|
215
|
+
set channelInterpretation(value) {
|
|
216
|
+
if (!['speakers','discrete'].includes(value)) throw new TypeError('Invalid channel interpretation');
|
|
217
|
+
nodeOf(this).channelInterpretation=value;
|
|
218
|
+
}
|
|
219
|
+
connect(destination, output=0, input=0) {
|
|
220
|
+
const state=nodeOf(this), target=nodes.get(destination)??params.get(destination);
|
|
221
|
+
if (!target) throw new TypeError('An AudioNode or AudioParam is required');
|
|
222
|
+
if (target.context!==state.context) throw new DOMException('Nodes belong to different contexts','InvalidAccessError');
|
|
223
|
+
if (output!==0||this.numberOfOutputs===0||input!==0||nodes.has(destination)&&destination.numberOfInputs===0) throw new DOMException('Invalid port index','IndexSizeError');
|
|
224
|
+
state.outputs.add(destination);target.inputs.add(this);
|
|
225
|
+
return nodes.has(destination)?destination:undefined;
|
|
226
|
+
}
|
|
227
|
+
disconnect(destination, output=0, input=0) {
|
|
228
|
+
const state=nodeOf(this);
|
|
229
|
+
if (destination===undefined||typeof destination==='number') {
|
|
230
|
+
if (typeof destination==='number'&&destination!==0) throw new DOMException('Invalid port index','IndexSizeError');
|
|
231
|
+
disconnectOutputs(this);return;
|
|
232
|
+
}
|
|
233
|
+
if (output!==0||input!==0) throw new DOMException('Invalid port index','IndexSizeError');
|
|
234
|
+
if (!nodes.has(destination)&&!params.has(destination)) throw new TypeError('An AudioNode or AudioParam is required');
|
|
235
|
+
if (!state.outputs.delete(destination)) throw new DOMException('The connection does not exist','InvalidAccessError');
|
|
236
|
+
(nodes.get(destination)??params.get(destination)).inputs.delete(this);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
class AudioDestinationNode extends AudioNode {
|
|
241
|
+
constructor(context, token) {if(token!==internal)throw new TypeError('Illegal constructor');super(context,'destination',internal);}
|
|
242
|
+
get maxChannelCount() {nodeOf(this);return 2;}
|
|
243
|
+
}
|
|
244
|
+
class GainNode extends AudioNode {
|
|
245
|
+
constructor(context, options={}) {
|
|
246
|
+
super(context,'gain',internal);
|
|
247
|
+
options ??= {};
|
|
248
|
+
nodeOf(this).gain=new AudioParam(context,1,-3.4028234663852886e38,3.4028234663852886e38,internal);
|
|
249
|
+
if(options.gain!==undefined)this.gain.value=options.gain;
|
|
250
|
+
}
|
|
251
|
+
get gain() {return nodeOf(this).gain;}
|
|
252
|
+
}
|
|
253
|
+
class BiquadFilterNode extends AudioNode {
|
|
254
|
+
constructor(context,options={}) {
|
|
255
|
+
super(context,'biquad',internal);
|
|
256
|
+
const state=nodeOf(this);
|
|
257
|
+
Object.assign(state,{type:'lowpass',z1:0,z2:0,
|
|
258
|
+
frequency:new AudioParam(context,350,0,context.sampleRate/2,internal),
|
|
259
|
+
detune:new AudioParam(context,0,-153600,153600,internal),
|
|
260
|
+
Q:new AudioParam(context,1,-3.4028234663852886e38,3.4028234663852886e38,internal),
|
|
261
|
+
gain:new AudioParam(context,0,-3.4028234663852886e38,3.4028234663852886e38,internal)});
|
|
262
|
+
options??={};
|
|
263
|
+
if(options.type!==undefined)this.type=options.type;
|
|
264
|
+
for(const name of ['frequency','detune','Q','gain'])if(options[name]!==undefined)this[name].value=options[name];
|
|
265
|
+
}
|
|
266
|
+
get type(){return nodeOf(this).type;}
|
|
267
|
+
set type(value){if(!['lowpass','highpass','bandpass','notch','allpass','peaking','lowshelf','highshelf'].includes(value))throw new TypeError('Invalid filter type');nodeOf(this).type=value;}
|
|
268
|
+
get frequency(){return nodeOf(this).frequency;}
|
|
269
|
+
get detune(){return nodeOf(this).detune;}
|
|
270
|
+
get Q(){return nodeOf(this).Q;}
|
|
271
|
+
get gain(){return nodeOf(this).gain;}
|
|
272
|
+
getFrequencyResponse(frequencyHz,magResponse,phaseResponse){
|
|
273
|
+
if(![frequencyHz,magResponse,phaseResponse].every(a=>a instanceof Float32Array))throw new TypeError('Float32Array required');
|
|
274
|
+
if(frequencyHz.length!==magResponse.length||frequencyHz.length!==phaseResponse.length)throw new DOMException('Array lengths differ','InvalidAccessError');
|
|
275
|
+
const state=nodeOf(this),rate=this.context.sampleRate,time=this.context.currentTime;
|
|
276
|
+
const [frequency,detune,q,gain]=['frequency','detune','Q','gain'].map(name=>parameterValue(paramOf(state[name]),time));
|
|
277
|
+
const [b0,b1,b2,a1,a2]=biquadCoefficients(state.type,frequency*2**(detune/1200),q,gain,rate);
|
|
278
|
+
for(let i=0;i<frequencyHz.length;i++){
|
|
279
|
+
const hz=frequencyHz[i];
|
|
280
|
+
if(!(hz>=0&&hz<=rate/2)){magResponse[i]=phaseResponse[i]=NaN;continue;}
|
|
281
|
+
const w=2*Math.PI*hz/rate,c=Math.cos(w),s=Math.sin(w),c2=Math.cos(2*w),s2=Math.sin(2*w);
|
|
282
|
+
const nr=b0+b1*c+b2*c2,ni=-b1*s-b2*s2,dr=1+a1*c+a2*c2,di=-a1*s-a2*s2;
|
|
283
|
+
magResponse[i]=Math.hypot(nr,ni)/Math.hypot(dr,di);phaseResponse[i]=Math.atan2(ni*dr-nr*di,nr*dr+ni*di);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
class AudioScheduledSourceNode extends AudioNode {
|
|
288
|
+
constructor(context, token,kind='oscillator') {if(token!==internal)throw new TypeError('Illegal constructor');super(context,kind,internal);}
|
|
289
|
+
start(when=0) {
|
|
290
|
+
const state=nodeOf(this);
|
|
291
|
+
if (state.start!==null) throw invalid('The source has already been started');
|
|
292
|
+
state.start=Math.max(timeValue(when),clock(this.context));
|
|
293
|
+
contextOf(this.context).sources.add(this);
|
|
294
|
+
}
|
|
295
|
+
stop(when=0) {
|
|
296
|
+
const state=nodeOf(this);
|
|
297
|
+
if(state.start===null)throw invalid('The source has not been started');
|
|
298
|
+
state.stop=Math.max(timeValue(when),clock(this.context));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
class OscillatorNode extends AudioScheduledSourceNode {
|
|
302
|
+
constructor(context, options={}) {
|
|
303
|
+
super(context,internal);
|
|
304
|
+
options ??= {};
|
|
305
|
+
Object.assign(nodeOf(this),{start:null,stop:Infinity,ended:false,phase:0,type:'sine',
|
|
306
|
+
frequency:new AudioParam(context,440,-context.sampleRate/2,context.sampleRate/2,internal),
|
|
307
|
+
detune:new AudioParam(context,0,-153600,153600,internal)});
|
|
308
|
+
if(options.type!==undefined)this.type=options.type;
|
|
309
|
+
if(options.frequency!==undefined)this.frequency.value=options.frequency;
|
|
310
|
+
if(options.detune!==undefined)this.detune.value=options.detune;
|
|
311
|
+
}
|
|
312
|
+
get type() {return nodeOf(this).type;}
|
|
313
|
+
set type(value) {
|
|
314
|
+
if(value==='custom')throw invalid('A periodic wave has not been supplied');
|
|
315
|
+
if(!['sine','square','sawtooth','triangle'].includes(value))throw new TypeError('Invalid oscillator type');
|
|
316
|
+
nodeOf(this).type=value;
|
|
317
|
+
}
|
|
318
|
+
get frequency() {return nodeOf(this).frequency;}
|
|
319
|
+
get detune() {return nodeOf(this).detune;}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
class AudioBufferSourceNode extends AudioScheduledSourceNode {
|
|
323
|
+
constructor(context,options={}){
|
|
324
|
+
super(context,internal,'buffer');
|
|
325
|
+
Object.assign(nodeOf(this),{buffer:null,start:null,stop:Infinity,ended:false,position:0,offset:0,duration:Infinity,played:0,loop:false,loopStart:0,loopEnd:0,
|
|
326
|
+
playbackRate:new AudioParam(context,1,-3.4028234663852886e38,3.4028234663852886e38,internal),detune:new AudioParam(context,0,-153600,153600,internal)});
|
|
327
|
+
for(const key of ['buffer','loop','loopStart','loopEnd'])if(options[key]!==undefined)this[key]=options[key];
|
|
328
|
+
this.playbackRate.automationRate='k-rate';this.detune.automationRate='k-rate';
|
|
329
|
+
if(options.playbackRate!==undefined)this.playbackRate.value=options.playbackRate;
|
|
330
|
+
if(options.detune!==undefined)this.detune.value=options.detune;
|
|
331
|
+
}
|
|
332
|
+
get buffer(){return nodeOf(this).buffer;}
|
|
333
|
+
set buffer(value){if(value!==null)bufferOf(value);const n=nodeOf(this);if(n.buffer&&value)throw invalid('Buffer is already set');n.buffer=value;}
|
|
334
|
+
get playbackRate(){return nodeOf(this).playbackRate;}
|
|
335
|
+
get detune(){return nodeOf(this).detune;}
|
|
336
|
+
get loop(){return nodeOf(this).loop;}
|
|
337
|
+
set loop(value){nodeOf(this).loop=!!value;}
|
|
338
|
+
get loopStart(){return nodeOf(this).loopStart;}
|
|
339
|
+
set loopStart(value){nodeOf(this).loopStart=finite(value);}
|
|
340
|
+
get loopEnd(){return nodeOf(this).loopEnd;}
|
|
341
|
+
set loopEnd(value){nodeOf(this).loopEnd=finite(value);}
|
|
342
|
+
start(when=0,offset=0,duration=undefined){
|
|
343
|
+
offset=timeValue(offset);if(duration!==undefined)duration=timeValue(duration);
|
|
344
|
+
super.start(when);const n=nodeOf(this);n.offset=offset;n.duration=duration??Infinity;
|
|
345
|
+
n.position=n.buffer?Math.min(n.buffer.length,offset*n.buffer.sampleRate):0;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function stopOutput(state) {
|
|
350
|
+
const id=state.stream;
|
|
351
|
+
state.stream=null;
|
|
352
|
+
state.pending=null;
|
|
353
|
+
state.pendingAt=0;
|
|
354
|
+
clearTimeout(state.audioTimer);state.audioTimer=undefined;state.requested=0;
|
|
355
|
+
return id===null?Promise.resolve():Promise.resolve(audio?.close(id));
|
|
356
|
+
}
|
|
357
|
+
function startOutput(context) {
|
|
358
|
+
const state=contextOf(context);
|
|
359
|
+
if (!audio||state.stream!==null||state.state!=='running') return;
|
|
360
|
+
const id=audio.open({rate:state.rate,channels:2,name:'Web Audio'},event=>{
|
|
361
|
+
if(state.stream!==id||state.state!=='running')return;
|
|
362
|
+
if(event.event==='request') {
|
|
363
|
+
if (!realtime) { audio.write(id,render(context,event.bytes)); return; }
|
|
364
|
+
// The controller already paces playback and bounds its PCM reserve.
|
|
365
|
+
// Yield between synthesis batches so page timers can enqueue future
|
|
366
|
+
// notes; an additional wall-clock throttle here starves that reserve.
|
|
367
|
+
state.requested += Math.max(0, Math.floor(event.bytes / 4)) * 4;
|
|
368
|
+
pump();
|
|
369
|
+
}
|
|
370
|
+
else if(event.event==='error'||event.event==='ended'){
|
|
371
|
+
state.stream=null;state.state='suspended';
|
|
372
|
+
clearTimeout(state.audioTimer);state.audioTimer=undefined;state.requested=0;
|
|
373
|
+
if(event.event==='error')reportError(new Error(`Web Audio: ${event.message}`));
|
|
374
|
+
notify(context,'statechange');
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
state.stream=id;
|
|
378
|
+
state.requested=0;state.lastPump=performance.now();
|
|
379
|
+
function pump() {
|
|
380
|
+
if(state.stream!==id||state.state!=='running'||disposed)return;
|
|
381
|
+
if(state.audioTimer!==undefined)return;
|
|
382
|
+
const now=performance.now(),gap=now-state.lastPump;
|
|
383
|
+
state.lastPump=now;
|
|
384
|
+
// Replenish what elapsed during a slow paint rather than limiting every
|
|
385
|
+
// opportunity to 96 ms, which slows the audio clock on >100 ms frames.
|
|
386
|
+
// Normal batches stay small for script lookahead; catch-up remains
|
|
387
|
+
// bounded by 250 ms and the controller's outstanding demand.
|
|
388
|
+
const seconds=gap>=64?Math.min(.25,Math.max(.096,gap/1000+.016)):.064;
|
|
389
|
+
const frames=Math.min(Math.floor(state.requested/4),Math.floor(state.rate*seconds));
|
|
390
|
+
if(frames>0){
|
|
391
|
+
state.requested-=frames*4;
|
|
392
|
+
audio.write(id,render(context,frames*4));
|
|
393
|
+
}
|
|
394
|
+
if(state.requested>0){
|
|
395
|
+
// A late synthesis task has already yielded to page work. Do not add
|
|
396
|
+
// another 25 ms delay while trying to replenish the device queue.
|
|
397
|
+
state.audioTimer=setTimeout(()=>{state.audioTimer=undefined;pump();},gap>=64?0:25);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
class BaseAudioContext extends EventTarget {
|
|
402
|
+
constructor(token) {super();if(token!==internal)throw new TypeError('Illegal constructor');}
|
|
403
|
+
get destination() {return contextOf(this).destination;}
|
|
404
|
+
get sampleRate() {return contextOf(this).rate;}
|
|
405
|
+
get currentTime() {return clock(this);}
|
|
406
|
+
get state() {return contextOf(this).state;}
|
|
407
|
+
createOscillator() {contextOf(this);return new OscillatorNode(this);}
|
|
408
|
+
createGain() {contextOf(this);return new GainNode(this);}
|
|
409
|
+
createBiquadFilter() {contextOf(this);return new BiquadFilterNode(this);}
|
|
410
|
+
createBuffer(channels,length,sampleRate){contextOf(this);return new AudioBuffer({numberOfChannels:channels,length,sampleRate});}
|
|
411
|
+
createBufferSource(){contextOf(this);return new AudioBufferSourceNode(this);}
|
|
412
|
+
decodeAudioData(input,success,error){
|
|
413
|
+
const rate=contextOf(this).rate;
|
|
414
|
+
let bytes;
|
|
415
|
+
try{
|
|
416
|
+
if(!(input instanceof ArrayBuffer))throw new TypeError('ArrayBuffer required');
|
|
417
|
+
if(input.byteLength>64*1024*1024)throw new DOMException('Audio input exceeds limit','EncodingError');
|
|
418
|
+
bytes=new Uint8Array(structuredClone(input,{transfer:[input]}));
|
|
419
|
+
}catch(error){return Promise.reject(error);}
|
|
420
|
+
const promise=Promise.resolve().then(()=>{if(disposed)throw new DOMException('Document closed','AbortError');return decodeBuffer(bytes,rate);});
|
|
421
|
+
return promise.then(value=>{if(disposed)throw new DOMException('Document closed','AbortError');if(typeof success==='function')try{success(value);}catch(e){reportError(e);}return value;},reason=>{if(!disposed&&typeof error==='function')try{error(reason);}catch(e){reportError(e);}throw reason;});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
class AudioContext extends BaseAudioContext {
|
|
425
|
+
constructor(options={}) {
|
|
426
|
+
super(internal);
|
|
427
|
+
if(disposed)throw invalid('The document is no longer active');
|
|
428
|
+
const rate=finite(options?.sampleRate??48000);
|
|
429
|
+
if(!Number.isInteger(rate)||rate<8000||rate>192000)throw new DOMException('Unsupported sample rate','NotSupportedError');
|
|
430
|
+
const state={rate,frame:0,state:'running',destination:null,sources:new Set(),tails:new Set(),stream:null,pending:null,pendingAt:0,handlers:new Map()};
|
|
431
|
+
contexts.set(this,state);live.add(this);
|
|
432
|
+
state.destination=new AudioDestinationNode(this,internal);
|
|
433
|
+
startOutput(this);
|
|
434
|
+
}
|
|
435
|
+
get baseLatency() {contextOf(this);return .12;}
|
|
436
|
+
get outputLatency() {contextOf(this);return 0;}
|
|
437
|
+
suspend() {
|
|
438
|
+
const state=contextOf(this);
|
|
439
|
+
if(state.state==='closed')return Promise.reject(invalid('The context is closed'));
|
|
440
|
+
if(state.state==='suspended')return Promise.resolve();
|
|
441
|
+
state.state='suspended';notify(this,'statechange');
|
|
442
|
+
return stopOutput(state).then(()=>undefined);
|
|
443
|
+
}
|
|
444
|
+
resume() {
|
|
445
|
+
const state=contextOf(this);
|
|
446
|
+
if(state.state==='closed')return Promise.reject(invalid('The context is closed'));
|
|
447
|
+
if(state.state==='running')return Promise.resolve();
|
|
448
|
+
state.state='running';startOutput(this);notify(this,'statechange');return Promise.resolve();
|
|
449
|
+
}
|
|
450
|
+
close() {
|
|
451
|
+
const state=contextOf(this);
|
|
452
|
+
if(state.state==='closed')return Promise.reject(invalid('The context is closed'));
|
|
453
|
+
state.state='closed';state.sources.clear();state.tails.clear();live.delete(this);notify(this,'statechange');
|
|
454
|
+
return stopOutput(state).then(()=>undefined);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function renderQuantum(context) {
|
|
459
|
+
const state=contextOf(context), cache=new Map(), visiting=new Set(), time=state.frame/state.rate;
|
|
460
|
+
const sumInputs=inputs=>{
|
|
461
|
+
let output=SILENCE;
|
|
462
|
+
for(const input of inputs){
|
|
463
|
+
const samples=pull(input);if(samples===SILENCE)continue;
|
|
464
|
+
if(output===SILENCE)output=new Float32Array(QUANTUM);
|
|
465
|
+
for(let i=0;i<QUANTUM;i++)output[i]+=samples[i];
|
|
466
|
+
}
|
|
467
|
+
return output;
|
|
468
|
+
};
|
|
469
|
+
const paramValues=param=>{
|
|
470
|
+
const p=paramOf(param),input=sumInputs(p.inputs),values=input===SILENCE?new Float32Array(QUANTUM):input;
|
|
471
|
+
const last=p.events.at(-1);
|
|
472
|
+
if(!last||last.time<=time&&last.kind!=='target'){
|
|
473
|
+
const value=last?last.value:p.initial;
|
|
474
|
+
if(input===SILENCE)values.fill(value);else for(let i=0;i<QUANTUM;i++)values[i]+=value;
|
|
475
|
+
return values;
|
|
476
|
+
}
|
|
477
|
+
for(let i=0;i<QUANTUM;i++)values[i]+=parameterValue(p,time+(p.automationRate==='k-rate'?0:i/state.rate));
|
|
478
|
+
return values;
|
|
479
|
+
};
|
|
480
|
+
const pull=node=>{
|
|
481
|
+
if(cache.has(node))return cache.get(node);
|
|
482
|
+
// A cycle without a delay node contributes silence, never recursion.
|
|
483
|
+
if(visiting.has(node))return new Float32Array(QUANTUM);
|
|
484
|
+
visiting.add(node);
|
|
485
|
+
const n=nodeOf(node);
|
|
486
|
+
let output;
|
|
487
|
+
if(n.kind==='oscillator'){
|
|
488
|
+
output=new Float32Array(QUANTUM);
|
|
489
|
+
if(n.start!==null&&!n.ended){
|
|
490
|
+
const frequency=paramValues(n.frequency),detune=paramValues(n.detune);
|
|
491
|
+
for(let i=0;i<QUANTUM;i++){
|
|
492
|
+
const t=time+i/state.rate;
|
|
493
|
+
if(t<n.start||t>=n.stop)continue;
|
|
494
|
+
const phase=n.phase;
|
|
495
|
+
output[i]=n.type==='sine'?Math.sin(phase):n.type==='triangle'?2/Math.PI*Math.asin(Math.sin(phase)):
|
|
496
|
+
n.type==='square'?(phase<Math.PI?1:-1):2*((phase/TAU+.5)%1)-1;
|
|
497
|
+
const hz=Math.max(-state.rate/2,Math.min(state.rate/2,frequency[i]*2**(detune[i]/1200)));
|
|
498
|
+
n.phase=((phase+TAU*hz/state.rate)%TAU+TAU)%TAU;
|
|
499
|
+
}
|
|
500
|
+
if(time+QUANTUM/state.rate>=n.stop){n.ended=true;state.sources.delete(node);disconnectOutputs(node);notify(node,'ended');}
|
|
501
|
+
}
|
|
502
|
+
}else if(n.kind==='buffer'){
|
|
503
|
+
output=new Float32Array(QUANTUM);
|
|
504
|
+
if(n.start!==null&&!n.ended){
|
|
505
|
+
const rate=paramValues(n.playbackRate),detune=paramValues(n.detune),b=n.buffer?bufferOf(n.buffer):null;
|
|
506
|
+
const end=()=>{n.ended=true;state.sources.delete(node);disconnectOutputs(node);notify(node,'ended');};
|
|
507
|
+
for(let i=0;i<QUANTUM;i++){
|
|
508
|
+
const t=time+i/state.rate;if(t<n.start)continue;
|
|
509
|
+
if(t>=n.stop||!b||n.played>=n.duration||!n.loop&&n.position>=b.length){end();break;}
|
|
510
|
+
const loopStart=Math.max(0,Math.min(b.length,n.loopStart*b.rate)),loopEnd=n.loopEnd>n.loopStart?Math.min(b.length,n.loopEnd*b.rate):b.length;
|
|
511
|
+
if(n.loop&&loopEnd>loopStart&&n.position>=loopEnd)n.position=loopStart+(n.position-loopStart)%(loopEnd-loopStart);
|
|
512
|
+
const pos=Math.max(0,Math.min(b.length-1,n.position)),a=Math.floor(pos),next=n.loop&&a+1>=loopEnd?Math.floor(loopStart):Math.min(b.length-1,a+1),f=pos-a;
|
|
513
|
+
let value=0;for(const data of b.data)value+=data[a]*(1-f)+data[next]*f;
|
|
514
|
+
output[i]=value/b.data.length;
|
|
515
|
+
const step=Math.max(0,rate[i]*2**(detune[i]/1200))*b.rate/state.rate;
|
|
516
|
+
n.position+=step;n.played+=step/b.rate;
|
|
517
|
+
}
|
|
518
|
+
if(!n.ended&&(time+QUANTUM/state.rate>=n.stop||n.played>=n.duration||b&&!n.loop&&n.position>=b.length))end();
|
|
519
|
+
}
|
|
520
|
+
}else{
|
|
521
|
+
output=sumInputs(n.inputs);
|
|
522
|
+
if(n.kind==='gain'&&output!==SILENCE){const gain=paramValues(n.gain);for(let i=0;i<QUANTUM;i++)output[i]*=gain[i];}
|
|
523
|
+
if(n.kind==='biquad' && !(output===SILENCE&&n.z1===0&&n.z2===0)){
|
|
524
|
+
if(output===SILENCE)output=new Float32Array(QUANTUM);
|
|
525
|
+
const frequency=paramValues(n.frequency),detune=paramValues(n.detune),q=paramValues(n.Q),gain=paramValues(n.gain);
|
|
526
|
+
for(let i=0;i<QUANTUM;i++){
|
|
527
|
+
const hz=frequency[i]*2**(detune[i]/1200);
|
|
528
|
+
if(!n.coefficients||n.lastType!==n.type||n.lastHz!==hz||n.lastQ!==q[i]||n.lastGain!==gain[i]){
|
|
529
|
+
n.coefficients=biquadCoefficients(n.type,hz,q[i],gain[i],state.rate);
|
|
530
|
+
n.lastType=n.type;n.lastHz=hz;n.lastQ=q[i];n.lastGain=gain[i];
|
|
531
|
+
}
|
|
532
|
+
const [b0,b1,b2,a1,a2]=n.coefficients,x=output[i],y=b0*x+n.z1;
|
|
533
|
+
n.z1=b1*x-a1*y+n.z2;n.z2=b2*x-a2*y;
|
|
534
|
+
if(Number.isFinite(y)&&Number.isFinite(n.z1)&&Number.isFinite(n.z2))output[i]=y;
|
|
535
|
+
else {output[i]=0;n.z1=n.z2=0;}
|
|
536
|
+
}
|
|
537
|
+
if(Math.abs(n.z1)<MIN_NORMAL_FLOAT32)n.z1=0;
|
|
538
|
+
if(Math.abs(n.z2)<MIN_NORMAL_FLOAT32)n.z2=0;
|
|
539
|
+
if(n.z1!==0||n.z2!==0)state.tails.add(node);
|
|
540
|
+
else state.tails.delete(node);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
visiting.delete(node);cache.set(node,output);return output;
|
|
544
|
+
};
|
|
545
|
+
const output=pull(state.destination);
|
|
546
|
+
// Disconnected started oscillators continue their phase for reconnect.
|
|
547
|
+
for(const source of state.sources)pull(source);
|
|
548
|
+
for(const filter of state.tails)pull(filter);
|
|
549
|
+
state.frame+=QUANTUM;
|
|
550
|
+
return output;
|
|
551
|
+
}
|
|
552
|
+
function render(context, bytes) {
|
|
553
|
+
const state=contextOf(context),frames=Math.max(0,Math.floor(bytes/4)),samples=new Float32Array(frames*2);
|
|
554
|
+
for(let i=0;i<frames;i++){
|
|
555
|
+
if(!state.pending||state.pendingAt>=state.pending.length){state.pending=renderQuantum(context);state.pendingAt=0;}
|
|
556
|
+
const value=state.pending[state.pendingAt++];samples[i*2]=value;samples[i*2+1]=value;
|
|
557
|
+
}
|
|
558
|
+
return floatToS16(samples);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
for(const [Constructor,name,stateOf]of [[BaseAudioContext,'statechange',contextOf],[AudioScheduledSourceNode,'ended',nodeOf]]){
|
|
562
|
+
Object.defineProperty(Constructor.prototype,`on${name}`,{configurable:true,enumerable:true,
|
|
563
|
+
get(){return stateOf(this).handlers.get(name)??null;},
|
|
564
|
+
set(handler){const handlers=stateOf(this).handlers,previous=handlers.get(name);if(previous)this.removeEventListener(name,previous);handlers.delete(name);if(typeof handler==='function'){handlers.set(name,handler);this.addEventListener(name,handler);}},
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
const api={AudioContext,webkitAudioContext:AudioContext,BaseAudioContext,AudioNode,AudioDestinationNode,AudioScheduledSourceNode,OscillatorNode,GainNode,BiquadFilterNode,AudioParam,AudioBuffer,AudioBufferSourceNode};
|
|
568
|
+
for(const Constructor of new Set(Object.values(api)))Object.defineProperty(Constructor.prototype,Symbol.toStringTag,{value:Constructor.name,configurable:true});
|
|
569
|
+
return {api,dispose(){disposed=true;for(const context of live){const state=contextOf(context);state.state='closed';state.sources.clear();state.tails.clear();void stopOutput(state).catch(reportError);}live.clear();}};
|
|
570
|
+
}
|