diff --git a/NOTES.md b/NOTES.md index c1772d8..14121b7 100644 --- a/NOTES.md +++ b/NOTES.md @@ -718,3 +718,63 @@ werden muessen. Python/pygame-Projektstruktur aufsetzen — Asset-Loader (Palette-Regeln aus `kellogg_formats.py` direkt uebernehmen), dann Tony-Bewegung/ Kollision/Level-Scrolling als erste spielbare Vertical Slice. + +## ENTSCHEIDUNG 2026-07-22 abends — erster Meilenstein: Boot-/Intro-Sequenz statt Gameplay + +Stefan-Wunsch: als leichten, sichtbaren Einstieg zuerst die komplette +Boot-Sequenz nachbauen (Rauser-Card → Factor5-Logo → Kellogg's-Logo mit +"Vorhang"-Aufdeck-Effekt → Titelbild → Hauptmenue mit Menuepunkten → +Weltkarte), BEVOR Tony ueberhaupt beweglich ist. Sinnvolle Wahl: das +zwingt genau die Kern-Engine-Bausteine, die spaeter das ganze Spiel +braucht (Asset-Loader, Bild-Blitting mit Palette, Timing/Sequenzer, +Sound-Playback, Input-Handling fuers Menue) — nur eben am kleinstmoeglichen +Slice, bevor Physik/Kollision/Level-Logik dazukommen. + +**Stand der benoetigten Assets (alle schon extrahiert, nur noch nicht +zu einer Sequenz zusammengesetzt):** +- Rauser-Card: `RAUSER1/2/3.PCC` (3 Scroll-/Fade-Frames, siehe oben) +- Factor5-Logo: `FACTOR5.PCC` + `FAC0-3.PCC` (kleine Begleit-Frames, + vermutlich Blink-/Reflex-Animation) +- Kellogg's-Logo: `KELLOGGS.PCC` — **"Vorhang"-Effekt (Stefans Beobachtung: + oberer Teil muesste sich von rechts nach links aufdecken) ist NICHT als + Asset gespeichert**, sondern eine Laufzeit-Animation von RTM.EXE (reines + Wipe/Reveal auf einem statischen Vollbild). Muss durch Beobachtung + nachgebaut werden (Screenshot-Serie waehrend des Boot-Vorgangs in + DOSBox, kurze Intervalle, um Richtung/Geschwindigkeit/Dauer zu + bestimmen) — noch nicht gemacht, naechster Schritt. +- Titelbild ("Tony & Friends in Kellogg's Land"): noch keine eigene PCC- + Datei dafuer identifiziert unter den 77 — moeglich, dass das Kellogg's- + Logo direkt das Titelbild IST (ein Screen), oder der Text wird von der + Engine ueber ein weiteres, noch nicht namentlich zugeordnetes PCC + gelegt. Zu klaeren beim naechsten DOSBox-Screenshot-Durchlauf. +- Hauptmenue: `MENU.PCC` (Rahmen/Box vorhanden) — Menuepunkte selbst + vermutlich als Text von der Engine gerendert (kein Font-Asset bisher + identifiziert) oder als eigene ICO/BOB-Sprites. Noch offen. +- Karte: `KARTE.PCC` + `KARTE0-9.PCC` (10 Zusatz-Frames, vermutlich + kleine Animationen auf der Karte — Wasser/Flagge/Feuer o.ae., noch + nicht einzeln zugeordnet). +- Sound: 3x SAM + 3x TFX (TFMX-Format, Amiga/DOS-Tracker) — bisher NICHT + decodiert. Ob Rauser/Factor5/Kellogg's-Screens ueberhaupt Sound haben, + ist unklar (DOSBox lief bisher im ALSA-nosound-Modus, kein Ton + gehoert) — zu klaeren durch Sichtung von `tools/tfmx_player` (C-Referenz- + Tool, liegt schon lokal unter `tools/tfmx_player/`, noch uncommitted) + und/oder DOSBox mit echtem Audio-Sink testen. + +**Plan (in dieser Reihenfolge):** +1. pygame auf `aria-wohnung` installiert (User-pip, erledigt). +2. DOSBox-Screenshot-Serie waehrend des Bootvorgangs aufnehmen (kurze + Intervalle ueber Rauser→Factor5→Kellogg's→Titel→Menue), um Timing + + Wipe-Richtung/-Geschwindigkeit des Kellogg's-Vorhang-Effekts zu + bestimmen, und um das noch unklare Titelbild/Menue-Text-Rendering zu + identifizieren. +3. `tools/intro_sequence.py` (neu): pygame-basierter Player, der die + Screens in der beobachteten Reihenfolge + Timing zeigt (erst ohne + Ton), Wipe-Effekt fuer Kellogg's-Logo nachbauen. +4. TFMX-Sound anhaengen, sobald `tfmx_player` als Referenz ausgewertet + ist (Python-Wrapper oder Re-Implementierung der Decodierung). +5. Committen/pushen nach jedem funktionierenden Teilschritt (nicht erst + am Ende), NOTES.md nach jedem Meilenstein aktualisieren — wie von + Stefan gewuenscht, damit bei einem VM-/Brain-Neustart nichts verloren + geht und einfach hier weitergelesen werden kann. + +Laeuft jetzt selbstaendig weiter, keine Rueckfrage noetig. diff --git a/tools/tfmx_player/LICENSE b/tools/tfmx_player/LICENSE new file mode 100644 index 0000000..2bf764b --- /dev/null +++ b/tools/tfmx_player/LICENSE @@ -0,0 +1,49 @@ +MIT License + +Copyright (c) 2026 Peter Fors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +================================================================================ + +This project is a port of replayers from NostalgicPlayer +(https://github.com/neumatho/NostalgicPlayer), which is distributed under the +MIT License with the following notice: + +MIT License + +Copyright (c) 2023 Thomas Neumann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tools/tfmx_player/build.sh b/tools/tfmx_player/build.sh new file mode 100755 index 0000000..bfcc96e --- /dev/null +++ b/tools/tfmx_player/build.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Builds render_tfmx, a tiny standalone harness around tfmx.h (see NOTICE.md +# in this directory for provenance/license). Produces ./render_tfmx which +# takes [seconds] and writes a stereo +# 16-bit/44100Hz WAV rendering of the TFMX module. +set -e +cd "$(dirname "$0")" +gcc -std=gnu99 -O2 -Wall -Wno-unused-function -Wno-unused-variable -Wno-unused-parameter \ + -o render_tfmx render_tfmx.c -lm +echo "built: $(dirname "$0")/render_tfmx" diff --git a/tools/tfmx_player/paula.h b/tools/tfmx_player/paula.h new file mode 100644 index 0000000..dfb1acb --- /dev/null +++ b/tools/tfmx_player/paula.h @@ -0,0 +1,632 @@ +// Copyright (c) 2026 Peter Fors +// SPDX-License-Identifier: MIT +// +// Amiga 500 Paula emulator for custom replayers. +// +// This is a hardware model, not a resampler. The channel mixer runs in the +// Paula clock domain (3546895 Hz PAL / 3579545 Hz NTSC). Each hardware +// channel has a period counter that, when it expires, latches the next 8-bit +// sample byte; between latches the channel holds that byte (the zero-order- +// hold staircase a real Paula produces). Volume is the real 6-bit PWM over a +// 64-clock window, not a multiply, so its quantization noise is reproduced. +// Channels 0+3 are summed to the left output, 1+2 to the right, hard-panned, +// in the Paula clock domain. The analog filter chain (always-on RC low-pass -- +// ~4.4 kHz on A500, ~34 kHz on A1200 -- plus the switchable ~3.3 kHz LED +// Butterworth) runs at the Paula clock rate. Only the final stage decimates +// to the host rate, by box-filter integration of the Paula-clock samples that +// fall in each output window. Aliasing and quantization noise that a real +// Amiga produces are preserved. +// +// Paula has exactly four hardware channels (0..3). There is no software- +// mixer extension and no side bus: a real Amiga has no extra channel in its +// signal path. Every format with more than four voices built a mixed buffer +// on the CPU and DMA'd THAT through these four channels, so any such mixdown +// is the replayer's job and its output IS Paula channel sample data, <= 4 +// channels, passing through this same hardware path. +// +// Output is ACCUMULATED into the caller's float buffer. The hardware output +// chain is modelled end to end, to the RCA jack, not just to the summer node: +// +// 1. Resistive averaging summer: the two channels on each side (0+3 left, +// 1+2 right) join through equal board resistors, so the per-side node +// is (ch_a + ch_b) / 2 (a ~6 dB attenuation). +// 2. The analog filter chain acts on that node: an always-on RC low-pass +// (~4.4 kHz on A500, ~34 kHz on A1200) plus the switchable ~3.3 kHz +// LED Butterworth. +// 3. Output buffer/amp: normalises int8 full scale to unity. The +// resistive divider's ~6 dB attenuation is preserved (not compensated) +// so the per-side level matches real hardware: a single full-scale +// channel lands at ~0.5, two correlated full-scale channels on the +// same side at ~1.0. No analog rail saturation is modelled -- at line +// out on a stock A500 the output op-amp runs with ~10 V of usable +// rail headroom against a ~1 V peak signal and never reaches its +// rails in practice. The output is then clamped to [-1, +1] purely +// as a digital safety guard for callers converting to fixed-point: +// small Butterworth step overshoot on transients (a few percent) +// cannot leak out as wrap/click noise after a (int16_t)(x * 32768) +// style cast. Absolute level is the host's concern. +// +// Host integration: the filter chain (always-on RC LP + switchable LED +// Butterworth + decimation anti-alias) runs IIR state at the Paula clock, +// and that state decays exponentially toward zero when channels go silent. +// Once any state slot crosses the float denormal threshold (~1.18e-38), +// every subsequent multiply touching it is denormal-slow on x86 (roughly +// two orders of magnitude); a single mix can blow past the host's audio +// buffer duration -- audible as underrun. Adding per-sample denormal- +// prevention bias inside the filter inner loops would cost a fadd per +// stage per Paula clock (millions/sec), so the agreed convention is: +// THE HOST AUDIO THREAD MUST RUN WITH MXCSR FTZ+DAZ ENABLED. Any thread +// that calls paula_mix_frames is in scope. The standard recipe is +// #include +// _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); +// _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON); +// at the top of the audio thread proc. MXCSR is per-thread on x86 so this +// must be set inside the thread, not once at program start. + +#pragma once + +#include +#include +#include + +// L+R packed double, used through the filter chain to halve biquad cost: all +// three filter stages use identical coefficients per side, only state differs, +// so each biquad line becomes one packed instruction (one packed FMA on +// x86-64-v3). GCC/Clang vector extension; arithmetic operators are overloaded +// to the right SIMD ops per -march. +typedef double paula_v2df __attribute__((vector_size(16))); + +// Opt-in mixer profiler. Compiled in only when PAULA_PROFILE is defined, so +// normal builds carry zero footprint. Accumulates process CPU time spent +// strictly inside paula_mix_frames (not replayer tick work) and the number of +// frames produced; paula_profile_report() turns that into a realtime factor. +#ifdef PAULA_PROFILE +#include +#include +static double paula_profile_cpu_ns = 0.0; +static uint64_t paula_profile_frames = 0; +#endif + +// Paula has exactly four hardware channels. Formats with more voices must +// CPU-mix down to <= 4 themselves; there is no extra channel here. +#define PAULA_NUM_CHANNELS 4 +#define PAULA_PAL_CLOCK 3546895 +#define PAULA_NTSC_CLOCK 3579545 +// Real Paula audio DMA floor. The Hardware Reference Manual's period-124 +// figure is the rate at which all four channels can DMA without the bus +// falling behind during display fetch; a single channel goes lower. The true +// hardware floor is period 113 -- the ProTracker/Soundtracker note table +// bottoms at exactly 113 (B-3) because that is where Paula stops. Clamping to +// 124 detunes the whole top octave flat (period 113 -> ~160 cents). All four +// hardware channels clamp to this. +#define PAULA_DMA_MIN_PERIOD 113 + +// Period accumulator fixed-point: one Paula clock advances the accumulator by +// PAULA_PERIOD_ONE; a channel consumes one sample byte every period_q of +// these. period_q is integer-exact for the Paula register path and fractional +// for the Hz path, so both keep exact pitch. +#define PAULA_PERIOD_SHIFT 16 +#define PAULA_PERIOD_ONE (1ull << PAULA_PERIOD_SHIFT) + +struct paula_channel { + int8_t *sample; + uint32_t length; // bytes (becomes loop_start+loop_length after first wrap) + uint32_t loop_start; // bytes + uint32_t loop_length; // bytes, 0 => one-shot + uint32_t pos; // current byte index into sample + uint64_t period_q; // Paula clocks per sample byte, Q16 + uint64_t period_acc; // period accumulator, Q16 + int8_t *pending_sample; // deferred switch on next wrap (Paula AUDxLC trick) + uint32_t pending_pos; + uint32_t pending_length; + int8_t cur; // latched sample byte (zero-order-hold output) + uint16_t volume; // 0..64 Amiga scale + uint8_t pwm_cnt; // 0..63 volume-PWM phase + uint8_t active; + uint8_t muted; + uint8_t has_pending; + uint8_t backwards; // 1 -> step DOWN through sample (DBP E3, etc.) +}; + +// Amiga model. Selects the always-on post-DAC RC low-pass corner: ~4.4 kHz on +// A500 (the classic muffled top end), ~34 kHz on A1200 (bright but not brick- +// walled -- the slight roll into the top octave that real hardware has, +// neither aliasing brightness nor A500 muffling). The LED filter exists on +// both. Default is the A500. +#define PAULA_MODEL_A500 0 +#define PAULA_MODEL_A1200 1 + +struct paula { + struct paula_channel ch[PAULA_NUM_CHANNELS]; + int32_t sample_rate; // host output rate + int32_t clock; // Paula clock (PAL/NTSC), internal mix rate + int32_t samples_per_tick; + int32_t tick_offset; + int32_t model; + + // Box-filter decimation from the Paula clock domain to the host rate. + // Each output sample averages the decim_step (Q16) Paula clocks that + // fall in its window; decim_phase carries the fraction across calls so + // the clock count alternates with no pitch drift. + uint64_t decim_step; + uint64_t decim_phase; + + // Always-on 1-pole RC low-pass, at the Paula clock rate. Corner depends + // on model: ~4.4 kHz for A500, ~34 kHz for A1200. State is L+R packed. + double fixed_lp_a; + paula_v2df fixed_lp; + + // Switchable LED filter: 2-pole Butterworth low-pass (~3.3 kHz, + // Q=1/sqrt(2)), at the Paula clock rate, RBJ bilinear coefficients. + // Driven by the replayer via paula_set_lp_filter; biquad state (TDF-II, + // L+R packed) persists across toggles so flips don't click. + int32_t lp_filter_on; + double led_b0; + double led_b1; + double led_b2; + double led_a1; + double led_a2; + paula_v2df led_z1; + paula_v2df led_z2; + + // Decimation anti-alias low-pass: 8th-order Butterworth (4 cascaded RBJ + // biquads) at 0.45*host_rate, run in the Paula clock domain just before the + // rate drop. This is a resampler reconstruction filter, NOT modelled + // hardware: it bandlimits to below the host Nyquist so the box-average + // decimation cannot fold ultrasonic ZOH images down into the audible band. + // Keyed to host_rate, so it runs for both models; on the A500 the analog + // chain has already removed everything near Nyquist, making it a no-op. + // State is L+R packed per stage. + double aa_b0[4]; + double aa_a1[4]; + double aa_a2[4]; + paula_v2df aa_z1[4]; + paula_v2df aa_z2[4]; +}; + +// [=]===^=[ paula_recalc ]=======================================================================[=] +// Recompute every rate-dependent coefficient from p->clock and +// p->sample_rate. The analog filters run at the Paula clock, so their +// coefficients are bilinear-transformed for that rate, not the host rate. +static void paula_recalc(struct paula *p) { + double fs = (double)p->clock; + double dt = 1.0 / fs; + + // Always-on RC low-pass. Corner is model-dependent: A500 ~4.4 kHz, + // A1200 ~34 kHz. a = dt / (RC + dt). + double lp_fc = (p->model == PAULA_MODEL_A1200) ? 34000.0 : 4400.0; + double lp_rc = 1.0 / (2.0 * 3.14159265358979323846 * lp_fc); + p->fixed_lp_a = dt / (lp_rc + dt); + + // LED filter: 2-pole Butterworth low-pass, ~3.3 kHz, Q = 1/sqrt(2), + // RBJ cookbook low-pass mapped via the bilinear transform at fs. + double fc = 3300.0; + double q = 0.70710678118654752440; + double w0 = 2.0 * 3.14159265358979323846 * fc / fs; + double cw = cos(w0); + double sw = sin(w0); + double alpha = sw / (2.0 * q); + double a0 = 1.0 + alpha; + p->led_b0 = ((1.0 - cw) * 0.5) / a0; + p->led_b1 = (1.0 - cw) / a0; + p->led_b2 = ((1.0 - cw) * 0.5) / a0; + p->led_a1 = (-2.0 * cw) / a0; + p->led_a2 = (1.0 - alpha) / a0; + + // Decimation anti-alias: 8th-order Butterworth low-pass at 0.45*host_rate, + // mapped via RBJ bilinear at the Paula clock. The four sections carry the + // standard 8th-order Butterworth section Q's; cascaded DC gain is unity. + double aa_q[4] = {0.50979558, 0.60134489, 0.89997622, 2.56291545}; + double aa_w0 = 2.0 * 3.14159265358979323846 * (0.45 * (double)p->sample_rate) / fs; + double aa_cw = cos(aa_w0); + double aa_sw = sin(aa_w0); + for(uint32_t st = 0; st < 4; ++st) { + double al = aa_sw / (2.0 * aa_q[st]); + double a0 = 1.0 + al; + p->aa_b0[st] = ((1.0 - aa_cw) * 0.5) / a0; + p->aa_a1[st] = (-2.0 * aa_cw) / a0; + p->aa_a2[st] = (1.0 - al) / a0; + } + + // Box-filter decimation step: Paula clocks per host output sample, Q16. + p->decim_step = ((uint64_t)p->clock << PAULA_PERIOD_SHIFT) / (uint64_t)p->sample_rate; +} + +// [=]===^=[ paula_init ]=========================================================================[=] +static void paula_init(struct paula *p, int32_t sample_rate, int32_t tick_rate_hz) { + memset(p, 0, sizeof(*p)); + p->sample_rate = sample_rate; + p->clock = PAULA_PAL_CLOCK; + p->samples_per_tick = sample_rate / tick_rate_hz; + p->model = PAULA_MODEL_A500; + // Hard-panned: channels 0+3 -> left, 1+2 -> right (fixed Paula wiring). + paula_recalc(p); +} + +// [=]===^=[ paula_set_clock ]====================================================================[=] +// Select the Paula clock (PAULA_PAL_CLOCK / PAULA_NTSC_CLOCK). Recomputes the +// rate-dependent coefficients. Default after paula_init is PAL. +static void paula_set_clock(struct paula *p, int32_t clock_hz) { + p->clock = clock_hz > 0 ? clock_hz : PAULA_PAL_CLOCK; + paula_recalc(p); +} + +// [=]===^=[ paula_set_model ]====================================================================[=] +// Select the emulated machine. The always-on post-DAC RC low-pass corner +// changes with model (A500 ~4.4 kHz, A1200 ~34 kHz); the LED filter exists on +// both. Default is the A500. +static void paula_set_model(struct paula *p, int32_t model) { + p->model = (model == PAULA_MODEL_A1200) ? PAULA_MODEL_A1200 : PAULA_MODEL_A500; + paula_recalc(p); +} + +// [=]===^=[ paula_set_lp_filter ]================================================================[=] +// Enable or disable the switchable Amiga LED filter (the power-LED-gated +// 2-pole low-pass). Replayers call this to mirror the module's own filter +// state. The always-on RC low-pass is not affected (A500 ~4.4 kHz, A1200 +// ~34 kHz). +static void paula_set_lp_filter(struct paula *p, int32_t on) { + p->lp_filter_on = on ? 1 : 0; +} + +// [=]===^=[ paula_set_period ]===================================================================[=] +// Amiga AUDxPER (DMA) period. All four hardware channels clamp to the real +// Paula DMA minimum period. +static void paula_set_period(struct paula *p, int32_t idx, uint16_t period) { + if(period != 0 && period < PAULA_DMA_MIN_PERIOD) { + period = PAULA_DMA_MIN_PERIOD; + } + if(period == 0) { + p->ch[idx].period_q = 0; + return; + } + p->ch[idx].period_q = (uint64_t)period << PAULA_PERIOD_SHIFT; +} + +// [=]===^=[ paula_set_freq_hz ]==================================================================[=] +// Set a channel's DMA rate directly in Hz, for replayers that DMA a +// CPU-built mixdown buffer through a Paula channel (DigiBoosterPro, FaceThe- +// Music). The period is fractional in the Paula clock domain so pitch stays +// exact. No DMA period floor: mixdown rates are well above it anyway. +static void paula_set_freq_hz(struct paula *p, int32_t idx, uint32_t freq_hz) { + if(freq_hz == 0) { + p->ch[idx].period_q = 0; + return; + } + p->ch[idx].period_q = ((uint64_t)p->clock << PAULA_PERIOD_SHIFT) / (uint64_t)freq_hz; +} + +// [=]===^=[ paula_set_volume ]===================================================================[=] +static void paula_set_volume(struct paula *p, int32_t idx, uint16_t volume) { + if(volume > 64) { + volume = 64; + } + p->ch[idx].volume = volume; +} + +// Volume is passed in 0..256 range in NostalgicPlayer convention; divide to 0..64. +// [=]===^=[ paula_set_volume_256 ]===============================================================[=] +static void paula_set_volume_256(struct paula *p, int32_t idx, uint16_t volume) { + if(volume > 256) { + volume = 256; + } + p->ch[idx].volume = volume >> 2; +} + +// [=]===^=[ paula_play_sample ]==================================================================[=] +static void paula_play_sample(struct paula *p, int32_t idx, int8_t *sample, uint32_t length) { + struct paula_channel *c = &p->ch[idx]; + c->sample = sample; + c->length = length; + c->pos = (c->backwards && length > 0) ? (length - 1) : 0; + c->loop_start = 0; + c->loop_length = 0; + c->has_pending = 0; + c->pending_sample = 0; + c->period_acc = 0; + c->active = (sample != 0) && (length > 0); + c->cur = c->active ? sample[c->pos] : 0; +} + +// [=]===^=[ paula_set_backwards ]================================================================[=] +// Set or clear the backwards-playback flag for a channel. Takes effect on the +// next paula_play_sample (which seeds pos at the high end) and reverses the +// per-byte advance direction. +static void paula_set_backwards(struct paula *p, int32_t idx, int32_t on) { + p->ch[idx].backwards = on ? 1 : 0; +} + +// [=]===^=[ paula_set_pos ]======================================================================[=] +// Move the channel's read position to `byte_offset` within the current sample +// and re-latch the held byte. Used by effects like ProTracker 9xx (sample +// offset). Clamps to [0, length-1]. +static void paula_set_pos(struct paula *p, int32_t idx, uint32_t byte_offset) { + struct paula_channel *c = &p->ch[idx]; + if(c->sample == 0 || c->length == 0) { + c->pos = 0; + c->cur = 0; + return; + } + if(byte_offset >= c->length) { + byte_offset = c->length - 1; + } + c->pos = byte_offset; + c->cur = c->sample[byte_offset]; +} + +// [=]===^=[ paula_queue_sample ]=================================================================[=] +// If the channel is active, the new sample takes effect when the current one +// reaches length (Amiga "write AUDxLC/AUDxLEN mid-DMA"). If inactive, it +// starts immediately. Plays from sample[start_offset] for `length` bytes, +// then wraps using the channel's current loop_start / loop_length. +static void paula_queue_sample(struct paula *p, int32_t idx, int8_t *sample, uint32_t start_offset, uint32_t length) { + struct paula_channel *c = &p->ch[idx]; + if(!c->active && sample != 0 && length > 0) { + c->sample = sample; + c->pos = start_offset; + c->length = start_offset + length; + c->has_pending = 0; + c->pending_sample = 0; + c->period_acc = 0; + c->active = 1; + c->cur = sample[start_offset]; + return; + } + c->pending_sample = sample; + c->pending_pos = start_offset; + c->pending_length = start_offset + length; + c->has_pending = (sample != 0) && (length > 0); +} + +// [=]===^=[ paula_set_loop ]=====================================================================[=] +static void paula_set_loop(struct paula *p, int32_t idx, uint32_t start, uint32_t length) { + struct paula_channel *c = &p->ch[idx]; + c->loop_start = start; + c->loop_length = length; +} + +// [=]===^=[ paula_mute ]=========================================================================[=] +static void paula_mute(struct paula *p, int32_t idx) { + p->ch[idx].active = 0; +} + +// [=]===^=[ paula_ch_advance ]===================================================================[=] +// Consume one sample byte for a channel: step the read position one byte +// (forward or backward), apply the pending-sample swap / loop wrap / one-shot +// stop exactly as Paula DMA does, and re-latch the held byte. +__attribute__((always_inline)) +static inline void paula_ch_advance(struct paula_channel *c) { + if(!c->backwards) { + uint32_t np = c->pos + 1; + if(np >= c->length) { + if(c->has_pending) { + c->sample = c->pending_sample; + np = c->pending_pos; + c->length = c->pending_length; + c->has_pending = 0; + c->pending_sample = 0; + } else if(c->loop_length > 0) { + uint32_t over = np - c->length; + np = c->loop_start + (over % c->loop_length); + c->length = c->loop_start + c->loop_length; + } else { + c->active = 0; + return; + } + } + c->pos = np; + } else { + if(c->pos == 0 || (c->loop_length > 0 && c->pos <= c->loop_start)) { + if(c->has_pending) { + c->sample = c->pending_sample; + c->length = c->pending_length; + c->pos = c->pending_length - 1; + c->has_pending = 0; + c->pending_sample = 0; + } else if(c->loop_length > 0) { + c->pos = c->loop_start + c->loop_length - 1; + } else { + c->active = 0; + return; + } + } else { + c->pos = c->pos - 1; + } + } + c->cur = c->sample[c->pos]; +} + +// [=]===^=[ paula_ch_sample ]====================================================================[=] +// Consume one Paula clock for a single channel: bump the period accumulator, +// advance the read position by as many bytes as the accumulator demands (may +// deactivate a one-shot channel), then return the PWM-gated sample value the +// channel contributes this clock. Returns 0.0 for a channel that is or just +// went inactive, so the caller's accumulator can stay branch-free. +// always_inline: called per Paula clock per active channel (~14M/s of audio), +// and the inliner's -O2 size budget refuses on its own. +__attribute__((always_inline)) +static inline double paula_ch_sample(struct paula_channel *c) { + if(!c->active) { + return 0.0; + } + c->period_acc += PAULA_PERIOD_ONE; + while(c->period_acc >= c->period_q) { + c->period_acc -= c->period_q; + paula_ch_advance(c); + if(!c->active) { + return 0.0; + } + } + c->pwm_cnt = (uint8_t)((c->pwm_cnt + 1) & 63); + int32_t v = (c->pwm_cnt < c->volume) ? (int32_t)c->cur : 0; + return (double)v; +} + +// [=]===^=[ paula_mix_frames ]===================================================================[=] +// Accumulates `frames` float stereo frames into `output`. Caller must +// pre-clear. The inner loop runs at the Paula clock; each output frame is the +// box-filter average of the Paula-clock samples in its window. +// +// L+R run packed as paula_v2df through the analog/AA chain: all three filter +// stages share coefficients across sides, only state differs, so each biquad +// line is one packed instruction (one packed FMA on x86-64-v3). The output +// safety clamp at the box-average store is also packed (one minpd, one +// maxpd). +static void paula_mix_frames(struct paula *p, float *output, int32_t frames) { +#ifdef PAULA_PROFILE + struct timespec prof_t0; + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &prof_t0); +#endif + int32_t led = p->lp_filter_on; + paula_v2df fa = {p->fixed_lp_a, p->fixed_lp_a}; + paula_v2df fl = p->fixed_lp; + paula_v2df lb0 = {p->led_b0, p->led_b0}; + paula_v2df lb1 = {p->led_b1, p->led_b1}; + paula_v2df lb2 = {p->led_b2, p->led_b2}; + paula_v2df la1 = {p->led_a1, p->led_a1}; + paula_v2df la2 = {p->led_a2, p->led_a2}; + paula_v2df lz1 = p->led_z1; + paula_v2df lz2 = p->led_z2; + paula_v2df ab0[4]; + paula_v2df aa1[4]; + paula_v2df aa2[4]; + paula_v2df az1[4]; + paula_v2df az2[4]; + for(uint32_t st = 0; st < 4; ++st) { + ab0[st] = (paula_v2df){p->aa_b0[st], p->aa_b0[st]}; + aa1[st] = (paula_v2df){p->aa_a1[st], p->aa_a1[st]}; + aa2[st] = (paula_v2df){p->aa_a2[st], p->aa_a2[st]}; + az1[st] = p->aa_z1[st]; + az2[st] = p->aa_z2[st]; + } + paula_v2df two = {2.0, 2.0}; + paula_v2df half = {0.5, 0.5}; + // amp_gain normalises int8 full scale (128) to 1.0 and deliberately does + // NOT make up the resistive divider's 6 dB attenuation -- the per-side + // level then matches real hardware (single full-scale channel at ~0.5, + // two correlated full-scale channels on a side at ~1.0). Output is the + // box-filter average over the window (/n), clamped to [-1, +1] at the + // float store as a digital safety guard for fixed-point conversion. + paula_v2df amp = {1.0 / 128.0, 1.0 / 128.0}; + uint64_t phase = p->decim_phase; + uint64_t dstep = p->decim_step; + + // Active-channel working set, split by side. The selection predicate + // (active / unmuted / has sample / nonzero period) is stable within a + // mix call: only `active` can drop when a one-shot sample ends mid-call, + // which paula_ch_sample handles per channel. Splitting by side kills the + // per-Paula-clock "ci == 0 || ci == 3" branch -- each per-side scalar + // accumulator now stays in a register through its sweep. Output is bit- + // identical because pl and pr are separate accumulators with fixed + // channel assignments (0+3 -> pl, 1+2 -> pr): the per-side sum only + // depends on which channels are active, not on iteration order. + struct paula_channel *hw_l[PAULA_NUM_CHANNELS]; + struct paula_channel *hw_r[PAULA_NUM_CHANNELS]; + uint32_t nl = 0; + uint32_t nr = 0; + for(int32_t ci = 0; ci < PAULA_NUM_CHANNELS; ++ci) { + struct paula_channel *c = &p->ch[ci]; + if(!c->active || c->muted || c->sample == 0 || c->period_q == 0) { + continue; + } + if(ci == 0 || ci == 3) { + hw_l[nl++] = c; + } else { + hw_r[nr++] = c; + } + } + + for(int32_t i = 0; i < frames; ++i) { + phase += dstep; + uint32_t n = (uint32_t)(phase >> PAULA_PERIOD_SHIFT); + phase &= (PAULA_PERIOD_ONE - 1); + if(n == 0) { + n = 1; + } + paula_v2df s = {0.0, 0.0}; + for(uint32_t k = 0; k < n; ++k) { + double pl = 0.0; + double pr = 0.0; + for(uint32_t j = 0; j < nl; ++j) { + pl += paula_ch_sample(hw_l[j]); + } + for(uint32_t j = 0; j < nr; ++j) { + pr += paula_ch_sample(hw_r[j]); + } + // Passive resistive averaging summer: the per-side filter node + // is (ch_a + ch_b) / 2, so it cannot exceed a single channel's + // full scale and the hardware path never clips. + paula_v2df x = (paula_v2df){pl, pr} * half; + // Always-on RC pole (model-dependent corner baked into fa). + fl = fl + (x - fl) * fa; + x = fl; + if(led) { + paula_v2df y = lb0 * x + lz1; + lz1 = lb1 * x - la1 * y + lz2; + lz2 = lb2 * x - la2 * y; + x = y; + } + // Downstream output buffer/amp: int8 -> unity normalisation. + // No rail saturation is modelled (see header for rationale). + x = x * amp; + // Anti-alias before the rate drop: 4 cascaded Butterworth biquads + // (TDF-II), L+R packed. Bandlimits below host Nyquist so the box- + // average decimation below cannot fold ultrasonic images down. + // RBJ low-pass identities baked in here: b1 = 2*b0 and b2 = b0, + // so only ab0[] is stored. Do not reuse this loop for a non-LP + // section -- it will silently produce wrong output. + for(uint32_t st = 0; st < 4; ++st) { + paula_v2df y = ab0[st] * x + az1[st]; + az1[st] = two * ab0[st] * x - aa1[st] * y + az2[st]; + az2[st] = ab0[st] * x - aa2[st] * y; + x = y; + } + s = s + x; + } + // Box-filter average over the window, then clamp to [-1, +1] as a + // digital safety guard so callers casting to fixed-point cannot get + // wrap/click from small Butterworth transient overshoot. Branchless + // packed -- one minpd, one maxpd via the GCC vector built-ins. + double inv = 1.0 / (double)n; + paula_v2df out = s * (paula_v2df){inv, inv}; + paula_v2df hi = {1.0, 1.0}; + paula_v2df lo = {-1.0, -1.0}; + out = __builtin_ia32_minpd(out, hi); + out = __builtin_ia32_maxpd(out, lo); + output[2 * i] += (float)out[0]; + output[2 * i + 1] += (float)out[1]; + } + + p->fixed_lp = fl; + p->led_z1 = lz1; + p->led_z2 = lz2; + for(uint32_t st = 0; st < 4; ++st) { + p->aa_z1[st] = az1[st]; + p->aa_z2[st] = az2[st]; + } + p->decim_phase = phase; + +#ifdef PAULA_PROFILE + struct timespec prof_t1; + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &prof_t1); + paula_profile_cpu_ns += (double)(prof_t1.tv_sec - prof_t0.tv_sec) * 1.0e9 + (double)(prof_t1.tv_nsec - prof_t0.tv_nsec); + paula_profile_frames += (uint64_t)frames; +#endif +} + +#ifdef PAULA_PROFILE +// [=]===^=[ paula_profile_report ]===============================================================[=] +// Print the accumulated mixer cost as a realtime factor. Call once at exit. +static void paula_profile_report(int32_t sample_rate) { + if(paula_profile_frames == 0) { + fprintf(stderr, "paula_mix_frames: never called (this player has its own mixer, not paula.h)\n"); + return; + } + double cpu_s = paula_profile_cpu_ns * 1.0e-9; + double audio_s = (sample_rate > 0) ? (double)paula_profile_frames / (double)sample_rate : 0.0; + double rt = (cpu_s > 0.0) ? audio_s / cpu_s : 0.0; + double core_pct = (audio_s > 0.0) ? 100.0 * cpu_s / audio_s : 0.0; + fprintf(stderr, "paula_mix_frames: %.3fs CPU for %.1fs audio -> %.1fx realtime (%.2f%% of one core)\n", + cpu_s, audio_s, rt, core_pct); +} +#endif diff --git a/tools/tfmx_player/player_api.h b/tools/tfmx_player/player_api.h new file mode 100644 index 0000000..c9b9e60 --- /dev/null +++ b/tools/tfmx_player/player_api.h @@ -0,0 +1,61 @@ +// Copyright (c) 2026 Peter Fors +// SPDX-License-Identifier: MIT +// +// Common interface every ported replayer exports. Each player's header declares +// a `struct player_api _api` global that the test player (or any host) +// can iterate to auto-detect the right replayer for a file. +// +// Audio output is interleaved float stereo, nominal range [-1.0, 1.0]. Players +// ACCUMULATE into the caller's buffer (caller must pre-clear). They do NOT +// clip; the host is responsible for any final saturation, dithering, or +// conversion to the audio backend's native sample format. + +#pragma once + +#include + +// Optional file-loader callback used by players that read companion files +// (IFF SMUS instrument files, Face The Music external samples, etc.). Hosts +// that pass a loader implement `fetch` to resolve a logical name (e.g. +// "Instruments/Bass6.instr") to a heap-allocated byte buffer + length. The +// player calls free() on the returned pointer when done. +struct player_loader { + void *ctx; + uint8_t *(*fetch)(void *ctx, const char *name, uint32_t *out_len); +}; + +struct player_api { + const char *name; + const char **extensions; /* null-terminated list of lowercase extensions, no dot */ + void *(*init)(void *data, uint32_t len, int32_t sample_rate); + void (*free)(void *state); + void (*get_audio)(void *state, float *output, int32_t frames); + // Optional: when non-null and the host has a loader for sibling files, + // the host should prefer this entry point. Players that don't need + // external files leave this null and the host falls back to init(). + void *(*init_ex)(void *data, uint32_t len, int32_t sample_rate, struct player_loader *loader); +}; + +// [=]===^=[ player_get_audio_s16 ]===============================================================[=] +// Convenience wrapper for hosts that want signed-16 PCM out. Drives the +// player's float get_audio into the caller-supplied scratch buffer (must hold +// at least frames * 2 floats), clears it first, then converts with hard +// saturation into `output` (frames * 2 int16 stereo samples). The scratch is +// caller-owned so the hot path never allocates; reuse the same buffer across +// calls. +#include +static void player_get_audio_s16(struct player_api *api, void *state, int16_t *output, float *scratch, int32_t frames) { + int32_t samples = frames * 2; + memset(scratch, 0, (size_t)samples * sizeof(float)); + api->get_audio(state, scratch, frames); + for(int32_t i = 0; i < samples; ++i) { + float v = scratch[i] * 32767.0f; + if(v > 32767.0f) { + v = 32767.0f; + } + if(v < -32768.0f) { + v = -32768.0f; + } + output[i] = (int16_t)v; + } +} diff --git a/tools/tfmx_player/render_tfmx.c b/tools/tfmx_player/render_tfmx.c new file mode 100644 index 0000000..bef16fd --- /dev/null +++ b/tools/tfmx_player/render_tfmx.c @@ -0,0 +1,107 @@ +// 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. +#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 [seconds]\n", argv[0]); + return 1; + } + double seconds = argc > 4 ? atof(argv[4]) : 15.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; + void *s = tfmx_api.init(bundle, bundle_len, sample_rate); + if (!s) { + fprintf(stderr, "tfmx_api.init failed (module not recognized)\n"); + return 2; + } + + int32_t total_frames = (int32_t)(seconds * sample_rate); + float *scratch = malloc(sizeof(float) * total_frames * 2); + int16_t *pcm = malloc(sizeof(int16_t) * total_frames * 2); + player_get_audio_s16(&tfmx_api, s, pcm, scratch, total_frames); + + write_wav(argv[3], pcm, total_frames, sample_rate); + fprintf(stderr, "wrote %s: %d frames @ %d Hz (%.1fs)\n", argv[3], total_frames, sample_rate, seconds); + + tfmx_api.free(s); + return 0; +} diff --git a/tools/tfmx_player/tfmx.h b/tools/tfmx_player/tfmx.h new file mode 100644 index 0000000..33f44cd --- /dev/null +++ b/tools/tfmx_player/tfmx.h @@ -0,0 +1,2738 @@ +// Copyright (c) 2026 Peter Fors +// SPDX-License-Identifier: MIT +// +// TFMX replayer (Chris Huelsbeck format), ported from NostalgicPlayer's +// LibTfmxAudioDecoder C# implementation. Drives an Amiga Paula (see paula.h) +// and supports the TFMX 1.5, TFMX Pro, and TFMX 7-Voice variants. +// +// Tick rate is module-defined (CIA-B timer): the player calls SetRate / +// SetBpm at runtime, expressed as a 24.8 fixed-point Hz value (default 50 Hz). +// Output mixing in get_audio is sample-rate driven; ticks are advanced from a +// fractional accumulator that converts (sample_rate / current_rate) into the +// next-tick boundary in output samples. +// +// Single-file formats (TFHD, TFMXPAK, TFMX-MOD) and inline sample data are +// supported by passing the entire file in init(). Two-file variants +// (mdat.X / smpl.X) are not supported through this single-buffer API: the +// host should merge the two files (mdat first, then smpl) before calling +// init() and the merged buffer will be detected as a non-tagged TFMX. +// In that mode, the boundary is auto-detected via offsets.MdatSize from the +// header offsets in the mdat data; for explicitly tagged TFMX files lacking +// inline samples, init() returns 0. +// +// Public API: +// struct tfmx_state *tfmx_init(void *data, uint32_t len, int32_t sample_rate); +// void tfmx_free(struct tfmx_state *s); +// void tfmx_get_audio(struct tfmx_state *s, float *output, int32_t frames); + +#pragma once + +#include +#include +#include +#include "paula.h" +#include "player_api.h" + +#define TFMX_TRACKS_MAX 8 +#define TFMX_TRACK_STEPS_MAX 512 +#define TFMX_VOICES_MAX 8 +#define TFMX_RECURSE_LIMIT 16 +#define TFMX_TRACK_CMD_MAX 4 +#define TFMX_MACRO_CMD_COUNT 0x40 +#define TFMX_PATT_CMD_COUNT 16 +#define TFMX_DEFAULT_TICK_HZ 50 +// Paula DMA period of the 7-voice ch3 software-mix output (Hippel/Huelsbeck +// mixing routine). Fidelity-only: each soft voice steps at its own pitch, +// so this does not move pitch. A/B'd against UADE's TFMX-7V. +#define TFMX_7V_CH3_PERIOD 320 + +struct tfmx_module_offsets { + uint32_t header; + uint32_t track_table; + uint32_t patterns; + uint32_t macros; + uint32_t sample_data; + uint32_t silence; +}; + +struct tfmx_admin { + int16_t speed; + int16_t count; + int32_t start_speed; + int32_t start_song; + uint8_t initialized; + uint16_t random_word; +}; + +struct tfmx_cmd { + uint8_t aa; + uint8_t bb; + uint8_t cd; + uint8_t ee; +}; + +struct tfmx_pattern { + uint32_t offset; + uint32_t offset_saved; + uint16_t step; + uint16_t step_saved; + uint8_t wait; + int8_t loops; + uint8_t eval_next; + uint8_t infinite_loop; +}; + +struct tfmx_track { + uint8_t pt; + int8_t tr; + uint8_t on; + struct tfmx_pattern pattern; +}; + +struct tfmx_fade { + uint8_t volume; + uint8_t target; + uint8_t speed; + uint8_t count; + int8_t delta; + uint8_t active; +}; + +struct tfmx_seq_step { + int32_t first; + int32_t last; + int32_t current; + uint8_t size; + uint8_t next; +}; + +struct tfmx_sequencer { + uint8_t tracks; + uint8_t eval_next; + int16_t loops; + struct tfmx_seq_step step; + uint8_t step_seen_before[TFMX_TRACK_STEPS_MAX]; +}; + +struct tfmx_player_info { + struct tfmx_admin admin; + struct tfmx_track track[TFMX_TRACKS_MAX]; + struct tfmx_cmd cmd; + uint8_t macro_eval_again; + struct tfmx_sequencer sequencer; + struct tfmx_fade fade; +}; + +struct tfmx_macro_state { + uint32_t offset; + uint32_t offset_saved; + uint32_t step; + uint32_t step_saved; + int16_t wait; + uint8_t loop; + uint8_t skip; + uint8_t extra_wait; +}; + +struct tfmx_vibrato_state { + uint8_t time; + uint8_t count; + int8_t intensity; + int16_t delta; +}; + +struct tfmx_envelope_state { + uint8_t flag; + uint8_t count; + uint8_t speed; + uint8_t target; +}; + +struct tfmx_portamento_state { + uint8_t count; + uint8_t wait; + uint16_t speed; + uint16_t period; +}; + +struct tfmx_sample_range_state { + uint32_t start; + uint16_t length; +}; + +struct tfmx_paula_orig { + uint32_t offset; + uint16_t length; +}; + +struct tfmx_sid_op1 { + uint16_t speed; + uint16_t count; + uint16_t inter_delta; + int16_t inter_mod; +}; + +struct tfmx_sid_op23 { + uint16_t speed; + uint16_t count; + uint32_t offset; + int16_t delta; +}; + +struct tfmx_sid_state { + uint32_t source_offset; + uint16_t source_length; + uint32_t target_offset; + uint16_t target_length; + int8_t last_sample; + struct tfmx_sid_op1 op1; + struct tfmx_sid_op23 op2; + struct tfmx_sid_op23 op3; +}; + +struct tfmx_rnd_arp { + uint32_t offset; + uint16_t pos; +}; + +struct tfmx_rnd_state { + uint8_t macro; + int8_t count; + int8_t speed; + int8_t flag; + uint8_t mode; + uint8_t block_wait; + uint8_t mask; + struct tfmx_rnd_arp arp; +}; + +// Per-voice runtime state. Mirrors VoiceVars from the C# port plus the +// PaulaVoice "paula.start/length/period/volume" image needed to implement +// the On/Off/TakeNextBuf semantics on top of the local paula mixer. +struct tfmx_voice_state { + uint16_t output_period; + uint8_t voice_num; + int8_t effects_mode; + int8_t volume; + uint8_t note; + uint8_t note_previous; + uint8_t note_volume; + uint16_t period; + int16_t detune; + uint8_t key_up; + + struct tfmx_macro_state macro; + + int16_t wait_on_dma_count; + uint16_t wait_on_dma_prev_loops; + + uint8_t add_begin_count; + uint8_t add_begin_arg; + int32_t add_begin_offset; + + struct tfmx_vibrato_state vibrato; + struct tfmx_envelope_state envelope; + struct tfmx_portamento_state portamento; + struct tfmx_sample_range_state sample; + struct tfmx_paula_orig paula_orig; + struct tfmx_sid_state sid; + struct tfmx_rnd_state rnd; + + // Mirror of the C# PaulaVoice state. paula_start_offset is an offset + // into the unified module buffer (state->buf). length is in 16-bit + // words to match the original Amiga Paula AUDxLEN convention. + uint32_t paula_start_offset; + uint16_t paula_length; + uint16_t paula_period; + uint16_t paula_volume; + uint8_t paula_dma_on; + uint16_t paula_loop_count; + uint16_t paula_prev_loop_count; +}; + +struct tfmx_variant { + uint8_t compressed; + uint8_t finetune_unscaled; + uint8_t vibrato_unscaled; + uint8_t porta_unscaled; + uint8_t porta_override; + uint8_t no_note_detune; + uint8_t bpm_speed5; + uint8_t no_add_begin_count; + uint8_t no_track_mute; +}; + +struct tfmx_input { + uint8_t *buf; // unified module buffer (mdat + smpl), allocated + uint32_t buf_len; + uint32_t len; + uint32_t mdat_size; + uint32_t smpl_size; + int32_t version_hint; + int32_t start_song_hint; +}; + +// Hybrid-7V soft voice. TFMX "7 voices" mode (Hippel's mixing routine that +// Huelsbeck ported into TFMX) plays three voices directly on Paula channels +// 0,1,2 and CPU-sums the rest into a single buffer DMA'd through Paula +// channel 3. Mix law (libtfmxaudiodecoder LamePaulaMixer::getSample_7V): +// per output sample, sum (int8 sample * volume / 64) of the soft voices, +// then hard-clamp the total to signed 8-bit. +struct tfmx_softvoice { + int8_t *sample; + uint32_t length; + uint32_t loop_start; + uint32_t loop_length; // 0 => one-shot + uint32_t pos; + uint32_t period; // Paula period (pitch) + uint64_t step_q; // sample bytes per ch3 output sample, Q32 + uint64_t step_acc; + int8_t *pending_sample; + uint32_t pending_pos; + uint32_t pending_length; + uint16_t volume; // 0..64 + uint8_t has_pending; + uint8_t active; +}; + +struct tfmx_state { + struct paula paula; + int32_t sample_rate; + + struct tfmx_input input; + struct tfmx_module_offsets offsets; + struct tfmx_variant variant; + struct tfmx_player_info player_info; + struct tfmx_voice_state voices[TFMX_VOICES_MAX]; + + uint8_t channel_to_voice_map[TFMX_VOICES_MAX]; + + uint8_t v_songs[64]; + uint32_t v_songs_count; + + int32_t voice_count; + uint8_t song_end; + uint8_t real_song_end; + uint8_t loop_mode; + uint8_t trigger_restart; + + uint32_t rate; // 24.8 fixed-point Hz + uint32_t tick_fp; // ms*256 per tick (currently unused, kept for parity) + uint32_t tick_fp_add; + + // Sample-rate driven tick cadence: number of output frames between + // player ticks, in 16.16 fixed point, regenerated whenever rate changes. + uint32_t frames_per_tick_fp; + uint32_t frames_until_tick_fp; + + uint8_t track_cmd_used[TFMX_TRACK_CMD_MAX + 1]; + uint8_t pattern_cmd_used[TFMX_PATT_CMD_COUNT]; + uint8_t macro_cmd_used[TFMX_MACRO_CMD_COUNT]; + + // Hybrid-7V CPU submixer (only used in 7-voice mode). Soft voices are + // the ones with voice_num >= 3 (voice_num 0,1,2 stay on real Paula + // channels). voice_num ranges 0..7, so 5 soft slots cover 3..7. + struct tfmx_softvoice softv[5]; + int8_t *softmix_buf; + uint32_t softmix_cap; + int32_t softmix_period; +}; + +static uint16_t tfmx_periods[] = { + 0x0d5c, + 0x0c9c, 0x0be8, 0x0b3c, 0x0a9a, 0x0a02, 0x0a02, 0x0972, + 0x08ea, 0x086a, 0x07f2, 0x0780, 0x0718, + 0x06ae, 0x064e, 0x05f4, 0x059e, 0x054d, 0x0501, 0x04b9, 0x0475, + 0x0435, 0x03f9, 0x03c0, 0x038c, + 0x0358, 0x032a, 0x02fc, 0x02d0, 0x02a8, 0x0282, 0x025e, 0x023b, + 0x021b, 0x01fd, 0x01e0, 0x01c6, + 0x01ac, 0x0194, 0x017d, 0x0168, 0x0154, 0x0140, 0x012f, 0x011e, + 0x010e, 0x00fe, 0x00f0, 0x00e3, + 0x00d6, 0x00ca, 0x00bf, 0x00b4, 0x00aa, 0x00a0, 0x0097, 0x008f, + 0x0087, 0x007f, 0x0078, 0x0071, + 0x00d6, 0x00ca, 0x00bf, 0x00b4, 0x00aa, 0x00a0, 0x0097, 0x008f, + 0x0087, 0x007f, 0x0078, 0x0071, + 0x00d6, 0x00ca, 0x00bf, 0x00b4 +}; + +static uint32_t tfmx_crc32_tab[] = { + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, + 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, + 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, + 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, + 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, + 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, + 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, + 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, + 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, + 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, + 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, + 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, + 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, + 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, + 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, + 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, + 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, + 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, + 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d +}; + +static const char *tfmx_extensions[] = { "tfx", "mdat", "tfm", "tfmx", 0 }; + +// [=]===^=[ tfmx_read_be_u32 ]===================================================================[=] +static uint32_t tfmx_read_be_u32(uint8_t *p, uint32_t off) { + return ((uint32_t)p[off] << 24) | ((uint32_t)p[off + 1] << 16) | ((uint32_t)p[off + 2] << 8) | (uint32_t)p[off + 3]; +} + +// [=]===^=[ tfmx_read_be_u16 ]===================================================================[=] +static uint16_t tfmx_read_be_u16(uint8_t *p, uint32_t off) { + return (uint16_t)(((uint32_t)p[off] << 8) | (uint32_t)p[off + 1]); +} + +// [=]===^=[ tfmx_make_word ]=====================================================================[=] +static uint16_t tfmx_make_word(uint8_t hi, uint8_t lo) { + return (uint16_t)(((uint32_t)hi << 8) | (uint32_t)lo); +} + +// [=]===^=[ tfmx_make_dword ]====================================================================[=] +static uint32_t tfmx_make_dword(uint8_t hihi, uint8_t hilo, uint8_t hi, uint8_t lo) { + return ((uint32_t)hihi << 24) | ((uint32_t)hilo << 16) | ((uint32_t)hi << 8) | (uint32_t)lo; +} + +// [=]===^=[ tfmx_byteswap_u32 ]==================================================================[=] +static uint32_t tfmx_byteswap_u32(uint32_t v) { + return ((v & 0xff000000u) >> 24) | ((v & 0x00ff0000u) >> 8) | ((v & 0x0000ff00u) << 8) | ((v & 0x000000ffu) << 24); +} + +// [=]===^=[ tfmx_crc32 ]=========================================================================[=] +static uint32_t tfmx_crc32(uint8_t *buf, uint32_t off, uint32_t len) { + uint32_t crc = 0xffffffffu; + uint32_t i; + for(i = 0; i < len; ++i) { + crc = tfmx_crc32_tab[(crc ^ buf[off + i]) & 0xff] ^ (crc >> 8); + } + return crc ^ 0xffffffffu; +} + +// [=]===^=[ tfmx_random_word ]===================================================================[=] +static uint16_t tfmx_random_word(struct tfmx_state *s) { + // Linear congruential, with the same general behaviour as the C# port. + uint32_t r = (uint32_t)s->player_info.admin.random_word * 1103515245u + 12345u; + s->player_info.admin.random_word = (uint16_t)(r >> 16); + return s->player_info.admin.random_word; +} + +// [=]===^=[ tfmx_get_macro_offset ]==============================================================[=] +static uint32_t tfmx_get_macro_offset(struct tfmx_state *s, uint8_t macro) { + return s->offsets.header + tfmx_read_be_u32(s->input.buf, s->offsets.macros + ((uint32_t)(macro & 0x7f) << 2)); +} + +// [=]===^=[ tfmx_get_patt_offset ]===============================================================[=] +static uint32_t tfmx_get_patt_offset(struct tfmx_state *s, uint8_t pt) { + return s->offsets.header + tfmx_read_be_u32(s->input.buf, s->offsets.patterns + ((uint32_t)pt << 2)); +} + +// [=]===^=[ tfmx_set_rate ]======================================================================[=] +// rate is in 24.8 fixed-point Hz (so default 50 Hz -> 50 << 8). +static void tfmx_set_rate(struct tfmx_state *s, uint32_t rate) { + if(rate == 0) { + rate = TFMX_DEFAULT_TICK_HZ << 8; + } + s->rate = rate; + s->tick_fp = (1000u << 16) / rate; + // frames per tick = sample_rate / (rate / 256) + uint64_t fpt_fp = ((uint64_t)s->sample_rate << 24) / rate; + s->frames_per_tick_fp = (uint32_t)fpt_fp; +} + +// [=]===^=[ tfmx_set_bpm ]=======================================================================[=] +static void tfmx_set_bpm(struct tfmx_state *s, uint16_t bpm) { + tfmx_set_rate(s, ((uint32_t)bpm << 8) * 2u / 5u); +} + +// [=]===^=[ tfmx_get_track_mute ]================================================================[=] +static uint8_t tfmx_get_track_mute(struct tfmx_state *s, uint8_t t) { + if(s->variant.no_track_mute) { + return 1; + } + return (uint8_t)(0 == tfmx_read_be_u16(s->input.buf, s->offsets.header + 0x1c0u + ((uint32_t)t << 1))); +} + +// [=]===^=[ tfmx_note_to_period ]================================================================[=] +static uint16_t tfmx_note_to_period(int32_t note) { + if(note >= 0) { + note &= 0x3f; + } else if(note < -13) { + note = -13; + } + return tfmx_periods[note + 13]; +} + +// 7-voice routing: voice_num 0,1,2 are real Paula channels; in 7V mode +// (voice_count >= 7) voice_num >= 3 are CPU-summed into Paula channel 3. +// 4-voice TFMX keeps all four voices on real channels (hardware as-is). +// [=]===^=[ tfmx_is_soft ]=======================================================================[=] +static int32_t tfmx_is_soft(struct tfmx_state *s, int32_t voice_num) { + return (s->voice_count >= 7) && (voice_num >= 3); +} + +// [=]===^=[ tfmx_pch_play_sample ]===============================================================[=] +static void tfmx_pch_play_sample(struct tfmx_state *s, int32_t vn, int8_t *sample, uint32_t length) { + if(!tfmx_is_soft(s, vn)) { + paula_play_sample(&s->paula, vn, sample, length); + return; + } + struct tfmx_softvoice *v = &s->softv[vn - 3]; + v->sample = sample; + v->length = length; + v->loop_start = 0; + v->loop_length = 0; + v->pos = 0; + v->step_acc = 0; + v->has_pending = 0; + v->pending_sample = 0; + v->active = (sample != 0) && (length > 0); +} + +// [=]===^=[ tfmx_pch_set_pos ]===================================================================[=] +static void tfmx_pch_set_pos(struct tfmx_state *s, int32_t vn, uint32_t byte_offset) { + if(!tfmx_is_soft(s, vn)) { + paula_set_pos(&s->paula, vn, byte_offset); + return; + } + struct tfmx_softvoice *v = &s->softv[vn - 3]; + if(v->sample == 0 || v->length == 0) { + v->pos = 0; + return; + } + if(byte_offset >= v->length) { + byte_offset = v->length - 1; + } + v->pos = byte_offset; +} + +// [=]===^=[ tfmx_pch_set_loop ]==================================================================[=] +static void tfmx_pch_set_loop(struct tfmx_state *s, int32_t vn, uint32_t start, uint32_t length) { + if(!tfmx_is_soft(s, vn)) { + paula_set_loop(&s->paula, vn, start, length); + return; + } + struct tfmx_softvoice *v = &s->softv[vn - 3]; + v->loop_start = start; + v->loop_length = length; +} + +// [=]===^=[ tfmx_pch_set_period ]================================================================[=] +static void tfmx_pch_set_period(struct tfmx_state *s, int32_t vn, uint16_t period) { + if(!tfmx_is_soft(s, vn)) { + paula_set_period(&s->paula, vn, period); + return; + } + s->softv[vn - 3].period = period; +} + +// [=]===^=[ tfmx_pch_set_volume ]================================================================[=] +static void tfmx_pch_set_volume(struct tfmx_state *s, int32_t vn, uint16_t volume) { + if(!tfmx_is_soft(s, vn)) { + paula_set_volume(&s->paula, vn, volume); + return; + } + if(volume > 64) { + volume = 64; + } + s->softv[vn - 3].volume = volume; +} + +// [=]===^=[ tfmx_pch_queue_sample ]==============================================================[=] +static void tfmx_pch_queue_sample(struct tfmx_state *s, int32_t vn, int8_t *sample, uint32_t start_offset, uint32_t length) { + if(!tfmx_is_soft(s, vn)) { + paula_queue_sample(&s->paula, vn, sample, start_offset, length); + return; + } + struct tfmx_softvoice *v = &s->softv[vn - 3]; + if(!v->active && sample != 0 && length > 0) { + v->sample = sample; + v->pos = start_offset; + v->length = start_offset + length; + v->step_acc = 0; + v->has_pending = 0; + v->pending_sample = 0; + v->active = 1; + return; + } + v->pending_sample = sample; + v->pending_pos = start_offset; + v->pending_length = start_offset + length; + v->has_pending = (sample != 0) && (length > 0); +} + +// [=]===^=[ tfmx_pch_mute ]======================================================================[=] +static void tfmx_pch_mute(struct tfmx_state *s, int32_t vn) { + if(!tfmx_is_soft(s, vn)) { + paula_mute(&s->paula, vn); + return; + } + s->softv[vn - 3].active = 0; +} + +// [=]===^=[ tfmx_softvoice_advance ]=============================================================[=] +static void tfmx_softvoice_advance(struct tfmx_softvoice *v) { + uint32_t np = v->pos + 1; + if(np >= v->length) { + if(v->has_pending) { + v->sample = v->pending_sample; + np = v->pending_pos; + v->length = v->pending_length; + v->has_pending = 0; + v->pending_sample = 0; + } else if(v->loop_length > 0) { + uint32_t over = np - v->length; + np = v->loop_start + (over % v->loop_length); + v->length = v->loop_start + v->loop_length; + } else { + v->active = 0; + return; + } + } + v->pos = np; +} + +// [=]===^=[ tfmx_softmix_refill ]================================================================[=] +// Regenerate Paula channel 3's DMA buffer from the soft voices for the next +// player tick and (re)arm channel 3. See tfmx_softvoice / getSample_7V. +static void tfmx_softmix_refill(struct tfmx_state *s) { + int32_t cp = s->softmix_period; + uint32_t fpt = s->frames_per_tick_fp >> 16; + if(fpt == 0) { + fpt = 1; + } + uint64_t n64 = ((uint64_t)s->paula.clock * (uint64_t)fpt) + / ((uint64_t)cp * (uint64_t)s->paula.sample_rate); + uint32_t n = (uint32_t)n64 + 2; + if(n > s->softmix_cap) { + uint32_t nc = s->softmix_cap ? s->softmix_cap : 1024; + while(nc < n) { + nc <<= 1; + } + int8_t *nb = (int8_t *)realloc(s->softmix_buf, nc); + if(!nb) { + return; + } + s->softmix_buf = nb; + s->softmix_cap = nc; + } + for(int32_t i = 0; i < 5; ++i) { + struct tfmx_softvoice *v = &s->softv[i]; + v->step_q = (v->period != 0) + ? (((uint64_t)(uint32_t)cp << 32) / (uint64_t)v->period) + : 0; + } + for(uint32_t k = 0; k < n; ++k) { + int32_t sam = 0; + for(int32_t i = 0; i < 5; ++i) { + struct tfmx_softvoice *v = &s->softv[i]; + if(!v->active || v->sample == 0 || v->step_q == 0) { + continue; + } + sam += ((int32_t)v->sample[v->pos] * (int32_t)v->volume) / 64; + v->step_acc += v->step_q; + while(v->step_acc >= ((uint64_t)1 << 32)) { + v->step_acc -= ((uint64_t)1 << 32); + tfmx_softvoice_advance(v); + if(!v->active) { + break; + } + } + } + if(sam > 127) { + sam = 127; + } + if(sam < -128) { + sam = -128; + } + s->softmix_buf[k] = (int8_t)sam; + } + paula_play_sample(&s->paula, 3, s->softmix_buf, n); + paula_set_loop(&s->paula, 3, 0, n); + paula_set_period(&s->paula, 3, (uint16_t)cp); + paula_set_volume(&s->paula, 3, 64); +} + +// Hardware shim helpers: thin layer between the TFMX engine and paula.h. +// These mirror the C# PaulaVoice / IChannel surface used by the port. + +// [=]===^=[ tfmx_paula_apply_dma ]================================================================[=] +// Push the voice's mirrored Paula registers to the actual paula channel. +// Called when DMA goes from off to on (On()) or to update period/volume/ +// length on each Run(). +static void tfmx_paula_apply_dma(struct tfmx_state *s, struct tfmx_voice_state *v) { + uint32_t length_bytes = (uint32_t)v->paula_length << 1; + if(length_bytes == 0) { + length_bytes = 2; + } + int8_t *sample_ptr = (int8_t *)s->input.buf; + if(v->paula_dma_on) { + tfmx_pch_play_sample(s, (int32_t)v->voice_num, sample_ptr, v->paula_start_offset + length_bytes); + tfmx_pch_set_pos(s, (int32_t)v->voice_num, v->paula_start_offset); + tfmx_pch_set_loop(s, (int32_t)v->voice_num, v->paula_start_offset, length_bytes); + tfmx_pch_set_period(s, (int32_t)v->voice_num, v->paula_period == 0 ? 0x100 : v->paula_period); + tfmx_pch_set_volume(s, (int32_t)v->voice_num, v->paula_volume > 64 ? 64 : v->paula_volume); + } +} + +// [=]===^=[ tfmx_paula_take_next_buf ]===========================================================[=] +// Equivalent to PaulaVoice.TakeNextBuf in the C# port: queues the current +// paula_start_offset / paula_length as the next-buffer pair so that when +// the currently playing one-shot wraps, the new bounds are picked up. +static void tfmx_paula_take_next_buf(struct tfmx_state *s, struct tfmx_voice_state *v) { + uint32_t length_bytes = (uint32_t)v->paula_length << 1; + if(length_bytes == 0) { + length_bytes = 2; + } + int8_t *sample_ptr = (int8_t *)s->input.buf; + if(!v->paula_dma_on) { + return; + } + tfmx_pch_queue_sample(s, (int32_t)v->voice_num, sample_ptr, v->paula_start_offset, length_bytes); + tfmx_pch_set_loop(s, (int32_t)v->voice_num, v->paula_start_offset, length_bytes); +} + +// [=]===^=[ tfmx_paula_on ]======================================================================[=] +static void tfmx_paula_on(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->paula_dma_on = 1; + tfmx_paula_apply_dma(s, v); +} + +// [=]===^=[ tfmx_paula_off ]=====================================================================[=] +static void tfmx_paula_off(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->paula_dma_on = 0; + v->paula_period = 0; + v->paula_volume = 0; + v->paula_loop_count = 0; + tfmx_pch_mute(s, (int32_t)v->voice_num); +} + +// [=]===^=[ tfmx_take_next_buf_checked ]=========================================================[=] +// Equivalent to TakeNextBufChecked: clamps the start+length to lie within +// the unified buffer, falling back to the silence sample if necessary, +// then mirrors to paula via paula_queue_sample. +static void tfmx_take_next_buf_checked(struct tfmx_state *s, struct tfmx_voice_state *v) { + if(v->paula_orig.offset >= s->input.len) { + v->paula_start_offset = s->offsets.silence; + v->paula_length = 1; + } else if((v->paula_orig.offset + ((uint32_t)v->paula_orig.length << 1)) > s->input.len) { + v->paula_length = (uint16_t)((s->input.len - v->paula_orig.offset) >> 1); + v->paula_start_offset = v->paula_orig.offset; + } else { + v->paula_start_offset = v->paula_orig.offset; + v->paula_length = v->paula_orig.length; + } + tfmx_paula_take_next_buf(s, v); +} + +// [=]===^=[ tfmx_to_paula_start ]================================================================[=] +static void tfmx_to_paula_start(struct tfmx_state *s, struct tfmx_voice_state *v, uint32_t offset) { + v->paula_orig.offset = offset; + v->paula_start_offset = offset; + tfmx_take_next_buf_checked(s, v); +} + +// [=]===^=[ tfmx_to_paula_length ]===============================================================[=] +static void tfmx_to_paula_length(struct tfmx_state *s, struct tfmx_voice_state *v, uint16_t length) { + v->paula_orig.length = length; + v->paula_length = length; + tfmx_take_next_buf_checked(s, v); +} + +// Macro command handler signatures. +typedef void (*tfmx_macro_fn)(struct tfmx_state *, struct tfmx_voice_state *); +typedef void (*tfmx_patt_fn)(struct tfmx_state *, struct tfmx_track *); +typedef void (*tfmx_track_fn)(struct tfmx_state *, uint32_t); + +// Forward macro/pattern/track function table indices via wrappers below. +// To preserve "no forward declarations", function order is: helpers first, +// then macro functions, then pattern functions, then track functions, then +// the dispatcher functions, then init/run, then API thunks. + +// [=]===^=[ tfmx_note_cmd ]======================================================================[=] +// Implementation depends on macro state, but only modifies voice via NoteCmd +// pattern path. Defined here because both NoteCmd path and macro $21 invoke it. +static uint16_t tfmx_note_cmd_period(int32_t note) { + return tfmx_note_to_period(note); +} + +// [=]===^=[ tfmx_note_cmd_apply ]================================================================[=] +static void tfmx_note_cmd_apply(struct tfmx_state *s) { + uint8_t v_num = s->channel_to_voice_map[s->player_info.cmd.cd & (TFMX_VOICES_MAX - 1)]; + struct tfmx_voice_state *v = &s->voices[v_num]; + struct tfmx_cmd *c = &s->player_info.cmd; + if(c->aa == 0xfc) { + // Lock note: nothing + } else if(c->aa == 0xf7) { + v->envelope.speed = c->bb; + uint8_t tmp = (uint8_t)((c->cd >> 4) + 1); + v->envelope.count = tmp; + v->envelope.flag = tmp; + v->envelope.target = c->ee; + } else if(c->aa == 0xf6) { + uint8_t tmp = (uint8_t)(c->bb & 0xfe); + v->vibrato.time = tmp; + v->vibrato.count = (uint8_t)(tmp >> 1); + v->vibrato.intensity = (int8_t)c->ee; + v->vibrato.delta = 0; + } else if(c->aa == 0xf5) { + v->key_up = 1; + } else if(c->aa < 0xc0) { + if(s->variant.no_note_detune) { + v->detune = 0; + } else { + v->detune = (int8_t)c->ee; + } + v->note_volume = (uint8_t)(c->cd >> 4); + v->note_previous = v->note; + v->note = c->aa; + v->key_up = 0; + v->macro.offset = tfmx_get_macro_offset(s, (uint8_t)(c->bb & 0x7f)); + v->macro.step = 0; + v->macro.wait = 0; + v->macro.loop = 0xff; + v->macro.skip = 0; + v->effects_mode = 0; + v->wait_on_dma_count = 0; + } else { + v->portamento.count = c->bb; + v->portamento.wait = 1; + if(v->portamento.speed == 0) { + v->portamento.period = v->period; + } + v->portamento.speed = c->ee; + v->note = (uint8_t)(c->aa & 0x3f); + v->period = tfmx_note_to_period((int32_t)v->note); + } +} + +// [=]===^=[ tfmx_macro_extra_wait ]==============================================================[=] +static void tfmx_macro_extra_wait(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->macro.step++; + if(!v->macro.extra_wait) { + v->macro.extra_wait = 1; + s->player_info.macro_eval_again = 1; + } +} + +// [=]===^=[ tfmx_macro_func_nop ]================================================================[=] +static void tfmx_macro_func_nop(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_stop_sample_sub ]====================================================[=] +static void tfmx_macro_func_stop_sample_sub(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->macro.step++; + tfmx_paula_off(s, v); + struct tfmx_cmd *c = &s->player_info.cmd; + if(c->bb != 0) { + v->macro.extra_wait = 0; + } else { + s->player_info.macro_eval_again = 1; + } + if(c->cd == 0) { + if(c->ee == 0) { + return; + } + uint8_t vol1 = (uint8_t)(v->note_volume * 3); + v->volume = (int8_t)(vol1 + tfmx_make_word(c->cd, c->ee)); + } else { + v->volume = (int8_t)c->ee; + } + int8_t vol = v->volume; + if(s->player_info.fade.volume < 64) { + vol = (int8_t)((4 * (int32_t)v->volume * (int32_t)s->player_info.fade.volume) / (4 * 0x40)); + } + v->paula_volume = (uint16_t)vol; +} + +// [=]===^=[ tfmx_macro_func_stop_sound ]=========================================================[=] +static void tfmx_macro_func_stop_sound(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->envelope.flag = 0; + v->vibrato.time = 0; + v->portamento.speed = 0; + v->sid.target_length = 0; + v->rnd.flag = 0; + tfmx_macro_func_stop_sample_sub(s, v); +} + +// [=]===^=[ tfmx_macro_func_start_sample ]=======================================================[=] +static void tfmx_macro_func_start_sample(struct tfmx_state *s, struct tfmx_voice_state *v) { + tfmx_paula_on(s, v); + v->effects_mode = (int8_t)s->player_info.cmd.bb; + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_set_begin_sub ]======================================================[=] +static void tfmx_macro_func_set_begin_sub(struct tfmx_state *s, struct tfmx_voice_state *v, uint32_t start) { + v->sample.start = start; + tfmx_to_paula_start(s, v, start); + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_set_begin ]==========================================================[=] +static void tfmx_macro_func_set_begin(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->add_begin_count = 0; + struct tfmx_cmd *c = &s->player_info.cmd; + uint32_t start = s->offsets.sample_data + tfmx_make_dword(0, c->bb, c->cd, c->ee); + tfmx_macro_func_set_begin_sub(s, v, start); +} + +// [=]===^=[ tfmx_macro_func_set_len ]============================================================[=] +static void tfmx_macro_func_set_len(struct tfmx_state *s, struct tfmx_voice_state *v) { + uint16_t len = tfmx_make_word(s->player_info.cmd.cd, s->player_info.cmd.ee); + v->sample.length = len; + tfmx_to_paula_length(s, v, len); + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_wait ]===============================================================[=] +static void tfmx_macro_func_wait(struct tfmx_state *s, struct tfmx_voice_state *v) { + if((s->player_info.cmd.bb & 1) != 0) { + if(v->rnd.block_wait) { + return; + } + v->rnd.block_wait = 1; + v->macro.step++; + s->player_info.macro_eval_again = 1; + } else { + v->macro.wait = (int16_t)tfmx_make_word(s->player_info.cmd.cd, s->player_info.cmd.ee); + tfmx_macro_extra_wait(s, v); + } +} + +// [=]===^=[ tfmx_macro_func_loop ]===============================================================[=] +static void tfmx_macro_func_loop(struct tfmx_state *s, struct tfmx_voice_state *v) { + if(v->macro.loop == 0) { + v->macro.loop = 0xff; + v->macro.step++; + } else { + if(v->macro.loop == 0xff) { + v->macro.loop = (uint8_t)(s->player_info.cmd.bb - 1); + } else { + v->macro.loop--; + } + v->macro.step = tfmx_make_word(s->player_info.cmd.cd, s->player_info.cmd.ee); + } + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_cont ]===============================================================[=] +static void tfmx_macro_func_cont(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->macro.offset = tfmx_get_macro_offset(s, (uint8_t)(s->player_info.cmd.bb & 0x7f)); + v->macro.step = tfmx_make_word(s->player_info.cmd.cd, s->player_info.cmd.ee); + v->macro.loop = 0xff; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_stop ]===============================================================[=] +static void tfmx_macro_func_stop(struct tfmx_state *s, struct tfmx_voice_state *v) { + (void)s; + v->macro.skip = 1; +} + +// [=]===^=[ tfmx_macro_func_add_note_sub ]=======================================================[=] +static void tfmx_macro_func_add_note_sub(struct tfmx_state *s, struct tfmx_voice_state *v, uint8_t note_add) { + int8_t n = (int8_t)((int32_t)note_add + (int8_t)s->player_info.cmd.bb); + uint16_t p = tfmx_note_to_period((int32_t)n); + int16_t finetune = (int16_t)((int32_t)v->detune + (int16_t)tfmx_make_word(s->player_info.cmd.cd, s->player_info.cmd.ee)); + if(s->variant.finetune_unscaled) { + p = (uint16_t)((int32_t)p + finetune); + } else if(finetune != 0) { + p = (uint16_t)((((int32_t)finetune + 0x100) * (int32_t)p) >> 8); + } + v->period = p; + if(v->portamento.speed == 0) { + v->output_period = p; + } +} + +// [=]===^=[ tfmx_macro_func_add_note ]===========================================================[=] +static void tfmx_macro_func_add_note(struct tfmx_state *s, struct tfmx_voice_state *v) { + tfmx_macro_func_add_note_sub(s, v, v->note); + tfmx_macro_extra_wait(s, v); +} + +// [=]===^=[ tfmx_macro_func_set_note ]===========================================================[=] +static void tfmx_macro_func_set_note(struct tfmx_state *s, struct tfmx_voice_state *v) { + tfmx_macro_func_add_note_sub(s, v, 0); + tfmx_macro_extra_wait(s, v); +} + +// [=]===^=[ tfmx_macro_func_reset ]==============================================================[=] +static void tfmx_macro_func_reset(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->add_begin_count = 0; + v->envelope.flag = 0; + v->vibrato.time = 0; + v->portamento.speed = 0; + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_portamento ]=========================================================[=] +static void tfmx_macro_func_portamento(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + v->portamento.count = c->bb; + v->portamento.wait = 1; + if(s->variant.porta_override || (v->portamento.speed == 0)) { + v->portamento.period = v->period; + } + v->portamento.speed = tfmx_make_word(c->cd, c->ee); + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_vibrato ]============================================================[=] +static void tfmx_macro_func_vibrato(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + v->vibrato.time = c->bb; + v->vibrato.count = (uint8_t)(c->bb >> 1); + v->vibrato.intensity = (int8_t)c->ee; + if(v->portamento.speed == 0) { + v->output_period = v->period; + v->vibrato.delta = 0; + } + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_add_volume ]=========================================================[=] +static void tfmx_macro_func_add_volume(struct tfmx_state *s, struct tfmx_voice_state *v) { + uint8_t vol = (uint8_t)(v->note_volume * 3); + vol = (uint8_t)(vol + s->player_info.cmd.ee); + v->volume = (int8_t)vol; + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_add_vol_note ]=======================================================[=] +static void tfmx_macro_func_add_vol_note(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + if(c->cd == 0xfe) { + uint8_t ee = c->ee; + c->cd = 0; + c->ee = 0; + tfmx_macro_func_add_note_sub(s, v, v->note); + c->ee = ee; + } + uint8_t vol = (uint8_t)(v->note_volume * 3); + vol = (uint8_t)(vol + c->ee); + v->volume = (int8_t)vol; + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_set_volume ]=========================================================[=] +static void tfmx_macro_func_set_volume(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + if(c->cd == 0xfe) { + uint8_t ee = c->ee; + c->cd = 0; + c->ee = 0; + tfmx_macro_func_add_note_sub(s, v, v->note); + c->ee = ee; + } + v->volume = (int8_t)c->ee; + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_envelope ]===========================================================[=] +static void tfmx_macro_func_envelope(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + v->envelope.speed = c->bb; + v->envelope.flag = c->cd; + v->envelope.count = c->cd; + v->envelope.target = c->ee; + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_loop_keyup ]=========================================================[=] +static void tfmx_macro_func_loop_keyup(struct tfmx_state *s, struct tfmx_voice_state *v) { + if(!v->key_up) { + tfmx_macro_func_loop(s, v); + } else { + v->macro.step++; + s->player_info.macro_eval_again = 1; + } +} + +// [=]===^=[ tfmx_macro_func_add_begin ]==========================================================[=] +static void tfmx_macro_func_add_begin(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + v->add_begin_count = c->bb; + v->add_begin_arg = c->bb; + int32_t offset = (int16_t)tfmx_make_word(c->cd, c->ee); + v->add_begin_offset = offset; + uint32_t begin = (uint32_t)((int64_t)v->sample.start + offset); + if(begin < s->offsets.sample_data) { + begin = s->offsets.sample_data; + } + if(v->sid.target_length == 0) { + tfmx_macro_func_set_begin_sub(s, v, begin); + } else { + v->sample.start = begin; + v->sid.source_offset = begin; + v->macro.step++; + s->player_info.macro_eval_again = 1; + } +} + +// [=]===^=[ tfmx_macro_func_add_len ]============================================================[=] +static void tfmx_macro_func_add_len(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->macro.step++; + s->player_info.macro_eval_again = 1; + int16_t len = (int16_t)tfmx_make_word(s->player_info.cmd.cd, s->player_info.cmd.ee); + v->sample.length = (uint16_t)((int32_t)v->sample.length + len); + if(v->sid.target_length == 0) { + tfmx_to_paula_length(s, v, v->sample.length); + } else { + v->sid.source_length = (uint16_t)len; + } +} + +// [=]===^=[ tfmx_macro_func_stop_sample ]========================================================[=] +static void tfmx_macro_func_stop_sample(struct tfmx_state *s, struct tfmx_voice_state *v) { + tfmx_macro_func_stop_sample_sub(s, v); +} + +// [=]===^=[ tfmx_macro_func_wait_keyup ]=========================================================[=] +static void tfmx_macro_func_wait_keyup(struct tfmx_state *s, struct tfmx_voice_state *v) { + if(v->key_up) { + v->macro.step++; + s->player_info.macro_eval_again = 1; + } else { + if(v->macro.loop == 0) { + v->macro.loop = 0xff; + v->macro.step++; + s->player_info.macro_eval_again = 1; + } else if(v->macro.loop == 0xff) { + v->macro.loop = (uint8_t)(s->player_info.cmd.ee - 1); + } else { + v->macro.loop--; + } + } +} + +// [=]===^=[ tfmx_macro_func_goto ]===============================================================[=] +static void tfmx_macro_func_goto(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->macro.offset_saved = v->macro.offset; + v->macro.step_saved = v->macro.step; + tfmx_macro_func_cont(s, v); +} + +// [=]===^=[ tfmx_macro_func_return ]=============================================================[=] +static void tfmx_macro_func_return(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->macro.offset = v->macro.offset_saved; + v->macro.step = v->macro.step_saved; + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_set_period ]=========================================================[=] +static void tfmx_macro_func_set_period(struct tfmx_state *s, struct tfmx_voice_state *v) { + uint16_t period = tfmx_make_word(s->player_info.cmd.cd, s->player_info.cmd.ee); + v->period = period; + if(v->portamento.speed == 0) { + v->output_period = period; + } + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_sample_loop ]========================================================[=] +static void tfmx_macro_func_sample_loop(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + uint32_t offset = tfmx_make_dword(0, c->bb, c->cd, c->ee); + v->sample.start += offset; + v->sample.length = (uint16_t)((int32_t)v->sample.length - (int32_t)(offset >> 1)); + tfmx_to_paula_start(s, v, v->sample.start); + tfmx_to_paula_length(s, v, v->sample.length); + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_one_shot ]===========================================================[=] +static void tfmx_macro_func_one_shot(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->add_begin_count = 0; + v->sample.start = s->offsets.sample_data; + v->sample.length = 1; + tfmx_to_paula_start(s, v, v->sample.start); + tfmx_to_paula_length(s, v, v->sample.length); + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_wait_on_dma ]========================================================[=] +static void tfmx_macro_func_wait_on_dma(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->wait_on_dma_count = (int16_t)tfmx_make_word(s->player_info.cmd.cd, s->player_info.cmd.ee); + v->macro.skip = 1; + v->wait_on_dma_prev_loops = v->paula_loop_count; + tfmx_macro_extra_wait(s, v); +} + +// [=]===^=[ tfmx_random_play ]===================================================================[=] +// Forward-declared semantically: defined after macro_func_random_play. +// Implementation lives further down (mutual recursion would have us forward +// declare, so we inline both via dispatcher: random_play_run is called by +// modulation and by the macro). Define stubs here that call into a single +// consolidated routine. +// +// The C# code uses random play with sometimes intricate state. We keep the +// minimal but functionally complete implementation; modules using it are +// extremely rare. +static void tfmx_randomize(struct tfmx_state *s) { + s->player_info.admin.random_word = (uint16_t)((s->player_info.admin.random_word ^ tfmx_random_word(s)) + 0x4335); +} + +static void tfmx_random_play_mask(struct tfmx_state *s, struct tfmx_voice_state *v) { + tfmx_randomize(s); + v->rnd.arp.pos = (uint16_t)((s->player_info.admin.random_word & 0xff) & v->rnd.mask); +} + +static void tfmx_random_play_check_wait(struct tfmx_state *s, struct tfmx_voice_state *v) { + if((s->player_info.cmd.aa & 0x80) != 0) { + v->rnd.block_wait = 0; + } +} + +static void tfmx_random_play_reverb(struct tfmx_state *s, struct tfmx_voice_state *v) { + if(((v->rnd.mode & 2) == 0) || ((((int32_t)v->rnd.speed * 3) / 8) != v->rnd.count)) { + return; + } + struct tfmx_voice_state *v2 = &s->voices[(v->voice_num + 1) & 3]; + v2->volume = (int8_t)(((int32_t)v->volume * 5) / 8); + if(v->period != v2->period) { + v2->period = v->period; + v2->output_period = v->period; + tfmx_random_play_check_wait(s, v); + } +} + +static void tfmx_random_play_run(struct tfmx_state *s, struct tfmx_voice_state *v) { + if(v->rnd.flag == 0) { + return; + } else if(v->rnd.flag > 0) { + v->rnd.arp.offset = tfmx_get_macro_offset(s, v->rnd.macro); + v->rnd.arp.pos = 0; + v->rnd.flag = -1; + if((v->rnd.mode & 1) != 0) { + tfmx_random_play_mask(s, v); + } + } + + if(--v->rnd.count == 0) { + v->rnd.count = v->rnd.speed; + while(1) { + uint8_t aa = s->input.buf[v->rnd.arp.offset + v->rnd.arp.pos]; + s->player_info.cmd.aa = aa; + if(aa != 0) { + break; + } + if(v->rnd.arp.pos == 0) { + return; + } + v->rnd.arp.pos = 0; + } + } else { + tfmx_random_play_reverb(s, v); + return; + } + + uint16_t n = (uint16_t)(((int8_t)s->player_info.cmd.aa + v->note) & 0x3f); + if(n == 0) { + tfmx_random_play_mask(s, v); + return; + } + + uint16_t p = tfmx_note_to_period((int32_t)n); + int16_t finetune = v->detune; + if(finetune != 0) { + p = (uint16_t)((((int32_t)finetune + 0x100) * (int32_t)p) >> 8); + } + + if((v->rnd.mode & 1) == 0) { + v->period = p; + if(v->portamento.speed != 0) { + return; + } + v->output_period = p; + tfmx_random_play_check_wait(s, v); + v->rnd.arp.pos++; + return; + } + + tfmx_randomize(s); + if(((v->rnd.mode & 4) != 0) || ((v->rnd.arp.pos & 3) != 0) || ((s->player_info.admin.random_word & 0xff) > 16)) { + tfmx_random_play_check_wait(s, v); + v->period = p; + if(v->portamento.speed == 0) { + v->output_period = p; + } + } + v->rnd.arp.pos++; + if((s->player_info.cmd.aa & 0x40) == 0) { + return; + } + tfmx_randomize(s); + if((s->player_info.admin.random_word >> 8) > 6) { + tfmx_random_play_mask(s, v); + return; + } +} + +// [=]===^=[ tfmx_macro_func_random_play ]========================================================[=] +static void tfmx_macro_func_random_play(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + v->rnd.macro = c->bb; + v->rnd.speed = (int8_t)c->cd; + v->rnd.mode = c->ee; + v->rnd.count = 1; + v->rnd.flag = 1; + v->rnd.block_wait = 1; + tfmx_random_play_run(s, v); + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_split_key ]==========================================================[=] +static void tfmx_macro_func_split_key(struct tfmx_state *s, struct tfmx_voice_state *v) { + if(s->player_info.cmd.bb >= v->note) { + v->macro.step++; + } else { + v->macro.step = tfmx_make_word(s->player_info.cmd.cd, s->player_info.cmd.ee); + } + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_split_volume ]=======================================================[=] +static void tfmx_macro_func_split_volume(struct tfmx_state *s, struct tfmx_voice_state *v) { + if((int8_t)s->player_info.cmd.bb >= v->volume) { + v->macro.step++; + } else { + v->macro.step = tfmx_make_word(s->player_info.cmd.cd, s->player_info.cmd.ee); + } + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_random_mask ]========================================================[=] +static void tfmx_macro_func_random_mask(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->rnd.mask = s->player_info.cmd.bb; + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_set_prev_note ]======================================================[=] +static void tfmx_macro_func_set_prev_note(struct tfmx_state *s, struct tfmx_voice_state *v) { + tfmx_macro_func_add_note_sub(s, v, v->note_previous); + tfmx_macro_extra_wait(s, v); +} + +// [=]===^=[ tfmx_macro_func_play_macro ]=========================================================[=] +static void tfmx_macro_func_play_macro(struct tfmx_state *s, struct tfmx_voice_state *v) { + s->player_info.cmd.aa = v->note; + s->player_info.cmd.cd = (uint8_t)(s->player_info.cmd.cd | (v->note_volume << 4)); + tfmx_note_cmd_apply(s); + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_22 ]=================================================================[=] +static void tfmx_macro_func_22(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + v->add_begin_count = 0; + v->sid.source_offset = s->offsets.sample_data + tfmx_make_dword(0, c->bb, c->cd, c->ee); + v->sample.start = v->sid.source_offset; + tfmx_to_paula_start(s, v, s->offsets.sample_data + v->sid.target_offset); + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_23 ]=================================================================[=] +static void tfmx_macro_func_23(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + uint16_t len = tfmx_make_word(c->aa, c->bb); + if(len == 0) { + len = 0x100; + } + tfmx_to_paula_length(s, v, (uint16_t)(len >> 1)); + v->sid.target_length = (uint16_t)((len - 1) & 0xff); + uint16_t len2 = tfmx_make_word(c->cd, c->ee); + v->sid.source_length = len2; + v->sample.length = len2; + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_24 ]=================================================================[=] +static void tfmx_macro_func_24(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + v->sid.op3.offset = tfmx_make_dword(c->bb, c->cd, c->ee, 0); + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_25 ]=================================================================[=] +static void tfmx_macro_func_25(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + v->sid.op3.speed = tfmx_make_word(c->aa, c->bb); + v->sid.op3.count = v->sid.op3.speed; + v->sid.op3.delta = (int16_t)tfmx_make_word(c->cd, c->ee); + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_26 ]=================================================================[=] +static void tfmx_macro_func_26(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + v->sid.op2.offset = tfmx_make_dword(0, c->bb, c->cd, c->ee); + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_27 ]=================================================================[=] +static void tfmx_macro_func_27(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + v->sid.op2.speed = tfmx_make_word(c->aa, c->bb); + v->sid.op2.count = v->sid.op2.speed; + v->sid.op2.delta = (int16_t)tfmx_make_word(c->cd, c->ee); + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_28 ]=================================================================[=] +static void tfmx_macro_func_28(struct tfmx_state *s, struct tfmx_voice_state *v) { + struct tfmx_cmd *c = &s->player_info.cmd; + v->sid.op1.inter_delta = tfmx_make_word(c->ee, (uint8_t)(v->sid.op1.inter_delta & 0xff)); + v->sid.op1.inter_mod = (int16_t)(16 * (int8_t)c->cd); + v->sid.op1.speed = tfmx_make_word(c->aa, c->bb); + v->sid.op1.count = v->sid.op1.speed; + v->macro.step++; + s->player_info.macro_eval_again = 1; +} + +// [=]===^=[ tfmx_macro_func_29 ]=================================================================[=] +static void tfmx_macro_func_29(struct tfmx_state *s, struct tfmx_voice_state *v) { + v->macro.step++; + v->sid.target_length = 0; + if(s->player_info.cmd.bb != 0) { + v->sid.op1.speed = 0; + v->sid.op1.count = 0; + v->sid.op1.inter_delta = 0; + v->sid.op1.inter_mod = 0; + v->sid.op2.speed = 0; + v->sid.op2.count = 0; + v->sid.op2.offset = 0; + v->sid.op2.delta = 0; + v->sid.op3.speed = 0; + v->sid.op3.count = 0; + v->sid.op3.offset = 0; + v->sid.op3.delta = 0; + } + s->player_info.macro_eval_again = 1; +} + +// Modulation routines. + +// [=]===^=[ tfmx_mod_add_begin ]=================================================================[=] +static void tfmx_mod_add_begin(struct tfmx_state *s, struct tfmx_voice_state *v) { + if(s->variant.no_add_begin_count || (v->add_begin_count == 0)) { + return; + } + v->sample.start = (uint32_t)((int64_t)v->sample.start + v->add_begin_offset); + if(v->sample.start < s->offsets.sample_data) { + v->sample.start = s->offsets.sample_data; + } + if(v->sid.target_length != 0) { + v->sid.source_offset = v->sample.start; + } else { + tfmx_to_paula_start(s, v, v->sample.start); + } + if(--v->add_begin_count == 0) { + v->add_begin_count = v->add_begin_arg; + v->add_begin_offset = -v->add_begin_offset; + } +} + +// [=]===^=[ tfmx_mod_envelope ]==================================================================[=] +static void tfmx_mod_envelope(struct tfmx_state *s, struct tfmx_voice_state *v) { + (void)s; + if(v->envelope.flag == 0) { + return; + } + if(v->envelope.count > 0) { + v->envelope.count--; + return; + } + v->envelope.count = v->envelope.flag; + if(v->volume < (int8_t)v->envelope.target) { + v->volume = (int8_t)((int32_t)v->volume + (int32_t)v->envelope.speed); + if(v->volume >= (int8_t)v->envelope.target) { + v->volume = (int8_t)v->envelope.target; + v->envelope.flag = 0; + } + } else { + v->volume = (int8_t)((int32_t)v->volume - (int32_t)v->envelope.speed); + if(v->volume <= (int8_t)v->envelope.target) { + v->volume = (int8_t)v->envelope.target; + v->envelope.flag = 0; + } + } +} + +// [=]===^=[ tfmx_mod_vibrato ]===================================================================[=] +static void tfmx_mod_vibrato(struct tfmx_state *s, struct tfmx_voice_state *v) { + if(v->vibrato.time == 0) { + return; + } + v->vibrato.delta = (int16_t)((int32_t)v->vibrato.delta + (int32_t)v->vibrato.intensity); + uint16_t p; + if(s->variant.vibrato_unscaled) { + p = (uint16_t)((int32_t)v->period + (int32_t)v->vibrato.delta); + } else { + p = (uint16_t)(((0x800 + (int32_t)v->vibrato.delta) * (int32_t)v->period) >> 11); + } + if(v->portamento.speed == 0) { + v->output_period = p; + } + if(--v->vibrato.count == 0) { + v->vibrato.count = v->vibrato.time; + v->vibrato.intensity = (int8_t)(-(int32_t)v->vibrato.intensity); + } +} + +// [=]===^=[ tfmx_mod_portamento ]================================================================[=] +static void tfmx_mod_portamento(struct tfmx_state *s, struct tfmx_voice_state *v) { + uint16_t current; + uint16_t target; + uint8_t do_set; + + if((v->portamento.speed == 0) || (--v->portamento.wait != 0)) { + return; + } + v->portamento.wait = v->portamento.count; + current = v->portamento.period; + target = v->period; + do_set = 0; + if(current == target) { + v->portamento.speed = 0; + current = target; + do_set = 1; + } else if(current < target) { + if(s->variant.porta_unscaled) { + current = (uint16_t)(current + v->portamento.speed); + } else { + current = (uint16_t)((((int32_t)0x100 + v->portamento.speed) * (int32_t)current) >> 8); + } + if(current < target) { + do_set = 1; + } else { + v->portamento.speed = 0; + current = target; + do_set = 1; + } + } else { + if(s->variant.porta_unscaled) { + current = (uint16_t)(current - v->portamento.speed); + } + current = (uint16_t)((((int32_t)0x100 - v->portamento.speed) * (int32_t)current) >> 8); + if(current > target) { + do_set = 1; + } else { + v->portamento.speed = 0; + current = target; + do_set = 1; + } + } + if(do_set) { + current &= 0x7ff; + v->portamento.period = current; + v->output_period = current; + } +} + +// [=]===^=[ tfmx_mod_sid ]=======================================================================[=] +static void tfmx_mod_sid(struct tfmx_state *s, struct tfmx_voice_state *v) { + if(v->sid.target_length == 0) { + return; + } + uint8_t *p_target = s->input.buf + s->offsets.sample_data + v->sid.target_offset; + uint32_t curr_source_offset = 0; + uint32_t x = v->sid.op3.offset; + uint8_t d = (uint8_t)(v->sid.op1.inter_delta >> 8); + uint8_t dx = 0; + int16_t cur = (int16_t)v->sid.last_sample; + + int32_t i; + for(i = (int32_t)v->sid.target_length; i >= 0; --i) { + x += v->sid.op2.offset; + curr_source_offset += x; + uint32_t src_idx = v->sid.source_offset + ((curr_source_offset >> 16) & v->sid.source_length); + if(src_idx >= s->input.len) { + break; + } + int16_t sval = (int8_t)s->input.buf[src_idx]; + uint32_t tgt_idx = (uint32_t)(p_target - s->input.buf); + if(tgt_idx + 1 >= s->input.buf_len) { + break; + } + if((d == 0) || (sval == cur)) { + p_target[0] = (uint8_t)sval; + p_target[1] = (uint8_t)sval; + } else if(sval > cur) { + cur = (int16_t)((int32_t)cur + (int32_t)d + (int32_t)dx); + dx = (uint8_t)(cur > 0x7f ? 1 : 0); + if((cur > 0x7f) || (cur >= sval)) { + cur = sval; + p_target[0] = (uint8_t)sval; + p_target[1] = (uint8_t)sval; + } else { + p_target[0] = (uint8_t)cur; + p_target[1] = (uint8_t)cur; + } + } else if(sval < cur) { + cur = (int16_t)((int32_t)cur - (int32_t)d - (int32_t)dx); + dx = (uint8_t)(cur < -128 ? 1 : 0); + if((cur < -128) || (cur <= sval)) { + cur = sval; + p_target[0] = (uint8_t)sval; + p_target[1] = (uint8_t)sval; + } else { + p_target[0] = (uint8_t)cur; + p_target[1] = (uint8_t)cur; + } + } + } + + v->sid.last_sample = (int8_t)cur; + if(d != 0) { + v->sid.op1.inter_delta = (uint16_t)((int32_t)v->sid.op1.inter_delta + (int32_t)v->sid.op1.inter_mod); + if(--v->sid.op1.count == 0) { + v->sid.op1.count = v->sid.op1.speed; + v->sid.op1.inter_mod = (int16_t)(-(int32_t)v->sid.op1.inter_mod); + } + } + v->sid.op3.offset = (uint32_t)((int64_t)v->sid.op3.offset + v->sid.op3.delta); + if(--v->sid.op3.count == 0) { + v->sid.op3.count = v->sid.op3.speed; + if(v->sid.op3.count != 0) { + v->sid.op3.delta = (int16_t)(-(int32_t)v->sid.op3.delta); + } + } + v->sid.op2.offset = (uint32_t)((int64_t)v->sid.op2.offset + v->sid.op2.delta); + if(--v->sid.op2.count == 0) { + v->sid.op2.count = v->sid.op2.speed; + if(v->sid.op2.count != 0) { + v->sid.op2.delta = (int16_t)(-(int32_t)v->sid.op2.delta); + } + } +} + +// [=]===^=[ tfmx_fade_init ]=====================================================================[=] +static void tfmx_fade_init(struct tfmx_state *s, uint8_t target, uint8_t speed) { + if(s->player_info.fade.active) { + return; + } + s->player_info.fade.active = 1; + s->player_info.fade.target = target; + s->player_info.fade.count = speed; + s->player_info.fade.speed = speed; + if((s->player_info.fade.speed == 0) || (s->player_info.fade.volume == s->player_info.fade.target)) { + s->player_info.fade.volume = s->player_info.fade.target; + s->player_info.fade.delta = 0; + s->player_info.fade.active = 0; + return; + } + if(s->player_info.fade.volume < s->player_info.fade.target) { + s->player_info.fade.delta = 1; + } else { + s->player_info.fade.delta = -1; + } +} + +// [=]===^=[ tfmx_fade_apply ]====================================================================[=] +static void tfmx_fade_apply(struct tfmx_state *s, struct tfmx_voice_state *v) { + if((s->player_info.fade.delta != 0) && (--s->player_info.fade.count == 0)) { + s->player_info.fade.count = s->player_info.fade.speed; + s->player_info.fade.volume = (uint8_t)((int32_t)s->player_info.fade.volume + s->player_info.fade.delta); + if(s->player_info.fade.volume == s->player_info.fade.target) { + s->player_info.fade.delta = 0; + s->player_info.fade.active = 0; + } + } + int8_t vol = v->volume; + if(s->player_info.fade.volume < 64) { + vol = (int8_t)((4 * (int32_t)v->volume * (int32_t)s->player_info.fade.volume) / (4 * 0x40)); + } + v->paula_volume = (uint16_t)vol; +} + +// [=]===^=[ tfmx_process_modulation ]============================================================[=] +static void tfmx_process_modulation(struct tfmx_state *s, struct tfmx_voice_state *v) { + if(v->effects_mode > 0) { + tfmx_mod_add_begin(s, v); + tfmx_mod_sid(s, v); + tfmx_mod_vibrato(s, v); + tfmx_mod_portamento(s, v); + tfmx_mod_envelope(s, v); + } else if(v->effects_mode == 0) { + v->effects_mode = 1; + } + tfmx_random_play_run(s, v); + tfmx_fade_apply(s, v); +} + +// Macro dispatch table - constructed at runtime in tfmx_setup_dispatch(). + +// [=]===^=[ tfmx_macro_dispatch ]================================================================[=] +static void tfmx_macro_dispatch(struct tfmx_state *s, struct tfmx_voice_state *v, uint8_t cmd) { + uint8_t c = (uint8_t)(cmd & 0x3f); + switch(c) { + case 0x00: { tfmx_macro_func_stop_sound(s, v); break; } + case 0x01: { tfmx_macro_func_start_sample(s, v); break; } + case 0x02: { tfmx_macro_func_set_begin(s, v); break; } + case 0x03: { tfmx_macro_func_set_len(s, v); break; } + case 0x04: { tfmx_macro_func_wait(s, v); break; } + case 0x05: { tfmx_macro_func_loop(s, v); break; } + case 0x06: { tfmx_macro_func_cont(s, v); break; } + case 0x07: { tfmx_macro_func_stop(s, v); break; } + case 0x08: { tfmx_macro_func_add_note(s, v); break; } + case 0x09: { tfmx_macro_func_set_note(s, v); break; } + case 0x0a: { tfmx_macro_func_reset(s, v); break; } + case 0x0b: { tfmx_macro_func_portamento(s, v); break; } + case 0x0c: { tfmx_macro_func_vibrato(s, v); break; } + case 0x0d: { + if(s->variant.compressed) { + tfmx_macro_func_add_volume(s, v); + } else { + tfmx_macro_func_add_vol_note(s, v); + } + break; + } + case 0x0e: { tfmx_macro_func_set_volume(s, v); break; } + case 0x0f: { tfmx_macro_func_envelope(s, v); break; } + case 0x10: { tfmx_macro_func_loop_keyup(s, v); break; } + case 0x11: { tfmx_macro_func_add_begin(s, v); break; } + case 0x12: { tfmx_macro_func_add_len(s, v); break; } + case 0x13: { tfmx_macro_func_stop_sample(s, v); break; } + case 0x14: { tfmx_macro_func_wait_keyup(s, v); break; } + case 0x15: { tfmx_macro_func_goto(s, v); break; } + case 0x16: { tfmx_macro_func_return(s, v); break; } + case 0x17: { tfmx_macro_func_set_period(s, v); break; } + case 0x18: { tfmx_macro_func_sample_loop(s, v); break; } + case 0x19: { tfmx_macro_func_one_shot(s, v); break; } + case 0x1a: { tfmx_macro_func_wait_on_dma(s, v); break; } + case 0x1b: { tfmx_macro_func_random_play(s, v); break; } + case 0x1c: { tfmx_macro_func_split_key(s, v); break; } + case 0x1d: { tfmx_macro_func_split_volume(s, v); break; } + case 0x1e: { tfmx_macro_func_random_mask(s, v); break; } + case 0x1f: { tfmx_macro_func_set_prev_note(s, v); break; } + case 0x21: { tfmx_macro_func_play_macro(s, v); break; } + case 0x22: { tfmx_macro_func_22(s, v); break; } + case 0x23: { tfmx_macro_func_23(s, v); break; } + case 0x24: { tfmx_macro_func_24(s, v); break; } + case 0x25: { tfmx_macro_func_25(s, v); break; } + case 0x26: { tfmx_macro_func_26(s, v); break; } + case 0x27: { tfmx_macro_func_27(s, v); break; } + case 0x28: { tfmx_macro_func_28(s, v); break; } + case 0x29: { tfmx_macro_func_29(s, v); break; } + + default: { tfmx_macro_func_nop(s, v); break; } + } + s->macro_cmd_used[c] = 1; +} + +// [=]===^=[ tfmx_process_macro_main ]============================================================[=] +static void tfmx_process_macro_main(struct tfmx_state *s, struct tfmx_voice_state *v) { + if(v->macro.skip) { + return; + } + if(v->macro.wait > 0) { + v->macro.wait--; + return; + } + + int32_t macro_len = 0; + do { + s->player_info.macro_eval_again = 0; + uint32_t p = v->macro.offset + (v->macro.step << 2); + if(p + 3 >= s->input.len) { + v->macro.skip = 1; + return; + } + uint8_t command = s->input.buf[p]; + s->player_info.cmd.aa = 0; + s->player_info.cmd.bb = s->input.buf[p + 1]; + s->player_info.cmd.cd = s->input.buf[p + 2]; + s->player_info.cmd.ee = s->input.buf[p + 3]; + tfmx_macro_dispatch(s, v, command); + } while(s->player_info.macro_eval_again && (++macro_len < 32)); +} + +// Pattern command handlers. + +// [=]===^=[ tfmx_patt_cmd_nop ]==================================================================[=] +static void tfmx_patt_cmd_nop(struct tfmx_state *s, struct tfmx_track *tr) { + (void)s; + tr->pattern.step++; + tr->pattern.eval_next = 1; +} + +// [=]===^=[ tfmx_process_track_step_dispatch ]===================================================[=] +// We call into ProcessTrackStep recursively, so define forward via the +// "dispatch" trampoline declared near the bottom; here we forward-declare +// only via the same trick: a function pointer set during init. +// To keep the "no forward declarations" rule, we avoid that and instead +// embed the small required logic inline through a re-entrant guard. +// +// In practice, PattCmd_End triggers a track-step advance which we set +// via player_info.sequencer.step.next; the main Run loop re-invokes +// ProcessTrackStep when that flag is set. + +// [=]===^=[ tfmx_patt_cmd_end ]==================================================================[=] +static void tfmx_patt_cmd_end(struct tfmx_state *s, struct tfmx_track *tr) { + tr->pt = 0xff; + if(s->player_info.sequencer.step.current != s->player_info.sequencer.step.last) { + s->player_info.sequencer.step.current++; + } + if(s->player_info.sequencer.step.current == s->player_info.sequencer.step.last) { + s->song_end = 1; + s->trigger_restart = 1; + return; + } + s->player_info.sequencer.step.next = 1; +} + +// [=]===^=[ tfmx_patt_cmd_loop ]=================================================================[=] +static void tfmx_patt_cmd_loop(struct tfmx_state *s, struct tfmx_track *tr) { + if(tr->pattern.loops == 0) { + tr->pattern.loops = -1; + tr->pattern.step++; + tr->pattern.eval_next = 1; + return; + } else if(tr->pattern.loops == -1) { + tr->pattern.loops = (int8_t)(s->player_info.cmd.bb - 1); + if(tr->pattern.loops == -1) { + tr->pattern.infinite_loop = 1; + } + } else { + tr->pattern.loops--; + } + tr->pattern.step = tfmx_make_word(s->player_info.cmd.cd, s->player_info.cmd.ee); + tr->pattern.eval_next = 1; +} + +// [=]===^=[ tfmx_patt_cmd_goto ]=================================================================[=] +static void tfmx_patt_cmd_goto(struct tfmx_state *s, struct tfmx_track *tr) { + tr->pt = s->player_info.cmd.bb; + tr->pattern.offset = tfmx_get_patt_offset(s, tr->pt); + tr->pattern.step = tfmx_make_word(s->player_info.cmd.cd, s->player_info.cmd.ee); + tr->pattern.eval_next = 1; +} + +// [=]===^=[ tfmx_patt_cmd_wait ]=================================================================[=] +static void tfmx_patt_cmd_wait(struct tfmx_state *s, struct tfmx_track *tr) { + tr->pattern.wait = s->player_info.cmd.bb; + tr->pattern.step++; +} + +// [=]===^=[ tfmx_patt_cmd_stop ]=================================================================[=] +static void tfmx_patt_cmd_stop(struct tfmx_state *s, struct tfmx_track *tr) { + (void)s; + tr->pt = 0xff; +} + +// [=]===^=[ tfmx_patt_cmd_note ]=================================================================[=] +static void tfmx_patt_cmd_note(struct tfmx_state *s, struct tfmx_track *tr) { + tfmx_note_cmd_apply(s); + tr->pattern.step++; + tr->pattern.eval_next = 1; +} + +// [=]===^=[ tfmx_patt_cmd_save_and_goto ]========================================================[=] +static void tfmx_patt_cmd_save_and_goto(struct tfmx_state *s, struct tfmx_track *tr) { + tr->pattern.offset_saved = tr->pattern.offset; + tr->pattern.step_saved = tr->pattern.step; + tfmx_patt_cmd_goto(s, tr); +} + +// [=]===^=[ tfmx_patt_cmd_return_from_goto ]=====================================================[=] +static void tfmx_patt_cmd_return_from_goto(struct tfmx_state *s, struct tfmx_track *tr) { + (void)s; + tr->pattern.offset = tr->pattern.offset_saved; + tr->pattern.step = tr->pattern.step_saved; + tr->pattern.step++; + tr->pattern.eval_next = 1; +} + +// [=]===^=[ tfmx_patt_cmd_fade ]=================================================================[=] +static void tfmx_patt_cmd_fade(struct tfmx_state *s, struct tfmx_track *tr) { + tfmx_fade_init(s, s->player_info.cmd.ee, s->player_info.cmd.bb); + tr->pattern.step++; + tr->pattern.eval_next = 1; +} + +// [=]===^=[ tfmx_patt_dispatch ]=================================================================[=] +static void tfmx_patt_dispatch(struct tfmx_state *s, struct tfmx_track *tr, uint8_t command) { + switch(command) { + case 0x0: { tfmx_patt_cmd_end(s, tr); break; } + case 0x1: { tfmx_patt_cmd_loop(s, tr); break; } + case 0x2: { tfmx_patt_cmd_goto(s, tr); break; } + case 0x3: { tfmx_patt_cmd_wait(s, tr); break; } + case 0x4: { tfmx_patt_cmd_stop(s, tr); break; } + case 0x5: { tfmx_patt_cmd_note(s, tr); break; } + case 0x6: { tfmx_patt_cmd_note(s, tr); break; } + case 0x7: { tfmx_patt_cmd_note(s, tr); break; } + case 0x8: { tfmx_patt_cmd_save_and_goto(s, tr); break; } + case 0x9: { tfmx_patt_cmd_return_from_goto(s, tr); break; } + case 0xa: { tfmx_patt_cmd_fade(s, tr); break; } + case 0xc: { tfmx_patt_cmd_note(s, tr); break; } + case 0xe: { tfmx_patt_cmd_stop(s, tr); break; } + + default: { tfmx_patt_cmd_nop(s, tr); break; } + } +} + +// [=]===^=[ tfmx_process_pattern ]===============================================================[=] +static void tfmx_process_pattern(struct tfmx_state *s, struct tfmx_track *tr) { + int32_t eval_max_loops = TFMX_RECURSE_LIMIT; + do { + tr->pattern.eval_next = 0; + uint32_t p = tr->pattern.offset + ((uint32_t)tr->pattern.step << 2); + if(p + 3 >= s->input.len) { + tr->pt = 0xff; + return; + } + s->player_info.cmd.aa = s->input.buf[p]; + s->player_info.cmd.bb = s->input.buf[p + 1]; + s->player_info.cmd.cd = s->input.buf[p + 2]; + s->player_info.cmd.ee = s->input.buf[p + 3]; + uint8_t aa_bak = s->player_info.cmd.aa; + if(s->player_info.cmd.aa < 0xf0) { + if((s->player_info.cmd.aa < 0xc0) && (s->player_info.cmd.aa >= 0x7f)) { + tr->pattern.wait = s->player_info.cmd.ee; + s->player_info.cmd.ee = 0; + } + s->player_info.cmd.aa = (uint8_t)(s->player_info.cmd.aa + tr->tr); + if(aa_bak < 0xc0) { + s->player_info.cmd.aa &= 0x3f; + } + if(tr->on) { + tfmx_note_cmd_apply(s); + } + if((aa_bak >= 0xc0) || (aa_bak < 0x7f)) { + tr->pattern.step++; + tr->pattern.eval_next = 1; + } else { + tr->pattern.step++; + } + } else { + uint8_t command = (uint8_t)(s->player_info.cmd.aa & 0xf); + s->pattern_cmd_used[command] = 1; + tfmx_patt_dispatch(s, tr, command); + } + } while(tr->pattern.eval_next && (--eval_max_loops > 0)); +} + +// [=]===^=[ tfmx_process_pttr ]==================================================================[=] +static void tfmx_process_pttr(struct tfmx_state *s, struct tfmx_track *tr) { + if(tr->pt < 0x90) { + if(tr->pattern.offset == 0) { + tr->pt = 0xff; + return; + } + if(tr->pattern.wait == 0) { + tfmx_process_pattern(s, tr); + } else { + tr->pattern.wait--; + } + } else { + if(tr->pt == 0xfe) { + tr->pt = 0xff; + uint8_t v_num = s->channel_to_voice_map[tr->tr & (TFMX_VOICES_MAX - 1)]; + struct tfmx_voice_state *v = &s->voices[v_num]; + v->macro.skip = 1; + tfmx_paula_off(s, v); + } + } +} + +// Track command handlers. + +// [=]===^=[ tfmx_track_cmd_stop ]================================================================[=] +static void tfmx_track_cmd_stop(struct tfmx_state *s, uint32_t step_offset) { + (void)step_offset; + s->song_end = 1; + s->trigger_restart = 1; +} + +// [=]===^=[ tfmx_track_cmd_loop ]================================================================[=] +static void tfmx_track_cmd_loop(struct tfmx_state *s, uint32_t step_offset) { + if(s->player_info.sequencer.loops == 0) { + s->player_info.sequencer.loops = -1; + s->player_info.sequencer.step.current++; + } else if(s->player_info.sequencer.loops < 0) { + s->player_info.sequencer.step.current = (int32_t)tfmx_read_be_u16(s->input.buf, step_offset); + s->player_info.sequencer.loops = (int16_t)(tfmx_read_be_u16(s->input.buf, step_offset + 2) - 1); + if((s->player_info.sequencer.step.current > (TFMX_TRACK_STEPS_MAX - 1)) || + (s->player_info.sequencer.step.current > s->player_info.sequencer.step.last)) { + s->song_end = 1; + s->trigger_restart = 1; + } else if(s->player_info.sequencer.step_seen_before[s->player_info.sequencer.step.current] && (s->player_info.sequencer.loops < 0)) { + s->song_end = 1; + } + if((s->player_info.sequencer.loops == 0xeff) || (s->player_info.sequencer.loops > 0x100)) { + s->player_info.sequencer.loops = 0; + } + } else { + s->player_info.sequencer.loops--; + s->player_info.sequencer.step.current = (int32_t)tfmx_read_be_u16(s->input.buf, step_offset); + } + s->player_info.sequencer.eval_next = 1; +} + +// [=]===^=[ tfmx_track_cmd_speed ]===============================================================[=] +static void tfmx_track_cmd_speed(struct tfmx_state *s, uint32_t step_offset) { + s->player_info.admin.speed = (int16_t)tfmx_read_be_u16(s->input.buf, step_offset); + s->player_info.admin.count = s->player_info.admin.speed; + uint16_t arg2 = (uint16_t)(0x81ff & tfmx_read_be_u16(s->input.buf, step_offset + 2)); + if((arg2 != 0) && (arg2 < 0x200)) { + if(arg2 < 32) { + arg2 = 125; + } + tfmx_set_rate(s, arg2); + } + s->player_info.sequencer.step.current++; + s->player_info.sequencer.eval_next = 1; +} + +// [=]===^=[ tfmx_track_cmd_7v ]==================================================================[=] +static void tfmx_track_cmd_7v(struct tfmx_state *s, uint32_t step_offset) { + int16_t arg2 = (int16_t)tfmx_read_be_u16(s->input.buf, step_offset + 2); + if(arg2 >= 0) { + int8_t x = (int8_t)s->input.buf[step_offset + 3]; + if(x < -0x20) { + x = -0x20; + } + tfmx_set_bpm(s, (uint16_t)((125 * 100) / (100 + x))); + } + s->track_cmd_used[3] = 1; + s->player_info.sequencer.step.current++; + s->player_info.sequencer.eval_next = 1; +} + +// [=]===^=[ tfmx_track_cmd_fade ]================================================================[=] +static void tfmx_track_cmd_fade(struct tfmx_state *s, uint32_t step_offset) { + tfmx_fade_init(s, s->input.buf[step_offset + 3], s->input.buf[step_offset + 1]); + s->player_info.sequencer.step.current++; + s->player_info.sequencer.eval_next = 1; +} + +// [=]===^=[ tfmx_track_dispatch ]================================================================[=] +static void tfmx_track_dispatch(struct tfmx_state *s, uint8_t command, uint32_t step_offset) { + switch(command) { + case 0: { tfmx_track_cmd_stop(s, step_offset); break; } + case 1: { tfmx_track_cmd_loop(s, step_offset); break; } + case 2: { tfmx_track_cmd_speed(s, step_offset); break; } + case 3: { tfmx_track_cmd_7v(s, step_offset); break; } + case 4: { tfmx_track_cmd_fade(s, step_offset); break; } + + default: { tfmx_track_cmd_stop(s, step_offset); break; } + } + s->track_cmd_used[command] = 1; +} + +// [=]===^=[ tfmx_process_track_step ]============================================================[=] +static void tfmx_process_track_step(struct tfmx_state *s) { + int32_t eval_max_loops = TFMX_RECURSE_LIMIT; + do { + s->player_info.sequencer.eval_next = 0; + if(s->player_info.sequencer.step.current > (TFMX_TRACK_STEPS_MAX - 1)) { + s->player_info.sequencer.step.current = s->player_info.sequencer.step.first; + } + s->player_info.sequencer.step_seen_before[s->player_info.sequencer.step.current] = 1; + uint32_t step_offset = s->offsets.track_table + ((uint32_t)s->player_info.sequencer.step.current << 4); + if(step_offset + 16 > s->input.len) { + s->song_end = 1; + s->trigger_restart = 1; + return; + } + if(0xeffe == tfmx_read_be_u16(s->input.buf, step_offset)) { + step_offset += 2; + uint16_t command = tfmx_read_be_u16(s->input.buf, step_offset); + step_offset += 2; + if(command > TFMX_TRACK_CMD_MAX) { + command = 0; + } + tfmx_track_dispatch(s, (uint8_t)command, step_offset); + } else { + uint8_t t; + for(t = 0; t < s->player_info.sequencer.tracks; ++t) { + struct tfmx_track *tr = &s->player_info.track[t]; + tr->pt = s->input.buf[step_offset++]; + tr->tr = (int8_t)s->input.buf[step_offset++]; + if(tr->pt < 0x80) { + tr->pattern.offset = tfmx_get_patt_offset(s, tr->pt); + tr->pattern.step = 0; + tr->pattern.wait = 0; + tr->pattern.loops = -1; + tr->pattern.infinite_loop = 0; + } + } + } + } while(s->player_info.sequencer.eval_next && (--eval_max_loops > 0)); +} + +// [=]===^=[ tfmx_set_tfmx_v1 ]===================================================================[=] +static void tfmx_set_tfmx_v1(struct tfmx_state *s) { + s->variant.no_add_begin_count = 1; + s->variant.vibrato_unscaled = 1; + s->variant.finetune_unscaled = 1; + s->variant.porta_unscaled = 0; + s->variant.porta_override = 1; + // In V1 mode, dispatch case 0x0d uses AddVolume directly. + // We model this via a flag in dispatch (variant.compressed acts as the + // flag in our implementation, but here we explicitly want AddVolume). + // Re-use compressed flag for that switching. + s->variant.compressed = 1; +} + +// [=]===^=[ tfmx_traits_by_checksum ]============================================================[=] +static void tfmx_traits_by_checksum(struct tfmx_state *s) { + uint32_t p0 = s->offsets.header + tfmx_read_be_u32(s->input.buf, s->offsets.patterns); + if(p0 + 0x100 > s->input.len) { + return; + } + uint32_t crc1 = tfmx_crc32(s->input.buf, p0, 0x100); + if((crc1 == 0x48960d8c) || (crc1 == 0x5dcd624f) || (crc1 == 0x3f0b151f)) { + tfmx_set_tfmx_v1(s); + s->variant.no_note_detune = 1; + s->variant.porta_unscaled = 0; + } else if((crc1 == 0x27f8998c) || (crc1 == 0x26447707) || (crc1 == 0xd404651b) || (crc1 == 0xb5348633)) { + tfmx_set_tfmx_v1(s); + s->variant.porta_unscaled = 1; + } else if(crc1 == 0x8ac70fc8) { + tfmx_set_tfmx_v1(s); + } else if(crc1 == 0xa8566760) { + s->variant.no_track_mute = 1; + } else if(crc1 == 0xab1a6c6e) { + s->variant.no_track_mute = 1; + } else if(crc1 == 0x76f8aa6e) { + s->variant.bpm_speed5 = 1; + } +} + +// [=]===^=[ tfmx_reset_voices ]==================================================================[=] +static void tfmx_reset_voices(struct tfmx_state *s) { + uint8_t t; + uint8_t v; + + s->player_info.cmd.aa = 0; + s->player_info.cmd.bb = 0; + s->player_info.cmd.cd = 0; + s->player_info.cmd.ee = 0; + + for(t = 0; t < s->player_info.sequencer.tracks; ++t) { + struct tfmx_track *tr = &s->player_info.track[t]; + tr->on = tfmx_get_track_mute(s, t); + tr->pt = 0xff; + tr->tr = 0; + tr->pattern.offset = 0; + tr->pattern.step = 0; + tr->pattern.wait = 0; + tr->pattern.loops = -1; + tr->pattern.infinite_loop = 0; + } + + for(v = 0; v < (uint8_t)s->voice_count; ++v) { + struct tfmx_voice_state *vs = &s->voices[v]; + memset(vs, 0, sizeof(*vs)); + vs->voice_num = v; + vs->macro.wait = 1; + vs->macro.skip = 1; + vs->macro.loop = 0xff; + vs->macro.extra_wait = 1; + vs->key_up = 1; + vs->wait_on_dma_count = -1; + vs->sid.target_offset = (uint32_t)(0x100u * v) + 4u; + tfmx_paula_off(s, vs); + tfmx_to_paula_length(s, vs, 1); + tfmx_to_paula_start(s, vs, s->offsets.silence); + vs->paula_volume = 0; + vs->paula_period = 0; + } + + memset(s->track_cmd_used, 0, sizeof(s->track_cmd_used)); + memset(s->pattern_cmd_used, 0, sizeof(s->pattern_cmd_used)); + memset(s->macro_cmd_used, 0, sizeof(s->macro_cmd_used)); +} + +// [=]===^=[ tfmx_soft_restart ]==================================================================[=] +static void tfmx_soft_restart(struct tfmx_state *s) { + uint8_t v; + + s->song_end = 0; + s->tick_fp_add = 0; + s->trigger_restart = 0; + s->frames_until_tick_fp = 0; + s->player_info.sequencer.step.next = 0; + s->player_info.sequencer.loops = -1; + memset(s->player_info.sequencer.step_seen_before, 0, sizeof(s->player_info.sequencer.step_seen_before)); + + for(v = 0; v < (uint8_t)s->voice_count; ++v) { + s->voices[v].key_up = 1; + s->voices[v].volume = 0; + } + + s->player_info.fade.active = 0; + s->player_info.fade.volume = 64; + s->player_info.fade.target = 64; + s->player_info.fade.delta = 0; + + uint32_t so = (uint32_t)s->v_songs[s->player_info.admin.start_song] << 1; + s->player_info.sequencer.step.first = (int32_t)tfmx_read_be_u16(s->input.buf, s->offsets.header + 0x100u + so); + s->player_info.sequencer.step.current = s->player_info.sequencer.step.first; + s->player_info.sequencer.step.last = (int32_t)tfmx_read_be_u16(s->input.buf, s->offsets.header + 0x140u + so); + s->player_info.admin.speed = (int16_t)tfmx_read_be_u16(s->input.buf, s->offsets.header + 0x180u + so); + + if(s->player_info.admin.speed >= 0x10) { + tfmx_set_bpm(s, (uint16_t)s->player_info.admin.speed); + s->player_info.admin.speed = 0; + if(s->variant.bpm_speed5) { + s->player_info.admin.speed = 5; + } + } + s->player_info.admin.start_speed = s->player_info.admin.speed; + s->player_info.admin.count = 0; + + tfmx_process_track_step(s); +} + +// [=]===^=[ tfmx_restart ]=======================================================================[=] +static void tfmx_restart(struct tfmx_state *s) { + tfmx_reset_voices(s); + tfmx_soft_restart(s); +} + +// [=]===^=[ tfmx_run ]===========================================================================[=] +// One player tick. Mirrors TfmxDecoder.Run: advances macros, modulation, +// applies new period/volume to paula, advances the track sequencer when +// the speed counter hits zero. +static void tfmx_run(struct tfmx_state *s) { + uint8_t v; + + if(!s->player_info.admin.initialized) { + return; + } + + s->real_song_end = 0; + + for(v = 0; v < (uint8_t)s->voice_count; ++v) { + if(!s->song_end || s->loop_mode) { + struct tfmx_voice_state *vs = &s->voices[v]; + if(vs->wait_on_dma_count >= 0) { + uint16_t x = vs->paula_loop_count; + uint16_t y = vs->wait_on_dma_prev_loops; + int32_t d; + if(x >= y) { + d = (int32_t)(x - y); + } else { + d = (int32_t)x + (0x10000 - (int32_t)y); + } + if(d > vs->wait_on_dma_count) { + vs->macro.skip = 0; + vs->wait_on_dma_count = -1; + } else { + vs->wait_on_dma_count = (int16_t)((int32_t)vs->wait_on_dma_count - d); + vs->wait_on_dma_prev_loops = vs->paula_loop_count; + } + } + tfmx_process_macro_main(s, vs); + tfmx_process_modulation(s, vs); + vs->paula_period = vs->output_period; + } + } + + if(!s->song_end || s->loop_mode) { + if(--s->player_info.admin.count < 0) { + s->player_info.admin.count = s->player_info.admin.speed; + do { + s->player_info.sequencer.step.next = 0; + int32_t count_inactive = 0; + int32_t count_infinite = 0; + uint8_t t; + for(t = 0; t < s->player_info.sequencer.tracks; ++t) { + struct tfmx_track *tr = &s->player_info.track[t]; + tr->on = tfmx_get_track_mute(s, t); + if(tr->pt >= 0x90) { + count_inactive++; + } else if(tr->pattern.infinite_loop) { + count_infinite++; + } + tfmx_process_pttr(s, tr); + if(s->player_info.sequencer.step.next) { + break; + } + } + if(!s->player_info.sequencer.step.next) { + if((count_inactive == s->player_info.sequencer.tracks) || + ((count_inactive + count_infinite) == s->player_info.sequencer.tracks)) { + s->song_end = 1; + s->trigger_restart = 1; + } + } + if(s->player_info.sequencer.step.next) { + tfmx_process_track_step(s); + } + } while(s->player_info.sequencer.step.next); + } + } + + if(s->song_end && s->loop_mode) { + s->song_end = 0; + if(s->trigger_restart) { + tfmx_soft_restart(s); + } + s->real_song_end = 1; + } + + // Push period/volume to paula channels each tick. + for(v = 0; v < (uint8_t)s->voice_count; ++v) { + struct tfmx_voice_state *vs = &s->voices[v]; + if(vs->paula_dma_on) { + tfmx_pch_set_period(s, (int32_t)vs->voice_num, vs->paula_period == 0 ? 0x100 : vs->paula_period); + uint16_t vol = vs->paula_volume > 64 ? 64 : vs->paula_volume; + tfmx_pch_set_volume(s, (int32_t)vs->voice_num, vol); + } + // Track Paula loop count for WaitOnDMA: increment when the channel + // has wrapped at least one full length while DMA-on. We approximate + // this by simply incrementing once per tick if the channel is on. + if(vs->paula_dma_on) { + vs->paula_loop_count++; + } + } +} + +// [=]===^=[ tfmx_find_songs ]====================================================================[=] +// Mirrors FindSongs(): collects valid (sub-)song definitions from the song +// table at offset 0x100 / 0x140 / 0x180. +static void tfmx_find_songs(struct tfmx_state *s) { + int32_t so; + s->v_songs_count = 0; + + struct seen { int32_t s1; int32_t s2; int32_t s3; }; + struct seen seen[32]; + uint32_t seen_count = 0; + + for(so = 0; so < 32; ++so) { + int32_t s1 = (int32_t)tfmx_read_be_u16(s->input.buf, s->offsets.header + 0x100u + ((uint32_t)so << 1)); + int32_t s2 = (int32_t)tfmx_read_be_u16(s->input.buf, s->offsets.header + 0x140u + ((uint32_t)so << 1)); + int32_t s3 = (int32_t)tfmx_read_be_u16(s->input.buf, s->offsets.header + 0x180u + ((uint32_t)so << 1)); + if((s1 > s2) || (s1 > 0x1ff) || (s2 > 0x1ff) || ((so > 1) && ((s1 == 0x1ff) || (s2 == 0x1ff)))) { + continue; + } + uint8_t skip = 0; + uint32_t i; + for(i = 0; i < seen_count; ++i) { + if((seen[i].s1 == s1) && (s2 == seen[i].s1)) { + skip = 1; + break; + } + } + if(skip) { + continue; + } + uint8_t dup = 0; + for(i = 0; i < seen_count; ++i) { + if((seen[i].s1 == s1) && (seen[i].s2 == s2) && (seen[i].s3 == s3)) { + dup = 1; + break; + } + } + if(!dup && (s->v_songs_count < sizeof(s->v_songs))) { + s->v_songs[s->v_songs_count++] = (uint8_t)so; + if(seen_count < 32) { + seen[seen_count].s1 = s1; + seen[seen_count].s2 = s2; + seen[seen_count].s3 = s3; + seen_count++; + } + } + } +} + +// [=]===^=[ tfmx_detect_single_file ]============================================================[=] +// Returns header offset for single-file formats (TFHD/TFMXPAK/TFMX-MOD), or 0 +// for "no single-file header" (in which case the data starts at offset 0 +// with mdat content). Sets input.mdat_size / smpl_size / version_hint / +// start_song_hint when the format provides them. +static uint32_t tfmx_detect_single_file(struct tfmx_state *s) { + uint8_t *buf = s->input.buf; + uint32_t len = s->input.len; + s->input.version_hint = 0; + s->input.start_song_hint = -1; + s->input.mdat_size = 0; + s->input.smpl_size = 0; + + if((len >= 0x12) && (buf[0] == 'T') && (buf[1] == 'F') && (buf[2] == 'H') && (buf[3] == 'D')) { + if(tfmx_read_be_u16(buf, 4) != 0) { + return 0; + } + uint32_t hdr = tfmx_read_be_u32(buf, 4); + s->input.version_hint = buf[8]; + s->input.mdat_size = tfmx_read_be_u32(buf, 10); + s->input.smpl_size = tfmx_read_be_u32(buf, 14); + if((s->input.mdat_size == 0) || (s->input.smpl_size == 0)) { + return 0; + } + return hdr; + } + + if((len >= 32) && (buf[0] == 'T') && (buf[1] == 'F') && (buf[2] == 'M') && (buf[3] == 'X') && (buf[4] == 'P') && (buf[5] == 'A') && (buf[6] == 'K')) { + uint32_t i; + uint32_t mdat = 0; + uint32_t smpl = 0; + for(i = 7; i < 32; ++i) { + while((i < 32) && (buf[i] == ' ')) { + ++i; + } + uint32_t n = 0; + while((i < 32) && (buf[i] >= '0') && (buf[i] <= '9')) { + n = n * 10 + (uint32_t)(buf[i] - '0'); + ++i; + } + if(mdat == 0) { + mdat = n; + } else if(smpl == 0) { + smpl = n; + } else { + break; + } + if((i < 32) && (buf[i] == '>')) { + break; + } + } + for(i = 8; i < 32; ++i) { + if((i + 2 < len) && (buf[i] == '>') && (buf[i + 1] == '>') && (buf[i + 2] == '>')) { + if((mdat == 0) || (smpl == 0)) { + return 0; + } + s->input.mdat_size = mdat; + s->input.smpl_size = smpl; + return i + 3; + } + } + return 0; + } + + if((len >= 16) && (buf[0] == 'T') && (buf[1] == 'F') && (buf[2] == 'M') && (buf[3] == 'X') && (buf[4] == '-') && (buf[5] == 'M') && (buf[6] == 'O') && (buf[7] == 'D')) { + // TFMX-MOD uses LITTLE-endian offsets. + if((buf[11] != 0) || (buf[15] != 0)) { + return 0; + } + uint32_t sample_data = tfmx_make_dword(buf[11], buf[10], buf[9], buf[8]); + uint32_t hdr = 8 + 12; + s->input.mdat_size = sample_data - hdr; + uint32_t offs = tfmx_make_dword(buf[15], buf[14], buf[13], buf[12]); + s->input.smpl_size = offs - s->input.mdat_size; + // We could parse the meta blocks here for start_song_hint; skipped. + (void)offs; + s->offsets.sample_data = sample_data; + return hdr; + } + + return 0; +} + +// [=]===^=[ tfmx_detect_naked ]==================================================================[=] +// Detect a non-tagged TFMX (just starts with "TFMX " or "TFMX-SONG" / +// "TFMX_SONG" or lowercase "tfmxsong" tag). +static uint8_t tfmx_detect_naked(uint8_t *buf, uint32_t len) { + if(len < 16) { + return 0; + } + if((buf[0] == 'T') && (buf[1] == 'F') && (buf[2] == 'M') && (buf[3] == 'X') && (buf[4] == 0x20)) { + return 1; + } + if((buf[0] == 'T') && (buf[1] == 'F') && (buf[2] == 'M') && (buf[3] == 'X') && (buf[4] == '-') && + (buf[5] == 'S') && (buf[6] == 'O') && (buf[7] == 'N') && (buf[8] == 'G')) { + return 1; + } + if((buf[0] == 'T') && (buf[1] == 'F') && (buf[2] == 'M') && (buf[3] == 'X') && (buf[4] == '_') && + (buf[5] == 'S') && (buf[6] == 'O') && (buf[7] == 'N') && (buf[8] == 'G')) { + return 1; + } + if((buf[0] == 't') && (buf[1] == 'f') && (buf[2] == 'm') && (buf[3] == 'x') && + (buf[4] == 's') && (buf[5] == 'o') && (buf[6] == 'n') && (buf[7] == 'g')) { + return 1; + } + return 0; +} + +// [=]===^=[ tfmx_init_decoder ]==================================================================[=] +// Build state from raw input buffer (already loaded into s->input.buf). +static int32_t tfmx_init_decoder(struct tfmx_state *s) { + uint32_t v_idx; + + for(v_idx = 0; v_idx < TFMX_VOICES_MAX; ++v_idx) { + s->channel_to_voice_map[v_idx] = (uint8_t)(v_idx & 3); + } + + s->offsets.header = tfmx_detect_single_file(s); + if(s->offsets.header == 0) { + if(!tfmx_detect_naked(s->input.buf, s->input.len)) { + return 0; + } + // For a naked file, mdat_size is approximated by the embedded header + // offsets. We assume the entire file is the mdat (samples either + // inline as appended raw 8-bit data or absent entirely). + s->input.mdat_size = s->input.len; + s->input.smpl_size = 0; + } + + uint32_t h = s->offsets.header; + uint32_t o1 = tfmx_read_be_u32(s->input.buf, h + 0x1d0); + uint32_t o2 = tfmx_read_be_u32(s->input.buf, h + 0x1d4); + uint32_t o3 = tfmx_read_be_u32(s->input.buf, h + 0x1d8); + + if((o1 | o2 | o3) != 0) { + s->variant.compressed = 1; + } else { + o1 = 0x800; + o2 = 0x400; + o3 = 0x600; + } + if((o1 >= s->input.len) || (o2 >= s->input.len) || (o3 >= s->input.len)) { + o1 = tfmx_byteswap_u32(o1); + o2 = tfmx_byteswap_u32(o2); + o3 = tfmx_byteswap_u32(o3); + } + s->offsets.track_table = h + o1; + s->offsets.patterns = h + o2; + s->offsets.macros = h + o3; + + if((s->offsets.track_table >= s->input.len) || (s->offsets.patterns >= s->input.len) || (s->offsets.macros >= s->input.len)) { + return 0; + } + + s->offsets.sample_data = h + s->input.mdat_size; + if(s->offsets.sample_data >= s->input.buf_len) { + // No inline sample data; create a small zero-padded extension. + uint32_t need = s->offsets.sample_data + 16; + if(need > s->input.buf_len) { + uint8_t *new_buf = (uint8_t *)realloc(s->input.buf, need); + if(!new_buf) { + return 0; + } + memset(new_buf + s->input.buf_len, 0, need - s->input.buf_len); + s->input.buf = new_buf; + s->input.buf_len = need; + s->input.len = need; + } + } + uint32_t o = s->offsets.sample_data; + if(o + 4 <= s->input.buf_len) { + s->input.buf[o] = 0; + s->input.buf[o + 1] = 0; + s->input.buf[o + 2] = 0; + s->input.buf[o + 3] = 0; + } + s->offsets.silence = s->offsets.sample_data; + + tfmx_set_rate(s, (uint32_t)TFMX_DEFAULT_TICK_HZ << 8); + s->voice_count = 4; + s->player_info.sequencer.tracks = 8; + s->player_info.sequencer.step.size = 16; + + memset(&s->variant, 0, sizeof(s->variant)); + + if(((tfmx_read_be_u32(s->input.buf, s->offsets.header) == 0x54464d58u) && (s->input.buf[s->offsets.header + 4] == 0x20)) || + (s->input.version_hint == 1)) { + tfmx_set_tfmx_v1(s); + } + + tfmx_traits_by_checksum(s); + tfmx_find_songs(s); + + if(s->v_songs_count == 0) { + return 0; + } + + if(s->input.start_song_hint >= 0) { + s->v_songs_count = 1; + s->v_songs[0] = (uint8_t)s->input.start_song_hint; + } + + s->player_info.admin.initialized = 1; + s->player_info.admin.start_song = 0; + + tfmx_restart(s); + + // Pre-scan for trackCmd 7V usage to enable 7-voice mapping. + uint8_t loop_bak = s->loop_mode; + s->loop_mode = 0; + int32_t i; + for(i = 0; i < TFMX_DEFAULT_TICK_HZ * 60; ++i) { + tfmx_run(s); + if(s->song_end) { + break; + } + } + s->loop_mode = loop_bak; + + if(s->track_cmd_used[3] || (s->input.version_hint == 3)) { + s->voice_count = 8; + s->channel_to_voice_map[3] = 7; + s->channel_to_voice_map[4] = 3; + s->channel_to_voice_map[5] = 4; + s->channel_to_voice_map[6] = 5; + s->channel_to_voice_map[7] = 6; + } + + // Reset to start of song after the prescan. + s->player_info.admin.start_song = 0; + tfmx_restart(s); + + // Stereo is the fixed Paula wiring: voices 0,3 -> left, 1,2 -> right + // (Pan4). In 7V the three hardware voices are 0,1,2 and voices >=3 are + // CPU-summed into channel 3 (left), so the image is intrinsic to the + // channel assignment -- no per-voice pan field exists any more. + return 1; +} + +// [=]===^=[ tfmx_init ]==========================================================================[=] +// Public API entry. The caller owns `data`; we copy it into our own buffer +// because the engine writes a 4-byte silence marker at offsets.SampleData, +// and may need to extend the buffer when no inline samples are present. +static struct tfmx_state *tfmx_init(void *data, uint32_t len, int32_t sample_rate) { + if(!data || (len < 512) || (sample_rate <= 0)) { + return 0; + } + + struct tfmx_state *s = (struct tfmx_state *)calloc(1, sizeof(*s)); + if(!s) { + return 0; + } + + s->sample_rate = sample_rate; + paula_init(&s->paula, sample_rate, TFMX_DEFAULT_TICK_HZ); + s->softmix_period = TFMX_7V_CH3_PERIOD; + + s->input.buf_len = len; + s->input.len = len; + s->input.buf = (uint8_t *)malloc(len); + if(!s->input.buf) { + free(s); + return 0; + } + memcpy(s->input.buf, data, len); + + s->loop_mode = 1; + + if(!tfmx_init_decoder(s)) { + free(s->input.buf); + free(s); + return 0; + } + + return s; +} + +// [=]===^=[ tfmx_free ]==========================================================================[=] +static void tfmx_free(struct tfmx_state *s) { + if(!s) { + return; + } + if(s->input.buf) { + free(s->input.buf); + } + if(s->softmix_buf) { + free(s->softmix_buf); + } + free(s); +} + +// [=]===^=[ tfmx_get_audio ]=====================================================================[=] +// Sample-rate-driven mixing loop. We service ticks at the rate set by the +// engine (default 50 Hz, but track commands change it via SetRate/SetBpm). +static void tfmx_get_audio(struct tfmx_state *s, float *output, int32_t frames) { + if(!s || !output || (frames <= 0)) { + return; + } + memset(output, 0, sizeof(float) * 2 * (size_t)frames); + + int32_t produced = 0; + while(produced < frames) { + if(s->frames_until_tick_fp == 0) { + tfmx_run(s); + if(s->voice_count >= 7) { + tfmx_softmix_refill(s); + } + s->frames_until_tick_fp = s->frames_per_tick_fp; + } + uint32_t avail_ticks_fp = s->frames_until_tick_fp; + int32_t avail_frames = (int32_t)(avail_ticks_fp >> 16); + if(avail_frames <= 0) { + avail_frames = 1; + } + int32_t need = frames - produced; + int32_t chunk = avail_frames < need ? avail_frames : need; + paula_mix_frames(&s->paula, output + produced * 2, chunk); + produced += chunk; + uint32_t consumed_fp = (uint32_t)chunk << 16; + if(consumed_fp >= s->frames_until_tick_fp) { + s->frames_until_tick_fp = 0; + } else { + s->frames_until_tick_fp -= consumed_fp; + } + } +} + +// player_api thunks. + +// [=]===^=[ tfmx_api_init ]======================================================================[=] +static void *tfmx_api_init(void *data, uint32_t len, int32_t sample_rate) { + return (void *)tfmx_init(data, len, sample_rate); +} + +// [=]===^=[ tfmx_api_free ]======================================================================[=] +static void tfmx_api_free(void *state) { + tfmx_free((struct tfmx_state *)state); +} + +// [=]===^=[ tfmx_api_get_audio ]=================================================================[=] +static void tfmx_api_get_audio(void *state, float *output, int32_t frames) { + tfmx_get_audio((struct tfmx_state *)state, output, frames); +} + +static struct player_api tfmx_api = { + "TFMX", + tfmx_extensions, + tfmx_api_init, + tfmx_api_free, + tfmx_api_get_audio, + 0, +};