|
| 1 | +# SPDX-FileCopyrightText: Copyright (c) 2026 Liz Clark for Adafruit Industries |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: MIT |
| 4 | +'''QT Py RP2040 MIDI Breath Controller with BMP585''' |
| 5 | +import time |
| 6 | +import board |
| 7 | +import simpleio |
| 8 | +from adafruit_bmp5xx import BMP5XX |
| 9 | +from adafruit_seesaw import digitalio, neopixel, rotaryio, seesaw |
| 10 | +import usb_midi |
| 11 | +import adafruit_midi |
| 12 | +from adafruit_midi.control_change import ControlChange |
| 13 | +from adafruit_midi.channel_pressure import ChannelPressure |
| 14 | + |
| 15 | +SEALEVELPRESSURE_HPA = 1013.25 |
| 16 | +change_sense = 0.1 # change sensitivity |
| 17 | +low_press = 995 # lowest pressure reading range |
| 18 | +high_press = 1032 # highest pressure reading range |
| 19 | + |
| 20 | +i2c = board.STEMMA_I2C() |
| 21 | +bmp = BMP5XX.over_i2c(i2c) |
| 22 | +bmp.sea_level_pressure = SEALEVELPRESSURE_HPA |
| 23 | + |
| 24 | +seesaw = seesaw.Seesaw(i2c, addr=0x36) |
| 25 | +seesaw.pin_mode(24, seesaw.INPUT_PULLUP) |
| 26 | +button = digitalio.DigitalIO(seesaw, 24) |
| 27 | +encoder = rotaryio.IncrementalEncoder(seesaw) |
| 28 | +pixel = neopixel.NeoPixel(seesaw, 6, 1) |
| 29 | +pixel.brightness = 0.2 |
| 30 | + |
| 31 | +midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0) |
| 32 | +# CC message channels |
| 33 | +# last index is place holder for Channel Pressure message type |
| 34 | +messages = [1, 2, 7, 64, 0] |
| 35 | +# neopixel colors to associate with CC messages |
| 36 | +colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), |
| 37 | + (255, 255, 0), (255, 0, 255)] |
| 38 | + |
| 39 | +pressure = 0 |
| 40 | +mod_val1 = 0 |
| 41 | +mod_val2 = 0 |
| 42 | +effect_index = 0 |
| 43 | +last_position = 0 |
| 44 | +button_state = False |
| 45 | +pixel.fill(colors[effect_index]) |
| 46 | + |
| 47 | +while True: |
| 48 | + position = -encoder.position |
| 49 | + if position != last_position: |
| 50 | + # encoder changes midi CC message type |
| 51 | + if position > last_position: |
| 52 | + effect_index = (effect_index + 1) % len(messages) |
| 53 | + else: |
| 54 | + effect_index = (effect_index - 1) % len(messages) |
| 55 | + pixel.fill(colors[effect_index]) |
| 56 | + last_position = position |
| 57 | + if not button.value and not button_state: |
| 58 | + pixel.fill((255, 255, 255)) |
| 59 | + # button press sends MIDI panic |
| 60 | + panic = ControlChange(123, 0) |
| 61 | + midi.send(panic) |
| 62 | + # and turns sustain off |
| 63 | + sus_off = ControlChange(64, 0) |
| 64 | + midi.send(sus_off) |
| 65 | + button_state = True |
| 66 | + if button.value and button_state: |
| 67 | + # reset neopixel to CC msg color |
| 68 | + pixel.fill(colors[effect_index]) |
| 69 | + button_state = False |
| 70 | + if bmp.data_ready: |
| 71 | + # get pressure reading |
| 72 | + pressure = bmp.pressure |
| 73 | + # if the pressure has changed enough |
| 74 | + # (adjust change_sense value at top to inc or dec) |
| 75 | + if abs(pressure - mod_val2) > change_sense: |
| 76 | + # map pressure reading to CC range |
| 77 | + mod_val1 = round(simpleio.map_range(pressure, low_press, high_press, 0, 127)) |
| 78 | + # updates previous value to hold current value |
| 79 | + mod_val2 = pressure |
| 80 | + # MIDI data has to be sent as an integer |
| 81 | + modulation = int(mod_val1) |
| 82 | + # possible midi messages determined by effect_index value: |
| 83 | + # 1: modulation |
| 84 | + # 2: breath controller |
| 85 | + # 7: volume |
| 86 | + # 64: sustain |
| 87 | + # ChannelPressure(modulation) |
| 88 | + if effect_index < 4: |
| 89 | + # prep CC message with CC number and value as mapped pressure reading |
| 90 | + modWheel = ControlChange(messages[effect_index], modulation) |
| 91 | + else: |
| 92 | + # prep Channel Pressure message with value as mapped pressure reading |
| 93 | + modWheel = ChannelPressure(modulation) |
| 94 | + # CC message is sent |
| 95 | + midi.send(modWheel) |
| 96 | + # print(modWheel) |
| 97 | + # delay to settle MIDI data |
| 98 | + time.sleep(0.001) |
0 commit comments