422 lines
14 KiB
C++
422 lines
14 KiB
C++
#include <stdint.h>
|
|
#include "opmplay.h"
|
|
#include "opmfile.h"
|
|
|
|
// very hacky extern lmao
|
|
void opn_write_reg(int chip, int reg, int data);
|
|
|
|
// -------------------------
|
|
// 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) {
|
|
if ((ctx == NULL)) return OPMPLAY_ERR_NULLPTR;
|
|
|
|
opmplay_memset(ctx, 0, sizeof(opmplay_context_t));
|
|
|
|
return OPMPLAY_ERR_OK;
|
|
}
|
|
|
|
int opmplay_free(opmplay_context_t* ctx)
|
|
{
|
|
for (int ch = 0; ch < ctx->header.streams; ch++) {
|
|
if (ctx->channels[ch].stream.data != NULL) {
|
|
opmplay_memfree((uint8_t*)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.streams));
|
|
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.streams))
|
|
!= sizeof(opm_header_stream_desc_t) * (ctx->header.streams))
|
|
return OPMPLAY_ERR_IO;
|
|
|
|
// allocate and copy channel streams
|
|
for (int ch = 0; ch < ctx->header.streams; ch++) {
|
|
if (streamdesc[ch].size > 0) {
|
|
ctx->channels[ch].stream.data = (const 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, (uint8_t*)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.streams; 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_OPN_DUAL:
|
|
for (int chip = 0; chip < 2; chip++) {
|
|
for (int r = 0; r < 0xF0; r++) {
|
|
//opn_write_reg(chip, r, 0);
|
|
}
|
|
// setup prescaler
|
|
opn_write_reg(chip, 0x2F, 0);
|
|
opn_write_reg(chip, 0x2D, 0);
|
|
}
|
|
break;
|
|
default:
|
|
return OPMPLAY_ERR_DEVICE;
|
|
}
|
|
|
|
for (int ch = 0; ch < ctx->header.streams; ch++) {
|
|
ctx->channels[ch].stream.loop = ctx->channels[ch].stream.data;
|
|
}
|
|
|
|
opmplay_loop(ctx);
|
|
|
|
return OPMPLAY_ERR_OK;
|
|
}
|
|
|
|
// get and parse delay
|
|
static uint32_t opmplay_set_delay(const 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;
|
|
}
|
|
|
|
// ------------------------------------------
|
|
// stream parsing procedures
|
|
// bruuuuuuuhhhhhhhhhhhh
|
|
extern "C" {
|
|
int chip_index;
|
|
bool endOfFrame;
|
|
bool doNextOp;
|
|
}
|
|
|
|
// FM control stream
|
|
const uint8_t* opmplay_parse_fm_control_stream(opmplay_context_t* ctx, opmplay_channel_context_t* chctx, int ch, const uint8_t* data) {
|
|
if ((*(data) & 0xE0) == OPM_CTRL_TIMER_CSM) {
|
|
// CSM/timer stuff
|
|
int mask = *data;
|
|
data++;
|
|
if (mask & OPM_CTRL_CMD80_REG25) opn_write_reg(chip_index, 0x25, *data++);
|
|
if (mask & OPM_CTRL_CMD80_REG24) opn_write_reg(chip_index, 0x24, *data++);
|
|
if (mask & OPM_CTRL_CMD80_REG27) opn_write_reg(chip_index, 0x27, *data++);
|
|
if (mask & OPM_CTRL_CMD80_REG22) opn_write_reg(chip_index, 0x22, *data++);
|
|
if (mask & OPM_CTRL_CMD80_EOF) endOfFrame = true;
|
|
doNextOp = true;
|
|
goto done;
|
|
}
|
|
if ((*(data) & 0x80) == OPM_CTRL_EXTCH3) {
|
|
// channel 3 ext mode
|
|
// OP4 freq is handled by FM channel 3 stream
|
|
int mask = *data;
|
|
data++;
|
|
if (mask & OPM_CTRL_EXTCH3_OP1_HIGH) opn_write_reg(chip_index, 0xAD, *data++);
|
|
if (mask & OPM_CTRL_EXTCH3_OP1_LOW) opn_write_reg(chip_index, 0xA9, *data++);
|
|
if (mask & OPM_CTRL_EXTCH3_OP2_HIGH) opn_write_reg(chip_index, 0xAC, *data++);
|
|
if (mask & OPM_CTRL_EXTCH3_OP2_LOW) opn_write_reg(chip_index, 0xA8, *data++);
|
|
if (mask & OPM_CTRL_EXTCH3_OP3_HIGH) opn_write_reg(chip_index, 0xAE, *data++);
|
|
if (mask & OPM_CTRL_EXTCH3_OP3_LOW) opn_write_reg(chip_index, 0xAA, *data++);
|
|
if (mask & OPM_CTRL_EXTCH3_EOF) endOfFrame = true;
|
|
doNextOp = true;
|
|
goto done;
|
|
}
|
|
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?
|
|
doNextOp = true;
|
|
goto done;
|
|
}
|
|
if ((*data) == OPM_STREAM_END) {
|
|
// rewind to start
|
|
chctx->stream.reload = -1;
|
|
endOfFrame = true;
|
|
doNextOp = true;
|
|
goto done;
|
|
}
|
|
done:
|
|
return data;
|
|
}
|
|
|
|
// FM channel stream
|
|
const uint8_t* opmplay_parse_fm_channel_stream(opmplay_context_t* ctx, opmplay_channel_context_t* chctx, int ch, const uint8_t* data) {
|
|
if ((*(data) & 0xC0) == OPM_FM_ADSR) {
|
|
// ADSR
|
|
int mask = *data;
|
|
int regbase = (ch & 3) + ((mask & OPM_FM_CMD00_OP_MASK) >> 2);
|
|
data++;
|
|
if (mask & OPM_FM_CMD00_REG50) opn_write_reg(chip_index, 0x50 + regbase, *data++);
|
|
if (mask & OPM_FM_CMD00_REG60) opn_write_reg(chip_index, 0x60 + regbase, *data++);
|
|
if (mask & OPM_FM_CMD00_REG70) opn_write_reg(chip_index, 0x70 + regbase, *data++);
|
|
if (mask & OPM_FM_CMD00_REG80) opn_write_reg(chip_index, 0x80 + regbase, *data++);
|
|
doNextOp = true;
|
|
goto done;
|
|
}
|
|
if ((*(data) & 0xC0) == OPM_FM_MUL_TL_EG) {
|
|
// MULT/TL/SSG-EG
|
|
int mask = *data;
|
|
int regbase = (ch & 3) + ((mask & OPM_FM_CMD40_OP_MASK) >> 2);
|
|
data++;
|
|
if (mask & OPM_FM_CMD40_REG30) opn_write_reg(chip_index, 0x30 + regbase, *data++);
|
|
if (mask & OPM_FM_CMD40_REG40) opn_write_reg(chip_index, 0x40 + regbase, *data++);
|
|
if (mask & OPM_FM_CMD40_REG90) opn_write_reg(chip_index, 0x90 + regbase, *data++);
|
|
if (mask & OPM_FM_CMD40_EOF) endOfFrame = true;
|
|
doNextOp = true;
|
|
goto done;
|
|
}
|
|
if ((*(data) & 0xE0) == OPM_FM_FREQ_FB_PAN) {
|
|
// frequency/feedpack
|
|
int mask = *data;
|
|
data++;
|
|
if (mask & OPM_FM_CMD80_REGA4) opn_write_reg(chip_index, 0xA4 + ch, *data++);
|
|
if (mask & OPM_FM_CMD80_REGA0) opn_write_reg(chip_index, 0xA0 + ch, *data++);
|
|
if (mask & OPM_FM_CMD80_REGB0) opn_write_reg(chip_index, 0xB0 + ch, *data++);
|
|
if (mask & OPM_FM_CMD80_REGB4) opn_write_reg(chip_index, 0xB4 + ch, *data++);
|
|
if (mask & OPM_FM_CMD80_EOF) endOfFrame = true;
|
|
doNextOp = true;
|
|
goto done;
|
|
}
|
|
if ((*(data) & 0xE0) == OPM_FM_KEY) {
|
|
// frequency/feedpack
|
|
int mask = *data; data++;
|
|
opn_write_reg(chip_index, 0x28, ((mask & OPM_FM_CMDA0_OP_MASK) << 4) + ch);
|
|
if (mask & OPM_FM_CMDA0_EOF) endOfFrame = true;
|
|
doNextOp = true;
|
|
goto done;
|
|
}
|
|
|
|
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?
|
|
doNextOp = true;
|
|
goto done;
|
|
}
|
|
if ((*data) == OPM_STREAM_END) {
|
|
// rewind to start
|
|
chctx->stream.reload = -1;
|
|
endOfFrame = true;
|
|
doNextOp = true;
|
|
goto done;
|
|
}
|
|
done:
|
|
return data;
|
|
}
|
|
|
|
// parse one individual stream
|
|
const int opmplay_parse_stream(opmplay_context_t* ctx, opmplay_channel_context_t* chctx, int ch, const uint8_t* (*proc)(opmplay_context_t* ctx, opmplay_channel_context_t* chctx, int ch, const uint8_t* data)) {
|
|
uint16_t newdelay;
|
|
const uint8_t* data = chctx->stream.ptr;
|
|
endOfFrame = false;
|
|
|
|
if (--chctx->stream.delay == 0) {
|
|
do {
|
|
|
|
doNextOp = false;
|
|
data = proc(ctx, chctx, ch, data);
|
|
|
|
if (doNextOp) {
|
|
doNextOp = false;
|
|
continue;
|
|
}
|
|
|
|
// check for common stuff
|
|
switch (*data) {
|
|
// just an NOP, break
|
|
case OPM_STREAM_NEW_ORDER:
|
|
case OPM_STREAM_NOP:
|
|
data++;
|
|
break;
|
|
case OPM_STREAM_LOOP:
|
|
// save loop point
|
|
ctx->pos.frame_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++;
|
|
endOfFrame = true;
|
|
break;
|
|
|
|
default:
|
|
// test for delay
|
|
newdelay = opmplay_set_delay(&data);
|
|
if (newdelay > 0) {
|
|
chctx->stream.reload = newdelay;
|
|
}
|
|
else
|
|
{
|
|
printf("unknown token %02x!\n", *data);
|
|
return OPMPLAY_ERR_BAD_FILE_STRUCTURE;
|
|
}
|
|
}
|
|
} while (!endOfFrame);
|
|
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;
|
|
}
|
|
}
|
|
|
|
return OPMPLAY_ERR_OK;
|
|
}
|
|
|
|
int opmplay_tick(opmplay_context_t* ctx) {
|
|
int ch = 0;
|
|
int rtn = OPMPLAY_ERR_OK;
|
|
|
|
// unroll this shit =)
|
|
chip_index = 0;
|
|
opmplay_parse_stream(ctx, ctx->channels + 0, 0, opmplay_parse_fm_control_stream);
|
|
opmplay_parse_stream(ctx, ctx->channels + 1, 0, opmplay_parse_fm_channel_stream);
|
|
opmplay_parse_stream(ctx, ctx->channels + 2, 1, opmplay_parse_fm_channel_stream);
|
|
opmplay_parse_stream(ctx, ctx->channels + 3, 2, opmplay_parse_fm_channel_stream);
|
|
chip_index = 1;
|
|
opmplay_parse_stream(ctx, ctx->channels + 8, 0, opmplay_parse_fm_control_stream);
|
|
opmplay_parse_stream(ctx, ctx->channels + 9, 0, opmplay_parse_fm_channel_stream);
|
|
opmplay_parse_stream(ctx, ctx->channels + 10, 1, opmplay_parse_fm_channel_stream);
|
|
opmplay_parse_stream(ctx, ctx->channels + 11, 2, opmplay_parse_fm_channel_stream);
|
|
|
|
// advance frame counter
|
|
ctx->pos.frame++;
|
|
|
|
return rtn;
|
|
}
|
|
|
|
|
|
|
|
|