declare var beeperOutput ; declare interface BeeperBuffer { totalLength : number ; startValue : boolean ; buffer : Uint16Array ; bufferLen : number ; } export class ZxAudioBeeper { protected MIN_LATENCY = 0.2 ; protected MAX_LATENCY = 0.4 ; protected FADE_TO_ZERO_TIME = 0.1 ; protected ctx : AudioContext ; protected volume : number ; protected sampleRate : number ; protected audioCtxStartTime : number ; protected lastVisualBeeperState : boolean ; protected visualBeeperChanging : boolean ; protected BEEPER_DISPLAY_AGGREGATE_TIME = 100 ; protected nextBuffer : AudioBuffer ; protected nextFrame : Float32Array ; protected fixedFrameLength : number ; protected fixedFrameTime : number ; protected nextFrameStartTime : number ; protected bufferedLength = 0 ; protected nextFrameIndex : number ; protected lastEnqueuedAudioSampleValue : number ; protected samplesInTopHalf : boolean ; protected stopped : boolean ; protected gainNode : GainNode ; /**
     * Constructor.
     */ constructor ( sampleRate ) { this . volume = 0.75 ; this . ctx = this . createAudioContext ( sampleRate ) ; this . sampleRate = this . ctx . sampleRate ; if ( this . sampleRate != sampleRate ) { vscode . postMessage ( { command : 'warning' , text : "Sample rate of " + sampleRate + "Hz could not be set. Try setting it to e.g. " + this . sampleRate + "Hz instead." } ) ; } this . fixedFrameLength = Math . ceil ( this . MIN_LATENCY / 4 * this . sampleRate ) ; this . fixedFrameTime = this . fixedFrameLength / this . sampleRate ; this . lastEnqueuedAudioSampleValue = 0 ; this . lastVisualBeeperState = ( this . lastEnqueuedAudioSampleValue != 0 ) ; this . visualBeeperChanging = false ; this . samplesInTopHalf = true ; this . audioCtxStartTime = 0 ; this . stopped = true ; this . gainNode = this . ctx . createGain ( ) ; this . gainNode . gain . value = 1.0 ; this . gainNode . connect ( this . ctx . destination ) ; this . prepareNextFrame ( ) ; setInterval ( ( ) => { this . updateVisualBeeper ( ) ; } , this . BEEPER_DISPLAY_AGGREGATE_TIME ) ; } /**
     * For testing this function is overwritten to return a mocked AudioContext.
     */ protected createAudioContext ( sampleRate ) { return new AudioContext ( { sampleRate } ) ; } /**
     * Stops audio.
     * Creates a fading audio frame.
     */ public stop ( ) { if ( ! this . stopped ) { this . startFadeToZero ( ) ; } } param volume public setVolume ( volume ) { this . volume = volume ; this . gainNode . gain . value = this . gainNode . gain . value ; this . gainNode . gain . linearRampToValueAtTime ( volume , this . ctx . currentTime + 0.1 ) ; } returns public getVolume ( ) { return this . volume ; } param beeperBuffer public writeBeeperSamples ( beeperBuffer ) { const bufLen = beeperBuffer . bufferLen ; const beeperLengths = beeperBuffer . buffer ; this . setVisualBeeperState ( beeperBuffer ) ; if ( this . stopped ) { this . stopped = false ; this . gainNode . gain . value = this . volume ; this . resetTime ( ) ; this . lastEnqueuedAudioSampleValue = 0 ; while ( this . bufferedLength < 2 * this . fixedFrameLength ) { this . startGapFiller ( ) ; } } let k = 0 ; let tmpBuffer = new Float32Array ( beeperBuffer . totalLength ) ; let beeperValue = beeperBuffer . startValue ; let audioValue = this . getAudioValueForBeeper ( beeperValue ) ; this . lastEnqueuedAudioSampleValue = audioValue ; for ( let i = 0 ; i < bufLen ; i ++ ) { const length = beeperLengths [ i ] ; for ( let j = length ; j > 0 ; j -- ) { tmpBuffer [ k ++ ] = audioValue ; } this . lastEnqueuedAudioSampleValue = audioValue ; beeperValue = ! beeperValue ; audioValue = this . getAudioValueForBeeper ( beeperValue ) ; } let remainingLen = beeperBuffer . totalLength ; let offset = 0 ; while ( remainingLen > 0 ) { if ( this . nextFrameIndex + remainingLen < this . fixedFrameLength ) { this . nextFrame . set ( tmpBuffer . slice ( offset , offset + remainingLen ) , this . nextFrameIndex ) ; this . nextFrameIndex += remainingLen ; break ; } const fillLen = this . fixedFrameLength - this . nextFrameIndex ; this . nextFrame . set ( tmpBuffer . slice ( offset , offset + fillLen ) , this . nextFrameIndex ) ; offset += fillLen ; remainingLen -= fillLen ; if ( this . bufferedLength < this . MAX_LATENCY * this . sampleRate + 2 * this . fixedFrameLength ) { this . playNextFrame ( "new frame" ) ; } else { this . nextFrameIndex = 0 ; } } } /**
     * Resets the ctx time.
     */ protected resetTime ( ) { this . audioCtxStartTime = this . ctx . currentTime ; this . nextFrameStartTime = this . audioCtxStartTime + ( this . MIN_LATENCY + this . MAX_LATENCY ) / 2 ; this . bufferedLength = 0 ; } /**
     * Prepares an empty frame.
     */ protected prepareNextFrame ( ) { this . nextBuffer = this . ctx . createBuffer ( 1 , this . fixedFrameLength , this . sampleRate ) ; this . nextFrame = this . nextBuffer . getChannelData ( 0 ) ; this . nextFrameIndex = 0 ; } param beeperValue returns protected getAudioValueForBeeper ( beeperValue ) { let audioValue = beeperValue ? 1 : 0 ; if ( ! this . samplesInTopHalf ) audioValue -= 1 ; return audioValue ; } /**
     * Creates a gap filler frame with all samples containing value
     * and starts it at the next starting time.
     * Skips creation if lastEnqueuedAudioSampleValue is 0 and there is no
     * pending frame.
     * I.e. it will immediately return after a fadeToZero frame(s).
     */ protected startGapFiller ( ) { const frame = this . nextFrame ; const value = this . getLastAudioValue ( ) ; for ( let i = this . nextFrameIndex ; i < this . fixedFrameLength ; i ++ ) frame [ i ] = value ; this . playNextFrame ( "gap filler frame" ) ; } /**
     * Returns the last decoded audio value.
     */ protected getLastAudioValue ( ) { if ( this . nextFrameIndex == 0 ) return this . lastEnqueuedAudioSampleValue ; return this . nextFrame [ this . nextFrameIndex - 1 ] ; } param value protected startFadeToZero ( ) { let prevLastAudioSample = this . getLastAudioValue ( ) ; const prevIndex = this . nextFrameIndex ; this . startGapFiller ( ) ; this . stopped = true ; const currentGain = this . gainNode . gain . value ; this . gainNode . gain . value = currentGain ; const fadeStartTime = this . nextFrameStartTime + prevIndex / this . sampleRate ; this . gainNode . gain . linearRampToValueAtTime ( currentGain , fadeStartTime ) ; this . gainNode . gain . linearRampToValueAtTime ( 0.0 , fadeStartTime + this . FADE_TO_ZERO_TIME ) ; if ( prevLastAudioSample != 0 ) { this . samplesInTopHalf = ( prevLastAudioSample < 0 ) ; } } /**
     * Assumes the audio frame (this.nextFrame) is filled and enqueues it for
     * playing.
     */ protected playNextFrame ( logDescription ) { const bufferSource = this . ctx . createBufferSource ( ) ; bufferSource . buffer = this . nextBuffer ; bufferSource . connect ( this . gainNode ) ; const self = this ; bufferSource . addEventListener ( 'ended' , function ( ) { self . bufferedLength -= self . fixedFrameLength ; if ( self . bufferedLength <= self . fixedFrameLength ) { if ( ! self . stopped || self . gainNode . gain . value > 0 ) { self . startGapFiller ( ) ; return ; } } } ) ; bufferSource . start ( this . nextFrameStartTime ) ; this . bufferedLength += this . fixedFrameLength ; this . nextFrameStartTime += this . fixedFrameTime ; this . prepareNextFrame ( ) ; } /**
     * Sets the visual state of the beeper: 0 or 1.
     */ protected setVisualBeeperState ( beeperBuffer ) { if ( beeperBuffer . bufferLen >= 2 ) { this . visualBeeperChanging = true ; if ( beeperBuffer . bufferLen % 2 == 0 ) this . lastVisualBeeperState = beeperBuffer . startValue ; else this . lastVisualBeeperState = ! beeperBuffer . startValue ; } else { if ( this . lastVisualBeeperState != beeperBuffer . startValue ) { this . visualBeeperChanging = true ; this . lastVisualBeeperState = beeperBuffer . startValue ; } } } /**
     * Called periodically to update the beeper displayed value.
     */ protected updateVisualBeeper ( ) { if ( this . visualBeeperChanging ) { beeperOutput . textContent = '*' ; this . visualBeeperChanging = false ; } else { beeperOutput . textContent = ( this . lastVisualBeeperState ) ? "1" : "0" ; } } }