goofy2k / MIDI-sequencer

1 stars 1 forks source link

MIDI-sequencer

MIDI sequencer/recorder functions are a too heavy load for the Faust DSP application on a TTGO TAudio V1.6 board. The DSP task is already pretty heavy in it's own, so it may be useful to dedicate the firmware of the audio board as much as possible to it's core task: synthesising audio.

A solution is to add additional functionality to a second board, that does not need to have an audio codec. This second board can send MIDI commands to the audio board over a suitable interface. The received timestamped MIDI commands are delivered in the right order and can be played "immediately", without further processing except for using a queue and timers for respecting the synchronized MIDI timing.

The MIDI sequencer board can be connected with the synthesizer board over bluetooth. Additionally, the sequencer board may have a wired (UART) connection with a MIDI instrument, such as a keyboard.

REMARKS:

Scan ESP-ADF for this kind of applications. NOT AVAILABLE

ESP-IDF and MIDI:

this is about applying BLE for MIDI

Arduino and MIDI:

Both examples (ESP-IDF and Arduino) are based on the Arduino BLE-MIDI transport lib. Also have a look at this Arduino MIDI Library. It may be wise to build the sequencer based on this in the Arduino environment. Allthough, then it is not possible to use thejdksmidi lib? Which can also be an advantage ;-)

For sequencing functionality, just use Google Arduino MIDI sequencer and you will find a lot, such as this Old-School Arduino MIDI Sequencer that may contain useful info on the wired connectivity.

Step 1: Create Arduino BLE app for testing

Running the 01-Basic-Midi-Device.ino example on a TTGO Lora32 board. See incoming MIDI data with the nRF Connect app on a mobile phone

Step 2: Add BT/BLE client to the TTGO TAudio app

Add MIDIBLE to the Faust DSP firmware: faust_mqtt_tcp6_nb_v6

Note: the .ino app is probably not based on Nimble. Is it compatible with devices running Nimble BLE?

More info on Nimble BLE: here
Basic server and client examples and a lot of information, here

nRF Connect diagnostic tool on mobile can write and receive data from the app (on command of nRF Connect)

nRF Connect diagnostic tool on mobile can write and receive data from the app (pushed by the .ino app)

How to connect both apps to each other????

Create basic Nimble server and client examples

A lot of information about Nimble BLE, including examples can be found here. The examples in the repo code (called esp-nimble-server and esp-nimble-client here) are different from those in the New User Guide (called esp-nimble-server2 and esp-nimble-client here2). Both examples run on the TTGO TAudio and the Heltec LORA32 boards. With the first ones data transfer has been shown.

Further investigations needed:

  1. which role can push data to the other one? We do not want to let the Audio board do polling! Read this 1 and specifically bullets 3 and 7 under data transfer.
  2. how to show data transfer between server2 and client2?
  3. look for the option that is lightest for the audio board.

Based on the role definitions during creation of the connection and during operation a first guess for implementation of the MIDI synthesizer / sequencer application would be let the sequencer have the master and server roles and the let the audio board be the slave and client. Note: master and slave are roles during making of the connection and client/ server describe roles after the connection has been established. Peripheral and Central are equivalents of slave resp. master.

Note: for compatibility, make sure that these roles are compatible with existing applications, such as Android apps. You can have a look at the configuration of existing MIDI BLE sound modules, e.g. here or here
https://blog.adafruit.com/2021/09/07/optimizing-ble-midi-with-regards-to-timing-bluetooth-midi-nordictweets/
Have a look at something like a MIDI BLE specification.
Because we have a semi-realtime application where we don't want the audio board give the task for polling for new events, we want the sequencer to push data to the audio board. So data transfer is done via the notify mechanism. Now that role definitions are clear, the MIDI BLE specification is kind of clear, you have to know what should be the actual BLE role of a MIDI device of a type. try to collect pairs of MIDI ble apps to sniff the roles.

Sequencer app (Nimble master/Central and server)

The output must be in the order that the events must be handled by a connected audio generating device.

This app will be built around the nimble_notify basic example from this library A working example is avaliable here as esp_nimble_notify_V2

Audio/DSP app (Nimble slave/Peripheral and slave)

Implementation plan

  1. Adapt the esp_nimble_notify_V2 example to send a a stream of single notes.
  2. Add the client part to the TTGO TAudio board demonstrate receipt of the data
  3. DONE (faust_ble_midi_v1) Add the BT client to the Faust DSP application: faust_ble_midi_v1 had to dismantle WIFI and MQTT to reduce firmware size
  4. DONE (faust_ble_midi_v1) Show incoming Nimble MIDI commands from the sequencer, while playing the metronome on the soundboard
  5. DONE (faust_ble_midi_v2) Demonstrate playing of the single notes based on incoming commands
  6. DONE (faust_ble_midi_v2) Add ESP logging to the sequencer
  7. DONE (faust_ble_midi_v2) Add WIFI/MQTT functionality to the "sequencer" to communicate with Nodered in the same way as the Faust DSP app now does (version faust_mqtt_tcp6_nb_v5)
  8. DONE (fckx_seq_v3) Demonstrate playing single notes entered via Nodered (use 5 byte buffer in Nodered), play immediately in API (/fckx_seq/midi/single)
  9. Re-use the browser-based Faust GUI for the sequencer. Just use appropriate MQTT topics
  10. Add more sequencing functionality, using e.g. the jdksmidi library. Be compliant with the MIDI specifications
  11. On success (matching the functionality of the current faust_mqtt_tcp6_nb_v5 firmware) remove the basic sequencing functionality from the DSP firmware
  12. Introduce visible or audible feedback to the user about BLE advertising and connection status
  13. Play MIDI from the web or a file
  14. Introduce interface with MIDI instrument, sucha the Yamaha PSR 500

Input / output specifications

Starting points

  1. The primary application for the sequencer is to act as source of input for synthesiser applications on the TTGO TAudio board
  2. Besides it's main task of generating digital synthesized audio, the TTGO TAudio board offers limited capabilities for processing of incoming commands
  3. Communication between the audio board and the sequencer should as much as possible comply with existing standards

Based on the above, we start with defining the spec for the sound board and derive sequencer specs from that.

Sound board I/O specifications

Sequencer I/O specifications

Output

Input

Sequencer mode(s) of operation

Sequencer implementation

As the operation involves a number of different tasks that are also time-critical, the sequencer implementation is based on using freeRTOS elements. This includes tasks, timers but may also include freeRTOS queues for efficient handling and communication between the tasks (under investigation). Important aspects: can a freeRTOS queue be sorted? Can you insert an element into a freeRTOS queue in a position of your choice? Can you hack cues to mimick that?

Sequencer tasks

Task in italics still to be implemented

  1. Maintain a MIDI clock / beat
  2. Output commands for an audible metronome
  3. Temporary storage of incoming events in order of receipt. This involves adding a timestamp representing the moment of receipt
  4. Send MIDI commands to the output for immediate playing. This may involve an output buffer that is emptied as fast as possible over the NimBLE interface. Note: this can involve commands that have just been received (MIDI through) or commands that are output by e.g. a looping task.

4b. Cast incoming MIDI into the MIDIMsg type

OPTION 1:

  1. Append (incoming) commands to an input queue with a timestamp for the moment of receipt
  2. Insert each message from the input queue into a track corresponding with it's channel number. After completing this task all tracks are in order of intended moment of execution (i.e. in order of the timestamps in this queue). When e.g. recording in a loop, this involves conversion of the timestamp of receipt in a timestamp for execution. This conversion depends on implementation of Task 1

OPTION 2:

  1. Insert (incoming) commands directly into a track with a timestamp, at the position representing it's timestamp (possibly adapted e.g. to fit it in a playing loop)

The option (2) for having a queue that is always sorted is attractive, but may be time critical.
It may become less time critical, when an input buffer is used for later insertion (option 1).

Progress on tasks

Partly executing the example, untill a runtime error is thrown at removal of the tick component
(resolved in v8 by only PARTLY BYPASSING Init of the tick component (only calls to RtMidi) i.s.o. COMPLETELY)

E (2939) APP_MAIN: Testing NiCMidi functionality: MidiMessage //erroneous message, repaired in v8 Starting the component ... Waiting 10 secs ... Stopping the component ... Waiting 5 secs without playing ... Exiting Executing MIDIManager::Init() BYPASSED !!! contains calls to RtMidi //is Init called at Exit??? //YES, at bool MIDIManager::RemoveMIDITick(MIDITickComponent* tick) Guru Meditation Error: Core 0 panic'ed (LoadProhibited). Exception was unhandled.

Core 0 register dump: PC : 0x400d5d66 PS : 0x00060e30 A0 : 0x800d70d3 A1 : 0x3ffbb450 0x400d5d66: MIDIManager::RemoveMIDITick(MIDITickComponent) at c:\users\fred.espressif\tools\xtensa-esp32-elf\esp-2020r3-8.4.0\xtensa-esp32-elf\xtensa-esp32-elf\include\c++\8.4.0\bits/stl_vector.h:806 (discriminator 1) (inlined by) MIDIManager::RemoveMIDITick(MIDITickComponent) at c:\users\fred\esp_projects\midi-sequencer\fckx_sequencer_v7\build/../main/manager_dirty.cpp:229 (discriminator 1)

A2 : 0x3ffbb4b0 A3 : 0x00000000 A4 : 0x3ffc3b98 A5 : 0x00000000 A6 : 0x00000000 A7 : 0xff000000 A8 : 0x800d5d5d A9 : 0x00000000 A10 : 0x00000000 A11 : 0x0000000a A12 : 0x00000007 A13 : 0x00000000 A14 : 0x00000000 A15 : 0x00000001 SAR : 0x00000016 EXCCAUSE: 0x0000001c EXCVADDR: 0x00000000 LBEG : 0x400014fd LEND : 0x4000150d LCOUNT : 0xffffffee

Backtrace:0x400d5d63:0x3ffbb450 0x400d70d0:0x3ffbb470 0x400d4e30:0x3ffbb490 0x400d5680:0x3ffbb4b0 0x400d5b1b:0x3ffbb520 0x400d39ab:0x3ffbb640 0x400d5d63: MIDIManager::RemoveMIDITick(MIDITickComponent*) at c:\users\fred\esp_projects\midi-sequencer\fckx_sequencer_v7\build/../main/manager_dirty.cpp:229 (discriminator 1)

0x400d70d0: MIDITickComponent::~MIDITickComponent() at c:\users\fred\esp_projects\midi-sequencer\fckx_sequencer_v7\build/../main/tick.cpp:32

0x400d4e30: TestComp::~TestComp() at c:\users\fred\esp_projects\midi-sequencer\fckx_sequencer_v7\build/../main/main.cpp:825

0x400d5680: main at c:\users\fred\esp_projects\midi-sequencer\fckx_sequencer_v7\build/../main/main.cpp:912

0x400d5b1b: app_main at c:\users\fred\esp_projects\midi-sequencer\fckx_sequencer_v7\build/../main/main.cpp:1033

0x400d39ab: main_task at C:/Users/Fred/esp-idf/components/esp32/cpu_start.c:600

Status in version 8: no runtime error (removed INFO logs of the MQTT client and handlers)

E (2819) APP_MAIN: Testing NiCMidi functionality: MIDItimer MIDITickComponent, MIDIManager E (2899) APP_MAIN: Testing NiCMidi functionality: test_component.cpp Starting the component ... Waiting 10 secs ... Stopping the component ... Waiting 5 secs without playing ... Exiting Executing MIDIManager::Init() PARTLY BYPASSED !!! contains calls to RtMidi Executing MIDIManager::Init() Exiting MIDIManager::Init() Found 0 midi out and 0 midi in

**Try to "silently" play the notes. Only show the screen logs. These are missing now. It looks like the TickProc is not called. This is most likely due to the fact that MIDIManager::AddMIDITick(&comp); is commented.

Status in version 9: no runtime error
Plays audible note to soundboard, without runtime errors! Need to check the MIDI codes that arrive at the board. Added logging of received MIDI messages to it's firmware.

Next steps:

Wrap the bluetooth (NimBLE) MIDI output port in driver.h/.cpp

In the TickProc for the test_component example the out of MIDI messages over the NinBLE bluetooth interface is done by sendToMIDIOut(msg) instead of MIDIManager::GetOutDriver(0)->OutputMessage(msg). If we modify the code behind GetOutDriver to use the NimBLE interface we prevent that we have to adapt the call in the entire library.

manager.cpp / class MIDIManager uses a call static MIDIOutDriver* GetOutDriver(unsigned int n) to get a pointer to the output driver

Change of patching strategy

The patched NiCMidi has become a pretty mess in v10. Files have been moved, renamed etc. New strategy:

Executing MIDIManager::Init() MidiOutDummy: This class provides no functionality. MidiInDummy: This class provides no functionality. Exiting MIDIManager::Init() Found 0 midi out and 0 midi in

abort() was called at PC 0x4014ba8f on core 0

0x400dbd2f: MIDISequencer::MIDISequencer(MIDIMultiTrack, MIDISequencerGUINotifier) at c:\users\fred\esp_projects\midi-sequencer\fckx_sequencer_v11_new_no_rtmidi\build/../components/NiCMidi/src/sequencer.cpp:361

0x400d5d35: AdvancedSequencer::AdvancedSequencer(MIDISequencerGUINotifier*) at c:\users\fred\esp_projects\midi-sequencer\fckx_sequencer_v11_new_no_rtmidi\build/../components/NiCMidi/src/advancedsequencer.cpp:114

sequencer.cpp:361 :
if (!MIDIManager::IsValidOutPortNumber(0))
throw RtMidiError("MIDISequencer needs almost a MIDI out port in the system\n", RtMidiError::INVALID_DEVICE);

It is time to bypass RTMidiError and use the NimBLE interface...

The dirty option: call Nimble if such a throw is on hand Better, replace calls to RtMidi by call to your own RtBLEMidi class !

BE AWARE of the limited firmware space! There is probably room for 4 MB! Have a look at partition settings! DONE!

Have a look at void MIDIManager::Init() and provide your own RtBLEMidi

Next step in implementation

V12 contains all (yet empty) API functions for NimBLE via RtMidi (dirty/hacked version). NimBLE out has been phased out of the main app and should be taken over by NiCMidi/RtMidi. Implement the API functions an make sure you maintain the same interface.

I Give In: no success...YET

Most versions before V13 are with RtMidi. V13 is without, buth this is too complicated..... see remarks by NicMidi (211203)
V13 route: rewrite NiCMidi driver to access already running nimBLE driver

V12 route: use RTMidi, but give it access to already running nimBLE driver via a GLOBAL class
Strange error in V12: /main/main.cpp:563: undefined reference to `NimBLEGluer::NimBLEGluer()' There is no code ther AT ALL referencing NimBLEGluer ! ????

Both routes probably need a global object that contains driver info after this has been instantiated / started

v25:

v26:

v27:

v28:

v29:

v30:

v31:

Re 10a and 4. :

Sequencer commands

Are issued over MQTT with topic /fckx_seq/command The message payload consists of byte sequences, pretty much lik MIDI commands The following table shows the commands

Nr Code Description main code line
1. 0x01 start recording recorder.Start()
2. 0x02 stop recording recorder.Stop()
0x03
0x04 dump recorder
3. 0x11 play sequencer.Start()
4. 0x12 stop sequencer.Stop()
5. 0x13 rewind sequencer.GoToZero()
6. 0x14 dump sequencer
7.
8.
9. 0x21 start thru
10. 0x22 stop thru
11.
12.
13.