Sona 0.50 Source

Sona 0.50/tools/mml2sona/mml_parse.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "main.h"
#include "mml.h"
#include "mml_parse.h"
#include "mml_parse_notes.h"
#include "mml_parse_loop.h"
#include "mml_parse_at.h"
#include "mml_channels.h"
#include "stream.h"
#include "text.h"

// Important values for measuring tempo
// MML uses a different scale than Sona
#define MML_UNIT_TEMPO     120   // MML tempo for 60Hz
#define SONA_UNIT_TEMPO    32    // SonaStream tempo for 60Hz
#define MAX_TEMPO          (0xFF * MML_UNIT_TEMPO / SONA_UNIT_TEMPO)

// Functions that handle each MML command
static int parse_default_length(MmlState *mml_state);
static int parse_volume(MmlState *mml_state, char cmd);
static int parse_pan(MmlState *mml_state);
static int parse_tempo(MmlState *mml_state);
static int parse_fm_write(MmlState *state);
static void parse_line_col(MmlState *mml_state);

//***************************************************************************
// parse_mml_commands
// Parses the MML commands for a given channel and adds them to a stream.
//---------------------------------------------------------------------------
// param stream: pointer to stream
// param channel: channel ID
// param filename: MML file name (for error reporting)
//---------------------------------------------------------------------------
// return: 0 on success, -1 on failure (syntax error, etc.)
//***************************************************************************

int parse_mml_commands(Stream *stream, Channel channel, const char *filename)
{
   MmlState mml_state = {
      .stream = stream,
      .channel = channel,
      .filename = filename,
      
      .instrument = 0,
      .length = 128,
      .octave = 0,
      .transpose = 0,
      .volume = 15,
      
      .slide = 0,
      .tie = 0,
   };
   
   // Reset the stream's timestamp
   // This is important since multiple tracks may work on the same stream
   // and all of them will cause the timestamp to be modified
   stream->timestamp = 0;
   
   // Scan entire track
   size_t len;
   get_channel_text(channel, &mml_state.ptr, &len);
   mml_state.start = mml_state.ptr;
   mml_state.endptr = mml_state.ptr + len;
   
   int errored = parse_mml_subloop(&mml_state);
   
   // Update the end of stream timestamp if we went past it
   // This is used to keep track of where the SONAEVENT_END command should go
   if (stream->timestamp > stream->end_timestamp)
      stream->end_timestamp = stream->timestamp;
   
   return errored ? -1 : 0;
}

//***************************************************************************
// parse_mml_subloop
// The recursive part of parse_mml_commands(), which needs to be split up
// into its own function because repeat loops work by parsing the wrapped
// commands multiple times (for now).
//---------------------------------------------------------------------------
// param mml_state: pointer to MML state
//---------------------------------------------------------------------------
// return: 0 on success, -1 on failure (syntax error, etc.)
//***************************************************************************

int parse_mml_subloop(MmlState *mml_state)
{
   int errored = 0;
   
   for (;;) {
      // Ignore spaces
      mml_state->ptr = skip_spaces(mml_state->ptr);
      
      // Are we done?
      if (mml_state->ptr == mml_state->endptr) break;
      
      // Determine column number where this command is located
      mml_state->col_num = mml_state->basecol_num +
                           (unsigned)(mml_state->ptr - mml_state->start);
      
      char cmd = *mml_state->ptr++;
      switch (cmd) {
         // Note?
         case 'c': case 'd': case 'e': case 'f':
         case 'g': case 'a': case 'b':
            if (parse_note(mml_state, cmd)) errored = 1;
            break;
         case 'n':
            if (parse_raw_note(mml_state)) errored = 1;
            break;
         
         // Rest?
         case 'r': case 's':
            if (parse_rest(mml_state, cmd)) errored = 1;
            break;
         
         // Slide or tie?
         case '_':
            mml_state->slide = 1;
            break;
         case '&':
            mml_state->tie = 1;
            break;
         
         // New octave?
         case 'o': case '<': case '>':
            if (parse_octave(mml_state, cmd)) errored = 1;
            break;
         
         // Change transpose?
         case 'k': case 'K':
            if (parse_transpose(mml_state, cmd)) errored = 1;
            break;
         
         // New default length?
         case 'l':
            if (parse_default_length(mml_state)) errored = 1;
            break;
         
         // Change volume?
         case 'v': case '(': case ')':
            if (parse_volume(mml_state, cmd)) errored = 1;
            break;
         
         // Change panning?
         case 'p':
            if (parse_pan(mml_state)) errored = 1;
            break;
         
         // Change tempo?
         case 't':
            if (parse_tempo(mml_state)) errored = 1;
            break;
         
         // FM register write?
         case 'y':
            if (parse_fm_write(mml_state)) errored = 1;
            break;
         
         // Repeat a sub-loop?
         case '[':
            if (parse_repeat(mml_state)) errored = 1;
            break;
         
         case ']':
            errored = 1;
            fprintf(stderr, ERRORMSG_BADREPEATEND,
                    mml_state->filename,
                    mml_state->line_num,
                    mml_state->col_num);
            break;
         
         // Set loop point?
         case 'L':
            if (parse_loop_point(mml_state)) errored = 1;
            break;
         
         // Extended commands?
         case '@':
            if (parse_at_command(mml_state)) errored = 1;
            break;
         
         // Bar separator? (does nothing)
         case '|':
            break;
         
         // Line and column number marker?
         case LINECOL_MARKER:
            parse_line_col(mml_state);
            break;
         
         // Invalid or otherwise unsupported command
         default:
            fprintf(stderr, ERRORMSG_BADCOMMAND,
                            mml_state->filename,
                            mml_state->line_num,
                            mml_state->col_num,
                            cmd);
            errored = 1;
            break;
      }
   }
   
   return errored ? -1 : 0;
}

//***************************************************************************
// parse_default_length [internal]
// Parses a default length command in a MML track ('l' command).
//---------------------------------------------------------------------------
// param mml_state: pointer to MML state
//---------------------------------------------------------------------------
// return: 0 on success, -1 on syntax error
//***************************************************************************

static int parse_default_length(MmlState *mml_state)
{
   unsigned length;
   int len_result = get_length(mml_state, &length);
   
   if (len_result > 0) {
      fprintf(stderr, ERRORMSG_NODEFLENGTH,
              mml_state->filename,
              mml_state->line_num,
              mml_state->col_num);
      return -1;
   }
   if (len_result < 0) {
      return -1;
   }
   
   mml_state->length = length;
   return 0;
}

//***************************************************************************
// parse_volume [internal]
// Parses any of the volume commands in a MML track ('v', '(' or ')').
//---------------------------------------------------------------------------
// param mml_state: pointer to MML state
// param cmd: which command it is ('v', '(', ')')
//---------------------------------------------------------------------------
// return: 0 on success, -1 on syntax error
//***************************************************************************

static int parse_volume(MmlState *mml_state, char cmd)
{
   // Look for a numeric argument
   int number;
   int num_result = get_number(&mml_state->ptr, &number);
   
   if (num_result) {
      // The v command *requires* a volume argument
      if (cmd == 'v') {
         fprintf(stderr, ERRORMSG_NOVOLUME,
                 mml_state->filename,
                 mml_state->line_num,
                 mml_state->col_num);
         return -1;
      }
      
      // The ( and ) commands don't require an argument
      // If no argument is provided, assume ±1 volume step
      else {
         number = 1;
      }
   }
   
   // Update the track's current volume
   // TO-DO: check if Sona will make this redundant?
   switch (cmd) {
      case 'v': mml_state->volume = number; break;
      case '(': mml_state->volume -= number; break;
      case ')': mml_state->volume += number; break;
      default: abort(); break;
   }
   
   // Add volume event
   Stream *stream = mml_state->stream;
   Channel chan = mml_state->channel;
   
   Event *event = create_event(stream);
   event->type = EVENT_VOLUME;
   event->channel = chan;
   event->arg[0] = mml_state->volume;
   
   return 0;
}

//***************************************************************************
// parse_pan [internal]
// Parses a panning command in a MML track ('p' command).
//---------------------------------------------------------------------------
// param mml_state: pointer to MML state
//---------------------------------------------------------------------------
// return: 0 on success, -1 on syntax error
//***************************************************************************

static int parse_pan(MmlState *mml_state)
{
   // Read pan value
   int pan;
   if (get_number(&mml_state->ptr, &pan)) {
      fprintf(stderr, ERRORMSG_NOPAN,
              mml_state->filename,
              mml_state->line_num,
              mml_state->col_num);
      return -1;
   }
   
   // Check if value is valid
   if (pan < 0 || pan > 3) {
      fprintf(stderr, ERRORMSG_BADPAN,
              mml_state->filename,
              mml_state->line_num,
              mml_state->col_num,
              pan);
      return -1;
   }
   
   // Add panning event
   Stream *stream = mml_state->stream;
   Channel chan = mml_state->channel;
   
   Event *event = create_event(stream);
   event->type = EVENT_PAN;
   event->channel = chan;
   event->arg[0] = pan;
   
   return 0;
}

//***************************************************************************
// parse_tempo [internal]
// Parses a tempo command in a MML track ('t' command).
//---------------------------------------------------------------------------
// param mml_state: pointer to MML state
//---------------------------------------------------------------------------
// return: 0 on success, -1 on syntax error
//***************************************************************************

static int parse_tempo(MmlState *mml_state)
{
   // Read tempo value
   int tempo;
   if (get_number(&mml_state->ptr, &tempo)) {
      fprintf(stderr, ERRORMSG_NOTEMPO,
              mml_state->filename,
              mml_state->line_num,
              mml_state->col_num);
      return -1;
   }
   
   // Check against the limits
   if (tempo < 0) {
      fprintf(stderr, ERRORMSG_LOWTEMPO,
              mml_state->filename,
              mml_state->line_num,
              mml_state->col_num,
              tempo);
      return -1;
   }
   if (tempo > MAX_TEMPO) {
      fprintf(stderr, ERRORMSG_HIGHTEMPO,
              mml_state->filename,
              mml_state->line_num,
              mml_state->col_num,
              tempo, MAX_TEMPO);
      return -1;
   }
   
   // Convert tempo from MML scale to SonaStream scale
   // The addition is for rounding to nearest
   tempo *= SONA_UNIT_TEMPO;
   tempo += MML_UNIT_TEMPO/2;
   tempo /= MML_UNIT_TEMPO;
   
   // Add tempo event
   Stream *stream = mml_state->stream;
   Event *event = create_event(stream);
   
   event->type = EVENT_TEMPO;
   event->arg[0] = tempo;
   
   return 0;
}

//***************************************************************************
// parse_fm_write [internal]
// Parses direct FM register writes (y command).
//---------------------------------------------------------------------------
// param mml_state: pointer to MML track's state
//---------------------------------------------------------------------------
// return: 0 on success, -1 on syntax error
//***************************************************************************

static int parse_fm_write(MmlState *mml_state)
{
   int reg = 0, value;
   
   // In the case of FM tracks, we can refer to the relevant register for
   // the current channel by its name instead, so check for those first
   if (is_fm(mml_state->channel)) {
      // Determine the register number offset for this channel
      // FM1~FM3 have an offset of +0 to +2
      // FM4~FM6 have an offset of +256 to +258
      unsigned offset = mml_state->channel - CHAN_FM1;
      if (offset >= 3) offset = offset % 3 + 0x100;
      
      // Retrieve the next two characters
      // Doing this way in case the next character is nul (in which case
      // ch2 would be an out-of-bounds access), which isn't valid but we
      // don't want to crash
      char ch1, ch2 = 0;
      ch1 = mml_state->ptr[0];
      if (ch1) ch2 = mml_state->ptr[1];
      
      // Many registers are repeated across multiple operators
      // This will get set if an operator number is needed
      int needs_oper = 0;
      
      // Check if they match any known name
      if (ch1 == 'D' && ch2 == 'M') {           // Detune and multiply
         reg = 0x30 + offset;
         needs_oper = 1;
      } else if (ch1 == 'T' && ch2 == 'L') {    // Total level
         reg = 0x40 + offset;
         needs_oper = 1;
      } else if (ch1 == 'K' && ch2 == 'A') {    // Attack rate and key scale
         reg = 0x50 + offset;
         needs_oper = 1;
      } else if (ch1 == 'D' && ch2 == 'R') {    // Decay rate
         reg = 0x60 + offset;
         needs_oper = 1;
      } else if (ch1 == 'S' && ch2 == 'R') {    // Sustain rate
         reg = 0x70 + offset;
         needs_oper = 1;
      } else if (ch1 == 'S' && ch2 == 'L') {    // Release rate and sustain level
         reg = 0x80 + offset;
         needs_oper = 1;
      } else if (ch1 == 'S' && ch2 == 'E') {    // SSG-EG
         reg = 0x90 + offset;
         needs_oper = 1;
      } else if (ch1 == 'F' && ch2 == 'B') {    // Algorithm and feedback
         reg = 0xB0 + offset;
         needs_oper = 0;
      }
      
      // Did we find a register by its name?
      if (reg != 0) mml_state->ptr += 2;
      
      // Is an operator number needed?
      if (needs_oper) {
         int oper_num;
         if (get_number(&mml_state->ptr, &oper_num)) {
            fprintf(stderr, ERRORMSG_NOFMOPER,
                    mml_state->filename,
                    mml_state->line_num,
                    mml_state->col_num);
            return -1;
         }
         if (oper_num < 0 || oper_num > 3) {
            fprintf(stderr, ERRORMSG_BADFMOPER,
                    mml_state->filename,
                    mml_state->line_num,
                    mml_state->col_num,
                    oper_num);
            return -1;
         }
         
         mml_state->ptr = skip_spaces(mml_state->ptr);
         if (*mml_state->ptr != ',') {
            fprintf(stderr, ERRORMSG_NOFMVAL,
                    mml_state->filename,
                    mml_state->line_num,
                    mml_state->col_num);
            return -1;
         }
         mml_state->ptr = skip_spaces(mml_state->ptr+1);
         
         // Determine actual register number for this operator
         // Thanks Yamaha for swapping S2 and S3 in the register numbers!
         switch (oper_num) {
            case 0: break;                // S1
            case 1: reg += 8; break;      // S2
            case 2: reg += 4; break;      // S3
            case 3: reg += 12; break;     // S4
         }
      }
   }
   
   // Retrieve register number and value
   if (reg == 0)
   if (get_number(&mml_state->ptr, &reg)) {
      fprintf(stderr, ERRORMSG_NOFMREG,
              mml_state->filename,
              mml_state->line_num,
              mml_state->col_num);
      return -1;
   }
   
   mml_state->ptr = skip_spaces(mml_state->ptr);
   if (*mml_state->ptr != ',') {
      fprintf(stderr, ERRORMSG_NOFMVAL,
              mml_state->filename,
              mml_state->line_num,
              mml_state->col_num);
      return -1;
   }
   
   mml_state->ptr = skip_spaces(mml_state->ptr+1);
   if (get_number(&mml_state->ptr, &value)) {
      fprintf(stderr, ERRORMSG_NOFMVAL,
              mml_state->filename,
              mml_state->line_num,
              mml_state->col_num);
      return -1;
   }
   
   // Check that they're valid
   if (reg < 0 || reg > 0x1FF) {
      fprintf(stderr, ERRORMSG_BADFMREG,
              mml_state->filename,
              mml_state->line_num,
              mml_state->col_num,
              reg);
      return -1;
   }
   if (value < 0 || value > 0xFF) {
      fprintf(stderr, ERRORMSG_BADFMREG,
              mml_state->filename,
              mml_state->line_num,
              mml_state->col_num,
              value);
      return -1;
   }
   
   // Add FM register write event
   Stream *stream = mml_state->stream;
   Event *event = create_event(stream);
   
   event->type = EVENT_FMREG;
   event->arg[0] = reg;
   event->arg[1] = value;
   
   return 0;
}

//***************************************************************************
// parse_line_col [internal]
// MML parser, it updates the current line and column number from an internal
// marker (track data loses the line divisions so we insert markers to keep
// track of them).
//---------------------------------------------------------------------------
// param mml_state: pointer to MML state
//---------------------------------------------------------------------------
// Format of marker data:
//
// - ASCII pipe ('|') (already read by parse_mml_commands())
// - LINECOL_MARKER (already read by parse_mml_commands())
// - line number (written in ASCII)
// - ASCII comma (',')
// - column number (written in ASCII)
// - ASCII space (' ')
//
// It's wasteful but embedding raw binary integers just keep bringing up
// trouble so I decided to be done with it and turn it into text like any
// other MML command. The pipe is needed because the strtol() call in
// get_number() sees it as whitespace, we need a proper fix for this later.
//***************************************************************************

static void parse_line_col(MmlState *mml_state)
{
   // Read new line and column numbers
   if (get_number(&mml_state->ptr, &mml_state->line_num)) abort();
   if (*mml_state->ptr++ != ',') abort();
   if (get_number(&mml_state->ptr, &mml_state->col_num)) abort();
   if (*mml_state->ptr++ != ' ') abort();
   
   // Update pointer to beginning of line
   mml_state->start = mml_state->ptr;
}

//***************************************************************************
// get_length [internal]
// Reads a length argument for a MML command.
//---------------------------------------------------------------------------
// param result: pointer to where to store length in ticks
// param mml_state: pointer to MML track's state
//---------------------------------------------------------------------------
// return: 0 on success, 1 if no length, -1 if syntax error
//---------------------------------------------------------------------------
// The pointer to the text is updated to point past the length if there's a
// valid one, and the length is stored in ticks (without accounting for
// tempo). On error, the pointer is left intact and a length of 0 is stored.
//
// If there's no length at all it returns 1 (since for some commands the
// length is optional). It'll only return -1 if there's an actual syntax
// error or invalid length value (i.e. guaranteed error).
//***************************************************************************

int get_length(MmlState *mml_state, unsigned *result)
{
   const char *ptr = mml_state->ptr;
   unsigned total = 0;
   
   for (;;) {
      // If first character is %, then length is measured in ticks
      if (*ptr == '%') {
         int number;
         ptr++;
         
         if (get_number(&ptr, &number)) {
            fprintf(stderr, ERRORMSG_NOLENCLOCK,
                    mml_state->filename,
                    mml_state->line_num,
                    mml_state->col_num);
            goto error;
         }
         if (number <= 0) {
            fprintf(stderr, ERRORMSG_BADLENCLOCK,
                    mml_state->filename,
                    mml_state->line_num,
                    mml_state->col_num);
            goto error;
         }
         
         total += number;
      }
      
      // Otherwise it's measured in fractions
      // Only power of two fractions down to 1/128 are supported
      else {
         int number;
         
         if (get_number(&ptr, &number)) {
            if (total == 0) goto error;
            fprintf(stderr, ERRORMSG_BADLENTIE,
                    mml_state->filename,
                    mml_state->line_num,
                    mml_state->col_num);
            goto error;
         }
         if (number < 1 || (number & (number - 1)) != 0) {
            fprintf(stderr, ERRORMSG_BADLENFRAC,
                    mml_state->filename,
                    mml_state->line_num,
                    mml_state->col_num,
                    number);
            goto error;
         }
         
         // Convert length to ticks
         unsigned ticks = 128 / number;
         
         // Dotted length? Extend by half
         // Note that 1/128th can't be dotted
         if (*ptr == '.') {
            if (number == 128) {
               fprintf(stderr, ERRORMSG_BADLENDOT,
                       mml_state->filename,
                       mml_state->line_num,
                       mml_state->col_num);
               goto error;
            }
            
            ptr++;
            ticks += ticks / 2;
         }
         
         // Accumulate the result
         total += ticks;
      }
      
      // The ^ character can be used to tie together multiple lengths
      if (*ptr == '^') {
         ptr++;
      } else {
         break;
      }
   }
   
   mml_state->ptr = ptr;
   *result = total;
   return 0;
   
error:
   *result = 0;
   return total ? -1 : 1;
}