tubasync · tapper · info
tubasync plays two pure tones, one in each ear, a few hertz apart. the brain hears the difference as a slow beat that isn't in either ear on its own. the tapper puts that beat in your hands: two coin motors, one per hand, buzzing left, right, left, right, in time with it.
everything runs in this page. the tones are made by the browser, the taps are scheduled by the page, and the device is a small bluetooth board. nothing is sent to a server. this is phase 1 of a hardware project, built in public. not a medical device.
signal
two sine waves, carrier f, split by the interaural offset Δ, panned hard left and hard right:
x_L(t) = sin(2π (f − Δ/2) t) x_R(t) = sin(2π (f + Δ/2) t)the right ear runs ahead of the left by a phase that grows steadily. that phase is the one clock everything on this page shares:
φ(t) = 2π ∫ Δ(t) dt (just 2πΔt when Δ holds still)when φ is a multiple of 2π the two ears are in step, and the beat is at its loudest. at φ = π they're opposite. the envelope of the summed signal shows it directly:
x_L + x_R = 2 cos(φ/2) · sin(2π f t + φ/2) envelope = 2|cos(φ/2)|in headphones the two waves never actually add (each ear gets one), so the beat is built by the brainstem comparing the two. that's the dichotic part. the sum is drawn because it's the cleanest picture of the phase.
presets
a coin motor can't follow a rate that keeps moving. it takes a few tens of milliseconds to spin up and longer to coast down, the rate only reaches the device about once a second over bluetooth, and in phase 1 the device can't be told where in its cycle to be. a smooth glide makes the tap rate slide continuously, so the hands never settle. the presets keep the tap rate still and let the sound do the moving.
| classic | Δ = 1 Hz, 1 : 1, r = 1 /s (120 taps a minute). the classic bilateral pace, ears and hands in step. |
|---|---|
| slow | Δ = 0.5 Hz, 1 : 1, r = 0.5 /s (60 taps a minute). |
| theta | Δ = 6 Hz, 1 : 6, r = 1 /s. |
| alpha | Δ = 10 Hz, 1 : 10, r = 1 /s. |
| step down | Δ steps 8, 7 … 1 Hz over 120 s, about 15 s a step. r = 1 /s throughout. |
| step wave | Δ steps 1 → 6 → 1 Hz, pingpong, period 120 s. r = 1 /s throughout. |
the step trick: with r aimed at 1 /s, any whole-number Δ is an exact ratio, 1 : Δ. so the glide snaps Δ to whole hertz (and to 1/2, 1/3 … below 1), and only the ratio changes at each step; the rate sent to the device never does:
steps: Δ_s = round(Δ) for Δ ≥ 1, Δ_s = 1 / round(1/Δ) below k = Δ_s, r = Δ_s / k = 1 /s (no hysteresis in step mode: every step is exact)the steps box under the glide is on by default. untick it for the old smooth glide, which still works but is better for ears than hands.
glide
the glide moves Δ between a low and a high value with period T, the same as the glide page. pingpong is a triangle wave, up and down are one-shot ramps:
pingpong: Δ(t) = Δ_lo + (Δ_hi − Δ_lo) · tri(t/T), tri(u) = 1 − |2·frac(u) − 1| up: Δ(t) = Δ_lo + (Δ_hi − Δ_lo) · t/T, then holds Δ_hi down: Δ(t) = Δ_hi − (Δ_hi − Δ_lo) · t/T, then holds Δ_lodefaults come from the 260215 glide notes: 0.1 to 8.8 Hz, pingpong, a 10 to 15 s period. as Δ heads toward 0.1 Hz the beat stretches into one slow swell every ten seconds.
device
| board | seeed xiao esp32c3 (risc-v, bluetooth low energy, usb-c) |
|---|---|
| actuators | two 3 V coin vibration motors (eccentric rotating mass), one per hand, each switched by a 2N2222 transistor through a 1 kΩ base resistor, a flyback diode across each motor |
| controls | a rotary knob. click: start / stop. hold half a second: move to the next setting (intensity, between, buzz length, shape). turn: change it. between is the time from one tap to the next, 0.25 to 2.5 s in 50 ms steps (so cycles/s = 0.5 / between); the quiet gap is between minus the buzz. with no page connected it's a complete tapper on its own; connected, the page sets the timing and the knob keeps the rest. |
| screen | 1.3" i2c oled: on + session time, bluetooth, the setting the knob is on in large type, all four settings along the bottom, L / R dots |
| firmware | v1.3 (261003). the board keeps its own clock and runs the L-R cycle itself on its own high-priority task, every millisecond, so bluetooth traffic can never hold a motor on. the page sets the rate and turns it on or off. |
one tap cycle is L then R. at cycle rate r, the period is T = 1/r, each side owns half of it, and the motor buzzes for the first part of its half:
T = 1/r side = L if (t mod T) < T/2, else R τ = min(ℓ, 0.8 · T/2) buzz per tap, ℓ = knob buzz length, 40…400 ms (default 120) d = 95 + (255 − 95)(s − 1)/9 pwm duty for knob intensity s = 1…10two buzz shapes, chosen on the knob. tap kicks the motor at full power for its first 12 ms, then holds at d: a coin motor takes tens of milliseconds to spin up, and the kick makes the onset sharper, most of all at low intensity. wave swells each side up and back down as half a sine, the hand version of the beat's own envelope:
tap: duty(u) = 255 for u < 12 ms, then d u = ms into this side's turn, u < τ wave: duty(u) = 95 + (d − 95) · sin(π u / τ)motors
a coin motor is a tiny spinning weight. it needs a few tens of milliseconds to spin up before you feel it, and a little longer to coast down. that sets the usable range:
| too fast | above about 1.6 cycles/s (≈ 190 taps a minute) the gap between taps gets shorter than the spin-up plus coast-down, and the two sides blur into one buzz |
|---|---|
| too slow | below about 0.2 cycles/s (one tap every 2.5 s per hand) it stops feeling like a rhythm and starts feeling like a reminder |
| comfort band | 0.2 to 1.6 cycles/s, the grey band on the trace. classic bilateral stimulation sits around 0.5 to 1.5 |
| hard limits | the firmware accepts 0.05 to 5 cycles/s and ignores anything else |
| weakest buzz | pwm duty 95 / 255 is the floor that still spins a motor reliably |
tubasync's Δ runs well outside that band, from 0.1 Hz up past 8. so the taps can't just follow Δ. they lock to it at a ratio.
the lock
in locked mode the page picks a whole-number ratio between the beat and the taps. above the band it aims the tap rate at a steady 1 cycle/s, so the hands feel an even pace while the sound sweeps:
Δ > 1.6 Hz: k = max(⌈Δ / 1.6⌉, round(Δ / 1.0)), r = Δ / k one tap cycle every k beats (1 : k) Δ < 0.2 Hz: k = ⌈0.2 / Δ⌉, r = k · Δ k tap cycles every beat (k : 1) otherwise: r = Δ one cycle per beat (1 : 1)a glide walks Δ through these ratios. to keep k from stepping too often, the current ratio is kept while its rate stays within 25% of the ideal one (|ln(r/r*)| < ln 1.25) and inside the band. above 1.6 Hz that holds r between about 0.8 and 1.25 cycles/s.
the page's own tap schedule is a tap phase ψ, in cycles, that integrates the rate and is pulled toward the beat by a first-order phase-locked loop:
dψ/dt = r + κ · wrap(ψ* − ψ), ψ* = φ / (2π k) for 1 : k, ψ* = k φ / 2π for k : 1 κ = 0.8 /s, wrap(x) ∈ (−½, ½] L tap when ψ crosses a whole number, R tap when it crosses a halfso an L tap lands on the loud part of the beat, and when the ratio steps mid-glide the schedule slides back onto it within a second or two instead of jumping.
in own rate mode r is just the slider and there's no pull toward the beat. the taps drift through it, which you can watch on the wheel.
the lock number is the mean resultant length of the beat phase at the last 16 L taps, a standard circular statistic:
lock = | (1/N) Σ e^{ i m φ_n } | m = k in k : 1 mode, else 1 1 = every tap on the same phase of the beat, 0 = scattered all the way rounddata panel
| hands | two lamps that light for the buzz length on every tap. from the device's own tap reports when it's connected and running, from the page schedule otherwise. between them: the current beat phase φ and the lock. |
|---|---|
| waves | x_L, x_R and their sum, at the real phase difference φ. the carrier is drawn slowed to a few cycles across the panel (260 Hz would be a grey smear), so the shape is exact and the speed is not. the dashed lines are the envelope ±2|cos(φ/2)|. |
| timeline | the last 12 s. top: the beat envelope |cos(φ/2)|. below: one lane per hand, a box for every tap as long as its buzz. solid boxes are taps the device reported, hollow boxes are the page's schedule. when they line up, the device is on the beat. |
| wheel | φ on a circle, beat peak at the top, the hand turning at Δ revolutions per second. every tap is a dot where it landed, L on the outer ring and R inside, fading over 40 s. a tight cluster is a lock, a smear is drift. |
| trace | the last 2 minutes of Δ (blue) and the tap rate r (black) on a log scale, with the comfort band in grey. in a glide you can see r stepping between ratios while Δ sweeps. |
bluetooth
web bluetooth, so chrome or edge on a mac, windows or android. safari and iphone don't support it. the page asks for the tapper by its service uuid and talks to five characteristics:
| service | 7a1b0001-2c3d-4e5f-8a9b-7475626173a1 |
|---|---|
| RATE · 0002 | read / write. tap cycles per second as text, e.g. 0.750. the page re-sends it when it moves more than 3%, at most once a second. from firmware 1.1 a rate change keeps the device's place in the L-R cycle (1.0 jumped, which doubled or dropped taps mid-glide). |
| RUN · 0003 | read / write. 1 on, 0 off. the knob can do the same; whoever acted last wins. if the page turned it on and the link drops, the device stops after 10 s. |
| STATUS · 0004 | notify, about once a second and on every change. json: firmware, on, started by, strength, rate, taps per minute, taps, session time, uptime, chip temperature. |
| TAP · 0005 | notify on every tap: L,<ms> or R,<ms>. the number is the device's own clock at the tap; the page maps it onto its clock (smallest arrival offset seen), so a report delayed in the radio still lands where the tap happened. these drive the solid boxes and the lamps. |
| PING · 0006 | write anything, it echoes back. the page pings every 3 s and shows the round trip. |
phase 1 limits
| rate-locked, not phase-locked | the device runs its own L-R clock. the page sends it the right rate, but it can't yet tell it where in the cycle to be, so the device's taps can sit at any fixed offset from the beat, and each rate change nudges that offset. the solid-vs-hollow boxes and the wheel make the gap visible. phase 2 adds a phase command to the firmware so the device lands on the beat too. |
|---|---|
| latency | bluetooth adds a few tens of milliseconds each way, and it varies. it's measured, not assumed, and only used to place taps on the panel. |
| knob settings | intensity, buzz length and shape are knob only. the page reads them, it doesn't set them. |
| one device | one tapper per page for now. |
| screen jitter | the bench prototype's oled flickers when a motor kicks on (supply dip, no reset). a bulk capacitor across the motor rail is the planned fix. |
where it's headed: phase 2, phase lock and strength from the page. later, a version 3 that's a finished object rather than a breadboard: its own enclosure, battery and charging, two tethered hand pieces.