diff --git a/.gitignore b/.gitignore index fd6d5f4..54d61e7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ DOSBox/ LOVE/ rel/ test/ +test2/ diff --git a/doc/api.md b/doc/api.md index 0523c5c..ca757db 100644 --- a/doc/api.md +++ b/doc/api.md @@ -10,6 +10,7 @@ * [love.filesystem](#lovefilesystem) * [love.audio](#loveaudio) * [love.event](#loveevent) +* [love.vgm](#lovevgm) ##### [Objects](#objects-1) * [Image](#image) @@ -38,6 +39,18 @@ Returns the amount of memory in kilobytes which is being used by LoveDOS. This includes the memory used by both the loaded assets and lua. +### love.vgm +VGM OPL2 playback + +##### love.vgm.VgmLoad(filename) +Load (or replace) a VGM. + +##### love.vgm.VgmPlay() +Start VGM playback. + +##### love.vgm.VgmStop() +Stop VGM playback. + ### love.graphics Provides functions for drawing lines, shapes, text and images. diff --git a/src/lib/pctimer/gccint8.c b/src/lib/pctimer/gccint8.c new file mode 100644 index 0000000..223db61 --- /dev/null +++ b/src/lib/pctimer/gccint8.c @@ -0,0 +1,243 @@ +/* PCTIMER 1.4 + Millisecond Resolution Timing Library for DJGPP V2 + Release Date: 3/15/1998 + Chih-Hao Tsai (hao520@yahoo.com) + */ + +#include +#include +#include +#include +#include +#include +#include "gccint8.h" + +#define IRQ0 0x8 +#define PIT0 0x40 +#define PIT1 0x41 +#define PIT2 0x42 +#define PITMODE 0x43 +#define PITCONST 1193180L +#define PIT0DEF 18.2067597 +#define KBCTRL 0x61 +#define NEW8H 1 +#define TRUE_FAILURE 0 +#define CHAIN_TERMINATION 1 +#define OUTPORTB_TERMINATION 2 + +static float tick_per_ms = 0.0182068; +static float ms_per_tick = 54.9246551; +static float freq8h = 18.2067597; +static unsigned char flag8h = 0; +static _go32_dpmi_seginfo rm_old_handler, rm_new_handler, pm_old_handler, pm_new_handler; +static _go32_dpmi_registers r, r1; +int counter_8h; +int counter_reset; +unsigned long int ticks_8h = 0; +unsigned char flag_pm_termination = TRUE_FAILURE; + +static void (*hook_function)(); + +void pctimer_init(unsigned int Hz) { + unsigned int pit0_set, pit0_value; + + if (flag8h != NEW8H) { + disable(); + + lock_rm_new8h(); + lock_pm_new8h(); + + _go32_dpmi_lock_data(&hook_function, sizeof(hook_function)); + _go32_dpmi_lock_data(&ticks_8h, sizeof(ticks_8h)); + _go32_dpmi_lock_data(&counter_8h, sizeof(counter_8h)); + _go32_dpmi_lock_data(&counter_8h, sizeof(counter_reset)); + _go32_dpmi_lock_data(&flag_pm_termination, sizeof(flag_pm_termination)); + _go32_dpmi_lock_data(&r, sizeof(r)); + _go32_dpmi_lock_data(&r1, sizeof(r1)); + + _go32_dpmi_get_protected_mode_interrupt_vector(8, &pm_old_handler); + pm_new_handler.pm_offset = (int)pm_new8h; + pm_new_handler.pm_selector = _go32_my_cs(); + _go32_dpmi_chain_protected_mode_interrupt_vector(8, &pm_new_handler); + + _go32_dpmi_get_real_mode_interrupt_vector(8, &rm_old_handler); + rm_new_handler.pm_offset = (int)rm_new8h; + _go32_dpmi_allocate_real_mode_callback_iret(&rm_new_handler, &r1); + _go32_dpmi_set_real_mode_interrupt_vector(8, &rm_new_handler); + + outportb(PITMODE, 0x36); + pit0_value = PITCONST / Hz; + pit0_set = (pit0_value & 0x00ff); + outportb(PIT0, pit0_set); + pit0_set = (pit0_value >> 8); + outportb(PIT0, pit0_set); + + ticks_8h = 0; + flag8h = NEW8H; + freq8h = Hz; + counter_8h = 0; + counter_reset = freq8h / PIT0DEF; + tick_per_ms = freq8h / 1000; + ms_per_tick = 1000 / freq8h; + + enable(); + } +} + +void pctimer_exit(void) { + // unsigned int pit0_set, pit0_value; + unsigned long tick; + char *cmostime; + + if (flag8h == NEW8H) { + disable(); + + outportb(PITMODE, 0x36); + outportb(PIT0, 0x00); + outportb(PIT0, 0x00); + + _go32_dpmi_set_protected_mode_interrupt_vector(8, &pm_old_handler); + + _go32_dpmi_set_real_mode_interrupt_vector(8, &rm_old_handler); + _go32_dpmi_free_real_mode_callback(&rm_new_handler); + + enable(); + + cmostime = get_cmostime(); + tick = PIT0DEF * ((((float)*cmostime) * 3600) + (((float)*(cmostime + 1)) * 60) + (((float)*(cmostime + 2)))); + biostime(1, tick); + + flag8h = 0; + freq8h = PIT0DEF; + counter_reset = freq8h / PIT0DEF; + tick_per_ms = freq8h / 1000; + ms_per_tick = 1000 / freq8h; + } +} + +void rm_new8h(void) { + disable(); + + if (flag_pm_termination == TRUE_FAILURE) { + ticks_8h++; + counter_8h++; + } + + if ((counter_8h == counter_reset) || (flag_pm_termination == CHAIN_TERMINATION)) { + flag_pm_termination = TRUE_FAILURE; + counter_8h = 0; + memset(&r, 0, sizeof(r)); + r.x.cs = rm_old_handler.rm_segment; + r.x.ip = rm_old_handler.rm_offset; + r.x.ss = r.x.sp = 0; + _go32_dpmi_simulate_fcall_iret(&r); + enable(); + } else { + flag_pm_termination = TRUE_FAILURE; + outportb(0x20, 0x20); + enable(); + } +} + +void lock_rm_new8h(void) { _go32_dpmi_lock_code(rm_new8h, (unsigned long)(lock_rm_new8h - rm_new8h)); } + +void pm_new8h(void) { + disable(); + + ticks_8h++; + counter_8h++; + + flag_pm_termination = TRUE_FAILURE; + + if (hook_function) { + hook_function(); + } + + if (counter_8h == counter_reset) { + flag_pm_termination = CHAIN_TERMINATION; + counter_8h = 0; + enable(); + } else { + flag_pm_termination = OUTPORTB_TERMINATION; + outportb(0x20, 0x20); + enable(); + } +} + +void lock_pm_new8h(void) { _go32_dpmi_lock_code(pm_new8h, (unsigned long)(lock_pm_new8h - pm_new8h)); } + +unsigned long pctimer_get_ticks(void) { return ticks_8h; } + +unsigned long pctimer_time(unsigned long start, unsigned long finish) { + unsigned long duration, millisec; + + if (finish < start) + return 0; + else { + duration = finish - start; + millisec = duration * ms_per_tick; + return millisec; + } +} + +void pctimer_sleep(unsigned int delayms) { + unsigned long int delaybegin = 0; + unsigned long int delayend = 0; + unsigned int delaytick; + + delaytick = delayms * tick_per_ms; + + if (flag8h == NEW8H) + delaybegin = pctimer_get_ticks(); + else + delaybegin = biostime(0, 0); + + do { + if (flag8h == NEW8H) + delayend = pctimer_get_ticks(); + else + delayend = biostime(0, 0); + } while ((delayend - delaybegin) < delaytick); +} + +void pctimer_sound(int freq, int duration) { + int byte; + unsigned int freq1; + + freq1 = PITCONST / freq; + outportb(PITMODE, 0xb6); + byte = (freq1 & 0xff); + outportb(PIT2, byte); + byte = (freq1 >> 8); + outportb(PIT2, byte); + byte = inportb(KBCTRL); + outportb(KBCTRL, (byte | 3)); + + pctimer_sleep(duration); + outportb(KBCTRL, (byte & 0xfc)); +} + +char *get_cmostime(void) { + char *buff; + static char buffer[6]; + char ch; + + buff = buffer; + memset(&r, 0, sizeof(r)); + r.h.ah = 0x02; + _go32_dpmi_simulate_int(0x1a, &r); + + ch = r.h.ch; + buffer[0] = (char)((int)(ch & 0x0f) + (int)((ch >> 4) & 0x0f) * 10); + ch = r.h.cl; + buffer[1] = (char)((int)(ch & 0x0f) + (int)((ch >> 4) & 0x0f) * 10); + ch = r.h.dh; + buffer[2] = (char)((int)(ch & 0x0f) + (int)((ch >> 4) & 0x0f) * 10); + buffer[3] = r.h.dl; + buffer[4] = (char)(r.x.flags & 0x0001); + buffer[5] = 0x00; + + return (buff); +} + +void pctimer_set_hook(void (*proc)()) { hook_function = proc; } diff --git a/src/lib/pctimer/gccint8.h b/src/lib/pctimer/gccint8.h new file mode 100644 index 0000000..c9917fe --- /dev/null +++ b/src/lib/pctimer/gccint8.h @@ -0,0 +1,19 @@ +/* PCTIMER 1.4 + Millisecond Resolution Timing Library for DJGPP V2 + Release Date: 3/15/1998 + Chih-Hao Tsai (hao520@yahoo.com) + */ + +void pctimer_init(unsigned int); +void pctimer_exit(void); +void rm_new8h(void); +void lock_rm_new8h(void); +void pm_new8h(void); +void lock_pm_new8h(void); +unsigned long pctimer_get_ticks(void); +unsigned long pctimer_time(unsigned long, unsigned long); +void pctimer_sleep(unsigned int); +void pctimer_sound(int, int); +char *get_cmostime(void); + +void pctimer_set_hook(void (*proc)()); diff --git a/src/lib/pctimer/readme.txt b/src/lib/pctimer/readme.txt new file mode 100644 index 0000000..de20460 --- /dev/null +++ b/src/lib/pctimer/readme.txt @@ -0,0 +1,157 @@ +PCTIMER: Millisecond Resolution Timing Library for DJGPP V2 +Version 1.4 Release Notes +March 15, 1998 +Status: Freeware. +Distribution status: Has to be distributed as this archive. +Send comments to: Chih-Hao Tsai (hao520@yahoo.com). + + +==== A FEW WORDS ON WIN95 + +Although I have only tested PCTIMER with Win95 and CWSDPMI, +PCTIMER should run on most DPMI servers. + +However, theoretically, applications running under Win95 +should not touch hardware interrupts. The "correct" method +of doing millisecond resolution timing under Win95 is to call +Windows API. The standard Multimedia Timer API can do +millisecond resolution timing. (You'll need to include + and link with winmm.lib. But, as far as I know, +RSXNT does not provide Multimedia API access.) + +If you need an example on using Windows API to do millisecond +resolution timing, check this out (I used Visual C++ 4.0): + +Test Report: Millisecond Resolution Timing with Win95/VC4 +http://www.geocities.com/hao510/w98timer/ + + +==== BASIC LOGIC + +PCTIMER reprograms the 8454 IC (Programmable Interrupt Timer) +to get high frequency of System Timer Interrupt (IRQ0) whose +default frequency is 18.2 Hz. Since the operating systems +rely heavily on the System Timer Interrupt, increasing its +frequency could cause instability. PCTIMER hooks both +protected and real mode int 8h handlers (default handler for +System Timer Interrupt) reprograms 8254 to get higher +frequency of interrupts, but calls the original handlers at +a fixed frequency of 18.2 Hz. + + +==== RELATIONSHIP BETWEEN PROTECTED & REAL MODE INT 8H + +According to DJGPP V2 FAQ, hardware interrupts are always +passed to protected mode first, and only if unhandled are +they reflected to real mode. In other words, we must at least +hook protected mode int 8h. To avoid potential loss of ticks +when the protected mode fails to handle the hardware +interrupt, we should also hook real mode int 8h. + +In actual implementation of the two handlers, things become +much more complex than that. + + +==== PCTIMER PROTECTED MODE INT8H HANDLER + +Here is PCTIMER's protected mode int 8h in pseudo code. The +meanings of pm_termination_flag's values are: + +TRUE_FAILURE: The handler failed to handle the hardware +interrupt. + +CHAIN_TERMINATION: The handler terminated with chaining to +the old handler. Note that after chaining the old protected +mode int 8h handler, the *real* mode int 8h *will* get called. +We need to take care of this. + +OUTPORTB_TERMINATION: The handler terminated with an +outportb (0x20, 0x20) instruction. This instruction sends +a hardware request to the Interrupt Controller to terminate +the interrupt. This works (although intrusive), but will +cause the DPMI server to believe that the protected mode +handler failed to do its job. Therefore, the real mode +handler *will* get called. We need to take care of this, +too. + +(Read the real code for details.) + +PCTIMER Protected Mode Int 8h Handler (Pseudo-code) + * pm_termination_flag = TRUE_FAILURE + * counter++ + * if it is time to chain old handler + - pm_termination_flag = CHAIN_TERMINATION + - let the wrapper chains the old handler + * else + - pm_termination_flag = OUTPORTB_TERMINATION + - outportb (0x20, 0x20) + + +==== PCTIMER REAL MODE INT8H HANDLER + +The real mode handler is considerably more complex than the +protected mode one. It depends on pm_termination_flag to +determine how it should behave. Always set +pm_termination_flag to TRUE_FAILURE before we leave, so in +case the protected mode handler should fail, the real mode +handler can detect it next time. + +(Read the real code for details.) + +PCTIMER Real Mode Int 8h Handler (Pseudo-code) + * if pm_termination_flag = TRUE_FAILURE + - counter++ + * if it is time to chain the old handler, or if the protected + mode handler decided to do that (i.e., + pm_termination_flag = CHAIN_TERMINATION) + - call old real mode handler + - pm_termination_flag = TRUE_FAILURE + * else + - outportb (0x20, 0x20) + - pm_termination_flag = TRUE_FAILURE + + +==== Example of Usage + +#include + +void main (void) +{ + unsigned long int start, finish, elapsed_time; + + /* initiate the timer with 1/1000 s resolution */ + /* you can use different resolution, but resolution */ + /* higher than 1000 is not recommended. */ + + pctimer_init (1000); + + start = pctimer_get_ticks (); + /* do some stuff here */ + finish = pctimer_get_ticks (); + elapsed_time = pctimer_time (start, finish); + /* always returns elapsed time in the unit of ms. */ + + pctimer_sleep (200); /* sleep 200 ms */ + pctimer_sound (800, 200); /* 800 Hz sound for 200 ms */ + + /* never forget to exit the timer!!! */ + /* otherwise, the system WILL crash!!! */ + + pctimer_exit (); +} + + +==== HISTORY +3/15/98 Version 1.4 +11/26/95 Version 1.3 +1/29/95 Version 1.2 +1/16/95 Version 1.1 +11/5/94 Version 1.0 + + +==== DISCLAIMER + +I am not at all responsible for any damages, consequential +or incidental, and by using PCTIMER, you are agreeing not +to hold either of the above responsible for any problems +whatsoever. diff --git a/src/main.c b/src/main.c index 465c1a6..95b9b0d 100644 --- a/src/main.c +++ b/src/main.c @@ -22,11 +22,15 @@ #include "image.h" #include "palette.h" #include "package.h" +#include "vgm.h" +#include "lib/pctimer/gccint8.h" static lua_State *L; static void deinit(void) { /* Deinit and clear up everything. Called at exit */ + shutdown_vgm(); + pctimer_exit(); audio_deinit(); vga_deinit(); keyboard_deinit(); @@ -60,6 +64,8 @@ int main(int argc, char **argv) { palette_init(); keyboard_init(); mouse_init(); + pctimer_init(1000); + init_vgm(); /* Init lua */ L = luaL_newstate(); diff --git a/src/modules/l_love.c b/src/modules/l_love.c index 6053ea3..7bf9e24 100644 --- a/src/modules/l_love.c +++ b/src/modules/l_love.c @@ -7,7 +7,7 @@ #include "luaobj.h" -#define LOVE_VERSION "0.42.23" +#define LOVE_VERSION "0.42.23.1" int l_love_getVersion(lua_State *L) { lua_pushstring(L, LOVE_VERSION); @@ -26,6 +26,7 @@ int luaopen_audio(lua_State *L); int luaopen_timer(lua_State *L); int luaopen_keyboard(lua_State *L); int luaopen_mouse(lua_State *L); +int luaopen_vgm(lua_State *L); int luaopen_love(lua_State *L) { int i; @@ -53,6 +54,7 @@ int luaopen_love(lua_State *L) { int (*fn)(lua_State *L); } mods[] = { {"system", luaopen_system}, + {"vgm", luaopen_vgm}, {"event", luaopen_event}, {"filesystem", luaopen_filesystem}, {"graphics", luaopen_graphics}, diff --git a/src/modules/l_vgm.c b/src/modules/l_vgm.c new file mode 100644 index 0000000..da09b15 --- /dev/null +++ b/src/modules/l_vgm.c @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2017 rxi + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the MIT license. See LICENSE for details. + */ + +#include +#include +#include "luaobj.h" +#include "vgm.h" + +int l_vgm_load(lua_State *L) { + const char *filename = luaL_checkstring(L, 1); + const char *err = VgmLoad(filename); + if (err) + luaL_error(L, err); + return 1; +} + +int l_vgm_play(lua_State *L) { + VgmPlay(); + return 1; +} + +int l_vgm_stop(lua_State *L) { + VgmStop(); + return 1; +} + +int luaopen_vgm(lua_State *L) { + luaL_Reg reg[] = { + {"VgmLoad", l_vgm_load}, + {"VgmPlay", l_vgm_play}, + {"VgmStop", l_vgm_stop}, + {0, 0}, + }; + luaL_newlib(L, reg); + return 1; +} diff --git a/src/vgm.c b/src/vgm.c new file mode 100644 index 0000000..945f122 --- /dev/null +++ b/src/vgm.c @@ -0,0 +1,370 @@ +/* +MIT License + +Copyright (c) 2019-2023 Andre Seidelt + +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 +#include +#include +#include +#include + +#include +#include + +#include "vgm.h" +#include "lib/pctimer/gccint8.h" +#include "filesystem.h" + +#define VGM_RESOLUTION 44100 +#define VGM_FACTOR 44 +#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 bool vgm_playback = false; +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; + vgm_playback = true; + + pctimer_set_hook(vgm_int); + } +} + +void VgmStop() { + vgm_playback = false; + pctimer_set_hook(NULL); + + vgm_opl_reset(); +} + +static void VgmDiscard() { + VgmStop(); + if (vgm) { + filesystem_free((void *)vgm); + } + vgm = NULL; + vgm_playback = false; + 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_playback); + LOCK_VARIABLE(vgm_pos); + LOCK_VARIABLE(vgm_data); + LOCK_VARIABLE(vgm_wait); +} + +void shutdown_vgm() { VgmDiscard(); } \ No newline at end of file diff --git a/src/vgm.h b/src/vgm.h new file mode 100644 index 0000000..f887d19 --- /dev/null +++ b/src/vgm.h @@ -0,0 +1,36 @@ +/* +MIT License + +Copyright (c) 2019-2023 Andre Seidelt + +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. +*/ + +#ifndef __VGMPLAY_H__ +#define __VGMPLAY_H__ + +/********************* +** static functions ** +*********************/ +extern char *VgmLoad(const char *fname); +extern void VgmPlay(void); +extern void VgmStop(void); + +extern void init_vgm(void); +extern void shutdown_vgm(void); + +#endif // __VGMPLAY_H__