106 lines
3.6 KiB
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]);
|
|
}
|