#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, ®)) {
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;
}