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