sensorium / Mozzi

sound synthesis library for Arduino
https://sensorium.github.io/Mozzi/
GNU Lesser General Public License v2.1
1.05k stars 184 forks source link

MIDI port disappearing when adding Mozzi to RP2040 #263

Open leobel96 opened 1 week ago

leobel96 commented 1 week ago

Hello, I have this very simple script on a Raspberry Pico W that is working as expected without Mozzi (I'm able to send MIDI packets and to see the messages being printed):

#include <Arduino.h>
#include <Adafruit_TinyUSB.h>
#include <MIDI.h>

#include "user.h"
#include "main.h"

// USB MIDI object
Adafruit_USBD_MIDI usb_midi;

// Create a new instance of the Arduino MIDI Library,
// and attach usb_midi as the transport.
MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, MIDI);

void setup() {
  // Manual begin() is required on core without built-in support e.g. mbed rp2040
  if (!TinyUSBDevice.isInitialized()) {
    TinyUSBDevice.begin(0);
  }

  pinMode(LED_BUILTIN, OUTPUT);

  usb_midi.setStringDescriptor("TinyUSB MIDI");

  // Initialize MIDI, and listen to all MIDI channels
  // This will also call usb_midi's begin()
  MIDI.begin(MIDI_CHANNEL_OMNI);

  MIDI.setHandleNoteOn(handleNoteOn);
  MIDI.setHandleNoteOff(handleNoteOff);
  MIDI.setHandleControlChange(handleControlChange);

  Serial.begin(115200);
}

void loop() {
  #ifdef TINYUSB_NEED_POLLING_TASK
  // Manual call tud_task since it isn't called by Core's background
  TinyUSBDevice.task();
  #endif

  // not enumerated()/mounted() yet: nothing to do
  if (!TinyUSBDevice.mounted()) {
    return;
  }
  // read any new MIDI messages
  MIDI.read();
}

void handleNoteOn(byte channel, byte pitch, byte velocity) {
  // Log when a note is pressed.
  Serial.print("Note on: channel = ");
  Serial.print(channel);

  Serial.print(" pitch = ");
  Serial.print(pitch);

  Serial.print(" velocity = ");
  Serial.println(velocity);
}

void handleNoteOff(byte channel, byte pitch, byte velocity) {
  // Log when a note is released.
  Serial.print("Note off: channel = ");
  Serial.print(channel);

  Serial.print(" pitch = ");
  Serial.print(pitch);

  Serial.print(" velocity = ");
  Serial.println(velocity);
}

void handleControlChange(byte channel, byte pitch, byte velocity) {
  // Log when a note is released.
  Serial.print("Control change: channel = ");
  Serial.print(channel);

  Serial.print(" pitch = ");
  Serial.print(pitch);

  Serial.print(" velocity = ");
  Serial.println(velocity);
}

When I add Mozzi, though, the MIDI port doesn't get opened at all and no message is printed:

#include <Arduino.h>
#include <Adafruit_TinyUSB.h>
#include <MIDI.h>

#define MOZZI_CONTROL_RATE 128 // Hz, powers of 2 are most reliable
#include <Mozzi.h>

#include "user.h"
#include "main.h"

// USB MIDI object
Adafruit_USBD_MIDI usb_midi;

// Create a new instance of the Arduino MIDI Library,
// and attach usb_midi as the transport.
MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, MIDI);

void setup() {
  // Manual begin() is required on core without built-in support e.g. mbed rp2040
  if (!TinyUSBDevice.isInitialized()) {
    TinyUSBDevice.begin(0);
  }

  pinMode(LED_BUILTIN, OUTPUT);

  usb_midi.setStringDescriptor("TinyUSB MIDI");

  // Initialize MIDI, and listen to all MIDI channels
  // This will also call usb_midi's begin()
  MIDI.begin(MIDI_CHANNEL_OMNI);

  MIDI.setHandleNoteOn(handleNoteOn);
  MIDI.setHandleNoteOff(handleNoteOff);
  MIDI.setHandleControlChange(handleControlChange);

  Serial.begin(115200);

  startMozzi();
}

void updateControl() {
  #ifdef TINYUSB_NEED_POLLING_TASK
  // Manual call tud_task since it isn't called by Core's background
  TinyUSBDevice.task();
  #endif

  // not enumerated()/mounted() yet: nothing to do
  if (!TinyUSBDevice.mounted()) {
    return;
  }
  // read any new MIDI messages
  MIDI.read();
}

void handleNoteOn(byte channel, byte pitch, byte velocity) {
  // Log when a note is pressed.
  Serial.print("Note on: channel = ");
  Serial.print(channel);

  Serial.print(" pitch = ");
  Serial.print(pitch);

  Serial.print(" velocity = ");
  Serial.println(velocity);
}

void handleNoteOff(byte channel, byte pitch, byte velocity) {
  // Log when a note is released.
  Serial.print("Note off: channel = ");
  Serial.print(channel);

  Serial.print(" pitch = ");
  Serial.print(pitch);

  Serial.print(" velocity = ");
  Serial.println(velocity);
}

void handleControlChange(byte channel, byte pitch, byte velocity) {
  // Log when a note is released.
  Serial.print("Control change: channel = ");
  Serial.print(channel);

  Serial.print(" pitch = ");
  Serial.print(pitch);

  Serial.print(" velocity = ");
  Serial.println(velocity);
}

AudioOutput updateAudio(){
  return MonoOutput::from16Bit(1234);
}

void loop() {
  audioHook(); // required here
}

Here a diff of what I changed to make your life easier:

4a5,7
> #define MOZZI_CONTROL_RATE 128 // Hz, powers of 2 are most reliable
> #include <Mozzi.h>
> 
34a38,39
> 
>   startMozzi();
37c42
< void loop() {
---
> void updateControl() {
85a91,98
> 
> AudioOutput updateAudio(){
>   return MonoOutput::from16Bit(1234);
> }
> 
> void loop() {
>   audioHook(); // required here
> }

This is my platformio.ini

[env:pico]
platform = https://github.com/maxgerhardt/platform-raspberrypi.git
framework = arduino
board_build.core = earlephilhower
board = rpipicow
monitor_speed = 9600
lib_deps =
    adafruit/Adafruit TinyUSB Library@^3.3.1
    sensorium/Mozzi@^2.0.0
build_flags =
    -DDEBUG_RP2040_PORT=Serial1
    -DUSE_TINYUSB
    -Iinclude/
    ; -DCFG_TUSB_CONFIG_FILE=\"custom_tusb_config.h\"
    -DMOZZI_AUDIO_MODE=MOZZI_OUTPUT_I2S_DAC
    -DPIO_FRAMEWORK_ARDUINO_ENABLE_EXCEPTIONS
tfry-git commented 5 days ago

Hi,

sorry for the delay. First idea: Your debug port appears to be Serial1. Isn't that pins 0, and 1, by default? That'd be the same as the default Mozzi output pins on RP2040. If so, try using either Serial2, instead, or change the Mozzi output pins (see https://sensorium.github.io/Mozzi/learn/configuration/ and https://sensorium.github.io/Mozzi/doc/html/hardware_rp2040.html).

leobel96 commented 4 days ago

Hello and thank you for your reply. I tried changing the debug port to Serial2 but nothing changed unfortunately 😞. Do you have any way of reproducing it on your side by any chance?

tfry-git commented 4 days ago

Hello and thank you for your reply. I tried changing the debug port to Serial2 but nothing changed unfortunately 😞. Do you have any way of reproducing it on your side by any chance?

I'll try. But how do I even go about this? MIDI is connected to the USB port, right, and then an FTDI attached for serial? To which pins?

leobel96 commented 4 days ago

No FTDI connected, the serialport enables the serial messages through the usb interface from my understanding https://arduino-pico.readthedocs.io/en/latest/serial.html

tfry-git commented 4 days ago

Ok, but so where is MIDI attached in your setup? Can't have both on the same USB port, can you?

tomcombriat commented 4 days ago

From the Doc I thought you would not need an ftdi, and do everything like it is done in teensy (with a select of how the usb should be seen by the computer) but I also do not have a clear view and no certitude, please enlighten :) I'm actually quite interested in having usb-midi on the Pico, and discovered this library with this issue. I'll try to give it a shot but I'm a bit away from breadboard at the moment...

leobel96 commented 4 days ago

Yeah, you can have both MIDI and Serial on USB. It works fine in the first example I posted without Mozzi

tomcombriat commented 4 days ago

Random guess, before testing myself, have you tried putting Mozzi completely on the other core of the RP2040?

leobel96 commented 4 days ago

What do you mean with "other core", sorry?

leobel96 commented 4 days ago

Oh, I just noticed https://arduino-pico.readthedocs.io/en/latest/multicore.html, I'll give it a try :)

tomcombriat commented 4 days ago

Big interest of the Pico :)

In brief, put all the calls for one library in loop and setup (like your first example), and for the other in setup1 and loop1.

That's especially interesting for audio when you want the audio generation to run without any blockage caused by slow hardware or operations

leobel96 commented 4 days ago

So, I tried the following code:

#include <Arduino.h>
#include <Adafruit_TinyUSB.h>
#include <MIDI.h>

#define MOZZI_CONTROL_RATE 128 // Hz, powers of 2 are most reliable
#include <Mozzi.h>
#include <Oscil.h> // oscillator template
#include <tables/sin2048_int8.h> // sine table for oscillator
#include <mozzi_midi.h>
#include <ADSR.h>

#include "user.h"
#include "main.h"

// USB MIDI object
Adafruit_USBD_MIDI usb_midi;

// Create a new instance of the Arduino MIDI Library,
// and attach usb_midi as the transport.
MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, MIDI);

// audio sinewave oscillator
Oscil <SIN2048_NUM_CELLS, MOZZI_AUDIO_RATE> aSin(SIN2048_DATA);

// envelope generator
ADSR <MOZZI_CONTROL_RATE, MOZZI_AUDIO_RATE> envelope;

void setup() {
  // Manual begin() is required on core without built-in support e.g. mbed rp2040
  if (!TinyUSBDevice.isInitialized()) {
    TinyUSBDevice.begin(0);
  }

  pinMode(14, OUTPUT);

  usb_midi.setStringDescriptor("TinyUSB MIDI");

  // Initialize MIDI, and listen to all MIDI channels
  // This will also call usb_midi's begin()
  MIDI.begin(MIDI_CHANNEL_OMNI);

  MIDI.setHandleNoteOn(handleNoteOn);
  MIDI.setHandleNoteOff(handleNoteOff);
  MIDI.setHandleControlChange(handleControlChange);

  Serial.begin(9600);
}

void setup1() {
  envelope.setADLevels(255,64);
  envelope.setTimes(50,200,10000,200); // 10000 is so the note will sustain 10 seconds unless a noteOff comes

  aSin.setFreq(440); // default frequency
  startMozzi();
  digitalWrite(14, HIGH);
}

void loop() {
  #ifdef TINYUSB_NEED_POLLING_TASK
  // Manual call tud_task since it isn't called by Core's background
  TinyUSBDevice.task();
  #endif

  // not enumerated()/mounted() yet: nothing to do
  if (!TinyUSBDevice.mounted()) {
    return;
  }
  // read any new MIDI messages
  MIDI.read();
}

void handleNoteOn(byte channel, byte pitch, byte velocity) {
  // Log when a note is pressed.
  Serial.print("Note on: channel = ");
  Serial.print(channel);

  Serial.print(" pitch = ");
  Serial.print(pitch);

  Serial.print(" velocity = ");
  Serial.println(velocity);

  aSin.setFreq(mtof(float(pitch)));
  envelope.noteOn();
  digitalWrite(14, LOW);
}

void handleNoteOff(byte channel, byte pitch, byte velocity) {
  // Log when a note is released.
  Serial.print("Note off: channel = ");
  Serial.print(channel);

  Serial.print(" pitch = ");
  Serial.print(pitch);

  Serial.print(" velocity = ");
  Serial.println(velocity);

  envelope.noteOff();
}

void handleControlChange(byte channel, byte pitch, byte velocity) {
  // Log when a note is released.
  Serial.print("Control change: channel = ");
  Serial.print(channel);

  Serial.print(" pitch = ");
  Serial.print(pitch);

  Serial.print(" velocity = ");
  Serial.println(velocity);
}

void updateControl(){
  // not enumerated()/mounted() yet: nothing to do
  if (!TinyUSBDevice.mounted()) {
    return;
  }
  envelope.update();
}

void loop1() {
  audioHook(); // required here
}

AudioOutput updateAudio(){
  return MonoOutput::from16Bit(envelope.next() * aSin.next());
}

And the MIDI port doesn't disappear anymore BUT the messages are still not being printed when a MIDI note is being received nor the LED is turned on (meaning that, probably, the execution never goes past the startMozzi() call). If I move that digitalWrite(14, HIGH); to the end of the setup(), it turns on as expected.

If you want to quickly test MIDI, I suggest you to use the mido library for python and use the script I wrote to send simple commands:

import mido

print(mido.get_output_names())

outport = mido.open_output("Pico W")

outport.send(mido.Message("control_change", control=3, value=10))
outport.send(mido.Message("control_change", control=4, value=100))
outport.send(mido.Message("control_change", control=5, value=127))
outport.send(mido.Message("control_change", control=6, value=40))

outport.send(mido.Message("note_on", note=60, velocity=40))
outport.send(mido.Message("note_on", note=61, velocity=40))
outport.send(mido.Message("note_on", note=62, velocity=40))
outport.send(mido.Message("note_on", note=63, velocity=40))
outport.send(mido.Message("note_on", note=64, velocity=40))
outport.send(mido.Message("note_on", note=65, velocity=40))
outport.send(mido.Message("note_on", note=66, velocity=40))
outport.send(mido.Message("note_on", note=67, velocity=40))
leobel96 commented 4 days ago

Actually, I just realised that I set the wrong serial port in my PC 🤦🏻‍♂️ and the prints work as well but I think that the library still has issues because everything I put in the second core after startMozzi() (either in the setup1 itself or in loop1) never gets executed

tfry-git commented 3 days ago

Ok, I finally tried, and after some initial trouble, it all seems to work for me without trouble (single core variant).

I.e. literally the sketch from your initial post (the one including Mozzi, of course) works for me, with the following differences:

It may be noteworthy, that after some initial experiments, I updated my rp2040 core to version 3.9.3 (don't remember which version I had, before). Before that, independent of Mozzi, no MIDI port got registered without an explicit call to usb_midi.setCableName(). I don't know what's going on here, but I do suggest one of the first things to try is to update the rp2040 core.


Edit: Afterthought: I have not tested anything dual-core, now, and not that much time, right now. I do suspect it's simply a reentrancy problem, though: You're calling a TinyUSB method from updateControl(), which will be on core 1, while the TinyUSB task is on core 0. That's quite expected to spell trouble. If you do want to investigate the dual-core approach, further, you'll have to cache the read from TinyUSBDevice.mounted() in a volatile bool from core 0, and only read that bool in core 1. You're going to run into similar (and more annoying) trouble with MIDI.read(), too, after that.

leobel96 commented 1 day ago

#include "user.h" and #include "main.h" were just containing the definitions of the functions I declared in the code. I tried using the Arduino IDE as you suggested and everything is working as expected! I don't know why I'm seeing those issues with Platformio and I tried updating the RP2040 core there as well but I cannot make it work. I think I'll stick with the Arduino IDE for now. Thanks a lot for your help! About the dual core approach, moving the TinyUSBDevice.mounted() to the core0 (i.e. loop()) should work (I should have put it there from the beginning tbf).