mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-14 15:16:40 +00:00
feat(sim): real buzzer/RTTTL sound via Web Audio
genericBuzzer (src/helpers/ui/buzzer.h/.cpp) gets a third platform branch, #elif defined(SIM_PLATFORM), alongside the existing NRF52 (direct PWM) and NonBlockingRtttl paths -- purely additive, no changes to either real-hardware branch. It reuses the NRF52 branch's already hardware-free RTTTL parser (_parseHeader/_parseNext/_noteFreq, now shared via a widened guard) but tracks (current frequency, note-end-time) instead of touching real PWM/timer registers, advancing on plain millis() polling from loop() -- same non-blocking shape UITask already drives every tick. Wired into the sim build the same way every real board sets its buzzer pin (-D PIN_BUZZER=<n> in build_flags/DEFINES; here it's a dummy sentinel since there's no real pin, just something to activate the existing #ifdef PIN_BUZZER guards in UITask.h/.cpp/SoundNotifier.h unchanged), plus two new small UITask accessors (isBuzzerPlaying/buzzerFreqHz/buzzerVolume) and three EMSCRIPTEN_KEEPALIVE exports so a host page can poll the buzzer's state. Browser side: one Web Audio oscillator+gain per companion instance (index.html single-instance; mesh.html per A/B, not R which is headless), created lazily on the first real user gesture (AudioContext autoplay policy), polled every 20ms and mapped to the oscillator frequency/gain -- so every notification sound, ringtone, alarm, and volume-blip that already worked on real hardware now actually produces audio in the browser, unchanged at the call-site level. Verified end-to-end with real RTTTL playback traces (not just "no errors"): the startup jingle's exact note frequencies (C6/E6/G6) and a real DM-received notification triggering the receiving instance's Web Audio gain node from 0 to its mapped volume and back, matching the actual "MsgRcv3" melody's notes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2157,6 +2157,23 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_enqueue_key(char c) {
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_enqueue_key_longpress(char c) {
|
||||
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->injectSimKeyLongPress(c);
|
||||
}
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
// Polled by the host page every ~20ms (see web/index.html/mesh.html) to
|
||||
// drive a Web Audio oscillator standing in for the real piezo buzzer --
|
||||
// genericBuzzer's own #ifdef SIM_PLATFORM branch (src/helpers/ui/buzzer.cpp)
|
||||
// tracks (is a note sounding, at what frequency) instead of touching real
|
||||
// PWM/timer hardware; these two exports are just the read side of that.
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE int sim_buzzer_is_playing() {
|
||||
return (g_sim_ui_task_for_js && g_sim_ui_task_for_js->isBuzzerPlaying()) ? 1 : 0;
|
||||
}
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE int sim_buzzer_freq_hz() {
|
||||
return g_sim_ui_task_for_js ? (int)g_sim_ui_task_for_js->buzzerFreqHz() : 0;
|
||||
}
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE int sim_buzzer_get_volume() {
|
||||
return g_sim_ui_task_for_js ? (int)g_sim_ui_task_for_js->buzzerVolume() : 0;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
bool UITask::dequeueKey(char& c) {
|
||||
|
||||
@@ -227,6 +227,18 @@ public:
|
||||
// never open the context menu at all -- injectSimKey() alone has no way
|
||||
// to signal "this press was held".
|
||||
void injectSimKeyLongPress(char c);
|
||||
#ifdef PIN_BUZZER
|
||||
// Lets a host page poll the buzzer's current state every frame to drive a
|
||||
// Web Audio oscillator (see sim_buzzer_is_playing()/sim_buzzer_freq_hz()
|
||||
// in main.cpp) -- `buzzer` itself is a private member, so a free function
|
||||
// outside this class needs these to reach it, same reason injectSimKey()
|
||||
// above is public.
|
||||
// Not const: genericBuzzer::isPlaying() itself isn't const-qualified (its
|
||||
// NRF52/non-NRF52 siblings don't need to be, so it wasn't worth widening).
|
||||
bool isBuzzerPlaying() { return buzzer.isPlaying(); }
|
||||
uint16_t buzzerFreqHz() const { return buzzer.currentFreqHz(); }
|
||||
uint8_t buzzerVolume() const { return buzzer.getVolume(); }
|
||||
#endif
|
||||
private:
|
||||
#endif
|
||||
|
||||
|
||||
@@ -3,12 +3,19 @@
|
||||
#include "buzzer.h"
|
||||
|
||||
void genericBuzzer::begin() {
|
||||
// No real GPIO pin to configure in the sim -- PIN_BUZZER there is just a
|
||||
// dummy sentinel value so the #ifdef PIN_BUZZER guards elsewhere (this
|
||||
// file included) activate at all; variants/sim/arduino/Arduino.h
|
||||
// deliberately has no pinMode()/digitalWrite() shim since nothing else
|
||||
// ever needed one before this.
|
||||
#ifndef SIM_PLATFORM
|
||||
#ifdef PIN_BUZZER_EN
|
||||
pinMode(PIN_BUZZER_EN, OUTPUT);
|
||||
digitalWrite(PIN_BUZZER_EN, HIGH);
|
||||
#endif
|
||||
pinMode(PIN_BUZZER, OUTPUT);
|
||||
digitalWrite(PIN_BUZZER, LOW); // need to pull low by default to avoid extreme power draw
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
_isr_instance = this;
|
||||
NRF_TIMER1->TASKS_STOP = 1;
|
||||
@@ -39,9 +46,11 @@ void genericBuzzer::startup() { play(startup_song); }
|
||||
void genericBuzzer::shutdown() { play(shutdown_song); }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// nRF52 path — direct NRF_PWM2 control, bypasses tone()
|
||||
// Shared RTTTL parser -- pure string/arithmetic, no hardware access, so both
|
||||
// the NRF52 direct-PWM player and the sim's poll-only player (below) reuse
|
||||
// it verbatim instead of each carrying their own copy.
|
||||
// ---------------------------------------------------------------------------
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM) || defined(SIM_PLATFORM)
|
||||
|
||||
// Chromatic frequencies for octave 4 (Hz): C C# D D# E F F# G G# A A# B
|
||||
static const uint16_t CHROM4[12] = { 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494 };
|
||||
@@ -100,6 +109,13 @@ bool genericBuzzer::_parseNext(const char*& p, uint8_t def_dur, uint8_t def_oct,
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // NRF52_PLATFORM || SIM_PLATFORM
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// nRF52 path — direct NRF_PWM2 control, bypasses tone()
|
||||
// ---------------------------------------------------------------------------
|
||||
#if defined(NRF52_PLATFORM)
|
||||
|
||||
uint8_t genericBuzzer::_dutyPct() const {
|
||||
// Inverted polarity (0x8000 bit): duty_HIGH = 100% - PCT.
|
||||
// Values chosen for ~6-8 dB perceptual steps: -24/-16/-9/-3/0 dB.
|
||||
@@ -261,9 +277,67 @@ void genericBuzzer::setVolume(uint8_t level) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Non-nRF52 path — NonBlockingRtttl + analogWrite for volume
|
||||
// Sim path — no real PWM/timer hardware; just track (freq, note-end-time)
|
||||
// and let loop() poll millis() to advance, same non-blocking shape as the
|
||||
// NonBlockingRtttl path below minus the library. A host page polls
|
||||
// currentFreqHz()/isPlaying() every frame to drive a Web Audio oscillator
|
||||
// (see variants/sim/web/index.html) instead of sounding real hardware.
|
||||
// ---------------------------------------------------------------------------
|
||||
#elif defined(SIM_PLATFORM)
|
||||
|
||||
void genericBuzzer::_advance() {
|
||||
uint16_t freq; uint32_t dur_ms;
|
||||
if (_parseNext(_rtttl_pos, _def_dur, _def_oct, _def_bpm, freq, dur_ms)) {
|
||||
_cur_freq = freq;
|
||||
_note_end_ms = millis() + dur_ms;
|
||||
} else {
|
||||
_cur_freq = 0;
|
||||
_rtttl_done = true;
|
||||
}
|
||||
}
|
||||
|
||||
void genericBuzzer::applyVolume() {
|
||||
// No hardware duty cycle to touch -- the host page maps getVolume()
|
||||
// (0-4) to a Web Audio gain value itself.
|
||||
}
|
||||
|
||||
void genericBuzzer::play(const char* melody) {
|
||||
if (_is_quiet) return;
|
||||
playForced(melody);
|
||||
}
|
||||
|
||||
void genericBuzzer::playForced(const char* melody) {
|
||||
_rtttl_done = true;
|
||||
_cur_freq = 0;
|
||||
if (!melody || !*melody) return;
|
||||
const char* notes;
|
||||
_parseHeader(melody, _def_dur, _def_oct, _def_bpm, notes);
|
||||
_rtttl_pos = notes;
|
||||
_rtttl_done = false;
|
||||
_advance();
|
||||
}
|
||||
|
||||
bool genericBuzzer::isPlaying() { return !_rtttl_done; }
|
||||
|
||||
void genericBuzzer::stop() {
|
||||
_rtttl_done = true;
|
||||
_cur_freq = 0;
|
||||
}
|
||||
|
||||
void genericBuzzer::loop() {
|
||||
if (_rtttl_done) return;
|
||||
if ((int32_t)(millis() - _note_end_ms) >= 0) _advance();
|
||||
}
|
||||
|
||||
void genericBuzzer::setVolume(uint8_t level) {
|
||||
_volume_level = level < 5 ? level : 4;
|
||||
}
|
||||
|
||||
#else // NRF52_PLATFORM / SIM_PLATFORM
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Non-nRF52, non-sim path — NonBlockingRtttl + analogWrite for volume
|
||||
// ---------------------------------------------------------------------------
|
||||
#else
|
||||
|
||||
void genericBuzzer::applyVolume() {
|
||||
// After tone() sets 50% duty, analogWrite overrides duty on the same PWM channel.
|
||||
@@ -299,6 +373,6 @@ void genericBuzzer::setVolume(uint8_t level) {
|
||||
if (isPlaying()) applyVolume();
|
||||
}
|
||||
|
||||
#endif // NRF52_PLATFORM
|
||||
#endif // NRF52_PLATFORM / SIM_PLATFORM
|
||||
|
||||
#endif // PIN_BUZZER
|
||||
|
||||
+40
-13
@@ -2,9 +2,10 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// NRF52 uses a custom non-blocking RTTTL player (see buzzer.cpp); only the
|
||||
// other platforms pull in the NonBlockingRtttl library here.
|
||||
#if !defined(NRF52_PLATFORM)
|
||||
// NRF52 (and the sim, see buzzer.cpp) use a custom non-blocking RTTTL
|
||||
// player; only the remaining platforms pull in the NonBlockingRtttl library
|
||||
// here.
|
||||
#if !defined(NRF52_PLATFORM) && !defined(SIM_PLATFORM)
|
||||
#include <NonBlockingRtttl.h>
|
||||
#endif
|
||||
|
||||
@@ -44,6 +45,25 @@ class genericBuzzer
|
||||
const char *shutdown_song = "Shutdown:d=4,o=5,b=100:8g5,16e5,16c5";
|
||||
bool _is_quiet = true;
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(SIM_PLATFORM)
|
||||
// Shared RTTTL cursor state + parser, reused by both the NRF52
|
||||
// direct-PWM player below and the sim's poll-only player (buzzer.cpp,
|
||||
// #elif defined(SIM_PLATFORM)) -- the parser itself never touches
|
||||
// hardware, only _nrfStartPwm/_nrfStopPwm/the TIMER1 IRQ do, so it's
|
||||
// free to share between the two.
|
||||
const char* _rtttl_pos = nullptr;
|
||||
bool _rtttl_done = true;
|
||||
uint8_t _def_dur = 4;
|
||||
uint8_t _def_oct = 5;
|
||||
uint16_t _def_bpm = 120;
|
||||
|
||||
static uint16_t _noteFreq(char letter, bool sharp, uint8_t octave);
|
||||
static bool _parseNext(const char*& pos, uint8_t def_dur, uint8_t def_oct,
|
||||
uint16_t bpm, uint16_t& freq_hz, uint32_t& dur_ms);
|
||||
static void _parseHeader(const char* melody, uint8_t& def_dur, uint8_t& def_oct,
|
||||
uint16_t& bpm, const char*& notes_start);
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// Own RTTTL player — bypasses tone() to allow volume control from note start.
|
||||
// tone() pre-computes seq_refresh so the DMA repeats 50% duty for ~30ms before
|
||||
@@ -51,23 +71,13 @@ class genericBuzzer
|
||||
// and setting REFRESH=0, DMA re-reads _duty_buf every period so duty takes effect
|
||||
// immediately at SEQSTART.
|
||||
volatile uint16_t _duty_buf = 0; // DMA source — must stay in RAM
|
||||
const char* _rtttl_pos = nullptr;
|
||||
bool _rtttl_done = true;
|
||||
bool _pwm_on = false;
|
||||
uint8_t _def_dur = 4;
|
||||
uint8_t _def_oct = 5;
|
||||
uint16_t _def_bpm = 120;
|
||||
|
||||
void _nrfBegin(const char* melody);
|
||||
void _nrfAdvance();
|
||||
void _nrfStartPwm(uint16_t freq);
|
||||
void _nrfStopPwm();
|
||||
uint8_t _dutyPct() const;
|
||||
static uint16_t _noteFreq(char letter, bool sharp, uint8_t octave);
|
||||
static bool _parseNext(const char*& pos, uint8_t def_dur, uint8_t def_oct,
|
||||
uint16_t bpm, uint16_t& freq_hz, uint32_t& dur_ms);
|
||||
static void _parseHeader(const char* melody, uint8_t& def_dur, uint8_t& def_oct,
|
||||
uint16_t& bpm, const char*& notes_start);
|
||||
|
||||
// TIMER1-driven note advance: a hardware compare interrupt calls
|
||||
// _nrfAdvance() at the exact moment the current note's duration ends,
|
||||
@@ -84,5 +94,22 @@ class genericBuzzer
|
||||
// a free function — the vector table requires that exact symbol — so
|
||||
// it needs access from outside the class to dispatch into it.
|
||||
static void _timer1ISR();
|
||||
private:
|
||||
#elif defined(SIM_PLATFORM)
|
||||
// No real PWM/timer hardware to drive -- just track which frequency
|
||||
// (0 = silent) should be sounding right now and when the current
|
||||
// note ends, advanced by plain millis()-polling from loop() (same
|
||||
// non-blocking shape as the NonBlockingRtttl-driven platforms, not
|
||||
// the NRF52 IRQ path). A host page polls currentFreqHz()/isPlaying()
|
||||
// every frame to drive a Web Audio oscillator -- see
|
||||
// variants/sim/web/index.html.
|
||||
uint32_t _note_end_ms = 0;
|
||||
|
||||
void _advance();
|
||||
|
||||
public:
|
||||
uint16_t currentFreqHz() const { return _cur_freq; }
|
||||
private:
|
||||
uint16_t _cur_freq = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -75,6 +75,7 @@ SRCS=(
|
||||
src/helpers/StaticPoolPacketManager.cpp
|
||||
src/helpers/TransportKeyStore.cpp
|
||||
src/helpers/TxtDataHelpers.cpp
|
||||
src/helpers/ui/buzzer.cpp
|
||||
lib/ed25519/add_scalar.c
|
||||
lib/ed25519/fe.c
|
||||
lib/ed25519/ge.c
|
||||
@@ -127,6 +128,9 @@ DEFINES=(
|
||||
# DisplayDriver (variants/sim/SimDisplayDriver.h's __EMSCRIPTEN__-guarded
|
||||
# SimDisplayDriverCanvas class) instead of the ASCII/stdout one.
|
||||
-DDISPLAY_CLASS=SimDisplayDriverCanvas
|
||||
# Dummy sentinel (no real pin) -- see platformio.ini's own comment on the
|
||||
# native env's identical flag.
|
||||
-DPIN_BUZZER=0
|
||||
-DMAX_CONTACTS=100
|
||||
-DMAX_GROUP_CHANNELS=8
|
||||
)
|
||||
|
||||
@@ -27,6 +27,12 @@ build_flags =
|
||||
-D SIM_PLATFORM
|
||||
-D MESH_DEBUG=0
|
||||
-D DISPLAY_CLASS=SimDisplayDriver
|
||||
; No real buzzer pin -- this is a dummy sentinel, purely to activate the
|
||||
; #ifdef PIN_BUZZER guards in UITask.h/.cpp/SoundNotifier.h/main.cpp
|
||||
; unchanged (real boards set this to their actual GPIO pin number the same
|
||||
; way). genericBuzzer's own #ifdef SIM_PLATFORM branch (src/helpers/ui/
|
||||
; buzzer.cpp) never touches a real pin, so the value itself is unused.
|
||||
-D PIN_BUZZER=0
|
||||
-D MAX_CONTACTS=100
|
||||
-D MAX_GROUP_CHANNELS=8
|
||||
-I variants/sim/arduino
|
||||
@@ -56,6 +62,7 @@ build_src_filter =
|
||||
+<../src/helpers/StaticPoolPacketManager.cpp>
|
||||
+<../src/helpers/TransportKeyStore.cpp>
|
||||
+<../src/helpers/TxtDataHelpers.cpp>
|
||||
+<../src/helpers/ui/buzzer.cpp>
|
||||
+<../lib/ed25519/*.c>
|
||||
+<../variants/sim/*.cpp>
|
||||
+<../variants/sim/thirdparty/crypto/*.cpp>
|
||||
|
||||
@@ -213,6 +213,54 @@
|
||||
if (Module && Module._sim_enqueue_key_longpress) Module._sim_enqueue_key_longpress(code);
|
||||
}
|
||||
|
||||
// Buzzer: a single Web Audio oscillator+gain standing in for the real
|
||||
// piezo buzzer, polled against sim_buzzer_is_playing()/
|
||||
// sim_buzzer_freq_hz() (see genericBuzzer's own #ifdef SIM_PLATFORM
|
||||
// branch, src/helpers/ui/buzzer.cpp) -- those already track exactly
|
||||
// which frequency (0 = silent) should be sounding right now, driven
|
||||
// by the real RTTTL playback the notification/ringtone/startup code
|
||||
// triggers unchanged. AudioContext can't be created before a real
|
||||
// user gesture (browser autoplay policy), so this is created lazily
|
||||
// on first pointerdown/keydown -- the boot startup jingle will
|
||||
// already be over by the time that first gesture happens, same
|
||||
// unavoidable limitation any embedded page with boot sound has.
|
||||
let audioCtx = null, buzzOsc = null, buzzGain = null, buzzWasPlaying = false;
|
||||
function ensureAudio() {
|
||||
if (audioCtx) return;
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
buzzOsc = audioCtx.createOscillator();
|
||||
buzzGain = audioCtx.createGain();
|
||||
buzzGain.gain.value = 0;
|
||||
buzzOsc.type = 'square'; // closer to a piezo buzzer's timbre than a sine
|
||||
buzzOsc.frequency.value = 440;
|
||||
buzzOsc.connect(buzzGain).connect(audioCtx.destination);
|
||||
buzzOsc.start();
|
||||
}
|
||||
// Exposed for console poking / Playwright, same as window.Module above.
|
||||
window.__buzzAudio = () => (audioCtx
|
||||
? { state: audioCtx.state, gain: buzzGain.gain.value, freq: buzzOsc.frequency.value }
|
||||
: null);
|
||||
// Volume steps roughly mirror genericBuzzer's own real-hardware duty-
|
||||
// cycle table (buzzer.cpp's PCT[]/duty[] -- ~6-8 dB per step); 0 is
|
||||
// silence regardless of what's "playing" (mutes without touching the
|
||||
// sim's own note-advance timing).
|
||||
const BUZZER_GAIN = [0, 0.02, 0.05, 0.09, 0.15];
|
||||
function pollBuzzer() {
|
||||
if (!audioCtx || !Module || !Module._sim_buzzer_is_playing) return;
|
||||
const playing = Module.ccall('sim_buzzer_is_playing', 'number', [], []) === 1;
|
||||
const now = audioCtx.currentTime;
|
||||
if (playing) {
|
||||
const freq = Module.ccall('sim_buzzer_freq_hz', 'number', [], []);
|
||||
const vol = Module.ccall('sim_buzzer_get_volume', 'number', [], []);
|
||||
buzzOsc.frequency.setValueAtTime(freq > 0 ? freq : 440, now);
|
||||
buzzGain.gain.linearRampToValueAtTime(freq > 0 ? BUZZER_GAIN[vol] || 0 : 0, now + 0.005);
|
||||
} else if (buzzWasPlaying) {
|
||||
buzzGain.gain.linearRampToValueAtTime(0, now + 0.01);
|
||||
}
|
||||
buzzWasPlaying = playing;
|
||||
}
|
||||
setInterval(pollBuzzer, 20);
|
||||
|
||||
// Press-and-hold for the on-page buttons: start a timer on press-down,
|
||||
// fire the long-press call if still held past the threshold, and
|
||||
// suppress the short click on release if it already fired.
|
||||
@@ -221,6 +269,7 @@
|
||||
let timer = null, longFired = false;
|
||||
btn.addEventListener('pointerdown', (e) => {
|
||||
e.preventDefault();
|
||||
ensureAudio();
|
||||
longFired = false;
|
||||
timer = setTimeout(() => { longFired = true; sendKeyLongPress(code); }, LONG_PRESS_MS);
|
||||
});
|
||||
@@ -297,6 +346,7 @@
|
||||
const code = resolveKey(e);
|
||||
if (code === null) return;
|
||||
e.preventDefault();
|
||||
ensureAudio();
|
||||
if (keyHold.has(e.key)) return; // auto-repeat, not a new press
|
||||
const state = { longFired: false };
|
||||
state.timer = setTimeout(() => { state.longFired = true; sendKeyLongPress(code); }, LONG_PRESS_MS);
|
||||
|
||||
@@ -421,12 +421,56 @@
|
||||
// current when the listener was attached.
|
||||
function currentInst(key) { return key === 'A' ? A : key === 'B' ? B : R; }
|
||||
|
||||
// Buzzer: one Web Audio oscillator+gain per companion instance (not R
|
||||
// -- a headless simple_repeater has no UITask/buzzer at all), same
|
||||
// mechanism as web/index.html (see that file's own comment). Keyed by
|
||||
// instance tag rather than closing over A/B directly, since Reset
|
||||
// (resetInstance() above) replaces those bindings with a fresh module
|
||||
// -- currentInst(key) below always resolves the live one, so a reset
|
||||
// instance's sound keeps working against the same AudioContext.
|
||||
const buzzAudio = {};
|
||||
function ensureAudio(key) {
|
||||
if (key === 'R' || buzzAudio[key]) return;
|
||||
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
gain.gain.value = 0;
|
||||
osc.type = 'square';
|
||||
osc.frequency.value = 440;
|
||||
osc.connect(gain).connect(ctx.destination);
|
||||
osc.start();
|
||||
buzzAudio[key] = { ctx, osc, gain, wasPlaying: false };
|
||||
}
|
||||
const BUZZER_GAIN = [0, 0.02, 0.05, 0.09, 0.15];
|
||||
function pollBuzzer(key) {
|
||||
const b = buzzAudio[key];
|
||||
const inst = currentInst(key);
|
||||
if (!b || !inst || !inst.isReady() || !inst.mod._sim_buzzer_is_playing) return;
|
||||
const playing = inst.mod.ccall('sim_buzzer_is_playing', 'number', [], []) === 1;
|
||||
const now = b.ctx.currentTime;
|
||||
if (playing) {
|
||||
const freq = inst.mod.ccall('sim_buzzer_freq_hz', 'number', [], []);
|
||||
const vol = inst.mod.ccall('sim_buzzer_get_volume', 'number', [], []);
|
||||
b.osc.frequency.setValueAtTime(freq > 0 ? freq : 440, now);
|
||||
b.gain.gain.linearRampToValueAtTime(freq > 0 ? (BUZZER_GAIN[vol] || 0) : 0, now + 0.005);
|
||||
} else if (b.wasPlaying) {
|
||||
b.gain.gain.linearRampToValueAtTime(0, now + 0.01);
|
||||
}
|
||||
b.wasPlaying = playing;
|
||||
}
|
||||
setInterval(() => { pollBuzzer('A'); pollBuzzer('B'); }, 20);
|
||||
// Exposed for console poking / Playwright, same as window.__meshSim below.
|
||||
window.__buzzAudio = (key) => (buzzAudio[key]
|
||||
? { state: buzzAudio[key].ctx.state, gain: buzzAudio[key].gain.gain.value, freq: buzzAudio[key].osc.frequency.value }
|
||||
: null);
|
||||
|
||||
document.querySelectorAll('button[data-instance]').forEach((btn) => {
|
||||
const key = btn.dataset.instance;
|
||||
const code = Number(btn.dataset.key);
|
||||
let timer = null, longFired = false;
|
||||
btn.addEventListener('pointerdown', (e) => {
|
||||
e.preventDefault();
|
||||
ensureAudio(key);
|
||||
longFired = false;
|
||||
timer = setTimeout(() => { longFired = true; sendKeyLongPress(currentInst(key), code); }, LONG_PRESS_MS);
|
||||
});
|
||||
@@ -606,6 +650,7 @@
|
||||
const code = resolveKey(e);
|
||||
if (code === null) return;
|
||||
e.preventDefault();
|
||||
ensureAudio(focusedKey);
|
||||
if (keyHold.has(e.key)) return;
|
||||
const state = { longFired: false };
|
||||
state.timer = setTimeout(() => { state.longFired = true; sendKeyLongPress(currentInst(focusedKey), code); }, LONG_PRESS_MS);
|
||||
|
||||
Reference in New Issue
Block a user