recently, my friend zero told me an idea it had to add some crowd-mediated interactivity to a live show: measure how close together a particular few people are by looking at bluetooth signal strengths, then report that back to the synth to modulate some parameters.
we whittled that down to a simpler starting place: estimate distances to a few devices directly from the central laptop, then emit some midi CC values based on that. with some trial and error, we got that to work! here’s a little demo video: it’s pretty inconsistent and high latency, as you can tell. i bet if we wrote a little app that sent out bluetooth pings every 25ms we could probably address those, though of course with some tradeoff between the two.
here’s the python script we wrote:
import asyncio
import hashlib
import ipdb
from bleak import BleakScanner
import mido
NUM_CCS = 8
BASE_CC = 70
MIN_RSSI = 0x30
MAX_RSSI = 0x60
def clamp(lo, hi, x):
if x < lo:
return lo
elif x > hi:
return hi
else:
return x
def scale(from_lo, from_hi, to_lo, to_hi, x):
return (clamp(from_lo, from_hi, x) - from_lo) / (from_hi - from_lo) * (to_hi - to_lo) + to_lo
def rssi_to_midi(rssi):
return round(scale(MIN_RSSI, MAX_RSSI, 0, 127, abs(rssi)))
def valid_ad(dev, ad):
return dev.name is not None and \
dev.name.startswith('Jessie') and \
ad.rssi is not None and \
(ad.tx_power is None or ad.tx_power < 50)
def update_cc(port, cc, val):
port.send(mido.Message('control_change', control=cc, value=val))
async def main():
port = mido.open_output()
cc_nums = {}
async with BleakScanner() as scanner:
async for bd, ad in scanner.advertisement_data():
if not valid_ad(bd, ad):
continue
if bd.address not in cc_nums:
if len(cc_nums) < NUM_CCS:
cc_nums[bd.address] = BASE_CC + len(cc_nums)
else:
continue
val = rssi_to_midi(ad.rssi - (ad.tx_power or 8))
print(f"{bd.name} -> {val}")
update_cc(port, cc_nums[bd.address], val)
if __name__ == '__main__':
asyncio.run(main())
if i/we work on it any more, i’ll put it in a repo somewhere