diff --git a/common.mk b/common.mk index f7e4701..7a9aead 100644 --- a/common.mk +++ b/common.mk @@ -4,6 +4,7 @@ TYPE = ps-exe THISDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) SRCS += $(THISDIR)thirdparty/nugget/common/crt0/crt0.s +SRCS += $(THISDIR)thirdparty/nugget/common/syscalls/printf.s CPPFLAGS += -I$(THISDIR)thirdparty/nugget/psyq/include -I$(THISDIR)psyq-4_7-converted/include -I$(THISDIR)psyq-4.7-converted-full/include -I$(THISDIR)psyq/include LDFLAGS += -L$(THISDIR)thirdparty/nugget/psyq/lib -L$(THISDIR)psyq-4_7-converted/lib -L$(THISDIR)psyq-4.7-converted-full/lib -L$(THISDIR)psyq/lib @@ -49,3 +50,7 @@ endef # convert VAG files to bin %.o: %.vag $(call OBJCOPYME) + +# convert HIT to bin +%.o: %.HIT + $(call OBJCOPYME) \ No newline at end of file diff --git a/hello_mod/HIT/STAR.HIT b/hello_mod/HIT/STAR.HIT new file mode 100644 index 0000000..891dca8 Binary files /dev/null and b/hello_mod/HIT/STAR.HIT differ diff --git a/hello_mod/Makefile b/hello_mod/Makefile new file mode 100644 index 0000000..602b988 --- /dev/null +++ b/hello_mod/Makefile @@ -0,0 +1,8 @@ +TARGET = hello_mod + +SRCS = hello_mod.c \ +src/mod.c \ +src/modplayer.c \ +HIT/STAR.HIT \ + +include ../common.mk diff --git a/hello_mod/README.md b/hello_mod/README.md new file mode 100644 index 0000000..e76c499 --- /dev/null +++ b/hello_mod/README.md @@ -0,0 +1,6 @@ +See the **wiki** for more details on MOD usage : [https://github.com/ABelliqueux/nolibgs_hello_worlds/wiki/MOD](https://github.com/ABelliqueux/nolibgs_hello_worlds/wiki/MOD). + +## Credits + +MOD : stardust memories by Jester : https://modarchive.org/index.php?request=view_by_moduleid&query=59344 +Modplayer port by @NicolasNoble : https://github.com/grumpycoders/pcsx-redux/tree/main/src/mips/modplayer \ No newline at end of file diff --git a/hello_mod/hello_mod.c b/hello_mod/hello_mod.c new file mode 100644 index 0000000..84bc300 --- /dev/null +++ b/hello_mod/hello_mod.c @@ -0,0 +1,229 @@ +// Play a MOD file converted to HIT +// MOD Wiki page : https://github.com/ABelliqueux/nolibgs_hello_worlds/wiki/MOD +#include +#include +#include +#include +#include +#include +// Mod playback +#include "src/mod.h" + +#define VMODE 0 // Video Mode : 0 : NTSC, 1: PAL +#define SCREENXRES 320 // Screen width +#define SCREENYRES 240 // Screen height : If VMODE is 0 = 240, if VMODE is 1 = 256 +#define CENTERX SCREENXRES/2 // Center of screen on x +#define CENTERY SCREENYRES/2 // Center of screen on y +#define FONTX 960 +#define FONTY 0 +#define OTLEN 8 +DISPENV disp[2]; // Double buffered DISPENV and DRAWENV +DRAWENV draw[2]; +short db = 1; // index of which buffer is used, values 0, 1 + +// Font color +CVECTOR fntColor = { 128, 255, 0 }; +CVECTOR fntColorBG = { 0, 0, 0 }; + +// Playback state +enum PLAYBACK { + STOP = 0, + PLAY = 1, + PAUSE = 2, +}; + +enum PLAYBACK state = PLAY; + +void init(void); +void FntColor(CVECTOR fgcol, CVECTOR bgcol ); +void display(void); +void drawBG(void); +void checkPad(void); + +void FntColor(CVECTOR fgcol, CVECTOR bgcol ) +{ + // The debug font clut is at tx, ty + 128 + // tx = bg color + // tx + 1 = fg color + // We can override the color by drawing a rect at these coordinates + // + RECT fg = { FONTX+1, FONTY + 128, 1, 1 }; + RECT bg = { FONTX, FONTY + 128, 1, 1 }; + ClearImage(&fg, fgcol.r, fgcol.g, fgcol.b); + ClearImage(&bg, bgcol.r, bgcol.g, bgcol.b); +} + +void init(void) +{ + ResetCallback(); + ResetGraph(0); // Initialize drawing engine with a complete reset (0) + InitGeom(); + SetGeomOffset(CENTERX,CENTERY); + SetGeomScreen(CENTERX); + SetDefDispEnv(&disp[0], 0, 0 , SCREENXRES, SCREENYRES); // Set display area for both &disp[0] and &disp[1] + SetDefDispEnv(&disp[1], 0, SCREENYRES, SCREENXRES, SCREENYRES); // &disp[0] is on top of &disp[1] + SetDefDrawEnv(&draw[0], 0, SCREENYRES, SCREENXRES, SCREENYRES); // Set draw for both &draw[0] and &draw[1] + SetDefDrawEnv(&draw[1], 0, 0 , SCREENXRES, SCREENYRES); // &draw[0] is below &draw[1] + // Set video mode + #if VMODE + SetVideoMode(MODE_PAL); + disp[0].disp.y = 8; + disp[1].disp.y = 8; + #endif + SetDispMask(1); // Display on screen + setRGB0(&draw[0], 40, 40, 40); // set color for first draw area + setRGB0(&draw[1], 40, 40, 40); // set color for second draw area + draw[0].isbg = 1; // set mask for draw areas. 1 means repainting the area with the RGB color each frame + draw[1].isbg = 1; + PutDispEnv(&disp[db]); // set the disp and draw environnments + PutDrawEnv(&draw[db]); + FntLoad(FONTX, FONTY); // Load font to vram at 960,0(+128) + FntOpen(32, 64, 260, 120, 0, 120 ); // FntOpen(x, y, width, height, black_bg, max. nbr. chars + FntColor(fntColor, fntColorBG); +} +void display(void) +{ + DrawSync(0); // Wait for all drawing to terminate + VSync(0); // Wait for the next vertical blank + PutDispEnv(&disp[db]); // set alternate disp and draw environnments + PutDrawEnv(&draw[db]); + db = !db; +} + +void checkPad(void) +{ + u_short pad = 0; + static u_short oldPad; + pad = PadRead(0); + + // Up + if ( pad & PADLup && !(oldPad & PADLup) ) + { + MOD_PlayNote(11, 25, 15, 63); + oldPad = pad; + } + if ( !(pad & PADLup) && oldPad & PADLup ) + { + oldPad = pad; + } + // Down + if ( pad & PADLdown && !(oldPad & PADLdown) ) + { + MOD_PlayNote(12, 26, 15, 63); + oldPad = pad; + } + if ( !(pad & PADLdown) && oldPad & PADLdown ) + { + oldPad = pad; + } + // Left + if ( pad & PADLleft && !(oldPad & PADLleft) ) + { + MOD_PlayNote(13, 27, 15, 63); + oldPad = pad; + } + if ( !(pad & PADLleft) && oldPad & PADLleft ) + { + oldPad = pad; + } + // Right + if ( pad & PADLright && !(oldPad & PADLright) ) + { + // Channel 1 is transition anim, only take input when !transition + MOD_PlayNote(6, 21, 15, 63); + oldPad = pad; + } + if ( !(pad & PADLright) && oldPad & PADLright ) + { + oldPad = pad; + } + // Cross button + if ( pad & PADRdown && !(oldPad & PADRdown) ) + { + // Select sound + MOD_PlayNote(7, 22, 15, 63); + oldPad = pad; + } + if ( !(pad & PADRdown) && oldPad & PADRdown ) + { + oldPad = pad; + } + // Square button + if ( pad & PADRleft && !(oldPad & PADRleft) ) + { + // Select sound + MOD_PlayNote(8, 23, 15, 63); + oldPad = pad; + } + if ( !(pad & PADRleft) && oldPad & PADRleft ) + { + oldPad = pad; + } + // Circle button + if ( pad & PADRright && !(oldPad & PADRright) ) + { + // Select sound + MOD_PlayNote(9, 28, 15, 63); + oldPad = pad; + } + if ( !(pad & PADRright) && oldPad & PADRright ) + { + oldPad = pad; + } + // Circle button + if ( pad & PADRup && !(oldPad & PADRup) ) + { + // Select sound + MOD_PlayNote(9, 24, 15, 63); + oldPad = pad; + } + if ( !(pad & PADRup) && oldPad & PADRup ) + { + oldPad = pad; + } + // Select button + if ( pad & PADselect && !(oldPad & PADselect) ) + { + if ( state == PLAY ) { stopMusic(); state = STOP; } + else if ( state == STOP ) { startMusic(); state = PLAY; } + oldPad = pad; + } + if ( !(pad & PADselect) && oldPad & PADselect ) + { + oldPad = pad; + } + // Start button + if ( pad & PADstart && !(oldPad & PADstart) ) + { + if ( state == PLAY ) { pauseMusic(); state = PAUSE; } + else if ( state == PAUSE ) { resumeMusic(); state = PLAY; } + oldPad = pad; + } + if ( !(pad & PADstart) && oldPad & PADstart ) + { + oldPad = pad; + } + +} +int main() { + u_int t = 0; + init(); + PadInit(0); + VSyncCallback(checkPad); + // Mod Playback + loadMod(); + startMusic(); + // Main loop + while (1) + { + // TODO: change volume, restart playback + t++; + FntPrint("Hello mod ! %d\nUse pad buttons to play sounds.\n", t); + FntPrint("State: %d\n", state); + FntPrint("Start : play/pause music.\n"); + FntFlush(-1); + display(); + } + return 0; +} + diff --git a/hello_mod/src/mod.c b/hello_mod/src/mod.c new file mode 100644 index 0000000..9ace81a --- /dev/null +++ b/hello_mod/src/mod.c @@ -0,0 +1,66 @@ +#include "mod.h" + +long musicEvent; +typedef struct SpuVoiceVolume { + short volL, volR; +} SpuVoiceVolume; + +SpuVoiceVolume volumeState[24] = {0}; + +void muteSPUvoices() { + for (unsigned i = 0; i < 24; i++) { + // Store current volume + SpuGetVoiceVolume(i, &(volumeState[i].volL), &(volumeState[i].volR) ); + // Mute + SpuSetVoiceVolume(i, 0, 0); + } +} + +void restoreSPUvoices() { + for (unsigned i = 0; i < 24; i++) { + // Restore volume + SpuSetVoiceVolume(i, volumeState[i].volL, volumeState[i].volR ); + } +} + +// Playing a sound effect (aka mod note): https://discord.com/channels/642647820683444236/642848592754901033/898249196174458900 +// Code by NicolasNoble : https://discord.com/channels/642647820683444236/663664210525290507/902624952715452436 +void loadMod() { + printf("Loading MOD:\'%s\'\n", HITFILE); + MOD_Load((struct MODFileFormat*)HITFILE); + printf("%02d Channels, %02d Orders\n", MOD_Channels, MOD_SongLength); +} + +void startMusic() { + ResetRCnt(RCntCNT1); + SetRCnt(RCntCNT1, MOD_hblanks, RCntMdINTR); + StartRCnt(RCntCNT1); + musicEvent = OpenEvent(RCntCNT1, EvSpINT, EvMdINTR, processMusic); + EnableEvent(musicEvent); + restoreSPUvoices(); +} + +long processMusic() { + uint32_t old_hblanks = MOD_hblanks; + MOD_Poll(); + uint32_t new_hblanks = MOD_hblanks; + if (old_hblanks != new_hblanks) SetRCnt(RCntCNT1, new_hblanks, RCntMdINTR); + return MOD_hblanks; +} + +void pauseMusic() { + muteSPUvoices(); + DisableEvent(musicEvent); +} + +void resumeMusic() { + restoreSPUvoices(); + EnableEvent(musicEvent); +} + +void stopMusic() { + MOD_Silence(); + StopRCnt(RCntCNT1); + DisableEvent(musicEvent); + CloseEvent(musicEvent); +} diff --git a/hello_mod/src/mod.h b/hello_mod/src/mod.h new file mode 100644 index 0000000..e614f00 --- /dev/null +++ b/hello_mod/src/mod.h @@ -0,0 +1,22 @@ +#pragma once +#include +#include +#include "../../thirdparty/nugget/common/hardware/hwregs.h" +#include "../../thirdparty/nugget/common/hardware/irq.h" +#include "../../thirdparty/nugget/common/syscalls/syscalls.h" +#define printf ramsyscall_printf + +// Mod Playback +#include "modplayer.h" +extern const uint8_t _binary_HIT_STAR_HIT_start[]; +#define HITFILE _binary_HIT_STAR_HIT_start +extern long musicEvent; + +void muteSPUvoices(); +void restoreSPUvoices(); +void loadMod(); +long processMusic(); +void startMusic(); +void pauseMusic(); +void resumeMusic(); +void stopMusic(); \ No newline at end of file diff --git a/hello_mod/src/modplayer.c b/hello_mod/src/modplayer.c new file mode 100644 index 0000000..c30efed --- /dev/null +++ b/hello_mod/src/modplayer.c @@ -0,0 +1,806 @@ +/* + +MIT License + +Copyright (c) 2021 PCSX-Redux authors + +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. + +*/ + +#include "modplayer.h" + +#include +#include + +#include "../../thirdparty/nugget/common/hardware/dma.h" +#include "../../thirdparty/nugget/common/hardware/spu.h" +#include "../../thirdparty/nugget/common/syscalls/syscalls.h" + +/* This code is a reverse engineering of the file MODPLAY.BIN, located in the zip file + "Asm-Mod" from http://hitmen.c02.at/html/psx_tools.html, that has the CRC32 bb91769f. */ + +struct MODSampleData { + char name[22]; + union { + uint16_t length; + uint8_t lenarr[2]; + }; + uint8_t finetune; + uint8_t volume; + uint16_t repeatLocation; + uint16_t repeatLength; +}; + +struct MODFileFormat { + char title[20]; + struct MODSampleData samples[31]; + uint8_t songLength; + uint8_t padding; + uint8_t patternTable[128]; + uint8_t signature[4]; +}; + +struct SPUChannelData { + uint16_t note; + int16_t period; + uint16_t slideTo; + uint8_t slideSpeed; + uint8_t volume; + uint8_t sampleID; + int8_t vibrato; + uint8_t fx[4]; + uint16_t samplePos; +}; + +struct SpuInstrumentData { + uint16_t baseAddress; + uint8_t finetune; + uint8_t volume; +}; + +static struct SpuInstrumentData s_spuInstrumentData[31]; + +static void SPUInit() { + DPCR |= 0x000b0000; + SPU_VOL_MAIN_LEFT = 0x3800; + SPU_VOL_MAIN_RIGHT = 0x3800; + SPU_CTRL = 0; + SPU_KEY_ON_LOW = 0; + SPU_KEY_ON_HIGH = 0; + SPU_KEY_OFF_LOW = 0xffff; + SPU_KEY_OFF_HIGH = 0xffff; + SPU_RAM_DTC = 4; + SPU_VOL_CD_LEFT = 0; + SPU_VOL_CD_RIGHT = 0; + SPU_PITCH_MOD_LOW = 0; + SPU_PITCH_MOD_HIGH = 0; + SPU_NOISE_EN_LOW = 0; + SPU_NOISE_EN_HIGH = 0; + SPU_REVERB_EN_LOW = 0; + SPU_REVERB_EN_HIGH = 0; + SPU_VOL_EXT_LEFT = 0; + SPU_VOL_EXT_RIGHT = 0; + SPU_CTRL = 0x8000; +} + +static void SPUResetVoice(int voiceID) { + SPU_VOICES[voiceID].volumeLeft = 0; + SPU_VOICES[voiceID].volumeRight = 0; + SPU_VOICES[voiceID].sampleRate = 0; + SPU_VOICES[voiceID].sampleStartAddr = 0; + SPU_VOICES[voiceID].ad = 0x000f; + SPU_VOICES[voiceID].currentVolume = 0; + SPU_VOICES[voiceID].sampleRepeatAddr = 0; + SPU_VOICES[voiceID].sr = 0x0000; +} + +static void SPUUploadInstruments(uint32_t SpuAddr, const uint8_t* data, uint32_t size) { + uint32_t bcr = size >> 6; + if (size & 0x3f) bcr++; + bcr <<= 16; + bcr |= 0x10; + + SPU_RAM_DTA = SpuAddr >> 3; + SPU_CTRL = (SPU_CTRL & ~0x0030) | 0x0020; + while ((SPU_CTRL & 0x0030) != 0x0020) + ; + // original code erroneously was doing SBUS_DEV4_CTRL = SBUS_DEV4_CTRL; + SBUS_DEV4_CTRL &= ~0x0f000000; + DMA_CTRL[DMA_SPU].MADR = (uint32_t)data; + DMA_CTRL[DMA_SPU].BCR = bcr; + DMA_CTRL[DMA_SPU].CHCR = 0x01000201; + + while ((DMA_CTRL[DMA_SPU].CHCR & 0x01000000) != 0) + ; +} + +static void SPUUnMute() { SPU_CTRL = 0xc000; } + +static void SPUSetVoiceVolume(int voiceID, uint16_t left, uint16_t right) { + SPU_VOICES[voiceID].volumeLeft = left >> 2; + SPU_VOICES[voiceID].volumeRight = right >> 2; +} + +static void SPUSetStartAddress(int voiceID, uint32_t spuAddr) { SPU_VOICES[voiceID].sampleStartAddr = spuAddr >> 3; } + +static void SPUWaitIdle() { + do { + for (unsigned c = 0; c < 2045; c++) __asm__ volatile(""); + } while ((SPU_STATUS & 0x07ff) != 0); +} + +static void SPUKeyOn(uint32_t voiceBits) { + SPU_KEY_ON_LOW = voiceBits; + SPU_KEY_ON_HIGH = voiceBits >> 16; +} + +static void SPUSetVoiceSampleRate(int voiceID, uint16_t sampleRate) { SPU_VOICES[voiceID].sampleRate = sampleRate; } + +unsigned MOD_Check(const struct MODFileFormat* module) { + if (syscall_strncmp(module->signature, "HIT", 3) == 0) { + return module->signature[3] - '0'; + } else if (syscall_strncmp(module->signature, "HM", 2) == 0) { + return ((module->signature[2] - '0') * 10) + module->signature[3] - '0'; + } + return 0; +} + +unsigned MOD_Channels = 0; +unsigned MOD_SongLength = 0; +// original code keeps this one to the very beginning of the file, +// while this code keeps the pointer to the beginning of the order table +static const uint8_t* MOD_ModuleData = NULL; +unsigned MOD_CurrentOrder = 0; +unsigned MOD_CurrentPattern = 0; +unsigned MOD_CurrentRow = 0; +unsigned MOD_Speed = 0; +unsigned MOD_Tick = 0; +// this never seems to be updated in the original code, which is a +// mistake; the F command handler was all wrong +unsigned MOD_BPM = 0; +// original code keeps this one to the NEXT row, +// while this code keeps the pointer to the CURRENT row +const uint8_t* MOD_RowPointer = NULL; +int MOD_ChangeRowNextTick = 0; +unsigned MOD_NextRow = 0; +int MOD_ChangeOrderNextTick = 0; +unsigned MOD_NextOrder = 0; +uint8_t MOD_PatternDelay = 0; +unsigned MOD_LoopStart = 0; +unsigned MOD_LoopCount = 0; +int MOD_Stereo = 0; +uint32_t MOD_hblanks; + +// This function is now more of a helper to calculate the number of hsync +// values to wait until the next call to MOD_Poll. If the user wants to use +// another method, they will have to inspect MOD_BPM manually and make their +// own math based on their own timer. +static void MOD_SetBPM(unsigned bpm) { + MOD_BPM = bpm; + // The original code only uses 39000 here but the reality is a bit more + // complex than that, as not all clocks are exactly the same, depending + // on the machine's region, and the video mode selected. + + uint32_t status = GPU_STATUS; + int isPalConsole = *((const char*)0xbfc7ff52) == 'E'; + int isPal = (status & 0x00100000) != 0; + uint32_t base; + if (isPal && isPalConsole) { // PAL video on PAL console + base = 39062; // 312.5 * 125 * 50.000 / 50 or 314 * 125 * 49.761 / 50 + } else if (isPal && !isPalConsole) { // PAL video on NTSC console + base = 39422; // 312.5 * 125 * 50.460 / 50 or 314 * 125 * 50.219 / 50 + } else if (!isPal && isPalConsole) { // NTSC video on PAL console + base = 38977; // 262.5 * 125 * 59.393 / 50 or 263 * 125 * 59.280 / 50 + } else { // NTSC video on NTSC console + base = 39336; // 262.5 * 125 * 59.940 / 50 or 263 * 125 * 59.826 / 50 + } + MOD_hblanks = base / bpm; +} + +static struct SPUChannelData s_channelData[24]; + +uint32_t MOD_Load(const struct MODFileFormat* module) { + SPUInit(); + MOD_Channels = MOD_Check(module); + + if (MOD_Channels == 0) return 0; + + uint32_t currentSpuAddress = 0x1010; + for (unsigned i = 0; i < 31; i++) { + s_spuInstrumentData[i].baseAddress = currentSpuAddress >> 4; + s_spuInstrumentData[i].finetune = module->samples[i].finetune; + s_spuInstrumentData[i].volume = module->samples[i].volume; + currentSpuAddress += module->samples[i].lenarr[0] * 0x100 + module->samples[i].lenarr[1]; + } + + MOD_SongLength = module->songLength; + + unsigned maxPatternID = 0; + for (unsigned i = 0; i < 128; i++) { + if (maxPatternID < module->patternTable[i]) maxPatternID = module->patternTable[i]; + } + + MOD_ModuleData = (const uint8_t*)&module->patternTable[0]; + + SPUUploadInstruments(0x1010, MOD_ModuleData + 4 + 128 + MOD_Channels * 0x100 * (maxPatternID + 1), + currentSpuAddress - 0x1010); + + MOD_CurrentOrder = 0; + MOD_CurrentPattern = module->patternTable[0]; + MOD_CurrentRow = 0; + MOD_Speed = 6; + MOD_Tick = 6; + MOD_RowPointer = MOD_ModuleData + 4 + 128 + MOD_CurrentPattern * MOD_Channels * 0x100; + // original code goes only up to MOD_Channels; let's reset all 24 + for (unsigned i = 0; i < 24; i++) SPUResetVoice(i); + MOD_ChangeRowNextTick = 0; + MOD_ChangeOrderNextTick = 0; + MOD_LoopStart = 0; + MOD_LoopCount = 0; + + // these two are erroneously missing from the original code, at + // least for being able to play more than one music + MOD_PatternDelay = 0; + syscall_memset(s_channelData, 0, sizeof(s_channelData)); + + SPUUnMute(); + + // this one is also missing, and is necessary, for being able to call MOD_Load + // after another song that changed the tempo previously + MOD_SetBPM(125); + + // the original code would do: + // return MOD_Channels; + // but we are returning the size for the MOD_Relocate call + return 4 + 128 + MOD_Channels * 0x100 * (maxPatternID + 1); +} + +void MOD_Silence() { + SPUInit(); + for (unsigned i = 0; i < 24; i++) { + SPUResetVoice(i); + } +} + +void MOD_Relocate(uint8_t* s1) { + if (MOD_ModuleData == s1) return; + unsigned maxPatternID = 0; + for (unsigned i = 0; i < 128; i++) { + if (maxPatternID < MOD_ModuleData[i]) maxPatternID = MOD_ModuleData[i]; + } + + size_t n = 4 + 128 + MOD_Channels * 0x100 * (maxPatternID + 1); + + const uint8_t* s2 = MOD_ModuleData; + size_t i; + + if (s1 < s2) { + for (i = 0; i < n; i++) *s1++ = *s2++; + } else if (s1 > s2) { + s1 += n; + s2 += n; + for (i = 0; i < n; i++) *--s1 = *--s2; + } + + MOD_ModuleData = s1; +} + +static const uint8_t MOD_SineTable[32] = { + 0x00, 0x18, 0x31, 0x4a, 0x61, 0x78, 0x8d, 0xa1, 0xb4, 0xc5, 0xd4, 0xe0, 0xeb, 0xf4, 0xfa, 0xfd, + 0xff, 0xfd, 0xfa, 0xf4, 0xeb, 0xe0, 0xd4, 0xc5, 0xb4, 0xa1, 0x8d, 0x78, 0x61, 0x4a, 0x31, 0x18, +}; + +// C C# D D# E F F# G G# A A# B +const uint16_t MOD_PeriodTable[36 * 16] = { + 856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453, // octave 1 tune 0 + 428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226, // octave 2 tune 0 + 214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113, // octave 3 tune 0 + 850, 802, 757, 715, 674, 637, 601, 567, 535, 505, 477, 450, // octave 1 tune 1 + 425, 401, 379, 357, 337, 318, 300, 284, 268, 253, 239, 225, // octave 2 tune 1 + 213, 201, 189, 179, 169, 159, 150, 142, 134, 126, 119, 113, // octave 3 tune 1 + 844, 796, 752, 709, 670, 632, 597, 563, 532, 502, 474, 447, // octave 1 tune 2 + 422, 398, 376, 355, 335, 316, 298, 282, 266, 251, 237, 224, // octave 2 tune 2 + 211, 199, 188, 177, 167, 158, 149, 141, 133, 125, 118, 112, // octave 3 tune 2 + 838, 791, 746, 704, 665, 628, 592, 559, 528, 498, 470, 444, // octave 1 tune 3 + 419, 395, 373, 352, 332, 314, 296, 280, 264, 249, 235, 222, // octave 2 tune 3 + 209, 198, 187, 176, 166, 157, 148, 140, 132, 125, 118, 111, // octave 3 tune 3 + 832, 785, 741, 699, 660, 623, 588, 555, 524, 495, 467, 441, // octave 1 tune 4 + 416, 392, 370, 350, 330, 312, 294, 278, 262, 247, 233, 220, // octave 2 tune 4 + 208, 196, 185, 175, 165, 156, 147, 139, 131, 124, 117, 110, // octave 3 tune 4 + 826, 779, 736, 694, 655, 619, 584, 551, 520, 491, 463, 437, // octave 1 tune 5 + 413, 390, 368, 347, 328, 309, 292, 276, 260, 245, 232, 219, // octave 2 tune 5 + 206, 195, 184, 174, 164, 155, 146, 138, 130, 123, 116, 109, // octave 3 tune 5 + 820, 774, 730, 689, 651, 614, 580, 547, 516, 487, 460, 434, // octave 1 tune 6 + 410, 387, 365, 345, 325, 307, 290, 274, 258, 244, 230, 217, // octave 2 tune 6 + 205, 193, 183, 172, 163, 154, 145, 137, 129, 122, 115, 109, // octave 3 tune 6 + 814, 768, 725, 684, 646, 610, 575, 543, 513, 484, 457, 431, // octave 1 tune 7 + 407, 384, 363, 342, 323, 305, 288, 272, 256, 242, 228, 216, // octave 2 tune 7 + 204, 192, 181, 171, 161, 152, 144, 136, 128, 121, 114, 108, // octave 3 tune 7 + 907, 856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, // octave 1 tune -8 + 453, 428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, // octave 2 tune -8 + 226, 214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, // octave 3 tune -8 + 900, 850, 802, 757, 715, 675, 636, 601, 567, 535, 505, 477, // octave 1 tune -7 + 450, 425, 401, 379, 357, 337, 318, 300, 284, 268, 253, 238, // octave 2 tune -7 + 225, 212, 200, 189, 179, 169, 159, 150, 142, 134, 126, 119, // octave 3 tune -7 + 894, 844, 796, 752, 709, 670, 632, 597, 563, 532, 502, 474, // octave 1 tune -6 + 447, 422, 398, 376, 355, 335, 316, 298, 282, 266, 251, 237, // octave 2 tune -6 + 223, 211, 199, 188, 177, 167, 158, 149, 141, 133, 125, 118, // octave 3 tune -6 + 887, 838, 791, 746, 704, 665, 628, 592, 559, 528, 498, 470, // octave 1 tune -5 + 444, 419, 395, 373, 352, 332, 314, 296, 280, 264, 249, 235, // octave 2 tune -5 + 222, 209, 198, 187, 176, 166, 157, 148, 140, 132, 125, 118, // octave 3 tune -5 + 881, 832, 785, 741, 699, 660, 623, 588, 555, 524, 494, 467, // octave 1 tune -4 + 441, 416, 392, 370, 350, 330, 312, 294, 278, 262, 247, 233, // octave 2 tune -4 + 220, 208, 196, 185, 175, 165, 156, 147, 139, 131, 123, 117, // octave 3 tune -4 + 875, 826, 779, 736, 694, 655, 619, 584, 551, 520, 491, 463, // octave 1 tune -3 + 437, 413, 390, 368, 347, 328, 309, 292, 276, 260, 245, 232, // octave 2 tune -3 + 219, 206, 195, 184, 174, 164, 155, 146, 138, 130, 123, 116, // octave 3 tune -3 + 868, 820, 774, 730, 689, 651, 614, 580, 547, 516, 487, 460, // octave 1 tune -2 + 434, 410, 387, 365, 345, 325, 307, 290, 274, 258, 244, 230, // octave 2 tune -2 + 217, 205, 193, 183, 172, 163, 154, 145, 137, 129, 122, 115, // octave 3 tune -2 + 862, 814, 768, 725, 684, 646, 610, 575, 543, 513, 484, 457, // octave 1 tune -1 + 431, 407, 384, 363, 342, 323, 305, 288, 272, 256, 242, 228, // octave 2 tune -1 + 216, 203, 192, 181, 171, 161, 152, 144, 136, 128, 121, 114, // octave 3 tune -1 +}; + +#define SETVOICESAMPLERATE(channel, newPeriod) \ + SPUSetVoiceSampleRate(channel, ((7093789 / (newPeriod * 2)) << 12) / 44100) +#define SETVOICEVOLUME(channel, volume) \ + volume <<= 8; \ + if (MOD_Stereo) { \ + int pan = (channel & 1) ^ (channel >> 1); \ + int16_t left = pan == 0 ? volume : 0; \ + int16_t right = pan == 0 ? 0 : volume; \ + SPUSetVoiceVolume(channel, left, right); \ + } else { \ + SPUSetVoiceVolume(channel, volume, volume); \ + } + +static void MOD_UpdateEffect() { + const uint8_t* rowPointer = MOD_RowPointer; + const unsigned channels = MOD_Channels; + for (unsigned channel = 0; channel < channels; channel++) { + uint8_t effectNibble23 = rowPointer[3]; + uint8_t effectNibble1 = rowPointer[2] & 0x0f; + uint8_t effectNibble2 = effectNibble23 & 0x0f; + uint8_t effectNibble3 = effectNibble23 >> 4; + + uint8_t arpeggioTick; + int32_t newPeriod; + int16_t volume; + uint16_t slideTo; + uint8_t fx; + uint32_t mutation; + int8_t newValue; + + struct SPUChannelData* const channelData = &s_channelData[channel]; + + switch (effectNibble1) { + case 0: // arpeggio + if (effectNibble23 == 0) break; + arpeggioTick = MOD_Tick; + arpeggioTick %= 3; + switch (arpeggioTick) { + case 0: + newPeriod = channelData->period; + break; + case 1: + newPeriod = MOD_PeriodTable[channelData->note + effectNibble3]; + break; + case 2: + newPeriod = MOD_PeriodTable[channelData->note + effectNibble2]; + break; + } + SETVOICESAMPLERATE(channel, newPeriod); + break; + case 1: // portamento up + newPeriod = channelData->period; + newPeriod -= effectNibble23; + if (newPeriod < 108) newPeriod = 108; + channelData->period = newPeriod; + SETVOICESAMPLERATE(channel, newPeriod); + break; + case 2: // portamento down + newPeriod = channelData->period; + newPeriod += effectNibble23; + if (newPeriod > 907) newPeriod = 907; + channelData->period = newPeriod; + SETVOICESAMPLERATE(channel, newPeriod); + break; + case 5: + volume = channelData->volume; + if (effectNibble23 <= 0x10) { + volume -= effectNibble23; + if (volume < 0) volume = 0; + } else { + volume += effectNibble3; + if (volume > 63) volume = 63; + } + channelData->volume = volume; + SETVOICEVOLUME(channel, volume); + /* fall through */ + case 3: // glissando + newPeriod = channelData->period; + slideTo = channelData->slideTo; + if (newPeriod < slideTo) { + newPeriod += channelData->slideSpeed; + if (newPeriod > slideTo) newPeriod = slideTo; + } else if (newPeriod > slideTo) { + newPeriod -= channelData->slideSpeed; + if (newPeriod < slideTo) newPeriod = slideTo; + } + channelData->period = newPeriod; + SETVOICESAMPLERATE(channel, newPeriod); + break; + case 6: + volume = channelData->volume; + if (effectNibble23 <= 0x10) { + volume -= effectNibble23; + if (volume < 0) volume = 0; + } else { + volume += effectNibble3; + if (volume > 63) volume = 63; + } + channelData->volume = volume; + SETVOICEVOLUME(channel, volume); + /* fall through */ + case 4: // vibrato + mutation = channelData->vibrato & 0x1f; + switch (channelData->fx[3] & 3) { + case 0: + case 3: // 3 is technically random + mutation = MOD_SineTable[mutation]; + break; + case 1: + if (channelData->vibrato < 0) { + mutation *= -8; + mutation += 0xff; + } else { + mutation *= 8; + } + break; + case 2: + mutation = 0xff; + break; + } + mutation *= channelData->fx[1] >> 4; + mutation >>= 7; + newPeriod = channelData->period; + if (channelData->vibrato < 0) { + newPeriod -= mutation; + } else { + newPeriod += mutation; + } + newValue = channelData->vibrato; + newValue += channelData->fx[1] & 0x0f; + if (newValue >= 32) newValue -= 64; + channelData->vibrato = newValue; + SETVOICESAMPLERATE(channel, newPeriod); + break; + case 7: // tremolo + mutation = s_channelData[0].fx[0] & 0x1f; + switch (s_channelData[0].fx[3] & 3) { + case 0: + case 3: // 3 is technically random + mutation = MOD_SineTable[mutation]; + break; + case 1: + if (channelData->fx[0] & 0x80) { + mutation *= -8; + mutation += 0xff; + } else { + mutation *= 8; + } + break; + case 2: + mutation = 0xff; + break; + } + mutation *= channelData->fx[3] >> 4; + mutation >>= 6; + volume = channelData->volume; + if (channelData->fx[0] & 0x80) { + volume -= mutation; + } else { + volume += mutation; + } + newValue = channelData->fx[0] + (channelData->fx[2] & 0x0f); + if (newValue >= 32) newValue -= 64; + channelData->fx[0] = newValue; + if (volume > 63) volume = 63; + SETVOICEVOLUME(channel, volume); + break; + case 10: // volume slide + volume = channelData->volume; + if (effectNibble23 <= 0x10) { + volume -= effectNibble23; + if (volume < 0) volume = 0; + } else { + volume += effectNibble3; + if (volume > 63) volume = 63; + } + channelData->volume = volume; + SETVOICEVOLUME(channel, volume); + break; + case 14: // extended + switch (effectNibble3) { + case 9: // retrigger sample + // this doesn't look right, we probably want to reset the sample location + if ((MOD_Tick % effectNibble2) == 0) SPUKeyOn(1 << channel); + break; + case 12: // cut sample + if (MOD_Tick != effectNibble2) break; + channelData->volume = 0; + SPUSetVoiceVolume(channel, 0, 0); + } + break; + } + + rowPointer += 4; + } +} + +static void MOD_UpdateRow() { + const unsigned channels = MOD_Channels; + if (MOD_ChangeOrderNextTick) { + unsigned newOrder = MOD_NextOrder; + if (newOrder >= MOD_SongLength) newOrder = 0; + MOD_CurrentRow = 0; + MOD_CurrentOrder = newOrder; + MOD_CurrentPattern = MOD_ModuleData[newOrder]; + } + if (MOD_ChangeRowNextTick) { + unsigned newRow = (MOD_NextRow >> 4) * 10 + (MOD_NextRow & 0x0f); + if (newRow >= 64) newRow = 0; + MOD_CurrentRow = newRow; + if (MOD_ChangeOrderNextTick) { + if (++MOD_CurrentOrder >= MOD_SongLength) MOD_CurrentOrder = 0; + MOD_CurrentPattern = MOD_ModuleData[MOD_CurrentOrder]; + } + } + MOD_ChangeRowNextTick = 0; + MOD_ChangeOrderNextTick = 0; + MOD_RowPointer = + MOD_ModuleData + 128 + 4 + MOD_CurrentPattern * MOD_Channels * 0x100 + MOD_CurrentRow * channels * 4; + const uint8_t* rowPointer = MOD_RowPointer; + + for (unsigned channel = 0; channel < channels; channel++) { + int16_t volume; + struct SPUChannelData* const channelData = &s_channelData[channel]; + + uint8_t effectNibble1 = rowPointer[2]; + uint8_t effectNibble23 = rowPointer[3]; + uint16_t nibble0 = rowPointer[0]; + unsigned sampleID = (nibble0 & 0xf0) | (effectNibble1 >> 4); + uint8_t effectNibble2 = effectNibble23 & 0x0f; + uint8_t effectNibble3 = effectNibble23 >> 4; + unsigned period = ((nibble0 & 0x0f) << 8) | rowPointer[1]; + int32_t newPeriod; + uint8_t fx; + effectNibble1 &= 0x0f; + + if (effectNibble1 != 9) channelData->samplePos = 0; + if (sampleID != 0) { + channelData->sampleID = --sampleID; + volume = s_spuInstrumentData[sampleID].volume; + if (volume > 63) volume = 63; + channelData->volume = volume; + if (effectNibble1 != 7) { + SETVOICEVOLUME(channel, volume); + } + SPUSetStartAddress(channel, s_spuInstrumentData[sampleID].baseAddress << 4 + channelData->samplePos); + } + + if (period != 0) { + int periodIndex; + // original code erroneously does >= 0 + for (periodIndex = 35; periodIndex--; periodIndex > 0) { + if (MOD_PeriodTable[periodIndex] == period) break; + } + channelData->note = periodIndex + s_spuInstrumentData[channelData->sampleID].finetune * 36; + fx = channelData->fx[3]; + if ((fx & 0x0f) < 4) { + channelData->vibrato = 0; + } + if ((fx >> 4) < 4) { + channelData->fx[0] = 0; + } + if ((effectNibble1 != 3) && (effectNibble1 != 5)) { + SPUWaitIdle(); + SPUKeyOn(1 << channel); + channelData->period = MOD_PeriodTable[channelData->note]; + } + newPeriod = channelData->period; + SETVOICESAMPLERATE(channel, newPeriod); + } + + switch (effectNibble1) { + case 3: // glissando + if (effectNibble23 != 0) { + channelData->slideSpeed = effectNibble23; + } + if (period != 0) { + channelData->slideTo = MOD_PeriodTable[channelData->note]; + } + break; + case 4: // vibrato + if (effectNibble3 != 0) { + fx = channelData->fx[1]; + fx &= ~0x0f; + fx |= effectNibble3; + channelData->fx[1] = fx; + } + if (effectNibble2 != 0) { + fx = channelData->fx[1]; + fx &= ~0xf0; + fx |= effectNibble3 << 4; + channelData->fx[1] = fx; + } + break; + case 7: // tremolo + if (effectNibble3 != 0) { + fx = channelData->fx[2]; + fx &= ~0x0f; + fx |= effectNibble3; + channelData->fx[2] = fx; + } + if (effectNibble2 != 0) { + fx = channelData->fx[2]; + fx &= ~0xf0; + fx |= effectNibble2 << 4; + channelData->fx[2] = fx; + } + break; + case 9: // sample jump + if (effectNibble23 != 0) { + uint16_t newSamplePos = effectNibble23; + channelData->samplePos = newSamplePos << 7; + } + break; + case 11: // order jump + if (!MOD_ChangeOrderNextTick) { + MOD_ChangeOrderNextTick = 1; + MOD_NextOrder = effectNibble23; + } + break; + case 12: // set volume + volume = effectNibble23; + if (volume > 64) volume = 63; + channelData->volume = volume; + SETVOICEVOLUME(channel, volume); + break; + case 13: // pattern break + if (!MOD_ChangeRowNextTick) { + MOD_ChangeRowNextTick = 1; + MOD_NextRow = effectNibble23; + } + break; + case 14: // extended + switch (effectNibble3) { + case 1: // fineslide up + newPeriod = channelData->period; + newPeriod -= effectNibble2; + channelData->period = newPeriod; + SETVOICESAMPLERATE(channel, newPeriod); + break; + case 2: // fineslide down + newPeriod = channelData->period; + newPeriod += effectNibble2; + channelData->period = newPeriod; + SETVOICESAMPLERATE(channel, newPeriod); + break; + case 4: // set vibrato waveform + fx = channelData->fx[3]; + fx &= ~0x0f; + fx |= effectNibble2; + channelData->fx[3] = fx; + break; + case 5: // set finetune value + s_spuInstrumentData[sampleID].finetune = effectNibble2; + break; + case 6: // loop pattern + if (MOD_LoopCount-- == 0) { + MOD_LoopCount = effectNibble2; + } + if (MOD_LoopCount != 0) { + MOD_CurrentRow = MOD_LoopStart; + } + break; + case 7: // set tremolo waveform + fx = channelData->fx[3]; + fx &= ~0xf0; + fx |= effectNibble2 << 4; + channelData->fx[3] = fx; + break; + case 10: // fine volume up + volume = channelData->volume; + volume += effectNibble2; + if (volume > 63) volume = 63; + channelData->volume = volume; + SETVOICEVOLUME(channel, volume); + break; + case 11: // fine volume down + volume = channelData->volume; + volume -= effectNibble2; + if (volume < 0) volume = 0; + channelData->volume = volume; + SETVOICEVOLUME(channel, volume); + break; + case 14: // delay pattern + MOD_PatternDelay = effectNibble2; + break; + } + break; + case 15: // set speed + // the original code here is very wrong with regards to + // how to interpret the command; also it was very opinionated + // about using timer1 for its clock source + if (effectNibble23 == 0) break; + if (effectNibble23 < 32) { + MOD_Speed = effectNibble23; + } else { + MOD_SetBPM(effectNibble23); + } + break; + } + + rowPointer += 4; + } +} + +void MOD_Poll() { + // the original code is getting the delay pattern wrong here, and + // isn't processing them as actual line delays, rather as a sort + // of ticks delay, and was basically going too fast + uint8_t newPatternDelay = MOD_PatternDelay; + if (++MOD_Tick < MOD_Speed) { + MOD_UpdateEffect(); + } else { + MOD_Tick = 0; + if (newPatternDelay-- == 0) { + MOD_UpdateRow(); + newPatternDelay = MOD_PatternDelay; + // I don't think the original code was handling this properly... + if (++MOD_CurrentRow >= 64 || MOD_ChangeRowNextTick) { + MOD_CurrentRow = 0; + if (++MOD_CurrentOrder >= MOD_SongLength) { + MOD_CurrentOrder = 0; + } + MOD_CurrentPattern = MOD_ModuleData[MOD_CurrentOrder]; + } + } else { + MOD_UpdateEffect(); + } + } + MOD_PatternDelay = newPatternDelay; +} + +void MOD_PlayNote(unsigned channel, unsigned sampleID, unsigned note, int16_t volume) { + if (volume < 0) volume = 0; + if (volume > 63) volume = 63; + struct SPUChannelData* const channelData = &s_channelData[channel]; + channelData->samplePos = 0; + SPUSetVoiceVolume(channel, volume << 8, volume << 8); + SPUSetStartAddress(channel, s_spuInstrumentData[sampleID].baseAddress << 4 + channelData->samplePos); + SPUWaitIdle(); + SPUKeyOn(1 << channel); + channelData->note = note = note + s_spuInstrumentData[sampleID].finetune * 36; + int32_t newPeriod = channelData->period = MOD_PeriodTable[note]; + SETVOICESAMPLERATE(channel, newPeriod); +} diff --git a/hello_mod/src/modplayer.h b/hello_mod/src/modplayer.h new file mode 100644 index 0000000..8a27a66 --- /dev/null +++ b/hello_mod/src/modplayer.h @@ -0,0 +1,150 @@ +/* + +MIT License + +Copyright (c) 2021 PCSX-Redux authors + +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. + +*/ + +#pragma once + +#include + +// Once MOD_Load returns, these values will be valid. +// Unless specified, consider them read only, but +// modifying them might be doable if you know what you are doing. +extern unsigned MOD_Channels; +extern unsigned MOD_SongLength; +extern unsigned MOD_CurrentOrder; +extern unsigned MOD_CurrentPattern; +extern unsigned MOD_CurrentRow; +extern unsigned MOD_Speed; +extern unsigned MOD_Tick; +extern unsigned MOD_BPM; +extern unsigned MOD_LoopStart; +extern unsigned MOD_LoopCount; +extern uint8_t MOD_PatternDelay; + +// This is a pointer to the current row that's +// being played. Used for decoding. The number +// of relevant bytes for a row is 4 * MOD_Channels. +extern const uint8_t* MOD_RowPointer; + +// These four are fine to change outside of MOD_Poll. +// The first two are booleans, and the next two are the values +// you need them to be set at when MOD_Poll is called next. +// If you need immediate row / pattern change, also set +// MOD_Tick to MOD_Speed. +extern int MOD_ChangeRowNextTick; +extern int MOD_ChangeOrderNextTick; +extern unsigned MOD_NextRow; +extern unsigned MOD_NextOrder; + +// This can be used to decode MOD_RowPointer. +extern const uint16_t MOD_PeriodTable[]; + +// Internal HIT file structure, but conformant to +// http://www.aes.id.au/modformat.html +struct MODFileFormat; + +// Returns the number of channel from this module, +// or 0 if the module is invalid. +unsigned MOD_Check(const struct MODFileFormat* module); + +// Loads the specified module and gets it ready for +// playback. Returns the number of bytes needed if +// relocation is desired. The pointer has to be +// aligned to a 4-bytes boundary. Will also setup +// the SPU. +uint32_t MOD_Load(const struct MODFileFormat* module); + +// Call this function periodically to play sound. The +// frequency at which this is called will determine the +// actual playback speed of the module. Most modules will +// not change the default tempo, which requires calling +// MOD_Poll 50 times per second, or exactly the vertical +// refresh rate in PAL. Preferably call this from timer1's +// IRQ however, and look up MOD_hblanks to decide of the +// next target value to use. +// To pause or stop playback, simply stop calling this +// function. The internal player doesn't need any +// sort of cleanup, and switching to another song simply +// requires calling MOD_Load with a new file. +void MOD_Poll(); + +// New APIs from the original code from there on. + +// Defaults to 0. This is a boolean indicating if we +// want the volume settings to be monaural or the same +// as the original Amiga's Paula chip. +extern int MOD_Stereo; + +// Indicates the number of hblank ticks to wait before +// calling MOD_Poll. This value may or may not change +// after a call to MOD_Poll, if the track requested a +// tempo change. +extern uint32_t MOD_hblanks; + +// It is possible to reclaim memory from the initial call +// to MOD_Load, in case the module was loaded from an +// external source. The number of bytes needed for the +// player will be returned by MOD_Load. Call MOD_Relocate +// with a new memory buffer that has at least this many bytes. +// Caller is responsible for managing the memory. +// It is fine to reuse the same buffer as the original input, +// if you wish to simply realloc it after relocating it, +// provided your realloc implementation guarantees that the +// shrunk buffer will remain at the same location. +// +// For example, this pseudo-code is valid: +// bool load_mod_file(File mod_file) { +// void * buffer = malloc(file_size(mod_file)); +// readfile(mod_file, buffer); +// uint32_t size = MOD_Load(buffer); +// if (size == 0) { +// free(buffer); +// return false; +// } +// MOD_Relocate(buffer); +// void * newbuffer = realloc(buffer, size); +// if (newbuffer != buffer) { +// free(newbuffer); +// return false; +// } +// return true; +// } +void MOD_Relocate(uint8_t* buffer); + +// Plays an arbitrary note from the MOD's samples bank. +// The volume will always be centered, so the sample will +// be monaural. The voiceID ideally should be set to a +// value that is less than MOD_Channels. Remember the PS1 +// has 24 channels total, so voiceID can be between 0 and 23. +// The note is a value between 0 and 35. The exact note played +// is on the normal 12-notes, C, C#, D, ... scale, and there +// are three octaves available, which gives the 12*3=36 +// interval value of the note argument. The volume argument +// is between 0 and 63. You can simulate KeyOff by simply +// setting the volume of the voice to 0. +void MOD_PlayNote(unsigned voiceID, unsigned sampleID, unsigned note, int16_t volume); + +// Added API to reset the SPU and silence everything. +void MOD_Silence();