Synthesized sine streaming
Synthesize a 16 kHz mono PCM16 sine on the MCU and stream it to the Hapbeat. No stored audio, and frequency / intensity are live.
One-shot
Section titled “One-shot”hb.playSine(160.0f, 0.7f, 400); // 160 Hz, intensity 0.7, 400 msplaySine blocks for durationMs. Good for short haptic feedback.
Continuous (hold / live control)
Section titled “Continuous (hold / live control)”To buzz while a button is held, or to drive frequency from a sensor, use
beginSine / pumpSine / endSine. Call pumpSine() often from loop() to keep
the device ring buffer filled.
void onPressDown() { hb.beginSine(160.0f, 0.8f); // start a continuous sine}
void loop() { // ... handle input ... if (hb.sineActive()) hb.pumpSine(); // call every loop (prevents dropouts)}
void onPressUp() { hb.endSine(); // stop}pumpSine()sends a bounded number of chunks per call, so as long as you don’t blockloop()for ~160 ms+, it self-paces.- To change frequency / intensity mid-stream, call
endSine()and thenbeginSine()again with the new values.
Reducing choppiness
Section titled “Reducing choppiness”Synthesized sine is sent over UDP, so Wi-Fi conditions can cause dropouts. To fix:
- Call
WiFi.setSleep(false)after connecting (kills ESP32 modem-sleep jitter). - Have the devices discovered: once
discover()has found them, sending is unicast and the radio’s MAC ACK + retry makes it far smoother. With none found, it falls back to broadcast — no ACK/retry, and subject to the AP’s DTIM holding (100–300 ms). See Discovery & targeting, and callping()every few seconds fromloop()to keep the table alive. - Call
pumpSine()frequently fromloop(). If heavy work stallsloop(), the device ring (~256 ms) starves and you hear gaps.