mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-26 12:46:39 +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:
@@ -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