/* synth.c version 0.05, dec 7, 2024. A simple MIDI synthesizer. Latest version available at https://ecomaan.nl/c/wraiff/synth COPYLEFT 2004 2024 Pieter Suurmond Demonstrates how to read a standard MIDI file and how to write an AIFF soundfile. It furthermore shows the elegance of a singly linked list as a musical datastructure (i.e. a sequence). Since our MIDI parser reads track by track, we first have to merge them all into one sequential datastructure (otherwise we possibly would have to allocate huge amounts of audio-memory). We do this by building up a linked list of chronologically ordered note-on and note-off events. Then, when all tracks are read, the MIDI file is closed and the linked list is passed-on to a routine that breaks it down again, while playing to AIFF file. */ #include #include /* Header rdmid.h needs FILE* and we also printf. */ #include /* For sin(). and pow(). */ #include "rdmid.h" /* Also include rdmid.c in your project or makefile. */ #include "wraiff.h" /* Also include wraiff.c in your project or makefile. */ /* Singly linked list with time-ordered events. */ typedef struct EVT* EVTp; /* Forward declaration, needed for recursion. */ typedef struct EVT /* Mention EVT twice: before and after { }. */ { EVTp next; long time; short what; /* <0 --> note-off; >0 --> note-on. */ } EVT; /* BUG: we cannot use note number 0 this way. */ /* Inserts a new event into the linked list, while maintaining the time-order. Called by the event handlers, during read_midi_file() building-up the list. The linked list is released (later on) by function write_aiff_file(). */ short insert_evt(EVTp* base, long time, short what) { EVTp fresh = malloc(sizeof(EVT)); if (!fresh) return 1; fresh->next = *base; /* Should be NULL when insert_evt() is called for the first time. */ fresh->time = time; fresh->what = what; /* Negative means note-off, positive note-on (cannot use #0). */ while (*base && (time >= (*base)->time)) { base = &(*base)->next; fresh->next = *base; } *base = fresh; return 0; } void destroy_evt(EVTp* base) { while (*base) { EVTp h = *base; *base = h->next; free(h); } } /* Event handler that will be invoked by the parser after having read the header. */ static int my_header(void* userdata, int format, int tracks, int division) { (void)userdata; /* Prevent warning for unused parameter. */ printf("Header: format=%d, tracks=%d, div=%d.\n", format, tracks, division); return 0; } /* Event handler that will be called after a note-off event is read. */ int my_note_off(void* userdata, long time, int channel, int note, int velocity) { (void)channel; /* Prevent warning for unused parameter. */ (void)velocity; if (insert_evt((EVTp*)userdata, time, 0-note)) return 1; /* Memory failure, stop the parser. */ return 0; } /* Event handler that will be called after a note-on event is read. */ int my_note_on(void* userdata, long time, int channel, int note, int velocity) { if (velocity) /* Don't react to alternative note-off events. */ { if (insert_evt((EVTp*)userdata, time, note)) return 1; /* Memory failure, stop the parser. */ } else /* Patch thru to above func, so all off-events pass my_note_off(). */ return my_note_off(userdata, time, channel, note, 0); return 0; } /* Reads a complete MIDI file and leaves a pointer to linked list in *list. In case of failure (when non-zero is returned), there MAY still be a linked list in memory! Make sure you clean it up. */ int read_midi_file(const char* filename, EVTp* list) { int e; FILE* fp; fp = fopen(filename, "rb"); if (fp) /* 24 arguments, sorry! */ { e = rdmid((void*)list, /* Userdata. */ fp, /* An open stream to read from. */ 0, /* Merge broken sysex messages. */ /* Here follow 21 function pointers to your handlers. */ /* 3 evt handlers for header and tracks: */ /* ----> */ my_header, /* eh_header() */ /* Only */ NULL, /* eh_trackstart() */ /* these */ NULL, /* eh_trackend() */ /* three */ /* 7 channel events: */ /* ----> */ my_note_off, /* eh_note_off() */ /* ----> */ my_note_on, /* eh_note_on() */ NULL, /* eh_poly_press() */ NULL, /* eh_controller() */ NULL, /* eh_program() */ NULL, /* eh_chan_press() */ NULL, /* eh_pitchbend() */ /* 1 sysex event: */ NULL, /* eh_sysex() */ /* 10 meta events: */ NULL, /* eh_seqnum() */ NULL, /* eh_text() */ NULL, /* eh_eot() */ NULL, /* eh_tempo() */ NULL, /* eh_smpte() */ NULL, /* eh_timesig() */ NULL, /* eh_keysig() */ NULL, /* eh_seq_spec() */ NULL, /* eh_metamisc() */ NULL ); /* eh_arbitrary() */ if (e == RDMID_OK) printf("Ok, read standard MIDI file '%s'.\n", filename); else if (e == RDMID_USER_TERM) printf("Noticed a memory failure while parsing '%s'!\n", filename); else /* Some error. */ printf("Sorry, an error occured: rdmid() = %d!\n", e); if (fclose(fp)) /* Apart from the file we opened ourselves, there is */ { printf("Error while closing the MIDI file!\n"); e = 1; /* nothing to close or to free after calling rdmid(). */ } } else { printf("Could not open standard MIDI file '%s'!\n", filename); e = 1; } return e; } /* Expects a pointer to a linked list in *list, and a pointer to a C-string to use as filename. It creates an 8 bit monofile. It is up to the student to add panning, volume, velocity,... and make a little better sound! :-) */ int write_aiff_file(EVTp* list, const char* filename) { #define kMAX_NUM_NOTES 128 /* We disregard MIDI channels for now. */ static const long sr = 11025; /* Sufficient for these dull sinewaves. */ int e; long m, n; short note; short state[kMAX_NUM_NOTES]; double freq[kMAX_NUM_NOTES]; double Pi2; double s; /* Interleaved mono buffer (containing */ WRAIFFp a; /* just a single frame for simplicity). */ e = WRAIFF_open(&a, /* Receive pointer to WRAIFF object. */ filename, /* Filename. */ sr, /* Samplerate (in Hertz). */ 1, /* Number of interleaved channels. */ 8); /* Number of bits resolution (8,16,24). */ /* Increase to 16 bits for better audio quality. Do you hear the q-noise? */ if (e) { printf("Cannot create/overwrite '%s'!\nopen_wraiff()=%d\n", filename, e); return 1; } Pi2 = 8.0 * atan(1.0); for (note = 0; note < kMAX_NUM_NOTES; note++) /* Clear states. */ { /* And calc frequencies. */ state[note] = 0; freq[note] = (8.25 * Pi2 * pow(2.0, (double)note / 12.0)) / (double)sr; } /* 8.25 Hz approximates frequency of MIDI note number 0. */ printf("Recording '%s'...\n", filename); n = 0L; while (*list) /* Eat up the linked list while playing it to file. */ { EVTp h = *list; /* Play audio, up to the new event. */ m = h->time * 50L; /* 50 audiosamples per MIDI time unit. */ while (n < m) { /* Here is the heart of our 'synthesizer'. */ s = 0.0; for (note = 0; note < kMAX_NUM_NOTES; note++) { if (state[note]) /* Non-zero means that note is on. */ s += 0.2 * sin((double)n * freq[note]); } /* No oscillator-sync yet. */ e = WRAIFF_double (a, &s, 1); /* Only 1 frame in buffer, */ if (e) /* therefore call with 1. */ { printf("Error writing '%s'!\nwrite_wraiff()=%d\n", filename, e); break; /* But we continue freeing the list. */ } n++; } /* The new event may alter one of the states. */ if (h->what > 0) state[h->what] = 1; else state[0-h->what] = 0; /* Release event from memory and advance to the next event. */ *list = h->next; free(h); } WRAIFF_info (a, stdout); /* Print file statistics to stdout. */ e = WRAIFF_close (&a); /* Releases the WRAIFF object, rewrites */ if (e) /* hdr, closes file and sets a to NULL. */ { printf("Error while closing '%s'!\nclose_wraiff()=%d\n", filename, e); e = 1; } return e; } int main(void) { int e = 0; /* No errors. */ EVTp list = (EVTp)NULL; /* Empty list. */ if (read_midi_file("zefile.mid", &list)) /* Receive linked list. */ { printf("Sorry, could not read the MIDI file!\n"); e = 1; } else if (write_aiff_file(&list, "zefile.aiff")) /* Destroy linked list. */ { printf("Sorry, could not write the AIFF file!\n"); e = 1; } destroy_evt(&list); /* There still could be a list left. */ return e; } /* 2004 - Code could be optimised: While reading a single track, we may expect the events to arrive in chronological order. So when inserting events to the linked list, we do not have to search the whole list each time, we'd better pass a pointer that follows our las insert instead of passing the base-pointer each time. Only when the parser advances to the next track, it is necessary to start inserting at base again (we could use event handlers eh_trackstart() or eh_trackend() to implement this). - About the sound: with Bach this is rather ok. The variaty in attack- and release-clicks is due to the lack of oscillator-sync. The clicks them- selves are due to the lack of envelopes (just raw tone-burst now). - Let this be an inspiration for improvements: add more features, make nicer sounds! */