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