const workerVer = "p" ; const awpPath = "awp/ennuicastr-awp.js?v=" + workerVer ; export const workerPath = "awp/ennuicastr-worker.js?v=" + workerVer ; export interface Capture { source : AudioNode ; worker : Worker ; node : AudioNode ; destination : MediaStream ; disconnect : ( ) => unknown ; } export interface CaptureOptions { workerCommand : any ; ms ? : MediaStream ; input ? : AudioNode ; sampleRate : string ; matchSampleRate ? : boolean ; bufferSize ? : number ; outStream ? : boolean ; } function isChrome ( ) { return navigator . userAgent . indexOf ( "Chrome" ) >= 0 ; } export function isSafari ( ) { return navigator . userAgent . indexOf ( "Safari" ) >= 0 && ! isChrome ( ) ; } export function createCapture ( ac , options ) { if ( typeof AudioWorkletNode !== "undefined" && ! isSafari ( ) ) { return createCaptureAWP ( ac , options ) ; } else { console . log ( "[INFO] Not using AWP" ) ; return createCaptureSP ( ac , options ) ; } } function createCaptureAWP ( ac , options ) { if ( options . matchSampleRate ) { const msSampleRate = options . ms . getAudioTracks ( ) [ 0 ] . getSettings ( ) . sampleRate ; if ( msSampleRate !== ac . sampleRate ) ac = new AudioContext ( { latencyHint : "playback" , sampleRate : msSampleRate } ) ; } return Promise . all ( [ ] ) . then ( ( ) => { if ( ! ac . ecAWPP ) ac . ecAWPP = ac . audioWorklet . addModule ( awpPath ) ; return ac . ecAWPP ; } ) . then ( ( ) => { let dead = false ; const awn = new AudioWorkletNode ( ac , "worker-processor" ) ; const worker = new Worker ( workerPath ) ; const mc = new MessageChannel ( ) ; awn . port . postMessage ( { c : "workerPort" , p : mc . port1 } , [ mc . port1 ] ) ; const cmd = Object . assign ( { port : mc . port2 } , options . workerCommand ) ; cmd [ options . sampleRate ] = ac . sampleRate ; worker . postMessage ( cmd , [ mc . port2 ] ) ; let source = null ; if ( options . ms ) source = ac . createMediaStreamSource ( options . ms ) ; else if ( options . input ) source = options . input ; if ( source ) source . connect ( awn ) ; let msd = null ; if ( options . outStream ) { msd = ac . createMediaStreamDestination ( ) ; awn . connect ( msd ) ; } function disconnect ( ) { if ( dead ) return ; dead = true ; if ( source ) source . disconnect ( awn ) ; if ( msd ) awn . disconnect ( msd ) ; worker . terminate ( ) ; } return { source : source , worker : worker , node : awn , destination : msd ? msd . stream : null , disconnect : disconnect } ; } ) ; } function createCaptureSP ( ac , options ) { if ( isSafari ( ) && options . ms ) return createCaptureSafari ( ac , options ) ; if ( options . matchSampleRate ) { const msSampleRate = options . ms . getAudioTracks ( ) [ 0 ] . getSettings ( ) . sampleRate ; if ( msSampleRate !== ac . sampleRate ) ac = new AudioContext ( { latencyHint : "playback" , sampleRate : msSampleRate } ) ; } let dead = false ; const node = ac . createScriptProcessor ( options . bufferSize || 4096 ) ; const worker = new Worker ( workerPath ) ; const mc = new MessageChannel ( ) ; const workerPort = mc . port1 ; const cmd = Object . assign ( { port : mc . port2 } , options . workerCommand ) ; cmd [ options . sampleRate ] = ac . sampleRate ; worker . postMessage ( cmd , [ mc . port2 ] ) ; node . onaudioprocess = createOnAudioProcess ( workerPort ) ; let source = null ; if ( options . ms ) source = ac . createMediaStreamSource ( options . ms ) ; else if ( options . input ) source = options . input ; if ( source ) source . connect ( node ) ; let msd = null ; if ( options . outStream ) { msd = ac . createMediaStreamDestination ( ) ; node . connect ( msd ) ; } function disconnect ( ) { if ( dead ) return ; dead = true ; source . disconnect ( node ) ; if ( msd ) node . disconnect ( msd ) ; worker . terminate ( ) ; } return Promise . resolve ( { source : source , worker : worker , node : node , destination : msd ? msd . stream : null , disconnect : disconnect } ) ; } function createCaptureSafari ( ac , options ) { if ( ! ac . ecSP ) ac . ecSP = { } ; let sp = ac . ecSP [ options . ms . id ] ; if ( ! sp ) { let name = "createScriptProcessor" ; if ( ! ( < any > ac ) [ name ] ) name = "createJavaScriptNode" ; sp = ac . ecSP [ options . ms . id ] = ( < any > ac ) [ name ] ( 4096 , 1 , 1 ) ; sp . ecUsers = [ ] ; sp . ecCt = 0 ; sp . onaudioprocess = function ( ev ) { sp . ecUsers . forEach ( function ( user ) { user . onaudioprocess ( ev ) ; } ) ; } ; const mss = ac . createMediaStreamSource ( options . ms ) ; mss . connect ( sp ) ; sp . ecSource = mss ; const msd = ac . createMediaStreamDestination ( ) ; sp . connect ( msd ) ; sp . ecDestination = msd ; sp . ecDisconnect = function ( ) { mss . disconnect ( sp ) ; sp . disconnect ( msd ) ; delete ac . ecSP [ options . ms . id ] ; } ; } let dead = false ; const node = { onaudioprocess : function ( ev ) { } } ; sp . ecUsers . push ( node ) ; sp . ecCt ++ ; const worker = new Worker ( workerPath ) ; const mc = new MessageChannel ( ) ; const workerPort = mc . port1 ; const cmd = Object . assign ( { port : mc . port2 } , options . workerCommand ) ; cmd [ options . sampleRate ] = ac . sampleRate ; worker . postMessage ( cmd , [ mc . port2 ] ) ; node . onaudioprocess = createOnAudioProcess ( workerPort ) ; function disconnect ( ) { if ( dead ) return ; dead = true ; for ( let i = 0 ; i < sp . ecUsers . length ; i ++ ) { if ( sp . ecUsers [ i ] === node ) { sp . ecUsers . splice ( i , 1 ) ; sp . ecCt -- ; break ; } } if ( sp . ecCt === 0 ) sp . ecDisconnect ( ) ; worker . terminate ( ) ; } return Promise . resolve ( { source : sp . ecSource , worker : worker , node : null , destination : options . outStream ? sp . ecDestination . stream : null , disconnect : disconnect } ) ; } function createOnAudioProcess ( workerPort ) { const buffer = [ ] ; let lenPerBuf = 0 ; workerPort . onmessage = function ( ev ) { const buf = ev . data . d ; const len = buf [ 0 ] . length ; if ( len > lenPerBuf ) lenPerBuf = len ; buffer . push ( buf ) ; } ; return function ( ev ) { const input = [ ] ; const cc = ev . inputBuffer . numberOfChannels ; for ( let i = 0 ; i < cc ; i ++ ) input . push ( ev . inputBuffer . getChannelData ( i ) ) ; workerPort . postMessage ( Date . now ( ) ) ; workerPort . postMessage ( input ) ; while ( buffer . length >= 3 && ( buffer . length * lenPerBuf >= 4800 ) ) buffer . shift ( ) ; const out = [ ] ; for ( let i = 0 ; i < cc ; i ++ ) out . push ( ev . outputBuffer . getChannelData ( i ) ) ; const len = out [ 0 ] . length ; let i = 0 ; while ( i < len && buffer . length ) { const remain = len - i ; const first = buffer [ 0 ] ; if ( first [ 0 ] . length > remain ) { for ( let c = 0 ; c < out . length ; c ++ ) out [ c ] . set ( first [ c % first . length ] . subarray ( 0 , remain ) , i ) ; for ( let c = 0 ; c < first . length ; c ++ ) first [ c ] = first [ c ] . subarray ( remain ) ; i += remain ; } else { for ( let c = 0 ; c < out . length ; c ++ ) out [ c ] . set ( first [ c % first . length ] , i ) ; i += first [ 0 ] . length ; buffer . shift ( ) ; } } } ; }