Although the documentation for this project shows a Feather M4 Express, a Feather RP2040 will work too.

Once you've finished setting up your Feather M4 Express with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.

To do this, click on the Download Project Bundle button in the window below. It will download as a zipped folder.

# SPDX-FileCopyrightText: 2022 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import board
import busio
import simpleio
import adafruit_vl53l4cd
import adafruit_tca9548a
import adafruit_midi
from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn
from adafruit_midi.program_change import ProgramChange
from adafruit_midi.control_change import ControlChange

# Create I2C bus as normal
i2c = board.I2C()  # uses board.SCL and board.SDA
# i2c = board.STEMMA_I2C()  # For using the built-in STEMMA QT connector on a microcontroller

# Create the TCA9548A object and give it the I2C bus
tca = adafruit_tca9548a.TCA9548A(i2c)

#  setup time of flight sensors to use TCA9548A inputs
tof_0 = adafruit_vl53l4cd.VL53L4CD(tca[0])
tof_1 = adafruit_vl53l4cd.VL53L4CD(tca[1])
tof_2 = adafruit_vl53l4cd.VL53L4CD(tca[2])
tof_3 = adafruit_vl53l4cd.VL53L4CD(tca[3])
tof_4 = adafruit_vl53l4cd.VL53L4CD(tca[4])
tof_5 = adafruit_vl53l4cd.VL53L4CD(tca[5])
tof_6 = adafruit_vl53l4cd.VL53L4CD(tca[6])
tof_7 = adafruit_vl53l4cd.VL53L4CD(tca[7])

#  array of tof sensors
flights = [tof_0, tof_1, tof_2, tof_3, tof_4, tof_5, tof_6, tof_7]

#  setup each tof sensor
for flight in flights:
    flight.inter_measurement = 0
    flight.timing_budget = 50
    flight.start_ranging()

#  midi uart setup for music maker featherwing
uart = busio.UART(board.TX, board.RX, baudrate=31250)

midi_in_channel = 1
midi_out_channel = 1
#  midi setup
#  UART is setup as the input
midi = adafruit_midi.MIDI(
    midi_in=uart,
    midi_out=uart,
    in_channel=(midi_in_channel - 1),
    out_channel=(midi_out_channel - 1),
    debug=False,
)

#  height cutoff for tof sensors
#  adjust depending on the height of your ceiling/performance area
flight_height = 150

#  state of each tof sensor
#  tracks if you have hit the laser range
pluck_0 = False
pluck_1 = False
pluck_2 = False
pluck_3 = False
pluck_4 = False
pluck_5 = False
pluck_6 = False
pluck_7 = False

#  array of tof sensor states
plucks = [pluck_0, pluck_1, pluck_2, pluck_3, pluck_4, pluck_5, pluck_6, pluck_7]

#  midi notes for each tof sensor
notes = [48, 52, 55, 59, 60, 64, 67, 71]

#  midi instrument voice
midi.send(ProgramChange(80))

while True:
    #  iterate through the 8 tof sensors
    for f in range(8):
        while not flights[f].data_ready:
            pass
        #  reset tof sensors
        flights[f].clear_interrupt()
        #  if the reading from a tof is not 0...
        if flights[f].distance != 0.0:
            #  map range of tof sensor distance to midi parameters
            #  modulation
            mod = round(simpleio.map_range(flights[f].distance, 0, 100, 120, 0))
            #  sustain
            sus = round(simpleio.map_range(flights[f].distance, 0, 100, 127, 0))
            #  velocity
            vel = round(simpleio.map_range(flights[f].distance, 0, 150, 120, 0))
            modulation = int(mod)
            sustain = int(sus)
            #  create sustain and modulation CC message
            pedal = ControlChange(71, sustain)
            modWheel = ControlChange(1, modulation)
            #  send the sustain and modulation messages
            midi.send([modWheel, pedal])
            #  if tof registers a height lower than the set max height...
            if int(flights[f].distance) < flight_height and not plucks[f]:
                #  set state tracker
                plucks[f] = True
                #  convert tof distance to a velocity value
                velocity = int(vel)
                #  send midi note with velocity and sustain message
                midi.send([NoteOn(notes[f], velocity), pedal])
            #  if tof registers a height = to or greater than set max height
            #  aka you remove your hand from above the sensor...
            if int(flights[f].distance) > flight_height and plucks[f]:
                #  reset state
                plucks[f] = False
                #  send midi note off
                midi.send(NoteOff(notes[f], velocity))

Upload the Code and Libraries to the Feather M4 Express

After downloading the Project Bundle, plug your Feather M4 Express into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the Feather M4 Express' CIRCUITPY drive. 

  • lib folder
  • code.py

Your Feather M4 Express CIRCUITPY drive should look like this after copying the lib folder and the code.py file.

circuitpy

Additional Examples

In addition to the main code.py file, there are two more CircuitPython code files that you can use for different features. 

The usb_midi_code.py file has code to allow the laser harp to be used as a USB MIDI controller. This way you can control your favorite software synth or DAW.

The laser_harp_two_voice.py file has extra features that build on the original code.py file. It can send out two different synth instruments depending on the height that the time of flight sensors detect. You can also control pitch bend or volume by raising or lowering your hand while playing a note. 

To use either of these files instead of the original code.py file, remove the code.py file from your CIRCUITPY drive and rename your chosen .py file as code.py.

How the CircuitPython Code Works

The code begins by setting up I2C to use board.SCL and board.SDA. The TCA9548A I2C multiplexer is setup as tca.

# Create I2C bus as normal
i2c = board.I2C()  # uses board.SCL and board.SDA

# Create the TCA9548A object and give it the I2C bus
tca = adafruit_tca9548a.TCA9548A(i2c)

VL53L4CD Setup with the TCA9548A

The time of flight sensors all use the same I2C address, 0x29. When they are setup using the adafruit_vl53l4cd library, their I2C pins are set as different channels on the TCA9548A.

The time of flight sensors are inserted into an array called flights. A for statement is used to setup each time of flight sensor with inter_measurement and timing_budget values, along with the function start_ranging() to begin reading data.

#  setup time of flight sensors to use TCA9548A inputs
tof_0 = adafruit_vl53l4cd.VL53L4CD(tca[0])
tof_1 = adafruit_vl53l4cd.VL53L4CD(tca[1])
tof_2 = adafruit_vl53l4cd.VL53L4CD(tca[2])
tof_3 = adafruit_vl53l4cd.VL53L4CD(tca[3])
tof_4 = adafruit_vl53l4cd.VL53L4CD(tca[4])
tof_5 = adafruit_vl53l4cd.VL53L4CD(tca[5])
tof_6 = adafruit_vl53l4cd.VL53L4CD(tca[6])
tof_7 = adafruit_vl53l4cd.VL53L4CD(tca[7])

#  array of tof sensors
flights = [tof_0, tof_1, tof_2, tof_3, tof_4, tof_5, tof_6, tof_7]

#  setup each tof sensor
for flight in flights:
    flight.inter_measurement = 0
    flight.timing_budget = 50
    flight.start_ranging()

MIDI Over UART Setup

MIDI is setup to use UART. The Music Maker FeatherWing takes in MIDI over UART to be used as a synth.

#  midi uart setup for music maker featherwing
uart = busio.UART(board.TX, board.RX, baudrate=31250)

midi_in_channel = 1
midi_out_channel = 1
#  midi setup
#  UART is setup as the input
midi = adafruit_midi.MIDI(
    midi_in=uart,
    midi_out=uart,
    in_channel=(midi_in_channel - 1),
    out_channel=(midi_out_channel - 1),
    debug=False,
)

Variables and States

There are a few variables that you may want to change depending on your preferences.

  • flight_height is used as a maximum height value for the time of flight sensors. Adjust this to increase or decrease the expected range for the laser harp
  • notes is the array of MIDI notes assigned to each time of flight sensor. Change these numbers to play different notes
  • The midi.send(ProgramChange(80) message changes the instrument sound being used by the Music Maker FeatherWing. You can change the number to set a different instrument sound. Check out this reference page for a list of possible sounds and their numbers.
#  height cutoff for tof sensors
#  adjust depending on the height of your ceiling/performance area
flight_height = 150

#  state of each tof sensor
#  tracks if you have hit the laser range
pluck_0 = False
pluck_1 = False
pluck_2 = False
pluck_3 = False
pluck_4 = False
pluck_5 = False
pluck_6 = False
pluck_7 = False

#  array of tof sensor states
plucks = [pluck_0, pluck_1, pluck_2, pluck_3, pluck_4, pluck_5, pluck_6, pluck_7]

#  midi notes for each tof sensor
notes = [48, 52, 55, 59, 60, 64, 67, 71]

#  midi instrument voice
midi.send(ProgramChange(80))

The Loop

In the loop, the time of flight sensors are iterated through and their values are read. The values are mapped to different MIDI parameters: modulation, sustain and velocity. Modulation and sustain are sent as a MIDI message together.

while True:
    #  iterate through the 8 tof sensors
    for f in range(8):
        while not flights[f].data_ready:
            pass
        #  reset tof sensors
        flights[f].clear_interrupt()
        #  if the reading from a tof is not 0...
        if flights[f].distance != 0.0:
            #  map range of tof sensor distance to midi parameters
            #  modulation
            mod = round(simpleio.map_range(flights[f].distance, 0, 100, 120, 0))
            #  sustain
            sus = round(simpleio.map_range(flights[f].distance, 0, 100, 127, 0))
            #  velocity
            vel = round(simpleio.map_range(flights[f].distance, 0, 150, 120, 0))
            modulation = int(mod)
            sustain = int(sus)
            #  create sustain and modulation CC message
            pedal = ControlChange(71, sustain)
            modWheel = ControlChange(1, modulation)
            #  send the sustain and modulation messages
            midi.send([modWheel, pedal])

Playing the Laser Harp

If the time of flight sensor detects a height that is lower than the maximum threshold set as flight_height, the assigned MIDI note is sent with a NoteOn message. If the time of flight sensor detects a height that is equal to or lower than flight_height, a NoteOff message is sent.

#  if tof registers a height lower than the set max height...
            if int(flights[f].distance) < flight_height and not plucks[f]:
                #  set state tracker
                plucks[f] = True
                #  convert tof distance to a velocity value
                velocity = int(vel)
                #  send midi note with velocity and sustain message
                midi.send([NoteOn(notes[f], velocity), pedal])
            #  if tof registers a height = to or greater than set max height
            #  aka you remove your hand from above the sensor...
            if int(flights[f].distance) > flight_height and plucks[f]:
                #  reset state
                plucks[f] = False
                #  send midi note off
                midi.send(NoteOff(notes[f], velocity))

This guide was first published on May 10, 2022. It was last updated on May 10, 2022.

This page (Coding the Laser Harp) was last updated on May 27, 2023.

Text editor powered by tinymce.