Created
January 6, 2012 11:34
-
-
Save magpie514/1570211 to your computer and use it in GitHub Desktop.
Simple MML parser in C+SDL_Mixer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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