Skip to content

Instantly share code, notes, and snippets.

@magpie514
Created January 6, 2012 11:34
Show Gist options
  • Save magpie514/1570211 to your computer and use it in GitHub Desktop.
Save magpie514/1570211 to your computer and use it in GitHub Desktop.
Simple MML parser in C+SDL_Mixer
//Project Fallvale MML parsing component
//Usage:
//You can edit the string "tmp" at main(), or you can pass a MML-formatted string with "-play":
//for example, mmlplay -play "t128 o4 cdec+8>d8e16d16d8e"
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <assert.h>
#include <string.h>
#include <SDL/SDL.h>
#include <SDL/SDL_mixer.h>
#define AUDIO_AMP 32000
#define AUDIO_RATE 44100
#define AUDIO_TEMPO 128
#define AUDIO_BUFFER 2048
#define NOTE_C 261.63
#define NOTE_D 293.66
#define NOTE_E 329.63
#define NOTE_F 349.23
#define NOTE_G 392.00
#define NOTE_A 440.00
#define NOTE_B 493.88
#define PI 3.141592
#define HALF_PI 1.570796
#define DOUBLE_PI 6.283184
#ifndef max
#define max( a, b ) ( ((a) > (b)) ? (a) : (b) )
#endif
#ifndef min
#define min( a, b ) ( ((a) < (b)) ? (a) : (b) )
#endif
typedef Uint8 bool;
#define true 1
#define false 0
typedef Uint32 timer;
typedef struct NoteObj {
float cycle;
int samples, samples_max;
float volume;
Uint32 envelope[4];
Uint8 envelope_amount, envelope_pos;
} NoteObj;
#define MAX_MML_CHANNEL 4
//NOTE(Hetdegon@2012-01-15):Each channel holds its settings individually. The channel data is pretty much a basic queue.
typedef struct {
bool done;
NoteObj* data;
Uint8 tempo, octave, length, volume, steps; //TODO(Hetdegon@2012-01-11):All Uint8s be held into a single Uint32...?
float cycle;
Uint16 size, front, count;
Sint16(*modulator)(float, float, float);
float modulator_width;
Uint32 envelope[4];
Uint8 envelope_amount;
} MMLChannel;
//This is the main object, nothing odd.
typedef struct {
MMLChannel channel[MAX_MML_CHANNEL];
Uint8 channels_remaining;
Uint16 channel_size;
} MMLobject;
//NOTE(Hetdegon@2012-01-15):This is a small stack, it is used to parse loops in MML files.
typedef struct {
int anchor;
int loop_max;
int loops;
} LoopElement;
typedef struct {
LoopElement* data;
int top, size;
} LoopStack;
//Loop stuff
LoopStack* loopstack_init(unsigned int size){
LoopStack* new = calloc(1, sizeof(LoopStack)); assert(new);
new->data = calloc(size, sizeof(LoopElement)); assert(new->data);
new->size = size;
new->top = -1;
return new;
}
void loopstack_push(LoopStack* LS, int anchor){
if(LS->top >= LS->size - 1) return;
else {
LS->data[++LS->top].anchor = anchor;
LS->data[LS->top].loop_max = 0;
LS->data[LS->top].loops = 0;
}
}
void loopstack_pop(LoopStack* LS){
if(LS->top == -1) return;
else LS->top--;
}
void loopstack_del(LoopStack* LS){
free(LS->data);
free(LS);
}
bool quit = false;
Uint8* mml_buffer;
Mix_Chunk* chip_out;
//Quick get/set functions to store/retrieve Uint8s from the channel data.
static inline Uint8 envelope_getval(Uint32 bits, int pos){ return (bits >> (pos * 4)) & 15; }
void envelope_addval(Uint32* bits, int pos, Uint8 val) { *bits |= (min(val, 16) << (pos * 4)); }
//The modulators/waveform generators. Correct me if the terminology is wrong.
static inline Sint16 modulator_tri(float cycle, float width, float volume){ //Triangle waveform!
return ((cycle > width) ? (4.0f * cycle - 3.0f) : (-4.0f * cycle + 1.0f)) * volume * AUDIO_AMP;
}
static inline Sint16 modulator_sqr(float cycle, float width, float volume){ //Square waveform!
return (cycle > width) ? volume * AUDIO_AMP : -volume * AUDIO_AMP;
}
static inline Sint16 modulator_saw(float cycle, float width, float volume){ //Sawtooth waveform!
return (2 * cycle - 1.0f) * volume * AUDIO_AMP;
}
static inline Sint16 modulator_wns(float cycle, float width, float volume){ //White noise... ...form!
return (rand()%AUDIO_AMP - cycle * 13.0f) * volume;
}
static inline Sint16 modulator_sin(float cycle, float width, float volume){ //Sine waveform!
return AUDIO_AMP * sinf(cycle * DOUBLE_PI) * volume;
//return DOUBLE_PI*cycle * volume; <- This sounds so C64, but I can't even tell what shape it is!
}
void mmlchannel_init(MMLobject* mml, MMLChannel* C, Sint16(*modulator)(float,float,float)){
C->data = calloc(mml->channel_size, sizeof(NoteObj));
C->size = mml->channel_size;
C->tempo = AUDIO_TEMPO;
C->steps = C->octave = C->length = 4;
C->volume = 10;
C->modulator = modulator;
C->modulator_width = 0.5f;
C->done = true;
//TODO(Hetdegon@2012-01-15): We aren't reading the volume envelope macros yet, so this sets up a temporary one.
envelope_addval(&C->envelope[0], 0, 3);
envelope_addval(&C->envelope[0], 1, 7);
envelope_addval(&C->envelope[0], 2, 10);
envelope_addval(&C->envelope[0], 3, 10);
envelope_addval(&C->envelope[0], 4, 10);
envelope_addval(&C->envelope[0], 5, 10);
envelope_addval(&C->envelope[0], 6, 0);
C->envelope_amount = 6;
}
void mmlchannel_enqueue(MMLobject* mml, MMLChannel* C, float freq, float time){
if(C->count >= C->size) return;
if(C->done == true) C->done = false;
NoteObj* note = &C->data[((C->count + C->front) % C->size)];
//NOTE(Hetdegon@2012-01-15):This alters the frequency to match the current octave.
freq *= powf(2.0f, C->octave - 5.0f);
note->cycle = 1.0f / (AUDIO_RATE / freq);
//NOTE(Hetdegon@2012-01-15):samples/samples_max is the time in samples for this note.
//Note that one second is equal to AUDIO_RATE.
note->samples_max = note->samples = (C->steps * time) * AUDIO_RATE * (AUDIO_TEMPO / (float)C->tempo);
note->volume = (float)C->volume / 15.0f;
for(int i = 0; i < 4; i++) note->envelope[i] = C->envelope[i];
note->envelope_amount = C->envelope_amount;
note->envelope_pos = 0;
C->count++;
}
bool mmlobject_check(MMLobject* mml){
for(int i = 0; i < MAX_MML_CHANNEL; i++) if(mml->channel[i].done == false) return false;
return true;
}
void mmlchannel_dequeue(MMLobject* mml, MMLChannel* C){
if(C->count == 1 && C->done == false){
C->done = true;
if(mmlobject_check(mml) == true) Mix_UnregisterAllEffects(0);
return;
}
C->front++;
C->front %= C->size;
C->count--;
}
MMLobject* mmlobject_init(int size){
MMLobject* mml = calloc(1, sizeof(MMLobject));
assert(mml);
mml->channel_size = size;
mmlchannel_init(mml, &mml->channel[0], &modulator_sqr);
mmlchannel_init(mml, &mml->channel[1], &modulator_tri);
mmlchannel_init(mml, &mml->channel[2], &modulator_sin);
mmlchannel_init(mml, &mml->channel[3], &modulator_wns);
return mml;
}
void mmlobject_clear(MMLobject* mml){
free(mml->channel[0].data);
free(mml->channel[1].data);
free(mml->channel[2].data);
free(mml->channel[3].data);
free(mml);
}
static inline float note_envelope(NoteObj* n){
if(n->samples % (n->samples_max / n->envelope_amount) == 0 && n->envelope_pos < n-> envelope_amount){
n->envelope_pos++;
}
//printf("envelope=%i(%i:%i)\n", envelope_getval(n->envelope[(n->envelope_pos / 16)], n->envelope_pos % 8), n->envelope_pos / 16, n->envelope_pos % 8);
return ((float)envelope_getval(n->envelope[n->envelope_pos / 16], n->envelope_pos % 8) / 15.0f) * n->volume;
}
Sint16 mml_channel_process(MMLobject* mml, MMLChannel* C){
if(C->done == true) return 0;
NoteObj* note = &C->data[C->front];
if(note->samples == 0) { mmlchannel_dequeue(mml, C); return 0; }
if(C->modulator != NULL){
Sint16 result = C->modulator(C->cycle, C->modulator_width, note_envelope(note));
C->cycle += note->cycle; if(C->cycle > 1.0f) C->cycle -= 1.0f;
note->samples--;
return result;
} else return 0;
}
void mml_play(MMLobject* mml, Sint16* stream, int len){
int i = 0;
Sint16 channel_out[MAX_MML_CHANNEL];
while(i < len){
channel_out[0] = mml_channel_process(mml, &mml->channel[0]);
channel_out[1] = mml_channel_process(mml, &mml->channel[1]);
channel_out[2] = mml_channel_process(mml, &mml->channel[2]);
channel_out[3] = mml_channel_process(mml, &mml->channel[3]);
stream[i++] = (channel_out[0] + channel_out[1] + channel_out[2] + channel_out[3]) >> 2;
}
}
void chip_out_finish2(int channel){
//NOTE(Hetdegon@2012-01-06):This is for compatibility with Fallvale, you can completely ignore this exists.
printf("Closing channel\n");
quit = true;
}
void chip_out_finish(int channel, void*_){
//NOTE(Hetdegon@2012-01-06):We ignore the userdata in this specific case. When used in Fallvale, it would be a good
//idea to clean up audio objects here.
printf("Stream finished\n");
quit = true;
}
void audio_callback(int channel, void *stream, int len, void* mml){
//NOTE(Hetdegon@2012-01-06):Once the effects system is set as seen in main(),
//this will work as a regular SDL Audio callback.
mml_play((MMLobject*)mml, (Sint16*)stream, len >> 1);
}
int token_value_read(char* str, int* i, int len, int defaultval){
int retval = -1, add = 0;
if(*i == len) return defaultval;
if(sscanf(&str[*i], "%3u%n", &retval, &add)){
*i += add;
return retval;
} else return defaultval;
}
void token_note_read(char* str, int* i, int len, MMLobject* mml, MMLChannel* C, char note){
//NOTE(Hetdegon@2012-01-06):We know that the order is <note><dot><duration><period>. Thus...
float retval = 1.0; int val = 0;
float freq, dot = 1.0;
switch (note){
case 'c': freq = NOTE_C; break;
case 'd': freq = NOTE_D; break;
case 'e': freq = NOTE_E; break;
case 'f': freq = NOTE_F; break;
case 'g': freq = NOTE_G; break;
case 'a': freq = NOTE_A; break;
case 'b': freq = NOTE_B; break;
case 'r': freq = false; break;
default: break;
}
if(*i == len){ //NOTE(Hetdegon@2012-01-06):End of string, no modifiers are present, so take a shortcut.
printf("Queuing %c(%.2f) with length %i %i. String is over\n", note, freq, C->length, *i);
mmlchannel_enqueue(mml, C, freq, 1.0/(float)C->length);
*i++;
return;
}
switch(str[*i]){//Get the dot value (sharp or flat notes).
case '+': case '#': dot = 1.059463; *i += 1; break; //NOTE(Hetdegon@2012-01-06):For some reason "*i++" didn't work.
case '-': dot = 0.943874; *i += 1; break;
default: dot = 1.0; break;
}
freq *= dot;
val = token_value_read(str, i, len, C->length);
char temp = str[*i];
if(temp == '.') while(temp == '.' && *i < len){ //We iterate through the string to see if there are one, many or no periods.
//NOTE(Hetdegon@2012-01-06):The period, as seen in MML references, should decrease duration by the double of the
//set duration, thus...
val = val + (val * 2);
temp = str[*++i];
};
val = min(max(val, 1), 64);
printf("Queuing %c(%.2f) with length %i, dot modifier %.3f. i:%i\n", note, freq, val, dot, *i);
mmlchannel_enqueue(mml, C, freq, 1.0/val);
*i++;
}
void string_copyinto(char* src, char* dest){ memcpy(dest, src, strlen(src) + 1); }
char* string_trimspace(char* str){
//TODO(Hetdegon@2012-01-06): Do not trim spaces inside {} signs. For macro support later on (such as envelope).
char* buffer = malloc((strlen(str) + 1)*sizeof(char));
char* s = str, *d = buffer;
do { while(isspace(*s)) s++; } while(*d++ = *s++);
return buffer;
}
void mml_parse(MMLobject* bq, char* mml_str){
printf("Parsing MML string: \"%s\"\n", mml_str);
int len = strlen(mml_str), i = 0;
MMLChannel* channel = &bq->channel[0];
LoopStack* loop_controller = loopstack_init(10);
while (i < len){
switch (mml_str[i]){
case 'c': case 'd': case 'e': case 'f': case 'g': case 'a': case 'b': case 'r': { //Individual notes.
char c = mml_str[i++];
token_note_read(mml_str, &i, len, bq, channel, c);
break;
}
case '[':{ //Loop start
i++;
//NOTE(Hetdegon@2012-01-14):We push an "anchor" point to a smallish stack.
//It will simply return to this point until done, then pop it.
//It avoids using recursion or parsing the string beforehand.
loopstack_push(loop_controller, i);
break;
}
case ']':{ //Loop end.
if(loop_controller->top == -1) {i++; break;} //No loops in the stack, this shouldn't really happen.
LoopElement* top = &loop_controller->data[loop_controller->top];
if(top->loop_max == 0){ //'0' meaning that we don't know how many loops there are until this point.
i++;
//NOTE(Hetdegon@2012-01-14):If there is a number after ']', we attempt to read it, otherwise, loop twice.
top->loop_max = token_value_read(mml_str, &i, len, 2);
top->loops++;
i = top->anchor;
}
if(top->loops == top->loop_max){ //We know how many loops exist at this point, so we can just check.
loopstack_pop(loop_controller);
i++;
//NOTE(Hetdegon@2012-01-14):This line ensures we get past the loop number.
while(isdigit(mml_str[i]) && i < len) i++;
} else { //We have loops remaining, return to anchor point.
top->loops++;
i = top->anchor;
}
break;
}
case 'A':{ //Channel 1
i++;
channel = &bq->channel[0];
break;
}
case 'B':{ //Channel 2
i++;
channel = &bq->channel[1];
break;
}
case 'C':{ //Channel 3
i++;
channel = &bq->channel[2];
break;
}
case 'D':{ //Channel 4
i++;
channel = &bq->channel[3];
break;
}
case 'w':{ //Wait or hold note.
i++;
NoteObj* note = &channel->data[channel->front];
if(i == len) { //No modifiers present because we ran out of string.
note->samples_max = note->samples += (channel->steps * (1.0f/(float)channel->length) * AUDIO_RATE * (AUDIO_TEMPO / (float)channel->tempo));
printf("Hold for %i(%i samples). String is over\n", channel->length, note->samples_max);
return;
} else { //We simply modify the value of the latest note in the channel, by adding the new duration.
int val = token_value_read(mml_str, &i, len, channel->length);
char temp = mml_str[i];
if(temp == '.') while(temp == '.' && i < len){
val = val + (val * 2);
temp = mml_str[++i];
};
note->samples_max = note->samples += (channel->steps * (1.0f/(float)val) * AUDIO_RATE * (AUDIO_TEMPO / (float)channel->tempo));
printf("Hold for %i(%i samples)\n", val, note->samples_max);
}
break;
}
case 't':{ //Tempo.
i++;
int val = token_value_read(mml_str, &i, len, 128);
val = min(max(val, 30), 240);
channel->tempo = val; printf("Tempo set to %i\n", val);
break;
}
case 'l':{ //Length.
i++;
int val = token_value_read(mml_str, &i, len, 4);
val = min(max(val, 1), 64);
channel->length = val; printf("Length set to %i\n", val);
break;
}
case 'v':{ //Volume.
//NOTE(Hetdegon@2012-01-04): A lone "v" should set volume to 0...I wonder how standard this can be.
i++;
int val = token_value_read(mml_str, &i, len, 12);
val = min(max(val, 0), 15);
channel->volume = val; printf("Volume set to %i\n", val);
break;
}
case 'o':{ //Octave.
//NOTE(Hetdegon@2012-01-04): Same as above...Perhaps I should make it so it resets to some default.
i++;
int val = token_value_read(mml_str, &i, len, 4);
val = min(max(val, 0), 8);
channel->octave = val; printf("Octave set to %i\n", val);
break;
}
case '<': /*Quick octave down.*/ i++; channel->octave--; break;
case '>': /*Quick octave up.*/ i++; channel->octave++; break;
default: i++; break;
}
}
printf("1:%i, 2:%i, 3:%i, 4:%i\n", bq->channel[0].count, bq->channel[1].count, bq->channel[2].count, bq->channel[3].count);
loopstack_del(loop_controller);
}
int main(int argc, char** argv){
char* tmp = NULL, *defstring = "t 128 v8 o4 c d e f v1 g4 a16 b16 >c c v15 d e f g4 a16 b16 >c<< r";
//NOTE(Hetdegon@2012-01-06):The string is messed up on purpose so the space removing function can be tested...
atexit(SDL_Quit);
while(argc--){ //Catch command line arguments.
if(!strcmp(*argv++, "-play")){
printf("%s\n", *argv);
tmp = malloc(sizeof(char)*strlen(*argv) + 1);
string_copyinto(*argv, tmp);
}
}
SDL_Init(SDL_INIT_AUDIO);
MMLobject* MMLO = mmlobject_init(128);
if(Mix_OpenAudio(AUDIO_RATE, AUDIO_S16SYS, 2, AUDIO_BUFFER)){
printf("Cannot initialize audio, exiting.\nReturned error was %s\n", Mix_GetError());
return EXIT_FAILURE;
} else printf("Audio initialized correctly\nRate:%i, Buffer size:%i\n", AUDIO_RATE, AUDIO_BUFFER);
Mix_AllocateChannels(1); //Only one channel for the purposes of this test. Fallvale will attempt to allocate 12.
//NOTE(Hetdegon@2012-01-06):This is the backbone. mml_buffer is pretty much the equivalent of loading a WAV file.
//This effectively works around the incompatibilities between SDL_Mixer and the audio component of SDL providing
//real-time sound synthesis without much effort.
mml_buffer = calloc(AUDIO_BUFFER, sizeof(Uint8));
chip_out = Mix_QuickLoad_RAW(mml_buffer, AUDIO_BUFFER);
if(tmp == NULL) { //Add default string if none specified.
tmp = malloc(sizeof(char)*strlen(defstring) + 1);
string_copyinto(defstring, tmp);
}
char* mml = string_trimspace(tmp);
mml_parse(MMLO, mml);
quit = false;
//NOTE(Hetdegon@2012-01-06):Now set, we have all the advantages of SDL_Audio's streams, but without having to lock/unlock!
//I am using channel 0 since nothing else is using sound, but in Fallvale this will probably be channel 10 or so.
Mix_PlayChannel(0, chip_out, -1);
Mix_RegisterEffect(0, &audio_callback, &chip_out_finish, MMLO);
Mix_ChannelFinished(&chip_out_finish2);
while(quit == false) SDL_Delay(250);
SDL_Delay(1);
free(mml_buffer);
Mix_FreeChunk(chip_out);
Mix_CloseAudio();
mmlobject_clear(MMLO);
return EXIT_SUCCESS;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment