ft2-clone

Fasttracker 2 clone
Log | Files | Refs | README | LICENSE

commit 8391cc5ac8ad74fa3d08c7228cb4a30ac657c56f
parent 1533d894234ecef766ee830d923c58bf0c8cda9b
Author: Olav Sørensen <olav.sorensen@live.no>
Date:   Fri, 23 Feb 2024 17:24:46 +0100

Add support for loading .BEM files (UN05, XM only)

Diffstat:
Msrc/ft2_module_loader.c | 13+++++++++++--
Asrc/modloaders/ft2_load_bem.c | 404+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mvs2019_project/ft2-clone/ft2-clone.vcxproj | 1+
Mvs2019_project/ft2-clone/ft2-clone.vcxproj.filters | 3+++
4 files changed, 419 insertions(+), 2 deletions(-)

diff --git a/src/ft2_module_loader.c b/src/ft2_module_loader.c @@ -26,6 +26,9 @@ #include "ft2_structs.h" #include "ft2_sysreqs.h" +bool detectBEM(FILE* f); +bool loadBEM(FILE* f, uint32_t filesize); + bool loadDIGI(FILE *f, uint32_t filesize); bool loadMOD(FILE *f, uint32_t filesize); bool loadS3M(FILE *f, uint32_t filesize); @@ -41,14 +44,15 @@ enum FORMAT_MOD = 3, FORMAT_S3M = 4, FORMAT_STM = 5, - FORMAT_DIGI = 6 + FORMAT_DIGI = 6, + FORMAT_BEM = 7 }; // file extensions accepted by Disk Op. in module mode char *supportedModExtensions[] = { "xm", "ft", "nst", "stk", "mod", "s3m", "stm", "fst", - "digi", + "digi", "bem", // IMPORTANT: Remember comma after last entry above "END_OF_LIST" // do NOT move, remove or edit this line! @@ -84,6 +88,10 @@ static int8_t detectModule(FILE *f) fread(I, 1, 4, f); rewind(f); + // BEM ("UN05", from XM only, MikMod) + if (detectBEM(f)) + return FORMAT_BEM; + // DIGI Booster (non-Pro) if (!memcmp("DIGI Booster module", &D[0x00], 19+1) && D[0x19] >= 1 && D[0x19] <= 8) return FORMAT_DIGI; @@ -193,6 +201,7 @@ static bool doLoadMusic(bool externalThreadFlag) case FORMAT_MOD: moduleLoaded = loadMOD(f, filesize); break; case FORMAT_POSSIBLY_STK: moduleLoaded = loadSTK(f, filesize); break; case FORMAT_DIGI: moduleLoaded = loadDIGI(f, filesize); break; + case FORMAT_BEM: moduleLoaded = loadBEM(f, filesize); break; default: loaderMsgBox("This file is not a supported module!"); diff --git a/src/modloaders/ft2_load_bem.c b/src/modloaders/ft2_load_bem.c @@ -0,0 +1,404 @@ +/* BEM (UN05, MikMod) loader. Supports modules converted from XM only! +** +** Note: Data sanitation is done in the last stage +** of module loading, so you don't need to do that here. +*/ + +#include <stdio.h> +#include <stdint.h> +#include <stdbool.h> +#include "../ft2_header.h" +#include "../ft2_module_loader.h" +#include "../ft2_sample_ed.h" +#include "../ft2_sysreqs.h" + +#define MAX_TRACKS (256*32) + +#define FLAG_XMPERIODS 1 +#define FLAG_LINEARSLIDES 2 + +#ifdef _MSC_VER // please don't mess with these structs! +#pragma pack(push) +#pragma pack(1) +#endif +typedef struct bemHdr_t +{ + char id[4]; + uint8_t numchn; + uint16_t numpos; + uint16_t reppos; + uint16_t numpat; + uint16_t numtrk; + uint16_t numins; + uint8_t initspeed; + uint8_t inittempo; + uint8_t positions[256]; + uint8_t panning[32]; + uint8_t flags; +} +#ifdef __GNUC__ +__attribute__((packed)) +#endif +bemHdr_t; +#ifdef _MSC_VER +#pragma pack(pop) +#endif + +enum +{ + UNI_NOTE = 1, + UNI_INSTRUMENT, + UNI_PTEFFECT0, + UNI_PTEFFECT1, + UNI_PTEFFECT2, + UNI_PTEFFECT3, + UNI_PTEFFECT4, + UNI_PTEFFECT5, + UNI_PTEFFECT6, + UNI_PTEFFECT7, + UNI_PTEFFECT8, + UNI_PTEFFECT9, + UNI_PTEFFECTA, + UNI_PTEFFECTB, + UNI_PTEFFECTC, + UNI_PTEFFECTD, + UNI_PTEFFECTE, + UNI_PTEFFECTF, + UNI_S3MEFFECTA, + UNI_S3MEFFECTD, + UNI_S3MEFFECTE, + UNI_S3MEFFECTF, + UNI_S3MEFFECTI, + UNI_S3MEFFECTQ, + UNI_S3MEFFECTT, + UNI_XMEFFECTA, + UNI_XMEFFECTG, + UNI_XMEFFECTH, + UNI_XMEFFECTP, + + UNI_LAST +}; + +static const uint8_t xmEfxTab[] = { 10, 16, 17, 25 }; // A, G, H, P + +static char *readString(FILE *f) +{ + uint16_t length; + fread(&length, 2, 1, f); + + char *out = (char *)malloc(length+1); + if (out == NULL) + return NULL; + + fread(out, 1, length, f); + out[length] = '\0'; + + return out; +} + +bool detectBEM(FILE *f) +{ + if (f == NULL) return false; + + uint32_t oldPos = (uint32_t)ftell(f); + + fseek(f, 0, SEEK_SET); + char ID[64]; + memset(ID, 0, sizeof (ID)); + fread(ID, 1, 4, f); + if (memcmp(ID, "UN05", 4) != 0) + goto error; + + fseek(f, 0x131, SEEK_SET); + if (feof(f)) + goto error; + + uint8_t flags = (uint8_t)fgetc(f); + if ((flags & FLAG_XMPERIODS) == 0) + goto error; + + fseek(f, 0x132, SEEK_SET); + if (feof(f)) + goto error; + + uint16_t strLength = 0; + fread(&strLength, 2, 1, f); + if (strLength == 0 || strLength > 512) + goto error; + + fseek(f, strLength+2, SEEK_CUR); + if (feof(f)) + goto error; + + fread(ID, 1, 64, f); + if (memcmp(ID, "FastTracker v2.00", 17) != 0) + goto error; + + fseek(f, oldPos, SEEK_SET); + return true; + +error: + fseek(f, oldPos, SEEK_SET); + return false; +} + +bool loadBEM(FILE *f, uint32_t filesize) +{ + bemHdr_t h; + + if (filesize < sizeof (h)) + { + loaderMsgBox("Error: This file is either not a module, or is not supported."); + return false; + } + + fread(&h, 1, sizeof (bemHdr_t), f); + + char *songName = readString(f); + if (songName == NULL) + return false; + + strcpy(songTmp.name, songName); + free(songName); + uint16_t strLength; + fread(&strLength, 2, 1, f); + fseek(f, strLength, SEEK_CUR); + fread(&strLength, 2, 1, f); + fseek(f, strLength, SEEK_CUR); + + if (h.numpos > 256 || h.numpat > 256 || h.numchn > 32 || h.numtrk > MAX_TRACKS) + { + loaderMsgBox("Error loading BEM: The module is corrupt!"); + return false; + } + + tmpLinearPeriodsFlag = !!(h.flags & FLAG_LINEARSLIDES); + + songTmp.numChannels = h.numchn; + songTmp.songLength = h.numpos; + songTmp.songLoopStart = h.reppos; + songTmp.BPM = h.inittempo; + songTmp.speed = h.initspeed; + + memcpy(songTmp.orders, h.positions, 256); + + // load instruments + for (int16_t i = 0; i < h.numins; i++) + { + if (!allocateTmpInstr(1 + i)) + { + loaderMsgBox("Not enough memory!"); + return false; + } + + instr_t *ins = instrTmp[1 + i]; + + ins->numSamples = (uint8_t)fgetc(f); + fread(ins->note2SampleLUT, 1, 96, f); + + ins->volEnvFlags = (uint8_t)fgetc(f); + ins->volEnvLength = (uint8_t)fgetc(f); + ins->volEnvSustain = (uint8_t)fgetc(f); + ins->volEnvLoopStart = (uint8_t)fgetc(f); + ins->volEnvLoopEnd = (uint8_t)fgetc(f); + fread(ins->volEnvPoints, 2, 12*2, f); + + ins->panEnvFlags = (uint8_t)fgetc(f); + ins->panEnvLength = (uint8_t)fgetc(f); + ins->panEnvSustain = (uint8_t)fgetc(f); + ins->panEnvLoopStart = (uint8_t)fgetc(f); + ins->panEnvLoopEnd = (uint8_t)fgetc(f); + fread(ins->panEnvPoints, 2, 12*2, f); + + ins->autoVibType = (uint8_t)fgetc(f); + ins->autoVibSweep = (uint8_t)fgetc(f); + ins->autoVibDepth = (uint8_t)fgetc(f); + ins->autoVibRate = (uint8_t)fgetc(f); + fread(&ins->fadeout, 2, 1, f); + + char *insName = readString(f); + if (insName == NULL) + return false; + + uint32_t insNameLen = (uint32_t)strlen(insName); + if (insNameLen > 22) + insNameLen = 22; + + memcpy(songTmp.instrName[1+i], insName, insNameLen); + free(insName); + + for (int32_t j = 0; j < ins->numSamples; j++) + { + sample_t *s = &ins->smp[j]; + + s->finetune = (int8_t)fgetc(f) ^ 0x80; + fseek(f, 1, SEEK_CUR); + s->relativeNote = (int8_t)fgetc(f); + s->volume = (uint8_t)fgetc(f); + s->panning = (uint8_t)fgetc(f); + fread(&s->length, 4, 1, f); + fread(&s->loopStart, 4, 1, f); + uint32_t loopEnd; + fread(&loopEnd, 4, 1, f); + s->loopLength = loopEnd - s->loopStart; + + uint16_t flags; + fread(&flags, 2, 1, f); + if (flags & 1) s->flags |= SAMPLE_16BIT; + if (flags & 16) s->flags |= LOOP_FWD; + if (flags & 32) s->flags |= LOOP_BIDI; + + char *smpName = readString(f); + if (smpName == NULL) + return false; + + uint32_t smpNameLen = (uint32_t)strlen(smpName); + if (smpNameLen > 22) + smpNameLen = 22; + + memcpy(s->name, smpName, smpNameLen); + free(smpName); + } + } + + // load tracks + + uint16_t rowsInPattern[256]; + uint16_t trackList[256*32]; + fread(rowsInPattern, 2, h.numpat, f); + fread(trackList, 2, h.numpat * h.numchn, f); + + note_t *decodedTrack[MAX_TRACKS]; + for (int32_t i = 0; i < h.numtrk; i++) + { + uint16_t trackBytesInFile; + fread(&trackBytesInFile, 2, 1, f); + if (trackBytesInFile == 0) + { + loaderMsgBox("Error loading BEM: This module is corrupt!"); +trackError: + for (int32_t j = 0; j < i; j++) + free(decodedTrack[j]); + + return false; + } + + decodedTrack[i] = (note_t *)calloc(rowsInPattern[i], sizeof (note_t)); + if (decodedTrack[i] == NULL) + { + loaderMsgBox("Not enough memory!"); + goto trackError; + } + + note_t *out = decodedTrack[i]; + + // decode track + + uint32_t trackPosInFile = (uint32_t)ftell(f); + while ((uint32_t)ftell(f) < trackPosInFile+trackBytesInFile) + { + uint8_t byte = (uint8_t)fgetc(f); + if (byte == 0) + break; // end of track + + uint8_t repeat = byte >> 5; + uint8_t opcodeBytes = (byte & 0x1F) - 1; + + uint32_t opcodeStart = (uint32_t)ftell(f); + uint32_t opcodeEnd = opcodeStart + opcodeBytes; + + for (int32_t j = 0; j <= repeat; j++, out++) + { + fseek(f, opcodeStart, SEEK_SET); + while ((uint32_t)ftell(f) < opcodeEnd) + { + uint8_t opcode = (uint8_t)fgetc(f); + + if (opcode == 0) + break; + + if (opcode == UNI_NOTE) + { + out->note = 1 + (uint8_t)fgetc(f); + } + else if (opcode == UNI_INSTRUMENT) + { + out->instr = 1 + (uint8_t)fgetc(f); + } + else if (opcode >= UNI_PTEFFECT0 && opcode <= UNI_PTEFFECTF) // PT effects + { + out->efx = opcode - UNI_PTEFFECT0; + out->efxData = (uint8_t)fgetc(f); + } + else if (opcode >= UNI_XMEFFECTA && opcode <= UNI_XMEFFECTP) // XM effects + { + out->efx = xmEfxTab[opcode-UNI_XMEFFECTA]; + out->efxData = (uint8_t)fgetc(f); + } + else + { + if (opcode >= UNI_LAST) // illegal opcode + { + loaderMsgBox("Error loading BEM: illegal pattern opcode!"); + goto trackError; + } + + // unsupported opcode, skip it + if (opcode > 0) + fseek(f, 1, SEEK_CUR); + } + } + } + } + } + + // create patterns from tracks + for (int32_t i = 0; i < h.numpat; i++) + { + uint16_t numRows = rowsInPattern[i]; + if (numRows == 0 || numRows > 256) + continue; + + if (!allocateTmpPatt(i, numRows)) + { + loaderMsgBox("Not enough memory!"); + return false; + } + + note_t *dst = patternTmp[i]; + for (int32_t j = 0; j < h.numchn; j++) + { + note_t *src = (note_t *)decodedTrack[trackList[(i * h.numchn) + j]]; + if (src != NULL) + { + for (int32_t k = 0; k < numRows; k++) + dst[(k * MAX_CHANNELS) + j] = src[k]; + } + } + } + + // load sample data + for (int32_t i = 0; i < h.numins; i++) + { + instr_t *ins = instrTmp[1 + i]; + if (ins == NULL) + continue; + + for (int32_t j = 0; j < ins->numSamples; j++) + { + sample_t *s = &ins->smp[j]; + + bool sampleIs16Bit = !!(s->flags & SAMPLE_16BIT); + if (!allocateSmpData(s, s->length, sampleIs16Bit)) + { + loaderMsgBox("Not enough memory!"); + return false; + } + + fread(s->dataPtr, 1 + sampleIs16Bit, s->length, f); + delta2Samp(s->dataPtr, s->length, s->flags); + } + } + + return true; +} diff --git a/vs2019_project/ft2-clone/ft2-clone.vcxproj b/vs2019_project/ft2-clone/ft2-clone.vcxproj @@ -361,6 +361,7 @@ <ClCompile Include="..\..\src\modloaders\ft2_load_s3m.c" /> <ClCompile Include="..\..\src\modloaders\ft2_load_stk.c" /> <ClCompile Include="..\..\src\modloaders\ft2_load_stm.c" /> + <ClCompile Include="..\..\src\modloaders\ft2_load_bem.c" /> <ClCompile Include="..\..\src\modloaders\ft2_load_xm.c" /> <ClCompile Include="..\..\src\rtmidi\RtMidi.cpp"> <ExceptionHandling Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Sync</ExceptionHandling> diff --git a/vs2019_project/ft2-clone/ft2-clone.vcxproj.filters b/vs2019_project/ft2-clone/ft2-clone.vcxproj.filters @@ -164,6 +164,9 @@ <ClCompile Include="..\..\src\mixer\ft2_cubic_spline.c"> <Filter>mixer</Filter> </ClCompile> + <ClCompile Include="..\..\src\modloaders\ft2_load_bem.c"> + <Filter>modloaders</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <ClInclude Include="..\..\src\rtmidi\RtMidi.h">