Simple USB Audio Controller in CircuitPython

I have plans to build a multi-function macro keyboard using the new Raspberry Pi Pico RP2040 microcontroller and CircuitPython. While I was waiting for it to arrive, I wanted to figure out how I was going to handle controlling the audio on my computer with a rotary encoder, so I built this simple project using CircuitPython and the QT Py M0 microcontroller. Unfortunately, rotaryio has not been implemented for the RP2040 yet in CircuitPython, so there is a bonus code snippet at the end using a potentiometer to accomplish the same thing for the Pi Pico.

Parts

3D Printed SnapFit Enclosure

I modified the enclosure I made for my Zoom/Teams mute button and computer lock button projects.

CAD files for the QT Py rotary encoder enclosure.

Wiring Diagram

wiring diagram for the QT Py and rotary encoder

Code

The code.py file is also available on GitHub.

import board
import time
import usb_hid
from digitalio import DigitalInOut, Direction, Pull
from rotaryio import IncrementalEncoder
from adafruit_hid.consumer_control_code import ConsumerControlCode
from adafruit_hid.consumer_control import ConsumerControl

# Change the below code for different outcomes
# https://circuitpython.readthedocs.io/projects/hid/en/latest/

# Button Press will mute
BUTTON_CODE = ConsumerControlCode.MUTE

# Rotating the encoder clockwise will increase the volume
INCREMENT_CODE = ConsumerControlCode.VOLUME_INCREMENT

# Rotating the encoder clockwise will decrease teh volume
DECREMENT_CODE = ConsumerControlCode.VOLUME_DECREMENT

# initialize as hid device
consumer_control = ConsumerControl(usb_hid.devices)

# initialize encoder on pins D0 and D1 (QT Py M0)
encoder = IncrementalEncoder(board.D0, board.D1)

# initialize encoder click on pin D2 (QT Py M0)
button = DigitalInOut(board.D2)
button.direction = Direction.INPUT
button.pull = Pull.UP

button_in = False
last_position = None

while True:

    if not button.value and not button_in:
        print("button press")
        button_in = True
        consumer_control.send(BUTTON_CODE)
        time.sleep(.2)

    elif button.value and button_in:
        button_in = False

    elif button.value and not button_in:
        position = encoder.position

        if last_position is not None and position != last_position:

            if position > last_position:
                print("rotate clockwise")
                consumer_control.send(INCREMENT_CODE)

            elif position < last_position:
                print("rotate counter-clockwise")
                consumer_control.send(DECREMENT_CODE)

        last_position = position

Bonus: Pi Pico 2040 Potentiometer

Raspberry Pi Pico 2040 with potentiometer
import board
import time
import usb_hid
from analogio import AnalogIn
from adafruit_hid.consumer_control_code import ConsumerControlCode
from adafruit_hid.consumer_control import ConsumerControl

READ_TIME = .001

def map_function(x, in_min, in_max, out_min, out_max):

  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;


# initialize hid device as consumer control 
consumer_control = ConsumerControl(usb_hid.devices)

# initialize potentiometer (pot) wiper connected to GP26_A0
potentiometer = AnalogIn(board.GP26)  

# intialize the read time
last_read = time.monotonic()

# decrease volume all the way down 
# this allows the volume to be set by the current value of the pot
for i in range(32):
    consumer_control.send(ConsumerControlCode.VOLUME_DECREMENT)


# initalize volume and last position
current_volume = 0
last_position = 0



while True:

    if time.monotonic() - last_read > READ_TIME:
        position = int(map_function(potentiometer.value , 200, 65520, 0, 32))

        if abs(position - last_position) > 1:

            last_position = position

            if current_volume < position:
                while current_volume < position:
                    # Raise volume.
                    print("Volume Up!")
                    consumer_control.send(ConsumerControlCode.VOLUME_INCREMENT)
                    current_volume+= 2
            elif current_volume > position:
                while current_volume > position:
                    # Lower volume.
                    print("Volume Down!")
                    consumer_control.send(ConsumerControlCode.VOLUME_DECREMENT)
                    current_volume -= 2

        # update last_read to current time
        last_read = time.monotonic()

    # handle time.monotonic() overflow
    if time.monotonic() < last_read:
        last_read = time.monotonic()