import { Reg , WaveType , DMC_LOOP_ENABLE , DMC_IRQ_ENABLE } from '../nes/apu' ; import { NoiseSampler } from './apu_util' ; const GLOBAL_MASTER_VOLUME = 0.5 ; abstract class SoundChannel { public abstract destroy ( ) ; public abstract setEnable ( enable ) ; public abstract setVolume ( volume ) ; public abstract start ( ) ; public setFrequency ( _frequency ) { throw new Error ( 'Invalid call' ) ; } public setDutyRatio ( _dutyRatio ) { throw new Error ( 'Invalid call' ) ; } public setNoisePeriod ( _period , _mode ) { throw new Error ( 'Invalid call' ) ; } public setDmcWrite ( _reg , _value ) { throw new Error ( 'Invalid call' ) ; } public setBlockiness ( _context , _destination , _blockiness ) { } } abstract class GainSoundChannel extends SoundChannel { protected gainNode : GainNode ; protected enable = true ; protected volume = 0 ; public constructor ( context ) { super ( ) ; this . gainNode = context . createGain ( ) ; this . gainNode . gain . setValueAtTime ( this . volume , context . currentTime ) ; } public destroy ( ) { if ( this . gainNode != null ) { this . gainNode . disconnect ( ) ; } } public setEnable ( enable ) { if ( enable === this . enable ) return ; this . enable = enable ; if ( ! enable ) this . gainNode . gain . setValueAtTime ( 0 , this . gainNode . context . currentTime ) ; } public setVolume ( volume ) { if ( volume === this . volume ) return ; this . volume = volume ; this . gainNode . gain . setValueAtTime ( volume , this . gainNode . context . currentTime ) ; } } abstract class OscillatorChannel extends GainSoundChannel { protected oscillator : OscillatorNode ; protected frequency = 1 ; public constructor ( context , destination ) { super ( context ) ; this . oscillator = context . createOscillator ( ) ; this . setupOscillator ( this . oscillator , context , destination ) ; } public destroy ( ) { super . destroy ( ) ; if ( this . oscillator != null ) { this . oscillator . stop ( ) ; this . oscillator . disconnect ( ) ; } } public start ( ) { this . oscillator . start ( ) ; } public setFrequency ( frequency ) { if ( frequency === this . frequency ) return ; this . frequency = frequency ; const now = this . oscillator . context . currentTime ; this . oscillator . frequency . setValueAtTime ( frequency , now ) ; } protected abstract setupOscillator ( oscillator , context , destination ) ; } function createQuantizedTriangleWave ( div , N ) { const an = new Float32Array ( N + 1 ) ; an [ 0 ] = 0 ; const coeff = 2 / ( div - 1 ) ; for ( let i = 1 ; i <= N ; ++ i ) { let a = 0 ; const fa = ( x ) => 1 / ( 2 * i * Math . PI ) * Math . sin ( 2 * i * Math . PI * x ) ; for ( let j = 0 ; j < div * 2 ; ++ j ) { const k = j < div ? 1 - j * coeff : - 1 + ( j - div ) * coeff ; a += k * ( fa ( ( j + 1 ) / ( 2 * div ) ) - fa ( j / ( 2 * div ) ) ) ; } an [ i ] = 2 * a ; } const bn = new Float32Array ( N + 1 ) ; bn . fill ( 0 ) ; return { an , bn } ; } class TriangleChannel extends OscillatorChannel { protected setupOscillator ( oscillator , context , destination ) { this . setOscillatorBlockiness ( context , destination , oscillator , false ) ; } public setBlockiness ( context , destination , blocky ) { this . oscillator . disconnect ( ) ; const oscillator = this . oscillator = context . createOscillator ( ) ; this . setOscillatorBlockiness ( context , destination , oscillator , blocky ) ; this . oscillator . frequency . setValueAtTime ( this . frequency , context . currentTime ) ; this . oscillator . start ( ) ; } private setOscillatorBlockiness ( context , destination , oscillator , blocky ) { if ( blocky ) { const { an , bn } = createQuantizedTriangleWave ( 16 , 128 ) ; const wave = context . createPeriodicWave ( an , bn ) ; oscillator . setPeriodicWave ( wave ) ; } else { oscillator . type = 'triangle' ; } oscillator . connect ( this . gainNode ) ; this . gainNode . connect ( destination ) ; } } class SawtoothChannel extends OscillatorChannel { protected setupOscillator ( oscillator , _context , destination ) { oscillator . type = 'sawtooth' ; oscillator . connect ( this . gainNode ) ; this . gainNode . connect ( destination ) ; } } const SP_NOISE_BUFFER_SIZE = 512 ; class SpNoiseChannel extends SoundChannel { private node : ScriptProcessorNode ; private sampler : NoiseSampler ; public constructor ( context , destination ) { super ( ) ; this . sampler = new NoiseSampler ( context . sampleRate ) ; this . node = context . createScriptProcessor ( SP_NOISE_BUFFER_SIZE , 0 , 1 ) ; this . node . onaudioprocess = ( e ) => { const output = e . outputBuffer . getChannelData ( 0 ) ; this . sampler . fillBuffer ( output ) ; } ; this . node . connect ( destination ) ; } public destroy ( ) { if ( this . node != null ) { this . node . disconnect ( ) ; } } public start ( ) { } public setEnable ( enable ) { this . sampler . setEnable ( enable ) ; } public setVolume ( volume ) { this . sampler . setVolume ( volume ) ; } public setNoisePeriod ( period , mode ) { this . sampler . setPeriod ( period , mode ) ; } } const NOISE_WORKER_PASS = 'assets/noise_channel_worker.js' ; class AwNoiseChannel extends SoundChannel { private node ? : AudioWorkletNode ; public static create ( context , destination ) { if ( typeof ( AudioWorkletNode ) === 'undefined' ) return null ; return new AwNoiseChannel ( context , destination ) ; } private constructor ( context , destination ) { super ( ) ; context . audioWorklet . addModule ( NOISE_WORKER_PASS ) . then ( ( ) => { this . node = new AudioWorkletNode ( context , 'noise_worklet' ) ; this . node . connect ( destination ) ; } ) . catch ( console . error ) ; } public destroy ( ) { if ( this . node != null ) { this . node . port . postMessage ( { action : 'stop' } ) ; this . node . disconnect ( ) ; } } public start ( ) { } public setEnable ( enable ) { if ( this . node != null ) this . node . port . postMessage ( { action : 'enable' , value : enable } ) ; } public setVolume ( volume ) { if ( this . node != null ) this . node . port . postMessage ( { action : 'volume' , value : volume } ) ; } public setNoisePeriod ( period , mode ) { if ( this . node != null ) this . node . port . postMessage ( { action : 'period' , period , mode } ) ; } } class PulseChannel extends OscillatorChannel { private delay : DelayNode ; private dutyRatio = 0.5 ; private negate = 1 ; public destroy ( ) { super . destroy ( ) ; if ( this . delay != null ) { this . delay . disconnect ( ) ; } } public setVolume ( volume ) { if ( volume === this . volume ) return ; this . volume = volume ; this . gainNode . gain . setValueAtTime ( volume * this . negate , this . gainNode . context . currentTime ) ; } public setFrequency ( frequency ) { if ( this . frequency === frequency ) return ; super . setFrequency ( frequency ) ; this . updateDelay ( ) ; } public setDutyRatio ( dutyRatio ) { if ( dutyRatio === this . dutyRatio ) return ; this . dutyRatio = dutyRatio ; this . negate = dutyRatio <= 0.5 ? 1 : - 1 ; this . updateDelay ( ) ; } protected setupOscillator ( oscillator , context , destination ) { oscillator . type = 'sawtooth' ; const inverter = context . createGain ( ) ; inverter . gain . value = - 1 ; oscillator . connect ( inverter ) ; inverter . connect ( this . gainNode ) ; const delay = context . createDelay ( ) ; oscillator . connect ( delay ) ; delay . connect ( this . gainNode ) ; this . delay = delay ; this . gainNode . connect ( destination ) ; } private updateDelay ( ) { const now = this . delay . context . currentTime ; const dutyRatio = this . dutyRatio <= 0.5 ? this . dutyRatio : 1 - this . dutyRatio ; this . delay . delayTime . setValueAtTime ( dutyRatio / this . frequency , now ) ; } } const SP_DMC_BUFFER_SIZE = 512 ; const kDmcRateTable = [ 428 , 380 , 340 , 320 , 286 , 254 , 226 , 214 , 190 , 160 , 142 , 128 , 106 , 84 , 72 , 54 ] ; function gcd ( m , n ) { if ( m < n ) { const t = m ; m = n ; n = t ; } let r ; while ( ( r = m % n ) !== 0 ) { m = n ; n = r ; } return n ; } class SpDmcChannel extends SoundChannel { private node ? : ScriptProcessorNode ; private regs = new Uint8Array ( 4 ) ; private enabled = false ; private volume = 0 ; private sampleStep = 0 ; private rateTable : Float32Array ; private rate = 0 ; private dmaAddress = 0xc000 ; private dmaLengthCounter = 1 ; private dmaBuffered = false ; private dmaBuffer = 0 ; private outActive = false ; private outShifter = 0 ; private outDac = 0 ; private outBuffer = 0 ; private timer = 0 ; public constructor ( private triggerDma , private context , private destination ) { super ( ) ; const APU_NOISE_HZ = 894887 * 2 ; const sampleRate = context . sampleRate ; const g = gcd ( APU_NOISE_HZ , sampleRate ) ; const multiplier = Math . min ( sampleRate / g , 0x7fff ) | 0 ; this . sampleStep = ( APU_NOISE_HZ * multiplier / sampleRate ) | 0 ; this . rateTable = new Float32Array ( kDmcRateTable . map ( x => x * multiplier ) ) ; this . rate = this . rateTable [ 0 ] ; } private createNode ( context , destination ) { const node = context . createScriptProcessor ( SP_DMC_BUFFER_SIZE , 0 , 1 ) ; node . onaudioprocess = ( e ) => { const output = e . outputBuffer . getChannelData ( 0 ) ; if ( this . volume <= 0 ) { output . fill ( this . outDac * ( 4.0 / 127 ) ) ; return ; } const volume = this . volume * ( 4.0 / 127 ) ; const sampleStep = this . sampleStep | 0 ; const rate = this . rate | 0 ; let timer = this . timer | 0 ; let value = this . outDac * volume ; for ( let i = 0 ; i < output . length ; ++ i ) { timer -= sampleStep ; if ( timer < 0 ) { do { this . clockDac ( ) ; this . clockDma ( ) ; timer += rate ; } while ( timer < 0 ) ; value = this . outDac * volume ; } output [ i ] = value ; } this . timer = timer ; } ; node . connect ( destination ) ; return node ; } public destroy ( ) { if ( this . node != null ) { this . node . disconnect ( ) ; } } public start ( ) { } public setEnable ( enable ) { this . enabled = enable ; if ( ! enable ) this . volume = 0 ; if ( this . node == null && this . enabled ) { this . node = this . createNode ( this . context , this . destination ) ; } } public setVolume ( volume ) { this . volume = volume ; } public setDmcWrite ( reg , value ) { if ( reg >= 4 ) { switch ( reg ) { case 0xff : if ( value === 0 ) { this . dmaLengthCounter = 0 ; } else if ( this . dmaLengthCounter <= 0 ) { this . dmaLengthCounter = ( this . regs [ Reg . SAMPLE_LENGTH ] << 4 ) + 1 ; this . dmaAddress = 0xc000 + ( this . regs [ Reg . SAMPLE_ADDRESS ] << 6 ) ; if ( ! this . dmaBuffered ) this . doDma ( ) ; } break ; default : break ; } return ; } this . regs [ reg ] = value ; switch ( reg ) { case Reg . STATUS : this . rate = this . rateTable [ value & 0x0f ] ; break ; case Reg . DIRECT_LOAD : this . outDac = value & 0x7f ; break ; case Reg . SAMPLE_ADDRESS : break ; case Reg . SAMPLE_LENGTH : break ; } } private clockDac ( ) { if ( this . outActive ) { const n = this . outDac + ( ( this . outBuffer & 1 ) << 2 ) - 2 ; this . outBuffer >>= 1 ; if ( 0 <= n && n <= 0x7f && n != this . outDac ) { this . outDac = n ; return true ; } } return false ; } private clockDma ( ) { if ( this . outShifter <= 0 ) { this . outShifter = 8 ; this . outActive = this . dmaBuffered ; if ( this . outActive ) { this . dmaBuffered = false ; this . outBuffer = this . dmaBuffer ; if ( this . dmaLengthCounter !== 0 ) this . doDma ( ) ; } } -- this . outShifter ; } private doDma ( ) { this . dmaBuffer = this . triggerDma ( this . dmaAddress ) ; this . dmaAddress = 0x8000 | ( ( this . dmaAddress + 1 ) & 0x7fff ) ; this . dmaBuffered = true ; this . dmaLengthCounter -= 1 ; if ( this . dmaLengthCounter <= 0 ) { if ( this . regs [ Reg . STATUS ] & DMC_LOOP_ENABLE ) { this . dmaLengthCounter = this . regs [ Reg . SAMPLE_LENGTH ] * 16 + 1 ; this . dmaAddress = 0xc000 + this . regs [ Reg . SAMPLE_ADDRESS ] * 64 ; } else if ( this . regs [ Reg . STATUS ] & DMC_IRQ_ENABLE ) { } } } } function createSoundChannel ( context , destination , type ) { switch ( type ) { case WaveType . PULSE : return new PulseChannel ( context , destination ) ; case WaveType . TRIANGLE : return new TriangleChannel ( context , destination ) ; case WaveType . NOISE : return AwNoiseChannel . create ( context , destination ) || new SpNoiseChannel ( context , destination ) ; case WaveType . SAWTOOTH : return new SawtoothChannel ( context , destination ) ; default : throw new Error ( 'Unhandled' ) ; } } export class AudioManager { private static initialized = false ; private static audioContextClass ? : AudioContext ; private static context ? : AudioContext ; private static masterGainNode : GainNode ; private static analyserNode ? : AnalyserNode ; private static masterVolume = 1.0 ; private channels = new Array < SoundChannel > ( ) ; private blockiness = false ; public static setUp ( audioContextClass ) { if ( AudioManager . initialized ) return ; if ( audioContextClass == null ) return ; AudioManager . audioContextClass = audioContextClass ; AudioManager . initialized = true ; } public static enableAudio ( ) { if ( AudioManager . context != null ) return ; const audioContextClass = AudioManager . audioContextClass ; if ( audioContextClass != null ) { const context = new audioContextClass ( ) as AudioContext ; AudioManager . context = context ; AudioManager . masterGainNode = context . createGain ( ) ; AudioManager . masterGainNode . gain . setValueAtTime ( AudioManager . masterVolume * GLOBAL_MASTER_VOLUME , context . currentTime ) ; AudioManager . masterGainNode . connect ( context . destination ) ; AudioManager . initialized = true ; } } public static setMasterVolume ( volume ) { AudioManager . checkSetUpCalled ( ) ; AudioManager . masterVolume = volume ; const context = AudioManager . context ; if ( context ) AudioManager . masterGainNode . gain . setValueAtTime ( volume * GLOBAL_MASTER_VOLUME , context . currentTime ) ; } public static createAnalyser ( ) { const context = AudioManager . context ; if ( context == null ) return null ; if ( AudioManager . analyserNode == null ) { AudioManager . analyserNode = context . createAnalyser ( ) ; AudioManager . masterGainNode . disconnect ( ) ; AudioManager . masterGainNode . connect ( AudioManager . analyserNode ) ; AudioManager . analyserNode . connect ( context . destination ) ; } return AudioManager . analyserNode ; } private static checkSetUpCalled ( ) { if ( ! AudioManager . initialized ) { console . error ( 'Audio.setUp must be called!' ) ; } } constructor ( private triggerDma ) { AudioManager . checkSetUpCalled ( ) ; } public release ( ) { this . releaseAllChannels ( ) ; } public releaseAllChannels ( ) { if ( this . channels != null ) { for ( const channel of this . channels ) { channel . destroy ( ) ; } this . channels . length = 0 ; } } public addChannel ( type ) { const context = AudioManager . context ; if ( context == null ) return ; let sc ; if ( type === WaveType . DMC ) { sc = new SpDmcChannel ( this . triggerDma , context , AudioManager . masterGainNode ) ; } else { sc = createSoundChannel ( context , AudioManager . masterGainNode , type ) ; } sc . start ( ) ; this . channels . push ( sc ) ; } public toggleBlockiness ( ) { this . blockiness = ! this . blockiness ; if ( AudioManager . context != null ) { for ( const channel of this . channels ) channel . setBlockiness ( AudioManager . context , AudioManager . masterGainNode , this . blockiness ) ; } } public getBlockiness ( ) { return this . blockiness ; } public setChannelEnable ( channel , enable ) { if ( AudioManager . context == null ) return ; this . channels [ channel ] . setEnable ( enable ) ; } public setChannelFrequency ( channel , frequency ) { if ( AudioManager . context == null ) return ; frequency = Math . min ( frequency , AudioManager . context . sampleRate * 0.5 ) ; this . channels [ channel ] . setFrequency ( frequency ) ; } public setChannelVolume ( channel , volume ) { if ( AudioManager . context == null ) return ; this . channels [ channel ] . setVolume ( volume ) ; } public setChannelDutyRatio ( channel , dutyRatio ) { if ( AudioManager . context == null ) return ; this . channels [ channel ] . setDutyRatio ( dutyRatio ) ; } public setChannelPeriod ( channel , period , mode ) { if ( AudioManager . context == null ) return ; this . channels [ channel ] . setNoisePeriod ( period , mode ) ; } public setChannelDmcWrite ( channel , buf ) { if ( AudioManager . context == null ) return ; for ( let i = 0 ; i < buf . length ; ++ i ) { const d = buf [ i ] ; const r = d >> 8 ; const v = d & 0xff ; this . channels [ channel ] . setDmcWrite ( r , v ) ; } } public muteAll ( ) { const n = this . channels . length ; for ( let ch = 0 ; ch < n ; ++ ch ) this . setChannelVolume ( ch , 0 ) ; } }