Sona 0.50 Source

Sona 0.50/tools/mml2sona/mml.c

#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;
}