Turning Light into Clicks

I recently hacked together a delightfully simple micro:bit app that converts ambient light into sound. Think of it as a poor man’s photodiode oscilloscope — or, more accurately, a light-controlled Geiger counter for photons.

The idea is straightforward: more light → faster clicks.

What the Code Does (in Human Terms)

The micro:bit constantly reads the ambient light level using its built-in LED matrix as a sensor. This value ranges from 0 (darkness) to 255 (bright office lighting).

Instead of displaying numbers (boring), we sonify the signal.

Here’s the core loop:

  1. Read light intensity lvl = display.read_light_level()
  2. If it’s pitch black, we stop the sound and chill for a moment. No data, no noise. if lvl <= 0: music.stop() sleep(50) continue
  3. Map brightness to time
    This is the fun part. We linearly map light intensity to the pause between clicks:
    • Bright → short pause → rapid clicking
    • Dark → long pause → slow clicking
    Classic operations research move: take a bounded input domain and transform it into a bounded output domain. pause = MAX_PAUSE - (lvl - 1) * (MAX_PAUSE - MIN_PAUSE) // 255
  4. Emit a click
    A short 440 Hz tone acts as our “tick”. music.pitch(440) sleep(CLICK_DURATION) music.stop()
  5. Wait, then repeat forever — because all good embedded systems are infinite loops with opinions.

Why This Is Fun

  • It’s signal processing without math anxiety
  • It turns a sensor reading into something perceptible
  • It’s trivial, deterministic, and oddly satisfying — like watching a queueing system stabilize

Cover the micro:bit with your hand and listen to the system slow down. Shine a phone flashlight on it and it goes full caffeine mode.

Minimal code. Maximum nerd joy. Here comes the code:

CLICK_DURATION = 5      # Duration of the click in ms
MIN_PAUSE = 10          # shortest pause (at lvl = 255)
MAX_PAUSE = 100         # longest pause (at lvl = 1)


while True:
    lvl = display.read_light_level()
    display.clear()

    # If there is no light at all: play nothing
    if lvl <= 0:
        music.stop()
        sleep(50)      # small pause to prevent the CPU from running at 100%
        continue

    # lvl is between 1 and 255
    # map lvl=1 -> pause=40ms, lvl=255 -> pause=5ms (linear)
    pause = MAX_PAUSE - (lvl - 1) * (MAX_PAUSE - MIN_PAUSE) // 255

    # Click (short tone)
    music.pitch(440)
    sleep(CLICK_DURATION)
    music.stop()

    # Pause depends on brightness
    sleep(pause)