- C 94.1%
- Swift 3.4%
- Python 2.1%
- Shell 0.3%
- CMake 0.1%
Brightness: over QSPI the CO5300 wants a command as 0x02 <cmd> 00 (the panel driver wraps its own that way); the bare 0x51 was ignored, so the level never changed. Touch: the CST9217's frame sits a quarter turn from the display's the other way, so raw (px,py) is logical (py, PANEL-1-px). The previous mapping mirrored both axes in turn (x2 lit 1/2). Swipes now follow their real direction: left to TEMPO / CLICK, right to BRIGHT. The multiplier buttons act on touch-down instead of on release. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| docs | ||
| mac-bridge | ||
| main | ||
| test | ||
| tools | ||
| .clang-format | ||
| .editorconfig | ||
| .gitignore | ||
| CMakeLists.txt | ||
| flash.sh | ||
| partitions.csv | ||
| README.md | ||
| ROADMAP.md | ||
| sdkconfig.defaults | ||
BPMMASTER
Live BPM counter + MIDI clock for the Waveshare ESP32-S3-Touch-AMOLED-1.75 (native ESP-IDF 5.5). The onboard mics hear the room, the S3 detects tempo and beat phase, the round AMOLED shows it, and the 8-pin header on the back sends a 24 PPQN MIDI clock on 3.5 mm TRS jacks in that tempo, locked to the beat. There is also an analog sync pulse for Volca / Pocket Operator / modular gear, and the same clock goes out over Bluetooth LE MIDI.
Display and touch code are shared with ../binarywatch-amoled-1.75 (same board).
Build / flash
./flash.sh # build, flash, open monitor (port auto-detected)
Using it
| Gesture | Does |
|---|---|
| tap | MIDI Start / Stop. Tap on the "one" of the loop: the nearest detected beat becomes beat 1 and Start goes out on the next bar line from it (a tap just after the one waits for the next bar). |
| swipe left / right | next / previous screen: BRIGHT ← BPM → TEMPO → CLICK (no wrap). It starts on BPM; every screen keeps the beat ring. |
Screens: BPM (above), BRIGHT, TEMPO and CLICK:
| Screen | Buttons |
|---|---|
| BRIGHT | five display levels, 10 % … 100 %, brightest on top; kept over a restart. |
| TEMPO | ½ / x1 / x2: tempo multiplier for half/double-time music. LOCK: stop listening; BPM and clock keep the current tempo. While locked, + (top) and − (bottom) set the tempo in 1 BPM steps; hold to repeat. Tap LOCK again to listen. |
| CLICK | ON / OFF: metronome on the onboard speaker, on the clock's beats: tick on beat 1, tack on 2–4. The mics hear it too, so LOCK the tempo if the click pulls the tracker. |
The status line shows LISTEN (no tempo yet), LIVE (tracking), HOLD (music
quiet or unclear, so the clock keeps the last tempo) or LOCKED. The ring is
the bar: four segments that walk with the clock, beat 1 in red.
Clock ticks (F8) run all the time once a tempo is known, as hardware clocks
do, so slaves stay locked while stopped. Start (FA) and Stop (FC) only
control the transport.
Back header (H2) → jacks
H2: IO18 IO17 IO16 RXD TXD 3V3 GND VBUS
MIDI2 SYNC MIDI1 free free
MIDI out (×2), 3.5 mm TRS, Type A
This follows the MIDI Association 3.3 V electrical spec and the TRS Type A standard:
3V3 ──[ 33 Ω ]── Ring (MIDI pin 4, current source)
IO16 ──[ 10 Ω ]── Tip (MIDI pin 5, current sink) MIDI2: same, from IO18
GND ──────────── Sleeve (shield)
- Most gear uses Type A (Korg, Make Noise, 1010music, and the standard TRS→DIN adapters).
- Some gear uses Type B (e.g. older Arturia, some Novation), where Tip and Ring are swapped. Swap those two wires or use a Type B adapter.
- For 5-pin DIN gear, use a Type A TRS→DIN adapter.
Both jacks carry the same signal: one UART TX is fanned out through the GPIO matrix. Each jack has its own resistors.
Sync out, 3.5 mm TS/TRS
IO17 ──[ 1 kΩ ]── Tip
GND ──────────── Sleeve
3.3 V pulses, SYNC_PPQN per quarter note (default 2, which Volca and Pocket
Operator expect). Each pulse is one MIDI tick long. If a module needs 5 V
triggers, add a transistor buffer.
Note: IO17/IO18 can also reach the GPS UART through R15/R16. Those are not
fitted by default (the LC76G uses I²C). If yours are fitted, move the pins in
main/board.h. TXD/RXD (UART0) are left free on purpose: the ROM bootloader
prints there on every reset.
Bluetooth MIDI out
BPMMASTER is a standard BLE MIDI device named BPMMASTER. It sends the same
F8 / FA / FC as the jacks, to up to 3 receivers at once. The bottom line on
the screen shows BLE in white when a receiver is listening (BLE x2 for two)
and dimmed while it waits.
Connecting:
- macOS: Audio MIDI Setup → Window → Show MIDI Studio → Bluetooth (the icon) → BPMMASTER → Connect. It then shows up as a MIDI input in any DAW.
- iOS / iPadOS: inside a MIDI app's Bluetooth MIDI page (e.g. GarageBand, AUM, Koala); iOS does not list BLE MIDI in the system settings.
- Windows: needs a host app with BLE MIDI support (e.g. MIDIberry, or a DAW using the Windows MIDI Services stack).
- A USB MIDI device on the same Mac (e.g. the FM-1): Audio MIDI Setup
connects devices but does not route between them, so use the bridge app
BPMMASTER Bridge (
mac-bridge/, build and install withmac-bridge/build.sh install, then open it from ~/Applications). Pick the clock source (BPMMASTER), tick any number of outputs, and it forwards clock / start / continue / stop / song position, keeping the Bluetooth timestamps. Its Bluetooth… button opens the macOS window that connects BPMMASTER. Without the app:python3 tools/midi_bridge.py(needspip install mido python-rtmidi) forwards to the first port named FM-1 (--in/--out/--list). On the FM-1 set GLO → SYSTEM → SYNC = USB. If it does not follow (e.g. right after a firmware update), unplug and replug its USB cable. - Hardware without Bluetooth: a BLE MIDI dongle in its MIDI IN, set to connect to BPMMASTER (CME WIDI Jack / WIDI Master and similar).
BLE sends in connection intervals (7.5–15 ms), so ticks arrive in small bursts. Every message carries a millisecond timestamp, and receivers that use it play the clock back evenly; others see a few ms of jitter. For tight sync on stage use a cable; Bluetooth is fine for apps, practice and loose sync.
Measured on the device (macOS receiver, fixed 120 BPM test build): 48.01
F8/s, timestamps 20–21 ms apart (ideal 20.83 ms).
How the detection works
main/beat.c is plain C (testable on the host):
- Mic: ES7210, both mics averaged, 16 kHz.
- Onset strength: 512-pt FFT every 8 ms, log-compressed spectral flux with the kick/bass band weighted up, and the local mean removed.
- Tempo, every ~0.26 s: autocorrelation of the last 8 s of onsets, then a comb filter scored over 60–200 BPM in 0.25 BPM steps, with parabolic refinement and a soft 120 BPM prior against octave errors.
- Phase: a comb over the newest onsets finds where the beat lands.
- Hysteresis: small drifts are followed smoothly. A different tempo needs about 1.5 s of agreement, and an octave jump about 4 s.
main/midi_clock.c then:
- runs the clock from a hardware timer, writing bytes straight into the UART FIFO from the ISR (jitter of a few µs);
- slews the phase toward the detected beats, at most 4 % of a tick per tick while running, so slaves never see a jump.
It is classic DSP, not a neural net. That keeps it deterministic, light, and testable. The onset stage is the place to swap in a small learned beat model (ESP-DL / TFLite Micro) later.
Tests (host)
cd test
cc -O2 -I../main beat_test.c ../main/beat.c -lm -o beat_test && ./beat_test
# any audio file:
afconvert -f WAVE -d LEI16@16000 -c 1 song.mp3 /tmp/s.wav
cc -O2 -I../main wav_bpm.c ../main/beat.c -lm -o wav_bpm && ./wav_bpm /tmp/s.wav
Tuning knobs
| Where | What |
|---|---|
main.c |
MIC_GAIN_DB, AUDIO_LATENCY_US (send the clock earlier or later), DISPLAY_ROTATE_CCW |
board.h |
output pins, SYNC_PPQN |
beat.c |
BPM range, prior, hysteresis counts |
Roadmap
See ROADMAP.md: driving SLOOP on the M-VAVE FM-1, Bluetooth MIDI, open items.