221 lines
6.9 KiB
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]);
|
|
}
|