// 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, };