mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
simple music
This commit is contained in:
@@ -1,4 +1,29 @@
|
|||||||
#include "cardboy/apps/gameboy_app.hpp"
|
#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/peanut_gb.h"
|
||||||
|
|
||||||
#include "cardboy/apps/menu_app.hpp"
|
#include "cardboy/apps/menu_app.hpp"
|
||||||
@@ -17,6 +42,7 @@
|
|||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -108,6 +134,22 @@ namespace {
|
|||||||
constexpr int kMenuStartY = 48;
|
constexpr int kMenuStartY = 48;
|
||||||
constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6;
|
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::AppContext;
|
||||||
using cardboy::sdk::AppEvent;
|
using cardboy::sdk::AppEvent;
|
||||||
@@ -176,6 +218,11 @@ public:
|
|||||||
context(ctx), framebuffer(ctx.framebuffer), filesystem(ctx.filesystem()), highResClock(ctx.highResClock()) {}
|
context(ctx), framebuffer(ctx.framebuffer), filesystem(ctx.filesystem()), highResClock(ctx.highResClock()) {}
|
||||||
|
|
||||||
void onStart() override {
|
void onStart() override {
|
||||||
|
::gAudioCtx = this;
|
||||||
|
::gAudioReadThunk = &GameboyApp::audioReadThunk;
|
||||||
|
::gAudioWriteThunk = &GameboyApp::audioWriteThunk;
|
||||||
|
apu.attach(this);
|
||||||
|
apu.reset();
|
||||||
cancelTick();
|
cancelTick();
|
||||||
frameDelayCarryUs = 0;
|
frameDelayCarryUs = 0;
|
||||||
GB_PERF_ONLY(perf.resetAll();)
|
GB_PERF_ONLY(perf.resetAll();)
|
||||||
@@ -195,6 +242,13 @@ public:
|
|||||||
frameDelayCarryUs = 0;
|
frameDelayCarryUs = 0;
|
||||||
GB_PERF_ONLY(perf.maybePrintAggregate(true);)
|
GB_PERF_ONLY(perf.maybePrintAggregate(true);)
|
||||||
unloadRom();
|
unloadRom();
|
||||||
|
apu.reset();
|
||||||
|
apu.attach(nullptr);
|
||||||
|
if (::gAudioCtx == this) {
|
||||||
|
::gAudioCtx = nullptr;
|
||||||
|
::gAudioReadThunk = nullptr;
|
||||||
|
::gAudioWriteThunk = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleEvent(const AppEvent& event) override {
|
void handleEvent(const AppEvent& event) override {
|
||||||
@@ -268,7 +322,223 @@ public:
|
|||||||
GB_PERF_ONLY(perf.maybePrintAggregate();)
|
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:
|
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<std::size_t>(addr - kBaseAddr);
|
||||||
|
return static_cast<uint8_t>(regs[idx] | kReadMask[idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void write(uint16_t addr, uint8_t value) {
|
||||||
|
if (!inRange(addr))
|
||||||
|
return;
|
||||||
|
const std::size_t idx = static_cast<std::size_t>(addr - kBaseAddr);
|
||||||
|
|
||||||
|
if (addr == kPowerAddr) {
|
||||||
|
enabled = (value & 0x80U) != 0;
|
||||||
|
regs[idx] = static_cast<uint8_t>(value & 0x80U);
|
||||||
|
if (!enabled) {
|
||||||
|
std::array<uint8_t, kWaveRamSize> 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<uint8_t>(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<uint8_t>((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<std::size_t>(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<std::size_t>(kWaveBase - kBaseAddr);
|
||||||
|
static constexpr std::size_t kWaveRamSize = static_cast<std::size_t>(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<uint8_t, kRegisterCount> regs{};
|
||||||
|
bool enabled = true;
|
||||||
|
|
||||||
|
static constexpr bool inRange(uint16_t addr) {
|
||||||
|
return addr >= kBaseAddr && addr <= (kBaseAddr + static_cast<uint16_t>(kRegisterCount) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] uint8_t reg(uint16_t addr) const { return regs[static_cast<std::size_t>(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<uint16_t>(((reg(freqHiAddr) & 0x07U) << 8) | reg(freqLoAddr));
|
||||||
|
if (raw >= 2048U)
|
||||||
|
return 0.0;
|
||||||
|
const double denom = static_cast<double>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint16_t>(((reg(kCh2TriggerAddr) & 0x07U) << 8) | reg(kCh2FreqLoAddr));
|
||||||
|
// Use wave constants: 2097152 / (2048 - N)
|
||||||
|
if (raw < 2048U) {
|
||||||
|
const double denom = static_cast<double>(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<uint8_t>(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<double>(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<uint8_t>(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<uint32_t>(clamped + 0.5);
|
||||||
|
outLoudness = best.loud;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
enum class Mode { Browse, Running };
|
enum class Mode { Browse, Running };
|
||||||
enum class ScaleMode { Original, FullHeight, FullHeightWide };
|
enum class ScaleMode { Original, FullHeight, FullHeightWide };
|
||||||
|
|
||||||
@@ -577,6 +847,11 @@ private:
|
|||||||
uint32_t fpsCurrent = 0;
|
uint32_t fpsCurrent = 0;
|
||||||
std::string activeRomName;
|
std::string activeRomName;
|
||||||
std::string activeRomSavePath;
|
std::string activeRomSavePath;
|
||||||
|
SimpleApu apu{};
|
||||||
|
// Smoothing state for buzzer tone
|
||||||
|
uint32_t lastFreqHz = 0;
|
||||||
|
uint8_t lastLoud = 0;
|
||||||
|
uint32_t stableFrames = 0;
|
||||||
|
|
||||||
void cancelTick() {
|
void cancelTick() {
|
||||||
if (tickTimer != kInvalidAppTimer) {
|
if (tickTimer != kInvalidAppTimer) {
|
||||||
@@ -898,6 +1173,7 @@ private:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apu.reset();
|
||||||
std::memset(&gb, 0, sizeof(gb));
|
std::memset(&gb, 0, sizeof(gb));
|
||||||
const auto initResult = gb_init(&gb, &GameboyApp::cartRamRead, &GameboyApp::cartRamWrite,
|
const auto initResult = gb_init(&gb, &GameboyApp::cartRamRead, &GameboyApp::cartRamWrite,
|
||||||
&GameboyApp::errorCallback, this, romDataView);
|
&GameboyApp::errorCallback, this, romDataView);
|
||||||
@@ -964,6 +1240,7 @@ private:
|
|||||||
activeRomName.clear();
|
activeRomName.clear();
|
||||||
activeRomSavePath.clear();
|
activeRomSavePath.clear();
|
||||||
std::memset(&gb, 0, sizeof(gb));
|
std::memset(&gb, 0, sizeof(gb));
|
||||||
|
apu.reset();
|
||||||
mode = Mode::Browse;
|
mode = Mode::Browse;
|
||||||
browserDirty = true;
|
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<GameboyApp*>(ctx);
|
||||||
|
return self ? self->audioReadRegister(addr) : 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void audioWriteThunk(void* ctx, uint16_t addr, uint8_t value) {
|
||||||
|
if (auto* self = static_cast<GameboyApp*>(ctx))
|
||||||
|
self->audioWriteRegister(addr, value);
|
||||||
|
}
|
||||||
|
|
||||||
void setStatus(std::string message) {
|
void setStatus(std::string message) {
|
||||||
statusMessage = std::move(message);
|
statusMessage = std::move(message);
|
||||||
browserDirty = true;
|
browserDirty = true;
|
||||||
@@ -1394,6 +1688,35 @@ private:
|
|||||||
drawLineOriginal(*self, pixels, static_cast<int>(line));
|
drawLineOriginal(*self, pixels, static_cast<int>(line));
|
||||||
break;
|
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) {
|
static const char* initErrorToString(enum gb_init_error_e err) {
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ DesktopRuntime::DesktopRuntime() :
|
|||||||
throw std::runtime_error("Failed to allocate texture for desktop framebuffer");
|
throw std::runtime_error("Failed to allocate texture for desktop framebuffer");
|
||||||
sprite.setTexture(texture, true);
|
sprite.setTexture(texture, true);
|
||||||
sprite.setScale(sf::Vector2f{static_cast<float>(kPixelScale), static_cast<float>(kPixelScale)});
|
sprite.setScale(sf::Vector2f{static_cast<float>(kPixelScale), static_cast<float>(kPixelScale)});
|
||||||
clearPixels(false);
|
clearPixels(true);
|
||||||
presentIfNeeded();
|
presentIfNeeded();
|
||||||
|
|
||||||
services.buzzer = &buzzerService;
|
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)
|
if (x < 0 || y < 0 || x >= cardboy::sdk::kDisplayWidth || y >= cardboy::sdk::kDisplayHeight)
|
||||||
return;
|
return;
|
||||||
const std::size_t idx = static_cast<std::size_t>(y * cardboy::sdk::kDisplayWidth + x) * 4;
|
const std::size_t idx = static_cast<std::size_t>(y * cardboy::sdk::kDisplayWidth + x) * 4;
|
||||||
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
|
const std::uint8_t value = on ? static_cast<std::uint8_t>(0) : static_cast<std::uint8_t>(255);
|
||||||
pixels[idx + 0] = value;
|
pixels[idx + 0] = value;
|
||||||
pixels[idx + 1] = value;
|
pixels[idx + 1] = value;
|
||||||
pixels[idx + 2] = value;
|
pixels[idx + 2] = value;
|
||||||
@@ -261,7 +261,7 @@ void DesktopRuntime::setPixel(int x, int y, bool on) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DesktopRuntime::clearPixels(bool on) {
|
void DesktopRuntime::clearPixels(bool on) {
|
||||||
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
|
const std::uint8_t value = on ? static_cast<std::uint8_t>(0) : static_cast<std::uint8_t>(255);
|
||||||
for (std::size_t i = 0; i < pixels.size(); i += 4) {
|
for (std::size_t i = 0; i < pixels.size(); i += 4) {
|
||||||
pixels[i + 0] = value;
|
pixels[i + 0] = value;
|
||||||
pixels[i + 1] = value;
|
pixels[i + 1] = value;
|
||||||
|
|||||||
Reference in New Issue
Block a user