////////////////////////////////////////////////////////////////////// // // // cpuSound.pas: Sound simulation code // // This manages sound channels 1,2,3,4,A,B // // // // The contents of this file are subject to the Bottled Light // // Public License Version 1.0 (the "License"); you may not use this // // file except in compliance with the License. You may obtain a // // copy of the License at http://www.bottledlight.com/BLPL/ // // // // Software distributed under the License is distributed on an // // "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or // // implied. See the License for the specific language governing // // rights and limitations under the License. // // // // The Original Code is the Mappy VM Core, released April 1st, 2003 // // The Initial Developer of the Original Code is Bottled Light, // // Inc. Portions created by Bottled Light, Inc. are Copyright // // (C) 2001 - 2003 Bottled Light, Inc. All Rights Reserved. // // // // Author(s): // // Michael Noland (joat), michael@bottledlight.com // // // // Changelog: // // 1.0: First public release (April 1st, 2003) // // // // Notes: // // Channel 1 is a square wave with envelope and sweep control // // Channel 2 is a square wave with only envelope control // // Channel 3 is a 4 bit arbitrary waveform with envelope control // // Channel 4 is a LFSR 'pseudonoise' channel /w envelope control // // Channels A and B are 8 bit FIFO fed waveforms // // // ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// unit cpuSound; /////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// interface //////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// uses SysUtils, Math, nexus, AddressSpace; ////////////////////////////////////////////////////////////////////// // Overall control functions ///////////////////////////////////////// ////////////////////////////////////////////////////////////////////// // soundProcess() runs the audio simulator for a number of CPU cycles. procedure soundProcess(cycles: integer); // Signal the UI that there are sound samples ready, then clear the // sample buffer. procedure soundFlushBuffer; // This function is called *before* any sound related registers are // modified, to allow the sound simulation to catch up to the current // time, using the original settings. procedure soundChanging; // vmSetAudioRate() sets the number of CPU cycles that should elapse // between each generated sound sample. procedure vmSetAudioRate(cyclesPerSample: integer); // vmGetAudioData() is called to get a pointer to the sample buffer, // which is stored in the data pointer. The number of valid samples // in that buffer is stuffed in length. The buffer size is actually // length * 2 bytes, since each sample consists of both a left and a // right volume level (even bytes = L, odd bytes = R). procedure vmGetAudioData(var data: pointer; var length: integer); ////////////////////////////////////////////////////////////////////// // These functions are called from cpuMemory whenever the // corresponding registers are modified ////////////////////////////////////////////////////////////////////// procedure soundSetSound1Sweep; procedure soundSetSound1Length; procedure soundSetSound1Freq; procedure soundSetSound2Length; procedure soundSetSound2Freq; procedure soundSetSound3CR; procedure soundSetSound3Length; procedure soundSetSound3Freq; procedure soundSetSound4Length; procedure soundSetSound4CR; procedure soundIncrementFifoA; procedure soundIncrementFifoB; procedure soundWriteDSoundCR; ////////////////////////////////////////////////////////////////////// // DS FIFO management //////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// procedure QueueTDSoundRecord(var r: TDSoundRecord; data: int8); procedure ResetTDSoundRecord(var r: TDSoundRecord); procedure DequeueTDSoundRecord(var r: TDSoundRecord); ////////////////////////////////////////////////////////////////////// exports vmSetAudioRate, vmGetAudioData; ////////////////////////////////////////////////////////////////////// var // Flags that enable sound generation on the different audio // channels, provided so the UI can filter channels. enableGBC1, enableGBC2, enableGBC3, enableGBC4: boolean; enableDSA, enableDSB: boolean; ////////////////////////////////////////////////////////////////////// implementation /////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// uses cpuMemory, cpuMisc; ////////////////////////////////////////////////////////////////////// // Globals shared between the sound functions //////////////////////// ////////////////////////////////////////////////////////////////////// const BUFFER_MASK = 32767; ////////////////////////////////////////////////////////////////////// var // even bytes = L, odd bytes = R soundBuffer: array[0..BUFFER_MASK] of uint8; soundBufPos: integer; audioRate: integer; ////////////////////////////////////////////////////////////////////// const // Channel 1 and 2 waveforms at different duty cycles SquareWaves: array[0..3, 0..7] of integer = ((+1,-1,-1,-1,-1,-1,-1,-1), (+1,+1,-1,-1,-1,-1,-1,-1), (+1,+1,+1,+1,-1,-1,-1,-1), (+1,+1,+1,+1,+1,+1,-1,-1)); // ((-1,-1,1,-1,-1,-1,-1,-1), // (-1,1,1,-1,-1,-1,-1,-1), // (-1,1,1,1,1,-1,-1,-1), // (1,-1,-1,1,1,1,1,1)); ////////////////////////////////////////////////////////////////////// // These functions are called from cpuMemory whenever the // // corresponding registers are modified // ////////////////////////////////////////////////////////////////////// procedure soundIncrementFifoA; begin QueueTDSoundRecord(soundA, registers[SOUNDA_FIFO+0]); QueueTDSoundRecord(soundA, registers[SOUNDA_FIFO+1]); QueueTDSoundRecord(soundA, registers[SOUNDA_FIFO+2]); QueueTDSoundRecord(soundA, registers[SOUNDA_FIFO+3]); end; ////////////////////////////////////////////////////////////////////// procedure soundIncrementFifoB; begin QueueTDSoundRecord(soundB, registers[SOUNDB_FIFO+0]); QueueTDSoundRecord(soundB, registers[SOUNDB_FIFO+1]); QueueTDSoundRecord(soundB, registers[SOUNDB_FIFO+2]); QueueTDSoundRecord(soundB, registers[SOUNDB_FIFO+3]); end; ////////////////////////////////////////////////////////////////////// procedure soundSetSound1Sweep; var r: byte; begin r := registers[SOUND1_SWEEP]; // the interval between sweeps is length/128 seconds, or length*131072 cycles sound1.sweepRate := ((r shr 4) and 7) * 131072; sound1.sweepDecrease := r and SOUND_SWEEP_DECREASE <> 0; sound1.sweepStep := r and 7; // Turn off the sound if theres a non-zero sweep rate when sweeping is disabled if (sound1.sweepRate = 0) and (sound1.sweepStep > 0) and not sound1.sweepDecrease then sound1.enabled := false; end; ////////////////////////////////////////////////////////////////////// procedure soundSetSound1Length; var r: uint16; begin r := Puint16(@(registers[SOUND1_LENGTH]))^; // total sound length is (64-length)/256 seconds, or (64-length)*65536 cycles sound1.soundLength := (64 - r and 63)*65536; sound1.waveDuty := (r shr 6) and 3; if not sound1.enabled then begin sound1.envIncrease := (r shr 8) and SOUND_ENVELOPE_INCREASE <> 0; sound1.volume := r shr 12; // envelope step rate is length/64 secs, or length*262144 cycles sound1.envRate := ((r shr 8) and 3)*262144; end; end; ////////////////////////////////////////////////////////////////////// procedure soundSetSound1Freq; var r: uint16; begin r := Puint16(@(registers[SOUND1_FREQUENCY]))^; // If this bit is written, reset sound 1 if r and SOUND_RESTART <> 0 then begin sound1.enabled := false; soundSetSound1Sweep; soundSetSound1Length; sound1.index := 0; // sound1.waveIndex := 0; according to belogic, no reset of this sound1.envIndex := 0; sound1.sweepIndex := 0; sound1.enabled := true; end; sound1.timed := r and (1 shl 14) <> 0; // the sound frequency is 131072/(2048-freq) Hz, every (2048-freq)*128 cycles sound1.freq := (2048 - r and $7FF) * 128; Puint16(@(registers[SOUND1_FREQUENCY]))^ := r and not SOUND_RESTART; end; ////////////////////////////////////////////////////////////////////// procedure soundSetSound2Length; var r: uint16; begin r := Puint16(@(registers[SOUND2_LENGTH]))^; // total sound length is (64-length)/256 seconds, or (64-length)*65536 cycles sound2.soundLength := (64 - r and 63)*65536; sound2.waveDuty := (r shr 6) and 3; if not sound2.enabled then begin sound2.envIncrease := (r shr 8) and SOUND_ENVELOPE_INCREASE <> 0; sound2.volume := r shr 12; // envelope step rate is length/64 secs, or length*262144 cycles sound2.envRate := ((r shr 8) and 3)*262144; end; end; ////////////////////////////////////////////////////////////////////// procedure soundSetSound2Freq; var r: uint16; begin r := Puint16(@(registers[SOUND2_FREQUENCY]))^; // If this bit is written, reset sound 2 if r and SOUND_RESTART <> 0 then begin sound2.enabled := false; soundSetSound2Length; sound2.index := 0; sound2.waveIndex := 0; sound2.envIndex := 0; sound2.enabled := true; end; sound2.timed := r and (1 shl 14) <> 0; // the sound frequency is 131072/(2048-freq) Hz, every (2048-freq)*128 cycles sound2.freq := (2048 - r and $7FF) * 128; Puint16(@(registers[SOUND2_FREQUENCY]))^ := r and not SOUND_RESTART; end; ////////////////////////////////////////////////////////////////////// procedure sound3swapBanks; var temp: byte; i: integer; begin for i := 0 to 15 do begin temp := sound3.backBank[i]; sound3.backBank[i] := registers[SOUND3_PATTERN+i]; registers[SOUND3_PATTERN+i] := temp; end; sound3.curBank := not sound3.curBank; end; ////////////////////////////////////////////////////////////////////// procedure soundSetSound3CR; var r: byte; bank: boolean; begin r := registers[SOUND3_CR]; // sound3.enabled := sound3.enabled or (r and SOUND3_ACTIVE <> 0); sound3.doubleLength := r and SOUND3_64SAMPLES <> 0; bank := r and SOUND3_BANK1 <> 0; if bank <> sound3.curBank then sound3swapBanks; end; ////////////////////////////////////////////////////////////////////// procedure soundSetSound3Length; // JSensebe says: 000=mute, 001=full vol, 010=1/2 vol, 011=1/4 vol, 1xx=3/4 vol const sound3volumes: array[0..7] of integer = (0,4,2,1,3,3,3,3); var r: uint16; begin r := Puint16(@(registers[SOUND3_LENGTH]))^; // total sound length is (256-length)/256 seconds, or (256-length)*65536 cycles sound3.soundLength := (256 - r and $FF)*65536; sound3.volume := sound3volumes[r shr 13]; end; ////////////////////////////////////////////////////////////////////// procedure soundSetSound3Freq; var r: uint16; i: integer; begin r := Puint16(@(registers[SOUND3_FREQUENCY]))^; // If this bit is written, reset sound 3 if r and SOUND_RESTART <> 0 then begin sound3.enabled := false; soundSetSound3Length; sound3.index := 0; sound3.waveIndex := 0; sound3.enabled := registers[SOUND3_CR] and SOUND3_ACTIVE <> 0; for i := 0 to 15 do begin sound3.backBank[i] := 0; registers[SOUND3_PATTERN+i] := 0; end; end; sound3.timed := r and (1 shl 14) <> 0; // the sound frequency is 131072/(2048-freq) Hz, every (2048-freq)*128 cycles sound3.freq := (2048 - r and $7FF) * 128; Puint16(@(registers[SOUND3_FREQUENCY]))^ := r and not SOUND_RESTART; end; ////////////////////////////////////////////////////////////////////// procedure soundSetSound4Length; var r: uint16; begin r := Puint16(@(registers[SOUND4_LENGTH]))^; // total sound length is (64-length)/256 seconds, or (64-length)*65536 cycles sound4.soundLength := (64 - r and 63)*65536; if not sound4.enabled then begin sound4.envIncrease := (r shr 8) and SOUND_ENVELOPE_INCREASE <> 0; sound4.volume := r shr 12; // envelope rate is length/64 secs, or length*262144 cycles sound4.envRate := ((r shr 8) and 3)*262144; end; end; ////////////////////////////////////////////////////////////////////// procedure soundSetSound4CR; var r: uint16; begin r := Puint16(@(registers[SOUND4_CR]))^; sound4.timed := r and (1 shl 14) <> 0; sound4.use7steps := r and SOUND4_7_STEPS <> 0; sound4.freq := max((r and 7) shl 1, 1) * (1 shl (6 + (r shr 4) and $F)); // If this bit is written, reset sound 4 if r and SOUND_RESTART <> 0 then begin sound4.enabled := false; soundSetSound4Length; sound4.index := 0; sound4.envIndex := 0; sound4.enabled := true; if sound4.use7steps then sound4.lfsr := $7F else sound4.lfsr := $7FFF; end; Puint16(@(registers[SOUND4_CR]))^ := r and not SOUND_RESTART; end; ////////////////////////////////////////////////////////////////////// procedure soundWriteDSoundCR; var cr: uint16; temp: integer; begin cr := Puint16(@(registers[SOUND_DSOUND_CR]))^; // Set DirectSound A shiznat if cr and DSA_RESET <> 0 then ResetTDSoundRecord(soundA); temp := (cr shr DSA_VOLUME_SH) and 1; if cr and DSA_LEFT <> 0 then soundA.lvol := temp+1 else soundA.lvol := 0; if cr and DSA_RIGHT <> 0 then soundA.rvol := temp+1 else soundA.rvol := 0; soundA.enabled := soundA.lvol + soundA.rvol <> -2; if cr and DSA_TIMER <> 0 then soundA.timer := 1 else soundA.timer := 0; // Set DirectSound B shiznat if cr and DSB_RESET <> 0 then ResetTDSoundRecord(soundB); temp := (cr shr DSB_VOLUME_SH) and 1; if cr and DSB_LEFT <> 0 then soundB.lvol := temp+1 else soundB.lvol := 0; if cr and DSB_RIGHT <> 0 then soundB.rvol := temp+1 else soundB.rvol := 0; soundB.enabled := soundB.lvol + soundB.rvol <> -2; if cr and DSB_TIMER <> 0 then soundB.timer := 1 else soundB.timer := 0; end; ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// // This function is called *before* any sound related registers are // modified, to allow the sound simulation to catch up to the current // time, using the original settings. procedure soundChanging; begin soundProcess(soundQuotaAtLastFlush - quota + soundCyclesUndone); soundQuotaAtLastFlush := quota; soundCyclesUndone := 0; end; ////////////////////////////////////////////////////////////////////// // vmSetAudioRate() sets the number of CPU cycles that should elapse // between each generated sound sample. procedure vmSetAudioRate(cyclesPerSample: integer); begin audioRate := cyclesPerSample; end; ////////////////////////////////////////////////////////////////////// // Signal the UI that there are sound samples ready, then clear the // sample buffer. procedure soundFlushBuffer; begin OnSoundReady(@soundBuffer, soundBufPos shr 1); soundBufPos := 0; end; ////////////////////////////////////////////////////////////////////// // Return a pointer to the sample buffer. procedure vmGetAudioData(var data: pointer; var length: integer); begin data := @soundBuffer; length := soundBufPos shr 1; soundBufPos := 0; end; ////////////////////////////////////////////////////////////////////// // soundProcess() runs the audio simulator for a number of CPU cycles. procedure soundProcess(cycles: integer); var gbcVolumeMask: integer; temp, t2, left, right: integer; begin if (cycles < 0) or (cycles > 1232*228) then Exit; // Set the GBC volume step gbcVolumeMask := Puint16(@(registers[SOUND_DSOUND_CR]))^ and 3; if gbcVolumeMask > 2 then gbcVolumeMask := 2; // Add the accumulated sound to the sound buffer while cycles > 0 do begin Dec(cycles, audioRate); left := 0; right := 0; if registers[SOUND_ENABLED] and (1 shl 7) <> 0 then begin ////////////////////////////////////////////////////////////////////// // Sound channel 1 /////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// if sound1.enabled then begin // Advance the waveform Inc(sound1.index, audioRate); while sound1.index >= sound1.freq do begin Inc(sound1.waveIndex); Dec(sound1.index, sound1.freq); end; // If the sound is timed, advance the timer if sound1.timed then begin Dec(sound1.soundLength, audioRate); sound1.enabled := sound1.soundLength > 0; end; // Do a possible envelope shift if sound1.envRate > 0 then begin Inc(sound1.envIndex, audioRate); while sound1.envIndex >= sound1.envRate do begin if sound1.envIncrease then sound1.volume := min(sound1.volume + 1, 15) else sound1.volume := max(sound1.volume - 1, 0); Dec(sound1.envIndex, sound1.envRate); end; end; // Do a possible sweep shift if sound1.sweepRate > 0 then begin Inc(sound1.sweepIndex, audioRate); while sound1.sweepIndex >= sound1.sweepRate do begin Dec(sound1.sweepIndex, sound1.sweepRate); temp := 2048 - sound1.freq shr 7; if sound1.sweepDecrease then begin if sound1.sweepStep > 0 then begin temp := temp - temp shr sound1.sweepStep; if temp >= 0 then sound1.freq := (2048 - temp) * 128; end; end else begin temp := temp + temp shr sound1.sweepStep; if temp < 2048 then sound1.freq := (2048 - temp) * 128 else sound1.enabled := false; end; end; end; // Mix the correct wave state into the left and right composites if enableGBC1 then begin temp := SquareWaves[sound1.waveDuty and 3, sound1.waveIndex and 7]*sound1.volume; if registers[SOUND_ACTIVE] and $01 <> 0 then right := right + temp; if registers[SOUND_ACTIVE] and $10 <> 0 then left := left + temp; end; end; ////////////////////////////////////////////////////////////////////// // Sound channel 2 /////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// if sound2.enabled then begin // Advance the waveform Inc(sound2.index, audioRate); while sound2.index >= sound2.freq do begin Inc(sound2.waveIndex); Dec(sound2.index, sound2.freq); end; // If the sound is timed, advance the timer if sound2.timed then begin Dec(sound2.soundLength, audioRate); sound2.enabled := sound2.soundLength > 0; end; // Do a possible envelope shift if sound2.envRate > 0 then begin Inc(sound2.envIndex, audioRate); while sound2.envIndex >= sound2.envRate do begin if sound2.envIncrease then sound2.volume := min(sound2.volume + 1, 15) else sound2.volume := max(sound2.volume - 1, 15); Dec(sound2.envIndex, sound2.envRate); end; end; // Mix the correct wave state into the left and right composites if enableGBC2 then begin temp := SquareWaves[sound2.waveDuty and 3, sound2.waveIndex and 7] * sound2.volume; if registers[SOUND_ACTIVE] and $02 <> 0 then right := right + temp; if registers[SOUND_ACTIVE] and $20 <> 0 then left := left + temp; end; end; ////////////////////////////////////////////////////////////////////// // Sound channel 3//////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// if sound3.enabled then begin // Advance the waveform Inc(sound3.index, audioRate); while sound3.index >= sound3.freq do begin Inc(sound3.waveIndex); Dec(sound3.index, sound3.freq); if sound3.waveIndex = 32 then begin if sound3.doubleLength then sound3swapBanks; sound3.waveIndex := 0; end; end; // If the sound is timed, advance the timer if sound3.timed then begin Dec(sound3.soundLength, audioRate); sound3.enabled := sound3.soundLength > 0; end; // Grab the byte that contains the currently playing nybble temp := sound3.backBank[(sound3.waveIndex shr 1) and 15]; // Grab the correct nybble (opposite to common sense) if sound3.waveIndex and 1 <> 0 then temp := temp and $F else temp := temp shr 4; // Mix sound 3 into the left and right composites if enableGBC3 then begin // if temp and 7 <> 0 then temp := temp-16; // temp := temp - 8; // temp := (temp * sound3.volume) div 4; temp := ((temp-8) * sound3.volume) div 4; if registers[SOUND_ACTIVE] and $04 <> 0 then right := right + temp; if registers[SOUND_ACTIVE] and $40 <> 0 then left := left + temp; end; end; ////////////////////////////////////////////////////////////////////// // Sound channel 4 /////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// if sound4.enabled then begin // Advance the LFSR Inc(sound4.index, audioRate); while sound4.index >= sound4.freq do begin // Clock a 7/15 stage shift right register with the MSB being fed by an xor tap on bits 0 and 1 if sound4.use7steps then sound4.lfsr := sound4.lfsr shr 1 + (((sound4.lfsr shr 1) and 1) xor (sound4.lfsr and 1)) shl 6 else sound4.lfsr := sound4.lfsr shr 1 + (((sound4.lfsr shr 1) and 1) xor (sound4.lfsr and 1)) shl 14; Dec(sound4.index, sound4.freq); end; // If the sound is timed, advance the timer if sound4.timed then begin Dec(sound4.soundLength, audioRate); sound4.enabled := sound4.soundLength > 0; end; // Do a possible envelope shift if sound4.envRate > 0 then begin Inc(sound4.envIndex, audioRate); while sound4.envIndex >= sound4.envRate do begin if sound4.envIncrease then sound4.volume := min(sound4.volume + 1, 15) else sound4.volume := max(sound4.volume - 1, 0); Dec(sound4.envIndex, sound4.envRate); end; end; // The output of channel 4 is the LSB of the shift register // modulated by its envelope temp := (sound4.lfsr and 1)*sound4.volume; if temp = 0 then temp := -sound4.volume; if enableGBC4 then begin if registers[SOUND_ACTIVE] and $08 <> 0 then right := right + temp; if registers[SOUND_ACTIVE] and $80 <> 0 then left := left + temp; end; end; ////////////////////////////////////////////////////////////////////// // Sound channels 1 to 4 rescaling /////////////////////////////////// ////////////////////////////////////////////////////////////////////// // Convert the 4 bit output from above to a 9 bit value temp := registers[SOUND_VOLUME]; left := (left*((temp shr 4) and $7)) * (1 shl gbcVolumeMask); right := (right*(temp and $7)) * (1 shl gbcVolumeMask); ////////////////////////////////////////////////////////////////////// // Sound channel A /////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// temp := 0; t2 := 0; if soundA.enabled and enableDSA then begin temp := temp + soundA.latched * soundA.lvol; t2 := t2 + soundA.latched * soundA.rvol; end; ////////////////////////////////////////////////////////////////////// // Sound channel B /////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// if soundB.enabled and enableDSB then begin temp := temp + soundB.latched * soundB.lvol; t2 := t2 + soundB.latched * soundB.rvol; end; left := left + temp; right := right + t2; end; ////////////////////////////////////////////////////////////////////// // Stick the sample in the buffer soundBuffer[soundBufPos] := max(min(left div 2, 255), 0); soundBuffer[soundBufPos + 1] := max(min(right div 2, 255), 0); soundBufPos := (soundBufPos + 2) and BUFFER_MASK; end; end; ////////////////////////////////////////////////////////////////////// // DS FIFO management //////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// // Read a sample out of a DS FIFO procedure DequeueTDSoundRecord(var r: TDSoundRecord); var i: integer; begin if r.writeIndex > 0 then begin r.latched := r.fifo[0]; for i := 1 to 31 do r.fifo[i-1] := r.fifo[i]; Dec(r.writeIndex, 1); end; end; ////////////////////////////////////////////////////////////////////// // Add a sample to a DS FIFO procedure QueueTDSoundRecord(var r: TDSoundRecord; data: int8); begin if r.writeIndex < 32 then begin r.fifo[r.writeIndex] := data; r.writeIndex := r.writeIndex + 1; end; end; ////////////////////////////////////////////////////////////////////// // Empty a DS FIFO procedure ResetTDSoundRecord(var r: TDSoundRecord); begin r.writeIndex := 0; FillChar(r.fifo, 32, 0); end; ////////////////////////////////////////////////////////////////////// end. //////////////////////////////////////////////////////////////////////