430 lines
16 KiB
C++
430 lines
16 KiB
C++
|
#include <stdint.h>
|
||
|
#include "opmplay.h"
|
||
|
#include "opmfile.h"
|
||
|
|
||
|
// -------------------------
|
||
|
// file I/O procedures
|
||
|
static uint32_t opmplay_mem_read(opmplay_io_t* io, void* dst, uint32_t size) {
|
||
|
if ((io == NULL) || dst == NULL) return OPMPLAY_ERR_NULLPTR;
|
||
|
if ((size + io->offset) > io->size) return OPMPLAY_ERR_IO;
|
||
|
opmplay_memcpy(dst, (uint8_t*)io->buf + io->offset, io->size);
|
||
|
io->offset += io->size;
|
||
|
return size;
|
||
|
}
|
||
|
static uint32_t opmplay_mem_seek(opmplay_io_t* io, uint32_t pos) {
|
||
|
if (io == NULL) return OPMPLAY_ERR_NULLPTR;
|
||
|
if (pos > io->size) return OPMPLAY_ERR_IO;
|
||
|
io->offset = pos;
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
#ifdef OPMPLAY_ENABLE_STDIO
|
||
|
static uint32_t opmplay_file_read(opmplay_io_t* io, void* dst, uint32_t size) {
|
||
|
return fread(dst, 1, size, io->io);
|
||
|
}
|
||
|
static uint32_t opmplay_file_seek(opmplay_io_t* io, uint32_t pos) {
|
||
|
return fseek(io->io, pos, SEEK_SET);
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
// -------------------------
|
||
|
|
||
|
int opmplay_init(opmplay_context_t* ctx, opl3_chip* chip) {
|
||
|
if ((ctx == NULL) || (chip == NULL)) return OPMPLAY_ERR_NULLPTR;
|
||
|
|
||
|
opmplay_memset(ctx, 0, sizeof(opmplay_context_t));
|
||
|
ctx->chip = chip;
|
||
|
|
||
|
return OPMPLAY_ERR_OK;
|
||
|
}
|
||
|
|
||
|
int opmplay_free(opmplay_context_t* ctx)
|
||
|
{
|
||
|
for (int ch = 0; ch < ctx->header.channels+1; ch++) {
|
||
|
if (ctx->channels[ch].stream.data != NULL) {
|
||
|
opmplay_memfree(ctx->channels[ch].stream.data); ctx->channels[ch].stream.data = NULL;
|
||
|
}
|
||
|
}
|
||
|
return OPMPLAY_ERR_OK;
|
||
|
}
|
||
|
|
||
|
// load file header
|
||
|
int opmplay_load_header(opmplay_context_t* ctx, opmplay_io_t* io) {
|
||
|
if ((ctx == NULL) || (io == NULL)) return OPMPLAY_ERR_NULLPTR;
|
||
|
|
||
|
// init file I/O handlers
|
||
|
switch (io->type) {
|
||
|
#ifdef OPMPLAY_ENABLE_STDIO
|
||
|
case OPMPLAY_IO_FILE:
|
||
|
io->read = &opmplay_file_read;
|
||
|
io->seek = &opmplay_file_seek;
|
||
|
break;
|
||
|
#endif
|
||
|
case OPMPLAY_IO_MEMORY:
|
||
|
io->read = &opmplay_mem_read;
|
||
|
io->seek = &opmplay_mem_seek;
|
||
|
break;
|
||
|
case OPMPLAY_IO_USER:
|
||
|
// use user-provided I/O functions
|
||
|
if ((io->read == NULL) || (io->seek == NULL)) return OPMPLAY_ERR_BAD_PARAMETER;
|
||
|
break;
|
||
|
default:
|
||
|
return OPMPLAY_ERR_BAD_PARAMETER;
|
||
|
}
|
||
|
|
||
|
// read header
|
||
|
if (io->read(io, &ctx->header, sizeof(ctx->header)) != sizeof(ctx->header)) return OPMPLAY_ERR_IO;
|
||
|
|
||
|
// and validate it
|
||
|
if ((opmplay_memcmp(ctx->header.magic, "OPM\x1A", sizeof(ctx->header.magic))) || (ctx->header.version.v != OPM_FORMAT_VERSION))
|
||
|
return OPMPLAY_ERR_BAD_FILE_STRUCTURE;
|
||
|
|
||
|
// done for now, waiting for opmplay_load_module :)
|
||
|
return OPMPLAY_ERR_OK;
|
||
|
}
|
||
|
|
||
|
int opmplay_load_module(opmplay_context_t* ctx, opmplay_io_t* io) {
|
||
|
if ((ctx == NULL) || (io == NULL)) return OPMPLAY_ERR_NULLPTR;
|
||
|
uint32_t filepos = sizeof(opm_header_t);
|
||
|
|
||
|
// allocate and copy stream data
|
||
|
opm_header_stream_desc_t* streamdesc = (opm_header_stream_desc_t*)opmplay_alloc(sizeof(opm_header_stream_desc_t) * (ctx->header.channels + 1));
|
||
|
if (streamdesc == NULL) return OPMPLAY_ERR_NULLPTR;
|
||
|
if (io->seek(io, filepos)) return OPMPLAY_ERR_IO;
|
||
|
if (io->read(io, streamdesc, sizeof(opm_header_stream_desc_t) * (ctx->header.channels + 1))
|
||
|
!= sizeof(opm_header_stream_desc_t) * (ctx->header.channels + 1))
|
||
|
return OPMPLAY_ERR_IO;
|
||
|
|
||
|
// allocate and copy channel streams
|
||
|
for (int ch = 0; ch < ctx->header.channels + 1; ch++) {
|
||
|
if (streamdesc[ch].size > 0) {
|
||
|
ctx->channels[ch].stream.data = (uint8_t*)opmplay_alloc(sizeof(uint8_t*) * (streamdesc[ch].size));
|
||
|
if (ctx->channels[ch].stream.data == NULL) return OPMPLAY_ERR_MEMALLOC;
|
||
|
if (io->read(io, ctx->channels[ch].stream.data, streamdesc[ch].size) != streamdesc[ch].size) return OPMPLAY_ERR_IO;
|
||
|
ctx->channels[ch].stream.delay = 1;
|
||
|
}
|
||
|
else ctx->channels[ch].stream.data = NULL;
|
||
|
}
|
||
|
|
||
|
// rewind to start
|
||
|
opmplay_rewind(ctx);
|
||
|
|
||
|
// done :)
|
||
|
return OPMPLAY_ERR_OK;
|
||
|
}
|
||
|
|
||
|
void opmplay_pop_stack(opmplay_channel_context_t* chctx) {
|
||
|
opmplay_channel_stack_t* st = chctx->stack + (--chctx->stack_pos);
|
||
|
chctx->stream.ptr = st->ptr;
|
||
|
chctx->stream.samples_to_play = st->frames_to_play;
|
||
|
}
|
||
|
|
||
|
void opmplay_push_stack(opmplay_channel_context_t* chctx) {
|
||
|
opmplay_channel_stack_t* st = chctx->stack + chctx->stack_pos;
|
||
|
st->ptr = chctx->stream.ptr;
|
||
|
st->frames_to_play = chctx->stream.samples_to_play;
|
||
|
chctx->stack_pos++;
|
||
|
}
|
||
|
|
||
|
int opmplay_loop(opmplay_context_t* ctx) {
|
||
|
// channel streams
|
||
|
for (int ch = 0; ch < ctx->header.channels + 1; ch++) {
|
||
|
// init stack
|
||
|
ctx->channels[ch].stack_pos = 0;
|
||
|
ctx->channels[ch].stream.samples_to_play = -1;
|
||
|
ctx->channels[ch].stream.ptr = ctx->channels[ch].stream.loop;
|
||
|
ctx->channels[ch].stream.delay = ctx->channels[ch].stream.reload = 1;
|
||
|
}
|
||
|
|
||
|
return OPMPLAY_ERR_OK;
|
||
|
}
|
||
|
|
||
|
int opmplay_rewind(opmplay_context_t* ctx) {
|
||
|
// set chip mode
|
||
|
switch (ctx->header.flags & OPM_FLAG_CHIP_TYPE) {
|
||
|
case OPM_FLAG_CHIP_OPL3:
|
||
|
// set OPL3 mode
|
||
|
OPL3_WriteRegBuffered(ctx->chip, 0x105, 0x01);
|
||
|
// disable 4-op
|
||
|
OPL3_WriteRegBuffered(ctx->chip, 0x104, 0x00);
|
||
|
// send key off for every channel
|
||
|
for (int ch = 0; ch < 9; ch++) {
|
||
|
OPL3_WriteRegBuffered(ctx->chip, 0x0B0 + ch, 0x00);
|
||
|
OPL3_WriteRegBuffered(ctx->chip, 0x1B0 + ch, 0x00);
|
||
|
}
|
||
|
break;
|
||
|
case OPM_FLAG_CHIP_OPL2:
|
||
|
// enable waveforms
|
||
|
OPL3_WriteRegBuffered(ctx->chip, 0x001, 0x20);
|
||
|
// send key off for every channel
|
||
|
for (int ch = 0; ch < 9; ch++) {
|
||
|
OPL3_WriteRegBuffered(ctx->chip, 0x0B0 + ch, 0x00);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
default: return OPMPLAY_ERR_DEVICE;
|
||
|
}
|
||
|
|
||
|
for (int ch = 0; ch < ctx->header.channels + 1; ch++) {
|
||
|
ctx->channels[ch].stream.loop = ctx->channels[ch].stream.data;
|
||
|
}
|
||
|
ctx->_4op = 0;
|
||
|
|
||
|
opmplay_loop(ctx);
|
||
|
|
||
|
return OPMPLAY_ERR_OK;
|
||
|
}
|
||
|
|
||
|
// get and parse delay
|
||
|
static uint32_t opmplay_set_delay(uint8_t** data) {
|
||
|
uint32_t delay = 0;
|
||
|
if (**data == OPM_STREAM_DELAY_INT32) {
|
||
|
delay = (
|
||
|
(*(*data + 1) << 0) |
|
||
|
(*(*data + 2) << 8) |
|
||
|
(*(*data + 3) << 16) |
|
||
|
(*(*data + 4) << 24)
|
||
|
);
|
||
|
*data += 5;
|
||
|
}
|
||
|
else if (**data == OPM_STREAM_DELAY_INT16) {
|
||
|
delay = (
|
||
|
(*(*data + 1) << 0) |
|
||
|
(*(*data + 2) << 8)
|
||
|
);
|
||
|
*data += 3;
|
||
|
}
|
||
|
else if ((**data & 0xF0) == OPM_STREAM_DELAY_INT12) {
|
||
|
delay = ((**data & 0x0F) << 8) | (*(*data + 1));
|
||
|
*data += 2;
|
||
|
}
|
||
|
else if ((**data & 0xF0) == OPM_STERAM_DELAY_SHORT) {
|
||
|
delay = (**data & 0xF) + 1;
|
||
|
(*data)++;
|
||
|
}
|
||
|
return delay;
|
||
|
}
|
||
|
|
||
|
// translation tables
|
||
|
static int opmplay_opregs_channel_offset[] = {
|
||
|
0x000, 0x001, 0x002, 0x008, 0x009, 0x00A, 0x010, 0x011, 0x012,
|
||
|
0x100, 0x101, 0x102, 0x108, 0x109, 0x10A, 0x110, 0x111, 0x112,
|
||
|
};
|
||
|
static int opmplay_regs_channel_offset[] = {
|
||
|
0x000, 0x001, 0x002, 0x003, 0x004, 0x005, 0x006, 0x007, 0x008,
|
||
|
0x100, 0x101, 0x102, 0x103, 0x104, 0x105, 0x106, 0x107, 0x108,
|
||
|
};
|
||
|
static int opmplay_4op_bit_offset[] = {
|
||
|
1<<0, 1<<1, 1<<2, 0, 0, 0, 0, 0, 0,
|
||
|
1<<3, 1<<4, 1<<5, 0, 0, 0, 0, 0, 0,
|
||
|
};
|
||
|
static int opmplay_ch_parsing_order[] = {
|
||
|
3, 0, 4, 1, 5, 2, 6, 7, 8,
|
||
|
12, 9, 13, 10, 14, 11, 15, 16, 17
|
||
|
};
|
||
|
|
||
|
int opmplay_tick(opmplay_context_t* ctx) {
|
||
|
|
||
|
int ch = 0;
|
||
|
int rtn = OPMPLAY_ERR_OK;
|
||
|
uint32_t newdelay = 0;
|
||
|
|
||
|
// parse channel streams
|
||
|
for (int i = 0; i < ctx->header.channels; i++) {
|
||
|
int ch = opmplay_ch_parsing_order[i]; // mangle channel order to make 4op mode work
|
||
|
|
||
|
opmplay_channel_context_t* chctx = ctx->channels + ch + 1;
|
||
|
uint8_t* data = chctx->stream.ptr; if (data == NULL) continue;
|
||
|
bool isRun = true;
|
||
|
if (--chctx->stream.delay == 0) {
|
||
|
while (isRun) {
|
||
|
// get streams
|
||
|
if ((*(data) & 0x80) == OPM_SET_OPERATOR) {
|
||
|
int mask = *data;
|
||
|
int op = (mask & OPM_CMD00_SELECT_OPERATOR ? 3 : 0) + opmplay_opregs_channel_offset[ch];
|
||
|
data++;
|
||
|
if (mask & OPM_CMD00_SET_MULT) OPL3_WriteRegBuffered(ctx->chip, 0x20 + op, *data++);
|
||
|
if (mask & OPM_CMD00_SET_TL) OPL3_WriteRegBuffered(ctx->chip, 0x40 + op, *data++);
|
||
|
if (mask & OPM_CMD00_SET_AD) OPL3_WriteRegBuffered(ctx->chip, 0x60 + op, *data++);
|
||
|
if (mask & OPM_CMD00_SET_SR) OPL3_WriteRegBuffered(ctx->chip, 0x80 + op, *data++);
|
||
|
if (mask & OPM_CMD00_SET_WAVEFORM) OPL3_WriteRegBuffered(ctx->chip, 0xE0 + op, *data++);
|
||
|
if (mask & OPM_CMD00_END_OF_FRAME) isRun = false;
|
||
|
continue;
|
||
|
}
|
||
|
if ((*(data) & 0xC0) == OPM_SET_FREQ_FB_VOL) {
|
||
|
int mask = *data;
|
||
|
data++;
|
||
|
if (mask & OPM_CMD80_SET_TL0) OPL3_WriteRegBuffered(ctx->chip, 0x40 + opmplay_opregs_channel_offset[ch], *data++);
|
||
|
if (mask & OPM_CMD80_SET_TL1) OPL3_WriteRegBuffered(ctx->chip, 0x43 + opmplay_opregs_channel_offset[ch], *data++);
|
||
|
if (mask & OPM_CMD80_SET_FEEDBACK) OPL3_WriteRegBuffered(ctx->chip, 0xC0 + opmplay_regs_channel_offset[ch], *data++);
|
||
|
if (mask & OPM_CMD80_SET_FREQ) OPL3_WriteRegBuffered(ctx->chip, 0xA0 + opmplay_regs_channel_offset[ch], *data++);
|
||
|
if (mask & OPM_CMD80_SET_KEYBLOCK) { chctx->block = *data; OPL3_WriteRegBuffered(ctx->chip, 0xB0 + opmplay_regs_channel_offset[ch], *data++); };
|
||
|
if (mask & OPM_CMD80_END_OF_FRAME) isRun = false;
|
||
|
continue;
|
||
|
}
|
||
|
if ((*data & 0xFC) == OPM_KEY_TRIGGER) {
|
||
|
if (*data & OPM_KEY_ON) chctx->block |= (1 << 5); else chctx->block &= ~(1 << 5);
|
||
|
OPL3_WriteRegBuffered(ctx->chip, 0xB0 + opmplay_regs_channel_offset[ch], chctx->block);
|
||
|
if (*data & OPM_KEY_END_OF_FRAME) isRun = false;
|
||
|
data++;
|
||
|
continue;
|
||
|
}
|
||
|
if ((*data & 0xFE) == OPM_4OP_TRIGGER) {
|
||
|
if (opmplay_4op_bit_offset[ch] != 0) {
|
||
|
if (*data & OPM_4OP_ON) ctx->_4op |= opmplay_4op_bit_offset[ch]; else ctx->_4op &= ~opmplay_4op_bit_offset[ch];
|
||
|
OPL3_WriteRegBuffered(ctx->chip, 0x104, ctx->_4op);
|
||
|
}
|
||
|
data++;
|
||
|
continue;
|
||
|
}
|
||
|
if ((*data & 0xF0) == OPM_STREAM_BACKREF) {
|
||
|
// back reference, nested call :)
|
||
|
int distance = ((*(data + 0) & 0x0F) << 8) | (*(data + 1));
|
||
|
int frames_to_play = *(data + 2);
|
||
|
chctx->stream.ptr = data + 3;
|
||
|
opmplay_push_stack(chctx);
|
||
|
data -= distance;
|
||
|
chctx->stream.samples_to_play = frames_to_play; // hack?
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// check for common stuff
|
||
|
switch (*data) {
|
||
|
case OPM_STREAM_END:
|
||
|
// end of current stream, delay forever
|
||
|
chctx->stream.reload = -1;
|
||
|
isRun = false;
|
||
|
break;
|
||
|
// just an NOP, break
|
||
|
case OPM_STREAM_NEW_ORDER:
|
||
|
case OPM_STREAM_NOP:
|
||
|
data++;
|
||
|
break;
|
||
|
case OPM_STREAM_LOOP:
|
||
|
// save loop point
|
||
|
chctx->stream.loop = data;
|
||
|
data++;
|
||
|
break;
|
||
|
case OPM_STREAM_END_FRAME:
|
||
|
// end of frame - special case here
|
||
|
data++;
|
||
|
isRun = false;
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
// test for delay
|
||
|
newdelay = opmplay_set_delay(&data);
|
||
|
if (newdelay > 0) {
|
||
|
chctx->stream.reload = newdelay;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
printf("unknonw token %02x!\n", *data);
|
||
|
return OPMPLAY_ERR_BAD_FILE_STRUCTURE;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
chctx->stream.delay = chctx->stream.reload;
|
||
|
|
||
|
// decrement samples to play counter
|
||
|
if (--chctx->stream.samples_to_play == 0) {
|
||
|
// pop context from the stack
|
||
|
do opmplay_pop_stack(chctx); while (--chctx->stream.samples_to_play == 0);
|
||
|
}
|
||
|
else {
|
||
|
// save data pointer
|
||
|
chctx->stream.ptr = data;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// process control stream
|
||
|
{
|
||
|
opmplay_channel_context_t* chctx = ctx->channels + 0;
|
||
|
uint8_t* data = chctx->stream.ptr;
|
||
|
bool isRun = true;
|
||
|
if (--chctx->stream.delay == 0) {
|
||
|
while (isRun) {
|
||
|
if ((*(data) & 0xE0) == OPM_CTRL_REG_SET) {
|
||
|
int mask = *data;
|
||
|
data++;
|
||
|
if (mask & OPM_CTRL_SET_REG01) OPL3_WriteRegBuffered(ctx->chip, 0x001, *data++);
|
||
|
if (mask & OPM_CTRL_SET_REG08) OPL3_WriteRegBuffered(ctx->chip, 0x008, *data++);
|
||
|
if (mask & OPM_CTRL_SET_REG105) OPL3_WriteRegBuffered(ctx->chip, 0x105, *data++);
|
||
|
if (mask & OPM_CTRL_SET_REG104) OPL3_WriteRegBuffered(ctx->chip, 0x104, *data++);
|
||
|
if (mask & OPM_CTRL_SET_REGBD) OPL3_WriteRegBuffered(ctx->chip, 0x0BD, *data++);
|
||
|
continue;
|
||
|
}
|
||
|
if ((*data & 0xF0) == OPM_STREAM_BACKREF) {
|
||
|
// back reference, nested call :)
|
||
|
int distance = ((*(data + 0) & 0x0F) << 8) | (*(data + 1));
|
||
|
int frames_to_play = *(data + 2);
|
||
|
chctx->stream.ptr = data + 3;
|
||
|
opmplay_push_stack(chctx);
|
||
|
data -= distance;
|
||
|
chctx->stream.samples_to_play = frames_to_play; // hack?
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// check for common stuff
|
||
|
switch (*data) {
|
||
|
// end of stream - rewind everything
|
||
|
case OPM_STREAM_END:
|
||
|
opmplay_loop(ctx);
|
||
|
isRun = false;
|
||
|
return OPMPLAY_ERR_END_OF_STREAM;
|
||
|
// just an NOP, break
|
||
|
case OPM_STREAM_NEW_ORDER:
|
||
|
case OPM_STREAM_NOP:
|
||
|
data++;
|
||
|
break;
|
||
|
case OPM_STREAM_LOOP:
|
||
|
// save loop point
|
||
|
ctx->pos.looped = ctx->pos.frame;
|
||
|
chctx->stream.loop = data;
|
||
|
data++;
|
||
|
break;
|
||
|
// set new frame rate
|
||
|
case OPM_STREAM_SET_FRAME_RATE:
|
||
|
ctx->header.frame_rate = *(uint16_t*)(data + 1); data += 3;
|
||
|
break;
|
||
|
case OPM_STREAM_END_FRAME:
|
||
|
// end of frame - special case here
|
||
|
data++;
|
||
|
isRun = false;
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
// test for delay
|
||
|
newdelay = opmplay_set_delay(&data);
|
||
|
if (newdelay > 0) {
|
||
|
chctx->stream.reload = newdelay;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
printf("unknonw token %02x!\n", *data);
|
||
|
return OPMPLAY_ERR_BAD_FILE_STRUCTURE;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
chctx->stream.delay = chctx->stream.reload;
|
||
|
// decrement samples to play counter
|
||
|
if (--chctx->stream.samples_to_play == 0) {
|
||
|
// pop context from the stack
|
||
|
do opmplay_pop_stack(chctx); while (--chctx->stream.samples_to_play == 0);
|
||
|
}
|
||
|
else {
|
||
|
// save data pointer
|
||
|
chctx->stream.ptr = data;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ctx->pos.frame++;
|
||
|
|
||
|
return rtn;
|
||
|
}
|
||
|
|
||
|
|
||
|
|