// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // Append a one-shot stop marker to a Protracker .MOD file so the Amiga // audio HAL (PTPlayer-based) can honor `jlAudioPlayMod(loop=false)`. // // PTPlayer has no built-in song-end signal -- songs loop forever // unless an E8x effect command is encountered, which latches x into // the public `mt_E8Trigger` byte. The Amiga's jlpAudioFrameTick 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 E8F 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 0x0F, and PlayMod(loop=false) // stops cleanly. JoeyLib's module player (DOS, the ST, the X68000 and // the hosted ports) stops a song that does not loop at an E8F too, and // NTP on the IIgs ignores E8, so the 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. The marking // itself is src/core/modStopMarker.c, which the Amiga HAL also runs on // every module it plays without looping, so a plain .MOD needs no // conversion; this tool remains for marking a file ahead of time. #include #include #include #include "modStopMarker.h" // ----- Prototypes ----- static int process(const char *inPath, const char *outPath); // ----- Internal helpers (alphabetical) ----- static int process(const char *inPath, const char *outPath) { FILE *fp; uint8_t *in; uint8_t *out; long inSize; size_t outSize; int status; fp = fopen(inPath, "rb"); if (fp == NULL) { fprintf(stderr, "modloopend: cannot open %s\n", inPath); return 2; } inSize = -1L; if (fseek(fp, 0L, SEEK_END) == 0) { inSize = ftell(fp); } if (inSize <= 0L || fseek(fp, 0L, SEEK_SET) != 0) { fprintf(stderr, "modloopend: cannot size %s\n", inPath); fclose(fp); return 2; } outSize = (size_t)inSize + MOD_STOP_MARKER_BYTES; in = (uint8_t *)malloc((size_t)inSize); out = (uint8_t *)malloc(outSize); status = 2; if (in == NULL || out == NULL) { fprintf(stderr, "modloopend: out of memory\n"); } else if (fread(in, 1, (size_t)inSize, fp) != (size_t)inSize) { fprintf(stderr, "modloopend: short read on %s\n", inPath); } else if (!modStopMarkerAdd(in, (uint32_t)inSize, out)) { fprintf(stderr, "modloopend: %s is not a Protracker module that can take a stop marker\n", inPath); } else { status = 0; } fclose(fp); if (status == 0) { fp = fopen(outPath, "wb"); if (fp == NULL || fwrite(out, 1, outSize, fp) != outSize) { fprintf(stderr, "modloopend: cannot write %s\n", outPath); status = 2; } if (fp != NULL) { fclose(fp); } } if (status == 0) { printf("modloopend: %s -> %s (stop marker added, %lu bytes)\n", inPath, outPath, (unsigned long)outSize); } free(in); free(out); return status; } // ----- 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]); }