#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "main.h"
#include "mml.h"
#include "mml_parse.h"
#include "mml_channels.h"
#include "mml_macros.h"
#include "stream.h"
#include "text.h"
//***************************************************************************
// load_mml
// Reads the source MML file and parses it. Returns a stream of its commands
// which can be fed to save_sona to generate the SonaStream file later.
//---------------------------------------------------------------------------
// param filename: input file name
//---------------------------------------------------------------------------
// return: pointer to stream (NULL on failure)
//***************************************************************************
Stream *load_mml(const char *filename)
{
// Try to open MML file in the first place
FILE *file = fopen(filename, "rb");
if (file == NULL) {
fprintf(stderr, ERRORMSG_OPENMML, filename);
return NULL;
}
// Allocate stream where we'll store the parsed MML commands
Stream *stream = create_stream();
// Counter used to keep track of the current line number
// Note that it's incremented *before* parsing the line, so the first
// line is actually line 1, not line 0 (which lines up nicely with how
// users expect to read line numbers in error messages)
unsigned line_num = 0;
int errored = 0;
// Scan through entire file
while (!feof(file)) {
// Read next line
char *line = read_line(file);
line_num++;
// Was there a problem reading the file?
if (ferror(file)) {
fprintf(stderr, ERRORMSG_READMML, filename);
free(line);
fclose(file);
delete_stream(stream);
return NULL;
}
// Scan for a comment and strip it out if present
char *comment = strchr(line, ';');
if (comment != NULL)
*comment = '\0';
// Some MML files start every line with ' which is the BASIC way to
// insert comments, since MML originates from BASIC's PLAY command and
// it was common to store MML data inside comments.
// Starting every line with ' isn't required here, but some composers
// will keep doing it out of habit anyway so skip it if found
const char *ptr = skip_spaces(line);
if (*ptr == '\'')
ptr++;
// Check where the first non-blank character is
// If there isn't any then move on (we ignore blank lines)
ptr = skip_spaces(ptr);
if (*ptr == '\0') {
free(line);
continue;
}
// Is it a macro definition?
if (*ptr == '!') {
// Get macro name
ptr++;
int id = parse_macro_id(&ptr, filename, line_num,
(unsigned)(ptr - line - 1));
if (id < 0) {
free(line);
errored = 1;
continue;
}
ptr = skip_spaces(ptr);
char *expanded_line = expand_macros(ptr,
filename, line_num, (size_t)(ptr - line));
set_macro(id, expanded_line);
free(line);
free(expanded_line);
continue;
}
// Get list of channels
unsigned chan_mask = decode_channel_list(ptr, filename, line_num);
if (chan_mask == 0) {
free(line);
errored = 1;
continue;
}
// Skip over list of channels to get to the commands
// It's possible that there are none, in which case we'll just pretend
// that it's the same as a blank line
ptr = skip_nonspaces(ptr);
if (*ptr == '\0') {
free(line);
continue;
}
// Note that at this point *ptr is pointing to a space. This matters,
// since it pretty much prevents tokens that shouldn't be broken up
// to wrap around lines (because the space will break them).
// Append the commands to each channel in the mask and move on
// We'll process the commands as a single string after we're done
// reading the MML file
char *expanded_line = expand_macros(ptr,
filename, line_num, (size_t)(ptr - line));
add_text_to_channel(chan_mask, expanded_line, line_num,
(size_t)(ptr - line));
free(line);
free(expanded_line);
}
// Done parsing the file
fclose(file);
for (Channel i = 0; i < NUM_CHANNELS; i++) {
int r = parse_mml_commands(stream, i, filename);
if (r) errored = 1;
}
// Did anything go wrong during parsing?
if (errored) {
delete_stream(stream);
return NULL;
}
// We're done
return stream;
}