joeylib2/tools/modloopend/modloopend.c

106 lines
3.6 KiB
C

// 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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#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]);
}