wasmboy
Version:
Gameboy / Gameboy Color Emulator written for Web Assembly using AssemblyScript. Shell/Debugger in Preact
419 lines (347 loc) • 16.7 kB
text/typescript
// NOTE: Tons of Copy-pasta btween channels, because Classes cannot be instantiated yet in assemblyscript
// Noise Channel
// http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Noise_Channel
import { getSaveStateMemoryOffset } from '../core';
import { Sound } from './sound';
import { Cpu } from '../cpu/index';
import { eightBitStoreIntoGBMemory, loadBooleanDirectlyFromWasmMemory, storeBooleanDirectlyToWasmMemory } from '../memory/index';
import { checkBitOnByte } from '../helpers/index';
export class Channel4 {
// Cycle Counter for our sound accumulator
static cycleCounter: i32 = 0;
// Max Length of our Length Load
static MAX_LENGTH: i32 = 64;
// Channel 4
// 'white noise' channel with volume envelope functions.
// Only used by register reading
static readonly memoryLocationNRx0: i32 = 0xff1f;
// NR41 -> Sound length (R/W)
static readonly memoryLocationNRx1: i32 = 0xff20;
// --LL LLLL Length load (64-L)
static NRx1LengthLoad: i32 = 0;
static updateNRx1(value: i32): void {
Channel4.NRx1LengthLoad = value & 0x3f;
// 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
Channel4.lengthCounter = Channel4.MAX_LENGTH - Channel4.NRx1LengthLoad;
}
// NR42 -> Volume Envelope (R/W)
static readonly memoryLocationNRx2: i32 = 0xff21;
// VVVV APPP Starting volume, Envelope add mode, period
static NRx2StartingVolume: i32 = 0;
static NRx2EnvelopeAddMode: boolean = false;
static NRx2EnvelopePeriod: i32 = 0;
static updateNRx2(value: i32): void {
// Handle "Zombie Mode" Obscure behavior
// https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Obscure_Behavior
if (Channel4.isEnabled) {
// If the old envelope period was zero and the envelope is still doing automatic updates,
// volume is incremented by 1, otherwise if the envelope was in subtract mode,
// volume is incremented by 2.
// NOTE: However, from my testing, it ALWAYS increments by one. This was determined
// by my testing for prehistoric man
if (Channel4.NRx2EnvelopePeriod === 0 && Channel4.isEnvelopeAutomaticUpdating) {
// Volume can't be more than 4 bits
Channel4.volume = (Channel4.volume + 1) & 0x0f;
}
// If the mode was changed (add to subtract or subtract to add),
// volume is set to 16-volume. But volume cant be more than 4 bits
if (Channel4.NRx2EnvelopeAddMode !== checkBitOnByte(3, value)) {
Channel4.volume = (16 - Channel4.volume) & 0x0f;
}
}
Channel4.NRx2StartingVolume = (value >> 4) & 0x0f;
Channel4.NRx2EnvelopeAddMode = checkBitOnByte(3, value);
Channel4.NRx2EnvelopePeriod = value & 0x07;
// Also, get our channel is dac enabled
let isDacEnabled = (value & 0xf8) > 0;
Channel4.isDacEnabled = isDacEnabled;
// Blargg length test
// Disabling DAC should disable channel immediately
if (!isDacEnabled) {
Channel4.isEnabled = isDacEnabled;
}
}
// NR43 -> Polynomial Counter (R/W)
static readonly memoryLocationNRx3: i32 = 0xff22;
// SSSS WDDD Clock shift, Width mode of LFSR, Divisor code
static NRx3ClockShift: i32 = 0;
static NRx3WidthMode: boolean = false;
static NRx3DivisorCode: i32 = 0;
static updateNRx3(value: i32): void {
let divisorCode = value & 0x07;
Channel4.NRx3ClockShift = value >> 4;
Channel4.NRx3WidthMode = checkBitOnByte(3, value);
Channel4.NRx3DivisorCode = divisorCode;
// Also, get our divisor
divisorCode <<= 1;
if (divisorCode < 1) divisorCode = 1;
Channel4.divisor = divisorCode << 3;
}
// NR44 -> Trigger, Length Enable
static readonly memoryLocationNRx4: i32 = 0xff23;
// TL-- ---- Trigger, Length enable
static NRx4LengthEnabled: boolean = false;
static updateNRx4(value: i32): void {
// 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 = !Channel4.NRx4LengthEnabled && checkBitOnByte(6, value);
if (!doesNextFrameSequencerUpdateLength) {
if (Channel4.lengthCounter > 0 && isBeingLengthEnabled) {
Channel4.lengthCounter -= 1;
if (!checkBitOnByte(7, value) && Channel4.lengthCounter === 0) {
Channel4.isEnabled = false;
}
}
}
// Set the length enabled from the value
Channel4.NRx4LengthEnabled = checkBitOnByte(6, value);
// Trigger out channel, unfreeze length if frozen
// Triggers should happen after obscure behavior
// See test 11 for trigger
if (checkBitOnByte(7, value)) {
Channel4.trigger();
// When we trigger on the obscure behavior, and we reset the length Counter to max
// We need to clock
if (!doesNextFrameSequencerUpdateLength && Channel4.lengthCounter === Channel4.MAX_LENGTH && Channel4.NRx4LengthEnabled) {
Channel4.lengthCounter -= 1;
}
}
}
// Channel Properties
static readonly channelNumber: i32 = 4;
static isEnabled: boolean = false;
static isDacEnabled: boolean = false;
static frequencyTimer: i32 = 0x00;
static envelopeCounter: i32 = 0x00;
static isEnvelopeAutomaticUpdating: boolean = false;
static lengthCounter: i32 = 0x00;
static volume: i32 = 0x00;
static divisor: i32 = 0;
// Noise properties
// NOTE: Is only 15 bits
static linearFeedbackShiftRegister: i32 = 0x00;
// Save States
static readonly saveStateSlot: i32 = 10;
// Function to save the state of the class
static saveState(): void {
// Cycle Counter
store<i32>(getSaveStateMemoryOffset(0x00, Channel4.saveStateSlot), Channel4.cycleCounter);
// NRx0
// No NRx0 Properties
// NRx1
store<u16>(getSaveStateMemoryOffset(0x04, Channel4.saveStateSlot), <u16>Channel4.NRx1LengthLoad);
// NRx2
store<u8>(getSaveStateMemoryOffset(0x06, Channel4.saveStateSlot), <u8>Channel4.NRx2StartingVolume);
storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x07, Channel4.saveStateSlot), Channel4.NRx2EnvelopeAddMode);
store<u8>(getSaveStateMemoryOffset(0x08, Channel4.saveStateSlot), <u8>Channel4.NRx2EnvelopePeriod);
// NRx3
store<u8>(getSaveStateMemoryOffset(0x09, Channel4.saveStateSlot), <u8>Channel4.NRx3ClockShift);
storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x0a, Channel4.saveStateSlot), Channel4.NRx3WidthMode);
store<u8>(getSaveStateMemoryOffset(0x0b, Channel4.saveStateSlot), <u8>Channel4.NRx3DivisorCode);
// NRx4
storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x0d, Channel4.saveStateSlot), Channel4.NRx4LengthEnabled);
// Channel Properties
storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x0f, Channel4.saveStateSlot), Channel4.isEnabled);
storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x10, Channel4.saveStateSlot), Channel4.isDacEnabled);
store<i32>(getSaveStateMemoryOffset(0x15, Channel4.saveStateSlot), Channel4.frequencyTimer);
store<i32>(getSaveStateMemoryOffset(0x19, Channel4.saveStateSlot), Channel4.envelopeCounter);
storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x1d, Channel4.saveStateSlot), Channel4.isEnvelopeAutomaticUpdating);
store<i32>(getSaveStateMemoryOffset(0x1e, Channel4.saveStateSlot), Channel4.lengthCounter);
store<i32>(getSaveStateMemoryOffset(0x22, Channel4.saveStateSlot), Channel4.volume);
// LSFR
store<u16>(getSaveStateMemoryOffset(0x26, Channel4.saveStateSlot), <u16>Channel4.linearFeedbackShiftRegister);
}
// Function to load the save state from memory
static loadState(): void {
// Cycle Counter
Channel4.cycleCounter = load<i32>(getSaveStateMemoryOffset(0x00, Channel4.cycleCounter));
// NRx0
// No NRx0
// NRx1
Channel4.NRx1LengthLoad = load<u8>(getSaveStateMemoryOffset(0x04, Channel4.saveStateSlot));
// NRx2
Channel4.NRx2StartingVolume = load<u8>(getSaveStateMemoryOffset(0x06, Channel4.saveStateSlot));
Channel4.NRx2EnvelopeAddMode = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x07, Channel4.saveStateSlot));
Channel4.NRx2EnvelopePeriod = load<u8>(getSaveStateMemoryOffset(0x08, Channel4.saveStateSlot));
// NRx3
Channel4.NRx3ClockShift = load<u8>(getSaveStateMemoryOffset(0x09, Channel4.saveStateSlot));
Channel4.NRx3WidthMode = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x0a, Channel4.saveStateSlot));
Channel4.NRx3DivisorCode = load<u8>(getSaveStateMemoryOffset(0x0b, Channel4.saveStateSlot));
// NRx4
Channel4.NRx4LengthEnabled = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x0d, Channel4.saveStateSlot));
// Channel Properties
Channel4.isEnabled = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x0f, Channel4.saveStateSlot));
Channel4.isDacEnabled = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x10, Channel4.saveStateSlot));
Channel4.frequencyTimer = load<i32>(getSaveStateMemoryOffset(0x15, Channel4.saveStateSlot));
Channel4.envelopeCounter = load<i32>(getSaveStateMemoryOffset(0x19, Channel4.saveStateSlot));
Channel4.isEnvelopeAutomaticUpdating = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x1d, Channel4.saveStateSlot));
Channel4.lengthCounter = load<i32>(getSaveStateMemoryOffset(0x1e, Channel4.saveStateSlot));
Channel4.volume = load<i32>(getSaveStateMemoryOffset(0x22, Channel4.saveStateSlot));
// LSFR
Channel4.linearFeedbackShiftRegister = load<u16>(getSaveStateMemoryOffset(0x26, Channel4.saveStateSlot));
}
static initialize(): void {
eightBitStoreIntoGBMemory(Channel4.memoryLocationNRx1 - 1, 0xff);
eightBitStoreIntoGBMemory(Channel4.memoryLocationNRx1, 0xff);
eightBitStoreIntoGBMemory(Channel4.memoryLocationNRx2, 0x00);
eightBitStoreIntoGBMemory(Channel4.memoryLocationNRx3, 0x00);
eightBitStoreIntoGBMemory(Channel4.memoryLocationNRx4, 0xbf);
}
// Function to get a sample using the cycle counter on the channel
static getSampleFromCycleCounter(): i32 {
let accumulatedCycles = Channel4.cycleCounter;
Channel4.cycleCounter = 0;
return Channel4.getSample(accumulatedCycles);
}
static getSample(numberOfCycles: i32): i32 {
// Decrement our channel timer
let frequencyTimer = Channel4.frequencyTimer;
frequencyTimer -= numberOfCycles;
// TODO: This can't be a while loop to use up all the cycles,
// Since noise is psuedo random and the period can be anything
if (frequencyTimer <= 0) {
// Get the amount that overflowed so we don't drop cycles
let overflowAmount = abs(frequencyTimer);
// Reset our timer
frequencyTimer = Channel4.getNoiseChannelFrequencyPeriod();
frequencyTimer -= overflowAmount;
// Do some cool stuff with lfsr
// http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Noise_Channel
// First XOR bit zero and one
let linearFeedbackShiftRegister = Channel4.linearFeedbackShiftRegister;
let lfsrBitZero = linearFeedbackShiftRegister & 0x01;
let lfsrBitOne = linearFeedbackShiftRegister >> 1;
lfsrBitOne = lfsrBitOne & 0x01;
let xorLfsrBitZeroOne = lfsrBitZero ^ lfsrBitOne;
// Shift all lsfr bits by one
linearFeedbackShiftRegister = linearFeedbackShiftRegister >> 1;
// Place the XOR result on bit 15
linearFeedbackShiftRegister = linearFeedbackShiftRegister | (xorLfsrBitZeroOne << 14);
// If the width mode is set, set xor on bit 6, and make lfsr 7 bit
if (Channel4.NRx3WidthMode) {
// Make 7 bit, by knocking off lower bits. Want to keeps bits 8 - 16, and then or on 7
linearFeedbackShiftRegister = linearFeedbackShiftRegister & ~0x40;
linearFeedbackShiftRegister = linearFeedbackShiftRegister | (xorLfsrBitZeroOne << 6);
}
Channel4.linearFeedbackShiftRegister = linearFeedbackShiftRegister;
}
// Make sure period never becomes negative
if (frequencyTimer < 0) {
frequencyTimer = 0;
}
Channel4.frequencyTimer = frequencyTimer;
// Get our ourput volume, set to zero for silence
let outputVolume = 0;
// Finally to set our output volume, the channel must be enabled,
// Our channel DAC must be enabled, and we must be in an active state
// Of our duty cycle
if (Channel4.isEnabled && Channel4.isDacEnabled) {
// Volume can't be more than 4 bits.
// Volume should never be more than 4 bits, but doing a check here
outputVolume = Channel4.volume & 0x0f;
} else {
// Return silence
// Since range from -15 - 15, or 0 to 30 for our unsigned
return 15;
}
// Declare our sample
let sample = 0;
// Wave form output is bit zero of lfsr, INVERTED
sample = !checkBitOnByte(0, Channel4.linearFeedbackShiftRegister) ? 1 : -1;
sample = sample * outputVolume;
// Noise Can range from -15 - 15. Therefore simply add 15
sample = sample + 15;
return <i32>sample;
}
//http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Trigger_Event
static trigger(): void {
Channel4.isEnabled = true;
// Length counter maximum handled by write
if (Channel4.lengthCounter === 0) {
Channel4.lengthCounter = Channel4.MAX_LENGTH;
}
// Reset our timers
Channel4.frequencyTimer = Channel4.getNoiseChannelFrequencyPeriod();
// The volume envelope and sweep timers treat a period of 0 as 8.
// Meaning, if the period is zero, set it to the max (8).
if (Channel4.NRx2EnvelopePeriod === 0) {
Channel4.envelopeCounter = 8;
} else {
Channel4.envelopeCounter = Channel4.NRx2EnvelopePeriod;
}
Channel4.isEnvelopeAutomaticUpdating = true;
Channel4.volume = Channel4.NRx2StartingVolume;
// Noise channel's LFSR bits are all set to 1.
Channel4.linearFeedbackShiftRegister = 0x7fff;
// Finally if DAC is off, channel is still disabled
if (!Channel4.isDacEnabled) {
Channel4.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
Channel4.cycleCounter += numberOfCycles;
// Dac enabled status cached by accumulator
return !(Channel4.frequencyTimer - Channel4.cycleCounter > 0);
}
static getNoiseChannelFrequencyPeriod(): i32 {
// Get our divisor from the divisor code, and shift by the clock shift
let response = Channel4.divisor << Channel4.NRx3ClockShift;
return response << (<i32>Cpu.GBCDoubleSpeed);
}
static updateLength(): void {
let lengthCounter = Channel4.lengthCounter;
if (lengthCounter > 0 && Channel4.NRx4LengthEnabled) {
lengthCounter -= 1;
}
if (lengthCounter === 0) {
Channel4.isEnabled = false;
}
Channel4.lengthCounter = lengthCounter;
}
static updateEnvelope(): void {
let envelopeCounter = Channel4.envelopeCounter - 1;
if (envelopeCounter <= 0) {
// Reset back to the sweep period
// Obscure behavior
// Envelopes treat a period of 0 as 8 (They reset back to the max)
if (Channel4.NRx2EnvelopePeriod === 0) {
envelopeCounter = 8;
} else {
envelopeCounter = Channel4.NRx2EnvelopePeriod;
// When the timer generates a clock and the envelope period is NOT zero, a new volume is calculated
// NOTE: There is some weiirrdd obscure behavior where zero can equal 8, so watch out for that
if (envelopeCounter !== 0 && Channel4.isEnvelopeAutomaticUpdating) {
let volume = Channel4.volume;
// Increment the volume
if (Channel4.NRx2EnvelopeAddMode) {
volume += 1;
} else {
volume -= 1;
}
// Don't allow the volume to go above 4 bits.
volume = volume & 0x0f;
// Check if we are below the max
if (volume < 15) {
Channel4.volume = volume;
} else {
Channel4.isEnvelopeAutomaticUpdating = false;
}
}
}
}
Channel4.envelopeCounter = envelopeCounter;
}
// Done!
}