mirror of
https://github.com/pelgraine/Meck.git
synced 2026-10-30 19:52:01 +01:00
GBC sound (T-Deck Max), mesh stays live in-game, fast waveform at 12 frames, wrap fix
SOUND (Max, ES8311; Pro stays silent)
- minigb_apu vendored from Meck-P4 (MIT), run at 22050 Hz: at 44100 it
cost 3.6-4.0 ms of the 16.7 ms frame and the game fell to 0.98x, which
drained the DMA queue and glitched. Per-frame timings (core / apu /
i2s wait) now print with the speed line.
- Bring-up reuses the notification-tone path: shared Audio object owns
I2S port 0 (with MCLK), XL9555 routing and amp, then codec init after
the clocks are running. Raw PCM written per frame; a repeated sample
every sixth frame keeps production a hair above the DAC so its clock
paces the game instead of the queue running dry.
- Software gain 1/8 (codec DAC sits at 0 dB); GBC_AUDIO_SHIFT to tune.
- 950 ms delay line (PSRAM) so sound lands with the picture, which
reaches the glass ~1 s after its frame; GBC_AUDIO_DELAY_MS to tune.
- Games start muted; mic key toggles the codec mute in both keyboard
drivers' raw mode; footer shows the current hint. Amp off on quit.
- Notification tones skipped while a game runs (game owns the codec).
MESH LIVE DURING PLAY
- The OTA-style radio pause is gone (GBC_PAUSE_RADIO=1 restores it):
the mesh runs on the other core and already tolerates the panel
blocking the loop. Game header now matches the home screen's -- the
cumulative unread channel count where the node name goes, clock,
battery percent. "New:" toasts suppressed only while a ROM is running
(Snake and Minesweeper keep them).
DISPLAY
- Fast register waveform set to 12 frames per push (FAST_LUT_FRAMES):
judged the best balance of UI speed and contrast. Interface only; the
game keeps the factory waveform.
- Windowed game refresh dropped: measured to save nothing, and it left
the footer row unrefreshed. Normal partial refresh, change-detected.
- Adafruit GFX auto-wrap turned off in the e-ink driver (all e-ink
builds): it dropped a glyph onto the next line's margin when a line
ran a few pixels past the edge (seen with Larger Noto in compose).
Compose wrap width also allows for the driver's 2-unit x offset.
- Game start does one full refresh (tried as a ghosting fix; NPC
ghosting in Pokemon Gold is the game + dither + factory partial
waveform and shows on the stock build too).
Files: examples/companion_radio/{main.cpp, ui-new/GBCEmulatorScreen.{h,cpp},
ui-new/UITask.cpp, ui-new/minigb_apu.{c,h}}, src/helpers/ui/GxEPDDisplay.cpp,
src/helpers/ui/GxEPD2_310_GDEQ031T10_FAST.cpp,
variants/lilygo_tdeck_{max,pro}/TCA8418Keyboard.h
This commit is contained in:
1 parent
1bad37d1ad
commit
5a911619f7
7 files changed
+959
-23
No files matched your search
@@ -3171,6 +3171,14 @@ void loop() {
|
||||
bool audioBusy = (alarmScr && alarmScr->isRinging()) ||
|
||||
(alarmScr && alarmScr->isAlarmAudioActive()) ||
|
||||
(abPlayer && abPlayer->isAudioActive());
|
||||
#if defined(LilyGo_TDeck_Pro)
|
||||
// A running Game Boy game owns the codec and I2S port: skip the tone
|
||||
// (vibration and the toast still happen).
|
||||
{
|
||||
GBCEmulatorScreen* gbcScr = (GBCEmulatorScreen*)ui_task.getGBCScreen();
|
||||
if (gbcScr && gbcScr->isRunning()) audioBusy = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!audioBusy && file[0] != '\0') {
|
||||
// Lazy-init Audio object
|
||||
|
||||
@@ -36,10 +36,7 @@
|
||||
// and copies the 6480-byte result into the shared snapshot buffer inside a
|
||||
// short critical section. render() copies the snapshot out under the same
|
||||
// lock and draws it with GxEPDDisplay::drawXbmRaw, whose CRC tracking means
|
||||
// the panel only refreshes when the picture actually changed. The first
|
||||
// picture after launch is a normal full-screen partial refresh (it has
|
||||
// to clear the ROM list); every picture after that refreshes only the
|
||||
// 216-line game window, so the panel moves less ink per picture. When a game
|
||||
// the panel only refreshes when the picture actually changed. When a game
|
||||
// quits the next refresh is a full one (the black/white flash) so the
|
||||
// residue partial refreshes leave behind does not follow the user back
|
||||
// to the ROM list and home screen. There is no periodic full refresh
|
||||
@@ -58,11 +55,12 @@
|
||||
// while a game runs a GxEPD2 busy callback keeps draining the keyboard
|
||||
// through the refresh, so input never waits for the panel.
|
||||
//
|
||||
// - While a ROM runs the LoRa radio is put into standby and the mesh loop
|
||||
// paused, reusing the OTA update's otaPauseRadio()/otaResumeRadio(). That
|
||||
// keeps the shared SPI bus (LoRa, e-ink, SD) free for the panel and the
|
||||
// save file, and keeps the mesh loop out of the frame budget. The node
|
||||
// receives nothing while a game is running.
|
||||
// - The LoRa radio and mesh loop keep running while a ROM runs (the mesh
|
||||
// is on the other core, and the loop already lives with the panel
|
||||
// blocking it during every refresh). Notification tones are suppressed
|
||||
// while a game runs because the game owns the codec; toasts, buzzer and
|
||||
// vibration behave as usual. GBC_PAUSE_RADIO=1 restores the OTA-style
|
||||
// pause that earlier builds used.
|
||||
//
|
||||
// - Saves: raw cart RAM as a .sav sidecar next to the ROM, size from the
|
||||
// cartridge header (MBC2 carts declare 0 but carry 512 bytes). Loaded
|
||||
@@ -88,7 +86,15 @@
|
||||
// driver back to the factory OTP waveform (dither needs its drive) and
|
||||
// restores fast on quit.
|
||||
//
|
||||
// - No sound yet (build 3).
|
||||
// - Sound (T-Deck Max): minigb_apu (MIT, vendored, patched to 44100 Hz as
|
||||
// in Meck-P4, at 22050 Hz here) is fed by the core's audio_read/
|
||||
// audio_write hooks; once per emulated frame the task renders 369 stereo
|
||||
// samples, passes them through a GBC_AUDIO_DELAY_MS delay line so they
|
||||
// land when the picture does, and writes them to the I2S driver the
|
||||
// shared Audio object installed on port 0, after the
|
||||
// ES8311 has been brought up the way the notification tones do it. Games
|
||||
// start muted; the mic key toggles the codec's hard mute. The Pro runs
|
||||
// silent for now.
|
||||
//
|
||||
// - Telemetry: the task prints a speed line every 5 s (frames in the
|
||||
// window, fps, and the cumulative average against 59.73) so whether the
|
||||
@@ -99,6 +105,7 @@
|
||||
|
||||
#include "GBCEmulatorScreen.h"
|
||||
#include "UITask.h"
|
||||
#include "../MyMesh.h" // the_mesh.getRTCClock() for the header clock
|
||||
#include <helpers/ui/GxEPDDisplay.h>
|
||||
#include "TCA8418Keyboard.h"
|
||||
|
||||
@@ -113,13 +120,39 @@
|
||||
#include <strings.h>
|
||||
#include <stdio.h>
|
||||
|
||||
// Sound: T-Deck Max (ES8311 codec) only in this build. The Pro's PCM5102A
|
||||
// path is different and stays silent for now.
|
||||
#if defined(HAS_ES8311_AUDIO)
|
||||
#define GBC_SOUND 1
|
||||
#else
|
||||
#define GBC_SOUND 0
|
||||
#endif
|
||||
|
||||
#if GBC_SOUND
|
||||
// The APU declarations must precede the core: with ENABLE_SOUND the core
|
||||
// calls audio_read/audio_write directly. minigb_apu.h has no C++ linkage
|
||||
// guard of its own, and minigb_apu.c is compiled as C.
|
||||
extern "C" {
|
||||
#include "minigb_apu.h"
|
||||
}
|
||||
#include "variant.h" // BOARD_I2S_* pins
|
||||
#include "target.h" // board (amplifier off on quit)
|
||||
#include "Audio.h" // ESP32-audioI2S: owns the I2S driver on port 0
|
||||
#include "ES8311.h" // es8311_set_mute
|
||||
#include "driver/i2s.h" // i2s_write / i2s_set_sample_rates on that driver
|
||||
extern Audio* audio; // main.cpp: shared Audio object
|
||||
extern void meck_audio_route_amp(); // target.cpp: AUDIO_SEL -> ES8311, amp on
|
||||
extern void meck_audio_codec_init(); // target.cpp: ES8311 register init, once per boot
|
||||
#endif
|
||||
|
||||
#define PEANUT_FULL_GBC_SUPPORT 1
|
||||
#define ENABLE_LCD 1
|
||||
#define ENABLE_SOUND 0
|
||||
#define ENABLE_SOUND GBC_SOUND
|
||||
#include "peanut_gb.h"
|
||||
|
||||
// ---- Firmware hooks ---------------------------------------------------------
|
||||
extern TCA8418Keyboard keyboard; // main.cpp
|
||||
extern MyMesh the_mesh; // main.cpp
|
||||
#if defined(LilyGo_TDeck_Pro_Max)
|
||||
extern void toggleKeyboardBacklight(); // main.cpp (both-shifts chord, Max keyboard backlight)
|
||||
#endif
|
||||
@@ -145,6 +178,12 @@ extern void otaResumeRadio(); // main.cpp
|
||||
#define GBC_SAVE_QUIET_MS 2000 // cart RAM untouched this long -> write .sav
|
||||
#define GBC_STOP_WAIT_MS 200 // max wait for the task to park on quit
|
||||
#define GBC_TASK_STACK 8192 // bytes, static, reserved at boot
|
||||
// The LoRa radio and mesh loop keep running while a game plays (they live on
|
||||
// the other core; the loop already tolerates the panel blocking it during
|
||||
// every refresh). Set GBC_PAUSE_RADIO=1 to restore the OTA-style pause.
|
||||
#ifndef GBC_PAUSE_RADIO
|
||||
#define GBC_PAUSE_RADIO 0
|
||||
#endif
|
||||
#define GBC_TASK_PRIO 2
|
||||
#define GBC_TASK_CORE 0
|
||||
|
||||
@@ -176,6 +215,29 @@ static volatile bool s_kbd_bl_req = false; // both-shifts chord seen by t
|
||||
static volatile unsigned long s_frames = 0;
|
||||
static unsigned s_pictures = 0; // pictures drawn this game (first one is full-screen)
|
||||
static TaskHandle_t s_task = NULL;
|
||||
#if GBC_SOUND
|
||||
// One emulated frame of stereo s16 APU output: 44100/59.73 = 738 sample pairs.
|
||||
#define GBC_AUDIO_FRAME_BYTES (AUDIO_SAMPLES * 2 * sizeof(int16_t))
|
||||
// Software gain: the codec's DAC sits at 0 dB (audiobooks scale their own
|
||||
// samples first), so the APU's full-scale output is divided by 2^SHIFT.
|
||||
#ifndef GBC_AUDIO_SHIFT
|
||||
#define GBC_AUDIO_SHIFT 3 // /8, about -18 dB
|
||||
#endif
|
||||
static bool s_audio_on = false; // codec and I2S taken for this game
|
||||
static int16_t *s_audio_buf = NULL; // internal RAM (+1 stereo pair for the pad)
|
||||
static bool s_muted = false;
|
||||
// Delay line between the APU and the codec so sound lines up with a picture
|
||||
// that reaches the glass ~0.7-1 s after the frame it shows. PSRAM ring of
|
||||
// stereo pairs; sequential access only.
|
||||
#ifndef GBC_AUDIO_DELAY_MS
|
||||
#define GBC_AUDIO_DELAY_MS 950 // tuned by ear on the Max
|
||||
#endif
|
||||
#define GBC_DELAY_PAIRS ((uint32_t)AUDIO_SAMPLE_RATE * GBC_AUDIO_DELAY_MS / 1000)
|
||||
#define GBC_RING_PAIRS (GBC_DELAY_PAIRS + 4 * AUDIO_SAMPLES)
|
||||
static int16_t *s_delay_ring = NULL; // PSRAM, GBC_RING_PAIRS stereo pairs
|
||||
static uint32_t s_ring_w = 0, s_ring_r = 0, s_ring_fill = 0;
|
||||
#endif
|
||||
static volatile uint32_t s_us_core = 0, s_us_apu = 0, s_us_i2s = 0; // per-window totals (all builds)
|
||||
static StackType_t s_task_stack[GBC_TASK_STACK / sizeof(StackType_t)]; // .bss, internal RAM
|
||||
static StaticTask_t s_task_tcb;
|
||||
|
||||
@@ -285,7 +347,52 @@ static void gbc_task(void *arg) {
|
||||
while (!s_stop) {
|
||||
// Peanut-GB's joypad register is active-low.
|
||||
s_gb->direct.joypad = (uint8_t)~keyboard.rawJoypad();
|
||||
const int64_t tc0 = esp_timer_get_time();
|
||||
gb_run_frame(s_gb);
|
||||
const int64_t tc1 = esp_timer_get_time();
|
||||
s_us_core += (uint32_t)(tc1 - tc0);
|
||||
#if GBC_SOUND
|
||||
if (s_audio_on) {
|
||||
// This frame's APU output, scaled down, through the delay line, to the
|
||||
// codec. The APU renders a fixed 369 stereo pairs per frame at 22050
|
||||
// (= 22040/s, a shade under the DAC's 22050/s), so every sixth frame
|
||||
// the last pair is sent twice: 369.17 per frame keeps the DMA queue
|
||||
// (~370 ms at this rate) full, and i2s_write then blocks briefly each
|
||||
// frame -- the audio clock pacing the game at exactly 1.00x rather
|
||||
// than the queue draining into gaps.
|
||||
audio_callback(NULL, (uint8_t*)s_audio_buf, (int)GBC_AUDIO_FRAME_BYTES);
|
||||
for (unsigned i = 0; i < AUDIO_SAMPLES * 2; i++) s_audio_buf[i] >>= GBC_AUDIO_SHIFT;
|
||||
uint32_t pairs = AUDIO_SAMPLES;
|
||||
if ((s_frames % 6) == 0) {
|
||||
s_audio_buf[AUDIO_SAMPLES * 2] = s_audio_buf[AUDIO_SAMPLES * 2 - 2];
|
||||
s_audio_buf[AUDIO_SAMPLES * 2 + 1] = s_audio_buf[AUDIO_SAMPLES * 2 - 1];
|
||||
pairs++;
|
||||
}
|
||||
// Push this frame into the ring, pop the same number of pairs from
|
||||
// GBC_AUDIO_DELAY_MS ago (silence until the ring has filled that far).
|
||||
for (uint32_t i = 0; i < pairs; i++) {
|
||||
s_delay_ring[s_ring_w * 2] = s_audio_buf[i * 2];
|
||||
s_delay_ring[s_ring_w * 2 + 1] = s_audio_buf[i * 2 + 1];
|
||||
if (++s_ring_w >= GBC_RING_PAIRS) s_ring_w = 0;
|
||||
}
|
||||
s_ring_fill += pairs;
|
||||
if (s_ring_fill >= GBC_DELAY_PAIRS + pairs) {
|
||||
for (uint32_t i = 0; i < pairs; i++) {
|
||||
s_audio_buf[i * 2] = s_delay_ring[s_ring_r * 2];
|
||||
s_audio_buf[i * 2 + 1] = s_delay_ring[s_ring_r * 2 + 1];
|
||||
if (++s_ring_r >= GBC_RING_PAIRS) s_ring_r = 0;
|
||||
}
|
||||
s_ring_fill -= pairs;
|
||||
} else {
|
||||
memset(s_audio_buf, 0, pairs * 2 * sizeof(int16_t));
|
||||
}
|
||||
const int64_t ta1 = esp_timer_get_time();
|
||||
s_us_apu += (uint32_t)(ta1 - tc1);
|
||||
size_t put = 0;
|
||||
i2s_write(I2S_NUM_0, s_audio_buf, pairs * 2 * sizeof(int16_t), &put, pdMS_TO_TICKS(100));
|
||||
s_us_i2s += (uint32_t)(esp_timer_get_time() - ta1);
|
||||
}
|
||||
#endif
|
||||
s_frames = s_frames + 1;
|
||||
f_win++;
|
||||
{
|
||||
@@ -293,8 +400,12 @@ static void gbc_task(void *arg) {
|
||||
if (t - t_win >= 5000000) {
|
||||
const float win_fps = (float)f_win * 1000000.0f / (float)(t - t_win);
|
||||
const float avg_fps = (float)s_frames * 1000000.0f / (float)(t - t_start);
|
||||
Serial.printf("[GBC] speed: %.1f fps last 5 s, %.1f fps average (%.2fx real-time)\n",
|
||||
win_fps, avg_fps, avg_fps / 59.73f);
|
||||
Serial.printf("[GBC] speed: %.1f fps last 5 s, %.1f fps average (%.2fx real-time); per frame: core %lu us, apu %lu us, i2s wait %lu us\n",
|
||||
win_fps, avg_fps, avg_fps / 59.73f,
|
||||
(unsigned long)(f_win ? s_us_core / f_win : 0),
|
||||
(unsigned long)(f_win ? s_us_apu / f_win : 0),
|
||||
(unsigned long)(f_win ? s_us_i2s / f_win : 0));
|
||||
s_us_core = s_us_apu = s_us_i2s = 0;
|
||||
t_win = t;
|
||||
f_win = 0;
|
||||
}
|
||||
@@ -575,9 +686,54 @@ bool GBCEmulatorScreen::launch(int idx) {
|
||||
Serial.printf("[GBC] cgbMode=%d mbc=%d rom=%u bytes save=%u bytes\n",
|
||||
(int)s_gb->cgb.cgbMode, (int)s_gb->mbc, (unsigned)s_rom_size, (unsigned)s_save_size);
|
||||
|
||||
#if GBC_SOUND
|
||||
// Sound. Same bring-up the notification tones and audiobooks use, minus
|
||||
// the decoder: the shared Audio object installs and clocks the I2S driver
|
||||
// on port 0 (with MCLK, which the ES8311 needs), routing and amp via the
|
||||
// XL9555, then -- with the clocks already running, which the codec insists
|
||||
// on -- the ES8311 register init. The game then writes raw stereo PCM into
|
||||
// that driver every frame. If any step fails the game runs silent.
|
||||
s_audio_on = false;
|
||||
// Internal RAM: the APU makes four read-modify-write passes over this
|
||||
// buffer per frame, which PSRAM handles badly. One extra stereo pair for
|
||||
// the every-third-frame pad.
|
||||
if (!s_audio_buf) s_audio_buf = (int16_t*)heap_caps_malloc(GBC_AUDIO_FRAME_BYTES + 2 * sizeof(int16_t),
|
||||
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
|
||||
if (!s_audio_buf) s_audio_buf = (int16_t*)heap_caps_malloc(GBC_AUDIO_FRAME_BYTES + 2 * sizeof(int16_t),
|
||||
MALLOC_CAP_SPIRAM);
|
||||
if (!s_delay_ring) s_delay_ring = (int16_t*)heap_caps_malloc(GBC_RING_PAIRS * 2 * sizeof(int16_t), MALLOC_CAP_SPIRAM);
|
||||
if (s_delay_ring) { s_ring_w = s_ring_r = s_ring_fill = 0; memset(s_delay_ring, 0, GBC_RING_PAIRS * 2 * sizeof(int16_t)); }
|
||||
if (s_audio_buf && s_delay_ring) {
|
||||
if (!audio) audio = new Audio();
|
||||
audio->stopSong(); // an audiobook or tone in progress loses the codec
|
||||
meck_audio_route_amp();
|
||||
bool ok = audio->setPinout(BOARD_I2S_BCLK, BOARD_I2S_LRC, BOARD_I2S_DOUT,
|
||||
I2S_PIN_NO_CHANGE, BOARD_I2S_MCLK);
|
||||
if (ok) {
|
||||
i2s_set_sample_rates(I2S_NUM_0, AUDIO_SAMPLE_RATE);
|
||||
memset(s_audio_buf, 0, GBC_AUDIO_FRAME_BYTES);
|
||||
for (int i = 0; i < 12; i++) { // ~200 ms of silence: clocks live, no pop
|
||||
size_t put = 0;
|
||||
i2s_write(I2S_NUM_0, s_audio_buf, GBC_AUDIO_FRAME_BYTES, &put, pdMS_TO_TICKS(100));
|
||||
}
|
||||
meck_audio_codec_init();
|
||||
es8311_set_mute(true); // games start muted; the mic key unmutes
|
||||
s_muted = true;
|
||||
audio_init(); // reset the APU
|
||||
s_audio_on = true;
|
||||
Serial.printf("[GBC] audio on (muted): %u Hz, %u samples/frame, gain 1/%u, delay %u ms, mic key = unmute\n",
|
||||
(unsigned)AUDIO_SAMPLE_RATE, (unsigned)AUDIO_SAMPLES, 1u << GBC_AUDIO_SHIFT, (unsigned)GBC_AUDIO_DELAY_MS);
|
||||
} else {
|
||||
Serial.println("[GBC] audio: setPinout failed -- running silent");
|
||||
}
|
||||
} else {
|
||||
Serial.println("[GBC] audio buffer alloc failed -- running silent");
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---- Hand the keyboard and the radio over, then start the task ----
|
||||
keyboard.setRawJoypad(true);
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
#if GBC_PAUSE_RADIO && defined(MECK_OTA_UPDATE)
|
||||
otaPauseRadio();
|
||||
#endif
|
||||
// The dithered game picture needs the factory waveform's longer drive;
|
||||
@@ -604,7 +760,7 @@ bool GBCEmulatorScreen::launch(int idx) {
|
||||
if (s_task == NULL) {
|
||||
Serial.println("[GBC] task create failed");
|
||||
keyboard.setRawJoypad(false);
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
#if GBC_PAUSE_RADIO && defined(MECK_OTA_UPDATE)
|
||||
otaResumeRadio();
|
||||
#endif
|
||||
gbc_release_core();
|
||||
@@ -631,10 +787,19 @@ void GBCEmulatorScreen::finishStop() {
|
||||
// navigation afterwards -- one impatient Q became four screens of "back".
|
||||
_releaseKbAfterDraw = true;
|
||||
_browserDrawn = false;
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
#if GBC_PAUSE_RADIO && defined(MECK_OTA_UPDATE)
|
||||
otaResumeRadio();
|
||||
#endif
|
||||
s_rom_size = 0; // arena retained
|
||||
#if GBC_SOUND
|
||||
if (s_audio_on) {
|
||||
s_audio_on = false;
|
||||
es8311_set_mute(false); // leave the codec unmuted for the next user
|
||||
i2s_zero_dma_buffer(I2S_NUM_0);
|
||||
board.amplifierDisable(); // the next tone or book re-enables it
|
||||
Serial.println("[GBC] audio off");
|
||||
}
|
||||
#endif
|
||||
gbc_release_core();
|
||||
_playing = -1;
|
||||
_mode = BROWSER;
|
||||
@@ -671,6 +836,15 @@ void GBCEmulatorScreen::poll() {
|
||||
s_cram_dirty = false;
|
||||
gbc_write_save();
|
||||
}
|
||||
#if GBC_SOUND
|
||||
if (keyboard.rawMutePressed() && s_audio_on) {
|
||||
s_muted = !s_muted;
|
||||
es8311_set_mute(s_muted);
|
||||
Serial.printf("[GBC] %s\n", s_muted ? "muted" : "unmuted");
|
||||
}
|
||||
#else
|
||||
keyboard.rawMutePressed(); // consume; no sound on this build
|
||||
#endif
|
||||
if (keyboard.rawExitPressed() || s_stop) {
|
||||
s_stop = true;
|
||||
_mode = STOPPING;
|
||||
@@ -734,6 +908,65 @@ bool GBCEmulatorScreen::handleInput(char c) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Game-screen header -----------------------------------------------------
|
||||
// The home screen's header, with the cumulative unread channel-message count
|
||||
// ("3 Unread", the same figure as the home screen's MSG strip) where the
|
||||
// node name goes, since the mesh keeps running during a game. Same text
|
||||
// size, colours and row (HOME_HDR_Y = -3 on the T-Deck) as the home screen;
|
||||
// battery as text-only percent at the right, which is what the home screen
|
||||
// shows for the Larger and custom-font styles; clock centred and kept
|
||||
// clear of the label, shown only once the time is valid.
|
||||
static void draw_game_header(DisplayDriver& display, UITask* task) {
|
||||
NodePrefs* prefs = task->getNodePrefs();
|
||||
const int hdrY = -3;
|
||||
char label[16];
|
||||
snprintf(label, sizeof(label), "%d Unread", task->getUnreadMsgCount());
|
||||
|
||||
display.setTextSize(prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, hdrY);
|
||||
display.print(label);
|
||||
|
||||
uint8_t pct = 0;
|
||||
#if HAS_BQ27220
|
||||
pct = task->getBatteryPercent();
|
||||
#else
|
||||
{
|
||||
const uint16_t mv = task->getBattMilliVolts();
|
||||
if (mv > 0) {
|
||||
int p = ((int)mv - 3000) * 100 / (4200 - 3000);
|
||||
if (p < 0) p = 0;
|
||||
if (p > 100) p = 100;
|
||||
pct = (uint8_t)p;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
char pctStr[6];
|
||||
snprintf(pctStr, sizeof(pctStr), "%d%%", (int)pct);
|
||||
const uint16_t pw = display.getTextWidth(pctStr);
|
||||
display.setCursor(display.width() - pw - 2, hdrY);
|
||||
display.print(pctStr);
|
||||
|
||||
const uint32_t now = the_mesh.getRTCClock()->getCurrentTime();
|
||||
if (now > 1700000000) {
|
||||
// now is past 2023 and the offset is at most +/-14 h, so local cannot
|
||||
// go negative; unsigned keeps the arithmetic simple.
|
||||
const uint32_t local = now + (uint32_t)((int32_t)prefs->utc_offset_hours * 3600);
|
||||
const unsigned hrs = (unsigned)((local / 3600) % 24);
|
||||
const unsigned mins = (unsigned)((local / 60) % 60);
|
||||
char timeBuf[8];
|
||||
snprintf(timeBuf, sizeof(timeBuf), "%02u:%02u", hrs, mins);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
const uint16_t tw = display.getTextWidth(timeBuf);
|
||||
int clockX = (display.width() - tw) / 2;
|
||||
const int labelRight = display.getTextWidth(label) + 4;
|
||||
if (clockX < labelRight) clockX = labelRight;
|
||||
display.setCursor(clockX, hdrY);
|
||||
display.print(timeBuf);
|
||||
}
|
||||
display.setTextSize(1);
|
||||
}
|
||||
|
||||
// ---- Render -----------------------------------------------------------------
|
||||
int GBCEmulatorScreen::render(DisplayDriver& display) {
|
||||
if (_mode == BROWSER || _mode == LOADING) return renderBrowser(display);
|
||||
@@ -808,6 +1041,7 @@ int GBCEmulatorScreen::renderGame(DisplayDriver& display) {
|
||||
_busyHooked = true;
|
||||
}
|
||||
display.startFrame();
|
||||
draw_game_header(display, _task);
|
||||
|
||||
// Ask the emulator task for a fresh picture for next time, and draw the
|
||||
// one it prepared last time.
|
||||
@@ -818,11 +1052,19 @@ int GBCEmulatorScreen::renderGame(DisplayDriver& display) {
|
||||
|
||||
const uint16_t fg = d.rawFgColor();
|
||||
d.drawXbmRaw(GBC_IMG_X, GBC_IMG_Y, s_mono_ui, OUT_W, OUT_H, fg);
|
||||
#if GBC_SOUND
|
||||
d.drawTextRaw(2, 308, (_mode == STOPPING) ? "Saving..."
|
||||
: (!s_audio_on ? "Q: Quit"
|
||||
: (s_muted ? "Q: Quit Press Mic key to unmute" : "Q: Quit Mic: Mute")), fg);
|
||||
#else
|
||||
d.drawTextRaw(2, 308, (_mode == STOPPING) ? "Saving..." : "Q: Quit", fg);
|
||||
if (_mode == PLAYING) {
|
||||
if (s_pictures > 0) d.requestWindowRefresh(GBC_IMG_X, GBC_IMG_Y, OUT_W, OUT_H);
|
||||
s_pictures++;
|
||||
}
|
||||
#endif
|
||||
// No windowed refresh: it was measured to save nothing (the panel's
|
||||
// waveform time is the same for any window), and it left the footer row
|
||||
// outside the window, so the mute hint never reached the glass. The
|
||||
// driver's normal partial refresh covers the whole panel and skips
|
||||
// refreshes when the frame has not changed.
|
||||
if (_mode == PLAYING) s_pictures++;
|
||||
return 100; // the UI loop's 800 ms e-ink floor sets the real cadence
|
||||
}
|
||||
|
||||
|
||||
@@ -1918,7 +1918,15 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
||||
// Don't interrupt user with popup - just show brief notification
|
||||
// Messages are stored in channel history, accessible via tile/key
|
||||
// Suppress toasts for room server messages (bulk sync would spam toasts)
|
||||
if (!isOnRepeaterAdmin() && !isRoomMsg && !suppressNotif) {
|
||||
#if defined(LilyGo_TDeck_Pro)
|
||||
// No toasts over a running Game Boy game either: on a busy mesh they would
|
||||
// be constant. The header on that screen shows the unread count instead.
|
||||
// Snake and Minesweeper are other screens and keep their toasts.
|
||||
const bool gbcPlaying = isOnGBCScreen() && ((GBCEmulatorScreen*)gbc_screen)->isRunning();
|
||||
#else
|
||||
const bool gbcPlaying = false;
|
||||
#endif
|
||||
if (!isOnRepeaterAdmin() && !isRoomMsg && !suppressNotif && !gbcPlaying) {
|
||||
char alertBuf[40];
|
||||
snprintf(alertBuf, sizeof(alertBuf), "New: %s", from_name);
|
||||
showAlert(alertBuf, 2000);
|
||||
|
||||
@@ -0,0 +1,605 @@
|
||||
/**
|
||||
* minigb_apu is released under the terms listed within the LICENSE file.
|
||||
*
|
||||
* minigb_apu emulates the audio processing unit (APU) of the Game Boy. This
|
||||
* project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "minigb_apu.h"
|
||||
|
||||
#define DMG_CLOCK_FREQ_U ((unsigned)DMG_CLOCK_FREQ)
|
||||
#define AUDIO_NSAMPLES (AUDIO_SAMPLES * 2u)
|
||||
|
||||
#define AUDIO_MEM_SIZE (0xFF3F - 0xFF10 + 1)
|
||||
#define AUDIO_ADDR_COMPENSATION 0xFF10
|
||||
|
||||
#define MAX(a, b) ( a > b ? a : b )
|
||||
#define MIN(a, b) ( a <= b ? a : b )
|
||||
|
||||
#define VOL_INIT_MAX (INT16_MAX/8)
|
||||
#define VOL_INIT_MIN (INT16_MIN/8)
|
||||
|
||||
/* Handles time keeping for sound generation.
|
||||
* FREQ_INC_REF must be equal to, or larger than AUDIO_SAMPLE_RATE in order
|
||||
* to avoid a division by zero error.
|
||||
* Using a square of 2 simplifies calculations. */
|
||||
#define FREQ_INC_REF (AUDIO_SAMPLE_RATE * 16)
|
||||
|
||||
#define MAX_CHAN_VOLUME 15
|
||||
|
||||
/**
|
||||
* Memory holding audio registers between 0xFF10 and 0xFF3F inclusive.
|
||||
*/
|
||||
static uint8_t audio_mem[AUDIO_MEM_SIZE];
|
||||
|
||||
struct chan_len_ctr {
|
||||
uint8_t load;
|
||||
unsigned enabled : 1;
|
||||
uint32_t counter;
|
||||
uint32_t inc;
|
||||
};
|
||||
|
||||
struct chan_vol_env {
|
||||
uint8_t step;
|
||||
unsigned up : 1;
|
||||
uint32_t counter;
|
||||
uint32_t inc;
|
||||
};
|
||||
|
||||
struct chan_freq_sweep {
|
||||
uint16_t freq;
|
||||
uint8_t rate;
|
||||
uint8_t shift;
|
||||
unsigned up : 1;
|
||||
uint32_t counter;
|
||||
uint32_t inc;
|
||||
};
|
||||
|
||||
static struct chan {
|
||||
unsigned enabled : 1;
|
||||
unsigned powered : 1;
|
||||
unsigned on_left : 1;
|
||||
unsigned on_right : 1;
|
||||
unsigned muted : 1;
|
||||
|
||||
uint8_t volume;
|
||||
uint8_t volume_init;
|
||||
|
||||
uint16_t freq;
|
||||
uint32_t freq_counter;
|
||||
uint32_t freq_inc;
|
||||
|
||||
int_fast16_t val;
|
||||
|
||||
struct chan_len_ctr len;
|
||||
struct chan_vol_env env;
|
||||
struct chan_freq_sweep sweep;
|
||||
|
||||
union {
|
||||
struct {
|
||||
uint8_t duty;
|
||||
uint8_t duty_counter;
|
||||
} square;
|
||||
struct {
|
||||
uint16_t lfsr_reg;
|
||||
uint8_t lfsr_wide;
|
||||
uint8_t lfsr_div;
|
||||
} noise;
|
||||
struct {
|
||||
uint8_t sample;
|
||||
} wave;
|
||||
};
|
||||
} chans[4];
|
||||
|
||||
static int32_t vol_l, vol_r;
|
||||
|
||||
static void set_note_freq(struct chan *c, const uint32_t freq)
|
||||
{
|
||||
/* Lowest expected value of freq is 64. */
|
||||
c->freq_inc = freq * (uint32_t)(FREQ_INC_REF / AUDIO_SAMPLE_RATE);
|
||||
}
|
||||
|
||||
static void chan_enable(const uint_fast8_t i, const bool enable)
|
||||
{
|
||||
uint8_t val;
|
||||
|
||||
chans[i].enabled = enable;
|
||||
val = (audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] & 0x80) |
|
||||
(chans[3].enabled << 3) | (chans[2].enabled << 2) |
|
||||
(chans[1].enabled << 1) | (chans[0].enabled << 0);
|
||||
|
||||
audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] = val;
|
||||
//audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] |= 0x80 | ((uint8_t)enable) << i;
|
||||
}
|
||||
|
||||
static void update_env(struct chan *c)
|
||||
{
|
||||
c->env.counter += c->env.inc;
|
||||
|
||||
while (c->env.counter > FREQ_INC_REF) {
|
||||
if (c->env.step) {
|
||||
c->volume += c->env.up ? 1 : -1;
|
||||
if (c->volume == 0 || c->volume == MAX_CHAN_VOLUME) {
|
||||
c->env.inc = 0;
|
||||
}
|
||||
c->volume = MAX(0, MIN(MAX_CHAN_VOLUME, c->volume));
|
||||
}
|
||||
c->env.counter -= FREQ_INC_REF;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_len(struct chan *c)
|
||||
{
|
||||
if (!c->len.enabled)
|
||||
return;
|
||||
|
||||
c->len.counter += c->len.inc;
|
||||
if (c->len.counter > FREQ_INC_REF) {
|
||||
chan_enable(c - chans, 0);
|
||||
c->len.counter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static bool update_freq(struct chan *c, uint32_t *pos)
|
||||
{
|
||||
uint32_t inc = c->freq_inc - *pos;
|
||||
c->freq_counter += inc;
|
||||
|
||||
if (c->freq_counter > FREQ_INC_REF) {
|
||||
*pos = c->freq_inc - (c->freq_counter - FREQ_INC_REF);
|
||||
c->freq_counter = 0;
|
||||
return true;
|
||||
} else {
|
||||
*pos = c->freq_inc;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_sweep(struct chan *c)
|
||||
{
|
||||
c->sweep.counter += c->sweep.inc;
|
||||
|
||||
while (c->sweep.counter > FREQ_INC_REF) {
|
||||
if (c->sweep.shift) {
|
||||
uint16_t inc = (c->sweep.freq >> c->sweep.shift);
|
||||
if (!c->sweep.up)
|
||||
inc *= -1;
|
||||
|
||||
c->freq += inc;
|
||||
if (c->freq > 2047) {
|
||||
c->enabled = 0;
|
||||
} else {
|
||||
set_note_freq(c,
|
||||
DMG_CLOCK_FREQ_U / ((2048 - c->freq)<< 5));
|
||||
c->freq_inc *= 8;
|
||||
}
|
||||
} else if (c->sweep.rate) {
|
||||
c->enabled = 0;
|
||||
}
|
||||
c->sweep.counter -= FREQ_INC_REF;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_square(int16_t* samples, const bool ch2)
|
||||
{
|
||||
uint32_t freq;
|
||||
struct chan* c = chans + ch2;
|
||||
|
||||
if (!c->powered || !c->enabled)
|
||||
return;
|
||||
|
||||
freq = DMG_CLOCK_FREQ_U / ((2048 - c->freq) << 5);
|
||||
set_note_freq(c, freq);
|
||||
c->freq_inc *= 8;
|
||||
|
||||
for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) {
|
||||
update_len(c);
|
||||
|
||||
if (!c->enabled)
|
||||
continue;
|
||||
|
||||
update_env(c);
|
||||
if (!ch2)
|
||||
update_sweep(c);
|
||||
|
||||
uint32_t pos = 0;
|
||||
uint32_t prev_pos = 0;
|
||||
int32_t sample = 0;
|
||||
|
||||
while (update_freq(c, &pos)) {
|
||||
c->square.duty_counter = (c->square.duty_counter + 1) & 7;
|
||||
sample += ((pos - prev_pos) / c->freq_inc) * c->val;
|
||||
c->val = (c->square.duty & (1 << c->square.duty_counter)) ?
|
||||
VOL_INIT_MAX / MAX_CHAN_VOLUME :
|
||||
VOL_INIT_MIN / MAX_CHAN_VOLUME;
|
||||
prev_pos = pos;
|
||||
}
|
||||
|
||||
if (c->muted)
|
||||
continue;
|
||||
|
||||
sample += c->val;
|
||||
sample *= c->volume;
|
||||
sample /= 4;
|
||||
|
||||
samples[i + 0] += sample * c->on_left * vol_l;
|
||||
samples[i + 1] += sample * c->on_right * vol_r;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t wave_sample(const unsigned int pos, const unsigned int volume)
|
||||
{
|
||||
uint8_t sample;
|
||||
|
||||
sample = audio_mem[(0xFF30 + pos / 2) - AUDIO_ADDR_COMPENSATION];
|
||||
if (pos & 1) {
|
||||
sample &= 0xF;
|
||||
} else {
|
||||
sample >>= 4;
|
||||
}
|
||||
return volume ? (sample >> (volume - 1)) : 0;
|
||||
}
|
||||
|
||||
static void update_wave(int16_t *samples)
|
||||
{
|
||||
uint32_t freq;
|
||||
struct chan *c = chans + 2;
|
||||
|
||||
if (!c->powered || !c->enabled)
|
||||
return;
|
||||
|
||||
freq = (DMG_CLOCK_FREQ_U / 64) / (2048 - c->freq);
|
||||
set_note_freq(c, freq);
|
||||
|
||||
c->freq_inc *= 32;
|
||||
|
||||
for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) {
|
||||
update_len(c);
|
||||
|
||||
if (!c->enabled)
|
||||
continue;
|
||||
|
||||
uint32_t pos = 0;
|
||||
uint32_t prev_pos = 0;
|
||||
int32_t sample = 0;
|
||||
|
||||
c->wave.sample = wave_sample(c->val, c->volume);
|
||||
|
||||
while (update_freq(c, &pos)) {
|
||||
c->val = (c->val + 1) & 31;
|
||||
sample += ((pos - prev_pos) / c->freq_inc) *
|
||||
((int)c->wave.sample - 8) * (INT16_MAX/64);
|
||||
c->wave.sample = wave_sample(c->val, c->volume);
|
||||
prev_pos = pos;
|
||||
}
|
||||
|
||||
sample += ((int)c->wave.sample - 8) * (int)(INT16_MAX/64);
|
||||
|
||||
if (c->volume == 0)
|
||||
continue;
|
||||
|
||||
{
|
||||
/* First element is unused. */
|
||||
int16_t div[] = { INT16_MAX, 1, 2, 4 };
|
||||
sample = sample / (div[c->volume]);
|
||||
}
|
||||
|
||||
if (c->muted)
|
||||
continue;
|
||||
|
||||
sample /= 4;
|
||||
|
||||
samples[i + 0] += sample * c->on_left * vol_l;
|
||||
samples[i + 1] += sample * c->on_right * vol_r;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_noise(int16_t *samples)
|
||||
{
|
||||
struct chan *c = chans + 3;
|
||||
|
||||
if (!c->powered)
|
||||
return;
|
||||
|
||||
{
|
||||
const uint32_t lfsr_div_lut[] = {
|
||||
8, 16, 32, 48, 64, 80, 96, 112
|
||||
};
|
||||
uint32_t freq;
|
||||
|
||||
freq = DMG_CLOCK_FREQ_U / (lfsr_div_lut[c->noise.lfsr_div] << c->freq);
|
||||
set_note_freq(c, freq);
|
||||
}
|
||||
|
||||
if (c->freq >= 14)
|
||||
c->enabled = 0;
|
||||
|
||||
for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) {
|
||||
update_len(c);
|
||||
|
||||
if (!c->enabled)
|
||||
continue;
|
||||
|
||||
update_env(c);
|
||||
|
||||
uint32_t pos = 0;
|
||||
uint32_t prev_pos = 0;
|
||||
int32_t sample = 0;
|
||||
|
||||
while (update_freq(c, &pos)) {
|
||||
c->noise.lfsr_reg = (c->noise.lfsr_reg << 1) |
|
||||
(c->val >= VOL_INIT_MAX/MAX_CHAN_VOLUME);
|
||||
|
||||
if (c->noise.lfsr_wide) {
|
||||
c->val = !(((c->noise.lfsr_reg >> 14) & 1) ^
|
||||
((c->noise.lfsr_reg >> 13) & 1)) ?
|
||||
VOL_INIT_MAX / MAX_CHAN_VOLUME :
|
||||
VOL_INIT_MIN / MAX_CHAN_VOLUME;
|
||||
} else {
|
||||
c->val = !(((c->noise.lfsr_reg >> 6) & 1) ^
|
||||
((c->noise.lfsr_reg >> 5) & 1)) ?
|
||||
VOL_INIT_MAX / MAX_CHAN_VOLUME :
|
||||
VOL_INIT_MIN / MAX_CHAN_VOLUME;
|
||||
}
|
||||
|
||||
sample += ((pos - prev_pos) / c->freq_inc) * c->val;
|
||||
prev_pos = pos;
|
||||
}
|
||||
|
||||
if (c->muted)
|
||||
continue;
|
||||
|
||||
sample += c->val;
|
||||
sample *= c->volume;
|
||||
sample /= 4;
|
||||
|
||||
samples[i + 0] += sample * c->on_left * vol_l;
|
||||
samples[i + 1] += sample * c->on_right * vol_r;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SDL2 style audio callback function.
|
||||
*/
|
||||
void audio_callback(void *userdata, uint8_t *stream, int len)
|
||||
{
|
||||
int16_t *samples = (int16_t *)stream;
|
||||
|
||||
/* Appease unused variable warning. */
|
||||
(void)userdata;
|
||||
|
||||
memset(stream, 0, len);
|
||||
|
||||
update_square(samples, 0);
|
||||
update_square(samples, 1);
|
||||
update_wave(samples);
|
||||
update_noise(samples);
|
||||
}
|
||||
|
||||
static void chan_trigger(uint_fast8_t i)
|
||||
{
|
||||
struct chan *c = chans + i;
|
||||
|
||||
chan_enable(i, 1);
|
||||
c->volume = c->volume_init;
|
||||
|
||||
// volume envelope
|
||||
{
|
||||
uint8_t val =
|
||||
audio_mem[(0xFF12 + (i * 5)) - AUDIO_ADDR_COMPENSATION];
|
||||
|
||||
c->env.step = val & 0x07;
|
||||
c->env.up = val & 0x08 ? 1 : 0;
|
||||
c->env.inc = c->env.step ?
|
||||
(FREQ_INC_REF * 64ul) / ((uint32_t)c->env.step * AUDIO_SAMPLE_RATE) :
|
||||
(8ul * FREQ_INC_REF) / AUDIO_SAMPLE_RATE ;
|
||||
c->env.counter = 0;
|
||||
}
|
||||
|
||||
// freq sweep
|
||||
if (i == 0) {
|
||||
uint8_t val = audio_mem[0xFF10 - AUDIO_ADDR_COMPENSATION];
|
||||
|
||||
c->sweep.freq = c->freq;
|
||||
c->sweep.rate = (val >> 4) & 0x07;
|
||||
c->sweep.up = !(val & 0x08);
|
||||
c->sweep.shift = (val & 0x07);
|
||||
c->sweep.inc = c->sweep.rate ?
|
||||
((128 * FREQ_INC_REF) / (c->sweep.rate * AUDIO_SAMPLE_RATE)) : 0;
|
||||
c->sweep.counter = FREQ_INC_REF;
|
||||
}
|
||||
|
||||
int len_max = 64;
|
||||
|
||||
if (i == 2) { // wave
|
||||
len_max = 256;
|
||||
c->val = 0;
|
||||
} else if (i == 3) { // noise
|
||||
c->noise.lfsr_reg = 0xFFFF;
|
||||
c->val = VOL_INIT_MIN / MAX_CHAN_VOLUME;
|
||||
}
|
||||
|
||||
c->len.inc = (256 * FREQ_INC_REF) / (AUDIO_SAMPLE_RATE * (len_max - c->len.load));
|
||||
c->len.counter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read audio register.
|
||||
* \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F.
|
||||
* This is not checked in this function.
|
||||
* \return Byte at address.
|
||||
*/
|
||||
uint8_t audio_read(const uint16_t addr)
|
||||
{
|
||||
static const uint8_t ortab[] = {
|
||||
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, 0xff,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
return audio_mem[addr - AUDIO_ADDR_COMPENSATION] |
|
||||
ortab[addr - AUDIO_ADDR_COMPENSATION];
|
||||
}
|
||||
|
||||
/**
|
||||
* Write audio register.
|
||||
* \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F.
|
||||
* This is not checked in this function.
|
||||
* \param val Byte to write at address.
|
||||
*/
|
||||
void audio_write(const uint16_t addr, const uint8_t val)
|
||||
{
|
||||
/* Find sound channel corresponding to register address. */
|
||||
uint_fast8_t i;
|
||||
|
||||
if(addr == 0xFF26)
|
||||
{
|
||||
audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val & 0x80;
|
||||
/* On APU power off, clear all registers apart from wave
|
||||
* RAM. */
|
||||
if((val & 0x80) == 0)
|
||||
{
|
||||
memset(audio_mem, 0x00, 0xFF26 - AUDIO_ADDR_COMPENSATION);
|
||||
chans[0].enabled = false;
|
||||
chans[1].enabled = false;
|
||||
chans[2].enabled = false;
|
||||
chans[3].enabled = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/* Ignore register writes if APU powered off. */
|
||||
if(audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] == 0x00)
|
||||
return;
|
||||
|
||||
audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val;
|
||||
i = (addr - AUDIO_ADDR_COMPENSATION) / 5;
|
||||
|
||||
switch (addr) {
|
||||
case 0xFF12:
|
||||
case 0xFF17:
|
||||
case 0xFF21: {
|
||||
chans[i].volume_init = val >> 4;
|
||||
chans[i].powered = (val >> 3) != 0;
|
||||
|
||||
// "zombie mode" stuff, needed for Prehistorik Man and probably
|
||||
// others
|
||||
if (chans[i].powered && chans[i].enabled) {
|
||||
if ((chans[i].env.step == 0 && chans[i].env.inc != 0)) {
|
||||
if (val & 0x08) {
|
||||
chans[i].volume++;
|
||||
} else {
|
||||
chans[i].volume += 2;
|
||||
}
|
||||
} else {
|
||||
chans[i].volume = 16 - chans[i].volume;
|
||||
}
|
||||
|
||||
chans[i].volume &= 0x0F;
|
||||
chans[i].env.step = val & 0x07;
|
||||
}
|
||||
} break;
|
||||
|
||||
case 0xFF1C:
|
||||
chans[i].volume = chans[i].volume_init = (val >> 5) & 0x03;
|
||||
break;
|
||||
|
||||
case 0xFF11:
|
||||
case 0xFF16:
|
||||
case 0xFF20: {
|
||||
const uint8_t duty_lookup[] = { 0x10, 0x30, 0x3C, 0xCF };
|
||||
chans[i].len.load = val & 0x3f;
|
||||
chans[i].square.duty = duty_lookup[val >> 6];
|
||||
break;
|
||||
}
|
||||
|
||||
case 0xFF1B:
|
||||
chans[i].len.load = val;
|
||||
break;
|
||||
|
||||
case 0xFF13:
|
||||
case 0xFF18:
|
||||
case 0xFF1D:
|
||||
chans[i].freq &= 0xFF00;
|
||||
chans[i].freq |= val;
|
||||
break;
|
||||
|
||||
case 0xFF1A:
|
||||
chans[i].powered = (val & 0x80) != 0;
|
||||
chan_enable(i, val & 0x80);
|
||||
break;
|
||||
|
||||
case 0xFF14:
|
||||
case 0xFF19:
|
||||
case 0xFF1E:
|
||||
chans[i].freq &= 0x00FF;
|
||||
chans[i].freq |= ((val & 0x07) << 8);
|
||||
/* Intentional fall-through. */
|
||||
case 0xFF23:
|
||||
chans[i].len.enabled = val & 0x40 ? 1 : 0;
|
||||
if (val & 0x80)
|
||||
chan_trigger(i);
|
||||
|
||||
break;
|
||||
|
||||
case 0xFF22:
|
||||
chans[3].freq = val >> 4;
|
||||
chans[3].noise.lfsr_wide = !(val & 0x08);
|
||||
chans[3].noise.lfsr_div = val & 0x07;
|
||||
break;
|
||||
|
||||
case 0xFF24:
|
||||
{
|
||||
vol_l = ((val >> 4) & 0x07);
|
||||
vol_r = (val & 0x07);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0xFF25:
|
||||
for (uint_fast8_t j = 0; j < 4; j++) {
|
||||
chans[j].on_left = (val >> (4 + j)) & 1;
|
||||
chans[j].on_right = (val >> j) & 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void audio_init(void)
|
||||
{
|
||||
/* Initialise channels and samples. */
|
||||
memset(chans, 0, sizeof(chans));
|
||||
chans[0].val = chans[1].val = -1;
|
||||
|
||||
/* Initialise IO registers. */
|
||||
{
|
||||
const uint8_t regs_init[] = { 0x80, 0xBF, 0xF3, 0xFF, 0x3F,
|
||||
0xFF, 0x3F, 0x00, 0xFF, 0x3F,
|
||||
0x7F, 0xFF, 0x9F, 0xFF, 0x3F,
|
||||
0xFF, 0xFF, 0x00, 0x00, 0x3F,
|
||||
0x77, 0xF3, 0xF1 };
|
||||
|
||||
for(uint_fast8_t i = 0; i < sizeof(regs_init); ++i)
|
||||
audio_write(0xFF10 + i, regs_init[i]);
|
||||
}
|
||||
|
||||
/* Initialise Wave Pattern RAM. */
|
||||
{
|
||||
const uint8_t wave_init[] = { 0xac, 0xdd, 0xda, 0x48,
|
||||
0x36, 0x02, 0xcf, 0x16,
|
||||
0x2c, 0x04, 0xe5, 0x2c,
|
||||
0xac, 0xdd, 0xda, 0x48 };
|
||||
|
||||
for(uint_fast8_t i = 0; i < sizeof(wave_init); ++i)
|
||||
audio_write(0xFF30 + i, wave_init[i]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* minigb_apu is released under the terms listed within the LICENSE file.
|
||||
*
|
||||
* minigb_apu emulates the audio processing unit (APU) of the Game Boy. This
|
||||
* project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* MECK PATCH: upstream runs the APU at the Game Boy's native 32768 Hz,
|
||||
* which the ES8311 codec's clock table on the T-Display P4 does not
|
||||
* support ("search _clock_coeff_list fail" in the bus driver, codec left
|
||||
* unclocked, silent playback). 44100 Hz is the audiobook player's proven
|
||||
* rate on this codec. Every APU timing quantity derives from this macro
|
||||
* (FREQ_INC_REF = rate * 16, all integer maths), so the synthesis stays
|
||||
* exact; the per-frame sample count becomes 738. */
|
||||
// Meck (T-Deck): 22050, half the P4's 44100. Halves the APU's per-frame
|
||||
// cost on the ESP32-S3 (measured 3.6-4.0 ms at 44100 against a 16.7 ms
|
||||
// frame); the 4-bit Game Boy sound loses nothing audible at 22 kHz.
|
||||
#define AUDIO_SAMPLE_RATE 22050
|
||||
|
||||
#define DMG_CLOCK_FREQ 4194304.0
|
||||
#define SCREEN_REFRESH_CYCLES 70224.0
|
||||
#define VERTICAL_SYNC (DMG_CLOCK_FREQ/SCREEN_REFRESH_CYCLES)
|
||||
|
||||
#define AUDIO_SAMPLES ((unsigned)(AUDIO_SAMPLE_RATE / VERTICAL_SYNC))
|
||||
|
||||
/**
|
||||
* Fill allocated buffer "data" with "len" number of 32-bit floating point
|
||||
* samples (native endian order) in stereo interleaved format.
|
||||
*/
|
||||
void audio_callback(void *ptr, uint8_t *data, int len);
|
||||
|
||||
/**
|
||||
* Read audio register at given address "addr".
|
||||
*/
|
||||
uint8_t audio_read(const uint16_t addr);
|
||||
|
||||
/**
|
||||
* Write "val" to audio register at given address "addr".
|
||||
*/
|
||||
void audio_write(const uint16_t addr, const uint8_t val);
|
||||
|
||||
/**
|
||||
* Initialise audio driver.
|
||||
*/
|
||||
void audio_init(void);
|
||||
@@ -53,6 +53,7 @@ private:
|
||||
bool _rawJoypad;
|
||||
volatile uint8_t _rawMask; // Peanut-GB direct.joypad bit layout, set = held
|
||||
volatile bool _rawExit; // Q or Shift+Backspace press-edge latch
|
||||
volatile bool _rawMute; // Mic key press-edge latch (game sound mute toggle)
|
||||
bool _rawShiftL; // left Shift (35) physically held
|
||||
bool _rawShiftR; // right Shift (31) physically held
|
||||
|
||||
@@ -73,6 +74,10 @@ private:
|
||||
if (pressed) _rawExit = true;
|
||||
return 0;
|
||||
}
|
||||
if (keyCode == 34) { // Mic = mute toggle
|
||||
if (pressed) _rawMute = true;
|
||||
return 0;
|
||||
}
|
||||
if (keyCode == 11) { // Backspace: Shift+Backspace = quit
|
||||
if (pressed && (_rawShiftL || _rawShiftR)) _rawExit = true;
|
||||
return 0;
|
||||
@@ -211,7 +216,7 @@ public:
|
||||
: _addr(addr), _wire(wire), _initialized(false),
|
||||
_shiftActive(false), _shiftConsumed(false), _shiftHeld(false), _leftShiftHeld(false), _rightShiftHeld(false), _shiftUsedWhileHeld(false), _altActive(false), _symActive(false), _micHeld(false), _lastShiftTime(0),
|
||||
_enterHeld(false), _enterPressTime(0),
|
||||
_rawJoypad(false), _rawMask(0), _rawExit(false), _rawShiftL(false), _rawShiftR(false) {}
|
||||
_rawJoypad(false), _rawMask(0), _rawExit(false), _rawMute(false), _rawShiftL(false), _rawShiftR(false) {}
|
||||
|
||||
bool begin() {
|
||||
// Check if device responds
|
||||
@@ -461,6 +466,7 @@ public:
|
||||
_rawJoypad = on;
|
||||
_rawMask = 0;
|
||||
_rawExit = false;
|
||||
_rawMute = false;
|
||||
_rawShiftL = _rawShiftR = false;
|
||||
_shiftActive = _shiftHeld = _leftShiftHeld = _rightShiftHeld = false;
|
||||
_shiftUsedWhileHeld = _shiftConsumed = false;
|
||||
@@ -475,6 +481,12 @@ public:
|
||||
_rawExit = false;
|
||||
return e;
|
||||
}
|
||||
// Mic key press-edge latch, consumed on read.
|
||||
bool rawMutePressed() {
|
||||
bool m = _rawMute;
|
||||
_rawMute = false;
|
||||
return m;
|
||||
}
|
||||
bool isMicHeld() const { return _micHeld; }
|
||||
|
||||
// Check if shift was pressed within the last N milliseconds
|
||||
|
||||
@@ -49,6 +49,7 @@ private:
|
||||
bool _rawJoypad;
|
||||
volatile uint8_t _rawMask; // Peanut-GB direct.joypad bit layout, set = held
|
||||
volatile bool _rawExit; // Q or Shift+Backspace press-edge latch
|
||||
volatile bool _rawMute; // Mic key press-edge latch (game sound mute toggle)
|
||||
bool _rawShiftL; // left Shift (35) physically held
|
||||
bool _rawShiftR; // right Shift (31) physically held
|
||||
|
||||
@@ -66,6 +67,10 @@ private:
|
||||
if (pressed) _rawExit = true;
|
||||
return;
|
||||
}
|
||||
if (keyCode == 34) { // Mic = mute toggle
|
||||
if (pressed) _rawMute = true;
|
||||
return;
|
||||
}
|
||||
if (keyCode == 11) { // Backspace: Shift+Backspace = quit
|
||||
if (pressed && (_rawShiftL || _rawShiftR)) _rawExit = true;
|
||||
return;
|
||||
@@ -203,7 +208,7 @@ public:
|
||||
: _addr(addr), _wire(wire), _initialized(false),
|
||||
_shiftActive(false), _shiftConsumed(false), _shiftHeld(false), _shiftUsedWhileHeld(false), _altActive(false), _symActive(false), _micHeld(false), _lastShiftTime(0),
|
||||
_enterHeld(false), _enterPressTime(0),
|
||||
_rawJoypad(false), _rawMask(0), _rawExit(false), _rawShiftL(false), _rawShiftR(false) {}
|
||||
_rawJoypad(false), _rawMask(0), _rawExit(false), _rawMute(false), _rawShiftL(false), _rawShiftR(false) {}
|
||||
|
||||
bool begin() {
|
||||
// Check if device responds
|
||||
@@ -431,6 +436,7 @@ public:
|
||||
_rawJoypad = on;
|
||||
_rawMask = 0;
|
||||
_rawExit = false;
|
||||
_rawMute = false;
|
||||
_rawShiftL = _rawShiftR = false;
|
||||
_shiftActive = _shiftHeld = false;
|
||||
_shiftUsedWhileHeld = _shiftConsumed = false;
|
||||
@@ -445,6 +451,12 @@ public:
|
||||
_rawExit = false;
|
||||
return e;
|
||||
}
|
||||
// Mic key press-edge latch, consumed on read.
|
||||
bool rawMutePressed() {
|
||||
bool m = _rawMute;
|
||||
_rawMute = false;
|
||||
return m;
|
||||
}
|
||||
bool isMicHeld() const { return _micHeld; }
|
||||
|
||||
// Check if shift was pressed within the last N milliseconds
|
||||
|
||||
Reference in new issue
Block a user