joeylib2/tools/modloopend/modloopend.c

221 lines
6.9 KiB
C

// Append a one-shot stop marker to a Protracker .MOD file so the Amiga
// audio HAL (PTPlayer-based) can honor `joeyAudioPlayMod(loop=false)`.
//
// PTPlayer has no built-in song-end signal -- songs loop forever
// unless an `E8FF` effect command is encountered, which latches into
// the public `mt_E8Trigger` byte. Our halAudioFrameTick polls that
// byte and clears mt_Enable when the marker fires, but the marker has
// to actually be IN the .MOD for the polling to ever match.
//
// This tool injects a new pattern at the end of the song that
// contains exactly one E8FF effect on row 0 and is otherwise empty.
// The order table is extended to play that pattern once after the
// existing song. Result: the song plays normally, then the marker
// pattern fires, mt_E8Trigger goes 0xFF, and PlayMod(loop=false)
// stops cleanly. Other replayers (libxmp on DOS/ST, NTP on IIgs)
// either ignore E8FF or treat it as a no-op filter command -- on
// those ports the engine's own song-end detection is what stops the
// song, so the injected marker is harmless there.
//
// Usage: modloopend input.mod output.mod
//
// The existing pattern data is not modified -- only the order table,
// song length, and a new pattern at the end are written.
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MOD_HEADER_SIZE 1084
#define MOD_SONG_LENGTH_OFFSET 950
#define MOD_RESTART_OFFSET 951
#define MOD_ORDER_TABLE_OFFSET 952
#define MOD_ORDER_TABLE_LEN 128
#define MOD_MAGIC_OFFSET 1080
#define MOD_MAGIC_LEN 4
#define MOD_PATTERN_BYTES 1024
#define MOD_MAX_PATTERNS 128
#define MOD_MIN_BYTES (MOD_HEADER_SIZE + MOD_PATTERN_BYTES)
// E8FF on row 0, channel 0. Per Protracker pattern row layout:
// byte 0 (SSSSPPPP): sample hi nibble + period hi nibble = 0x00
// byte 1 (PPPPPPPP): period lo byte = 0x00
// byte 2 (SSSSEEEE): sample lo nibble + effect cmd = 0x0E
// byte 3 (PPPPPPPP): effect param = 0xFF
#define E8FF_BYTE_0 0x00
#define E8FF_BYTE_1 0x00
#define E8FF_BYTE_2 0x0E
#define E8FF_BYTE_3 0xFF
// ----- Prototypes -----
static bool magicMatches(const uint8_t *header);
static int process(const char *inPath, const char *outPath);
// ----- Internal helpers (alphabetical) -----
static bool magicMatches(const uint8_t *header) {
static const char *known[] = {
"M.K.", "M!K!", "FLT4", "FLT8", "6CHN", "8CHN", "CD81", "OKTA", "OCTA"
};
size_t i;
for (i = 0; i < sizeof(known) / sizeof(known[0]); i++) {
if (memcmp(header + MOD_MAGIC_OFFSET, known[i], MOD_MAGIC_LEN) == 0) {
return true;
}
}
return false;
}
static int process(const char *inPath, const char *outPath) {
FILE *fp;
uint8_t *buf;
long fileSize;
size_t readBytes;
size_t newSize;
int i;
int songLength;
int maxPattern;
int patternCount;
long samplesOffset;
long samplesLen;
uint8_t *newBuf;
uint8_t *newPatternStart;
fp = fopen(inPath, "rb");
if (fp == NULL) {
fprintf(stderr, "modloopend: cannot open %s\n", inPath);
return 2;
}
if (fseek(fp, 0, SEEK_END) != 0) {
fprintf(stderr, "modloopend: seek failed on %s\n", inPath);
fclose(fp);
return 2;
}
fileSize = ftell(fp);
if (fileSize < MOD_MIN_BYTES) {
fprintf(stderr, "modloopend: %s is %ld bytes, too small for a MOD\n", inPath, fileSize);
fclose(fp);
return 2;
}
rewind(fp);
buf = (uint8_t *)malloc((size_t)fileSize);
if (buf == NULL) {
fprintf(stderr, "modloopend: out of memory\n");
fclose(fp);
return 2;
}
readBytes = fread(buf, 1, (size_t)fileSize, fp);
fclose(fp);
if (readBytes != (size_t)fileSize) {
fprintf(stderr, "modloopend: short read on %s\n", inPath);
free(buf);
return 2;
}
if (!magicMatches(buf)) {
fprintf(stderr, "modloopend: %s does not have a Protracker magic at offset 1080\n", inPath);
free(buf);
return 2;
}
songLength = buf[MOD_SONG_LENGTH_OFFSET];
if (songLength <= 0 || songLength >= MOD_ORDER_TABLE_LEN) {
fprintf(stderr, "modloopend: %s has unusable song length %d\n", inPath, songLength);
free(buf);
return 2;
}
maxPattern = 0;
for (i = 0; i < MOD_ORDER_TABLE_LEN; i++) {
int p = buf[MOD_ORDER_TABLE_OFFSET + i];
if (p > maxPattern) {
maxPattern = p;
}
}
patternCount = maxPattern + 1;
if (patternCount >= MOD_MAX_PATTERNS) {
fprintf(stderr, "modloopend: %s already uses pattern %d, no room for stop marker\n",
inPath, maxPattern);
free(buf);
return 2;
}
samplesOffset = MOD_HEADER_SIZE + (long)patternCount * MOD_PATTERN_BYTES;
if (samplesOffset > fileSize) {
fprintf(stderr, "modloopend: %s claims %d patterns but file is only %ld bytes\n",
inPath, patternCount, fileSize);
free(buf);
return 2;
}
samplesLen = fileSize - samplesOffset;
newSize = (size_t)fileSize + MOD_PATTERN_BYTES;
newBuf = (uint8_t *)malloc(newSize);
if (newBuf == NULL) {
fprintf(stderr, "modloopend: out of memory\n");
free(buf);
return 2;
}
// Copy header + existing patterns.
memcpy(newBuf, buf, (size_t)samplesOffset);
// New pattern: 1024 bytes of zeros except row 0 channel 0 = E8FF.
newPatternStart = newBuf + samplesOffset;
memset(newPatternStart, 0, MOD_PATTERN_BYTES);
newPatternStart[0] = E8FF_BYTE_0;
newPatternStart[1] = E8FF_BYTE_1;
newPatternStart[2] = E8FF_BYTE_2;
newPatternStart[3] = E8FF_BYTE_3;
// Append samples after the new pattern.
if (samplesLen > 0) {
memcpy(newBuf + samplesOffset + MOD_PATTERN_BYTES, buf + samplesOffset, (size_t)samplesLen);
}
// Patch song length: now plays one extra position pointing at the
// new pattern index.
newBuf[MOD_SONG_LENGTH_OFFSET] = (uint8_t)(songLength + 1);
newBuf[MOD_ORDER_TABLE_OFFSET + songLength] = (uint8_t)patternCount;
free(buf);
fp = fopen(outPath, "wb");
if (fp == NULL) {
fprintf(stderr, "modloopend: cannot open %s for writing\n", outPath);
free(newBuf);
return 2;
}
if (fwrite(newBuf, 1, newSize, fp) != newSize) {
fprintf(stderr, "modloopend: short write on %s\n", outPath);
fclose(fp);
free(newBuf);
return 2;
}
fclose(fp);
free(newBuf);
printf("modloopend: %s -> %s (added pattern %d as stop marker, %zu bytes)\n",
inPath, outPath, patternCount, newSize);
return 0;
}
// ----- Public API (alphabetical) -----
int main(int argc, char **argv) {
if (argc != 3) {
fprintf(stderr, "usage: modloopend input.mod output.mod\n");
return 2;
}
return process(argv[1], argv[2]);
}