Sona 0.50 Source

Sona 0.50/tools/mml2sona/sona.c

#include <stdio.h>
#include <stdlib.h>
#include "main.h"
#include "stream.h"
#include "sona.h"
#include "blob.h"

static void insert_load(Blob *blob, const Event *event);
static void insert_key_on(Blob *blob, const Event *event);
static void insert_key_off(Blob *blob, const Event *event);
static void insert_set_volume(Blob *blob, const Event *event);
static void insert_set_pan(Blob *blob, const Event *event);
static void insert_set_tempo(Blob *blob, const Event *event);
static void insert_fm_write(Blob *blob, const Event *event);
static void insert_loop_point(Blob *blob);
static void insert_lock(Blob *blob, const Event *event);
static void insert_2op_alu_event(Blob *blob, const Event *event);
static void insert_raw_byte(Blob *blob, uint8_t byte);
static void insert_delay(Blob *blob, uint64_t delay);
static unsigned channel_to_sona_chanid(Channel channel);
static uint8_t pitch_to_sona_pitch(unsigned pitch);

//***************************************************************************
// save_sona
// Takes the intermediate stream and generates the final SonaStream data out
// of it, then writes it into the output file.
//---------------------------------------------------------------------------
// param filename: output file name
// param stream:   pointer to intermediate stream
//---------------------------------------------------------------------------
// return: 0 on success, -1 on failure
//---------------------------------------------------------------------------
// notes: assumes the stream events have been sorted by timestamp
//***************************************************************************

int save_sona(const char *filename, const Stream *stream)
{
   Blob *blob = create_blob();
   
   // Used to keep track of the current timestamp as we scan the stream
   // This is used to know how much delay to add between events
   uint64_t last_timestamp = 0;
   
   // Parse entire stream
   for (size_t i = 0; i < stream->num_events; i++) {
      const Event *event = &stream->events[i];
      
      // If there's a delay between this event and the last one, insert it
      // now (if not then the "delay" will be 0 which will insert nothing)
      insert_delay(blob, event->timestamp - last_timestamp);
      last_timestamp = event->timestamp;
      
      switch (event->type) {
         case EVENT_NONE: break;
         case EVENT_LOAD: insert_load(blob, event); break;
         case EVENT_KEYON: insert_key_on(blob, event); break;
         case EVENT_KEYOFF: insert_key_off(blob, event); break;
         case EVENT_PITCH: insert_key_on(blob, event); break;
         case EVENT_VOLUME: insert_set_volume(blob, event); break;
         case EVENT_PAN: insert_set_pan(blob, event); break;
         case EVENT_TEMPO: insert_set_tempo(blob, event); break;
         case EVENT_FMREG: insert_fm_write(blob, event); break;
         case EVENT_LOOP: insert_loop_point(blob); break;
         case EVENT_LOCK: insert_lock(blob, event); break;
         case EVENT_VMMOVE: insert_2op_alu_event(blob, event); break;
         case EVENT_VMADD: insert_2op_alu_event(blob, event); break;
         case EVENT_VMSUB: insert_2op_alu_event(blob, event); break;
         case EVENT_VMAND: insert_2op_alu_event(blob, event); break;
         case EVENT_VMOR: insert_2op_alu_event(blob, event); break;
         case EVENT_VMXOR: insert_2op_alu_event(blob, event); break;
         case EVENT_RAW: insert_raw_byte(blob, event->arg[0]); break;
         default: abort(); break;
      }
   }
   
   // Append end of stream
   insert_delay(blob, stream->end_timestamp - last_timestamp);
   uint8_t end_of_stream[] = {
      stream->looped ? SONAEVENT_GOTOLOOP : SONAEVENT_END
   };
   append_to_blob(blob, end_of_stream, sizeof(end_of_stream));
   
   // Create SonaStream file
   FILE *file = fopen(filename, "wb");
   if (file == NULL) {
      fprintf(stderr, ERRORMSG_OPENSONA, filename);
      return -1;
   }
   
   if (fwrite(blob->data, 1, blob->len, file) < blob->len) {
      fprintf(stderr, ERRORMSG_WRITESONA, filename);
      fclose(file);
      return -1;
   }
   
   fclose(file);
   
   // We're done
   delete_blob(blob);
   return 0;
}

//***************************************************************************
// insert_load [internal]
// Takes care of generating SonaStream data for instrument load events.
//---------------------------------------------------------------------------
// param blob: pointer to SonaStream blob
// param event: pointer to event data
//***************************************************************************

static void insert_load(Blob *blob, const Event *event)
{
   uint8_t instrument = event->arg[0];
   uint8_t chan_id = channel_to_sona_chanid(event->channel);
   
   uint8_t data[] = {
      SONAEVENT_LOAD | chan_id,
      instrument
   };
   append_to_blob(blob, data, sizeof(data));
}

//***************************************************************************
// insert_key_on [internal]
// Takes care of generating SonaStream data for key-on and set pitch events.
//---------------------------------------------------------------------------
// param blob: pointer to SonaStream blob
// param event: pointer to event data
//***************************************************************************

static void insert_key_on(Blob *blob, const Event *event)
{
   // This function handles both key-on and set pitch (since they're
   // essentially the same aside from the command number issued), so
   // determine which of the two commands it is
   uint8_t cmd = (event->type == EVENT_PITCH) ?
                 SONAEVENT_PITCH : SONAEVENT_KEYON;
   
   Channel channel = event->channel;
   switch (channel) {
      // FM channels
      case CHAN_FM1: case CHAN_FM2: case CHAN_FM3:
      case CHAN_FM4: case CHAN_FM5: case CHAN_FM6: {
         // Read pitch and clamp against the limits
         // FM channels implement o0 to o7
         int pitch = event->arg[0];
         if (pitch < 0) pitch = 0;
         if (pitch > 95) pitch = 95;
         
         // Convert channel ID to SonaStream format
         uint8_t chan_id = channel_to_sona_chanid(event->channel);
         
         // Generate SonaStream event
         uint8_t data[] = {
            cmd | chan_id,
            pitch_to_sona_pitch(pitch)
         };
         append_to_blob(blob, data, sizeof(data));
      } break;
      
      // Square channels
      case CHAN_PSG1: case CHAN_PSG2: case CHAN_PSG3: {
         // Read pitch and clamp against the limits
         // Square channels implement o2 to o7
         int pitch = event->arg[0];
         if (pitch < 24) pitch = 24;
         if (pitch > 95) pitch = 95;
         pitch -= 24;
         
         // Convert channel ID to SonaStream format
         uint8_t chan_id = channel_to_sona_chanid(event->channel);
         
         // Generate SonaStream event
         uint8_t data[] = {
            cmd | chan_id,
            pitch_to_sona_pitch(pitch)
         };
         append_to_blob(blob, data, sizeof(data));
      } break;
      
      // Noise channel
      case CHAN_PSG4: {
         // Use the event value as-is as the noise tone type
         // 0 to 3 are periodic noise, 4 to 7 are white noise
         uint8_t tone = event->arg[0];
         
         // Generate SonaStream event
         uint8_t data[] = {
            cmd | SONACHAN_PSG4, tone
         };
         append_to_blob(blob, data, sizeof(data));
      } break;
      
      // PCM channels
      case CHAN_PCM1: case CHAN_PCM2: {
         // Use the event value as-is as the instrument ID
         uint8_t instrument = event->arg[0];
         
         // Convert channel ID to SonaStream format
         uint8_t chan_id = channel_to_sona_chanid(event->channel);
         
         // Generate SonaStream event
         uint8_t data[] = {
            SONAEVENT_KEYON | chan_id, instrument
         };
         append_to_blob(blob, data, sizeof(data));
      } break;
      
      default: {
         // Oops!
         fprintf(stderr, "[" PROGRAM_NAME ":insert_key_on] "
                 "channel %u not handled!\n", (unsigned)(channel));
      } break;
   }
}

//***************************************************************************
// insert_key_off [internal]
// Takes care of generating SonaStream data for key-off events.
//---------------------------------------------------------------------------
// param blob: pointer to SonaStream blob
// param event: pointer to event data
//***************************************************************************

static void insert_key_off(Blob *blob, const Event *event)
{
   uint8_t chan_id = channel_to_sona_chanid(event->channel);
   uint8_t data[] = { SONAEVENT_KEYOFF | chan_id };
   append_to_blob(blob, data, sizeof(data));
}

//***************************************************************************
// insert_set_volume [internal]
// Takes care of generating SonaStream data for set volume events.
//---------------------------------------------------------------------------
// param blob: pointer to SonaStream blob
// param event: pointer to event data
//***************************************************************************

static void insert_set_volume(Blob *blob, const Event *event)
{
   // Only FM and PSG channels support volume
   if (!is_fm(event->channel) && !is_psg(event->channel))
      return;
   
   // Get volume and convert it from MML scale to SonaStream scale
   unsigned volume = event->arg[0];
   if (volume == 0) {
      volume = 0x7F;
   } else {
      volume = 15 - volume;
      volume = volume * 8 / 3;
   }
   
   // Insert event
   uint8_t chan_id = channel_to_sona_chanid(event->channel);
   uint8_t data[] = { SONAEVENT_VOLUME | chan_id, volume };
   append_to_blob(blob, data, sizeof(data));
}

//***************************************************************************
// insert_set_pan [internal]
// Takes care of generating SonaStream data for set panning events.
//---------------------------------------------------------------------------
// param blob: pointer to SonaStream blob
// param event: pointer to event data
//***************************************************************************

static void insert_set_pan(Blob *blob, const Event *event)
{
   Channel chan = event->channel;
   
   // PCM plays on FM6
   if (is_pcm(chan))
      chan = CHAN_FM6;
   
   // Anything else that isn't FM can't be panned
   if (!is_fm(chan))
      return;
   
   // MML stores panning as 0 to 3, reflecting the pan bits values, but those
   // are stored in bits 7-6 of the register, so shift them in the correct
   // place
   uint8_t pan = event->arg[0] << 6;
   
   // Insert event
   uint8_t chan_id = channel_to_sona_chanid(chan);
   uint8_t data[] = { SONAEVENT_PAN | chan_id, pan };
   append_to_blob(blob, data, sizeof(data));
}

//***************************************************************************
// insert_tempo [internal]
// Takes care of generating SonaStream data for tempo events.
//---------------------------------------------------------------------------
// param blob: pointer to SonaStream blob
// param event: pointer to event data
//***************************************************************************

static void insert_set_tempo(Blob *blob, const Event *event)
{
   uint8_t data[] = {
      SONAEVENT_TEMPO, event->arg[0]
   };
   append_to_blob(blob, data, sizeof(data));
}

//***************************************************************************
// insert_fm_write [internal]
// Takes care of generating SonaStream data for FM register write events.
//---------------------------------------------------------------------------
// param blob: pointer to SonaStream blob
// param event: pointer to event data
//***************************************************************************

static void insert_fm_write(Blob *blob, const Event *event)
{
   uint8_t bank = event->arg[0] >> 8;
   uint8_t reg = event->arg[0] & 0xFF;
   uint8_t value = event->arg[1];
   
   uint8_t data[] = {
      SONAEVENT_FMREG | bank, reg, value
   };
   append_to_blob(blob, data, sizeof(data));
}

//***************************************************************************
// insert_loop_point [internal]
// Takes care of generating SonaStream data for the set loop point event.
//---------------------------------------------------------------------------
// param blob: pointer to SonaStream blob
//***************************************************************************

static void insert_loop_point(Blob *blob)
{
   uint8_t data[] = { SONAEVENT_SETLOOP };
   append_to_blob(blob, data, sizeof(data));
}

//***************************************************************************
// insert_lock [internal]
// Takes care of generating SonaStream data for lock channel events.
//---------------------------------------------------------------------------
// param blob: pointer to SonaStream blob
// param event: pointer to event data
//***************************************************************************

static void insert_lock(Blob *blob, const Event *event)
{
   // Make sure that it's a real channel (not control)
   Channel chan = event->channel;
   if (!is_real_chan(chan)) return;
   
   // Generate event
   uint8_t chan_id = channel_to_sona_chanid(chan);
   uint8_t data[] = { SONAEVENT_LOCK | chan_id };
   append_to_blob(blob, data, sizeof(data));
}

//***************************************************************************
// insert_2op_alu_event [internal]
// Inserts a VM ALU event with two operands.
//---------------------------------------------------------------------------
// param blob: pointer to SonaStream blob
// param event: pointer to event data
//***************************************************************************

static void insert_2op_alu_event(Blob *blob, const Event *event)
{
   uint8_t opcode;
   switch (event->type) {
      case EVENT_VMMOVE: opcode = SONAEVENT_VMMOVE; break;
      case EVENT_VMADD: opcode = SONAEVENT_VMADD; break;
      case EVENT_VMSUB: opcode = SONAEVENT_VMSUB; break;
      case EVENT_VMAND: opcode = SONAEVENT_VMAND; break;
      case EVENT_VMOR: opcode = SONAEVENT_VMOR; break;
      case EVENT_VMXOR: opcode = SONAEVENT_VMXOR; break;
      default: abort(); break;
   }
   
   // These commands come in two variants: one where the source is another
   // variable and one where the source is a fixed value. They have different
   // opcodes, so pick the correct one.
   opcode += event->arg[2];
   
   // Generate event
   uint8_t dest = event->arg[0];
   uint8_t src = event->arg[1];
   uint8_t data[] = { opcode, dest, src };
   append_to_blob(blob, data, sizeof(data));
}

//***************************************************************************
// insert_raw_byte [internal]
// Inserts a single byte into a blob.
//---------------------------------------------------------------------------
// param blob: pointer to SonaStream blob
// param byte: value of byte to insert
//***************************************************************************

static void insert_raw_byte(Blob *blob, uint8_t byte)
{
   uint8_t data[] = { byte };
   append_to_blob(blob, data, sizeof(data));
}

//***************************************************************************
// insert_delay [internal]
// Inserts a delay into a SonaStream blob.
//---------------------------------------------------------------------------
// param blob:  pointer to blob
// param delay: number of ticks to delay
//***************************************************************************

static void insert_delay(Blob *blob, uint64_t delay)
{
   // Delay commands can only delay up to 256 ticks
   // Handle longer delays by splitting them up
   while (delay >= 0x100) {
      uint8_t data[] = { SONAEVENT_DELAY, 0x00 };
      append_to_blob(blob, data, sizeof(data));
      delay -= 0x100;
   }
   
   // Now add a command for the remainder of the delay
   if (delay == 0) return;
   
   if (delay <= 0x10) {
      uint8_t data[] = { SONAEVENT_QDELAY | (delay-1) };
      append_to_blob(blob, data, sizeof(data));
   } else {
      uint8_t data[] = { SONAEVENT_DELAY, delay };
      append_to_blob(blob, data, sizeof(data));
   }
}

//***************************************************************************
// channel_to_sona_chanid [internal]
// Converts internal channel IDs to SonaStream's channel IDs.
//---------------------------------------------------------------------------
// param channel: internal channel ID (see CHAN_*)
//---------------------------------------------------------------------------
// return: SonaStream channel ID (see SONACHAN_*)
//***************************************************************************

static unsigned channel_to_sona_chanid(Channel channel)
{
   switch (channel) {
      case CHAN_FM1: return SONACHAN_FM1; break;
      case CHAN_FM2: return SONACHAN_FM2; break;
      case CHAN_FM3: return SONACHAN_FM3; break;
      case CHAN_FM4: return SONACHAN_FM4; break;
      case CHAN_FM5: return SONACHAN_FM5; break;
      case CHAN_FM6: return SONACHAN_FM6; break;
      case CHAN_FMX: return SONACHAN_FMX; break;
      case CHAN_PSG1: return SONACHAN_PSG1; break;
      case CHAN_PSG2: return SONACHAN_PSG2; break;
      case CHAN_PSG3: return SONACHAN_PSG3; break;
      case CHAN_PSG4: return SONACHAN_PSG4; break;
      case CHAN_PSGX: return SONACHAN_PSGX; break;
      case CHAN_PCM1: return SONACHAN_PCM1; break;
      case CHAN_PCM2: return SONACHAN_PCM2; break;
      case CHAN_PCMX: return SONACHAN_PCMX; break;
      
      default:
         fprintf(stderr, "[" PROGRAM_NAME ":channel_to_sona_chanid] "
                 "not handled channel %u!!\n", (unsigned)(channel));
         return 0;
   }
}

//***************************************************************************
// pitch_to_sona_pitch [internal]
// Converts a pitch measured in semitones to the format used by SonaStream's
// events (see doc-format/sonastream.txt for details)
//---------------------------------------------------------------------------
// param pitch: pitch in semitones (0 to 71)
//---------------------------------------------------------------------------
// return: pitch in SonaStream's pitch format (0yyyyxxx)
//***************************************************************************

static uint8_t pitch_to_sona_pitch(unsigned pitch)
{
   unsigned semitone = pitch % 12;
   unsigned octave = pitch / 12;
   return (semitone << 3) | octave;
}