@@ -25,6 +25,7 @@ class AudioContext extends BaseAudioContext {
2525 #bitDepth
2626 #encoder
2727 #speaker = null // audio-speaker write function (default output)
28+ #stream = null // writable stream sink (when sinkId is a stream)
2829 #loopDeferred = false
2930 #sinkId = ''
3031 #playbackStats = new PlaybackStats ( )
@@ -70,9 +71,10 @@ class AudioContext extends BaseAudioContext {
7071 }
7172
7273 // Validate sinkId option
73- if ( opts . sinkId !== undefined ) {
74- if ( typeof opts . sinkId === 'object' && opts . sinkId !== null ) {
75- if ( opts . sinkId . type !== 'none' )
74+ let sinkId = opts . sinkId
75+ if ( sinkId !== undefined ) {
76+ if ( typeof sinkId === 'object' && sinkId !== null && typeof sinkId . write !== 'function' ) {
77+ if ( sinkId . type !== 'none' )
7678 throw new TypeError ( "Failed to construct 'AudioContext': Invalid AudioSinkOptions.type value." )
7779 }
7880 }
@@ -86,38 +88,39 @@ class AudioContext extends BaseAudioContext {
8688 this . #bitDepth = opts . bitDepth || 16
8789
8890 // Handle sinkId from constructor options
89- if ( opts . sinkId !== undefined ) {
90- if ( typeof opts . sinkId === 'object' && opts . sinkId !== null ) {
91- this . #sinkId = { type : opts . sinkId . type }
92- } else if ( typeof opts . sinkId === 'string' ) {
93- // Validate against known devices if a registry exists
91+ if ( sinkId !== undefined ) {
92+ if ( typeof sinkId === 'object' && sinkId !== null ) {
93+ if ( typeof sinkId . write === 'function' ) {
94+ this . #stream = sinkId
95+ } else {
96+ this . #sinkId = { type : sinkId . type }
97+ }
98+ } else if ( typeof sinkId === 'string' ) {
9499 let known = this . constructor . _knownDeviceIds || AudioContext . _knownDeviceIds
95- if ( opts . sinkId !== '' && known && ! known . has ( opts . sinkId ) ) {
96- // Invalid device ID: dispatch onerror asynchronously per spec
100+ if ( sinkId !== '' && known && ! known . has ( sinkId ) ) {
97101 setTimeout ( ( ) => this . dispatchEvent ( new Event ( 'error' ) ) , 0 )
98102 } else {
99- this . #sinkId = opts . sinkId
103+ this . #sinkId = sinkId
100104 }
101105 }
102106 }
103107
104- this . format = {
108+ let format = {
105109 numberOfChannels : this . #numberOfChannels,
106110 bitDepth : this . #bitDepth,
107111 sampleRate : this . sampleRate
108112 }
109- if ( opts . bufferSize ) this . format . bufferSize = opts . bufferSize
110- if ( opts . numBuffers ) this . format . numBuffers = opts . numBuffers
113+ if ( opts . bufferSize ) format . bufferSize = opts . bufferSize
114+ if ( opts . numBuffers ) format . numBuffers = opts . numBuffers
111115
112- this . #encoder = BufferEncoder ( this . format )
113- this . outStream = null
116+ this . #encoder = BufferEncoder ( format )
114117
115118 // Start render loop when a connection is established.
116119 // Deferred via queueMicrotask so user can finish setting up the graph
117120 // (connect + start) before rendering begins.
118121 this . _destination . _inputs [ 0 ] . on ( 'connection' , ( ) => {
119122 if ( this . #loopRunning || this . #loopDeferred || this . _state !== 'running' ) return
120- if ( ! this . outStream && ! this . #speaker) return
123+ if ( ! this . #stream && ! this . #speaker) return
121124 this . #loopDeferred = true
122125 queueMicrotask ( ( ) => {
123126 this . #loopDeferred = false
@@ -156,6 +159,14 @@ class AudioContext extends BaseAudioContext {
156159 if ( this . _state === 'closed' )
157160 return Promise . reject ( DOMErr ( 'Cannot setSinkId on a closed AudioContext' , 'InvalidStateError' ) )
158161 if ( typeof sinkId === 'object' && sinkId !== null ) {
162+ if ( typeof sinkId . write === 'function' ) {
163+ // Writable stream as sink
164+ if ( this . #speaker) { this . #speaker. close ( ) ; this . #speaker = null }
165+ let prev = this . #stream
166+ this . #stream = sinkId
167+ if ( prev !== sinkId ) this . dispatchEvent ( new Event ( 'sinkchange' ) )
168+ return Promise . resolve ( )
169+ }
159170 if ( sinkId . type !== 'none' )
160171 return Promise . reject ( new TypeError ( 'Invalid AudioSinkOptions.type value.' ) )
161172 let prev = this . #sinkId
@@ -195,15 +206,16 @@ class AudioContext extends BaseAudioContext {
195206 if ( this . _state === 'closed' ) return Promise . reject ( DOMErr ( 'Cannot resume a closed AudioContext' , 'InvalidStateError' ) )
196207 // Create speaker eagerly on resume — starts device with silence so there's
197208 // no hardware pop when audio actually begins (same as browsers).
198- if ( ! this . outStream && ! this . #speaker) {
209+ let isNone = typeof this . #sinkId === 'object' && this . #sinkId?. type === 'none'
210+ if ( ! this . #stream && ! this . #speaker && ! isNone ) {
199211 this . #speaker = await Speaker ( {
200212 sampleRate : this . sampleRate ,
201213 channels : this . #numberOfChannels,
202214 bitDepth : this . #bitDepth
203215 } )
204216 }
205217 this . _setState ( 'running' )
206- if ( ! this . #loopRunning && ( this . #speaker || this . outStream ) && this . _destination . _inputs [ 0 ] . sources . length ) {
218+ if ( ! this . #loopRunning && ( this . #speaker || this . #stream ) && this . _destination . _inputs [ 0 ] . sources . length ) {
207219 this . #loopRunning = true
208220 this . _renderLoop ( )
209221 }
@@ -219,7 +231,7 @@ class AudioContext extends BaseAudioContext {
219231
220232 _closeOutput ( ) {
221233 if ( this . #speaker) { this . #speaker. close ( ) ; this . #speaker = null }
222- if ( this . outStream ) ( this . outStream . close ?? this . outStream . end ) ?. call ( this . outStream )
234+ if ( this . #stream ) { ( this . #stream . close ?? this . #stream . end ) ?. call ( this . #stream ) ; this . #stream = null }
223235 }
224236
225237 _renderLoop ( ) {
@@ -230,20 +242,17 @@ class AudioContext extends BaseAudioContext {
230242 try {
231243 let buf = this . _renderQuantum ( )
232244
245+ let nch = buf . numberOfChannels
246+ let channels = [ ]
247+ for ( let c = 0 ; c < nch ; c ++ ) channels . push ( buf . getChannelData ( c ) )
248+ let encoded = this . #encoder( channels )
249+
233250 if ( this . #speaker) {
234- let nch = buf . numberOfChannels
235- let channels = [ ]
236- for ( let c = 0 ; c < nch ; c ++ ) channels . push ( buf . getChannelData ( c ) )
237- let encoded = this . #encoder( channels )
238251 this . #speaker( encoded , ( ) => this . _renderLoop ( ) )
239- } else if ( this . outStream ) {
240- let nch = buf . numberOfChannels
241- let channels = [ ]
242- for ( let c = 0 ; c < nch ; c ++ ) channels . push ( buf . getChannelData ( c ) )
243- let encoded = this . #encoder( channels )
244- let ok = this . outStream . write ( encoded )
245- if ( ok || ! this . outStream . once ) setTimeout ( ( ) => this . _renderLoop ( ) , 0 )
246- else this . outStream . once ( 'drain' , ( ) => this . _renderLoop ( ) )
252+ } else if ( this . #stream) {
253+ let ok = this . #stream. write ( encoded )
254+ if ( ok || ! this . #stream. once ) setTimeout ( ( ) => this . _renderLoop ( ) , 0 )
255+ else this . #stream. once ( 'drain' , ( ) => this . _renderLoop ( ) )
247256 }
248257 } catch ( e ) {
249258 console . error ( 'AudioContext render error:' , e )
0 commit comments