Files
tony-remake/tools/tfmx_player/tfmx.h
T

2739 lines
93 KiB
C

// 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 <stdint.h>
#include <stdlib.h>
#include <string.h>
#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,
};