Thursday, September 21, 2017

Playing MIDI on an STM32F446 Part 1: Square Waves

I've decided to try to make an 8-bit style music player out of a microcontroller. There are some really cool arduino versions of this project, but they all seem to require a very confusing custom file format to describe the song. My goal is to use a simple format that can be easily generated from a midi file.  As a proof of concept, I wrote some code which plays a midi file converted to a list of note data in the format "start time, duration, pitch, volume".

#include "mbed.h"
#include "music.h"
#include <math.h>
AnalogIn in(A0);
Ticker music_ticker;
#define knum_notes 8
#define ksamples 71
#define kfreq 71000
AnalogOut out(PA_4);
void music_isr();
DigitalOut led(LED1);
float frequencies[127];
int periods[127];
unsigned long song_data_counter = 0;
unsigned long cycles_counter = 0;
char notes_playing[knum_notes];
int speedup = 1;
struct playing_note
{
uint16_t cycles_to_go;
char midi_pitch;
};
struct osc
{
uint16_t phase;
uint16_t period;
uint16_t vol;
};
osc oscs[knum_notes];
playing_note current_notes[knum_notes];
int main()
{
led = 1;
for(int i = 0; i < 127; i++)
{
frequencies[i] = 440*pow(2,(i - 69)/12.0f);
periods[i] = kfreq/frequencies[i];
}
for(int i = 0; i < knum_notes; i++)
notes_playing[i] = 0;
music_ticker.attach(&music_isr, 1.0f/kfreq);
led = 0;
}
int max(int a, int b)
{
if(a>b) return a;
return b;
}
char remap_midi(char in)
{
while(in > 95) in-=12;
while(in < 40) in+=12;
return in;
}
void handle_notes();
void music_isr()
{
handle_notes();
int now = speedup*cycles_counter/ksamples;
cycles_counter++;
const int* note_data = &(songdata[song_data_counter*4]);
int milliseconds = note_data[0];
if(milliseconds < 0 )
{
song_data_counter++;
return;
}
if(milliseconds > now )
return;
for(int i = 0; i < knum_notes; i++)
{
if(!notes_playing[i])
{
notes_playing[i] = 1;
current_notes[i].cycles_to_go = max(note_data[1]*ksamples/speedup,6500);
current_notes[i].midi_pitch = remap_midi(note_data[2]);
song_data_counter++;
return;
}
}
}
void handle_notes()
{
int num_ons = 0;
for(int i = 0; i < knum_notes; i++)
{
if(notes_playing[i])
{
current_notes[i].cycles_to_go--;
if(current_notes[i].cycles_to_go == 0)
{
notes_playing[i] = false;
continue;
}
oscs[i].period = periods[current_notes[i].midi_pitch];
}
}
for(int i = 0; i < knum_notes; i++)
{
if(notes_playing[i])
{
oscs[i].phase++;
if(oscs[i].phase > kfreq) oscs[i].phase = 0;
if(oscs[i].phase > (oscs[i].period/2))
num_ons++;
if(oscs[i].phase > (oscs[i].period))
oscs[i].phase = 0;
}
out.write_u16(8000*num_ons);
}
}
view raw music.c hosted with ❤ by GitHub
This version has a known issue with high pitch notes. If a note requires a duty cycle of 80.5 DAC cycles, it will just round down to 80 cycles, which makes it sound flat. Instead, it should alternate between 80 and 81.

Eventually, I plan to add more sounds and effects other than "square wave with 50% duty cycle", but for now, that's the only choice. The plan is to make each instrument kind of like a script. Each instrument would have an array of function pointers and arguments which get executed in order. Functions could do things like "delay 20 instrument cycles", "set output to triangle wave", "increase pitch by major third", "do a vibrato effect", or even "move function pointer array pointer back n steps".

I plugged the DAC into my computer's line in port, and recorded this:
https://www.youtube.com/watch?v=bGQ_Zj2jo8s

No comments:

Post a Comment