diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5c7247b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,7 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..31053f6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,59 @@ +{ + "files.associations": { + "chrono": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "cstddef": "cpp", + "cstdlib": "cpp", + "cwchar": "cpp", + "deque": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "exception": "cpp", + "functional": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "string_view": "cpp", + "type_traits": "cpp", + "algorithm": "cpp", + "numeric": "cpp", + "random": "cpp", + "iosfwd": "cpp", + "limits": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "ratio": "cpp", + "stdexcept": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "system_error": "cpp", + "thread": "cpp", + "tuple": "cpp", + "typeinfo": "cpp", + "utility": "cpp", + "cctype": "cpp", + "cerrno": "cpp", + "cfloat": "cpp", + "climits": "cpp", + "clocale": "cpp", + "cstdarg": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwctype": "cpp", + "iterator": "cpp", + "initializer_list": "cpp", + "ios": "cpp", + "queue": "cpp", + "semaphore": "cpp", + "cinttypes": "cpp" + } +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..08d9005 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,28 @@ +{ + "tasks": [ + { + "type": "cppbuild", + "label": "C/C++: gcc build active file", + "command": "/usr/bin/gcc", + "args": [ + "-fdiagnostics-color=always", + "-g", + "${file}", + "-o", + "${fileDirname}/${fileBasenameNoExtension}" + ], + "options": { + "cwd": "${fileDirname}" + }, + "problemMatcher": [ + "$gcc" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "detail": "Task generated by Debugger." + } + ], + "version": "2.0.0" +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index f0ecf96..3ee4d19 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,9 +8,12 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_VERBOSE_MAKEFILE ON CACHE BOOL "ON") +find_package(SDL2 REQUIRED) + if (DEBUG) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DDEBUG") endif() add_executable(${PROJECT_NAME} src/main.cpp) +target_link_libraries(${PROJECT_NAME} SDL2::SDL2 SDL2::SDL2main) add_subdirectory(src) diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..f6fd32c --- /dev/null +++ b/run.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +current_directory=$(pwd) +last_keyword=$(basename "$current_directory") + +if [[ $last_keyword == "build" ]]; then + # Execute commands for the specified directory + echo "Executing commands for build" + echo "removing build directory" + cd .. + rm -r build + cd .. + + # Add your commands here +elif [[ $last_keyword == "gbemu" ]]; then + # Execute commands for another directory + echo "Executing commands for gbemu" + + if [[ -d "$current_directory/build" ]]; then + rm -r build + echo "removing build directory" + echo "making new build directory" + mkdir build + cd build + cmake .. + cmake --build . -j8 + ./gbemu + else + echo "making new build directory" + mkdir build + cd build + cmake .. + cmake --build . -j8 + ./gbemu + fi + + # Add your commands here +else + # Default case if no match is found + echo "No matching directory found." +fi diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2177555..4a39bb9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,7 @@ set(SOURCES gameBoy.cpp mmap.cpp graphics.cpp + audio.cpp # ------- # Header Files cpu.h @@ -12,6 +13,7 @@ set(SOURCES mmap.h types.h graphics.h + audio.h ) target_sources(${PROJECT_NAME} PRIVATE ${SOURCES}) diff --git a/src/audio.cpp b/src/audio.cpp new file mode 100644 index 0000000..61cceb3 --- /dev/null +++ b/src/audio.cpp @@ -0,0 +1,466 @@ +#include "audio.h" +#include "types.h" + +APU::APU() +{ + printf("init APU\n"); + + enabled = false; + frameSequencer = 0; + soundPann = 0; + enableVINLeft = false; + enableVINRight = false; + volumeLeft = 0; + volumeRight = 0; + + channel1 = new PulseChannel(CH1); + channel2 = new PulseChannel(CH2); + channel3 = new WaveChannel(); + channel4 = new NoiseChannel(); +} + +void APU::test() +{ + printf("APU test\n"); +} + +void APU::writeByte(Word address, Byte value) +{ + printf("APU Address: %X, Value: %X\n", address, value); + if (address == 0xFF26) + { + bool enable = (value & 0x80) >> 7; + + if (enabled && !enable) + { + clearRegisters(); + } + else if (!enabled && enable) + { + frameSequencer = 0; + } + + enabled = enable; + return; + } + else if (address >= 0xFF30 && address <= 0xFF3F) + { + // Wave Pattern RAM + channel3->writeByte(address, value); + return; + } + + else if (!enabled) + { + return; + } + + if (address >= 0xFF10 && address <= 0xFF14) + { + channel1->writeByte(address, value); + return; + } + else if (address >= 0xFF16 && address <= 0xFF19) + { + channel2->writeByte(address, value); + return; + } + else if (address >= 0xFF1A && address <= 0xFF1E) + { + channel3->writeByte(address, value); + return; + } + else if (address >= 0xFF20 && address <= 0xFF23) + { + channel4->writeByte(address, value); + return; + } + + switch (address) + { + case 0xFF24: + enableVINLeft = (value & 0x80) >> 7; + enableVINRight = (value & 0x08) >> 3; + volumeLeft = (value & 0x70) >> 4; + volumeRight = (value & 0x07); + return; + case 0xFF25: + soundPann = value; + return; + default: + return; + } +} + +Byte APU::readByte(Word address) +{ + printf("APU Address: %X\n", address); + if (address >= 0xFF10 && address <= 0xFF14) + { + return channel1->readByte(address); + } + else if (address >= 0xFF16 && address <= 0xFF19) + { + return channel2->readByte(address); + } + else if (address >= 0xFF1A && address <= 0xFF1E) + { + return channel3->readByte(address); + } + else if (address >= 0xFF20 && address <= 0xFF23) + { + return channel4->readByte(address); + } + else if (address >= 0xFF30 && address <= 0xFF3F) + { + // Wave Pattern RAM + return channel3->readByte(address); + } + switch (address) + { + case 0xFF24: + return (enableVINLeft ? 0x80 : 0) | (volumeLeft << 4) | (enableVINRight ? 0x08 : 0) | volumeRight; + + case 0xFF25: + return soundPann; + + case 0xFF26: + return (enabled ? 0x80 : 0) | (channel1->isEnabled() ? 0x01 : 0) | (channel2->isEnabled() ? 0x02 : 0) | (channel3->isEnabled() ? 0x04 : 0) | (channel4->isEnabled() ? 0x08 : 0) | 0x70; + + default: + break; + } + + return 0xFF; +} + +void APU::stepAPU(int cycles) +{ + printf("APU step\n"); +} + +void APU::clearRegisters() +{ + printf("APU clear registers\n"); + enableVINLeft = 0; + enableVINRight = 0; + volumeLeft = 0; + volumeRight = 0; + enabled = 0; + soundPann = 0; + channel1->powerOff(); + channel2->powerOff(); + channel3->powerOff(); + channel4->powerOff(); +} + +// PulseChannel + +PulseChannel::PulseChannel(Channel channel) +{ + this->channel = channel; + enabled = 0; + sweepPeriod = 0; + sweepNegate = 0; + sweepShift = 0; + waveDuty = 0; + lengthTimer = 0; + envelopeInitialVolume = 0; + envelopeIncrease = 0; + envelopePeriod = 0; + frequency = 0; + soundLengthEnable = 0; +} + +void PulseChannel::writeByte(Word address, Byte value) +{ + switch (address) + { + case 0xFF10: + // NR10 + // Sweep + if (channel == CH1) + { + sweepPeriod = (value & 0x70) >> 4; + sweepNegate = (value & 0x08) >> 3; + sweepShift = value & 0x07; + } + return; + case 0xFF11: + case 0xFF16: + // NR11 + // Sound length/Wave pattern duty + waveDuty = (value & 0xC0) >> 6; + lengthTimer = value & 0x3F; + return; + case 0xFF12: + case 0xFF17: + // NR12 + // Volume Envelope + envelopeInitialVolume = (value & 0xF0) >> 4; + envelopeIncrease = (value & 0x08) >> 3; + envelopePeriod = value & 0x07; + return; + case 0xFF13: + case 0xFF18: + // NR13 + // Frequency lo + frequency = (frequency & 0x0700) | value; + return; + case 0xFF14: + case 0xFF19: + // NR14 + // Frequency hi + frequency = (frequency & 0x00FF) | ((value & 0x07) << 8); + soundLengthEnable = (value & 0x40) >> 6; + if (value & 0x80) + { + // trigger(); + } + return; + default: + return; + } +} + +Byte PulseChannel::readByte(Word address) +{ + switch (address) + { + case 0xFF10: + // NR10 + return (sweepPeriod << 4) | (sweepNegate ? 0x08 : 0) | sweepShift | 0x80; + case 0xFF11: + case 0xFF16: + // NR11 NR21 + return (waveDuty << 6) | 0x3F; + case 0xFF12: + case 0xFF17: + // NR12 NR22 + return (envelopeInitialVolume << 4) | (envelopeIncrease ? 0x08 : 0) | envelopePeriod; + case 0xFF13: + case 0xFF18: + // NR13 NR23 + return 0xFF; + case 0xFF14: + case 0xFF19: + // NR14 NR24 + return (soundLengthEnable ? 0x40 : 0) | 0xBF; + default: + return 0xFF; + } +} + +bool PulseChannel::isEnabled() +{ + return enabled; +} + +void PulseChannel::powerOff() +{ + enabled = 0; + sweepPeriod = 0; + sweepNegate = 0; + sweepShift = 0; + waveDuty = 0; + lengthTimer = 0; + envelopeInitialVolume = 0; + envelopeIncrease = 0; + envelopePeriod = 0; + frequency = 0; + soundLengthEnable = 0; +} + +// WaveChannel + +WaveChannel::WaveChannel() +{ + dacEnabled = 0; + enabled = 0; + lengthTimer = 0; + maxLengthTimer = 0; + outputLevel = 0; + frequency = 0; + soundLengthEnable = 0; +} + +void WaveChannel::writeByte(Word address, Byte value) +{ + if (address >= 0xFF30 && address <= 0xFF3F) + { + // Wave Pattern RAM + waveRAM[address - 0xFF30] = value; + return; + } + switch (address) + { + case 0xFF1A: + // NR30 + // Sound on/off + dacEnabled = (value & 0x80) >> 7; + enabled = dacEnabled; + return; + case 0xFF1B: + // NR31 + // Sound length + lengthTimer = value; + return; + case 0xFF1C: + // NR32 + // Select output level + outputLevel = (value & 0x60) >> 5; + return; + case 0xFF1D: + // NR33 + // Frequency lo + frequency = (frequency & 0x0700) | value; + return; + case 0xFF1E: + // NR34 + // Frequency hi + frequency = (frequency & 0x00FF) | ((value & 0x07) << 8); + soundLengthEnable = (value & 0x40) >> 6; + if (value & 0x80) + { + // trigger(); + } + return; + default: + return; + } +} + +Byte WaveChannel::readByte(Word address) +{ + if (address >= 0xFF30 && address <= 0xFF3F) + { + // Wave Pattern RAM + return waveRAM[address - 0xFF30]; + } + switch (address) + { + case 0xFF1A: + // NR30 + return (dacEnabled ? 0x80 : 0) | 0x7F; + case 0xFF1B: + // NR31 + return 0xFF; + case 0xFF1C: + // NR32 + return (outputLevel << 5) | 0x9F; + case 0xFF1D: + // NR33 + return 0xFF; + case 0xFF1E: + // NR34 + return (soundLengthEnable ? 0x40 : 0) | 0xBF; + default: + return 0xFF; + } +} + +bool WaveChannel::isEnabled() +{ + return enabled; +} + +void WaveChannel::powerOff() +{ + enabled = 0; + dacEnabled = 0; + lengthTimer = 0; + outputLevel = 0; + frequency = 0; + soundLengthEnable = 0; +} + +// Noise Channel + +NoiseChannel::NoiseChannel() +{ + enabled = 0; + lengthTimer = 0; + maxLengthTimer = 0; + clockShift = 0; + LFSRWidthMode = 0; + clockDivider = 0; + LFSR = 0; + soundLengthEnable = 0; +} + +void NoiseChannel::writeByte(Word address, Byte value) +{ + switch (address) + { + case 0xFF20: + // NR41 + // Sound length + lengthTimer = value & 0x3F; + return; + case 0xFF21: + // NR42 + // Volume Envelope + envelopeInitialVolume = (value & 0xF0) >> 4; + envelopeIncrease = (value & 0x08) >> 3; + envelopePeriod = value & 0x07; + return; + case 0xFF22: + // NR43 + // Polynomial counter + clockShift = (value & 0xF0) >> 4; + LFSRWidthMode = (value & 0x08) >> 3; + clockDivider = value & 0x07; + return; + case 0xFF23: + // NR44 + // Counter/consecutive; initial + soundLengthEnable = (value & 0x40) >> 6; + if (value & 0x80) + { + // trigger(); + } + return; + default: + return; + } +} + +Byte NoiseChannel::readByte(Word address) +{ + switch (address) + { + case 0xFF20: + // NR41 + return 0xFF; + case 0xFF21: + // NR42 + return (envelopeInitialVolume << 4) | (envelopeIncrease ? 0x08 : 0) | envelopePeriod; + case 0xFF22: + // NR43 + return (clockShift << 4) | (LFSRWidthMode ? 0x08 : 0) | clockDivider; + case 0xFF23: + // NR44 + return (soundLengthEnable ? 0x40 : 0) | 0xBF; + default: + return 0xFF; + } +} + +bool NoiseChannel::isEnabled() +{ + return enabled; +} + +void NoiseChannel::powerOff() +{ + enabled = 0; + lengthTimer = 0; + envelopeInitialVolume = 0; + envelopeIncrease = 0; + envelopePeriod = 0; + clockShift = 0; + LFSRWidthMode = 0; + clockDivider = 0; + LFSR = 0; + soundLengthEnable = 0; +} \ No newline at end of file diff --git a/src/audio.h b/src/audio.h new file mode 100644 index 0000000..e97ce80 --- /dev/null +++ b/src/audio.h @@ -0,0 +1,155 @@ +#pragma once +#include "types.h" +#include +#include + +enum Channel +{ + CH1 = 0, + CH2 = 1, + CH3 = 2, + CH4 = 3 +}; + +class PulseChannel +{ +private: + Channel channel; + bool enabled; + bool dacEnabled; + + Byte sweepPeriod; + bool sweepNegate; + Byte sweepShift; + + // NRx1 + Byte waveDuty; + int lengthTimer; + + Byte envelopeInitialVolume; + bool envelopeIncrease; + Byte envelopePeriod; + + int frequency; + + bool soundLengthEnable; + +public: + PulseChannel(Channel channel); + void test(); + void writeByte(Word address, Byte value); + Byte readByte(Word address); + bool isEnabled(); + void powerOff(); +}; + +class WaveChannel +{ +private: + + Byte waveRAM[16]; + bool dacEnabled; + bool enabled; + + int lengthTimer; + int maxLengthTimer; + + Byte outputLevel; + + int frequency; + + bool soundLengthEnable; + +public: + WaveChannel(); + void test(); + void writeByte(Word address, Byte value); + Byte readByte(Word address); + void trigger(); + bool isEnabled(); + void powerOff(); +}; + +class NoiseChannel +{ +private: + bool enabled; + bool dacEnabled; + int lengthTimer; + int maxLengthTimer; + + Byte envelopeInitialVolume; + bool envelopeIncrease; + Byte envelopePeriod; + + // NRx3 + Byte clockShift; + bool LFSRWidthMode; + Byte clockDivider; + Word LFSR; + + Byte dividerTable[8] = { 8, 16, 32, 48, 64, 80, 96, 112 }; + + // NRx4 + // bool trigger; + bool soundLengthEnable; + +public: + NoiseChannel(); + void test(); + void writeByte(Word address, Byte value); + Byte readByte(Word address); + void trigger(); + bool isEnabled(); + void powerOff(); +}; + +class APU +{ +private: + // SDL Audio + // https://documentation.help/SDL/guideaudioexamples.html + SDL_AudioSpec wanted, obtained; + SDL_AudioDeviceID audioDeviceID; + + static Uint8* audio_chunk; + static Uint32 audio_len; + static Uint8* audio_pos; + + // Gets an audio sample every 95 clock cycles. + // clockSpeed/sampleRate ~ 95 + int sampleCounter; + + // This updates the frame sequencer at 512Hz. + // Must be reset after every 8192 clock cycles. + int frameSequencerCounter; + int frameSequencer; + + // Buffer + unsigned int bufferSize = 4096; + unsigned int bufferIndex = 0; + float buffer[4096] = { 0 }; + + bool enabled; + + Byte soundPann; + + bool enableVINLeft; + bool enableVINRight; + Byte volumeLeft; + Byte volumeRight; + + // Audio Channels + PulseChannel* channel1; + PulseChannel* channel2; + WaveChannel* channel3; + NoiseChannel* channel4; + +public: + APU(); + void test(); + void writeByte(Word address, Byte value); + Byte readByte(Word address); + void stepAPU(int cycles); + void clearRegisters(); +}; \ No newline at end of file diff --git a/src/gameBoy.cpp b/src/gameBoy.cpp index cc7c7e9..7ac19a2 100644 --- a/src/gameBoy.cpp +++ b/src/gameBoy.cpp @@ -15,6 +15,9 @@ GBE::GBE() // Initialize the Graphics gbe_graphics = new PPU(); + // audio = new Audio(); + // APU = new APU(); + // Unify the CPU and MemoryMap gbe_cpu->setMemory(gbe_mMap); @@ -30,8 +33,12 @@ GBE::GBE() if ((bootROM = fopen("../src/dmg_boot.gb", "rb")) == NULL) printf("boot rom file not opened"); + // // Open the Game ROM + // if ((gameROM = fopen("../tests/tetris.gb", "rb")) == NULL) + // printf("game rom file not opened"); + // Open the Game ROM - if ((gameROM = fopen("../tests/halt_bug.gb", "rb")) == NULL) + if ((gameROM = fopen("../tests/dmg_sound/rom_singles/02-len ctr.gb", "rb")) == NULL) printf("game rom file not opened"); // Set the Boot ROM @@ -115,6 +122,7 @@ void GBE::update() // update the DIV and TIMA timers gbe_cpu->updateTimers(s_Cycles); gbe_graphics->executePPU(s_Cycles); + // gbe_mMap->audio->stepAPU(s_Cycles); s_Cycles = 0; s_Cycles += gbe_cpu->performInterrupt(); gbe_graphics->pollEvents(); diff --git a/src/mmap.cpp b/src/mmap.cpp index 25a4315..6d71288 100644 --- a/src/mmap.cpp +++ b/src/mmap.cpp @@ -104,6 +104,8 @@ MemoryMap::MemoryMap() bootRomFile = nullptr; romFile = nullptr; + audio = new APU(); + mbcMode = 0x0; } @@ -172,8 +174,11 @@ bool MemoryMap::writeMemory(Word address, Byte value) { readInput(value); } - //if (value != 0xFF) - //printf("0x%02x\n", ioPorts[0]);} + // Write to Audio Registers + else if (address >= 0xFF10 && address <= 0xFF3F) + { + audio->writeByte(address, value); + } else ioPorts[address - 0xFF00] = value; } @@ -246,8 +251,14 @@ Byte MemoryMap::readMemory(Word address) } else if (address < 0xFF80) { + // Read from Audio Registers + if (address >= 0xFF10 && address <= 0xFF3F) + { + return audio->readByte(address); + } // Read from I/O Ports - return ioPorts[address - 0xFF00]; + else + return ioPorts[address - 0xFF00]; } else if (address < 0xFFFF) { diff --git a/src/mmap.h b/src/mmap.h index 09c29e2..7756c80 100644 --- a/src/mmap.h +++ b/src/mmap.h @@ -1,6 +1,7 @@ #pragma once #include "types.h" #include +#include "audio.h" // The Memory Map for GBE // Pulled from https://gbdev.io/pandocs/Memory_Map.html @@ -138,8 +139,12 @@ class MemoryMap Byte* reg_WX; public: + // Audio Unit + // I know this is not the best way to do it + // But I am not sure how to do it better + APU* audio; + Byte* joyPadState; - // Constructor MemoryMap();