2019-10-20 16:53:23 +00:00
// this controls if the new AudioWorklet API should be used if available.
// the engine will still fall back to the ScriptProcessorNode if this is set to true but not available in the browser.
var useAudioWorklets = true ;
function AudioEngine ( maxBufferLength , audioReporter ) {
this . audioReporter = audioReporter ;
2019-10-26 20:32:25 +00:00
this . initStats ( ) ;
2019-10-20 16:53:23 +00:00
this . resetStats ( ) ;
var ctx = window . AudioContext || window . webkitAudioContext ;
if ( ! ctx ) {
return ;
}
this . audioContext = new ctx ( ) ;
this . allowed = this . audioContext . state === 'running' ;
this . started = false ;
2020-01-05 22:26:27 +00:00
this . audioCodec = new ImaAdpcmCodec ( ) ;
2019-10-20 16:53:23 +00:00
this . compression = 'none' ;
this . setupResampling ( ) ;
2020-01-06 15:29:23 +00:00
this . resampler = new Interpolator ( this . resamplingFactor ) ;
2019-10-20 16:53:23 +00:00
this . maxBufferSize = maxBufferLength * this . getSampleRate ( ) ;
}
AudioEngine . prototype . start = function ( callback ) {
var me = this ;
if ( me . resamplingFactor === 0 ) return ; //if failed to find a valid resampling factor...
if ( me . started ) {
if ( callback ) callback ( false ) ;
return ;
}
me . audioContext . resume ( ) . then ( function ( ) {
me . allowed = me . audioContext . state === 'running' ;
if ( ! me . allowed ) {
if ( callback ) callback ( false ) ;
return ;
}
me . started = true ;
me . gainNode = me . audioContext . createGain ( ) ;
me . gainNode . connect ( me . audioContext . destination ) ;
if ( useAudioWorklets && me . audioContext . audioWorklet ) {
me . audioContext . audioWorklet . addModule ( 'static/lib/AudioProcessor.js' ) . then ( function ( ) {
me . audioNode = new AudioWorkletNode ( me . audioContext , 'openwebrx-audio-processor' , {
numberOfInputs : 0 ,
numberOfOutputs : 1 ,
outputChannelCount : [ 1 ] ,
processorOptions : {
maxBufferSize : me . maxBufferSize
}
} ) ;
me . audioNode . connect ( me . gainNode ) ;
me . audioNode . port . addEventListener ( 'message' , function ( m ) {
var json = JSON . parse ( m . data ) ;
if ( typeof ( json . buffersize ) !== 'undefined' ) {
2019-10-26 20:32:25 +00:00
me . audioReporter ( {
buffersize : json . buffersize
} ) ;
}
if ( typeof ( json . samplesProcessed ) !== 'undefined' ) {
me . audioSamples . add ( json . samplesProcessed ) ;
2019-10-20 16:53:23 +00:00
}
} ) ;
me . audioNode . port . start ( ) ;
if ( callback ) callback ( true , 'AudioWorklet' ) ;
} ) ;
} else {
me . audioBuffers = [ ] ;
if ( ! AudioBuffer . prototype . copyToChannel ) { //Chrome 36 does not have it, Firefox does
AudioBuffer . prototype . copyToChannel = function ( input , channel ) //input is Float32Array
{
var cd = this . getChannelData ( channel ) ;
for ( var i = 0 ; i < input . length ; i ++ ) cd [ i ] = input [ i ] ;
}
}
var bufferSize ;
if ( me . audioContext . sampleRate < 44100 * 2 )
bufferSize = 4096 ;
else if ( me . audioContext . sampleRate >= 44100 * 2 && me . audioContext . sampleRate < 44100 * 4 )
bufferSize = 4096 * 2 ;
else if ( me . audioContext . sampleRate > 44100 * 4 )
bufferSize = 4096 * 4 ;
function audio _onprocess ( e ) {
var total = 0 ;
var out = new Float32Array ( bufferSize ) ;
while ( me . audioBuffers . length ) {
var b = me . audioBuffers . shift ( ) ;
// not enough space to fit all data, so splice and put back in the queue
2019-10-26 20:32:25 +00:00
if ( total + b . length > bufferSize ) {
var spaceLeft = bufferSize - total ;
var tokeep = b . subarray ( 0 , spaceLeft ) ;
2019-10-20 16:53:23 +00:00
out . set ( tokeep , total ) ;
2019-10-26 20:32:25 +00:00
var tobuffer = b . subarray ( spaceLeft , b . length ) ;
2019-10-20 16:53:23 +00:00
me . audioBuffers . unshift ( tobuffer ) ;
2019-10-26 20:32:25 +00:00
total += spaceLeft ;
2019-10-20 16:53:23 +00:00
break ;
} else {
out . set ( b , total ) ;
2019-10-26 20:32:25 +00:00
total += b . length ;
2019-10-20 16:53:23 +00:00
}
}
e . outputBuffer . copyToChannel ( out , 0 ) ;
2019-10-26 20:32:25 +00:00
me . audioSamples . add ( total ) ;
2019-10-20 16:53:23 +00:00
}
//on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor
var method = 'createScriptProcessor' ;
if ( me . audioContext . createJavaScriptNode ) {
method = 'createJavaScriptNode' ;
}
me . audioNode = me . audioContext [ method ] ( bufferSize , 0 , 1 ) ;
me . audioNode . onaudioprocess = audio _onprocess ;
me . audioNode . connect ( me . gainNode ) ;
if ( callback ) callback ( true , 'ScriptProcessorNode' ) ;
}
setInterval ( me . reportStats . bind ( me ) , 1000 ) ;
} ) ;
2019-10-20 21:38:58 +00:00
} ;
2019-10-20 16:53:23 +00:00
AudioEngine . prototype . isAllowed = function ( ) {
return this . allowed ;
2019-10-20 21:38:58 +00:00
} ;
2019-10-20 16:53:23 +00:00
AudioEngine . prototype . reportStats = function ( ) {
if ( this . audioNode . port ) {
2019-10-26 20:32:25 +00:00
this . audioNode . port . postMessage ( JSON . stringify ( { cmd : 'getStats' } ) ) ;
2019-10-20 16:53:23 +00:00
} else {
2019-10-26 20:32:25 +00:00
this . audioReporter ( {
buffersize : this . getBuffersize ( )
} ) ;
2019-10-20 16:53:23 +00:00
}
2019-10-26 20:32:25 +00:00
} ;
AudioEngine . prototype . initStats = function ( ) {
var me = this ;
var buildReporter = function ( key ) {
return function ( v ) {
var report = { } ;
report [ key ] = v ;
me . audioReporter ( report ) ;
}
2019-10-20 16:53:23 +00:00
2019-10-26 20:32:25 +00:00
} ;
this . audioBytes = new Measurement ( ) ;
this . audioBytes . report ( 10000 , 1000 , buildReporter ( 'audioByteRate' ) ) ;
this . audioSamples = new Measurement ( ) ;
this . audioSamples . report ( 10000 , 1000 , buildReporter ( 'audioRate' ) ) ;
2019-10-20 21:38:58 +00:00
} ;
2019-10-20 16:53:23 +00:00
AudioEngine . prototype . resetStats = function ( ) {
2019-10-26 20:32:25 +00:00
this . audioBytes . reset ( ) ;
this . audioSamples . reset ( ) ;
2019-10-20 21:38:58 +00:00
} ;
2019-10-20 16:53:23 +00:00
AudioEngine . prototype . setupResampling = function ( ) { //both at the server and the client
var output _range _max = 12000 ;
var output _range _min = 8000 ;
var targetRate = this . audioContext . sampleRate ;
var i = 1 ;
while ( true ) {
var audio _server _output _rate = Math . floor ( targetRate / i ) ;
if ( audio _server _output _rate < output _range _min ) {
this . resamplingFactor = 0 ;
this . outputRate = 0 ;
divlog ( 'Your audio card sampling rate (' + targetRate + ') is not supported.<br />Please change your operating system default settings in order to fix this.' , 1 ) ;
break ;
} else if ( audio _server _output _rate >= output _range _min && audio _server _output _rate <= output _range _max ) {
this . resamplingFactor = i ;
this . outputRate = audio _server _output _rate ;
break ; //okay, we're done
}
i ++ ;
}
2019-10-20 21:38:58 +00:00
} ;
2019-10-20 16:53:23 +00:00
AudioEngine . prototype . getOutputRate = function ( ) {
return this . outputRate ;
2019-10-20 21:38:58 +00:00
} ;
2019-10-20 16:53:23 +00:00
AudioEngine . prototype . getSampleRate = function ( ) {
return this . audioContext . sampleRate ;
2019-10-20 21:38:58 +00:00
} ;
2019-10-20 16:53:23 +00:00
AudioEngine . prototype . pushAudio = function ( data ) {
if ( ! this . audioNode ) return ;
2019-10-26 20:32:25 +00:00
this . audioBytes . add ( data . byteLength ) ;
2019-10-20 16:53:23 +00:00
var buffer ;
if ( this . compression === "adpcm" ) {
//resampling & ADPCM
buffer = this . audioCodec . decode ( new Uint8Array ( data ) ) ;
} else {
buffer = new Int16Array ( data ) ;
}
2020-01-06 15:29:23 +00:00
buffer = this . resampler . process ( buffer ) ;
2019-10-20 16:53:23 +00:00
if ( this . audioNode . port ) {
// AudioWorklets supported
this . audioNode . port . postMessage ( buffer ) ;
} else {
// silently drop excess samples
if ( this . getBuffersize ( ) + buffer . length <= this . maxBufferSize ) {
this . audioBuffers . push ( buffer ) ;
}
}
2019-10-20 21:38:58 +00:00
} ;
2019-10-20 16:53:23 +00:00
AudioEngine . prototype . setCompression = function ( compression ) {
this . compression = compression ;
2019-10-20 21:38:58 +00:00
} ;
2019-10-20 16:53:23 +00:00
AudioEngine . prototype . setVolume = function ( volume ) {
this . gainNode . gain . value = volume ;
2019-10-20 21:38:58 +00:00
} ;
2019-10-20 16:53:23 +00:00
AudioEngine . prototype . getBuffersize = function ( ) {
// only available when using ScriptProcessorNode
if ( ! this . audioBuffers ) return 0 ;
return this . audioBuffers . map ( function ( b ) { return b . length ; } ) . reduce ( function ( a , b ) { return a + b ; } , 0 ) ;
2019-10-20 21:38:58 +00:00
} ;
2020-01-05 22:26:27 +00:00
function ImaAdpcmCodec ( ) {
2020-01-05 22:33:07 +00:00
this . reset ( ) ;
}
ImaAdpcmCodec . prototype . reset = function ( ) {
2020-01-05 22:26:27 +00:00
this . stepIndex = 0 ;
this . predictor = 0 ;
this . step = 0 ;
2020-01-05 22:33:07 +00:00
} ;
2020-01-05 22:26:27 +00:00
ImaAdpcmCodec . imaIndexTable = [ - 1 , - 1 , - 1 , - 1 , 2 , 4 , 6 , 8 , - 1 , - 1 , - 1 , - 1 , 2 , 4 , 6 , 8 ] ;
ImaAdpcmCodec . imaStepTable = [
7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 16 , 17 ,
19 , 21 , 23 , 25 , 28 , 31 , 34 , 37 , 41 , 45 ,
50 , 55 , 60 , 66 , 73 , 80 , 88 , 97 , 107 , 118 ,
130 , 143 , 157 , 173 , 190 , 209 , 230 , 253 , 279 , 307 ,
337 , 371 , 408 , 449 , 494 , 544 , 598 , 658 , 724 , 796 ,
876 , 963 , 1060 , 1166 , 1282 , 1411 , 1552 , 1707 , 1878 , 2066 ,
2272 , 2499 , 2749 , 3024 , 3327 , 3660 , 4026 , 4428 , 4871 , 5358 ,
5894 , 6484 , 7132 , 7845 , 8630 , 9493 , 10442 , 11487 , 12635 , 13899 ,
15289 , 16818 , 18500 , 20350 , 22385 , 24623 , 27086 , 29794 , 32767
] ;
ImaAdpcmCodec . prototype . decode = function ( data ) {
var output = new Int16Array ( data . length * 2 ) ;
for ( var i = 0 ; i < data . length ; i ++ ) {
output [ i * 2 ] = this . decodeNibble ( data [ i ] & 0x0F ) ;
output [ i * 2 + 1 ] = this . decodeNibble ( ( data [ i ] >> 4 ) & 0x0F ) ;
}
return output ;
} ;
ImaAdpcmCodec . prototype . decodeNibble = function ( nibble ) {
this . stepIndex += ImaAdpcmCodec . imaIndexTable [ nibble ] ;
this . stepIndex = Math . min ( Math . max ( this . stepIndex , 0 ) , 88 ) ;
var diff = this . step >> 3 ;
if ( nibble & 1 ) diff += this . step >> 2 ;
if ( nibble & 2 ) diff += this . step >> 1 ;
if ( nibble & 4 ) diff += this . step ;
if ( nibble & 8 ) diff = - diff ;
this . predictor += diff ;
this . predictor = Math . min ( Math . max ( this . predictor , - 32768 ) , 32767 ) ;
this . step = ImaAdpcmCodec . imaStepTable [ this . stepIndex ] ;
return this . predictor ;
2020-01-06 15:29:23 +00:00
} ;
function Interpolator ( factor ) {
this . factor = factor ;
2020-01-06 18:48:54 +00:00
this . lowpass = new Lowpass ( factor )
2020-01-06 15:29:23 +00:00
}
Interpolator . prototype . process = function ( data ) {
var output = new Float32Array ( data . length * this . factor ) ;
for ( var i = 0 ; i < data . length ; i ++ ) {
output [ i * this . factor ] = ( data [ i ] + 0.5 ) / 32768 ;
}
2020-01-06 18:48:54 +00:00
return this . lowpass . process ( output ) ;
} ;
function Lowpass ( interpolation ) {
var transitionBandwidth = 0.05 ;
this . numtaps = Math . round ( 4 / transitionBandwidth ) ;
if ( this . numtaps % 2 == 0 ) this . numtaps += 1 ;
var cutoff = 1 / interpolation ;
this . coefficients = this . getCoefficients ( cutoff / 2 ) ;
this . delay = new Float32Array ( this . numtaps ) ;
for ( var i = 0 ; i < this . numtaps ; i ++ ) {
this . delay [ i ] = 0 ;
}
this . delayIndex = 0 ;
}
Lowpass . prototype . getCoefficients = function ( cutoffRate ) {
var middle = Math . floor ( this . numtaps / 2 ) ;
// hamming window
var window _function = function ( r ) {
var rate = 0.5 + r / 2 ;
return 0.54 - 0.46 * Math . cos ( 2 * Math . PI * rate ) ;
}
var output = [ ] ;
output [ middle ] = 2 * Math . PI * cutoffRate * window _function ( 0 ) ;
for ( var i = 1 ; i <= middle ; i ++ ) {
output [ middle - i ] = output [ middle + i ] = ( Math . sin ( 2 * Math . PI * cutoffRate * i ) / i ) * window _function ( i / middle ) ;
}
return this . normalizeCoefficients ( output ) ;
} ;
Lowpass . prototype . normalizeCoefficients = function ( input ) {
var sum = 0 ;
var output = [ ] ;
for ( var i = 0 ; i < input . length ; i ++ ) {
sum += input [ i ] ;
}
for ( var i = 0 ; i < input . length ; i ++ ) {
output [ i ] = input [ i ] / sum ;
}
return output ;
} ;
Lowpass . prototype . process = function ( input ) {
output = new Float32Array ( input . length ) ;
for ( var oi = 0 ; oi < input . length ; oi ++ ) {
this . delay [ this . delayIndex ] = input [ oi ] ;
this . delayIndex = ( this . delayIndex + 1 ) % this . numtaps ;
var acc = 0 ;
var index = this . delayIndex ;
for ( var i = 0 ; i < this . numtaps ; ++ i ) {
var index = index != 0 ? index - 1 : this . numtaps - 1 ;
acc += this . delay [ index ] * this . coefficients [ i ] ;
if ( isNaN ( acc ) ) debugger ;
}
output [ oi ] = acc ;
}
2020-01-06 15:29:23 +00:00
return output ;
2020-01-06 18:48:54 +00:00
} ;