ref: 5dca43d1a307ba6d2288c4be9ff7d3f705aaabc4
dir: /src/ft2_module_saver.c/
// for finding memory leaks in debug mode with Visual Studio
#if defined _DEBUG && defined _MSC_VER
#include <crtdbg.h>
#endif
#include <stdio.h>
#include <stdbool.h>
#include "ft2_header.h"
#include "ft2_audio.h"
#include "ft2_gui.h"
#include "ft2_mouse.h"
#include "ft2_sample_ed.h"
#include "ft2_module_loader.h"
#include "ft2_tables.h"
#include "ft2_structs.h"
static int8_t smpChunkBuf[1024];
static uint8_t packedPattData[65536], modPattData[64*32*4];
static SDL_Thread *thread;
static const char modIDs[32][5] =
{
"1CHN", "2CHN", "3CHN", "4CHN", "5CHN", "6CHN", "7CHN", "8CHN",
"9CHN", "10CH", "11CH", "12CH", "13CH", "14CH", "15CH", "16CH",
"17CH", "18CH", "19CH", "20CH", "21CH", "22CH", "23CH", "24CH",
"25CH", "26CH", "27CH", "28CH", "29CH", "30CH", "31CH", "32CH"
};
static uint16_t packPatt(uint8_t *writePtr, uint8_t *pattPtr, uint16_t numRows);
bool saveXM(UNICHAR *filenameU)
{
int16_t i, j, k, a;
size_t result;
xmHdr_t h;
xmPatHdr_t ph;
instr_t *ins;
xmInsHdr_t ih;
sample_t *s;
xmSmpHdr_t *dst;
FILE *f = UNICHAR_FOPEN(filenameU, "wb");
if (f == NULL)
{
okBoxThreadSafe(0, "System message", "Error opening file for saving, is it in use?");
return false;
}
memcpy(h.ID, "Extended Module: ", 17);
// song name
int32_t nameLength = (int32_t)strlen(song.name);
if (nameLength > 20)
nameLength = 20;
memset(h.name, ' ', 20); // yes, FT2 pads the name with spaces
if (nameLength > 0)
memcpy(h.name, song.name, nameLength);
h.x1A = 0x1A;
// program/tracker name
nameLength = (int32_t)strlen(PROG_NAME_STR);
if (nameLength > 20)
nameLength = 20;
memset(h.progName, ' ', 20); // yes, FT2 pads the name with spaces
if (nameLength > 0)
memcpy(h.progName, PROG_NAME_STR, nameLength);
h.version = 0x0104;
h.headerSize = 20 + 256;
h.numOrders = song.songLength;
h.songLoopStart = song.songLoopStart;
h.numChannels = (uint16_t)song.numChannels;
h.speed = song.speed;
h.BPM = song.BPM;
// count number of patterns
i = MAX_PATTERNS;
do
{
if (patternEmpty(i-1))
i--;
else
break;
}
while (i > 0);
h.numPatterns = i;
// count number of instruments
i = 128;
while (i > 0 && getUsedSamples(i) == 0 && song.instrName[i][0] == '\0')
i--;
h.numInstr = i;
h.flags = audio.linearPeriodsFlag;
memcpy(h.orders, song.orders, 256);
if (fwrite(&h, sizeof (h), 1, f) != 1)
{
fclose(f);
okBoxThreadSafe(0, "System message", "Error saving module: general I/O error!");
return false;
}
for (i = 0; i < h.numPatterns; i++)
{
if (patternEmpty(i))
{
if (pattern[i] != NULL)
{
free(pattern[i]);
pattern[i] = NULL;
}
patternNumRows[i] = 64;
}
ph.headerSize = sizeof (xmPatHdr_t);
ph.numRows = patternNumRows[i];
ph.type = 0;
if (pattern[i] == NULL)
{
ph.dataSize = 0;
if (fwrite(&ph, ph.headerSize, 1, f) != 1)
{
fclose(f);
okBoxThreadSafe(0, "System message", "Error saving module: general I/O error!");
return false;
}
}
else
{
ph.dataSize = packPatt(packedPattData, (uint8_t *)pattern[i], patternNumRows[i]);
result = fwrite(&ph, ph.headerSize, 1, f);
result += fwrite(packedPattData, ph.dataSize, 1, f);
if (result != 2) // write was not OK
{
fclose(f);
okBoxThreadSafe(0, "System message", "Error saving module: general I/O error!");
return false;
}
}
}
memset(&ih, 0, sizeof (ih)); // important, clears reserved stuff
for (i = 1; i <= h.numInstr; i++)
{
if (instr[i] == NULL)
j = 0;
else
j = i;
a = getUsedSamples(i);
nameLength = (int32_t)strlen(song.instrName[i]);
if (nameLength > 22)
nameLength = 22;
memset(ih.name, 0, 22); // pad with zero
if (nameLength > 0)
memcpy(ih.name, song.instrName[i], nameLength);
ih.type = 0;
ih.numSamples = a;
ih.sampleSize = sizeof (xmSmpHdr_t);
if (a > 0)
{
ins = instr[j];
memcpy(ih.note2SampleLUT, ins->note2SampleLUT, 96);
memcpy(ih.volEnvPoints, ins->volEnvPoints, 12*2*sizeof(int16_t));
memcpy(ih.panEnvPoints, ins->panEnvPoints, 12*2*sizeof(int16_t));
ih.volEnvLength = ins->volEnvLength;
ih.panEnvLength = ins->panEnvLength;
ih.volEnvSustain = ins->volEnvSustain;
ih.volEnvLoopStart = ins->volEnvLoopStart;
ih.volEnvLoopEnd = ins->volEnvLoopEnd;
ih.panEnvSustain = ins->panEnvSustain;
ih.panEnvLoopStart = ins->panEnvLoopStart;
ih.panEnvLoopEnd = ins->panEnvLoopEnd;
ih.volEnvFlags = ins->volEnvFlags;
ih.panEnvFlags = ins->panEnvFlags;
ih.vibType = ins->vibType;
ih.vibSweep = ins->vibSweep;
ih.vibDepth = ins->vibDepth;
ih.vibRate = ins->vibRate;
ih.fadeout = ins->fadeout;
ih.midiOn = ins->midiOn ? 1 : 0;
ih.midiChannel = ins->midiChannel;
ih.midiProgram = ins->midiProgram;
ih.midiBend = ins->midiBend;
ih.mute = ins->mute ? 1 : 0;
ih.instrSize = INSTR_HEADER_SIZE;
for (k = 0; k < a; k++)
{
s = &instr[j]->smp[k];
dst = &ih.smp[k];
bool sample16Bit = !!(s->flags & SAMPLE_16BIT);
dst->length = s->length;
dst->loopStart = s->loopStart;
dst->loopLength = s->loopLength;
if (sample16Bit)
{
dst->length <<= 1;
dst->loopStart <<= 1;
dst->loopLength <<= 1;
}
dst->volume = s->volume;
dst->finetune = s->finetune;
dst->flags = s->flags;
dst->panning = s->panning;
dst->relativeNote = s->relativeNote;
nameLength = (int32_t)strlen(s->name);
if (nameLength > 22)
nameLength = 22;
dst->nameLength = (uint8_t)nameLength;
memset(dst->name, ' ', 22); // yes, FT2 pads the name with spaces
if (nameLength > 0)
memcpy(dst->name, s->name, nameLength);
if (s->dataPtr == NULL)
dst->length = 0;
}
}
else
{
ih.instrSize = 22 + 11;
}
if (fwrite(&ih, ih.instrSize + (a * sizeof (xmSmpHdr_t)), 1, f) != 1)
{
fclose(f);
okBoxThreadSafe(0, "System message", "Error saving module: general I/O error!");
return false;
}
for (k = 1; k <= a; k++)
{
s = &instr[j]->smp[k-1];
if (s->dataPtr != NULL)
{
unfixSample(s);
samp2Delta(s->dataPtr, s->length, s->flags);
result = fwrite(s->dataPtr, 1, SAMPLE_LENGTH_BYTES(s), f);
delta2Samp(s->dataPtr, s->length, s->flags);
fixSample(s);
if (result != (size_t)SAMPLE_LENGTH_BYTES(s)) // write not OK
{
fclose(f);
okBoxThreadSafe(0, "System message", "Error saving module: general I/O error!");
return false;
}
}
}
}
removeSongModifiedFlag();
fclose(f);
editor.diskOpReadDir = true; // force diskop re-read
setMouseBusy(false);
return true;
}
static bool saveMOD(UNICHAR *filenameU)
{
int16_t i;
int32_t j, k;
instr_t *ins;
sample_t *smp;
modHdr_t hdr;
if (audio.linearPeriodsFlag)
okBoxThreadSafe(0, "System message", "Warning: Amiga frequency table isn't used!");
int32_t songLength = song.songLength;
if (songLength > 128)
{
songLength = 128;
okBoxThreadSafe(0, "System message", "Warning: Song length is above 128!");
}
// calculate number of patterns referenced (max 128 orders)
int32_t numPatterns = 0;
for (i = 0; i < songLength; i++)
{
if (song.orders[i] > numPatterns)
numPatterns = song.orders[i];
}
numPatterns++;
if (numPatterns > 100)
{
numPatterns = 100;
okBoxThreadSafe(0, "System message", "Warning: Song has more than 100 patterns!");
}
// check if song has more than 31 instruments
for (i = 32; i <= 128; i++)
{
if (getRealUsedSamples(i) > 0)
{
okBoxThreadSafe(0, "System message", "Warning: Song has more than 31 instruments!");
break;
}
}
// check if the first 31 samples have a length above 65534 samples
bool test = false;
bool test2 = false;
for (i = 1; i <= 31; i++)
{
ins = instr[i];
if (ins == NULL)
continue;
smp = &ins->smp[0];
if (smp->length > 131070)
test = true;
else if (smp->length > 65534)
test2 = true;
}
if (test) okBoxThreadSafe(0, "System message", "Warning: Song has sample lengths that are too long for the MOD format!");
else if (test2) okBoxThreadSafe(0, "System message", "Warning: Song has sample lengths above 65534! Not all MOD players support this.");
// check if XM instrument features are being used
test = false;
for (i = 1; i <= 31; i++)
{
ins = instr[i];
if (ins == NULL)
continue;
smp = &ins->smp[0];
j = getRealUsedSamples(i);
if (j > 1)
{
test = true;
break;
}
if (j == 1)
{
if (ins->fadeout != 0 || ins->volEnvFlags != 0 || ins->panEnvFlags != 0 || ins->vibRate > 0 ||
GET_LOOPTYPE(smp->flags) == LOOP_BIDI || smp->relativeNote != 0 || ins->midiOn)
{
test = true;
break;
}
}
}
if (test) okBoxThreadSafe(0, "System message", "Warning: Song is using XM instrument features!");
bool tooLongPatterns = false;
bool tooManyInstr = false;
bool incompatEfx = false;
bool noteUnderflow = false;
for (i = 0; i < numPatterns; i++)
{
if (pattern[i] == NULL)
continue;
if (patternNumRows[i] < 64)
{
okBoxThreadSafe(0, "System message", "Error: Pattern lengths can't be below 64! Module wasn't saved.");
return false;
}
if (patternNumRows[i] > 64)
tooLongPatterns = true;
for (j = 0; j < 64; j++)
{
for (k = 0; k < song.numChannels; k++)
{
note_t *p = &pattern[i][(j * MAX_CHANNELS) + k];
if (p->instr > 31)
tooManyInstr = true;
if (p->efx > 0xF || p->vol != 0)
incompatEfx = true;
// added security that wasn't present in FT2
if (p->note > 0 && p->note < 10)
noteUnderflow = true;
}
}
}
if (tooLongPatterns) okBoxThreadSafe(0, "System message", "Warning: Song has pattern lengths above 64!");
if (tooManyInstr) okBoxThreadSafe(0, "System message", "Warning: Patterns have instrument numbers above 31!");
if (incompatEfx) okBoxThreadSafe(0, "System message", "Warning: Patterns have incompatible effects!");
if (noteUnderflow) okBoxThreadSafe(0, "System message", "Warning: Patterns have notes below A-0!");
// save module now
memset(&hdr, 0, sizeof (hdr));
// song name
int32_t nameLength = (int32_t)strlen(song.name);
if (nameLength > 20)
nameLength = 20;
memset(hdr.name, 0, 20); // pad with zeroes
if (nameLength > 0)
memcpy(hdr.name, song.name, nameLength);
hdr.numOrders = (uint8_t)songLength; // pre-clamped to 0..128
hdr.songLoopStart = (uint8_t)song.songLoopStart;
if (hdr.songLoopStart >= hdr.numOrders) // repeat-point must be lower than the song length
hdr.songLoopStart = 0;
memcpy(hdr.orders, song.orders, hdr.numOrders);
if (song.numChannels == 4)
memcpy(hdr.ID, (numPatterns > 64) ? "M!K!" : "M.K.", 4);
else
memcpy(hdr.ID, modIDs[song.numChannels-1], 4);
// fill MOD sample headers
for (i = 1; i <= 31; i++)
{
modSmpHdr_t *modSmp = &hdr.smp[i-1];
nameLength = (int32_t)strlen(song.instrName[i]);
if (nameLength > 22)
nameLength = 22;
memset(modSmp->name, 0, 22); // pad with zeroes
if (nameLength > 0)
memcpy(modSmp->name, song.instrName[i], nameLength);
if (instr[i] != NULL && getRealUsedSamples(i) != 0)
{
smp = &instr[i]->smp[0];
int32_t length = smp->length >> 1;
int32_t loopStart = smp->loopStart >> 1;
int32_t loopLength = smp->loopLength >> 1;
// length/loopStart/loopLength are now in units of words
if (length > UINT16_MAX)
length = UINT16_MAX;
if (GET_LOOPTYPE(smp->flags) == LOOP_OFF)
{
loopStart = 0;
loopLength = 1;
}
else // looped sample
{
if (loopLength == 0) // ProTracker hates loopLengths of zero
loopLength = 1;
if (loopStart+loopLength > length)
{
loopStart = 0;
loopLength = 1;
}
}
modSmp->length = (uint16_t)SWAP16(length);
modSmp->finetune = FINETUNE_XM2MOD(smp->finetune);
modSmp->volume = smp->volume;
modSmp->loopStart = (uint16_t)SWAP16(loopStart);
modSmp->loopLength = (uint16_t)SWAP16(loopLength);
}
}
FILE *f = UNICHAR_FOPEN(filenameU, "wb");
if (f == NULL)
{
okBoxThreadSafe(0, "System message", "Error opening file for saving, is it in use?");
return false;
}
// write header
if (fwrite(&hdr, 1, sizeof (hdr), f) != sizeof (hdr))
{
okBoxThreadSafe(0, "System message", "Error saving module: general I/O error!");
goto modSaveError;
}
// write pattern data
const int32_t patternBytes = song.numChannels * 64 * 4;
for (i = 0; i < numPatterns; i++)
{
if (pattern[i] == NULL) // empty pattern
{
memset(modPattData, 0, patternBytes);
}
else
{
int32_t offs = 0;
for (j = 0; j < 64; j++)
{
for (k = 0; k < song.numChannels; k++)
{
note_t *p = &pattern[i][(j * MAX_CHANNELS) + k];
uint8_t inst = p->instr;
uint8_t note = p->note;
// FT2 bugfix: prevent overflow
if (inst > 31)
inst = 0;
// FT2 bugfix: convert note-off into no note for MOD saving
if (note == NOTE_OFF)
note = 0;
// FT2 bugfix: clamp notes below 10 (A-0) to prevent 12-bit period overflow
if (note > 0 && note < 10)
note = 10;
if (note == 0)
{
modPattData[offs+0] = inst & 0xF0;
modPattData[offs+1] = 0;
}
else
{
modPattData[offs+0] = (inst & 0xF0) | ((amigaPeriod[note-1] >> 8) & 0x0F);
modPattData[offs+1] = amigaPeriod[note-1] & 0xFF;
}
// FT2 bugfix: if effect is overflowing (0xF in .MOD), set effect and param to 0
if (p->efx > 0x0F)
{
modPattData[offs+2] = (inst & 0x0F) << 4;
modPattData[offs+3] = 0;
}
else
{
modPattData[offs+2] = ((inst & 0x0F) << 4) | (p->efx & 0x0F);
modPattData[offs+3] = p->efxData;
}
offs += 4;
}
}
}
if (fwrite(modPattData, 1, patternBytes, f) != (size_t)patternBytes)
{
okBoxThreadSafe(0, "System message", "Error saving module: general I/O error!");
goto modSaveError;
}
}
// write sample data
for (i = 1; i <= 31; i++)
{
if (instr[i] == NULL || getRealUsedSamples(i) == 0)
continue;
smp = &instr[i]->smp[0];
if (smp->dataPtr == NULL || smp->length <= 0)
continue;
modSmpHdr_t *modSmp = &hdr.smp[i-1];
unfixSample(smp);
int32_t sampleBytes = SWAP16(modSmp->length) * 2;
if (smp->flags & SAMPLE_16BIT) // 16-bit sample (convert to 8-bit)
{
int8_t *dstPtr = (int8_t *)smpChunkBuf;
int32_t writeLen = sampleBytes;
int32_t samplesWritten = 0;
while (samplesWritten < writeLen) // write in chunks
{
int32_t samplesToWrite = sizeof (smpChunkBuf);
if (samplesWritten+samplesToWrite > writeLen)
samplesToWrite = writeLen - samplesWritten;
int16_t *srcPtr16 = (int16_t *)smp->dataPtr + samplesWritten;
for (j = 0; j < samplesToWrite; j++)
dstPtr[j] = srcPtr16[j] >> 8; // convert 16-bit to 8-bit
if (fwrite(dstPtr, 1, samplesToWrite, f) != (size_t)samplesToWrite)
{
fixSample(smp);
okBoxThreadSafe(0, "System message", "Error saving module: general I/O error!");
goto modSaveError;
}
samplesWritten += samplesToWrite;
}
}
else // 8-bit sample
{
if (fwrite(smp->dataPtr, 1, sampleBytes, f) != (size_t)sampleBytes)
{
fixSample(smp);
okBoxThreadSafe(0, "System message", "Error saving module: general I/O error!");
goto modSaveError;
}
}
fixSample(smp);
}
fclose(f);
removeSongModifiedFlag();
editor.diskOpReadDir = true; // force diskop re-read
setMouseBusy(false);
return true;
modSaveError:
fclose(f);
return false;
}
static int32_t SDLCALL saveMusicThread(void *ptr)
{
assert(editor.tmpFilenameU != NULL);
if (editor.tmpFilenameU == NULL)
return false;
pauseAudio();
if (editor.moduleSaveMode == 1)
saveXM(editor.tmpFilenameU);
else
saveMOD(editor.tmpFilenameU);
resumeAudio();
return true;
(void)ptr;
}
void saveMusic(UNICHAR *filenameU)
{
UNICHAR_STRCPY(editor.tmpFilenameU, filenameU);
mouseAnimOn();
thread = SDL_CreateThread(saveMusicThread, NULL, NULL);
if (thread == NULL)
{
okBoxThreadSafe(0, "System message", "Couldn't create thread!");
return;
}
SDL_DetachThread(thread);
}
static uint16_t packPatt(uint8_t *writePtr, uint8_t *pattPtr, uint16_t numRows)
{
uint8_t bytes[5];
if (pattPtr == NULL)
return 0;
uint16_t totalPackLen = 0;
const int32_t pitch = sizeof (note_t) * (MAX_CHANNELS - song.numChannels);
for (int32_t row = 0; row < numRows; row++)
{
for (int32_t chn = 0; chn < song.numChannels; chn++)
{
bytes[0] = *pattPtr++;
bytes[1] = *pattPtr++;
bytes[2] = *pattPtr++;
bytes[3] = *pattPtr++;
bytes[4] = *pattPtr++;
uint8_t *firstBytePtr = writePtr++;
uint8_t packBits = 0;
if (bytes[0] > 0) { packBits |= 1; *writePtr++ = bytes[0]; } // note
if (bytes[1] > 0) { packBits |= 2; *writePtr++ = bytes[1]; } // instrument
if (bytes[2] > 0) { packBits |= 4; *writePtr++ = bytes[2]; } // volume column
if (bytes[3] > 0) { packBits |= 8; *writePtr++ = bytes[3]; } // effect
if (packBits == 15) // first four bits set?
{
// no packing needed, write pattern data as is
// point to first byte (and overwrite data)
writePtr = firstBytePtr;
*writePtr++ = bytes[0];
*writePtr++ = bytes[1];
*writePtr++ = bytes[2];
*writePtr++ = bytes[3];
*writePtr++ = bytes[4];
totalPackLen += 5;
continue;
}
if (bytes[4] > 0) { packBits |= 16; *writePtr++ = bytes[4]; } // effect parameter
*firstBytePtr = packBits | 128; // write pack bits byte
totalPackLen += (uint16_t)(writePtr - firstBytePtr); // bytes writen
}
// skip unused channels (unpacked patterns always have 32 channels)
pattPtr += pitch;
}
return totalPackLen;
}