adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
Other
4.1k stars 1.22k forks source link

Macropad MIDI loses connection after a few minutes #5101

Closed ATMakersBill closed 3 years ago

ATMakersBill commented 3 years ago

CircuitPython version

Adafruit CircuitPython 7.0.0-alpha.5 on 2021-07-21; Adafruit Macropad RP2040 with rp2040

Code/REPL

//This is the code from John Park's Ableton learning guide

# SPDX-FileCopyrightText: 2021 John Park for Adafruit Industries
# SPDX-License-Identifier: MIT
# Ableton Live Macropad Launcher
# In Ableton, choose "Launchpad Mini Mk3" as controller with MacroPad 2040 as in and out
# Use empty fifth scene to allow "unlaunching" of tracks with encoder modifier
import board
from adafruit_macropad import MacroPad
import displayio
import terminalio
from adafruit_simplemath import constrain
from adafruit_display_text import label
import usb_midi
import adafruit_midi
from adafruit_midi.control_change import ControlChange
from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn
from adafruit_midi.midi_message import MIDIUnknownEvent

macropad = MacroPad()

TITLE_TEXT = "Live Launcher 2040"
print(TITLE_TEXT)
TRACK_NAMES = ["DRUM", "BASS", "SYNTH"]  # Customize these
LIVE_CC_NUMBER = 74  # CC number to send w encoder
FADER_TEXT = "cutoff"  # change for intended CC name

# --- MIDI recieve is complex, so not using macropad.midi
midi = adafruit_midi.MIDI(
                        midi_in=usb_midi.ports[0],
                        in_channel=(0, 1, 2),
                        midi_out=usb_midi.ports[1],
                        out_channel=0
)

# ---Official Launchpad colors---
LP_COLORS = (
    0x000000, 0x101010, 0x202020, 0x3f3f3f, 0x3f0f0f, 0x3f0000, 0x200000, 0x100000,
    0x3f2e1a, 0x3f0f00, 0x200800, 0x100400, 0x3f2b0b, 0x3f3f00, 0x202000, 0x101000,
    0x213f0c, 0x143f00, 0x0a2000, 0x051000, 0x123f12, 0x003f00, 0x002000, 0x001000,
    0x123f17, 0x003f06, 0x002003, 0x001001, 0x123f16, 0x003f15, 0x00200b, 0x001006,
    0x123f2d, 0x003f25, 0x002012, 0x001009, 0x12303f, 0x00293f, 0x001520, 0x000b10,
    0x12213f, 0x00153f, 0x000b20, 0x000610, 0x0b093f, 0x00003f, 0x000020, 0x000010,
    0x1a0d3e, 0x0b003f, 0x060020, 0x030010, 0x3f0f3f, 0x3f003f, 0x200020, 0x100010,
    0x3f101b, 0x3f0014, 0x20000a, 0x100005, 0x3f0300, 0x250d00, 0x1d1400, 0x080d01,
    0x000e00, 0x001206, 0x00051b, 0x00003f, 0x001113, 0x040032, 0x1f1f1f, 0x070707,
    0x3f0000, 0x2e3f0b, 0x2b3a01, 0x183f02, 0x032200, 0x003f17, 0x00293f, 0x000a3f,
    0x06003f, 0x16003f, 0x2b061e, 0x0a0400, 0x3f0c00, 0x213701, 0x1c3f05, 0x003f00,
    0x0e3f09, 0x153f1b, 0x0d3f32, 0x16223f, 0x0c1430, 0x1a1439, 0x34073f, 0x3f0016,
    0x3f1100, 0x2d2900, 0x233f00, 0x201601, 0x0e0a00, 0x001203, 0x031308, 0x05050a,
    0x050716, 0x190e06, 0x200000, 0x36100a, 0x351204, 0x3f2f09, 0x27370b, 0x192c03,
    0x05050b, 0x36341a, 0x1f3a22, 0x26253f, 0x23193f, 0x0f0f0f, 0x1c1c1c, 0x373f3f,
    0x270000, 0x0d0000, 0x063300, 0x011000, 0x2d2b00, 0x0f0c00, 0x2c1400, 0x120500,
)

LP_PADS = {
    81: 0, 82: 1, 83: 2,
    71: 3, 72: 4, 73: 5,
    61: 6, 62: 7, 63: 8,
    51: 9, 52: 10, 53: 11
}

LIVE_NOTES = [81, 82, 83, 71, 72, 73, 61, 62, 63, 51, 52, 53]
CC_OFFSET = 20
modifier = False  # use to add encoder switch modifier to keys for clip mute
MODIFIER_NOTES = [41, 42, 43, 41, 42, 43, 41, 42, 43, 41, 42, 43]  # blank row in Live

last_position = 0  # encoder position state

# ---NeoPixel setup---
BRIGHT = 0.125
DIM = 0.0625
macropad.pixels.brightness = BRIGHT

# ---Display setup---
display = board.DISPLAY
screen = displayio.Group()
display.show(screen)
WIDTH = 128
HEIGHT = 64
FONT = terminalio.FONT
# Draw a title label
title = TITLE_TEXT
title_area = label.Label(FONT, text=title, color=0xFFFFFF, x=6, y=3)
screen.append(title_area)

# --- create display strings and positions
x1 = 5
x2 = 35
x3 = 65
y1 = 17
y2 = 27
y3 = 37
y4 = 47
y5 = 57

# ---Push knob text setup
push_text_area = label.Label(FONT, text="[o]", color=0xffffff, x=WIDTH-22, y=y2)
screen.append(push_text_area)

# ---CC knob text setup
fader_text_area = label.Label(FONT, text=FADER_TEXT, color=0xffffff, x=WIDTH - 42, y=y4)
screen.append(fader_text_area)
# --- cc value display
cc_val_text = str(CC_OFFSET)
cc_val_text_area = label.Label(FONT, text=cc_val_text, color=0xffffff, x=WIDTH - 20, y=y5)
screen.append(cc_val_text_area)

label_data = (
    # text, x, y
    (TRACK_NAMES[0], x1, y1), (TRACK_NAMES[1], x2, y1), (TRACK_NAMES[2], x3, y1),
    (".", x1, y2), (".", x2, y2), (".", x3, y2),
    (".", x1, y3), (".", x2, y3), (".", x3, y3),
    (".", x1, y4), (".", x2, y4), (".", x3, y4),
    (".", x1, y5), (".", x2, y5), (".", x3, y5)
)

labels = []

for data in label_data:
    text, x, y = data
    label_area = label.Label(FONT, text=text, color=0xffffff)
    group = displayio.Group(x=x, y=y)
    group.append(label_area)
    screen.append(group)
    labels.append(label_area)  # these are individually addressed later

num = 1

while True:
    msg_in = midi.receive()
    if isinstance(msg_in, NoteOn) and msg_in.velocity != 0:
        print(
            "received NoteOn",
            "from channel",
            msg_in.channel + 1,
            "MIDI note",
            msg_in.note,
            "velocity",
            msg_in.velocity,
            "\n"
        )
    # send neopixel lightup code to key, text to display
        if msg_in.note in LP_PADS:
            macropad.pixels[LP_PADS[msg_in.note]] = LP_COLORS[msg_in.velocity]
            macropad.pixels.show()
            if msg_in.velocity == 21:  # active pad is indicated by Live as vel 21
                labels[LP_PADS[msg_in.note]+3].text = "o"
            else:
                labels[LP_PADS[msg_in.note]+3].text = "."

    elif isinstance(msg_in, NoteOff):
        print(
            "received NoteOff",
            "from channel",
            msg_in.channel + 1,
            "\n"
        )

    elif isinstance(msg_in, NoteOn) and msg_in.velocity == 0:
        print(
            "received NoteOff",
            "from channel",
            msg_in.channel + 1,
            "MIDI note",
            msg_in.note,
            "velocity",
            msg_in.velocity,
            "\n"
        )

    elif isinstance(msg_in, ControlChange):
        print(
            "received CC",
            "from channel",
            msg_in.channel + 1,
            "controller",
            msg_in.control,
            "value",
            msg_in.value,
            "\n"
        )

    elif isinstance(msg_in, MIDIUnknownEvent):
        # Message are only known if they are imported
        print("Unknown MIDI event status ", msg_in.status)

    elif msg_in is not None:
        midi.send(msg_in)

    key_event = macropad.keys.events.get()  # check for keypad events

    if not key_event:  # Event is None; no keypad event happened, do other stuff

        position = macropad.encoder  # store encoder position state
        cc_position = int(constrain((position + CC_OFFSET), 0, 127))  # lock to cc range
        if last_position is None or position != last_position:

            if position < last_position:
                midi.send(ControlChange(LIVE_CC_NUMBER, cc_position))
                print("CC", cc_position)
                cc_val_text_area.text = str(cc_position)

            elif position > last_position:
                midi.send(ControlChange(LIVE_CC_NUMBER, cc_position))
                print("CC", cc_position)
                cc_val_text_area.text = str(cc_position)
        last_position = position

        macropad.encoder_switch_debounced.update()  # check the encoder switch w debouncer
        if macropad.encoder_switch_debounced.pressed:
            print("Mod")
            push_text_area.text = "[.]"
            modifier = True
            macropad.pixels.brightness = DIM

        if macropad.encoder_switch_debounced.released:
            modifier = False
            push_text_area.text = "[o]"
            macropad.pixels.brightness = BRIGHT

        continue

    num = key_event.key_number

    if key_event.pressed and not modifier:
        midi.send(NoteOn(LIVE_NOTES[num], 127))
        print("\nsent note", LIVE_NOTES[num], "\n")

    if key_event.pressed and modifier:
        midi.send(NoteOn(MODIFIER_NOTES[num], 127))

    if key_event.released and not modifier:
        midi.send(NoteOff(LIVE_NOTES[num], 0))

    if key_event.released and modifier:
        midi.send(NoteOff(MODIFIER_NOTES[num], 0))

    macropad.pixels.show()

Behavior

I have been using this code to send MIDI events that can then be used for controlling OBS Studio (using the obs-midi plugin) or other systems. I have tested only on Windows 10.

This code sends Note On/Off when the keys on the Macropad and CC events when the encoder is rotated.

An easy way to view them is to use https://www.onlinemusictools.com/webmiditest/

After a few minutes (especially of inactivity), the events will stop. I have looked into several things with info below.

Description

I have connected to the REPL using Mu and even after the MIDI stops being sent, the REPL still shows the debugging output being printed such as ... CC 21 CC 22 CC 21 CC 20 ... When turning the encoder.

After the MIDI is no longer connected, you can Control-C into the REPL and it will usually be in the debouncer code... when you Control-D to restart the board it runs fine but never sends MIDI events over USB again until you hit the hardware Reset button.

Interestingly, if you break into the REPL while the software IS sending MIDI messages, and then hit Control-D to restart the code it DOES still work. So it seems something is causing the USB connection to fail without stopping the Python code from running. Perhaps something in TinyUSB?

Additional information

No response

jedgarpark commented 3 years ago

I'm suspecting this could be Windows 10 related, I'll try testing it too.

ATMakersBill commented 3 years ago

Also tested with the simpler code from this project instead https://learn.adafruit.com/adafruit-macropad-rp2040/macropad-midi

Same issues

jedgarpark commented 3 years ago

I am not able to reproduce the error when testing on Windows 10

hathach commented 3 years ago

probably similar to #4190

update: other user has issue with win10 as well, so maybe it is slightly different

dhalbert commented 3 years ago

I chatted with @ATMakersBill last night about the details of this, and gave him some more recent builds to try, which did not help. It does sound like #4190. I have seen #4190-like symptoms on Ubuntu Linux 20.04 without using MIDI at all, with a quiescent connection, while doing some audio testing.

It is merely the connection that is broken. I can reconnect CDC and resume where I left off.

@jedgarpark Try https://www.onlinemusictools.com/webmiditest/ as Bill did and leave it quiescent (don't press any keys) for 5-10 minutes. Then turn the encoder and see if you see any MIDI messages. If not, then it has quietly disconnected.

ATMakersBill commented 3 years ago

So... perhaps this will help - I think this might be an interaction problem between the CP code (or maybe TinyUSB) and the Dell hardware/firmware/drivers. I noticed on #4190 that the user was on a Dell XPS. I am testing on my Dell Inspiron 7506 2n1 and getting the issue consistently. On the same machine, I tested a Behringer Control Surface (hardware MIDI device) and it ran continually all night without problem. I also moved the Macropad to my wife's MSI laptop and IT ran all night continuously without a problem.

So perhaps the Dell driver is sending some USB control message (like @hathach suggests in #4190) that we are not handling correctly? I have other CP and TinyUSB-compatible hardware here (lots, actually) if there's anything you folks would like me to test. I thought I'd put a basic CP script that sends MIDI Note Down/Note UP every few seconds on a spare Feather M4? Would you rather I try Arduino/TinyUSB?

jedgarpark commented 3 years ago

@dhalbert I left it quiescent for 10 minutes on a Windows 10 PC (not a Dell, my son's DIY gaming rig) without problems. I also left it overnight on my MacBook Pro without any problems.

hathach commented 3 years ago

I am working on rp2040 suspend/disconnect detection since I think it may be the cause. The problem is I couldn't reproduce the issue on my local machine. Though I made an cpy with usb debug message enabled (in the other issue). @ATMakersBill would you mind trying it out (you will need to hooked its gpio0,1 to FTDI/CP210x for the log).

I have enabled the tinyusb log output to UART @115200 baudrate. It print out all the usb information with level 2 and level 3. Would you mind using this for testing on your device, then post your log as attached filed here for analysis (since it print out quite a lots specially msc communicate very frequent). pico-debug-level.zip

Originally posted by @hathach in https://github.com/adafruit/circuitpython/issues/4190#issuecomment-886409558

hathach commented 3 years ago

@ATMakersBill I tried my best to update tinyusb to address this issue. However, since all my PC cannot reproduce the issue. Would you mind trying this out again. zip contains uf2 for for pico and macropad with multiple debug level (0, 2, 3) . 0 is no debug just for verifying (in case debug log accidentally resolve it by slowing down cpu) Note: pico uart tx is GPIO0, macropad, uart tx is GPIO20 (SDA in qtstemma connector). Looking forward to your feedback.

midi-usb-connection.zip

dhalbert commented 3 years ago

I tried the Ableton demo program on a Dell Optiplex 3020 (WIndows 10) and a 7010 (Ubuntu 20.04) and was not able to reproduce the problem. These have similar motherboards. In both cases I was not using a USB hub. So either it seems very specific to certain USB controllers or else there is some OS-related issue that is not present all the time.

tannewt commented 3 years ago

@ATMakersBill We've changed a number of things and this issue is likely fixed. I'm closing now but will reopen if you test and find that it is still an issue. Either way, I don't want to block 7.0.0 on this.