├── .gitignore ├── .gitmodules ├── .vscode ├── c_cpp_properties.json ├── launch.json └── settings.json ├── CMakeLists.txt ├── LICENSE ├── README.md ├── keylab_essential_mc_filter.c ├── midi_app.c ├── midi_filter.h ├── midi_mc_fader_pickup.c ├── midi_mc_fader_pickup.h ├── pico_sdk_import.cmake ├── pictures ├── pico-usb-midi-filter-bottom-view.jpg ├── pico-usb-midi-filter-top-view.jpg └── pico-usb-midi-filter-with-picoprobe.jpg ├── tinyusb_patches └── 0001-Allow-defining-CFG_TUD_ENDPOINT0_SIZE-as-a-function.patch ├── tusb_config.h ├── usb_descriptors.c └── usb_descriptors.h /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .vscode/.cortex-debug.peripherals.state.json 3 | .vscode/.cortex-debug.registers.state.json 4 | 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/usb_midi_host"] 2 | path = lib/usb_midi_host 3 | url = https://github.com/rppicomidi/usb_midi_host 4 | [submodule "lib/usb_midi_dev_ac_optional"] 5 | path = lib/usb_midi_dev_ac_optional 6 | url = https://github.com/rppicomidi/usb_midi_dev_ac_optional.git 7 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [], 9 | "compilerPath": "/usr/bin/gcc", 10 | "cStandard": "gnu17", 11 | "cppStandard": "gnu++14", 12 | "intelliSenseMode": "linux-gcc-x64", 13 | "configurationProvider": "ms-vscode.cmake-tools", 14 | "compileCommands": "${workspaceFolder}/build/compile_commands.json" 15 | } 16 | ], 17 | "version": 4 18 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Pico Debug", 6 | "type":"cortex-debug", 7 | "cwd": "${workspaceRoot}", 8 | "executable": "${command:cmake.launchTargetPath}", 9 | "request": "launch", 10 | "servertype": "external", 11 | // This may need to be arm-none-eabi-gdb depending on your system 12 | "gdbPath" : "gdb-multiarch", 13 | // Connect to an already running OpenOCD instance 14 | "gdbTarget": "localhost:3333", 15 | "svdFile": "${env:PICO_SDK_PATH}/src/rp2040/hardware_regs/rp2040.svd", 16 | "runToEntryPoint": "main", 17 | // Work around for stopping at main on restart 18 | "postRestartCommands": [ 19 | "break main", 20 | "continue" 21 | ] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // These settings tweaks to the cmake plugin will ensure 3 | // that you debug using cortex-debug instead of trying to launch 4 | // a Pico binary on the host 5 | "cmake.statusbar.advanced": { 6 | "debug": { 7 | "visibility": "hidden" 8 | }, 9 | "launch": { 10 | "visibility": "hidden" 11 | }, 12 | "build": { 13 | "visibility": "hidden" 14 | }, 15 | "buildTarget": { 16 | "visibility": "hidden" 17 | } 18 | }, 19 | "cmake.buildBeforeRun": true, 20 | "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", 21 | "cortex-debug.variableUseNaturalFormat": false, 22 | "files.associations": { 23 | "limits": "c", 24 | "new": "c", 25 | "midi_filter.h": "c", 26 | "clocks.h": "c" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | include (pico_sdk_import.cmake) 3 | project(pico_usb_midi_filter) 4 | # To use with the Adafruit RP2040 Feather with Type A Host board (see 5 | # https://learn.adafruit.com/adafruit-feather-rp2040-with-usb-type-a-host) 6 | # or a compatible circuit, either set the environment variable 7 | # PICO_BOARD to adafruit_feather_rp2040_usb_host or run cmake from the build directory as 8 | # cmake -DPICO_BOARD=adafruit_feather_rp2040_usb_host .. 9 | pico_sdk_init() 10 | 11 | add_executable(pico_usb_midi_filter) 12 | 13 | add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/lib/usb_midi_host) 14 | add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/lib/usb_midi_dev_ac_optional) 15 | pico_enable_stdio_uart(pico_usb_midi_filter 1) 16 | 17 | target_sources(pico_usb_midi_filter PRIVATE 18 | midi_app.c 19 | usb_descriptors.c 20 | keylab_essential_mc_filter.c 21 | midi_mc_fader_pickup.c 22 | ) 23 | target_link_options(pico_usb_midi_filter PRIVATE -Xlinker --print-memory-usage) 24 | target_compile_options(pico_usb_midi_filter PRIVATE -Wall -Wextra) 25 | if (${PICO_BOARD} MATCHES adafruit_feather_rp2040_usb_host) 26 | message("Set PIO defaults for Adafruit RP2040 Feather with USB Type A Host") 27 | target_compile_definitions(pico_usb_midi_filter PRIVATE 28 | USE_ADAFRUIT_FEATHER_RP2040_USBHOST=1 29 | ) 30 | else() 31 | message("Set PIO defaults for Pico board; unused GP22 will be driven high") 32 | target_compile_definitions(pico_usb_midi_filter PRIVATE 33 | PICO_DEFAULT_UART_TX_PIN=16 34 | PICO_DEFAULT_UART_RX_PIN=17 35 | PICO_DEFAULT_PIO_USB_DP_PIN=0 36 | ) 37 | endif() 38 | 39 | target_include_directories(pico_usb_midi_filter PRIVATE ${PICO_PIO_USB_SRC} ${CMAKE_CURRENT_LIST_DIR}) 40 | 41 | target_link_libraries(pico_usb_midi_filter PRIVATE pico_stdlib pico_multicore hardware_pio hardware_dma tinyusb_board tinyusb_device 42 | tinyusb_host tinyusb_pico_pio_usb usb_midi_host_app_driver usb_midi_device_app_driver) 43 | pico_add_extra_outputs(pico_usb_midi_filter) 44 | 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 rppicomidi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pico-usb-midi-filter 2 | 3 | ## Description 4 | This project uses the Raspberry Pi Pico built-in USB port and an additional USB port created from the PIOs 5 | to make a MIDI data filter. You plug the PC into the Raspberry Pi Pico's MicroUSB port. You plug your 6 | MIDI keyboard or other MIDI device into the added USB Host port. The added USB Host port enumerates your MIDI device 7 | and then initializes the Pico's USB Device port so it looks to your PC just the same as the downstream 8 | USB device. Because the Pico now sits between your PC and your MIDI device, it can maniputlate the MIDI 9 | data stream to filter it as required. 10 | 11 | This program demostrates translating some 12 | Mackie Control protocol button messages from the Arturia Keylab Essential 88 to other Mackie Control protocol 13 | button messages so that they work correctly with the Cubase DAW, and it demostrates Mackie Control fader pickup (because 14 | it seems Arturia Keylab Essential only supports pickup with HUI. See the [FAQ](https://support.arturia.com/hc/en-us/articles/4405741358738-KeyLab-Essential-Tips-Tricks)). However, the code is 15 | structured so that it may be modified to perform just about any other 16 | filtering. That is, this code should serve as a reasonable starting point for other more sophisticated processing such as 17 | 18 | - Removing MIDI clock or Activie Sensing messages 19 | - Transposing notes 20 | - Re-scaling note velocity 21 | - Inserting controller messages or program change messages 22 | - Changing MIDI channel for notes in a range (to perform keyboard split, for example). 23 | - etc. 24 | 25 | Any brand name stuff I mention in this README file is just documentation of what I did. 26 | This is not an advertisement. I don't get paid to do this. 27 | 28 | This project would not have been possible without the code of the TinyUSB project and the 29 | Pico-PIO-USB project. Thank you to the developers for publishing the source on github. 30 | 31 | If you find issues with this document or with the code, please report them on this project's github page. 32 | 33 | Note that this project may not work well for if you operate it in a noisy environment. 34 | The Pico-PIO-USB PIO program does not implement a differential receiver. It only samples 35 | the USB D+ pin. Also, if your project requires USB-IF certification, this project won't work for you. 36 | 37 | ## Hardware 38 | 39 | This project uses the RP2040's built-in USB port as the USB downstream MIDI device port (connects to a 40 | PC's USB host port or an upstream USB hub port). It also uses two GPIO pins, PIO0 and ARM core core1 of the RP2040 41 | to create an upstream USB MIDI host port (connects 42 | to your MIDI keyboard or other MIDI device). The circuit board and the MIDI device get power from the PC host port 43 | or upstream USB hub. Because this project uses the RP2040's on-board USB for MIDI, it can't use it for 44 | stdio debug prints. This project uses UART0 for the stdio debug UART. If your project needs the debug UART pins for something else, please 45 | disable the stdio UART in the Cmake file. 46 | 47 | The Pico board circuit described here has no current limiting, ESD protection, or other safeguards. Measure voltages carefully and 48 | please don't hook this to your expensive computer and MIDI equipment until you have done some testing with at least 49 | some basic test equipment. It uses GP0 and GP1 (Pico board pins 1 and 2) for USB host D+ and D-, and it uses pins GP16 50 | (Pico Board pin 21) and GP17 (Pico Board pin 22) for the debug UART0 I/O. The LED is on RP2040 GP25. 51 | 52 | If you don't want to make your own hardware from a Pico board and want a bit more robust circuit, 53 | you can configure this project to work with the 54 | [Adafruit RP2040 Feather with Type A USB Host](https://learn.adafruit.com/adafruit-feather-rp2040-with-usb-type-a-host) 55 | board. That board uses GP16 and GP17 for 56 | the USB Host, GP18 to enable the USB host power supply, and GP0 and GP1 for the UART interface. The LED is on GP13. 57 | Sadly, this board does not support the RP2040's Single Wire Debug interface. Skip to the [Software](#software) section 58 | if you are using this board. 59 | 60 | ### Wiring the USB Host Port 61 | I used a [Sparkfun CAB-10177](https://www.sparkfun.com/products/10177) with the green and white 62 | wires (D+ and D- signals) swapped on the 4-pin female header connector. I soldered a 4-pin male header 63 | to pins 1-3 of the Pico board and left one of the pins hanging off the edge of the board. I soldered 64 | a wire from that pin hanging off the edge of the board to Pin 40 of the Pico board (VBus). I then 65 | plugged the 4-pin female header connector so the black wire (ground) connects to Pin 3 of the Pico 66 | board, the red wire connects to pin hanging off the edge of the Pico board, the green wire connects 67 | to pin 1 of the Pico board, and the white wire connects to pin 2 of the Pico board. If you want to 68 | add series termination resistors to D+ and D-, resistors between 22 and 33 ohms are probably close. I didn't bother and it seemed good enough for my testing. 69 | 70 | Here is a photo of the bottom view wiring. 71 | 72 | ![](pictures/pico-usb-midi-filter-bottom-view.jpg) 73 | 74 | Here is a photo of the top view wiring. 75 | 76 | ![](pictures/pico-usb-midi-filter-top-view.jpg) 77 | 78 | ### Wiring for a Pico Probe 79 | I like to use a Debugprobe (formerly called a picoprobe) (see Appendix A of the [Getting Started with 80 | Raspberry Pi Pico](https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf) guide) 81 | for code development in a Microsoft Visual Studio Code IDE. I soldered in a 3-pin male header on the 82 | 3 DEBUG pins at the bottom edge of the Pico board. I also soldered a 2-pin male header to pins 83 | 21 and 22 (GPIO pins GP16 and GP17) for the debug UART. I wired the GND, SWCLK and SWDIO as shown 84 | in the Appendix. However, because I used Pico board pins 1 and 2 for the USB host, I had to wire 85 | the debug UART to different pins. Connect the Pico A UART 1 Rx on Pico A pin 7 to Pico B UART0 Tx signal on Pico B 86 | Pin 21 (not 1). Connect the Pico A UART Tx on Pico A pin 6 and the Pico B UART0 Rx signal to target Pico board pin 22 (not 2). 87 | 88 | Here is a photo of my complete setup with the Pico Probe attached. The Pico Probe board is the lower Pico board. 89 | 90 | ![](pictures/pico-usb-midi-filter-with-picoprobe.jpg) 91 | 92 | ### This is not commercial quality hardware 93 | The USB host port hardware created from the PIO is not compliant 94 | with the USB specification, but it seems to work OK in my experimental setup. The specification 95 | deviations that I know about are: 96 | 97 | - The D+ and D- pins are wired directly to the I/O pins and don't use termination resistors to match 98 | the 50 ohm impedance the USB spec recommends. You can try to do better by adding in-line 99 | resistors as described above. 100 | - As mentioned, the USB port receiver is not differential. The software that decodes the USB signaling only uses 101 | the D+ signal to decode the NRZI signal. 102 | - I wired in no current limiting for the USB host port Vbus supply. It relies on the current limiting 103 | from the upstream PC's USB host that supplies power to the Pico board. 104 | 105 | Someone who has an oscilloscope capable of looking at the USB eye pattern should recommend correct resistor 106 | values to fix termination issues given you can't easily place resistors close to the pins on the RP2040 107 | chip on an off-the-shelf Raspberry Pi Pico board. 108 | 109 | ## Software 110 | This project relies on the [Raspberry Pi Pico SDK](https://github.com/raspberrypi/pico-sdk), 111 | which includes the TinyUSB library. For best results, please use version 2.0 or later 112 | of the Pico SDK. This project can work with version 1.5.1 of the Pico SDK with some tweaks. 113 | 114 | This project also relies on the TinyUSB library and usb_midi_host library. 115 | 116 | ### Special Development Environment Setup 117 | As of this writing, the Pico SDK version 2.0 needs some adjustment 118 | before it will work with this project. Follow first the instructions [here](https://github.com/rppicomidi/usb_midi_host/blob/main/README.md#building-cc-applications). You can ignore the section called `Building the usb_midi_host Library in Your Project`. 119 | 120 | ### Install the project software 121 | Set your current directory to some project directory `${PROJECTS}` 122 | (e.g. `${HOME}/projects/pico`) and then clone the pico-usb-midi-filter project: 123 | 124 | ``` 125 | cd ${PROJECTS} 126 | git clone https://github.com/rppicomidi/pico-usb-midi-filter.git 127 | ``` 128 | 129 | ### Patching the TinyUSB library 130 | TinyUSB has a few checks in it that break the ability of the USB Device 131 | driver to copy the USB descriptor from the device plugged to the USB host 132 | port. The project code will not build unless you apply these patches. 133 | Because this is special purpose, I suggest you apply the patches in 134 | a branch of the TinyUSB git submodule: 135 | ``` 136 | git checkout -b ep0sz-fn 137 | git apply ${PROJECTS}/pico-usb-midi-filter/tinyusb_patches/0001-Allow-defining-CFG_TUD_ENDPOINT0_SIZE-as-a-function.patch 138 | git add src/ 139 | git commit -m 'Allow defining CFG_TUD_ENDPOINT0_SIZE as a function' 140 | ``` 141 | Note: If you need to update TinyUSB in the future, follow these steps 142 | from the `${PICO_SDK_PATH}/lib/tinyusb` directory to undo the patch: 143 | ``` 144 | git checkout master 145 | git branch -D ep0sz-fn 146 | git pull 147 | ``` 148 | Then update TinyUSB and repeat the patching process above. 149 | 150 | ### Building from the command line and loading the Pico software using the file system interface 151 | 152 | The command line build process is pretty common for most Pico projects. I have the pico-sdk installed in $HOME/projects/pico/pico-sdk. 153 | 154 | ``` 155 | export PICO_SDK_PATH=$HOME/projects/pico/pico-sdk/ 156 | cd [the parent directory where you cloned this project]/pico-usb-midi-filter 157 | mkdir build 158 | cd build 159 | cmake .. 160 | make 161 | ``` 162 | To make a version of code for the Adafruit RP2040 Feather With USB Type A Host board 163 | or a circuit with similar GPIO pin usage, replace `cmake ..` with 164 | ``` 165 | cmake -DPICO_BOARD=adafruit_feather_rp2040_usb_host .. 166 | ``` 167 | If the board is a Pico board but you are wiring the USB Host pins like the Feather board, use 168 | ``` 169 | cmake -DPICO_BOARD=pico -DUSE_ADAFRUIT_FEATHER_RP2040_USBHOST=1 .. 170 | ``` 171 | This process should generate the file `pico-usb-midi-filter\build\pico_usb_midi_filter.uf2`. 172 | Connect a USB cable to your PC. Do not connect it to your Pico board yet. 173 | Hold the BOOTSEL button on your Pico board and plug the cable to your Pico board's microUSB 174 | connector. The Pico should automount on your computer. Use the PC file manager to drag and 175 | drop the `pico-usb-midi-filter.uf2` file to the Pico "file system". The Pico should 176 | automatically unmount when the file copy is complete. 177 | 178 | ### Building and debugging using VS Code and the Pico Probe 179 | If you are using the Raspberry Pi Pico VS Extension for VS Code, click 180 | the Pico icon on the left toolbar and import this project. If you are using 181 | an older setup, the hidden `.vscode` directory will configure the project for 182 | VS Code if you use the file menu to open this project folder as long as the 183 | environment variable `PICO_SDK_PATH` points to the `pico-sdk` directory and 184 | the build toolchain is in your `PATH` environment variable. 185 | 186 | To make a version of code for the Adafruit RP2040 Feather With USB Type A Host board, 187 | set the `PICO_BOARD` CMake variable as described in the [usb_midi_host README](https://github.com/rppicomidi/usb_midi_host?tab=readme-ov-file#vs-code-build). 188 | If the board is a Pico board but you are wiring the USB Host pins like the Feather board, 189 | set `-DPICO_BOARD=pico` and add another item `-DUSE_ADAFRUIT_FEATHER_RP2040_USBHOST=1` 190 | using the pre-0.15.2 Pico VS Code extension workflow. 191 | 192 | Once you have successfully imported or opened this project, you should 193 | be able to build and run the code by following the instructions in the 194 | [Getting started with Raspberry Pi Pico](https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf) document. 195 | 196 | ## Testing 197 | 198 | Before messing with a DAW, plug your MIDI device to the Pico's added USB host port and, from the Linux command line, type 199 | 200 | ``` 201 | amidi -l 202 | ``` 203 | 204 | You should see something like this if you have the Arturia KeyLab Essential plugged to the Pico's added USB Host port: 205 | 206 | ``` 207 | Dir Device Name 208 | IO hw:1,0,0 Arturia KeyLab Essential 88 MID 209 | IO hw:1,0,1 Arturia KeyLab Essential 88 DAW 210 | ``` 211 | 212 | You will need to use the appriate Mac or PC Tools to run low level tests using those computers as hosts. 213 | 214 | This program should just pass MIDI data transparently between the PC and whatever other MIDI device you have connected. If you plug an Arturia Keylab Essential keyboard to the Pico's added USB host port, and set the Controller Map to DAW mode, you can see the MIDI filter in action. 215 | 216 | If you have an Arturia Keylab Essential, make sure 217 | you set the keyboard Map Setup to DAW mode and you use Arturia Midi Control Center software to set the Controller Map DAW Mode to Mackie Control and the DAW Fader mode to Jump. The Mackie Control commands will 218 | come out of virtual MIDI cable 1, which is called something like MIDIIN2 and MIDIOUT2 in Windows and DAW in Linux or Mac. 219 | 220 | If you want to test it with Cubase, follow the instructions for setting up Cubase to work with Mackie control [here](https://steinberg.help/cubase_pro_artist/v9/en/cubase_nuendo/topics/remote_control/remote_controlling_c.html). With this code, the Save, Undo and Punch buttons work as the button labels suggest. The Metro button functions as the marker add button. And the faders will have soft pickup instead of 221 | jumping the first time you move them. 222 | 223 | ## Creating your own MIDI filter 224 | 225 | I created the filter I needed for my project. However, you may need 226 | your own filter. The API for your filter is in `midi_filter.h`. Create a new filter source file (e.g., mykeyboard_split_filter.c) 227 | and make sure you implement all functions in the midi_filter.h. 228 | Remove `keylab_essential_mc_filter.c` from `CMakeLists.txt` and 229 | replace it with the name of your filter source file. Rebuild 230 | the project, and it all should just work. 231 | -------------------------------------------------------------------------------- /keylab_essential_mc_filter.c: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2022 rppicomidi 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | #include "midi_filter.h" 26 | #include "class/midi/midi.h" 27 | #include "midi_mc_fader_pickup.h" 28 | 29 | #define KEYLAB_ESSENTIAL_NFADERS 9 30 | 31 | // Assume that if abs(hardware fader value - daw fader value) is within 127, then the faders are synchronized 32 | #define KEYLAB_ESSENTIAL_FADERS_DELTA 0x7f 33 | static mc_fader_pickup_t fader_pickup[KEYLAB_ESSENTIAL_NFADERS]; // fader channels 1-8 plus the main fader 34 | 35 | void filter_midi_init(void) 36 | { 37 | for (int chan = 0; chan < KEYLAB_ESSENTIAL_NFADERS; chan++) 38 | { 39 | mc_fader_pickup_init(fader_pickup+chan, KEYLAB_ESSENTIAL_FADERS_DELTA); 40 | } 41 | } 42 | 43 | /** 44 | * @brief 45 | * 46 | * @param packet the 4-byte USB MIDI packet 47 | * @return uint8_t the virtual MIDI cable 0-15 48 | */ 49 | static uint8_t get_cable_num(uint8_t packet[4]) 50 | { 51 | return (packet[0] >> 4) & 0xf; 52 | } 53 | 54 | #if 0 55 | /** 56 | * @brief Get the channel num object 57 | * 58 | * @param packet the 4-byte USB MIDI packet 59 | * @return uint8_t 0 if not a channel message or the 60 | * MIDI channel number 1-16 61 | */ 62 | static uint8_t get_channel_num(uint8_t packet[4]) 63 | { 64 | uint8_t channel = 0; 65 | if (packet[1]>= 0x80 && packet[0] <= 0xEF) 66 | { 67 | channel = (packet[1] & 0xf) + 1; 68 | } 69 | return channel; 70 | } 71 | #endif 72 | 73 | // Filter messages from the Arturia Keylab Essential 74 | bool filter_midi_in(uint8_t packet[4]) 75 | { 76 | bool packet_not_filtered_out = true; 77 | if (get_cable_num(packet) == 1) 78 | { 79 | // remap the note numbers for certain button presses 80 | if (packet[1] == 0x90 || packet[1] == 0x80) 81 | { 82 | switch (packet[2]) 83 | { 84 | default: 85 | break; 86 | case 0x50: // Save button 87 | packet[2] = 0x48; 88 | break; 89 | case 0x51: // Undo button 90 | packet[2] = 0x46; 91 | break; 92 | case 0x58: 93 | packet_not_filtered_out = false; 94 | break; 95 | } 96 | } 97 | else if (packet[1] >= 0xe0 && packet[1] <= 0xe8) 98 | { 99 | // fader move from the Keylab Essential. Filter it out if the fader is not in sync with the DAW 100 | TU_LOG2("received packet %02x %02x %02x\r\n", packet[1], packet[2], packet[3]); 101 | packet_not_filtered_out = mc_fader_pickup_set_hw_fader_value(&fader_pickup[packet[1] & 0xf], mc_fader_extract_value(packet)); 102 | } 103 | } 104 | return packet_not_filtered_out; 105 | } 106 | 107 | // Filter messages from the DAW 108 | bool filter_midi_out(uint8_t packet[4]) 109 | { 110 | bool packet_not_filtered_out = true; 111 | if (get_cable_num(packet) == 1) 112 | { 113 | // remap the note numbers for certain button LEDs 114 | if (packet[1] == 0x90 || packet[1] == 0x80) 115 | { 116 | switch (packet[2]) 117 | { 118 | default: 119 | break; 120 | case 0x48: // Save button 121 | packet[2] = 0x50; 122 | break; 123 | case 0x46: // Undo button 124 | packet[2] = 0x51; 125 | break; 126 | } 127 | } 128 | else if (packet[1] >= 0xe0 && packet[1] <= 0xe8) 129 | { 130 | // fader move command from DAW. Update Mackie Control fader synchronization 131 | packet_not_filtered_out = false; 132 | (void)mc_fader_pickup_set_daw_fader_value(&fader_pickup[packet[1] & 0xf], mc_fader_extract_value(packet)); 133 | } 134 | } 135 | return packet_not_filtered_out; 136 | } 137 | -------------------------------------------------------------------------------- /midi_app.c: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2021, Ha Thach (tinyusb.org) 5 | * 2022 rppicomidi 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | * 25 | */ 26 | 27 | /** 28 | * This program initializes pins GP1 and GP2 for use as a MIDI host port using 29 | * the Pico-PIO-USB library. It then waits for a USB device to be attached. 30 | * The software clones the USB descriptor of the attached USB device, and if 31 | * it is a MIDI device, it will initialized the RP2040 USB port as a MIDI 32 | * Device with the same descriptor as the attached device. It will transparently 33 | * pass data between the attached upstream USB host and the downstream USB MIDI 34 | * device. It will translate certain note messages from one note to another 35 | * note to allow DAW control buttons messages from an Arturia Keylab Essential 88 36 | * to work correctly with the Cubase DAW. 37 | */ 38 | #include 39 | #include 40 | #include 41 | 42 | #include "pico/stdlib.h" 43 | #include "pico/multicore.h" 44 | #include "pico/bootrom.h" 45 | #include "hardware/watchdog.h" 46 | #include "pio_usb.h" 47 | 48 | #include "tusb.h" 49 | #include "bsp/board_api.h" 50 | #include "usb_midi_host.h" 51 | #include "class/midi/midi_device.h" 52 | #include "usb_descriptors.h" 53 | #include "midi_filter.h" 54 | //--------------------------------------------------------------------+ 55 | // MACRO TYPEDEF CONSTANT ENUM DECLARATION 56 | //--------------------------------------------------------------------+ 57 | 58 | //--------------------------------------------------------------------+ 59 | // STATIC GLOBALS DECLARATION 60 | //--------------------------------------------------------------------+ 61 | static uint8_t midi_dev_addr = 0; 62 | 63 | static void poll_midi_dev_rx(bool connected) 64 | { 65 | // device must be attached and have at least one endpoint ready to receive a message 66 | if (!connected) 67 | { 68 | return; 69 | } 70 | uint8_t packet[4]; 71 | while (tud_midi_packet_read(packet)) 72 | { 73 | if (filter_midi_out(packet)) 74 | tuh_midi_packet_write(midi_dev_addr, packet); 75 | } 76 | } 77 | 78 | static void midi_host_app_task(void) 79 | { 80 | if (cloning_is_required()) { 81 | if (midi_dev_addr != 0 && tuh_midi_configured(midi_dev_addr)) { 82 | TU_LOG2("start descriptor cloning\r\n"); 83 | start_cloning(midi_dev_addr); 84 | } 85 | } 86 | else if (clone_next_string_is_required()) { 87 | clone_next_string(); 88 | } 89 | else if (descriptors_are_cloned()) { 90 | tuh_midi_stream_flush(midi_dev_addr); 91 | } 92 | } 93 | 94 | //--------------------------------------------------------------------+ 95 | // TinyUSB Callbacks 96 | //--------------------------------------------------------------------+ 97 | 98 | // Invoked when device with hid interface is mounted 99 | // Report descriptor is also available for use. tuh_hid_parse_report_descriptor() 100 | // can be used to parse common/simple enough descriptor. 101 | // Note: if report descriptor length > CFG_TUH_ENUMERATION_BUFSIZE, it will be skipped 102 | // therefore report_desc = NULL, desc_len = 0 103 | void tuh_midi_mount_cb(uint8_t dev_addr, uint8_t in_ep, uint8_t out_ep, uint8_t num_cables_rx, uint16_t num_cables_tx) 104 | { 105 | (void)in_ep; 106 | (void)out_ep; 107 | (void)num_cables_rx; 108 | (void)num_cables_tx; 109 | TU_LOG1("Attached MIDI device addr=%u, IN EPT=%u has %u cables, OUT EPT=%u has %u cables\r\n", 110 | dev_addr, in_ep & 0xf, num_cables_rx, out_ep & 0xf, num_cables_tx); 111 | 112 | midi_dev_addr = dev_addr; 113 | set_cloning_required(); 114 | } 115 | 116 | // Invoked when device with hid interface is un-mounted 117 | void tuh_midi_umount_cb(uint8_t dev_addr, uint8_t instance) 118 | { 119 | (void)dev_addr; 120 | (void)instance; 121 | midi_dev_addr = 0; 122 | set_descriptors_uncloned(); 123 | TU_LOG1("MIDI device address = %d, instance = %d is unmounted\r\n", dev_addr, instance); 124 | watchdog_reboot(0,0,10); // wait 10 ms and then reboot 125 | } 126 | 127 | void tuh_midi_rx_cb(uint8_t dev_addr, uint32_t num_packets) 128 | { 129 | if (midi_dev_addr == dev_addr) 130 | { 131 | while (num_packets>0) 132 | { 133 | --num_packets; 134 | uint8_t packet[4]; 135 | while (tuh_midi_packet_read(dev_addr, packet)) 136 | { 137 | if (filter_midi_in(packet)) 138 | tud_midi_packet_write(packet); 139 | } 140 | } 141 | } 142 | } 143 | 144 | void tuh_midi_tx_cb(uint8_t dev_addr) 145 | { 146 | (void)dev_addr; 147 | } 148 | 149 | //--------------------------------------------------------------------+ 150 | // MACRO CONSTANT TYPEDEF PROTYPES 151 | //--------------------------------------------------------------------+ 152 | static void led_blinking_task(void); 153 | // core1: handle host events 154 | void core1_main() { 155 | sleep_ms(10); 156 | 157 | // To run USB SOF interrupt in core1, init host stack for pio_usb (roothub 158 | // port1) on core1 159 | tuh_init(BOARD_TUH_RHPORT); 160 | 161 | while (true) { 162 | tuh_task(); // tinyusb host task 163 | 164 | midi_host_app_task(); 165 | } 166 | } 167 | static enum {MIDI_DEVICE_NOT_INITIALIZED, MIDI_DEVICE_NEEDS_INIT, MIDI_DEVICE_IS_INITIALIZED} midi_device_status = MIDI_DEVICE_NOT_INITIALIZED; 168 | void device_clone_complete_cb() 169 | { 170 | midi_device_status = MIDI_DEVICE_NEEDS_INIT; 171 | } 172 | 173 | // core0: handle device events 174 | int main(void) { 175 | // set up board clocks, UART pins, PIO Pins 176 | board_init(); 177 | 178 | multicore_reset_core1(); 179 | // all USB task run in core1 180 | multicore_launch_core1(core1_main); 181 | 182 | TU_LOG1("pico-usb-midi-filter\r\n"); 183 | filter_midi_init(); 184 | while (1) 185 | { 186 | if (midi_device_status == MIDI_DEVICE_NEEDS_INIT) { 187 | tud_init(0); 188 | TU_LOG1("MIDI device initialized\r\n"); 189 | midi_device_status = MIDI_DEVICE_IS_INITIALIZED; 190 | } 191 | else if (midi_device_status == MIDI_DEVICE_IS_INITIALIZED) { 192 | tud_task(); 193 | bool connected = tud_midi_mounted(); 194 | poll_midi_dev_rx(connected); 195 | } 196 | 197 | led_blinking_task(); 198 | } 199 | 200 | return 0; 201 | } 202 | 203 | //--------------------------------------------------------------------+ 204 | // TinyUSB Callbacks 205 | //--------------------------------------------------------------------+ 206 | 207 | //--------------------------------------------------------------------+ 208 | // Blinking Task 209 | //--------------------------------------------------------------------+ 210 | 211 | static void led_blinking_task(void) 212 | { 213 | const uint32_t interval_ms = 1000; 214 | static uint32_t start_ms = 0; 215 | 216 | static bool led_state = false; 217 | 218 | // Blink every interval ms 219 | if ( board_millis() - start_ms < interval_ms) return; // not enough time 220 | start_ms += interval_ms; 221 | 222 | board_led_write(led_state != 0); 223 | led_state = 1 - led_state; // toggle 224 | } 225 | -------------------------------------------------------------------------------- /midi_filter.h: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2022 rppicomidi 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | 26 | #pragma once 27 | #include 28 | #include 29 | 30 | #ifdef __cplusplus 31 | extern "C" { 32 | #endif 33 | // Initialize any state variables the MIDI filter requires 34 | void filter_midi_init(void); 35 | 36 | // Modify the data heading to the USB Host MIDI IN port 37 | // if need be. 38 | bool filter_midi_in(uint8_t packet[4]); 39 | 40 | // Modify the data heading to the USB Host MIDI OUT port 41 | // if need be. 42 | bool filter_midi_out(uint8_t packet[4]); 43 | #ifdef __cplusplus 44 | } 45 | #endif -------------------------------------------------------------------------------- /midi_mc_fader_pickup.c: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2022 rppicomidi 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | 26 | #include "midi_mc_fader_pickup.h" 27 | #include 28 | void mc_fader_pickup_init(mc_fader_pickup_t* pickup, uint16_t sync_delta) 29 | { 30 | pickup->state = MC_FADER_PICKUP_RESET; 31 | pickup->sync_delta = sync_delta; 32 | pickup->daw = 0; 33 | pickup->fader = 0; 34 | } 35 | 36 | uint16_t mc_fader_extract_value(uint8_t packet[4]) 37 | { 38 | return ((uint16_t)packet[2] & 0x7f) | ((uint16_t)(packet[3] & 0x7f) << 7); 39 | } 40 | 41 | void mc_fader_encode_value(uint16_t fader_value, uint8_t packet[4]) 42 | { 43 | packet[2] = fader_value & 0x7f; 44 | packet[3] = (fader_value >> 7) & 0x7f; 45 | } 46 | 47 | static bool mc_fader_state_is_synchronized(mc_fader_pickup_state_t state) 48 | { 49 | return state == MC_FADER_PICKUP_SYNCED; 50 | } 51 | 52 | bool mc_fader_pickup_set_daw_fader_value(mc_fader_pickup_t* pickup, uint16_t daw_fader_value) 53 | { 54 | mc_fader_pickup_state_t next_state = pickup->state; // assume state will not change 55 | int16_t delta = (int16_t)daw_fader_value - (int16_t)pickup->fader; 56 | pickup->daw = daw_fader_value; 57 | switch(pickup->state) 58 | { 59 | case MC_FADER_PICKUP_RESET: 60 | case MC_FADER_PICKUP_HW_UNKNOWN: 61 | next_state = MC_FADER_PICKUP_HW_UNKNOWN; 62 | break; 63 | 64 | default: // both DAW fader value and Hardware fader value are known 65 | { 66 | uint16_t abs_delta = delta; 67 | if (delta < 0) 68 | abs_delta = -delta; 69 | if (abs_delta < pickup->sync_delta) 70 | { 71 | next_state = MC_FADER_PICKUP_SYNCED; 72 | } 73 | else if (delta < 0) 74 | { 75 | next_state = MC_FADER_PICKUP_TOO_HIGH; 76 | } 77 | else 78 | { 79 | next_state = MC_FADER_PICKUP_TOO_LOW; 80 | } 81 | } 82 | } 83 | pickup->state = next_state; 84 | return mc_fader_state_is_synchronized(next_state); 85 | } 86 | 87 | bool mc_fader_pickup_set_hw_fader_value(mc_fader_pickup_t* pickup, uint16_t hw_fader_value) 88 | { 89 | int16_t delta = (int16_t)hw_fader_value - (int16_t)pickup->daw; 90 | uint16_t abs_delta = delta; 91 | mc_fader_pickup_state_t next_state = pickup->state; // assume state will not change 92 | if (delta < 0) 93 | abs_delta = -delta; 94 | switch(pickup->state) 95 | { 96 | case MC_FADER_PICKUP_RESET: 97 | case MC_FADER_PICKUP_DAW_UNKNOWN: 98 | next_state = MC_FADER_PICKUP_DAW_UNKNOWN; 99 | break; 100 | case MC_FADER_PICKUP_SYNCED: 101 | break; 102 | case MC_FADER_PICKUP_TOO_HIGH: 103 | if (abs_delta < pickup->sync_delta) 104 | next_state = MC_FADER_PICKUP_SYNCED; 105 | else if (delta < 0) { 106 | // previous fader value was higher than the DAW fader value, and now is 107 | // lower, so the hardware fader must have moved past the DAW value 108 | next_state = MC_FADER_PICKUP_SYNCED; 109 | } 110 | // otherwise, still too high 111 | break; 112 | case MC_FADER_PICKUP_TOO_LOW: 113 | if (abs_delta < pickup->sync_delta) 114 | next_state = MC_FADER_PICKUP_SYNCED; 115 | else if (delta > 0) { 116 | // previous fader value was lower than the DAW fader value, and now is 117 | // higher, so the hardware fader must have moved past the DAW value 118 | next_state = MC_FADER_PICKUP_SYNCED; 119 | } 120 | // otherwise, still too low 121 | break; 122 | case MC_FADER_PICKUP_HW_UNKNOWN: 123 | if (abs_delta < pickup->sync_delta) 124 | next_state = MC_FADER_PICKUP_SYNCED; 125 | else if (delta > 0) 126 | next_state = MC_FADER_PICKUP_TOO_HIGH; 127 | else 128 | next_state = MC_FADER_PICKUP_TOO_LOW; 129 | break; 130 | default: 131 | printf("unknown pickup state %u\r\n", pickup->state); 132 | next_state = MC_FADER_PICKUP_RESET; 133 | break; 134 | } 135 | pickup->state = next_state; 136 | pickup->fader = hw_fader_value; 137 | return mc_fader_state_is_synchronized(next_state); 138 | } 139 | 140 | -------------------------------------------------------------------------------- /midi_mc_fader_pickup.h: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2022 rppicomidi 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | 26 | /** 27 | * @file midi_mc_fader_pickup.h 28 | * 29 | * This file contains functions and data that implement fader "pickup" synchronization 30 | * between a DAW that expects to be connected to a Mackie Control control surface that 31 | * has motorized faders and a Mackie Control compatible control surface that has 32 | * non-motorized faders. The Mackie control fader move message is the MIDI pitch 33 | * bend message; pitch bend MIDI channels 1-8 correspond to fader channels 1-8; 34 | * pitch bend MIDI channel 9 corresponds to the main fader. Each fader value is a 14-bit 35 | * unsigned value encoded in the pitch bend position, 7 bit data, LSB first. 36 | * 37 | * The DAW will send the current expected fader values to the control surface on 38 | * startup, fader bank change, mode change, etc. The faders on a control surface with 39 | * motorized faders will move to match the target fader values from the DAW. Non-motorized 40 | * faders cannot move to match target values all by themselves. This code provides data structures 41 | * that allow higher level code to filter out fader move messages to the DAW for an individual fader 42 | * until the user moves the fader up to or past the last target fader value the DAW sent for that fader. 43 | * 44 | * To use this code: 45 | * 1. Create as many mc_fader_pickup_t structures as you have hardware faders on your control surface 46 | * 2. Call mc_fader_pickup_init() to initialize each structure 47 | * 3. For each Mackie Control fader move message (MIDI pitch bend) you receive, extract the fader value 48 | * by calling mc_fader_extract_value(). 49 | * 4a. If the fader move message was from the DAW, update the structure by calling mc_fader_pickup_set_daw_fader_value(). 50 | * Do not send the message to the control surface (it can't do anything with it anyway) 51 | * 4b. If the fader move message was from the hardware control surface, update the structure by calling 52 | * mc_fader_pickup_set_hw_fader_value() and note the return value. If the return value value was true, send 53 | * the fader move message to the DAW because the fader is in sync with the value the DAW thinks it should have. 54 | * Otherwise, do not send the fader move message to the DAW. 55 | */ 56 | #pragma once 57 | #include 58 | #include 59 | 60 | #ifdef __cplusplus 61 | extern "C" { 62 | #endif 63 | typedef enum { 64 | MC_FADER_PICKUP_RESET, //!< Fader state and the fader target value is unknown 65 | MC_FADER_PICKUP_HW_UNKNOWN, //!< Hardware Fader value is unknown but the DAW value is known 66 | MC_FADER_PICKUP_DAW_UNKNOWN, //!< Hardware Fader value is known but the DAW value is unknown 67 | MC_FADER_PICKUP_TOO_HIGH, //!< Fader is too high; need to reduce it down to or less than the fader target value 68 | MC_FADER_PICKUP_TOO_LOW, //!< Fader is too low; need to increase it up to or greater than the fader target value 69 | MC_FADER_PICKUP_SYNCED 70 | } mc_fader_pickup_state_t; 71 | 72 | typedef struct { 73 | mc_fader_pickup_state_t state; // the current pickup state of the fader 74 | uint16_t daw; // the last fader value the DAW sent (14-bits, unsigned) 75 | uint16_t fader; // the last fader value the control surface sent (14-bits, unsigned) 76 | uint16_t sync_delta; // the minimum difference between the fader values before they are considered "equal" (14-bits, unsigned) 77 | } mc_fader_pickup_t; 78 | 79 | /** 80 | * @brief initialize a mc_fader_pickup structure 81 | * 82 | * @param pickup is a pointer to the structure to initialize 83 | * @param sync_delta is the unsigned 14-bit absolute fader value difference 84 | * that is close enough to call the fader in sync with the DAW 85 | */ 86 | void mc_fader_pickup_init(mc_fader_pickup_t* pickup, uint16_t sync_delta); 87 | 88 | /** 89 | * @brief get the 14-bit unsigned fader value from the pitch bend USB MIDI packet 90 | * 91 | * @param packet a standard 4-byte USB MIDI pitch bend message 92 | * @return uint16_t the 14-bit unsigned fader value 93 | * 94 | * @note this function ignores the first two bytes of the packet 95 | */ 96 | uint16_t mc_fader_extract_value(uint8_t packet[4]); 97 | 98 | /** 99 | * @brief encode the 14-bit unsigned fader value to the pitch bend USB MIDI packet 100 | * 101 | * @param fader_value the 14-bit fader value to encode in the pitch bend message 102 | * @param packet the standard USB MIDI pitch bend message with the fader value enocded 103 | * 104 | * @note this function does not set the the first two bytes of the packet 105 | */ 106 | void mc_fader_encode_value(uint16_t fader_value, uint8_t packet[4]); 107 | 108 | /** 109 | * @brief write a new DAW fader value to the pickup structure 110 | * 111 | * @param pickup a pointer to a mc_fader_pickupt_t structure 112 | * @param daw_fader_value the 14-bit unsigned DAW fader value 113 | * @return true if setting the DAW fader causes the hardware fader and DAW fader to be in sync 114 | */ 115 | bool mc_fader_pickup_set_daw_fader_value(mc_fader_pickup_t* pickup, uint16_t daw_fader_value); 116 | 117 | /** 118 | * @brief write a new hardare fader value to the pickup structure 119 | * 120 | * @param pickup a pointer to a mc_fader_pickupt_t structure 121 | * @param hw_fader_value the 14-bit unsigned hardware fader value 122 | * @return true if setting the hardware fader causes the hardware fader and DAW fader to be in sync 123 | */ 124 | bool mc_fader_pickup_set_hw_fader_value(mc_fader_pickup_t* pickup, uint16_t hw_fader_value); 125 | 126 | #ifdef __cplusplus 127 | } 128 | #endif -------------------------------------------------------------------------------- /pico_sdk_import.cmake: -------------------------------------------------------------------------------- 1 | # This is a copy of /external/pico_sdk_import.cmake 2 | 3 | # This can be dropped into an external project to help locate this SDK 4 | # It should be include()ed prior to project() 5 | 6 | if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) 7 | set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) 8 | message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") 9 | endif () 10 | 11 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) 12 | set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) 13 | message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") 14 | endif () 15 | 16 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) 17 | set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) 18 | message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") 19 | endif () 20 | 21 | set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") 22 | set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") 23 | set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") 24 | 25 | if (NOT PICO_SDK_PATH) 26 | if (PICO_SDK_FETCH_FROM_GIT) 27 | include(FetchContent) 28 | set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) 29 | if (PICO_SDK_FETCH_FROM_GIT_PATH) 30 | get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") 31 | endif () 32 | FetchContent_Declare( 33 | pico_sdk 34 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 35 | GIT_TAG master 36 | ) 37 | if (NOT pico_sdk) 38 | message("Downloading Raspberry Pi Pico SDK") 39 | FetchContent_Populate(pico_sdk) 40 | set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) 41 | endif () 42 | set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) 43 | else () 44 | message(FATAL_ERROR 45 | "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." 46 | ) 47 | endif () 48 | endif () 49 | 50 | get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") 51 | if (NOT EXISTS ${PICO_SDK_PATH}) 52 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") 53 | endif () 54 | 55 | set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) 56 | if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) 57 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") 58 | endif () 59 | 60 | set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) 61 | 62 | include(${PICO_SDK_INIT_CMAKE_FILE}) 63 | -------------------------------------------------------------------------------- /pictures/pico-usb-midi-filter-bottom-view.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rppicomidi/pico-usb-midi-filter/0139f9360716bbd19a395704e5214079494623b3/pictures/pico-usb-midi-filter-bottom-view.jpg -------------------------------------------------------------------------------- /pictures/pico-usb-midi-filter-top-view.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rppicomidi/pico-usb-midi-filter/0139f9360716bbd19a395704e5214079494623b3/pictures/pico-usb-midi-filter-top-view.jpg -------------------------------------------------------------------------------- /pictures/pico-usb-midi-filter-with-picoprobe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rppicomidi/pico-usb-midi-filter/0139f9360716bbd19a395704e5214079494623b3/pictures/pico-usb-midi-filter-with-picoprobe.jpg -------------------------------------------------------------------------------- /tinyusb_patches/0001-Allow-defining-CFG_TUD_ENDPOINT0_SIZE-as-a-function.patch: -------------------------------------------------------------------------------- 1 | From e449b250d1ec15a7e34f97749b60e9f74f309c9c Mon Sep 17 00:00:00 2001 2 | From: rppicomidi 3 | Date: Mon, 21 Aug 2023 12:09:03 -0700 4 | Subject: [PATCH] Allow defining CFG_TUD_ENDPOINT0_SIZE as a function 5 | 6 | --- 7 | src/device/usbd_control.c | 6 +++++- 8 | src/tusb_option.h | 25 +++++++++++++++++++++++-- 9 | 2 files changed, 28 insertions(+), 3 deletions(-) 10 | 11 | diff --git a/src/device/usbd_control.c b/src/device/usbd_control.c 12 | index 76d062e40..ae5f48c22 100644 13 | --- a/src/device/usbd_control.c 14 | +++ b/src/device/usbd_control.c 15 | @@ -56,7 +56,11 @@ typedef struct 16 | tu_static usbd_control_xfer_t _ctrl_xfer; 17 | 18 | CFG_TUD_MEM_SECTION CFG_TUSB_MEM_ALIGN 19 | -tu_static uint8_t _usbd_ctrl_buf[CFG_TUD_ENDPOINT0_SIZE]; 20 | +#if CFG_TUD_EP0_SZ_IS_FN 21 | + tu_static uint8_t _usbd_ctrl_buf[CFG_TUD_ENDPOINT0_MAX]; 22 | +#else 23 | + tu_static uint8_t _usbd_ctrl_buf[CFG_TUD_ENDPOINT0_SIZE]; 24 | +#endif 25 | 26 | //--------------------------------------------------------------------+ 27 | // Application API 28 | diff --git a/src/tusb_option.h b/src/tusb_option.h 29 | index c72117850..6e9b9e896 100644 30 | --- a/src/tusb_option.h 31 | +++ b/src/tusb_option.h 32 | @@ -333,7 +333,24 @@ 33 | #define CFG_TUD_MEM_ALIGN CFG_TUSB_MEM_ALIGN 34 | #endif 35 | 36 | +// You can define CFG_TUD_ENDPOINT0_SIZE to be a function that returns uint8_t 37 | +// if CFG_TUD_EP0_SZ_IS_FN is not zero. If you do that, you must define 38 | +// CFG_TUD_ENDPOINT0_MAX, the size of the EP0 buffer, to a constant positive integer <= 64 39 | +#ifndef CFG_TUD_EP0_SZ_IS_FN 40 | + #define CFG_TUD_EP0_SZ_IS_FN 0 41 | +#else 42 | + #ifndef CFG_TUD_ENDPOINT0_MAX 43 | + #error Must define CFG_TUD_ENDPOINT0_MAX if CFG_TUD_EP0_SZ_IS_FN != 0 44 | + #endif 45 | +#endif 46 | + 47 | +// CFG_TUD_ENDPOINT0_SIZE is the maximum EP0 transfer size. Normally defined 48 | +// as a constant 8 <= CFG_TUD_ENDPOINT0_SIZE <= 64, but can 49 | +// be defined as a function that returns uint8_t for special purposes. 50 | #ifndef CFG_TUD_ENDPOINT0_SIZE 51 | + #if CFG_TUD_EP0_SZ_IS_FN 52 | + #error Must define CFG_TUD_ENDPOINT0_SIZE if CFG_TUD_EP0_SZ_IS_FN==1 53 | + #endif 54 | #define CFG_TUD_ENDPOINT0_SIZE 64 55 | #endif 56 | 57 | @@ -484,8 +501,12 @@ 58 | //------------------------------------------------------------------ 59 | // Configuration Validation 60 | //------------------------------------------------------------------ 61 | -#if CFG_TUD_ENDPOINT0_SIZE > 64 62 | - #error Control Endpoint Max Packet Size cannot be larger than 64 63 | +#if !CFG_TUD_EP0_SZ_IS_FN 64 | + #if CFG_TUD_ENDPOINT0_SIZE > 64 65 | + #error Control Endpoint Max Packet Size cannot be larger than 64 66 | + #endif 67 | + #elif CFG_TUD_ENDPOINT0_MAX > 64 68 | + #error Control Endpoint Max Packet Size cannot be larger than 64 69 | #endif 70 | 71 | // To avoid GCC compiler warnings when -pedantic option is used (strict ISO C) 72 | -- 73 | 2.34.1 74 | 75 | -------------------------------------------------------------------------------- /tusb_config.h: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 Ha Thach (tinyusb.org) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | 26 | #ifndef _TUSB_CONFIG_H_ 27 | #define _TUSB_CONFIG_H_ 28 | #include 29 | #ifdef __cplusplus 30 | extern "C" { 31 | #endif 32 | 33 | //-------------------------------------------------------------------- 34 | // COMMON CONFIGURATION 35 | //-------------------------------------------------------------------- 36 | 37 | #define CFG_TUSB_OS OPT_OS_PICO 38 | 39 | // Enable device stack 40 | #define CFG_TUD_ENABLED 1 41 | 42 | // Enable host stack with pio-usb if Pico-PIO-USB library is available 43 | #define CFG_TUH_ENABLED 1 44 | #define CFG_TUH_RPI_PIO_USB 1 45 | 46 | // CFG_TUSB_DEBUG is defined by compiler in DEBUG build 47 | // #define CFG_TUSB_DEBUG 0 48 | 49 | /* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. 50 | * Tinyusb use follows macros to declare transferring memory so that they can be put 51 | * into those specific section. 52 | * e.g 53 | * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) 54 | * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) 55 | */ 56 | #ifndef CFG_TUSB_MEM_SECTION 57 | #define CFG_TUSB_MEM_SECTION 58 | #endif 59 | 60 | #ifndef CFG_TUSB_MEM_ALIGN 61 | #define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4))) 62 | #endif 63 | 64 | //-------------------------------------------------------------------- 65 | // DEVICE CONFIGURATION 66 | //-------------------------------------------------------------------- 67 | 68 | #ifdef CFG_TUD_ENDPOINT0_SIZE 69 | #error "must define CFG_TUD_ENDPOINT0_SIZE here" 70 | #else 71 | #define CFG_TUD_EP0_SZ_IS_FN 1 72 | #define CFG_TUD_ENDPOINT0_MAX 64 73 | extern uint8_t midid_get_endpoint0_size(); 74 | #define CFG_TUD_ENDPOINT0_SIZE midid_get_endpoint0_size() 75 | #endif 76 | 77 | // Do not use the built-in USB MIDI Device driver 78 | #define CFG_TUD_MIDI 0 79 | #define CFG_TUD_MIDI_RX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) 80 | #define CFG_TUD_MIDI_TX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) 81 | 82 | 83 | //-------------------------------------------------------------------- 84 | // HOST CONFIGURATION 85 | //-------------------------------------------------------------------- 86 | 87 | // Size of buffer to hold descriptors and other data used for enumeration 88 | #define CFG_TUH_ENUMERATION_BUFSIZE 512 89 | 90 | #define CFG_TUH_HUB 1 91 | // max device support (excluding hub device) 92 | #define CFG_TUH_DEVICE_MAX (CFG_TUH_HUB ? 4 : 1) // hub typically has 4 ports 93 | #define BOARD_TUH_RHPORT 1 94 | 95 | // Use the app driver for USB MIDI Host 96 | #define CFG_TUH_MIDI 0 97 | #define CFG_MIDI_HOST_DEVSTRINGS 1 98 | #ifdef __cplusplus 99 | } 100 | #endif 101 | 102 | #endif /* _TUSB_CONFIG_H_ */ 103 | -------------------------------------------------------------------------------- /usb_descriptors.c: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 Ha Thach (tinyusb.org) 5 | * 2022 rppicomidi 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | * 25 | */ 26 | 27 | #include "tusb.h" 28 | #include "stdlib.h" 29 | #include "usb_descriptors.h" 30 | #include "usb_midi_host.h" 31 | static tusb_desc_device_t desc_device_connected; 32 | 33 | static uint8_t* desc_fs_configuration = NULL; 34 | static uint8_t daddr; 35 | struct devstring_set_s { 36 | uint16_t langid; 37 | uint16_t** string_list; 38 | }; 39 | 40 | static struct devstring_set_s* devstrings = NULL; 41 | static uint8_t num_langids = 0; 42 | static uint8_t langid_idx = 0; 43 | static uint8_t string_idx = 0; 44 | static uint8_t* string_idx_list = NULL; 45 | static uint8_t nstrings = 0; 46 | #define SZ_SCRATCHPAD 128 47 | static uint8_t scratchpad[SZ_SCRATCHPAD]; 48 | static enum {UNCLONED, START_CLONING, CLONING, CLONE_NEXT_DESCRIPTOR, CLONED} clone_state = UNCLONED; 49 | 50 | 51 | void set_cloning_required() 52 | { 53 | clone_state = START_CLONING; 54 | } 55 | 56 | bool cloning_is_required() 57 | { 58 | return clone_state == START_CLONING; 59 | } 60 | 61 | bool clone_next_string_is_required() 62 | { 63 | return clone_state == CLONE_NEXT_DESCRIPTOR; 64 | } 65 | 66 | bool descriptors_are_cloned(void) 67 | { 68 | return clone_state == CLONED; 69 | } 70 | 71 | void set_descriptors_uncloned(void) 72 | { 73 | clone_state = UNCLONED; 74 | } 75 | 76 | static void clone_string_cb(tuh_xfer_t* xfer) 77 | { 78 | if (XFER_RESULT_SUCCESS == xfer->result) { 79 | devstrings[langid_idx].string_list[string_idx] = malloc(xfer->buffer[0]); 80 | memcpy(devstrings[langid_idx].string_list[string_idx], xfer->buffer, xfer->buffer[0]); 81 | if (++string_idx < nstrings) { 82 | clone_state = CLONE_NEXT_DESCRIPTOR; 83 | } 84 | else { 85 | TU_LOG2("All strings for langid 0x%04x:\r\n", devstrings[langid_idx].langid); 86 | for (uint8_t idx=0; idx < nstrings; idx++) { 87 | uint8_t length_string = (*((uint8_t*)(devstrings[langid_idx].string_list[idx])) - 2); 88 | char* cptr = (char*)(devstrings[langid_idx].string_list[idx])+2; 89 | for (uint8_t jdx=0; jdx < length_string; jdx++) { 90 | if (cptr[jdx]) { 91 | TU_LOG2("%c", cptr[jdx]); 92 | } 93 | } 94 | TU_LOG2("\r\n"); 95 | } 96 | if (++langid_idx < num_langids) { 97 | string_idx = 0; 98 | clone_state = CLONE_NEXT_DESCRIPTOR; 99 | } 100 | else { 101 | clone_state = CLONED; 102 | TU_LOG2("all strings cloned\r\n"); 103 | if (device_clone_complete_cb) device_clone_complete_cb(); 104 | } 105 | } 106 | } 107 | } 108 | 109 | void clone_next_string() 110 | { 111 | if (langid_idx < num_langids && string_idx < nstrings) 112 | { 113 | if (tuh_descriptor_get_string(daddr, string_idx_list[string_idx], devstrings[langid_idx].langid, scratchpad, SZ_SCRATCHPAD, clone_string_cb, 0)) { 114 | clone_state = CLONING; 115 | } 116 | } 117 | } 118 | 119 | static void clone_langids_cb(tuh_xfer_t* xfer) 120 | { 121 | if (XFER_RESULT_SUCCESS == xfer->result) { 122 | num_langids = (xfer->actual_len - 2)/2; 123 | uint16_t* langids = (uint16_t*)(xfer->buffer+2); 124 | devstrings = calloc(num_langids, sizeof(uint16_t)); 125 | TU_LOG2("num_langids=%u\r\n", num_langids); 126 | int idx; 127 | for (idx = 0; idx < num_langids; idx++) { 128 | devstrings[idx].langid = langids[idx]; 129 | TU_LOG2("langid %u=0x%04x\r\n", idx, langids[idx]); 130 | devstrings[idx].string_list = calloc(nstrings, sizeof(*(devstrings[idx].string_list))); 131 | } 132 | if (nstrings > 0) { 133 | clone_state = CLONE_NEXT_DESCRIPTOR; 134 | } 135 | else { 136 | clone_state = CLONED; 137 | } 138 | } 139 | 140 | // Continue until all strings for all langids are fetched. 141 | // Then call clone_complete_cb()--that should trigger tud_init(0) call. 142 | } 143 | 144 | // The callback functions capture the device descriptor and configuration descriptor 145 | // This function starts the process that extracts the string descriptors. 146 | // It assumes that captured device descriptor and configuration descriptor 147 | // are valid and that all string descriptor indices are available 148 | static void clone_string_descriptors(uint8_t dev_addr) 149 | { 150 | int idx, jdx; 151 | const uint8_t* midi_string_idxs; 152 | uint8_t nmidi_strings; 153 | TU_LOG2("clone string descriptors for addr=%u\r\n", dev_addr); 154 | if (dev_addr == daddr) { 155 | // first free any old devstrings 156 | if (devstrings != NULL) { 157 | for (idx = 0; idx < num_langids; idx++) { 158 | for (jdx = 0; jdx < nstrings; jdx++) { 159 | free(devstrings[idx].string_list[jdx]); 160 | } 161 | free(devstrings+idx); 162 | } 163 | } 164 | // Get the list of string indexs 165 | nmidi_strings = tuh_midi_get_all_istrings(dev_addr, &midi_string_idxs); 166 | nstrings = nmidi_strings; 167 | if (desc_device_connected.iManufacturer != 0) { 168 | ++nstrings; 169 | } 170 | if (desc_device_connected.iProduct != 0) { 171 | ++nstrings; 172 | } 173 | if (desc_device_connected.iSerialNumber != 0) { 174 | ++nstrings; 175 | } 176 | 177 | string_idx_list = malloc(nstrings); 178 | string_idx = 0; 179 | if (desc_device_connected.iManufacturer != 0) { 180 | string_idx_list[string_idx++] = desc_device_connected.iManufacturer; 181 | } 182 | if (desc_device_connected.iProduct != 0) { 183 | string_idx_list[string_idx++] = desc_device_connected.iProduct; 184 | } 185 | if (desc_device_connected.iSerialNumber != 0) { 186 | string_idx_list[string_idx++] = desc_device_connected.iSerialNumber; 187 | } 188 | if (nmidi_strings > 0) 189 | memcpy(string_idx_list+string_idx, midi_string_idxs, nmidi_strings); 190 | 191 | TU_LOG2("All %u string indices\n", string_idx+nmidi_strings); 192 | TU_LOG2_MEM(string_idx_list, string_idx+nmidi_strings, 2); 193 | // Kick off the process of fetching all strings for all languages from the host-connected device 194 | string_idx = 0; 195 | langid_idx = 0; 196 | // get the string langid list 197 | if (tuh_descriptor_get_string(daddr, 0, 0, scratchpad, SZ_SCRATCHPAD, clone_langids_cb, 0)) { 198 | clone_state = CLONING; 199 | TU_LOG2("getting langid list\r\n"); 200 | } 201 | else { 202 | TU_LOG2("getting langid list failed\r\n"); 203 | clone_state = UNCLONED; 204 | } 205 | } 206 | } 207 | 208 | static void clone_config_cb(tuh_xfer_t* xfer) 209 | { 210 | if (XFER_RESULT_SUCCESS == xfer->result && xfer->actual_len == xfer->user_data) { 211 | // We have the configuration descriptor. Now clone the string descriptors 212 | TU_LOG2("cloning the string descriptors\r\n"); 213 | clone_string_descriptors(xfer->daddr); 214 | } 215 | else { 216 | TU_LOG2("failed to clone the config descriptor\r\n"); 217 | } 218 | } 219 | 220 | static void clone_config_sz_cb(tuh_xfer_t* xfer) 221 | { 222 | if (XFER_RESULT_SUCCESS == xfer->result) { 223 | TU_LOG2("clone got config size\r\n"); 224 | tusb_desc_configuration_t* base = (tusb_desc_configuration_t*)scratchpad; 225 | desc_fs_configuration = malloc(base->wTotalLength); 226 | if (!desc_fs_configuration || !tuh_descriptor_get_configuration(xfer->daddr, 0, desc_fs_configuration, base->wTotalLength, 227 | clone_config_cb, base->wTotalLength)) { 228 | if (desc_fs_configuration) { 229 | free(desc_fs_configuration); 230 | desc_fs_configuration = NULL; 231 | } 232 | clone_state = UNCLONED; 233 | TU_LOG2("failed to start cloning the config descriptor\r\n"); 234 | } 235 | else { 236 | TU_LOG2("cloning the configuration descriptor\r\n"); 237 | } 238 | } 239 | } 240 | 241 | static void clone_device_cb(tuh_xfer_t* xfer) 242 | { 243 | if (XFER_RESULT_SUCCESS == xfer->result) { 244 | if (xfer->actual_len == sizeof(desc_device_connected) && desc_device_connected.bNumConfigurations == 1) { 245 | // device descriptor copied and has exactly one configuration (all this software can handle) 246 | desc_fs_configuration = calloc(desc_device_connected.bNumConfigurations, sizeof(desc_fs_configuration[0])); 247 | if (!desc_fs_configuration || !tuh_descriptor_get_configuration(xfer->daddr, 0, scratchpad, sizeof(tusb_desc_configuration_t), 248 | clone_config_sz_cb, 0)) { 249 | clone_state = UNCLONED; 250 | TU_LOG2("failed send get config size\r\n"); 251 | } 252 | else { 253 | TU_LOG2("sent get config size\r\n"); 254 | } 255 | } 256 | else { 257 | TU_LOG2("xfer->actual_len=%lu num config=%u\r\n", xfer->actual_len, desc_device_connected.bNumConfigurations); 258 | } 259 | } 260 | else { 261 | TU_LOG2("device clone cb xfer failed result=%u\r\n", xfer->result); 262 | } 263 | } 264 | 265 | void start_cloning(uint8_t dev_addr) 266 | { 267 | // Free any old configuration descriptors 268 | if (desc_fs_configuration != NULL) { 269 | free(desc_fs_configuration); 270 | desc_fs_configuration = NULL; 271 | } 272 | daddr = dev_addr; 273 | if (tuh_descriptor_get_device(dev_addr, &desc_device_connected, sizeof(desc_device_connected), 274 | clone_device_cb, 0)) { 275 | clone_state = CLONING; 276 | TU_LOG2("Sent get device descriptor to addr %u\r\n", dev_addr); 277 | } 278 | else { 279 | clone_state = UNCLONED; 280 | TU_LOG2("Failed send get device descriptor to addr %u\r\n", dev_addr); 281 | } 282 | } 283 | 284 | #if 0 285 | // grab the device descriptor during enumeration 286 | void tuh_desc_device_cb(uint8_t dev_addr, const tusb_desc_device_t *desc) 287 | { 288 | daddr = dev_addr; 289 | memcpy(&desc_device_connected, desc, sizeof(desc_device_connected)); 290 | TU_LOG2("device descriptor cloned\r\n"); 291 | TU_LOG2_MEM((uint8_t*)&desc_device_connected, sizeof(desc_device_connected), 2); 292 | } 293 | 294 | void tuh_desc_config_cb(uint8_t dev_addr, const tusb_desc_configuration_t *desc_config) 295 | { 296 | if (daddr == dev_addr && desc_config->wTotalLength > 0) { 297 | desc_fs_configuration = malloc(desc_config->wTotalLength); 298 | if (desc_fs_configuration) { 299 | memcpy(desc_fs_configuration, desc_config, desc_config->wTotalLength); 300 | TU_LOG2("configuration descriptor cloned\r\n"); 301 | TU_LOG2_MEM((uint8_t*)desc_fs_configuration, desc_config->wTotalLength, 2); 302 | } 303 | } 304 | } 305 | #endif 306 | 307 | // Invoked when received GET DEVICE DESCRIPTOR 308 | // Application return pointer to descriptor 309 | uint8_t const * tud_descriptor_device_cb(void) 310 | { 311 | TU_LOG2("midi device descriptor returned\r\n"); 312 | return (uint8_t const *) &desc_device_connected; 313 | } 314 | 315 | uint8_t midid_get_endpoint0_size() 316 | { 317 | return desc_device_connected.bMaxPacketSize0; 318 | } 319 | 320 | //--------------------------------------------------------------------+ 321 | // Configuration Descriptor 322 | //--------------------------------------------------------------------+ 323 | 324 | // Invoked when received GET CONFIGURATION DESCRIPTOR 325 | // Application return pointer to descriptor 326 | // Descriptor contents must exist long enough for transfer to complete 327 | uint8_t const * tud_descriptor_configuration_cb(uint8_t index) 328 | { 329 | (void) index; 330 | if (desc_fs_configuration == NULL) { 331 | TU_LOG2("midi device configuration $u not available\r\n", index); 332 | return NULL; 333 | } 334 | TU_LOG2("midi device configuration $u returned\r\n", index); 335 | return desc_fs_configuration; 336 | } 337 | 338 | //--------------------------------------------------------------------+ 339 | // String Descriptors 340 | //--------------------------------------------------------------------+ 341 | // Invoked when received GET STRING DESCRIPTOR request 342 | // Application return pointer to descriptor, whose contents must exist long enough for transfer to complete 343 | uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) 344 | { 345 | uint16_t* ptr = NULL; 346 | // return the langid list descriptor if index == 0 347 | if (index == 0) { 348 | scratchpad[0] = (num_langids * 2) + 2; // descriptor length 349 | scratchpad[1] = TUSB_DESC_STRING; 350 | ptr = (uint16_t*)(scratchpad+2); 351 | for (int idx=0; idx < num_langids; idx++) { 352 | *ptr++ = devstrings[idx].langid; 353 | } 354 | ptr = (uint16_t*)scratchpad; 355 | } 356 | else { 357 | // find the string descriptor index for this langid 358 | int lang; 359 | for (lang = 0; lang < num_langids && devstrings[lang].langid != langid; lang++) { 360 | } 361 | if (lang < num_langids) { 362 | // then we found the devstrings structure for this langid 363 | int idx; 364 | for (idx = 0; idx < nstrings && string_idx_list[idx] != index; idx++) { 365 | } 366 | if (idx < nstrings) { 367 | ptr = (uint16_t*)(devstrings[lang].string_list[idx]); 368 | } 369 | } 370 | } 371 | return ptr; 372 | } 373 | -------------------------------------------------------------------------------- /usb_descriptors.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "tusb.h" 4 | void start_cloning(uint8_t dev_addr); 5 | void set_cloning_required(); 6 | bool cloning_is_required(); 7 | bool clone_next_string_is_required(); 8 | void clone_next_string(); 9 | bool descriptors_are_cloned(void); 10 | void set_descriptors_uncloned(void); 11 | TU_ATTR_WEAK void device_clone_complete_cb(); 12 | --------------------------------------------------------------------------------