// Minimal harness: load .TFX (TFMX module) + .SAM (samples), // bundle into a TFHD container (same layout as amiga_exotic_players' // try_tfmx_bundle), run tfmx.h's player for N seconds, write a WAV file. // // Provenance: tfmx.h, paula.h and player_api.h in this directory are // vendored, unmodified, from https://github.com/vtlmks/amiga_exotic_players // (MIT License, itself a C99 port of the TFMX replayer from NostalgicPlayer, // https://github.com/neumatho/NostalgicPlayer, also MIT). See LICENSE in // this directory for both notices. This file (render_tfmx.c) and build.sh // are original code written for the Kellogg's Remake project. Kellogg's // Tony & Friends stores TFMX music as separate .TFX (module/"mdat") // and .SAM (samples/"smpl") files inside PCKELL.DAT; this harness // re-bundles that pair into the TFHD container tfmx.h expects, since the // game's naming convention differs from TFMX's usual mdat./smpl. // convention. // // SONG-INDEX (added 22.07.2026 abends, Stefans Meldung "das Spiel hat noch // wesentlich mehr Lieder"): ein einzelnes TFX-Modul kann MEHRERE Songs // buendeln (tfmx.h's v_songs[]/v_songs_count, ausgewaehlt ueber // player_info.admin.start_song). Bisher haben wir immer nur Song 0 // gerendert. Stichprobe ergab: TITEL.TFX hat 3 Songs, TITEL2.TFX hat 2, // ONGAME2.TFX sogar 14 (vermutlich je ein Level-Track). Der optionale 5. // CLI-Arg waehlt den gewuenschten Song; ohne Angabe bleibt das Verhalten // exakt wie vorher (Song 0, kein tfmx_restart-Aufruf noetig weil das schon // der Init-Default ist). #include #include #include #include #include "player_api.h" #include "tfmx.h" static uint8_t *load_file(const char *path, uint32_t *out_len) { FILE *f = fopen(path, "rb"); if (!f) { perror(path); return 0; } fseek(f, 0, SEEK_END); long len = ftell(f); fseek(f, 0, SEEK_SET); uint8_t *buf = malloc(len); fread(buf, 1, len, f); fclose(f); *out_len = (uint32_t)len; return buf; } static void write_wav(const char *path, int16_t *pcm, int32_t frames, int32_t sample_rate) { FILE *f = fopen(path, "wb"); uint32_t data_bytes = frames * 2 * sizeof(int16_t); uint32_t riff_size = 36 + data_bytes; fwrite("RIFF", 1, 4, f); fwrite(&riff_size, 4, 1, f); fwrite("WAVE", 1, 4, f); fwrite("fmt ", 1, 4, f); uint32_t fmt_size = 16; fwrite(&fmt_size, 4, 1, f); uint16_t audio_format = 1, num_channels = 2; fwrite(&audio_format, 2, 1, f); fwrite(&num_channels, 2, 1, f); fwrite(&sample_rate, 4, 1, f); uint32_t byte_rate = sample_rate * 2 * sizeof(int16_t); fwrite(&byte_rate, 4, 1, f); uint16_t block_align = 2 * sizeof(int16_t); fwrite(&block_align, 2, 1, f); uint16_t bits_per_sample = 16; fwrite(&bits_per_sample, 2, 1, f); fwrite("data", 1, 4, f); fwrite(&data_bytes, 4, 1, f); fwrite(pcm, 1, data_bytes, f); fclose(f); } int main(int argc, char **argv) { if (argc < 4) { fprintf(stderr, "usage: %s [max_seconds] [song_index]\n", argv[0]); return 1; } // WICHTIG (Fix 22.07.2026 abends): [max_seconds] ist NUR noch eine // Sicherheits-OBERGRENZE, keine Ziel-Laenge mehr! Vorher wurde exakt // `seconds` lang gerendert -- war der Wert kleiner als die echte // Songlaenge, wurde mitten im Stueck abgeschnitten (Stefans Meldung: // "die Musik ist zu kurz, da fehlt der Rest", verursacht durch TITEL mit // frueher genutzten 60s bzw. einen stehengebliebenen 8s-Cache-Rest). // Stattdessen rendern wir jetzt in kleinen Haeppchen und beobachten // tfmx.h's eigenes `real_song_end`-Flag -- sobald es feuert, haben wir // GENAU eine vollstaendige natuerliche Song-Schleife im Kasten und // brechen ab, egal wie lang das Stueck wirklich ist. max_seconds greift // nur als Notbremse, falls ein Modul (defekt/kaputt) nie ein Ende meldet. double max_seconds = argc > 4 ? atof(argv[4]) : 180.0; int song_index = argc > 5 ? atoi(argv[5]) : -1; // -1 = Default-Song (0) uint32_t mdat_len = 0, smpl_len = 0; uint8_t *mdat = load_file(argv[1], &mdat_len); uint8_t *smpl = load_file(argv[2], &smpl_len); if (!mdat || !smpl) return 1; uint32_t hdr_off = 18; uint32_t bundle_len = hdr_off + mdat_len + smpl_len; uint8_t *bundle = malloc(bundle_len); memset(bundle, 0, hdr_off); bundle[0] = 'T'; bundle[1] = 'F'; bundle[2] = 'H'; bundle[3] = 'D'; bundle[4] = 0; bundle[5] = 0; bundle[6] = (uint8_t)(hdr_off >> 8); bundle[7] = (uint8_t)hdr_off; bundle[8] = 0; bundle[9] = 0; bundle[10] = (uint8_t)(mdat_len >> 24); bundle[11] = (uint8_t)(mdat_len >> 16); bundle[12] = (uint8_t)(mdat_len >> 8); bundle[13] = (uint8_t)mdat_len; bundle[14] = (uint8_t)(smpl_len >> 24); bundle[15] = (uint8_t)(smpl_len >> 16); bundle[16] = (uint8_t)(smpl_len >> 8); bundle[17] = (uint8_t)smpl_len; memcpy(bundle + hdr_off, mdat, mdat_len); memcpy(bundle + hdr_off + mdat_len, smpl, smpl_len); int32_t sample_rate = 44100; // Direkt tfmx_init/tfmx_get_audio statt der generischen player_api- // Indirektion nutzen -- wir brauchen Zugriff auf state->real_song_end, // das die generische Schnittstelle (nur get_audio(state, out, frames)) // nicht durchreicht. struct tfmx_state *s = tfmx_init(bundle, bundle_len, sample_rate); if (!s) { fprintf(stderr, "tfmx_init failed (module not recognized)\n"); return 2; } if (song_index >= 0) { if ((uint32_t)song_index >= s->v_songs_count) { fprintf(stderr, "song_index %d out of range (v_songs_count=%u)\n", song_index, s->v_songs_count); return 4; } // tfmx_init hat bereits Song 0 aufgesetzt (tfmx_init_decoder -> // tfmx_restart, siehe tfmx.h). Fuer einen anderen Index muessen wir // den Sequencer neu auf den gewuenschten Song ausrichten: // start_song setzen, dann tfmx_restart() erneut aufrufen (liest // v_songs[start_song] und initialisiert Sequencer-Position/Speed neu). s->player_info.admin.start_song = song_index; tfmx_restart(s); } int32_t max_frames = (int32_t)(max_seconds * sample_rate); // Chunk klein genug waehlen, dass real_song_end nicht durch einen // zweiten Tick INNERHALB desselben Chunks schon wieder auf 0 zurueck- // gesetzt wurde, bevor wir nachsehen (ein Tick liegt typischerweise bei // ~20ms/882 Frames bei 44.1kHz -- 64 Frames sind davon weit entfernt). const int32_t CHUNK = 64; float *scratch = malloc(sizeof(float) * CHUNK * 2); int16_t *pcm = malloc(sizeof(int16_t) * (size_t)max_frames * 2); if (!scratch || !pcm) { fprintf(stderr, "out of memory\n"); return 3; } int32_t total = 0; int natural_end = 0; while (total < max_frames) { int32_t this_chunk = CHUNK; if (total + this_chunk > max_frames) { this_chunk = max_frames - total; } memset(scratch, 0, sizeof(float) * (size_t)this_chunk * 2); tfmx_get_audio(s, scratch, this_chunk); int16_t *out_ptr = pcm + (size_t)total * 2; for (int32_t i = 0; i < this_chunk * 2; ++i) { float v = scratch[i] * 32767.0f; if (v > 32767.0f) v = 32767.0f; if (v < -32768.0f) v = -32768.0f; out_ptr[i] = (int16_t)v; } total += this_chunk; if (s->real_song_end) { natural_end = 1; break; } } write_wav(argv[3], pcm, total, sample_rate); fprintf(stderr, "wrote %s: song=%d %d frames @ %d Hz (%.2fs)%s\n", argv[3], song_index, total, sample_rate, (double)total / sample_rate, natural_end ? " [natuerliches Songende/Loop-Punkt erkannt]" : " [Sicherheits-Obergrenze erreicht, kein Songende gefunden -- Modul pruefen]"); tfmx_free(s); return 0; }