From 6a1f7d48ceb292c05a1c1ee79b3f76a998139bd9 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Mon, 13 Oct 2025 00:05:56 +0200 Subject: [PATCH] simple music --- Firmware/sdk/apps/gameboy/src/gameboy_app.cpp | 323 ++++++++++++++++++ .../backends/desktop/src/desktop_backend.cpp | 6 +- 2 files changed, 326 insertions(+), 3 deletions(-) diff --git a/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp b/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp index eb1b2b2..765424e 100644 --- a/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp +++ b/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp @@ -1,4 +1,29 @@ #include "cardboy/apps/gameboy_app.hpp" + +uint8_t audio_read(uint16_t addr); +void audio_write(uint16_t addr, uint8_t value); + +namespace { +using AudioReadThunk = uint8_t (*)(void*, uint16_t); +using AudioWriteThunk = void (*)(void*, uint16_t, uint8_t); + +void* gAudioCtx = nullptr; +AudioReadThunk gAudioReadThunk = nullptr; +AudioWriteThunk gAudioWriteThunk = nullptr; +} // namespace + +uint8_t audio_read(uint16_t addr) { + if (gAudioReadThunk && gAudioCtx) + return gAudioReadThunk(gAudioCtx, addr); + return 0xFF; +} + +void audio_write(uint16_t addr, uint8_t value) { + if (gAudioWriteThunk && gAudioCtx) + gAudioWriteThunk(gAudioCtx, addr, value); +} + +#define ENABLE_SOUND 1 #include "cardboy/apps/peanut_gb.h" #include "cardboy/apps/menu_app.hpp" @@ -17,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -108,6 +134,22 @@ namespace { constexpr int kMenuStartY = 48; constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6; +// Compile-time toggles for APU channel mix +#ifndef GB_BUZZER_ENABLE_CH1 +#define GB_BUZZER_ENABLE_CH1 1 +#endif +#ifndef GB_BUZZER_ENABLE_CH2 +#define GB_BUZZER_ENABLE_CH2 1 +#endif +#ifndef GB_BUZZER_ENABLE_CH3 +#define GB_BUZZER_ENABLE_CH3 1 +#endif +#ifndef GB_BUZZER_ENABLE_CH4 +#define GB_BUZZER_ENABLE_CH4 1 +#endif + +class GameboyApp; + using cardboy::sdk::AppContext; using cardboy::sdk::AppEvent; @@ -176,6 +218,11 @@ public: context(ctx), framebuffer(ctx.framebuffer), filesystem(ctx.filesystem()), highResClock(ctx.highResClock()) {} void onStart() override { + ::gAudioCtx = this; + ::gAudioReadThunk = &GameboyApp::audioReadThunk; + ::gAudioWriteThunk = &GameboyApp::audioWriteThunk; + apu.attach(this); + apu.reset(); cancelTick(); frameDelayCarryUs = 0; GB_PERF_ONLY(perf.resetAll();) @@ -195,6 +242,13 @@ public: frameDelayCarryUs = 0; GB_PERF_ONLY(perf.maybePrintAggregate(true);) unloadRom(); + apu.reset(); + apu.attach(nullptr); + if (::gAudioCtx == this) { + ::gAudioCtx = nullptr; + ::gAudioReadThunk = nullptr; + ::gAudioWriteThunk = nullptr; + } } void handleEvent(const AppEvent& event) override { @@ -268,7 +322,223 @@ public: GB_PERF_ONLY(perf.maybePrintAggregate();) } + [[nodiscard]] uint8_t audioReadRegister(uint16_t addr) const { return apu.read(addr); } + void audioWriteRegister(uint16_t addr, uint8_t value) { apu.write(addr, value); } + private: +public: + class SimpleApu { + public: + void attach(GameboyApp* ownerInstance) { owner = ownerInstance; } + + void reset() { + regs.fill(0); + for (std::size_t i = 0; i < kInitialRegistersCount; ++i) + regs[i] = kInitialRegisters[i]; + for (std::size_t i = 0; i < kInitialWaveCount; ++i) + regs[kWaveOffset + i] = kInitialWave[i]; + regs[kPowerIndex] = 0x80; + enabled = true; + } + + [[nodiscard]] uint8_t read(uint16_t addr) const { + if (!inRange(addr)) + return 0xFF; + const std::size_t idx = static_cast(addr - kBaseAddr); + return static_cast(regs[idx] | kReadMask[idx]); + } + + void write(uint16_t addr, uint8_t value) { + if (!inRange(addr)) + return; + const std::size_t idx = static_cast(addr - kBaseAddr); + + if (addr == kPowerAddr) { + enabled = (value & 0x80U) != 0; + regs[idx] = static_cast(value & 0x80U); + if (!enabled) { + std::array wave{}; + for (std::size_t i = 0; i < kWaveRamSize; ++i) + wave[i] = regs[kWaveOffset + i]; + regs.fill(0); + for (std::size_t i = 0; i < kWaveRamSize; ++i) + regs[kWaveOffset + i] = wave[i]; + regs[kPowerIndex] = static_cast(value & 0x80U); + } + return; + } + + if (!enabled) { + if (addr >= kWaveBase && addr <= kWaveEnd) + regs[idx] = value; + return; + } + + regs[idx] = value; + + if ((addr == kCh1TriggerAddr && (value & 0x80U)) || (addr == kCh2TriggerAddr && (value & 0x80U))) { + const int channelIndex = (addr == kCh1TriggerAddr) ? 0 : 1; + // Reflect channel enable in NR52 status bits (no immediate beep; handled in per-frame mixer) + regs[kPowerIndex] = static_cast((regs[kPowerIndex] & 0xF0U) | 0x80U | (1U << channelIndex)); + } + } + + private: + static constexpr uint16_t kBaseAddr = 0xFF10; + static constexpr std::size_t kRegisterCount = 0x30; + static constexpr uint16_t kPowerAddr = 0xFF26; + static constexpr std::size_t kPowerIndex = static_cast(kPowerAddr - kBaseAddr); + static constexpr uint16_t kCh1LenAddr = 0xFF11; + static constexpr uint16_t kCh1EnvAddr = 0xFF12; + static constexpr uint16_t kCh1FreqLoAddr = 0xFF13; + static constexpr uint16_t kCh1TriggerAddr = 0xFF14; + static constexpr uint16_t kCh2LenAddr = 0xFF16; + static constexpr uint16_t kCh2EnvAddr = 0xFF17; + static constexpr uint16_t kCh2FreqLoAddr = 0xFF18; + static constexpr uint16_t kCh2TriggerAddr = 0xFF19; + static constexpr uint16_t kVolumeAddr = 0xFF24; + static constexpr uint16_t kRoutingAddr = 0xFF25; + static constexpr uint16_t kWaveBase = 0xFF30; + static constexpr uint16_t kWaveEnd = 0xFF3F; + static constexpr std::size_t kWaveOffset = static_cast(kWaveBase - kBaseAddr); + static constexpr std::size_t kWaveRamSize = static_cast(kWaveEnd - kWaveBase + 1); + static constexpr uint8_t kReadMask[kRegisterCount] = { + 0x80, 0x3F, 0x00, 0xFF, 0xBF, 0xFF, 0x3F, 0x00, 0xFF, 0xBF, 0x7F, 0xFF, 0x9F, 0xFF, 0xBF, 0xFF, + 0xFF, 0x00, 0x00, 0xBF, 0x00, 0x00, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::size_t kInitialRegistersCount = 0x17; + static constexpr uint8_t kInitialRegisters[kInitialRegistersCount] = { + 0x80, 0xBF, 0xF3, 0xFF, 0x3F, 0xFF, 0x3F, 0x00, 0xFF, 0x3F, 0x7F, 0xFF, + 0x9F, 0xFF, 0x3F, 0xFF, 0xFF, 0x00, 0x00, 0x3F, 0x77, 0xF3, 0xF1}; + static constexpr std::size_t kInitialWaveCount = 16; + static constexpr uint8_t kInitialWave[kInitialWaveCount] = {0xAC, 0xDD, 0xDA, 0x48, 0x36, 0x02, 0xCF, 0x16, + 0x2C, 0x04, 0xE5, 0x2C, 0xAC, 0xDD, 0xDA, 0x48}; + + GameboyApp* owner = nullptr; + std::array regs{}; + bool enabled = true; + + static constexpr bool inRange(uint16_t addr) { + return addr >= kBaseAddr && addr <= (kBaseAddr + static_cast(kRegisterCount) - 1); + } + + [[nodiscard]] uint8_t reg(uint16_t addr) const { return regs[static_cast(addr - kBaseAddr)]; } + + [[nodiscard]] double squareFrequency(int channelIndex) const { + const uint16_t freqLoAddr = (channelIndex == 0) ? kCh1FreqLoAddr : kCh2FreqLoAddr; + const uint16_t freqHiAddr = (channelIndex == 0) ? kCh1TriggerAddr : kCh2TriggerAddr; + const uint16_t raw = static_cast(((reg(freqHiAddr) & 0x07U) << 8) | reg(freqLoAddr)); + if (raw >= 2048U) + return 0.0; + const double denom = static_cast(2048U - raw); + if (denom <= 0.0) + return 0.0; + return 131072.0 / denom; + } + + // Mixer: compute best single-tone approximation for the buzzer. + // Returns true if a tone is suggested, with outFreqHz set. + public: + bool computeEffectiveTone(uint32_t& outFreqHz, uint8_t& outLoudness) const { + // Master volume and routing + const uint8_t nr50 = reg(kVolumeAddr); + const uint8_t master = static_cast(std::max(nr50 & 0x07U, (nr50 >> 4) & 0x07U)); + if (master == 0) + return false; + const uint8_t routing = reg(kRoutingAddr); + + struct Candidate { + double freq; + uint8_t loud; + int prio; + } best{0.0, 0, 0}; + +#if GB_BUZZER_ENABLE_CH1 + // CH1 square with sweep + if (reg(kPowerAddr) & 0x01U) { + const uint8_t env = reg(kCh1EnvAddr); + const uint8_t vol4 = (env >> 4) & 0x0FU; + const bool routed = ((routing & 0x11U) != 0); + if (vol4 && routed) { + const double f = squareFrequency(0); + if (std::isfinite(f) && f > 10.0) { + uint8_t loud = static_cast(vol4 * master); + if (loud > best.loud) + best = {f, loud, 3}; + } + } + } +#endif +#if GB_BUZZER_ENABLE_CH2 + // CH2 square + if (reg(kPowerAddr) & 0x02U) { + const uint8_t env = reg(kCh2EnvAddr); + const uint8_t vol4 = (env >> 4) & 0x0FU; + const bool routed = ((routing & 0x22U) != 0); + if (vol4 && routed) { + const double f = squareFrequency(1); + if (std::isfinite(f) && f > 10.0) { + uint8_t loud = static_cast(vol4 * master); + if (loud >= best.loud) + best = {f, loud, 2}; + } + } + } +#endif +#if GB_BUZZER_ENABLE_CH3 + // CH3 wave (approximate as square at its base frequency scaled by level) + if (reg(kPowerAddr) & 0x04U) { + const uint8_t nr32 = regs[0xFF1C - kBaseAddr]; + const uint8_t levelSel = (nr32 >> 5) & 0x03U; // 0=0,1=100%,2=50%,3=25% + const bool routed = ((routing & 0x44U) != 0); + if (levelSel != 0 && routed) { + const uint16_t raw = + static_cast(((reg(kCh2TriggerAddr) & 0x07U) << 8) | reg(kCh2FreqLoAddr)); + // Use wave constants: 2097152 / (2048 - N) + if (raw < 2048U) { + const double denom = static_cast(2048U - raw); + double f = 2097152.0 / denom; + if (std::isfinite(f) && f > 10.0) { + uint8_t loudBase = (levelSel == 1 ? 16 : levelSel == 2 ? 8 : 4); + uint8_t loud = static_cast(loudBase * master); + if (loud > best.loud) + best = {f, loud, 1}; + } + } + } + } +#endif +#if GB_BUZZER_ENABLE_CH4 + // CH4 noise (approximate as pitched noise -> pick a center frequency from regs) + if (reg(kPowerAddr) & 0x08U) { + const bool routed = ((routing & 0x88U) != 0); + const uint8_t env = regs[0xFF21 - kBaseAddr]; + const uint8_t vol4 = (env >> 4) & 0x0FU; + if (vol4 && routed) { + const uint8_t nr43 = regs[0xFF22 - kBaseAddr]; + const uint8_t s = (nr43 >> 4) & 0x0FU; // shift clock frequency + const uint8_t d = nr43 & 0x07U; // divider code (0->8) + static const int divLut[8] = {8, 16, 32, 48, 64, 80, 96, 112}; + const int div = divLut[d]; + double f = 4194304.0 / (static_cast(div) * std::pow(2.0, s + 1)); + // clamp to sensible buzzer range; noise sounds better around 1-3k + f = std::clamp(f, 600.0, 3200.0); + uint8_t loud = static_cast(vol4 * master); + if (loud > best.loud) + best = {f, loud, 0}; + } + } +#endif + if (best.loud == 0 || !std::isfinite(best.freq)) + return false; + // Clamp final freq to buzzer range + const double clamped = std::clamp(best.freq, 40.0, 5500.0); + outFreqHz = static_cast(clamped + 0.5); + outLoudness = best.loud; + return true; + } + }; + enum class Mode { Browse, Running }; enum class ScaleMode { Original, FullHeight, FullHeightWide }; @@ -577,6 +847,11 @@ private: uint32_t fpsCurrent = 0; std::string activeRomName; std::string activeRomSavePath; + SimpleApu apu{}; + // Smoothing state for buzzer tone + uint32_t lastFreqHz = 0; + uint8_t lastLoud = 0; + uint32_t stableFrames = 0; void cancelTick() { if (tickTimer != kInvalidAppTimer) { @@ -898,6 +1173,7 @@ private: return false; } + apu.reset(); std::memset(&gb, 0, sizeof(gb)); const auto initResult = gb_init(&gb, &GameboyApp::cartRamRead, &GameboyApp::cartRamWrite, &GameboyApp::errorCallback, this, romDataView); @@ -964,6 +1240,7 @@ private: activeRomName.clear(); activeRomSavePath.clear(); std::memset(&gb, 0, sizeof(gb)); + apu.reset(); mode = Mode::Browse; browserDirty = true; } @@ -1173,6 +1450,23 @@ private: } } + void playTone(uint32_t freqHz, uint32_t durationMs, uint32_t gapMs) { + if (freqHz == 0 || durationMs == 0) + return; + if (auto* buzzer = context.buzzer()) + buzzer->tone(freqHz, durationMs, gapMs); + } + + static uint8_t audioReadThunk(void* ctx, uint16_t addr) { + auto* self = static_cast(ctx); + return self ? self->audioReadRegister(addr) : 0xFF; + } + + static void audioWriteThunk(void* ctx, uint16_t addr, uint8_t value) { + if (auto* self = static_cast(ctx)) + self->audioWriteRegister(addr, value); + } + void setStatus(std::string message) { statusMessage = std::move(message); browserDirty = true; @@ -1394,6 +1688,35 @@ private: drawLineOriginal(*self, pixels, static_cast(line)); break; } + + // Simple per-scanline hook: at end of last line, decide tone for the frame. + if (line + 1 == LCD_HEIGHT) { + uint32_t freqHz = 0; + uint8_t loud = 0; + if (self->apu.computeEffectiveTone(freqHz, loud)) { + // Basic smoothing: if freq didn't change much, keep it; otherwise snap quickly + const uint32_t prev = self->lastFreqHz; + if (prev != 0 && freqHz != 0) { + const uint32_t diff = (prev > freqHz) ? (prev - freqHz) : (freqHz - prev); + if (diff < 15) { + freqHz = prev; // minor jitter suppression + ++self->stableFrames; + } else { + self->stableFrames = 0; + } + } else { + self->stableFrames = 0; + } + self->lastFreqHz = freqHz; + self->lastLoud = loud; + const uint32_t durMs = 17; + self->playTone(freqHz, durMs, 0); + } else { + self->lastFreqHz = 0; + self->lastLoud = 0; + // Don't enqueue anything; queue naturally drains and buzzer stops + } + } } static const char* initErrorToString(enum gb_init_error_e err) { diff --git a/Firmware/sdk/backends/desktop/src/desktop_backend.cpp b/Firmware/sdk/backends/desktop/src/desktop_backend.cpp index c14b601..761f9d3 100644 --- a/Firmware/sdk/backends/desktop/src/desktop_backend.cpp +++ b/Firmware/sdk/backends/desktop/src/desktop_backend.cpp @@ -233,7 +233,7 @@ DesktopRuntime::DesktopRuntime() : throw std::runtime_error("Failed to allocate texture for desktop framebuffer"); sprite.setTexture(texture, true); sprite.setScale(sf::Vector2f{static_cast(kPixelScale), static_cast(kPixelScale)}); - clearPixels(false); + clearPixels(true); presentIfNeeded(); services.buzzer = &buzzerService; @@ -252,7 +252,7 @@ void DesktopRuntime::setPixel(int x, int y, bool on) { if (x < 0 || y < 0 || x >= cardboy::sdk::kDisplayWidth || y >= cardboy::sdk::kDisplayHeight) return; const std::size_t idx = static_cast(y * cardboy::sdk::kDisplayWidth + x) * 4; - const std::uint8_t value = on ? static_cast(255) : static_cast(0); + const std::uint8_t value = on ? static_cast(0) : static_cast(255); pixels[idx + 0] = value; pixels[idx + 1] = value; pixels[idx + 2] = value; @@ -261,7 +261,7 @@ void DesktopRuntime::setPixel(int x, int y, bool on) { } void DesktopRuntime::clearPixels(bool on) { - const std::uint8_t value = on ? static_cast(255) : static_cast(0); + const std::uint8_t value = on ? static_cast(0) : static_cast(255); for (std::size_t i = 0; i < pixels.size(); i += 4) { pixels[i + 0] = value; pixels[i + 1] = value;