UNPKG

wasmboy

Version:

Gameboy / Gameboy Color Emulator written for Web Assembly using AssemblyScript. Shell/Debugger in Preact

447 lines (369 loc) 17 kB
// NOTE: Tons of Copy-pasta btween channels, because Classes cannot be instantiated yet in assemblyscript // How to Search for similar things in binjgb // Wave channel trigger : APU_NR34_ADDR // Wave Channel getSample : update_wave // Wave Channel // http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Wave_Channel import { getSaveStateMemoryOffset } from '../core'; import { Sound } from './sound'; import { Cpu } from '../cpu/index'; import { eightBitLoadFromGBMemory, eightBitStoreIntoGBMemory, loadBooleanDirectlyFromWasmMemory, storeBooleanDirectlyToWasmMemory } from '../memory/index'; import { checkBitOnByte, log } from '../helpers/index'; import { i32Portable } from '../portable/portable'; export class Channel3 { // Cycle Counter for our sound accumulator static cycleCounter: i32 = 0; // Max Length of our Length Load static MAX_LENGTH: i32 = 256; // Voluntary Wave channel with 32 4-bit programmable samples, played in sequence. // NR30 -> Sound on/off (R/W) static readonly memoryLocationNRx0: i32 = 0xff1a; // E--- ---- DAC power static updateNRx0(value: i32): void { let isDacEnabled = checkBitOnByte(7, value); // Sample buffer reset to zero when powered on if (!Channel3.isDacEnabled && isDacEnabled) { Channel3.sampleBuffer = 0x00; } Channel3.isDacEnabled = isDacEnabled; // Blargg length test // Disabling DAC should disable channel immediately if (!isDacEnabled) { Channel3.isEnabled = isDacEnabled; } } // NR31 -> Sound length (R/W) static readonly memoryLocationNRx1: i32 = 0xff1b; // LLLL LLLL Length load (256-L) static NRx1LengthLoad: i32 = 0; static updateNRx1(value: i32): void { Channel3.NRx1LengthLoad = value; // Also need to set our length counter. Taken from the old, setChannelLengthCounter // Channel length is determined by 64 (or 256 if channel 3), - the length load // http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Registers // Note, this will be different for channel 3 // Supposed to be 256, so subtracting 255 and then adding 1 if that makes sense Channel3.lengthCounter = Channel3.MAX_LENGTH - Channel3.NRx1LengthLoad; } // NR32 -> Select ouput level (R/W) static readonly memoryLocationNRx2: i32 = 0xff1c; // -VV- ---- Volume code (00=0%, 01=100%, 10=50%, 11=25%) static NRx2VolumeCode: i32 = 0; static updateNRx2(value: i32): void { Channel3.NRx2VolumeCode = (value >> 5) & 0x0f; } // NR33 -> Frequency lower data (W) static readonly memoryLocationNRx3: i32 = 0xff1d; // FFFF FFFF Frequency LSB static NRx3FrequencyLSB: i32 = 0; static updateNRx3(value: i32): void { Channel3.NRx3FrequencyLSB = value; // Update Channel Frequency Channel3.frequency = (Channel3.NRx4FrequencyMSB << 8) | value; } // NR34 -> Frequency higher data (R/W) static readonly memoryLocationNRx4: i32 = 0xff1e; // TL-- -FFF Trigger, Length enable, Frequency MSB static NRx4LengthEnabled: boolean = false; static NRx4FrequencyMSB: i32 = 0; static updateNRx4(value: i32): void { // Handle our frequency // Must be done first for our upcoming trigger // To correctly reset timing let frequencyMSB = value & 0x07; Channel3.NRx4FrequencyMSB = frequencyMSB; Channel3.frequency = (frequencyMSB << 8) | Channel3.NRx3FrequencyLSB; // Obscure behavior // http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Obscure_Behavior // Also see blargg's cgb sound test // Extra length clocking occurs when writing to NRx4, // when the frame sequencer's next step is one that, // doesn't clock the length counter. let frameSequencer = Sound.frameSequencer; let doesNextFrameSequencerUpdateLength = (frameSequencer & 1) === 1; let isBeingLengthEnabled = false; if (!doesNextFrameSequencerUpdateLength) { // Check lengthEnable isBeingLengthEnabled = !Channel3.NRx4LengthEnabled && checkBitOnByte(6, value); if (Channel3.lengthCounter > 0 && isBeingLengthEnabled) { Channel3.lengthCounter -= 1; if (!checkBitOnByte(7, value) && Channel3.lengthCounter === 0) { Channel3.isEnabled = false; } } } // Set the length enabled from the value Channel3.NRx4LengthEnabled = checkBitOnByte(6, value); // Trigger our channel, unfreeze length if frozen // Triggers should happen after obscure behavior // See test 11 for trigger if (checkBitOnByte(7, value)) { Channel3.trigger(); // When we trigger on the obscure behavior, and we reset the length Counter to max // We need to clock if (!doesNextFrameSequencerUpdateLength && Channel3.lengthCounter === Channel3.MAX_LENGTH && Channel3.NRx4LengthEnabled) { Channel3.lengthCounter -= 1; } } } // Our wave table location static readonly memoryLocationWaveTable: i32 = 0xff30; // Channel Properties static readonly channelNumber: i32 = 3; static isEnabled: boolean = false; static isDacEnabled: boolean = false; static frequency: i32 = 0; static frequencyTimer: i32 = 0x00; static lengthCounter: i32 = 0x00; // WaveTable Properties static waveTablePosition: i32 = 0x00; static volumeCode: i32 = 0x00; static volumeCodeChanged: boolean = false; static sampleBuffer: i32 = 0x00; // Save States static readonly saveStateSlot: i32 = 9; // Function to save the state of the class static saveState(): void { // Cycle Counter store<i32>(getSaveStateMemoryOffset(0x00, Channel3.saveStateSlot), Channel3.cycleCounter); // NRx0 // No NRx0 Properties // NRx1 store<u16>(getSaveStateMemoryOffset(0x08, Channel3.saveStateSlot), <u16>Channel3.NRx1LengthLoad); // NRx2 store<u8>(getSaveStateMemoryOffset(0x0a, Channel3.saveStateSlot), <u8>Channel3.NRx2VolumeCode); // NRx3 store<u8>(getSaveStateMemoryOffset(0x0c, Channel3.saveStateSlot), <u8>Channel3.NRx3FrequencyLSB); // NRx4 storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x0d, Channel3.saveStateSlot), Channel3.NRx4LengthEnabled); store<u8>(getSaveStateMemoryOffset(0x0e, Channel3.saveStateSlot), <u8>Channel3.NRx4FrequencyMSB); // Channel Properties storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x0f, Channel3.saveStateSlot), Channel3.isEnabled); storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x10, Channel3.saveStateSlot), Channel3.isDacEnabled); store<i32>(getSaveStateMemoryOffset(0x11, Channel3.saveStateSlot), Channel3.frequency); store<i32>(getSaveStateMemoryOffset(0x15, Channel3.saveStateSlot), Channel3.frequencyTimer); // No Envelope store<i32>(getSaveStateMemoryOffset(0x19, Channel3.saveStateSlot), Channel3.lengthCounter); // WaveTable Properties store<i32>(getSaveStateMemoryOffset(0x21, Channel3.saveStateSlot), Channel3.waveTablePosition); store<u8>(getSaveStateMemoryOffset(0x25, Channel3.saveStateSlot), Channel3.volumeCode); storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x26, Channel3.saveStateSlot), Channel3.volumeCodeChanged); store<i32>(getSaveStateMemoryOffset(0x27, Channel3.saveStateSlot), Channel3.sampleBuffer); } // Function to load the save state from memory static loadState(): void { // Cycle Counter Channel3.cycleCounter = load<i32>(getSaveStateMemoryOffset(0x00, Channel3.cycleCounter)); // NRx0 // No NRx0 // NRx1 Channel3.NRx1LengthLoad = load<u16>(getSaveStateMemoryOffset(0x08, Channel3.saveStateSlot)); // NRx2 Channel3.NRx2VolumeCode = load<u8>(getSaveStateMemoryOffset(0x0a, Channel3.saveStateSlot)); // NRx3 Channel3.NRx3FrequencyLSB = load<u8>(getSaveStateMemoryOffset(0x0c, Channel3.saveStateSlot)); // NRx4 Channel3.NRx4LengthEnabled = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x0d, Channel3.saveStateSlot)); Channel3.NRx4FrequencyMSB = load<u8>(getSaveStateMemoryOffset(0x0e, Channel3.saveStateSlot)); // Channel Properties Channel3.isEnabled = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x0f, Channel3.saveStateSlot)); Channel3.isDacEnabled = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x10, Channel3.saveStateSlot)); Channel3.frequency = load<i32>(getSaveStateMemoryOffset(0x11, Channel3.saveStateSlot)); Channel3.frequencyTimer = load<i32>(getSaveStateMemoryOffset(0x15, Channel3.saveStateSlot)); // No Envelope Channel3.lengthCounter = load<i32>(getSaveStateMemoryOffset(0x19, Channel3.saveStateSlot)); // Wave Table Properties Channel3.waveTablePosition = load<i32>(getSaveStateMemoryOffset(0x21, Channel3.saveStateSlot)); Channel3.volumeCode = load<i32>(getSaveStateMemoryOffset(0x25, Channel3.saveStateSlot)); Channel3.volumeCodeChanged = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x26, Channel3.saveStateSlot)); Channel3.sampleBuffer = load<i32>(getSaveStateMemoryOffset(0x27, Channel3.saveStateSlot)); } // Memory Read Trap static handleWaveRamRead(): i32 { // Obscure behavior // http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware // If the wave channel is enabled, accessing any byte from $FF30-$FF3F is equivalent to, // accessing the current byte selected by the waveform position. Further, on the DMG accesses will only work in this manner, // if made within a couple of clocks of the wave channel accessing wave RAM; // if made at any other time, reads return $FF and writes have no effect. // TODO: Handle DMG case return readCurrentSampleByteFromWaveRam(); } // Memory Write Trap static handleWaveRamWrite(value: i32): void { // Obscure behavior // http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware // If the wave channel is enabled, accessing any byte from $FF30-$FF3F is equivalent to, // accessing the current byte selected by the waveform position. Further, on the DMG accesses will only work in this manner, // if made within a couple of clocks of the wave channel accessing wave RAM; // if made at any other time, reads return $FF and writes have no effect. // Thus we want to write the value to the current sample position // Will Find the position, and knock off any remainder let positionIndexToAdd = i32Portable(Channel3.waveTablePosition >> 1); let memoryLocationWaveSample = Channel3.memoryLocationWaveTable + positionIndexToAdd; eightBitStoreIntoGBMemory(memoryLocationWaveSample, value); } static initialize(): void { eightBitStoreIntoGBMemory(Channel3.memoryLocationNRx0, 0x7f); eightBitStoreIntoGBMemory(Channel3.memoryLocationNRx1, 0xff); eightBitStoreIntoGBMemory(Channel3.memoryLocationNRx2, 0x9f); eightBitStoreIntoGBMemory(Channel3.memoryLocationNRx3, 0x00); eightBitStoreIntoGBMemory(Channel3.memoryLocationNRx4, 0xb8); // The volume code changed Channel3.volumeCodeChanged = true; } // Function to get a sample using the cycle counter on the channel static getSampleFromCycleCounter(): i32 { let accumulatedCycles = Channel3.cycleCounter; Channel3.cycleCounter = 0; return Channel3.getSample(accumulatedCycles); } // Function to reset our timer, useful for GBC double speed mode static resetTimer(): void { let frequencyTimer = (2048 - Channel3.frequency) << 1; // TODO: Ensure this is correct for GBC Double Speed Mode Channel3.frequencyTimer = frequencyTimer << (<i32>Cpu.GBCDoubleSpeed); } static getSample(numberOfCycles: i32): i32 { // Check if we are enabled if (!Channel3.isEnabled || !Channel3.isDacEnabled) { // Return silence // Since range from -15 - 15, or 0 to 30 for our unsigned return 15; } // Get our volume code // Need this to compute the sample let volumeCode = Channel3.volumeCode; if (Channel3.volumeCodeChanged) { volumeCode = eightBitLoadFromGBMemory(Channel3.memoryLocationNRx2); volumeCode = volumeCode >> 5; volumeCode = volumeCode & 0x0f; Channel3.volumeCode = volumeCode; Channel3.volumeCodeChanged = false; } // Get the current sample let sample = getSampleFromSampleBufferForWaveTablePosition(); // Shift our sample and set our volume depending on the volume code // Since we can't multiply by float, simply divide by 4, 2, 1 // http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Wave_Channel let outputVolume = 0; switch (volumeCode) { case 0: sample >>= 4; break; case 1: // Dont Shift sample outputVolume = 1; break; case 2: sample >>= 1; outputVolume = 2; break; default: sample >>= 2; outputVolume = 4; break; } // Apply out output volume sample = outputVolume > 0 ? sample / outputVolume : 0; // Square Waves Can range from -15 - 15. Therefore simply add 15 sample += 15; // Update the sample based on our timer let frequencyTimer = Channel3.frequencyTimer; frequencyTimer -= numberOfCycles; while (frequencyTimer <= 0) { // Get the amount that overflowed so we don't drop cycles let overflowAmount = abs(frequencyTimer); // Reset our timer // A wave channel's frequency timer period is set to (2048-frequency) * 2. // http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Wave_Channel Channel3.resetTimer(); frequencyTimer = Channel3.frequencyTimer; frequencyTimer -= overflowAmount; // Update our sample buffer advanceWavePositionAndSampleBuffer(); } Channel3.frequencyTimer = frequencyTimer; // Finally return the sample return sample; } //http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Trigger_Event static trigger(): void { Channel3.isEnabled = true; // Length counter maximum handled by write if (Channel3.lengthCounter === 0) { Channel3.lengthCounter = Channel3.MAX_LENGTH; } // Reset our timer // A wave channel's frequency timer period is set to (2048-frequency)*2. Channel3.resetTimer(); // Add some delay to our frequency timer // So Honestly, lifted this from binjgb // https://github.com/binji/binjgb/blob/68eb4b2f6d5d7a98d270e12c4b8ff065c07f5e94/src/emulator.c#L2625 // I have no clue why this is, but it passes 09-wave read while on.s // blargg test. // I think this has to do with obscure behavior? // http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware // When triggering the wave channel, // the first sample to play is the previous one still in the high nibble of the sample buffer, // and the next sample is the second nibble from the wave table. // This is because it doesn't load the first byte on trigger like it "should". // The first nibble from the wave table is thus not played until the waveform loops. Channel3.frequencyTimer += 6; // Reset our wave table position Channel3.waveTablePosition = 0; // Finally if DAC is off, channel is still disabled if (!Channel3.isDacEnabled) { Channel3.isEnabled = false; } } // Function to determine if the current channel would update when getting the sample // This is used to accumulate samples static willChannelUpdate(numberOfCycles: i32): boolean { //Increment our cycle counter Channel3.cycleCounter += numberOfCycles; // Dac enabled status cached by accumulator return !(!Channel3.volumeCodeChanged && Channel3.frequencyTimer - Channel3.cycleCounter > 0); } static updateLength(): void { let lengthCounter = Channel3.lengthCounter; if (lengthCounter > 0 && Channel3.NRx4LengthEnabled) { lengthCounter -= 1; } if (lengthCounter === 0) { Channel3.isEnabled = false; } Channel3.lengthCounter = lengthCounter; } } // Functions specific to wave memory function advanceWavePositionAndSampleBuffer(): void { // Advance the wave table position, and loop back if needed let waveTablePosition = Channel3.waveTablePosition; waveTablePosition += 1; while (waveTablePosition >= 32) { waveTablePosition -= 32; } Channel3.waveTablePosition = waveTablePosition; // Load the next sample byte from wave ram, // into the sample buffer Channel3.sampleBuffer = readCurrentSampleByteFromWaveRam(); } function readCurrentSampleByteFromWaveRam(): i32 { // Will Find the position, and knock off any remainder let positionIndexToAdd = i32Portable(Channel3.waveTablePosition >> 1); let memoryLocationWaveSample = Channel3.memoryLocationWaveTable + positionIndexToAdd; return eightBitLoadFromGBMemory(memoryLocationWaveSample); } function getSampleFromSampleBufferForWaveTablePosition(): i32 { let sample = Channel3.sampleBuffer; // Need to grab the top or lower half for the correct sample sample >>= (<i32>((Channel3.waveTablePosition & 1) === 0)) << 2; sample &= 0x0f; return sample; }