Files
lovedos/src/vgm.c

366 lines
9.3 KiB
C

/*
MIT License
Copyright (c) 2019-2023 Andre Seidelt <superilu@yahoo.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
VGM specs are at https://vgmrips.net/wiki/VGM_Specification
*/
#include <pc.h>
#include <stdlib.h>
#include <string.h>
#include <go32.h>
#include <dpmi.h>
#include <stdbool.h>
#include <stdint.h>
#include "vgm.h"
#include "lib/pctimer/gccint8.h"
#include "filesystem.h"
#include "main.h"
#define VGM_RESOLUTION 44100
#define VGM_FACTOR (VGM_RESOLUTION / TICKS_PER_SEC)
#define VGM_OPL_ADDR 0x388
#define VGM_OPL_DATA 0x389
#define END_OF_FUNCTION(x) \
static void x##_end(void); \
static void x##_end() {}
#define LOCK_VARIABLE(x) _go32_dpmi_lock_data((void *)&x, sizeof(x))
#define LOCK_FUNCTION(x) _go32_dpmi_lock_code(x, (long)x##_end - (long)x)
// #define VGM_DUMP
typedef struct {
uint32_t Vgmident;
uint32_t EoFoffset;
uint32_t Version;
uint32_t SN76489clock;
uint32_t YM2413clock;
uint32_t GD3offset;
uint32_t Totalsamples;
uint32_t Loopoffset;
uint32_t Loopsamples;
uint32_t Rate;
uint32_t SNFBSNWSF;
uint32_t YM2612clock;
uint32_t YM2151clock;
uint32_t VGMdataoffset;
uint32_t SegaPCMclock;
uint32_t SPCMInterface;
uint32_t RF5C68clock;
uint32_t YM2203clock;
uint32_t YM2608clock;
uint32_t YM2610Bclock;
uint32_t YM3812clock;
uint32_t YM3526clock;
uint32_t Y8950clock;
uint32_t YMF262clock;
uint32_t YMF278Bclock;
uint32_t YMF271clock;
uint32_t YMZ280Bclock;
uint32_t RF5C164clock;
uint32_t PWMclock;
uint32_t AY8910clock;
uint32_t AYTAYFlags;
uint32_t VMLBLM;
uint32_t GBDMGclock;
uint32_t NESAPUclock;
uint32_t MultiPCMclock;
uint32_t uPD7759clock;
uint32_t OKIM6258clock;
uint32_t OFKFCF;
uint32_t OKIM6295clock;
uint32_t K051649clock;
uint32_t K054539clock;
uint32_t HuC6280clock;
uint32_t C140clock;
uint32_t K053260clock;
uint32_t Pokeyclock;
uint32_t QSoundclock;
uint32_t SCSPclock;
uint32_t ExtraHdrofs;
uint32_t WonderSwanclock;
uint32_t VSUclock;
uint32_t SAA1099clock;
uint32_t ES5503clock;
uint32_t ES5506clock;
uint32_t ESchnsCD;
uint32_t X1010clock;
uint32_t C352clock;
uint32_t GA20clock;
uint32_t unused1;
uint32_t unused2;
uint32_t unused3;
uint32_t unused4;
uint32_t unused5;
uint32_t unused6;
uint32_t unused7;
} vgm_t;
static volatile uint8_t *vgm = NULL;
static volatile vgm_t *vgm_header = NULL;
static volatile uint8_t *vgm_data = NULL;
static volatile uint32_t vgm_pos = 0;
static volatile uint32_t vgm_end = 0;
static volatile int32_t vgm_wait = 0;
/**
* Send the given byte of data to the given register of the OPL2 chip.
*/
static void vgm_opl_write(uint8_t reg, uint8_t val) {
// Select register
outp(VGM_OPL_ADDR, reg);
// Wait for card to accept value
for (int i = 1; i < 25; i++) {
inp(VGM_OPL_ADDR);
}
// Send value
outp(VGM_OPL_DATA, val);
// Wait for card to accept value
for (int i = 1; i < 100; i++) {
inp(VGM_OPL_ADDR);
}
}
END_OF_FUNCTION(vgm_opl_write);
static void vgm_int() {
if (vgm_wait > 0) {
vgm_wait -= VGM_FACTOR;
} else {
while (vgm_pos < vgm_end) {
uint8_t cmd = vgm_data[vgm_pos];
if (cmd >= 0x70 && cmd < 0x80) {
// wait n+1 samples, n can range from 0 to 15.
vgm_wait = 1 + (cmd & 0x0F);
vgm_pos++;
} else if (cmd == 0x5a) {
// YM2413, write value dd to register aa
uint8_t aa = vgm_data[vgm_pos + 1];
uint8_t dd = vgm_data[vgm_pos + 2];
vgm_opl_write(aa, dd);
vgm_pos += 3;
} else if (cmd == 0x61) {
// Wait n samples, n can range from 0 to 65535 (approx 1.49 seconds).
// Longer pauses than this are represented by multiple wait commands.
vgm_wait = *((uint16_t *)(&vgm_data[vgm_pos + 1]));
vgm_pos += 3;
} else if (cmd == 0x62) {
// wait 735 samples (60th of a second), a shortcut for 0x61 0xdf 0x02
vgm_wait = 735;
vgm_pos++;
} else if (cmd == 0x63) {
// wait 882 samples (50th of a second), a shortcut for 0x61 0x72 0x03
vgm_wait = 882;
vgm_pos++;
} else if (cmd == 0x66) {
// end of sound data --> loop
vgm_pos = 0;
break;
} else if (cmd == 0x67) {
// data block: see below
vgm_pos += 3; // cmd 0x66 and type
vgm_pos += *((uint32_t *)(&vgm_data[vgm_pos])); // add size
vgm_pos += 4; // add 4 bytes because of size
} else {
// unknown cmd
}
// only wait when the waiting time is longer than our IRQ interval
if (vgm_wait > VGM_FACTOR) {
break;
} else {
vgm_wait = 0;
}
}
}
}
END_OF_FUNCTION(vgm_int);
/**
* detect the presence of a FM card.
*/
static bool vgm_opl_detect() {
uint8_t A, B;
vgm_opl_write(1, 0);
vgm_opl_write(4, 0x60);
vgm_opl_write(4, 0x80);
A = inp(VGM_OPL_ADDR);
vgm_opl_write(2, 0xFF);
vgm_opl_write(4, 0x21);
pctimer_sleep(80);
B = inp(VGM_OPL_ADDR);
vgm_opl_write(4, 0x60);
vgm_opl_write(4, 0x80);
if ((A & 0xE0) == 0 && (B & 0xE0) == 0xC0) {
return true;
} else {
return false;
}
}
/**
* Hard reset the OPL2 chip. This should be done before sending any register
* data to the chip.
*/
static void vgm_opl_reset() {
for (int i = 0; i < 256; i++) {
vgm_opl_write(i, 0x00);
}
}
#ifdef VGM_DUMP
static void vgm_dump() {
LOGF("VGM version %08X\n", vgm_header->Version);
int pos = 0;
while (pos < vgm_end) {
uint8_t cmd = vgm_data[pos];
if (cmd >= 0x70 && cmd < 0x80) {
// wait n+1 samples, n can range from 0 to 15.
LOGF("WAIT_7x %d\n", 1 + (cmd & 0x0F));
pos++;
} else if (cmd == 0x5a) {
// YM2413, write value dd to register aa
uint8_t aa = vgm_data[pos + 1];
uint8_t dd = vgm_data[pos + 2];
LOGF("WRITE 0x%02X 0x%02X\n", aa, dd);
pos += 3;
} else if (cmd == 0x61) {
// Wait n samples, n can range from 0 to 65535 (approx 1.49 seconds).
// Longer pauses than this are represented by multiple wait commands.
LOGF("WAIT_62 %d\n", *((uint16_t *)(&vgm_data[pos + 1])));
pos += 3;
} else if (cmd == 0x62) {
// wait 735 samples (60th of a second), a shortcut for 0x61 0xdf 0x02
LOG("WAIT_62 735\n");
pos++;
} else if (cmd == 0x63) {
// wait 882 samples (50th of a second), a shortcut for 0x61 0x72 0x03
LOG("WAIT_63 882\n");
pos++;
} else if (cmd == 0x66) {
// end of sound data
LOG("EOS\n");
break;
} else if (cmd == 0x67) {
// data block: see below
vgm_pos += 3; // cmd 0x66 and type
LOGF("DATA size=%ld\n", *((uint32_t *)(&vgm_data[vgm_pos])));
vgm_pos += *((uint32_t *)(&vgm_data[vgm_pos])); // add size
vgm_pos += 4; // add 4 bytes because of size
} else {
LOGF("UNKNOWN 0x%02X\n", cmd);
}
}
}
#endif
void VgmPlay() {
if (vgm_data) {
vgm_pos = 0;
vgm_wait = 0;
pctimer_set_hook(vgm_int);
}
}
void VgmStop() {
pctimer_set_hook(NULL);
vgm_opl_reset();
}
static void VgmDiscard() {
VgmStop();
if (vgm) {
filesystem_free((void *)vgm);
}
vgm = NULL;
vgm_pos = 0;
vgm_end = 0;
vgm_wait = 0;
vgm_header = NULL;
vgm_data = NULL;
}
char *VgmLoad(const char *fname) {
if (!vgm_opl_detect()) {
return "OPL2 not detected!";
}
VgmDiscard();
/* Load file data */
int size;
vgm = filesystem_read(fname, &size);
if (!vgm) {
return "could not read file";
}
if (memcmp((const void *)vgm, "Vgm ", 4) != 0) {
filesystem_free((void *)vgm);
return "VGM header error.";
}
vgm_header = (vgm_t *)vgm;
if (vgm_header->EoFoffset != size - 4) {
filesystem_free((void *)vgm);
return "VGM format error.";
}
if (vgm_header->Version < 0x00000151) {
filesystem_free((void *)vgm);
return "only VGM >= 1.51 is supported.";
}
// set pointers
vgm_data =
((uint8_t *)&vgm_header->VGMdataoffset) + vgm_header->VGMdataoffset;
vgm_end = size - (vgm_data - vgm);
return NULL;
}
/***********************
** exported functions **
***********************/
/**
* @brief initialize vgm subsystem.
*
* @param J VM state.
*/
void init_vgm() {
// lock down
LOCK_FUNCTION(vgm_int);
LOCK_FUNCTION(vgm_opl_write);
LOCK_VARIABLE(vgm_pos);
LOCK_VARIABLE(vgm_data);
LOCK_VARIABLE(vgm_wait);
}
void shutdown_vgm() { VgmDiscard(); }