The micro USB port on the STMF4Discovery board is a OTG (“on the go”) USB port. This means it can function as both a slave or master USB port. To use it as a MIDI host (so we can use MIDI keyboard and fader controllers) we need a OTG adapter. Here is one from Amazon. These are typically used to attach flash drives to Android phones. The STM32Cube has an example of USB Host and Device code in the Middleware folder. However, instead of using that to develop a class-compliant MIDI host, I have simply adapted Xavier Halgand‘s USB MIDI host code to work with the current version of the STM HAL libraries. Xavier is the author of the wonderful Dekrispator synthesizer for the STM32F4Discovery board.
My MIDI framework with a basic (terrible sounding) polyphonic synthesizer can be downloaded here: f4disco-midi. You should be able to to unzip, place it in your workspace folder, and import to STM32CubeIDE.
You will see a number of things added to this project, including a struct and enum to support each synthesizer voice. Each voice represents an independent sound generator, and these can become fairly complex. Our voice will be a simple, unaliased, sawtooth oscillator with a fixed ASR gain envelope. It requires the following for each voice:
typedef struct synthVoice { int active; int note; int velocity; float volume; float frequency; float phase; } synthVoice; typedef enum { INACTIVE = 0, ATTACK, DECAY, SUSTAIN, RELEASE } voiceState;
The active variable will indicate the various states that each voice can be in, these states are in the following enum. The remaining members of the synthVoice struct are synthesis parameters. Later you will see 16 synthVoices being allocated
synthVoice voice[16];
In the function main(), the USB MIDI driver is initialized, and then two functions are called in the main while loop. The function MIDIApplication() parses any MIDI received through USB, and the function USBH_Process(&hUSBHost) maintains the USB connection – it allows plugging and unplugging of USB devices.
/*## Init Host Library ################################################*/ USBH_Init(&hUSBHost, USBH_UserProcess_callback, 0); /*## Add Supported Class ##############################################*/ USBH_RegisterClass(&hUSBHost, USBH_MIDI_CLASS); /*## Start Host Process ###############################################*/ USBH_Start(&hUSBHost); // Initialize and start audio codec BSP_AUDIO_OUT_Init(OUTPUT_DEVICE_HEADPHONE, 90, 48000); BSP_AUDIO_OUT_Play((uint16_t *)codecBuffer, 128); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { HAL_Delay(1); /* USER CODE END WHILE */ // MX_USB_HOST_Process(); /* USER CODE BEGIN 3 */ MIDI_Application(); /* USBH_Background Process */ USBH_Process(&hUSBHost); }
MIDI_Application() calls a function I wrote called ProcessMIDI(). This processes one MIDI message, identifying the byte code for the type of MIDI message, and copys the data from the message. I have implemented the parsing for “note on”, “note off” and “controller change”. Framework for other MIDI messages (status type), is included. To parse all of the types of MIDI message, you will need to refer to the MIDI Specification (easily found online).
void ProcessMIDI(midi_package_t pack) { int i; uint8_t status; // Status messages that start with F, for all MIDI channels // None of these are implemented - though we will flash an LED // for the MIDI clock status = pack.evnt0 & 0xF0; if(status == 0xF0) { switch(pack.evnt0) { case 0xF0: // Start of System Exclusive case 0xF1: // MIDI Time Code Quarter Fram case 0xF2: // Song Position Pointer case 0xF3: // Song Select case 0xF4: // Undefined case 0xF5: // Undefined case 0xF6: // Tune Request case 0xF7: // End of System Exclusive status = runningStatus = 0x00; break; case 0xF8: // Timing Clock (24 times a quarter note) HAL_GPIO_TogglePin(LD5_GPIO_Port, LD5_Pin); // RED LED break; case 0xF9: // Undefined case 0xFA: // Start Sequence case 0xFB: // Continue Sequence case 0xFC: // Pause Sequence case 0xFD: // Undefined case 0xFE: // Active Sensing case 0xFF: // Reset all synthesizers to power up break; } } // MIDI running status (same status as last message) doesn't seem to work over this USB driver // code commented out. // else if((pack.evnt0 & 0x80) == 0x00) // status = runningStatus; else runningStatus = status = pack.evnt0 & 0xF0; switch(status) { case 0x80: // Note Off // turn off all voices that match the note off note for(i = 0; i<16; i++) { if(voice[i].note == pack.evnt1) { voice[i].active = RELEASE; keyboard[voice[i].note] = -1; } } break; case 0x90: // Note On if(pack.evnt2 == 0) // velocity 0 means note off // turn off all voices that match the note off note for(i = 0; i<16; i++) { if((voice[i].note == pack.evnt1) && (voice[i].active != INACTIVE)) { voice[i].active = RELEASE; keyboard[voice[i].note] = -1; } } else { // if this key is already on, end the associated note and turn it off if(keyboard[pack.evnt1] != -1) { voice[keyboard[pack.evnt1]].active = RELEASE; keyboard[pack.evnt1] = -1; } // find an inactive voice and assign this key to it for(i = 0; i<16; i++) { if(voice[i].active == INACTIVE) { voice[i].active = ATTACK; voice[i].note = pack.evnt1; voice[i].velocity = pack.evnt2; keyboard[pack.evnt1] = i; break; } } } break; case 0xA0: // Polyphonic Pressure break; case 0xB0: // Control Change switch(pack.evnt1) // CC number { case 3 : break ; // tempo case 7 : break; // master volume } break; case 0xC0: // Program Change break; case 0xD0: // After Touch break; case 0xE0: // Pitch Bend pitchbend = pack.evnt2 * 128 + pack.evnt1; break; } }
My implementation of ProcessMIDI() doesn’t do voice allocation (though it does look for inactive voices before assigning a new one), and isn’t doing anything with the data from CC (controller change) messages. There is lots of room for improvement, this is just intended as a starting point.
The audioBlock() audio callback function has been modified to generate up to 16 voices, each synthesizing a harsh sawtooth wave. The data set in ProcessMIDI() is being used to set the frequency of each voice, and to manage the ASR envelope in each voice. Note that when a note off is received by ProcessMIDI, the voice is not made inactive. Instead, the voice is put in the RELEASE part of the envelope, and is only made INACTIVE when the envelope reaches zero.
void audioBlock(float *input, float *output, int32_t samples) { int i, v; float increment1, increment2; for(i = 0; i < samples; i += 2) output[i<<1] = output[(i<<1) + 1] = 0.0f; for(v = 0; v < 16; v++) { if(voice[v].active != INACTIVE) { voice[v].frequency = mtoinc[voice[v].note]; for(i = 0; i < samples; i += 2) { voice[v].phase += voice[v].frequency; if(voice[v].phase > 1.0f) voice[v].phase = voice[v].phase - 1.0f; output[i<<1] += ((voice[v].phase * 2.0f) - 1.0f) * voice[v].volume * 0.125f; output[(i<<1) + 1] = output[i<<1]; if(voice[v].active == ATTACK) { voice[v].volume += 0.02f; if(voice[v].volume >= 1.0f) voice[v].active = SUSTAIN; } else if(voice[v].active == SUSTAIN) voice[v].volume = 1.0f; else if(voice[v].active == RELEASE) { voice[v].volume -= 0.0002f; if(voice[v].volume <= 0.0f) voice[v].active = INACTIVE; } } } } }
This code should be useful as a starting point for synthesizer creation. Those creating drum machines or effects processors will want to handle MIDI messages differently.