BPM counter per mic with midi clock out. WaveShare 1,75 inch AMOLED
  • C 94.1%
  • Swift 3.4%
  • Python 2.1%
  • Shell 0.3%
  • CMake 0.1%
Find a file
Tom Peak Walcher e5f4549bbc Fix brightness and the touch frame; 1/2 / x1 / x2 switch on press
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>
2026-10-28 18:26:58 +01:00
docs Rename the project back to BPMMASTER 2026-10-27 03:23:37 +01:00
mac-bridge mac-bridge: BPMMASTER Bridge, a macOS app that forwards the Bluetooth clock to chosen MIDI outputs 2026-10-27 10:28:49 +01:00
main Fix brightness and the touch frame; 1/2 / x1 / x2 switch on press 2026-10-28 18:26:58 +01:00
test first commit 2026-10-27 02:47:56 +01:00
tools tools/midi_bridge.py: forward BPMMASTER's Bluetooth clock to a USB MIDI port 2026-10-27 09:55:18 +01:00
.clang-format first commit 2026-10-27 02:47:56 +01:00
.editorconfig first commit 2026-10-27 02:47:56 +01:00
.gitignore mac-bridge: BPMMASTER Bridge, a macOS app that forwards the Bluetooth clock to chosen MIDI outputs 2026-10-27 10:28:49 +01:00
CMakeLists.txt Rename the project back to BPMMASTER 2026-10-27 03:23:37 +01:00
flash.sh Rename the project back to BPMMASTER 2026-10-27 03:23:37 +01:00
partitions.csv first commit 2026-10-27 02:47:56 +01:00
README.md BRIGHT screen (5 levels, saved) and fix the touch Y axis 2026-10-28 17:32:39 +01:00
ROADMAP.md Screens: swipe to TEMPO (1/2, x1, x2, LOCK) and CLICK (metronome) 2026-10-28 16:31:27 +01:00
sdkconfig.defaults Add Bluetooth LE MIDI out (NimBLE) 2026-10-27 09:38:51 +01:00

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

Back side of the board: H2 header, connectors, chips

Wiring: MIDI out and sync on 3.5 mm TRS

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 with mac-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 (needs pip 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):

  1. Mic: ES7210, both mics averaged, 16 kHz.
  2. Onset strength: 512-pt FFT every 8 ms, log-compressed spectral flux with the kick/bass band weighted up, and the local mean removed.
  3. 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.
  4. Phase: a comb over the newest onsets finds where the beat lands.
  5. 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.