Adding the USB OTG MIDI Driver – Polyphonic framework

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.

Leave a Reply

Your email address will not be published.