/*
 * Adplug - Replayer for many OPL2/OPL3 audio file formats.
 * Copyright (C) 1999 - 2008 Simon Peter, <dn.tlp@gmx.net>, et al.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * a2m-v2.cpp - Adlib Tracker II Player by Dmitry Smagin <dmitry.s.smagin@gmail.com>
 *              Originally by Stanislav Baranec <subz3ro.altair@gmail.com>
 *
 * NOTES:
 * This player loads a2m and a2t modules versions 1 - 14.
 * The code is adapted directly from FreePascal sources of the Adlib Tracker II
 *
 * REFERENCES:
 * https://github.com/ijsf/at2
 * http://www.adlibtracker.net/
 *
 */

#include "a2m-v2.h"
#include "debug.h"
#include <climits>
#include <math.h>

static const uint8_t _panning[3] = { 0x30, 0x10, 0x20 };
static const uint8_t def_vibtrem_table[256] = {
    0,24,49,74,97,120,141,161,180,197,212,224,235,244,250,253,255,
    253,250,244,235,224,212,197,180,161,141,120,97,74,49,24,
    0,24,49,74,97,120,141,161,180,197,212,224,235,244,250,253,255,
    253,250,244,235,224,212,197,180,161,141,120,97,74,49,24,
    0,24,49,74,97,120,141,161,180,197,212,224,235,244,250,253,255,
    253,250,244,235,224,212,197,180,161,141,120,97,74,49,24,
    0,24,49,74,97,120,141,161,180,197,212,224,235,244,250,253,255,
    253,250,244,235,224,212,197,180,161,141,120,97,74,49,24,
    0,24,49,74,97,120,141,161,180,197,212,224,235,244,250,253,255,
    253,250,244,235,224,212,197,180,161,141,120,97,74,49,24,
    0,24,49,74,97,120,141,161,180,197,212,224,235,244,250,253,255,
    253,250,244,235,224,212,197,180,161,141,120,97,74,49,24,
    0,24,49,74,97,120,141,161,180,197,212,224,235,244,250,253,255,
    253,250,244,235,224,212,197,180,161,141,120,97,74,49,24,
    0,24,49,74,97,120,141,161,180,197,212,224,235,244,250,253,255,
    253,250,244,235,224,212,197,180,161,141,120,97,74,49,24
};

/*** public methods *************************************/

CPlayer *Ca2mv2Player::factory(Copl *newopl)
{
    return new Ca2mv2Player(newopl);
}

Ca2mv2Player::Ca2mv2Player(Copl *newopl) : CPlayer(newopl)
{
    songinfo = new tSONGINFO();
    instrinfo = new tINSTR_INFO();
    eventsinfo = new tEVENTS_INFO();
    ch = new tCHDATA();
}

Ca2mv2Player::~Ca2mv2Player()
{
    arpvib_tables_free();
    patterns_free();
    instruments_free();

    delete songinfo;
    delete instrinfo;
    delete eventsinfo;
    delete ch;
}

bool Ca2mv2Player::update()
{
    newtimer();

    return !songend;
}

void Ca2mv2Player::rewind(int subsong)
{
    chip = 0;
    opl->init();
    opl->setchip(0);

    init_player();

    songend = false;
    set_current_order(0);

    if ((songinfo->pattern_order[current_order] > 0x7f))
        return;

    current_pattern = songinfo->pattern_order[current_order];
    current_line = 0;
    pattern_break = false;
    pattern_delay = false;
    tickXF = 0;
    ticks = 0;
    tick0 = 0;
    tickD = 0;
    next_line = 0;
    irq_mode = true;
    play_status = isPlaying;

    ticklooper = 0;
    macro_ticklooper = 0;
    speed = songinfo->speed;
    macro_speedup = songinfo->macro_speedup;
    update_timer(songinfo->tempo);
}

float Ca2mv2Player::getrefresh()
{
    return (float)tempo * _macro_speedup();
}

std::string Ca2mv2Player::gettype() {
    char tmpstr[42];

    snprintf(tmpstr, sizeof (tmpstr), "Adlib Tracker 2 (%sversion %d)", (type == 1 ? "tiny module " : ""), ffver);
    return std::string(tmpstr);
};

bool Ca2mv2Player::load(const std::string &filename, const CFileProvider &fp)
{
    binistream *f = fp.open(filename);
    if (!f)
        return false;

    if (!fp.extension(filename, ".a2m") && !fp.extension(filename, ".a2t")) {
        fp.close(f);
        return false;
    }

    // Read the whole file
    unsigned long size = fp.filesize(f);
    char *tune = (char *)calloc(1, size);
    f->readString(tune, size);
    fp.close(f);

    bool result = a2_import(tune, size);

    free(tune);

    if (result)
        rewind(0);

    return result;
}

// Helpers for instruments ========================================================================

void Ca2mv2Player::instruments_free()
{
    if (instrinfo->instruments) {
        for (unsigned int i = 0; i < instrinfo->count; i++) {
            if (instrinfo->instruments[i].fmreg) {
                free(instrinfo->instruments[i].fmreg);
                instrinfo->instruments[i].fmreg = NULL;
            }
        }

        free(instrinfo->instruments);
        instrinfo->instruments = NULL;
        instrinfo->count = 0;
        instrinfo->size = 0;
    }
}

void Ca2mv2Player::instruments_allocate(size_t number)
{
    if (editor_mode) {
        number = 255; // Allocate max possible
    }

    instruments_free();

    size_t size = number * sizeof(tINSTR_DATA_EXT);
    instrinfo->instruments = (tINSTR_DATA_EXT *)calloc(1, size);
    assert(instrinfo->instruments);
    instrinfo->count = number;
    instrinfo->size = size;
}

tINSTR_DATA_EXT *Ca2mv2Player::get_instr(uint8_t ins)
{
    if (ins == 0 || ins > instrinfo->count ) {
        return NULL;
    }

    return &instrinfo->instruments[ins - 1];
}

inline int8_t Ca2mv2Player::get_instr_fine_tune(uint8_t ins)
{
    tINSTR_DATA_EXT *instrument = get_instr(ins);

    return instrument ? instrument->instr_data.fine_tune : 0;
}

inline tINSTR_DATA *Ca2mv2Player::get_instr_data_by_ch(int chan)
{
    tINSTR_DATA_EXT *instrument = get_instr(ch->voice_table[chan]);

    return instrument ? &instrument->instr_data : NULL;
}

inline char Ca2mv2Player::get_fm_connect_by_chan(int chan)
{
    tINSTR_DATA *instr = get_instr_data_by_ch(chan);
    if (!instr) {
        AdPlug_LogWrite("get_fm_connect_by_chan: instr not set for channel %d\n", chan);
    }

    return instr ? instr->fm.connect : 0;
}

inline tINSTR_DATA *Ca2mv2Player::get_instr_data(uint8_t ins)
{
    tINSTR_DATA_EXT *instrument = get_instr(ins);

    return instrument ? &instrument->instr_data : NULL;
}

// Fill tFM_INST_DATA from the raw byte array
void Ca2mv2Player::fmdata_fill_from_raw(tFM_INST_DATA *fm, uint8_t *src)
{
    fm->multipM = src[0] & 0xf;
    fm->ksrM    = (src[0] >> 4) & 1;
    fm->sustM   = (src[0] >> 5) & 1;
    fm->vibrM   = (src[0] >> 6) & 1;
    fm->tremM   = (src[0] >> 7) & 1;

    fm->multipC = src[1] & 0xf;
    fm->ksrC    = (src[1] >> 4) & 1;
    fm->sustC   = (src[1] >> 5) & 1;
    fm->vibrC   = (src[1] >> 6) & 1;
    fm->tremC   = (src[1] >> 7) & 1;

    fm->volM    = src[2] & 0x3f;
    fm->kslM    = (src[2] >> 6) & 3;

    fm->volC    = src[3] & 0x3f;
    fm->kslC    = (src[3] >> 6) & 3;

    fm->decM    = src[4] & 0xf;
    fm->attckM  = (src[4] >> 4) & 0xf;

    fm->decC    = src[5] & 0xf;
    fm->attckC  = (src[5] >> 4) & 0xf;

    fm->relM    = src[6] & 0xf;
    fm->sustnM  = (src[6] >> 4) & 0xf;

    fm->relC    = src[7] & 0xf;
    fm->sustnC  = (src[7] >> 4) & 0xf;

    fm->wformM  = src[8] & 7;
    fm->wformC  = src[9] & 7;

    fm->connect = src[10] & 1;
    fm->feedb   = (src[10] >> 1) & 7;
}

// Fill uint8_t data[11] array from tFM_INST_DATA
void Ca2mv2Player::raw_fill_from_fmdata(uint8_t *dst, tFM_INST_DATA *fm)
{
    dst[0] = (fm->multipM & 0xf) |
            ((fm->ksrM  & 1) << 4) |
            ((fm->sustM & 1) << 5) |
            ((fm->vibrM & 1) << 6) |
            ((fm->tremM & 1) << 7);
    dst[1] = (fm->multipC & 0xf) |
            ((fm->ksrC  & 1) << 4) |
            ((fm->sustC & 1) << 5) |
            ((fm->vibrC & 1) << 6) |
            ((fm->tremC & 1) << 7);
    dst[2] = (fm->volM & 0x3f) | ((fm->kslM & 3) << 6);
    dst[3] = (fm->volC & 0x3f) | ((fm->kslC & 3) << 6);
    dst[4] = (fm->decM & 0xf) | ((fm->attckM & 0xf) << 4);
    dst[5] = (fm->decC & 0xf) | ((fm->attckC & 0xf) << 4);
    dst[6] = (fm->relM & 0xf) | ((fm->sustnM & 0xf) << 4);
    dst[7] = (fm->relC & 0xf) | ((fm->sustnC & 0xf) << 4);
    dst[8] = fm->wformM & 7;
    dst[9] = fm->wformC & 7;
    dst[10] = (fm->connect & 1) | ((fm->feedb & 7) << 1);
}

// Helpers for macro tables =======================================================================

/* If FMREG length byte is 0, infer span from macro cells when any cell has duration, freq_slide,
 * or macro_flags. Also allocate fmreg when header bytes [1..5] are non-zero even if length stays 0
 * — Pascal still binds arpeggio/vibrato indices from the FMREG header (top-2act instr 19: vibrato
 * table 1 with empty macro cells). */
uint8_t Ca2mv2Player::fmreg_infer_length_from_cells(const uint8_t *src)
{
    uint8_t inferred = 0;

    for (unsigned int e = 0; e < 255; e++) {
        const uint8_t *cell = &src[6 + e * tREGISTER_TABLE_DEF_V9_14_SIZE];
        uint8_t dur = cell[14];
        int16_t fs = (int16_t)(cell[11] | (cell[12] << 8));
        uint8_t mf = (uint8_t)(cell[10] & 0xf0);

        if (dur != 0 || fs != 0 || mf != 0)
            inferred = (uint8_t)(e + 1);
    }

    return inferred;
}

void Ca2mv2Player::fmreg_table_allocate(size_t n, uint8_t *src)
{
    n = editor_mode ? 255 : n;

    // Note: for editor_mode allocate max entries possible
    for (unsigned int i = 0; i < n; i++, src += tFMREG_TABLE_V9_14_SIZE) {
        uint8_t real_length = src[0];

        if (real_length == 0) {
            uint8_t inferred = fmreg_infer_length_from_cells(src);

            if (inferred)
                real_length = inferred;
        }

        /* Loop/keyoff/arp/vib header — Pascal keeps instr_macros[] row even when length=0 and no cells */
        bool header_links = (src[1] | src[2] | src[3] | src[4] | src[5]) != 0;

        /* Pascal: when src[0]==0, macro_poll_proc immediately terminates (fmreg_pos→finished_flag),
         * never processing cell data. Only allocate if original length is non-zero or header links exist. */
        if (editor_mode || src[0] || header_links) {
            tINSTR_DATA_EXT *instrument = get_instr(i + 1);
            assert(instrument);
            if (!instrument)
                continue;

            instrument->fmreg = (tFMREG_TABLE *)calloc(1, sizeof(tFMREG_TABLE));
            assert(instrument->fmreg);

            // Copy field by field — use src[0] (original), not real_length (inferred)
            instrument->fmreg->length         = src[0];
            instrument->fmreg->loop_begin     = src[1]; // loop_begin
            instrument->fmreg->loop_length    = src[2]; // loop_length
            instrument->fmreg->keyoff_pos     = src[3]; // keyoff_pos
            instrument->fmreg->arpeggio_table = src[4]; // arpeggio_table
            instrument->fmreg->vibrato_table  = src[5]; // vibrato_table

            uint8_t *rts = &src[6];
            for (unsigned int e = 0; e < 255; e++, rts += tREGISTER_TABLE_DEF_V9_14_SIZE) {
                tREGISTER_TABLE_DEF *rtd = &instrument->fmreg->data[e];

                fmdata_fill_from_raw(&rtd->fm, rts);

                rtd->freq_slide = (int16_t)(rts[11] | (rts[12] << 8));
                rtd->panning = rts[13];       // panning
                rtd->duration = rts[14];      // duration
                rtd->macro_flags = rts[10] & 0xf0;
                if (rtd->macro_flags)
                    AdPlug_LogWrite("instr(%02x).macro_flags: 0x%x\n", i + 1, rtd->macro_flags);
            }
        }
    }
}

void Ca2mv2Player::disabled_fmregs_import(size_t n, bool dis_fmregs[255][28])
{
    n = editor_mode ? 255 : n;

    // shrink bool[255][28] to uint32_t[255], use bits as enable/disable flag
    for (unsigned int i = 0; i < n; i++) {
        uint32_t result = 0; // all enabled by default
        for (unsigned int bit = 0; bit < 28; bit++) {
            result |= (dis_fmregs[i][bit] & 1) << bit;
        }

        tINSTR_DATA_EXT *instrument = get_instr(i + 1);
        assert(instrument);
        if (!instrument)
            continue;

        instrument->dis_fmreg_cols = result;
    }
}

void Ca2mv2Player::arpvib_tables_free()
{
    if (!vibrato_table || !arpeggio_table)
        return;

    for (unsigned int i = 0; i < arpvib_count; i++) {
        free(vibrato_table[i]);
        free(arpeggio_table[i]);
        vibrato_table[i] = 0;
        arpeggio_table[i] = 0;
    }

    delete[] vibrato_table;
    vibrato_table = NULL;
    delete[] arpeggio_table;
    arpeggio_table = NULL;
    arpvib_count = 0;
}

void Ca2mv2Player::arpvib_tables_allocate(size_t n, uint8_t *src)
{
    arpvib_tables_free();

    // Note: for editor_mode allocate max entries possible
    n = editor_mode ? 255 : n;

    vibrato_table = new tVIBRATO_TABLE *[n]();
    arpeggio_table = new tARPEGGIO_TABLE *[n]();
    arpvib_count = n;

    for (unsigned int i = 0; i < n; i++, src += tARPVIB_TABLE_V9_14_SIZE) {
        if (editor_mode || src[0] /*arpeggio.length*/) {
            arpeggio_table[i] = (tARPEGGIO_TABLE *)calloc(1, sizeof(tARPEGGIO_TABLE));

            // Copy field by field
            arpeggio_table[i]->length       = src[0] /*arpeggio.length*/;
            arpeggio_table[i]->speed        = src[1] /*arpeggio.speed*/;
            arpeggio_table[i]->loop_begin   = src[2] /*arpeggio.loop_begin*/;
            arpeggio_table[i]->loop_length  = src[3] /*arpeggio.loop_length*/;
            arpeggio_table[i]->keyoff_pos   = src[4] /*arpeggio.keyoff_pos*/;
            memcpy(arpeggio_table[i]->data, &src[5] /*arpeggio.data*/, 255);
        }
        if (editor_mode || src[260] /*vibrato.length*/) {
            vibrato_table[i] = (tVIBRATO_TABLE *)calloc(1, sizeof(tVIBRATO_TABLE));

            // Copy field by field
            vibrato_table[i]->length        = src[260] /*vibrato.length*/;
            vibrato_table[i]->speed         = src[261] /*vibrato.speed*/;
            vibrato_table[i]->delay         = src[262] /*vibrato.delay*/;
            vibrato_table[i]->loop_begin    = src[263] /*vibrato.loop_begin*/;
            vibrato_table[i]->loop_length   = src[264] /*vibrato.loop_length*/;
            vibrato_table[i]->keyoff_pos    = src[265] /*vibrato.keyoff_pos*/;
            memcpy(vibrato_table[i]->data, &src[266] /*vibrato.data*/, 255);
        }
    }
}

tARPEGGIO_TABLE *Ca2mv2Player::get_arpeggio_table(uint8_t arp_table)
{
    return arp_table && arpeggio_table && arpeggio_table[arp_table - 1] ? arpeggio_table[arp_table - 1] : NULL;
}

tVIBRATO_TABLE *Ca2mv2Player::get_vibrato_table(uint8_t vib_table)
{
    return vib_table && vibrato_table && vibrato_table[vib_table - 1] ? vibrato_table[vib_table - 1] : NULL;
}

tFMREG_TABLE *Ca2mv2Player::get_fmreg_table(uint8_t fmreg_ins)
{
    tINSTR_DATA_EXT *instrument = get_instr(fmreg_ins);

    return instrument && instrument->fmreg ? instrument->fmreg : NULL;
}

// Helpers for patterns ===========================================================================

// event = pattern * (channels * rows) + ch * rows + row
tADTRACK2_EVENT *Ca2mv2Player::get_event_p(int pattern, int channel, int row)
{
    static tADTRACK2_EVENT null_event = { 0 };

    return (
        pattern < eventsinfo->patterns
            ? &eventsinfo->events[
                pattern * eventsinfo->channels * eventsinfo->rows +
                channel * eventsinfo->rows + row]
            : &null_event
    );
}

void Ca2mv2Player::patterns_free()
{
    if (eventsinfo->events && eventsinfo->size) {
        free(eventsinfo->events);
        eventsinfo->events = NULL;
        eventsinfo->size = 0;
    }
}

void Ca2mv2Player::patterns_allocate(int patterns, int channels, int rows)
{
    if (editor_mode) { // allocate max possible
        patterns = 128;
        channels = 20;
        rows = 256;
    }

    patterns_free();

    size_t size = patterns * channels * rows * sizeof(tADTRACK2_EVENT);

    eventsinfo->events = (tADTRACK2_EVENT *)calloc(1, size);
    assert(eventsinfo->events);

    eventsinfo->patterns = patterns;
    eventsinfo->channels = channels;
    eventsinfo->rows = rows;
    eventsinfo->size = size;
}

// End of patterns helpers ========================================================================

inline bool note_in_range(uint8_t note)
{
    uint8_t n = note & (uint8_t)~keyoff_flag;
    /* Pascal output_note: NOT (note in [1..12*8+1]) uses the inclusive upper bound 97 (a2player.pas). */
    return n >= 1 && n <= (uint8_t)(12 * 8 + 1);
}

inline uint16_t Ca2mv2Player::regoffs_n(int chan)
{
    static const uint16_t _ch_n[2][20] = {
        {  // mm
            0x003,0x000,0x004,0x001,0x005,0x002,0x006,0x007,0x008,0x103,
            0x100,0x104,0x101,0x105,0x102,0x106,0x107,0x108,BYTE_NULL,BYTE_NULL
        }, { // pm
            0x003,0x000,0x004,0x001,0x005,0x002,0x106,0x107,0x108,0x103,
            0x100,0x104,0x101,0x105,0x102,0x006,0x007,0x008,0x008,0x007
        }
    };

    return _ch_n[!!percussion_mode][chan];
}

inline uint16_t Ca2mv2Player::regoffs_m(int chan)
{
    static const uint16_t _ch_m[2][20] = {
        {  // mm
            0x008,0x000,0x009,0x001,0x00a,0x002,0x010,0x011,0x012,0x108,
            0x100,0x109,0x101,0x10a,0x102,0x110,0x111,0x112,BYTE_NULL,BYTE_NULL
        }, { // pm
            0x008,0x000,0x009,0x001,0x00a,0x002,0x110,0x111,0x112,0x108,
            0x100,0x109,0x101,0x10a,0x102,0x010,0x014,0x012,0x015,0x011
        }
    };

    return _ch_m[!!percussion_mode][chan];
}

inline uint16_t Ca2mv2Player::regoffs_c(int chan)
{
    static const uint16_t _ch_c[2][20] = {
        {
            0x00b,0x003,0x00c,0x004,0x00d,0x005,0x013,0x014,0x015,0x10b,
            0x103,0x10c,0x104,0x10d,0x105,0x113,0x114,0x115,BYTE_NULL,BYTE_NULL
        }, {
            0x00b,0x003,0x00c,0x004,0x00d,0x005,0x113,0x114,0x115,0x10b,
            0x103,0x10c,0x104,0x10d,0x105,0x013,BYTE_NULL,BYTE_NULL,BYTE_NULL,BYTE_NULL
        }
    };

    return _ch_c[!!percussion_mode][chan];
}

#define FreqStart   0x156
#define FreqEnd     0x2ae
#define FreqRange   (FreqEnd - FreqStart)

/* Round a/b to nearest integer (matching Pascal's Round(a/b) for positive ints) */
#define ROUND_DIV(a, b) (((a) + (b) / 2) / (b))

/* PLAYER */
void Ca2mv2Player::opl2out(uint16_t reg, uint16_t data)
{
    if (chip != 0) {
        chip = 0;
        opl->setchip(chip);
    }
    opl->write(reg, data);
}

void Ca2mv2Player::opl3out(uint16_t reg, uint8_t data)
{
    int _chip = reg < 0x100 ? 0 : 1;
    if (chip != _chip) {
        chip = _chip;
        opl->setchip(chip);
    }
    opl->write(reg & 0xff, data);
}

void Ca2mv2Player::opl3exp(uint16_t data)
{
    if (chip != 1) {
        chip = 1;
        opl->setchip(chip);
    }
    opl->write(data & 0xff, (data >> 8) & 0xff);
}

static uint16_t nFreq(uint8_t note)
{
    static uint16_t Fnum[12] = {0x157,0x16b,0x181,0x198,0x1b0,0x1ca,0x1e5,
                0x202,0x220,0x241,0x263,0x287};

    if (note >= 12 * 8)
        return (7 << 10) | FreqEnd;

    return (note / 12 << 10) | Fnum[note % 12];
}

static uint16_t calc_freq_shift_up(uint16_t freq, uint16_t shift)
{
    uint16_t oc = (freq >> 10) & 7;
    uint16_t fr = (freq & 0x3ff) + shift;

    if (fr >= FreqEnd) {
        if (oc == 7) {
            fr = FreqEnd;
        } else {
            oc++;
            fr -= FreqRange;
        }
    }

    return (uint16_t)((oc << 10) + fr);
}

static uint16_t calc_freq_shift_down(uint16_t freq, uint16_t shift)
{
    uint16_t oc = (freq >> 10) & 7;
    uint16_t fr = (freq & 0x3ff) - shift;

    if (fr <= FreqStart) {
        if (oc == 0) {
            fr = FreqStart;
        } else {
            oc--;
            fr += FreqRange;
        }
    }

    return (uint16_t)((oc << 10) + fr);
}

/* == calc_vibtrem_shift() in AT2 */
uint16_t Ca2mv2Player::calc_vibrato_shift(uint8_t depth, uint8_t position)
{
    /* depth: 0..F, max result: FF*F=EF1 */
    uint16_t X = (uint16_t)depth * vibtrem_table[position & (vibtrem_table_size - 1)];
    uint16_t rotated = (X << 1) | (X >> 15);
    uint16_t result  = ((rotated >> 8) & 0xFF) | ((rotated & 1) << 8);

    return result;
}

void Ca2mv2Player::change_freq(int chan, uint16_t freq)
{
    if (is_4op_chan(chan) && is_4op_chan_hi(chan)) {
        ch->freq_table[chan + 1] = ch->freq_table[chan];
        chan++;
    }

    ch->freq_table[chan] &= ~0x1fff;
    ch->freq_table[chan] |= (freq & 0x1fff);

    uint16_t n = regoffs_n(chan);

    opl3out(0xa0 + n, ch->freq_table[chan] & 0xFF);
    opl3out(0xb0 + n, (ch->freq_table[chan] >> 8) & 0xFF);

    if (is_4op_chan(chan) && is_4op_chan_lo(chan)) {
        ch->freq_table[chan - 1] = ch->freq_table[chan];
    }
}

bool Ca2mv2Player::is_chan_adsr_data_empty(int chan)
{
    tFM_INST_DATA *fmpar = &ch->fmpar_table[chan];
    uint8_t data[11];
    raw_fill_from_fmdata(data, fmpar);

    return (
        !data[4] &&
        !data[5] &&
        !data[6] &&
        !data[7]
    );
}

bool Ca2mv2Player::is_ins_adsr_data_empty(int ins)
{
    tINSTR_DATA *i = get_instr_data(ins);
    if (!i)
        return true;

    uint8_t data[11];
    raw_fill_from_fmdata(data, &i->fm);

    return (
        !data[4] &&
        !data[5] &&
        !data[6] &&
        !data[7]
    );
}

static bool is_data_empty(char *data, unsigned int size)
{
    while (size--) {
        if (*(char *)data++)
            return false;
    }

    return true;
}

static inline uint16_t max(uint16_t value, uint16_t maximum)
{
    return (value > maximum ? maximum : value);
}

static inline uint16_t min(uint16_t value, uint16_t minimum)
{
    return (value < minimum ? minimum : value);
}

void Ca2mv2Player::change_frequency(int chan, uint16_t freq)
{
    ch->macro_table[chan].vib_paused = true;
    change_freq(chan, freq);

    if (is_4op_chan(chan)) {
        int i = is_4op_chan_hi(chan) ? 1 : -1;

        ch->macro_table[chan + i].vib_count = 1;
        ch->macro_table[chan + i].vib_pos = 0;
        ch->macro_table[chan + i].vib_freq = freq;
        ch->macro_table[chan + i].vib_paused = false;
    }

    ch->macro_table[chan].vib_count = 1;
    ch->macro_table[chan].vib_pos = 0;
    ch->macro_table[chan].vib_freq = freq;
    ch->macro_table[chan].vib_paused = false;
}

inline uint16_t Ca2mv2Player::_macro_speedup()
{
    return macro_speedup ? macro_speedup : 1;
}

void Ca2mv2Player::set_clock_rate(uint8_t clock_rate)
{
}

void Ca2mv2Player::update_timer(int Hz)
{
    if (Hz == 0) {
        set_clock_rate(0);
        return;
    } else {
        tempo = Hz;
    }

    if (tempo == 18 && timer_fix) {
        IRQ_freq = ((float)tempo + 0.2) * 20.0;
    } else {
        IRQ_freq = 250;
    }

    while (IRQ_freq % (tempo * _macro_speedup()) != 0) {
        IRQ_freq++;
    }

    if (IRQ_freq > MAX_IRQ_FREQ)
        IRQ_freq = MAX_IRQ_FREQ;

    while ((IRQ_freq + IRQ_freq_shift + playback_speed_shift > MAX_IRQ_FREQ) && (playback_speed_shift > 0))
        playback_speed_shift--;

    while ((IRQ_freq + IRQ_freq_shift + playback_speed_shift > MAX_IRQ_FREQ) && (IRQ_freq_shift > 0))
        IRQ_freq_shift--;

    set_clock_rate(1193180 / max(IRQ_freq + IRQ_freq_shift + playback_speed_shift, MAX_IRQ_FREQ));
}

void Ca2mv2Player::update_playback_speed(int speed_shift)
{
    if (!speed_shift)
        return;

    if ((speed_shift > 0) && (IRQ_freq + playback_speed_shift + speed_shift > MAX_IRQ_FREQ)) {
        while (IRQ_freq + IRQ_freq_shift + playback_speed_shift + speed_shift > MAX_IRQ_FREQ)
            speed_shift--;
    } else if ((speed_shift < 0) && (IRQ_freq + IRQ_freq_shift + playback_speed_shift + speed_shift < MIN_IRQ_FREQ)) {
        while (IRQ_freq + IRQ_freq_shift + playback_speed_shift + speed_shift < MIN_IRQ_FREQ)
            speed_shift++;
    }

    playback_speed_shift += speed_shift;
    update_timer(tempo);
}

void Ca2mv2Player::key_on(int chan)
{
    int i = is_4op_chan(chan) && is_4op_chan_hi(chan) ? 1 : 0;

    opl3out(0xb0 + regoffs_n(chan + i), 0);
}

void Ca2mv2Player::key_off(int chan)
{
    ch->freq_table[chan] &= ~0x2000;
    change_frequency(chan, ch->freq_table[chan]);
    ch->event_table[chan].note |= keyoff_flag;
}

void Ca2mv2Player::release_sustaining_sound(int chan)
{
    uint16_t m = regoffs_m(chan);
    uint16_t c = regoffs_c(chan);

    opl3out(0x40 + m, 63);
    opl3out(0x40 + c, 63);

    // clear adsrw_mod and adsrw_car (match Pascal's FillChar zeroing all fields)
    ch->fmpar_table[chan].decM = 0;
    ch->fmpar_table[chan].attckM = 0;
    ch->fmpar_table[chan].relM = 0;
    ch->fmpar_table[chan].sustnM = 0;
    ch->fmpar_table[chan].wformM = 0;
    ch->fmpar_table[chan].decC = 0;
    ch->fmpar_table[chan].attckC = 0;
    ch->fmpar_table[chan].relC = 0;
    ch->fmpar_table[chan].sustnC = 0;
    ch->fmpar_table[chan].wformC = 0;

    key_on(chan);
    opl3out(0x60 + m, BYTE_NULL);
    opl3out(0x60 + c, BYTE_NULL);
    opl3out(0x80 + m, BYTE_NULL);
    opl3out(0x80 + c, BYTE_NULL);

    key_off(chan);
    ch->event_table[chan].instr_def = 0;
    ch->reset_chan[chan] = true;
}

// inverted volume here
static uint8_t scale_volume(uint8_t volume, uint8_t scale_factor)
{
    return 63 - (((63 - volume) * (63 - scale_factor) + 31) / 63);
}

// former _4op_data_flag()
t4OP_DATA Ca2mv2Player::get_4op_data(uint8_t chan)
{
    t4OP_DATA d = { false, 0, 0, 0, 0, 0 };

    if (!is_4op_chan(chan))
        return d;

    d.mode = true;

    if (is_4op_chan_hi(chan)) {
        d.ch1 = chan;
        d.ch2 = chan + 1;
    } else {
        d.ch1 = chan - 1;
        d.ch2 = chan;
    }

    d.ins1 = ch->event_table[d.ch1].instr_def;
    if (d.ins1 == 0) d.ins1 = ch->voice_table[d.ch1];

    d.ins2 = ch->event_table[d.ch2].instr_def;
    if (d.ins2 == 0) d.ins2 = ch->voice_table[d.ch2];

    if (d.ins1 && d.ins2) {
        tINSTR_DATA *id1 = get_instr_data(d.ins1);
        tINSTR_DATA *id2 = get_instr_data(d.ins2);
        if (id1 && id2)
            d.conn = (id1->fm.connect << 1) | id2->fm.connect;
    }

    return d;
}

bool Ca2mv2Player::_4op_vol_valid_chan(int chan)
{
    t4OP_DATA d = get_4op_data(chan);

    return d.mode && ch->vol4op_lock[chan] && d.ins1 && d.ins2;
}

// inverted volume here
void Ca2mv2Player::set_ins_volume(uint8_t modulator, uint8_t carrier, uint8_t chan)
{
    tINSTR_DATA *instr = get_instr_data_by_ch(chan);

    uint8_t volM = instr ? instr->fm.volM : 0;
    uint8_t volC = instr ? instr->fm.volC : 0;
    uint8_t conn = instr ? instr->fm.connect : 0;

    uint16_t m = regoffs_m(chan);
    uint16_t c = regoffs_c(chan);

    // Note: fmpar[].volM/volC has pure unscaled volume,
    // modulator_vol/carrier_vol have scaled but without overall_volume
    if (modulator != BYTE_NULL) {
        uint8_t regm;
        bool is_perc_chan = conn ||
                            (percussion_mode && chan >= 16); // in [17..20]

        ch->fmpar_table[chan].volM = modulator;

        if (is_perc_chan) { // in [17..20]
            if (volume_scaling)
                modulator = scale_volume(volM, modulator);

            modulator = scale_volume(modulator, scale_volume(63 - global_volume, 63 - fade_out_volume));
            regm = scale_volume(modulator, 63 - overall_volume) + (ch->fmpar_table[chan].kslM << 6);
        } else {
            regm = modulator + (ch->fmpar_table[chan].kslM << 6);
        }

        opl3out(0x40 + m, regm);
        ch->modulator_vol[chan] = 63 - modulator;
    }

    if (carrier != BYTE_NULL) {
        uint8_t regc;

        ch->fmpar_table[chan].volC = carrier;

        if (volume_scaling)
            carrier = scale_volume(volC, carrier);

        carrier = scale_volume(carrier, scale_volume(63 - global_volume, 63 - fade_out_volume));
        regc = scale_volume(carrier, 63 - overall_volume) + (ch->fmpar_table[chan].kslC << 6);

        opl3out(0x40 + c, regc);
        ch->carrier_vol[chan] = 63 - carrier;
    }
}

// Used by set_ins_volume_4op() only
void Ca2mv2Player::set_volume(uint8_t modulator, uint8_t carrier, uint8_t chan)
{
    tINSTR_DATA *instr = get_instr_data_by_ch(chan);

    uint8_t volM = instr ? instr->fm.volM : 0;
    uint8_t volC = instr ? instr->fm.volC : 0;

    uint16_t m = regoffs_m(chan);
    uint16_t c = regoffs_c(chan);

    if (modulator != BYTE_NULL) {
        uint8_t regm;
        ch->fmpar_table[chan].volM = modulator;

        modulator = scale_volume(volM, modulator);
        modulator = scale_volume(modulator, scale_volume(63 - global_volume, 63 - fade_out_volume));

        regm = scale_volume(modulator, 63 - overall_volume) + (ch->fmpar_table[chan].kslM << 6);

        opl3out(0x40 + m, regm);
        ch->modulator_vol[chan] = 63 - modulator;
    }

    if (carrier != BYTE_NULL) {
        uint8_t regc;
        ch->fmpar_table[chan].volC = carrier;

        carrier = scale_volume(volC, carrier);
        carrier = scale_volume(carrier, scale_volume(63 - global_volume, 63 - fade_out_volume));

        regc = scale_volume(carrier, 63 - overall_volume) + (ch->fmpar_table[chan].kslC << 6);

        opl3out(0x40 + c, regc);
        ch->carrier_vol[chan] = 63 - carrier;
    }
}

void Ca2mv2Player::set_ins_volume_4op(uint8_t volume, uint8_t chan)
{
    t4OP_DATA d = get_4op_data(chan);

    if (!_4op_vol_valid_chan(chan))
        return;

    uint8_t volM1 = BYTE_NULL;
    uint8_t volC1 = BYTE_NULL;
    uint8_t volM2 = BYTE_NULL;
    uint8_t volC2 = BYTE_NULL;

    volC1 = volume == BYTE_NULL ? ch->fmpar_table[d.ch1].volC : volume;

    switch (d.conn) {
    case 0: // FM/FM ins1=FM, ins2=FM
        break;
    case 1: // FM/AM ins1=FM, ins2=AM
        volM2 = volume == BYTE_NULL ? ch->fmpar_table[d.ch2].volM : volume;
        break;
    case 2: // AM/FM ins1=AM, ins2=FM
        volC2 = volume == BYTE_NULL ? ch->fmpar_table[d.ch2].volC : volume;
        break;
    case 3:// AM/AM ins1=AM, ins2=AM
        volM1 = volume == BYTE_NULL ? ch->fmpar_table[d.ch1].volM : volume;
        volM2 = volume == BYTE_NULL ? ch->fmpar_table[d.ch2].volM : volume;
        break;
    }

    set_volume(volM1, volC1, d.ch1);
    set_volume(volM2, volC2, d.ch2);
}

void Ca2mv2Player::reset_ins_volume(int chan)
{
    tINSTR_DATA *instr = get_instr_data_by_ch(chan);
    if (!instr) {
        set_ins_volume(0, 0, chan);
        return;
    }

    uint8_t vol_mod = instr->fm.volM;
    uint8_t vol_car = instr->fm.volC;
    uint8_t conn = instr->fm.connect;

    if (volume_scaling) {
        vol_mod = (!conn ? vol_mod : 0);
        vol_car = 0;
    }

    set_ins_volume(vol_mod, vol_car, chan);
}

void Ca2mv2Player::set_global_volume()
{
    for (int chan = 0; chan < songinfo->nm_tracks; chan++) {
        if (_4op_vol_valid_chan(chan)) {
            set_ins_volume_4op(BYTE_NULL, chan);
        } else if (ch->carrier_vol[chan] || ch->modulator_vol[chan]) {
            char instr_fm_connect = get_fm_connect_by_chan(chan);

            set_ins_volume(instr_fm_connect ? ch->fmpar_table[chan].volM : BYTE_NULL, ch->fmpar_table[chan].volC, chan);
        }
    }
}

void Ca2mv2Player::set_overall_volume(unsigned char level)
{
    overall_volume = min(level, 63);
    set_global_volume();
}

void Ca2mv2Player::init_macro_table(int chan, uint8_t note, uint8_t ins, uint16_t freq)
{
    tINSTR_DATA_EXT *instrument = get_instr(ins);

    uint8_t arp_table = instrument ? instrument->arpeggio : 0;
    ch->macro_table[chan].fmreg_pos = 0;
    ch->macro_table[chan].fmreg_duration = 0;
    ch->macro_table[chan].fmreg_ins = ins; // todo: check against instruments->fmreg.length
    ch->macro_table[chan].arpg_count = 1;
    ch->macro_table[chan].arpg_pos = 0;
    ch->macro_table[chan].arpg_table = arp_table;
    ch->macro_table[chan].arpg_note = note;

    uint8_t vib_table = instrument ? instrument->vibrato : 0;
    tVIBRATO_TABLE *vib = get_vibrato_table(vib_table);
    uint8_t vib_delay = vib ? vib->delay : 0;

    ch->macro_table[chan].vib_count = 1;
    ch->macro_table[chan].vib_paused = false;
    ch->macro_table[chan].vib_pos = 0;
    ch->macro_table[chan].vib_table = vib_table;
    ch->macro_table[chan].vib_freq = freq;
    ch->macro_table[chan].vib_delay = vib_delay;

    ch->zero_fq_table[chan] = 0;
}

void Ca2mv2Player::set_ins_data(uint8_t ins, int chan)
{
    static tINSTR_DATA zeroins = { 0 };

    if (ins == 0) return;

    tINSTR_DATA *i = get_instr_data(ins);
    i = i ? i : &zeroins;
    uint8_t data[11];
    raw_fill_from_fmdata(data, &i->fm);

    if (is_data_empty((char *)i, sizeof(tINSTR_DATA))) {
        release_sustaining_sound(chan);
    }

    if ((ins != ch->event_table[chan].instr_def) || ch->reset_chan[chan]) {
        ch->panning_table[chan] = !ch->pan_lock[chan]
                                  ? i->panning
                                  : songinfo->lock_flags[chan] & 3;

        if (ch->panning_table[chan] >= sizeof (_panning))
            ch->panning_table[chan] = 0; /* various code paths can lead to this value going out of the 0-2 range */

        uint16_t m = regoffs_m(chan);
        uint16_t c = regoffs_c(chan);
        uint16_t n = regoffs_n(chan);

        opl3out(0x20 + m, data[0]);
        opl3out(0x20 + c, data[1]);
        opl3out(0x40 + m, (data[2] & 0xc0) + 63);
        opl3out(0x40 + c, (data[3] & 0xc0) + 63);
        opl3out(0x60 + m, data[4]);
        opl3out(0x60 + c, data[5]);
        opl3out(0x80 + m, data[6]);
        opl3out(0x80 + c, data[7]);
        opl3out(0xe0 + m, data[8]);
        opl3out(0xe0 + c, data[9]);
        opl3out(0xc0 + n, data[10] | _panning[ch->panning_table[chan]]);

        ch->fmpar_table[chan] = i->fm; // Copy struct

        // Stop instr macro if resetting voice
        if (!ch->reset_chan[chan])
            ch->keyoff_loop[chan] = false;

        if (ch->reset_chan[chan]) {
            ch->voice_table[chan] = ins;
            reset_ins_volume(chan);
            ch->reset_chan[chan] = false;
        }

        uint8_t note = ch->event_table[chan].note & ~keyoff_flag;
        note = note_in_range(note) ? note : 0;

        init_macro_table(chan, note, ins, ch->freq_table[chan]);
    }

    ch->voice_table[chan] = ins;
    uint8_t old_ins = ch->event_table[chan].instr_def;
    ch->event_table[chan].instr_def = ins;

    if (!ch->volume_lock[chan] || (ins != old_ins))
        reset_ins_volume(chan);
}

void Ca2mv2Player::update_modulator_adsrw(int chan)
{
    tFM_INST_DATA *fmpar = &ch->fmpar_table[chan];
    uint16_t m = regoffs_m(chan);
    uint8_t data[11];
    raw_fill_from_fmdata(data, fmpar);

    opl3out(0x60 + m, data[4]);
    opl3out(0x80 + m, data[6]);
    opl3out(0xe0 + m, data[8]);
}

void Ca2mv2Player::update_carrier_adsrw(int chan)
{
    tFM_INST_DATA *fmpar = &ch->fmpar_table[chan];
    uint16_t c = regoffs_c(chan);
    uint8_t data[11];
    raw_fill_from_fmdata(data, fmpar);

    opl3out(0x60 + c, data[5]);
    opl3out(0x80 + c, data[7]);
    opl3out(0xe0 + c, data[9]);
}

void Ca2mv2Player::update_fmpar(int chan)
{
    tFM_INST_DATA *fmpar = &ch->fmpar_table[chan];
    uint8_t data[11];
    raw_fill_from_fmdata(data, fmpar);

    opl3out(0x20 + regoffs_m(chan), data[0]);
    opl3out(0x20 + regoffs_c(chan), data[1]);
    opl3out(0xc0 + regoffs_n(chan), data[10] | _panning[ch->panning_table[chan]]);

    set_ins_volume(fmpar->volM, fmpar->volC, chan);
}

inline bool Ca2mv2Player::is_4op_chan(int chan) // 0..19
{
    static char mask[20] = {
        (1<<0), (1<<0), (1<<1), (1<<1), (1<<2), (1<<2), 0, 0, 0,
        (1<<3), (1<<3), (1<<4), (1<<4), (1<<5), (1<<5), 0, 0, 0, 0, 0
    };
/*
    4-op track extension flags byte, channels 1-18
    0  - tracks 1,2
    1  - tracks 3,4
    2  - tracks 5,6
    3  - tracks 10,11
    4  - tracks 12,13
    5  - tracks 14,15
    6  - %unused%
    7  - %unused%
*/
    return (chan > 14 ? false : !!(songinfo->flag_4op & mask[chan]));
}

inline bool Ca2mv2Player::is_4op_chan_hi(int chan)
{
    static bool _4op_hi[20] = {
        true, false, true, false, true, false, false, false, false,                // 0, 2, 4
        true, false, true, false, true, false, false, false, false, false, false   // 9, 10, 13
    };

    return _4op_hi[chan];
}

inline bool Ca2mv2Player::is_4op_chan_lo(int chan)
{
    static bool _4op_lo[20] = {
        false, true, false, true, false, true, false, false, false,               // 1, 3, 5
        false, true, false, true, false, true, false, false, false, false, false  // 10, 12, 14
    };

    return _4op_lo[chan];
}

void Ca2mv2Player::output_note(uint8_t note, uint8_t ins, int chan, bool restart_macro, bool restart_adsr)
{
    uint16_t freq;

    if ((note == 0) && (ch->ftune_table[chan] == 0)) return;

    if ((note & keyoff_flag) || !note_in_range(note)) {
        freq = ch->freq_table[chan];
    } else {
        freq = nFreq(note - 1) + get_instr_fine_tune(ins);

        if (restart_adsr) {
            key_on(chan);
        } else {
            AdPlug_LogWrite("restart_adsr == false in output_note()\n");
        }

        ch->freq_table[chan] |= 0x2000;
    }

    if (ch->ftune_table[chan] == -127)
        ch->ftune_table[chan] = 0;

    freq = (uint16_t)((int32_t)freq + (int32_t)ch->ftune_table[chan]);
    change_frequency(chan, freq);

    if (note) {
        ch->event_table[chan].note = note;

        /* Pascal (a2player.pas ~1163–1165): If is_4op_chan(chan) then
         * event_table[PRED(chan)].note := note — applies to hi and lo tracks. */
        if (is_4op_chan(chan) && chan >= 1)
            ch->event_table[chan - 1].note = note;

        if (restart_macro) {
            // Check if no ZFF - force no restart
            bool force_no_restart = (
                    ((ch->event_table[chan].eff[0].def == ef_Extended) &&
                     (ch->event_table[chan].eff[0].val == ef_ex_ExtendedCmd2 * 16 + ef_ex_cmd2_NoRestart)) ||
                    ((ch->event_table[chan].eff[1].def == ef_Extended) &&
                     (ch->event_table[chan].eff[1].val == ef_ex_ExtendedCmd2 * 16 + ef_ex_cmd2_NoRestart))
                );
            if (!force_no_restart) {
                init_macro_table(chan, note, ins, freq);
            } else {
                ch->macro_table[chan].arpg_note = note;
            }
        }
    }
}

bool Ca2mv2Player::no_loop(uint8_t current_chan, uint8_t current_line)
{
    for (int chan = 0; chan < current_chan; chan++) {
        if ((ch->loop_table[chan][current_line] != 0) &&
            (ch->loop_table[chan][current_line] != BYTE_NULL))
            return false;
    }

    return true;
}

static int get_effect_group(uint8_t def)
{
    switch (def) {
    case ef_ArpggVSlide:
    case ef_ArpggVSlideFine:    return EFGR_ARPVOLSLIDE;
    case ef_FSlideUpVSlide:
    case ef_FSlUpVSlF:
    case ef_FSlideDownVSlide:
    case ef_FSlDownVSlF:
    case ef_FSlUpFineVSlide:
    case ef_FSlUpFineVSlF:
    case ef_FSlDownFineVSlide:
    case ef_FSlDownFineVSlF:    return EFGR_FSLIDEVOLSLIDE;
    case ef_TonePortamento:     return EFGR_TONEPORTAMENTO;
    case ef_Vibrato:
    case ef_ExtraFineVibrato:   return EFGR_VIBRATO;
    case ef_Tremolo:
    case ef_ExtraFineTremolo:   return EFGR_TREMOLO;
    case ef_VibratoVolSlide:
    case ef_VibratoVSlideFine:  return EFGR_VIBRATOVOLSLIDE;
    case ef_TPortamVolSlide:
    case ef_TPortamVSlideFine:  return EFGR_PORTAVOLSLIDE;
    case ef_RetrigNote:
    case ef_MultiRetrigNote:    return EFGR_RETRIGNOTE;
    default: return -1;
    }

    return -1;
}

// In case of x00 set value of the previous compatible effect command
void Ca2mv2Player::update_effect_table(int slot, int chan, int eff_group, uint8_t def, uint8_t val)
{
    uint8_t lval = ch->last_effect[slot][chan].val;

    ch->effect_table[slot][chan].def = def;

    if (val) {
        ch->effect_table[slot][chan].val = val;
    } else if (get_effect_group(ch->last_effect[slot][chan].def) == eff_group && lval) {
        ch->effect_table[slot][chan].val = lval;
    } else {
        // x00 without any previous compatible command — Pascal: effect_table := effect_def (val=0, def kept)
        ch->effect_table[slot][chan].val = 0;
    }
}

/* a2player.pas play_line first loop: single else-if between arpgg_table and arpgg_table2 — not two
 * independent cleanups, and cleanup runs before Case effect_def (FineTune etc.) on either column. */
void Ca2mv2Player::play_line_arpgg_cleanup(const tADTRACK2_EVENT *event, int chan)
{
    for (int slot = 0; slot < 2; slot++) {
        uint8_t def = event->eff[slot].def;
        uint8_t val = event->eff[slot].val;

        bool arp = (((def == ef_Arpeggio) && (val != 0)) || (def == ef_ExtraFineArpeggio));

        if (!arp &&
            ch->arpgg_table[slot][chan].note != 0 &&
            ch->arpgg_table[slot][chan].state != 1) {
            ch->arpgg_table[slot][chan].state = 1;
            change_frequency(chan, nFreq(ch->arpgg_table[slot][chan].note - 1) +
                get_instr_fine_tune(ch->event_table[chan].instr_def));
        }
    }
}

/* a2player.pas play_line first loop (~1364–1442): GlobalFSlide for column 1 then 2,
 * per channel, before the cross-channel effect_def / effect_def2 Case passes. */
void Ca2mv2Player::play_line_apply_global_fslide_row(tADTRACK2_EVENT *event, int chan)
{
    for (int slot = 0; slot < 2; slot++) {
        uint8_t def = event->eff[slot].def;
        uint8_t val = event->eff[slot].val;

        if ((def != ef_GlobalFSlideUp) && (def != ef_GlobalFSlideDown))
            continue;

        if ((event->eff[slot ^ 1].def == ef_Extended) &&
            (event->eff[slot ^ 1].val == ef_ex_ExtendedCmd * 16 + ef_ex_cmd_ForceBpmSld)) {

            AdPlug_LogWrite("ef_GlobalFSlideUp or ef_GlobalFSlideDown with ef_ex_cmd_ForceBpmSld\n");

            if (def == ef_GlobalFSlideUp) {
                update_playback_speed(val);
            } else {
                update_playback_speed(-val);
            }
        } else {
            uint8_t eff;

            switch (def) {
            case ef_GlobalFSlideUp:
                eff = ef_FSlideUp;

                // >xx + ZFE
                if ((event->eff[slot ^ 1].def == ef_Extended) &&
                    (event->eff[slot ^ 1].val == ef_ex_ExtendedCmd2 * 16 + ef_ex_cmd2_FTrm_XFGFS)) {
                    eff = ef_GlobalFreqSlideUpXF;
                }

                // >xx + ZFD
                if ((event->eff[slot ^ 1].def == ef_Extended) &&
                    (event->eff[slot ^ 1].val == ef_ex_ExtendedCmd2 * 16 + ef_ex_cmd2_FVib_FGFS)) {
                    eff = ef_FSlideUpFine;
                }

                ch->effect_table[slot][chan].def = eff;
                ch->effect_table[slot][chan].val = val;
                break;
            case ef_GlobalFSlideDown:
                eff = ef_FSlideDown;

                // <xx + ZFE
                if ((event->eff[slot ^ 1].def == ef_Extended) &&
                    (event->eff[slot ^ 1].val == ef_ex_ExtendedCmd2 * 16 + ef_ex_cmd2_FTrm_XFGFS)) {
                    eff = ef_GlobalFreqSlideDnXF;
                }

                // <xx + ZFD
                if ((event->eff[slot ^ 1].def == ef_Extended) &&
                    (event->eff[slot ^ 1].val == ef_ex_ExtendedCmd2 * 16 + ef_ex_cmd2_FVib_FGFS)) {
                    eff = ef_FSlideDownFine;
                }

                ch->effect_table[slot][chan].def = eff;
                ch->effect_table[slot][chan].val = val;
                break;
            }

            for (int c = chan; c < songinfo->nm_tracks; c++) {
                ch->fslide_table[slot][c] = val;
                ch->glfsld_table[slot][c].def = ch->effect_table[slot][chan].def;
                ch->glfsld_table[slot][c].val = ch->effect_table[slot][chan].val;
            }
        }
    }
}

/* a2player.pas ~1452–1466: once per row after init loop, from pattern columns. */
void Ca2mv2Player::play_line_tremor_row_reset(const tADTRACK2_EVENT *event, int chan)
{
    for (int slot = 0; slot < 2; slot++) {
        if (ch->tremor_table[slot][chan].pos && (event->eff[slot].def != ef_Tremor)) {
            ch->tremor_table[slot][chan].pos = 0;
            set_ins_volume(ch->tremor_table[slot][chan].volM, ch->tremor_table[slot][chan].volC, chan);
        }
    }
}

void Ca2mv2Player::process_effects_prepare(tADTRACK2_EVENT *event, int slot, int chan)
{
    uint8_t def = event->eff[slot].def;

    if ((def != ef_Vibrato) &&
        (def != ef_ExtraFineVibrato) &&
        (def != ef_VibratoVolSlide) &&
        (def != ef_VibratoVSlideFine))
        memset(&ch->vibr_table[slot][chan], 0, sizeof(ch->vibr_table[slot][chan]));

    if ((def != ef_RetrigNote) &&
        (def != ef_MultiRetrigNote))
        memset(&ch->retrig_table[slot][chan], 0, sizeof(ch->retrig_table[slot][chan]));

    if ((def != ef_Tremolo) &&
        (def != ef_ExtraFineTremolo))
        memset(&ch->trem_table[slot][chan], 0, sizeof(ch->trem_table[slot][chan]));
}

void Ca2mv2Player::process_effects(tADTRACK2_EVENT *event, int slot, int chan)
{
    uint8_t def = event->eff[slot].def;
    uint8_t val = event->eff[slot].val;

    switch (def) {
    case ef_Arpeggio:
    case ef_ExtraFineArpeggio:
    case ef_ArpggVSlide:
    case ef_ArpggVSlideFine:
        /* Pascal (a2player.pas ~1498-1499): skip entire arpeggio block when
         * effect_def == ef_Arpeggio AND effect == 0. This preserves the
         * previous arpeggio state/add1/add2 instead of resetting them. */
        if ((def == ef_Arpeggio) && (val == 0))
            break;

        switch (def) {
        case ef_Arpeggio:
            ch->effect_table[slot][chan].def = ef_Arpeggio;
            ch->effect_table[slot][chan].val = val;
            break;
        case ef_ExtraFineArpeggio:
            ch->effect_table[slot][chan].def = ef_ExtraFineArpeggio;
            ch->effect_table[slot][chan].val = val;
            break;
        case ef_ArpggVSlide:
        case ef_ArpggVSlideFine:
            update_effect_table(slot, chan, EFGR_ARPVOLSLIDE, def, val);
            break;
        }

        {
            uint8_t raw_note = event->note & ~keyoff_flag;
            bool has_new_note = note_in_range(raw_note);
            uint8_t resolved_note = has_new_note
                ? raw_note
                : (note_in_range(ch->event_table[chan].note & ~keyoff_flag)
                    ? ch->event_table[chan].note & ~keyoff_flag
                    : 0);

            /* Pascal (a2player.pas ~1527-1529): check last_effect (saved at
             * start of play_line before effect_table was cleared) to decide
             * whether to reset arpeggio state on carry-over rows. */
            uint8_t prev_def = ch->last_effect[slot][chan].def;
            bool prev_was_arp = (prev_def == ef_Arpeggio) ||
                                (prev_def == ef_ExtraFineArpeggio) ||
                                (prev_def == ef_ArpggVSlide) ||
                                (prev_def == ef_ArpggVSlideFine);

            if (resolved_note) {
                if (has_new_note) {
                    /* Pascal: event[chan].note is in range — always reset */
                    ch->arpgg_table[slot][chan].state = 0;
                } else if (!prev_was_arp) {
                    /* Pascal: carry-over from event_table — check last_effect */
                    ch->arpgg_table[slot][chan].state = 0;
                }

                ch->arpgg_table[slot][chan].note = resolved_note;
                if ((def == ef_Arpeggio) || (def == ef_ExtraFineArpeggio)) {
                    ch->arpgg_table[slot][chan].add1 = (val >> 4) & 0x0f;
                    ch->arpgg_table[slot][chan].add2 = val & 0x0f;
                }
            } else {
                ch->effect_table[slot][chan].def = 0;
                ch->effect_table[slot][chan].val = 0;
            }
        }
        break;

    case ef_FSlideUp:
    case ef_FSlideDown:
    case ef_FSlideUpFine:
    case ef_FSlideDownFine:
        ch->effect_table[slot][chan].def = def;
        ch->effect_table[slot][chan].val = val;
        ch->fslide_table[slot][chan] = val;
        break;

    case ef_FSlideUpVSlide:
    case ef_FSlUpVSlF:
    case ef_FSlideDownVSlide:
    case ef_FSlDownVSlF:
    case ef_FSlUpFineVSlide:
    case ef_FSlUpFineVSlF:
    case ef_FSlDownFineVSlide:
    case ef_FSlDownFineVSlF:
        update_effect_table(slot, chan, EFGR_FSLIDEVOLSLIDE, def, val);
        break;

    case ef_TonePortamento:
        /* Pascal (a2player.pas ~1577-1600, ~2155-2176): TonePortamento is activated
         * only when the row carries a plain note (1..12*8+1, no keyoff bit) OR when
         * last_effect's type already matches ef_TonePortamento (carry-over).
         * A key-off note (note | 0x80) is NOT in [1..97], so Pascal falls through
         * to the carry-over branch — effect_table/speed only set if prior porta existed. */
        {
            uint8_t has_note = event->note >= 1 && event->note  <= 12*8+1;
            bool has_carry = ch->last_effect[slot][chan].def == ef_TonePortamento;

            if (has_note || has_carry) {
                if (event->eff[slot].val != 0) {
                    ch->effect_table[slot][chan].def = ef_TonePortamento;
                    ch->effect_table[slot][chan].val = val;
                } else if (has_carry && ch->last_effect[slot][chan].val != 0) {
                    ch->effect_table[slot][chan].def = ef_TonePortamento;
                    ch->effect_table[slot][chan].val = ch->last_effect[slot][chan].val;
                } else {
                    ch->effect_table[slot][chan].def = ef_TonePortamento;
                    ch->effect_table[slot][chan].val = 0;
                }

                ch->porta_table[slot][chan].speed = ch->effect_table[slot][chan].val;
                if (has_note) {
                    ch->porta_table[slot][chan].freq =
                        nFreq((event->note - 1)) +
                        get_instr_fine_tune(ch->event_table[chan].instr_def);
                }
            }
        }
        break;

    case ef_TPortamVolSlide:
    case ef_TPortamVSlideFine:
        update_effect_table(slot, chan, EFGR_PORTAVOLSLIDE, def, val);

        break;

    case ef_Vibrato:
    case ef_ExtraFineVibrato:
        update_effect_table(slot, chan, EFGR_VIBRATO, def, val);

        if ((event->eff[slot ^ 1].def == ef_Extended) &&
            (event->eff[slot ^ 1].val == ef_ex_ExtendedCmd2 * 16 + ef_ex_cmd2_FVib_FGFS)) {
            ch->vibr_table[slot][chan].fine = true;
        }

        /* Pascal: vibr_table.speed := HI(effect_table) DIV 16 (after merge / x00 carry). */
        {
            uint8_t m = ch->effect_table[slot][chan].val;
            ch->vibr_table[slot][chan].speed = m / 16;
            ch->vibr_table[slot][chan].depth = m % 16;
        }
        break;

    case ef_Tremolo:
    case ef_ExtraFineTremolo:
        update_effect_table(slot, chan, EFGR_TREMOLO, def, val);

        if ((event->eff[slot ^ 1].def == ef_Extended) &&
            (event->eff[slot ^ 1].val == ef_ex_ExtendedCmd2 * 16 + ef_ex_cmd2_FTrm_XFGFS)) {
            ch->trem_table[slot][chan].fine = true;
        }

        {
            uint8_t m = ch->effect_table[slot][chan].val;
            ch->trem_table[slot][chan].speed = m / 16;
            ch->trem_table[slot][chan].depth = m % 16;
        }
        break;

    case ef_VibratoVolSlide:
    case ef_VibratoVSlideFine:
        update_effect_table(slot, chan, EFGR_VIBRATOVOLSLIDE, def, val);

        if ((event->eff[slot ^ 1].def == ef_Extended) &&
            (event->eff[slot ^ 1].val == ef_ex_ExtendedCmd2 * 16 + ef_ex_cmd2_FVib_FGFS))
            ch->vibr_table[slot][chan].fine = true;
        break;

    case ef_SetCarrierVol:
        set_ins_volume(BYTE_NULL, 63 - val, chan);
        /* Pascal (a2player.pas ~1661): one-shot — does NOT set effect_table.
         * Clear def so last_effect captures {def=0, val=preserved}. */
        ch->effect_table[slot][chan].def = 0;
        break;

    case ef_SetModulatorVol:
        set_ins_volume(63 - val, BYTE_NULL, chan);
        ch->effect_table[slot][chan].def = 0;
        break;

    case ef_SetInsVolume:
        {
            tINSTR_DATA *instr = get_instr_data_by_ch(chan);
            if (!instr || is_data_empty((char *)instr, sizeof(tINSTR_DATA))) {
                ch->effect_table[slot][chan].def = 0;
                break;
            }

            if (_4op_vol_valid_chan(chan)) {
                set_ins_volume_4op(63 - val, chan);
            } else if (percussion_mode && ((chan >= 16) && (chan <= 19))) {
                set_ins_volume(63 - val, BYTE_NULL, chan);
            } else if (instr->fm.connect == 0) {
                set_ins_volume(BYTE_NULL, 63 - val, chan);
            } else {
                set_ins_volume(63 - val, 63 - val, chan);
            }
            ch->effect_table[slot][chan].def = 0;
            break;
    }

    case ef_ForceInsVolume:
        {
            tINSTR_DATA *instr = get_instr_data_by_ch(chan);
            if (!instr || is_data_empty((char *)instr, sizeof(tINSTR_DATA))) {
                ch->effect_table[slot][chan].def = 0;
                break;
            }

            if (percussion_mode && ((chan >= 16) && (chan <= 19))) {
                set_ins_volume(63 - val, BYTE_NULL, chan);
            } else if (instr->fm.connect == 0) {
                set_ins_volume(scale_volume(instr->fm.volM, 63 - val), 63 - val, chan);
            } else {
                set_ins_volume(63 - val, 63 - val, chan);
            }
            ch->effect_table[slot][chan].def = 0;
            break;
        }

    case ef_PositionJump:
        if (no_loop(chan, current_line)) {
            pattern_break = true;
            next_line = pattern_break_flag + chan;
        }
        ch->effect_table[slot][chan].def = 0;
        break;

    case ef_PatternBreak:
        if (no_loop(chan, current_line)) {
            pattern_break = true;
            next_line = max(val, songinfo->patt_len - 1);
        }
        ch->effect_table[slot][chan].def = 0;
        break;

    case ef_SetSpeed:
        speed = val;
        ch->effect_table[slot][chan].def = 0;
        break;

    case ef_SetTempo:
        update_timer(val);
        ch->effect_table[slot][chan].def = 0;
        break;

    case ef_SetWaveform:
        if (val / 16 <= 7) {
            ch->fmpar_table[chan].wformC = val / 16;
            update_carrier_adsrw(chan);
        }

        if (val % 16 <= 7) {
            ch->fmpar_table[chan].wformM = val % 16;
            update_modulator_adsrw(chan);
        }
        ch->effect_table[slot][chan].def = 0;
        break;

    case ef_VolSlide:
        ch->effect_table[slot][chan].def = def;
        ch->effect_table[slot][chan].val = val;
        break;

    case ef_VolSlideFine:
        ch->effect_table[slot][chan].def = def;
        ch->effect_table[slot][chan].val = val;
        break;

    case ef_RetrigNote:
        if (val) {
            if (get_effect_group(ch->last_effect[slot][chan].def) != EFGR_RETRIGNOTE) {
                ch->retrig_table[slot][chan] = 1;
            }

            ch->effect_table[slot][chan].def = def;
            ch->effect_table[slot][chan].val = val;
        }
        break;

    case ef_MultiRetrigNote:
        if (val / 16) {
            if (get_effect_group(ch->last_effect[slot][chan].def) != EFGR_RETRIGNOTE) {
                ch->retrig_table[slot][chan] = 1;
            }

            ch->effect_table[slot][chan].def = def;
            ch->effect_table[slot][chan].val = val;
        }
        break;

    case ef_SetGlobalVolume:
        global_volume = val;
        set_global_volume();
        ch->effect_table[slot][chan].def = 0;
        break;

    case ef_Tremor:
        if (val) {
            if (ch->last_effect[slot][chan].def != ef_Tremor) {
                ch->tremor_table[slot][chan].pos = 0;
                ch->tremor_table[slot][chan].volM = ch->fmpar_table[chan].volM;
                ch->tremor_table[slot][chan].volC = ch->fmpar_table[chan].volC;
            }

            ch->effect_table[slot][chan].def = def;
            ch->effect_table[slot][chan].val = val;
        }
        break;

    case ef_Extended:
        switch (val / 16) {
        case ef_ex_SetTremDepth:
            switch (val % 16) {
            case 0:
                opl3out(0xbd, misc_register & 0x7f);
                current_tremolo_depth = 0;
                break;

            case 1:
                opl3out(0xbd, misc_register | 0x80);
                current_tremolo_depth = 1;
                break;
            }
            break;

        case ef_ex_SetVibDepth:
            switch (val % 16) {
            case 0:
                opl3out(0xbd, misc_register & 0xbf);
                current_vibrato_depth = 0;
                break;

            case 1:
                opl3out(0xbd, misc_register | 0x40);
                current_vibrato_depth = 1;
                break;
            }
            break;

        case ef_ex_SetAttckRateM:
            ch->fmpar_table[chan].attckM = val % 16;
            update_modulator_adsrw(chan);
            break;

        case ef_ex_SetDecayRateM:
            ch->fmpar_table[chan].decM = val % 16;
            update_modulator_adsrw(chan);
            break;

        case ef_ex_SetSustnLevelM:
            ch->fmpar_table[chan].sustnM = val % 16;
            update_modulator_adsrw(chan);
            break;

        case ef_ex_SetRelRateM:
            ch->fmpar_table[chan].relM = val % 16;
            update_modulator_adsrw(chan);
            break;

        case ef_ex_SetAttckRateC:
            ch->fmpar_table[chan].attckC = val % 16;
            update_carrier_adsrw(chan);
            break;

        case ef_ex_SetDecayRateC:
            ch->fmpar_table[chan].decC = val % 16;
            update_carrier_adsrw(chan);
            break;

        case ef_ex_SetSustnLevelC:
            ch->fmpar_table[chan].sustnC = val % 16;
            update_carrier_adsrw(chan);
            break;

        case ef_ex_SetRelRateC:
            ch->fmpar_table[chan].relC = val % 16;
            update_carrier_adsrw(chan);
            break;

        case ef_ex_SetFeedback:
            ch->fmpar_table[chan].feedb = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex_SetPanningPos:
            ch->panning_table[chan] = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex_PatternLoop:
        case ef_ex_PatternLoopRec:
            if (val % 16 == 0) {
                ch->loopbck_table[chan] = current_line;
            } else {
                if (ch->loopbck_table[chan] != BYTE_NULL) {
                    if (ch->loop_table[chan][current_line] == BYTE_NULL)
                        ch->loop_table[chan][current_line] = val % 16;

                    if (ch->loop_table[chan][current_line] != 0) {
                        pattern_break = true;
                        next_line = pattern_loop_flag + chan;
                    } else {
                        if (val / 16 == ef_ex_PatternLoopRec)
                            ch->loop_table[chan][current_line] = BYTE_NULL;
                    }
                }
            }
            break;
        case ef_ex_ExtendedCmd:
            switch (val & 0x0f) {
            case ef_ex_cmd_MKOffLoopDi: ch->keyoff_loop[chan] = false;   break;
            case ef_ex_cmd_MKOffLoopEn: ch->keyoff_loop[chan] = true;    break;
            case ef_ex_cmd_TPortaFKdis: ch->portaFK_table[chan] = false; break;
            case ef_ex_cmd_TPortaFKenb: ch->portaFK_table[chan] = true;  break;
            case ef_ex_cmd_RestartEnv:
                key_on(chan);
                change_freq(chan, ch->freq_table[chan]);
                break;
            case ef_ex_cmd_4opVlockOff:
                if (is_4op_chan(chan)) {
                    ch->vol4op_lock[chan] = false;
                    int i = is_4op_chan_hi(chan) ? 1 : -1;

                    ch->vol4op_lock[chan + i] = false;
                }
                break;
            case ef_ex_cmd_4opVlockOn:
                if (is_4op_chan(chan)) {
                    ch->vol4op_lock[chan] = true;
                    int i = is_4op_chan_hi(chan) ? 1 : -1;

                    ch->vol4op_lock[chan + i] = true;
                }
                break;
            }
            break;
        case ef_ex_ExtendedCmd2:
            switch (val % 16) {
            case ef_ex_cmd2_RSS:        release_sustaining_sound(chan); break;
            case ef_ex_cmd2_ResetVol:   reset_ins_volume(chan); break;
            case ef_ex_cmd2_LockVol:    ch->volume_lock  [chan] = true; break;
            case ef_ex_cmd2_UnlockVol:  ch->volume_lock  [chan] = false; break;
            case ef_ex_cmd2_LockVP:     ch->peak_lock    [chan] = true; break;
            case ef_ex_cmd2_UnlockVP:   ch->peak_lock    [chan] = false; break;
            case ef_ex_cmd2_VSlide_def: ch->volslide_type[chan] = 0; break;
            case ef_ex_cmd2_LockPan:    ch->pan_lock     [chan] = true; break;
            case ef_ex_cmd2_UnlockPan:  ch->pan_lock     [chan] = false; break;
            case ef_ex_cmd2_VibrOff:    change_frequency(chan, ch->freq_table[chan]); break;
            case ef_ex_cmd2_TremOff:
                if (is_4op_chan(chan)) {
                    set_ins_volume_4op(BYTE_NULL, chan);
                } else {
                    set_ins_volume(ch->fmpar_table[chan].volM, ch->fmpar_table[chan].volC, chan);
                }
                break;
            case ef_ex_cmd2_VSlide_car:
                if ((event->eff[slot ^ 1].def == ef_Extended) &&
                    (event->eff[slot ^ 1].val == ef_ex_ExtendedCmd2 * 16 +
                              ef_ex_cmd2_VSlide_mod)) {
                    ch->volslide_type[chan] = 3;
                } else {
                    ch->volslide_type[chan] = 1;
                }
                break;

            case ef_ex_cmd2_VSlide_mod:
                if ((event->eff[slot ^ 1].def == ef_Extended) &&
                    (event->eff[slot ^ 1].val == ef_ex_ExtendedCmd2 * 16 +
                              ef_ex_cmd2_VSlide_car)) {
                    ch->volslide_type[chan] = 3;
                } else {
                    ch->volslide_type[chan] = 2;
                }
                break;
            }
            break;
        }
        break;

    case ef_Extended2:
        switch (val / 16) {
        case ef_ex2_PatDelayFrame:
            pattern_delay = true;
            tickD = val % 16;
            break;

        case ef_ex2_PatDelayRow:
            pattern_delay = true;
            tickD = speed * (val % 16);
            break;

        case ef_ex2_NoteDelay:
            ch->effect_table[slot][chan].def = ef_Extended2;
            ch->effect_table[slot][chan].val = val;
            ch->notedel_table[chan] = val % 16;
            break;

        case ef_ex2_NoteCut:
            ch->effect_table[slot][chan].def = ef_Extended2;
            ch->effect_table[slot][chan].val = val;
            ch->notecut_table[chan] = val % 16;
            break;

        case ef_ex2_FineTuneUp:
            ch->ftune_table[chan] += val % 16;
            break;

        case ef_ex2_FineTuneDown:
            ch->ftune_table[chan] -= val % 16;
            break;

        case ef_ex2_GlVolSlideUp:
        case ef_ex2_GlVolSlideDn:
        case ef_ex2_GlVolSlideUpF:
        case ef_ex2_GlVolSlideDnF:
        case ef_ex2_GlVolSldUpXF:
        case ef_ex2_GlVolSldDnXF:
        case ef_ex2_VolSlideUpXF:
        case ef_ex2_VolSlideDnXF:
        case ef_ex2_FreqSlideUpXF:
        case ef_ex2_FreqSlideDnXF:
            ch->effect_table[slot][chan].def = ef_Extended2;
            ch->effect_table[slot][chan].val = val;
            break;
        }
        break;

    case ef_Extended3:
        switch (val / 16) {
        case ef_ex3_SetConnection:
            ch->fmpar_table[chan].connect = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex3_SetMultipM:
            ch->fmpar_table[chan].multipM = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex3_SetKslM:
            ch->fmpar_table[chan].kslM = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex3_SetTremoloM:
            ch->fmpar_table[chan].tremM = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex3_SetVibratoM:
            ch->fmpar_table[chan].vibrM = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex3_SetKsrM:
            ch->fmpar_table[chan].ksrM = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex3_SetSustainM:
            ch->fmpar_table[chan].sustM = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex3_SetMultipC:
            ch->fmpar_table[chan].multipC = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex3_SetKslC:
            ch->fmpar_table[chan].kslC = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex3_SetTremoloC:
            ch->fmpar_table[chan].tremC = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex3_SetVibratoC:
            ch->fmpar_table[chan].vibrC = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex3_SetKsrC:
            ch->fmpar_table[chan].ksrC = val % 16;
            update_fmpar(chan);
            break;

        case ef_ex3_SetSustainC:
            ch->fmpar_table[chan].sustC = val % 16;
            update_fmpar(chan);
            break;
        }
        break;
    }
}

static bool no_swap_and_restart(tADTRACK2_EVENT *event)
{
    // [!xx/@xx] swap arp/swap vib + [zff] no force restart
    return
        !(((event->eff[1].def == ef_SwapArpeggio) ||
            (event->eff[1].def == ef_SwapVibrato)) &&
            (event->eff[0].def == ef_Extended) &&
            (event->eff[0].val == ef_ex_ExtendedCmd2 * 16 + ef_ex_cmd2_NoRestart)) &&
        !(((event->eff[0].def == ef_SwapArpeggio) ||
            (event->eff[0].def == ef_SwapVibrato)) &&
            (event->eff[1].def == ef_Extended) &&
            (event->eff[1].val == ef_ex_ExtendedCmd2 * 16 + ef_ex_cmd2_NoRestart));
}

static bool is_eff_porta(tADTRACK2_EVENT *event)
{
    int eff0 = event->eff[0].def;
    bool is_p0 = (eff0 == ef_TonePortamento) ||
                (eff0 == ef_TPortamVolSlide) ||
                (eff0 == ef_TPortamVSlideFine);
    int eff1 = event->eff[1].def;
    bool is_p1 = (eff1 == ef_TonePortamento) ||
                (eff1 == ef_TPortamVolSlide) ||
                (eff1 == ef_TPortamVSlideFine);
    return is_p0 || is_p1;
}

static bool is_eff_notedelay(tADTRACK2_EVENT *event)
{
    return (
        (event->eff[0].def == ef_Extended2 && (event->eff[0].val / 16 == ef_ex2_NoteDelay)) ||
        (event->eff[1].def == ef_Extended2 && (event->eff[1].val / 16 == ef_ex2_NoteDelay))
    );
}

void Ca2mv2Player::new_process_note(tADTRACK2_EVENT *event, int chan)
{
    bool tporta_flag = is_eff_porta(event);
    bool notedelay_flag = is_eff_notedelay(event);
    bool defer_note_row = tporta_flag || notedelay_flag;

    if (event->note == 0) {
        if (ch->ftune_table[chan])
            output_note(0, ch->voice_table[chan], chan, true, true);
        return;
    }

    /*
     * Pascal play_line (a2player.pas): key_off runs when the row carries key-off
     * BEFORE the branches that defer notes (tone porta / note delay). Otherwise a
     * cell like Extended2+NoteDelay + key-off never calls key_off and OPL KEY-ON
     * stays set (fank5 ~IRQ 47232, secondary shadow_regs[1][0xb0]).
     */
    if (event->note & keyoff_flag) {
        key_off(chan);
        return;
    }

    /* Same-row immediate output (a2player.pas ~2656–2667): both slots must lack porta & notedelay in LO. */
    if (!defer_note_row) {
        output_note(event->note, ch->voice_table[chan], chan, true, no_swap_and_restart(event));
        return;
    }

    /* a2player.pas: old note had keyoff — retrigger from stored pitch */
    if ((event->note != 0) && tporta_flag && (ch->event_table[chan].note & keyoff_flag)) {
        output_note(ch->event_table[chan].note & ~keyoff_flag,
                    ch->voice_table[chan], chan, false, true);
        return;
    }

    if (event->note != 0) {
        if (ch->portaFK_table[chan] && tporta_flag) {
            output_note(event->note, event->instr_def, chan, false, true);
        } else {
            ch->event_table[chan].note = event->note;
        }
    }
}

void Ca2mv2Player::play_line()
{
    /* Align with a2player.pas play_line:
     * - Per channel: pattern + set_ins + prepare both slots + arpeggio cleanup + GlobalFSlide
     *   (Pascal's first loop), then tremor row reset (second loop).
     * - Then process column-1 (slot 0) row effects for *all* channels, then column-2
     *   for all (Pascal's Case effect_def then Case effect_def2).
     *   Running both columns per channel before advancing let column-2 effects (e.g.
     *   Z21 pattern-delay-rows: tickD = speed * n) see a stale speed when a higher
     *   channel's column-1 had Dxx SetSpeed (tunes/MLF/PINK.A2T).
     * - Final pass: new_process_note + swap macros + update_fine_effects per channel. */
    tADTRACK2_EVENT events[20];
    tADTRACK2_EVENT *event;

    assert((int)songinfo->nm_tracks <= 20);

    for (int chan = 0; chan < songinfo->nm_tracks; chan++) {
        event = &events[chan];
        // save effect_table into last_effect
        for (int slot = 0; slot < 2; slot++) {
            if (ch->effect_table[slot][chan].def | ch->effect_table[slot][chan].val) {
                ch->last_effect[slot][chan].def = ch->effect_table[slot][chan].def;
                ch->last_effect[slot][chan].val = ch->effect_table[slot][chan].val;
            }
            if (ch->glfsld_table[slot][chan].def | ch->glfsld_table[slot][chan].val) {
                ch->effect_table[slot][chan].def = ch->glfsld_table[slot][chan].def;
                ch->effect_table[slot][chan].val = ch->glfsld_table[slot][chan].val;
            } else {
                /* a2player.pas ~1288: effect_table := effect_table AND $0ff00 — clear LO (def), keep HI (val) */
                ch->effect_table[slot][chan].def = 0;
            }
        }

        ch->ftune_table[chan] = 0;

        // Do a full copy of the event, because we modify event->note
        *event = *get_event_p(current_pattern, chan, current_line); // Copy struct

        // Fixup event->note
        if (event->note == 0xff) { // Key off
            event->note = ch->event_table[chan].note | keyoff_flag;
        } else if ((event->note >= fixed_note_flag + 1) && (event->note <= fixed_note_flag + 12*8+1)) {
            event->note -= fixed_note_flag;
        }

        /* Normalize arpeggio: raw file uses def=0 for arpeggio. Convert to
         * ef_Arpeggio (0x80) so it's distinguishable from "no effect". */
        for (int slot = 0; slot < 2; slot++) {
            if (event->eff[slot].def == 0 && event->eff[slot].val != 0)
                event->eff[slot].def = ef_Arpeggio;
        }

        /* Pascal play_line (~1313-1322): unconditional eff copy — when note, instr,
         * or any eff field is non-zero, write ALL eff fields (including zeros) to
         * event_table.  This clears previous eff when a new note appears without eff. */
        if (event->note != 0 || event->instr_def != 0 ||
            (event->eff[0].def | event->eff[0].val) ||
            (event->eff[1].def | event->eff[1].val))
        {
            ch->event_table[chan].eff[0].def = event->eff[0].def;
            ch->event_table[chan].eff[0].val = event->eff[0].val;
            ch->event_table[chan].eff[1].def = event->eff[1].def;
            ch->event_table[chan].eff[1].val = event->eff[1].val;
        }

        // alters ch->event_table[].instr_def
        set_ins_data(event->instr_def, chan);

        // set effect_table here
        process_effects_prepare(event, 0, chan);
        process_effects_prepare(event, 1, chan);
        play_line_arpgg_cleanup(event, chan);
        play_line_apply_global_fslide_row(event, chan);
        play_line_tremor_row_reset(event, chan);
    }

    for (int chan = 0; chan < songinfo->nm_tracks; chan++) {
        event = &events[chan];
        process_effects(event, 0, chan);
    }

    for (int chan = 0; chan < songinfo->nm_tracks; chan++) {
        event = &events[chan];
        process_effects(event, 1, chan);
    }

    for (int chan = 0; chan < songinfo->nm_tracks; chan++) {
        for (int slot = 0; slot < 2; slot++) {
            if ((events[chan].eff[slot].def == 0) && (events[chan].eff[slot].val == 0)) {
                if ((ch->glfsld_table[slot][chan].def == 0) && (ch->glfsld_table[slot][chan].val == 0)) {
                    ch->effect_table[slot][chan].def = 0;
                    ch->effect_table[slot][chan].val = 0;
                }
            } else {
                ch->event_table[chan].eff[slot].def = events[chan].eff[slot].def;
                ch->event_table[chan].eff[slot].val = events[chan].eff[slot].val;
            }
        }
    }

    for (int chan = 0; chan < songinfo->nm_tracks; chan++) {
        event = &events[chan];
        new_process_note(event, chan);

        check_swap_arp_vibr(event, 0, chan);
        check_swap_arp_vibr(event, 1, chan);

        update_fine_effects(0, chan);
        update_fine_effects(1, chan);
    }
}

void Ca2mv2Player::generate_custom_vibrato(uint8_t value)
{
    const uint8_t vibtab_size[16] = { 16,16,16,16,32,32,32,32,64,64,64,64,128,128,128,128 };
    int idx, idx2;

    #define min0(VALUE) ((int)VALUE >= 0 ? (int)VALUE : 0)

    if (value == 0) {
        // 0: set default speed table
        vibtrem_table_size = def_vibtrem_table_size;
        memcpy(&vibtrem_table, &def_vibtrem_table, sizeof(vibtrem_table));
    } else if (value <= 239) {
        // 1-239: set custom speed table (fixed size = 32)
        vibtrem_table_size = def_vibtrem_table_size;
        double mul_r = (double)value / 16.0;

        for (idx2 = 0; idx2 <= 7; idx2++) {
            vibtrem_table[idx2 * 32] = 0;

            for (idx = 1; idx <= 16; idx++) {
                vibtrem_table[idx2 * 32 + idx] = (uint8_t)round(idx * mul_r);
            }

            for (idx = 17; idx <= 31; idx++) {
                vibtrem_table[idx2 * 32 + idx] = (uint8_t)round((32 - idx) * mul_r);
            }
        }
    } else {
        // 240-255: set custom speed table (speed factor = 1-4)
        vibtrem_speed_factor = (value - 240) % 4 + 1;
        vibtrem_table_size = 2 * vibtab_size[value - 240];
        int mul_b = 256 / (vibtab_size[value - 240]);

        for (idx2 = 0; idx2 <= 128 / vibtab_size[value - 240] - 1; idx2++) {
            vibtrem_table[2 * vibtab_size[value - 240] * idx2] = 0;

            for (idx = 1; idx <= vibtab_size[value - 240]; idx++) {
                vibtrem_table[2 * vibtab_size[value - 240] * idx2 + idx] =
                    min0(idx * mul_b - 1);
            }

            for (idx = vibtab_size[value - 240] + 1; idx <= 2 * vibtab_size[value - 240] - 1; idx++) {
                vibtrem_table[2 * vibtab_size[value - 240] * idx2 + idx] =
                    min0((2 * vibtab_size[value - 240] - idx) * mul_b - 1);
            }
        }
    }
}

void Ca2mv2Player::check_swap_arp_vibr(tADTRACK2_EVENT *event, int slot, int chan)
{
    // Check if second effect is ZFF - force no restart
    bool is_norestart = (event->eff[slot ^ 1].def == ef_Extended) &&
                        (event->eff[slot ^ 1].val == (ef_ex_ExtendedCmd2 * 16 + ef_ex_cmd2_NoRestart));

    switch (event->eff[slot].def) {
    case ef_SwapArpeggio:
        if (is_norestart) {
            tARPEGGIO_TABLE *arp = get_arpeggio_table(event->eff[slot].val);
            uint8_t length = arp ? arp->length : 0;

            if (ch->macro_table[chan].arpg_pos > length)
                ch->macro_table[chan].arpg_pos = length;
            ch->macro_table[chan].arpg_table = event->eff[slot].val;
        } else {
            ch->macro_table[chan].arpg_count = 1;
            ch->macro_table[chan].arpg_pos = 0;
            ch->macro_table[chan].arpg_table = event->eff[slot].val;
            ch->macro_table[chan].arpg_note = ch->event_table[chan].note;
        }
        break;

    case ef_SwapVibrato:
        if (is_norestart) {
            tVIBRATO_TABLE *vib = get_vibrato_table(event->eff[slot].val);
            uint8_t length = vib ? vib->length : 0;

            if (ch->macro_table[chan].vib_pos > length)
                ch->macro_table[chan].vib_pos = length;
            ch->macro_table[chan].vib_table = event->eff[slot].val;
        } else {
            tVIBRATO_TABLE *vib = get_vibrato_table(ch->macro_table[chan].vib_table);
            uint8_t vib_delay = vib ? vib->delay : 0;

            ch->macro_table[chan].vib_count = 1;
            ch->macro_table[chan].vib_pos = 0;
            ch->macro_table[chan].vib_table = event->eff[slot].val;
            ch->macro_table[chan].vib_delay = vib_delay;
        }
        break;
    case ef_SetCustomSpeedTab:
        generate_custom_vibrato(event->eff[slot].val);
        break;
    }
}

void Ca2mv2Player::portamento_up(int chan, uint16_t slide, uint16_t limit)
{
    uint16_t freq;

    if ((ch->freq_table[chan] & 0x1fff) == 0) return;

    freq = calc_freq_shift_up(ch->freq_table[chan] & 0x1fff, slide);

    change_frequency(chan, freq <= limit ? freq : limit);
}

void Ca2mv2Player::portamento_down(int chan, uint16_t slide, uint16_t limit)
{
    uint16_t freq;

    if ((ch->freq_table[chan] & 0x1fff) == 0) return;

    freq = calc_freq_shift_down(ch->freq_table[chan] & 0x1fff, slide);

    change_frequency(chan, freq >= limit ? freq : limit);
}

void Ca2mv2Player::macro_vibrato__porta_up(int chan, uint8_t depth)
{
    uint16_t freq = calc_freq_shift_up(ch->macro_table[chan].vib_freq & 0x1fff, depth);
    uint16_t newfreq = freq <= nFreq(12*8+1) ? freq : nFreq(12*8+1);

    change_freq(chan, newfreq);
}

void Ca2mv2Player::macro_vibrato__porta_down(int chan, uint8_t depth)
{
    uint16_t freq = calc_freq_shift_down(ch->macro_table[chan].vib_freq & 0x1fff, depth);
    uint16_t newfreq = freq >= nFreq(0) ? freq : nFreq(0);

    change_freq(chan, newfreq);
}

void Ca2mv2Player::tone_portamento(int slot, int chan)
{
    uint16_t freq = ch->freq_table[chan] & 0x1fff;
    /* Pascal tone_portamento: compare to porta_table.freq without masking (cf. a2player.pas). */
    uint16_t portafreq = ch->porta_table[slot][chan].freq;

    if (freq > portafreq) {
        portamento_down(chan, ch->porta_table[slot][chan].speed, portafreq);
    } else if (freq < portafreq) {
        portamento_up(chan, ch->porta_table[slot][chan].speed, portafreq);
    }
}

void Ca2mv2Player::slide_carrier_volume_up(uint8_t chan, uint8_t slide, uint8_t limit)
{
    uint8_t volC = ch->fmpar_table[chan].volC;
    uint8_t newvolC = (volC - slide >= limit) ? volC - slide : limit;

    set_ins_volume(BYTE_NULL, newvolC, chan);
}

void Ca2mv2Player::slide_modulator_volume_up(uint8_t chan, uint8_t slide, uint8_t limit)
{
    uint8_t volM = ch->fmpar_table[chan].volM;
    uint8_t newvolM = (volM - slide >= limit) ? volM - slide : limit;

    set_ins_volume(newvolM, BYTE_NULL, chan);
}

void Ca2mv2Player::slide_volume_up(int chan, uint8_t slide)
{
    uint8_t limit1 = 0, limit2 = 0;
    t4OP_DATA d = get_4op_data(chan);

    if (!_4op_vol_valid_chan(chan)) {
        tINSTR_DATA *ins = get_instr_data(ch->event_table[chan].instr_def);

        limit1 = ch->peak_lock[chan] && ins ? ins->fm.volC : 0;
        limit2 = ch->peak_lock[chan] && ins ? ins->fm.volM : 0;
    }

    switch (ch->volslide_type[chan]) {
    case 0:
        if (!_4op_vol_valid_chan(chan)) {
            slide_carrier_volume_up(chan, slide, limit1);

            tINSTR_DATA *i = get_instr_data_by_ch(chan);
            if (i && (i->fm.connect || (percussion_mode && (chan >= 16))))  // in [17..20]
                slide_modulator_volume_up(chan, slide, limit2);
        } else {
            // Can use get_instr_data_by_ch()
            tINSTR_DATA *ins1 = get_instr_data(d.ins1);
            tINSTR_DATA *ins2 = get_instr_data(d.ins2);

            uint8_t limit1_volC = ch->peak_lock[d.ch1] && ins1 ? ins1->fm.volC : 0;
            uint8_t limit1_volM = ch->peak_lock[d.ch1] && ins1 ? ins1->fm.volM : 0;
            uint8_t limit2_volC = ch->peak_lock[d.ch2] && ins2 ? ins2->fm.volC : 0;
            uint8_t limit2_volM = ch->peak_lock[d.ch2] && ins2 ? ins2->fm.volM : 0;

            switch (d.conn) {
            // FM/FM
            case 0:
                slide_carrier_volume_up(d.ch1, slide, limit1_volC);
                break;
            // FM/AM
            case 1:
                slide_carrier_volume_up(d.ch1, slide, limit1_volC);
                slide_modulator_volume_up(d.ch2, slide, limit2_volM);
                break;
            // AM/FM
            case 2:
                slide_carrier_volume_up(d.ch1, slide, limit1_volC);
                slide_carrier_volume_up(d.ch2, slide, limit2_volC);
                break;
            // AM/AM
            case 3:
                slide_carrier_volume_up(d.ch1, slide, limit1_volC);
                slide_modulator_volume_up(d.ch1, slide, limit1_volM);
                slide_modulator_volume_up(d.ch2, slide, limit2_volM);
                break;
           }
        }
        break;

    case 1:
        slide_carrier_volume_up(chan, slide, limit1);
        break;

    case 2:
        slide_modulator_volume_up(chan, slide, limit2);
        break;

    case 3:
        slide_carrier_volume_up(chan, slide, limit1);
        slide_modulator_volume_up(chan, slide, limit2);
        break;
    }
}

void Ca2mv2Player::slide_carrier_volume_down(uint8_t chan, uint8_t slide)
{
    uint8_t volC = ch->fmpar_table[chan].volC;
    uint8_t newvolC = volC + slide <= 63 ? volC + slide : 63;

    set_ins_volume(BYTE_NULL, newvolC, chan);
}

void Ca2mv2Player::slide_modulator_volume_down(uint8_t chan, uint8_t slide)
{
    uint8_t volM = ch->fmpar_table[chan].volM;
    uint8_t newvolM = volM + slide <= 63 ? volM + slide : 63;

    set_ins_volume(newvolM, BYTE_NULL, chan);
}

void Ca2mv2Player::slide_volume_down(int chan, uint8_t slide)
{
    t4OP_DATA d = get_4op_data(chan);

    switch (ch->volslide_type[chan]) {
    case 0:
        if (!_4op_vol_valid_chan(chan)) {
            slide_carrier_volume_down(chan, slide);

            tINSTR_DATA *i = get_instr_data_by_ch(chan);
            if (i && (i->fm.connect || (percussion_mode && (chan >= 16)))) { //in [17..20]
                slide_modulator_volume_down(chan, slide);
            }
        } else {
            switch (d.conn) {
            // FM/FM
            case 0:
                slide_carrier_volume_down(d.ch1, slide);
                break;
            // FM/AM
            case 1:
                slide_carrier_volume_down(d.ch1, slide);
                slide_modulator_volume_down(d.ch2, slide);
                break;
            // AM/FM
            case 2:
                slide_carrier_volume_down(d.ch1, slide);
                slide_carrier_volume_down(d.ch2, slide);
                break;
            // AM/AM
            case 3:
                slide_carrier_volume_down(d.ch1, slide);
                slide_modulator_volume_down(d.ch1, slide);
                slide_modulator_volume_down(d.ch2, slide);
                break;
            }
        }
        break;

    case 1:
        slide_carrier_volume_down(chan, slide);
        break;

    case 2:
        slide_modulator_volume_down(chan, slide);
        break;

    case 3:
        slide_carrier_volume_down(chan, slide);
        slide_modulator_volume_down(chan, slide);
        break;
    }
}

void Ca2mv2Player::volume_slide(int chan, uint8_t up_speed, uint8_t down_speed)
{
    if (up_speed)
        slide_volume_up(chan, up_speed);
    else if (down_speed) {
        slide_volume_down(chan, down_speed);
    }
}

void Ca2mv2Player::global_volume_slide(uint8_t up_speed, uint8_t down_speed)
{
    if (up_speed != BYTE_NULL)
        global_volume = max(global_volume + up_speed, 63);

    if (down_speed != BYTE_NULL) {
        if (global_volume >= down_speed)
            global_volume -= down_speed;
        else
            global_volume = 0;
    }

    set_global_volume();
}

void Ca2mv2Player::arpeggio(int slot, int chan)
{
    static uint8_t arpgg_state[3] = {1, 2, 0};

    uint16_t freq;

    switch (ch->arpgg_table[slot][chan].state) {
    case 0: freq = nFreq(ch->arpgg_table[slot][chan].note - 1); break;
    case 1: freq = nFreq(ch->arpgg_table[slot][chan].note - 1 + ch->arpgg_table[slot][chan].add1); break;
    case 2: freq = nFreq(ch->arpgg_table[slot][chan].note - 1 + ch->arpgg_table[slot][chan].add2); break;
    default: freq = 0;
    }

    ch->arpgg_table[slot][chan].state = arpgg_state[ch->arpgg_table[slot][chan].state];
    change_frequency(chan, freq +
            get_instr_fine_tune(ch->event_table[chan].instr_def));
}

void Ca2mv2Player::vibrato(int slot, int chan)
{
    uint16_t freq, slide;
    uint8_t direction;

    freq = ch->freq_table[chan];

    ch->vibr_table[slot][chan].pos += ch->vibr_table[slot][chan].speed * vibtrem_speed_factor;
    slide = calc_vibrato_shift(ch->vibr_table[slot][chan].depth, ch->vibr_table[slot][chan].pos);
    direction = ch->vibr_table[slot][chan].pos & vibtrem_table_size;
    ch->vibr_table[slot][chan].dir = (direction != 0) ? 1 : 0;

    if (direction == 0)
        portamento_down(chan, slide, nFreq(0));
    else
        portamento_up(chan, slide, nFreq(12*8+1));

    ch->freq_table[chan] = freq;
}

void Ca2mv2Player::tremolo(int slot, int chan)
{
    uint16_t slide;
    uint8_t direction;

    uint8_t volM = ch->fmpar_table[chan].volM;
    uint8_t volC = ch->fmpar_table[chan].volC;

    ch->trem_table[slot][chan].pos += ch->trem_table[slot][chan].speed * vibtrem_speed_factor;
    slide = calc_vibrato_shift(ch->trem_table[slot][chan].depth, ch->trem_table[slot][chan].pos);
    direction = ch->trem_table[slot][chan].pos & vibtrem_table_size;
    ch->trem_table[slot][chan].dir = (direction != 0) ? 1 : 0;

    /* a2player.pas: tremolo uses calc_vibtrem_shift dir; tremolo2 uses (pos = 0) after Inc. */
    if (slot == 1) {
        if (ch->trem_table[slot][chan].pos == 0)
            slide_volume_down(chan, slide);
        else
            slide_volume_up(chan, slide);
    } else {
        direction = ch->trem_table[slot][chan].pos & vibtrem_table_size;
        if (direction == 0)
            slide_volume_down(chan, slide);
        else
            slide_volume_up(chan, slide);
    }

    // is this needed?
    ch->fmpar_table[chan].volM = volM;
    ch->fmpar_table[chan].volC = volC;
}

inline int Ca2mv2Player::chanvol(int chan)
{
    char instr_fm_connect = get_fm_connect_by_chan(chan);

    if (instr_fm_connect == 0)
        return 63 - ch->fmpar_table[chan].volC;
    else
        return 63 - ROUND_DIV(ch->fmpar_table[chan].volM + ch->fmpar_table[chan].volC, 2);
}

void Ca2mv2Player::update_effects_slot(int slot, int chan)
{
    uint8_t def = ch->effect_table[slot][chan].def;
    uint8_t val = ch->effect_table[slot][chan].val;

    switch (def) {
    case ef_Arpeggio:
        if (!val)
            break;

        arpeggio(slot, chan);
        break;

    case ef_ArpggVSlide:
        volume_slide(chan, val / 16, val % 16);
        arpeggio(slot, chan);
        break;

    case ef_ArpggVSlideFine:
        arpeggio(slot, chan);
        break;

    case ef_FSlideUp:
        portamento_up(chan, val, nFreq(12*8+1));
        break;

    case ef_FSlideDown:
        portamento_down(chan, val, nFreq(0));
        break;

    case ef_FSlideUpVSlide:
        portamento_up(chan, ch->fslide_table[slot][chan], nFreq(12*8+1));
        volume_slide(chan, val / 16, val % 16);
        break;

    case ef_FSlUpVSlF:
        portamento_up(chan, ch->fslide_table[slot][chan], nFreq(12*8+1));
        break;

    case ef_FSlideDownVSlide:
        portamento_down(chan, ch->fslide_table[slot][chan], nFreq(0));
        volume_slide(chan, val / 16, val % 16);
        break;

    case ef_FSlDownVSlF:
        portamento_down(chan, ch->fslide_table[slot][chan], nFreq(0));
        break;

    case ef_FSlUpFineVSlide:
        volume_slide(chan, val / 16, val % 16);
        break;

    case ef_FSlDownFineVSlide:
        volume_slide(chan, val / 16, val % 16);
        break;

    case ef_TonePortamento:
        tone_portamento(slot, chan);
        break;

    case ef_TPortamVolSlide:
        volume_slide(chan, val / 16, val % 16);
        tone_portamento(slot, chan);
        break;

    case ef_TPortamVSlideFine:
        tone_portamento(slot, chan);
        break;

    case ef_Vibrato:
        if (!ch->vibr_table[slot][chan].fine)
            vibrato(slot, chan);
        break;

    case ef_Tremolo:
        if (!ch->trem_table[slot][chan].fine)
            tremolo(slot, chan);
        break;

    case ef_VibratoVolSlide:
        volume_slide(chan, val / 16, val % 16);
        if (!ch->vibr_table[slot][chan].fine)
            vibrato(slot, chan);
        break;

    case ef_VibratoVSlideFine:
        if (!ch->vibr_table[slot][chan].fine)
            vibrato(slot, chan);
        break;

    case ef_VolSlide:
        volume_slide(chan, val / 16, val % 16);
        break;

    case ef_RetrigNote:
        if (ch->retrig_table[slot][chan] >= val) {
            ch->retrig_table[slot][chan] = 0;
            output_note(ch->event_table[chan].note, ch->event_table[chan].instr_def, chan, true, true);
        } else {
            ch->retrig_table[slot][chan]++;
        }
        break;

    case ef_MultiRetrigNote:
        if (ch->retrig_table[slot][chan] >= val / 16) {
            switch (val % 16) {
            case 0: break;
            case 8: break;

            case 1: slide_volume_down(chan, 1); break;
            case 2: slide_volume_down(chan, 2); break;
            case 3: slide_volume_down(chan, 4); break;
            case 4: slide_volume_down(chan, 8); break;
            case 5: slide_volume_down(chan, 16); break;

            case 9: slide_volume_up(chan, 1); break;
            case 10: slide_volume_up(chan, 2); break;
            case 11: slide_volume_up(chan, 4); break;
            case 12: slide_volume_up(chan, 8); break;
            case 13: slide_volume_up(chan, 16); break;

            case 6: slide_volume_down(chan, chanvol(chan) - chanvol(chan) * 2 / 3);
                break;

            case 7: slide_volume_down(chan, chanvol(chan) - chanvol(chan) * 1 / 2);
                break;

            case 14: slide_volume_up(chan, max(chanvol(chan) * 3 / 2 - chanvol(chan), 63));
                break;

            case 15: slide_volume_up(chan,max(chanvol(chan) * 2 - chanvol(chan), 63));
                break;
            }

            ch->retrig_table[slot][chan] = 0;
            output_note(ch->event_table[chan].note, ch->event_table[chan].instr_def, chan, true, true);
        } else {
            ch->retrig_table[slot][chan]++;
        }
        break;

    case ef_Tremor:
        if (ch->tremor_table[slot][chan].pos >= 0) {
            if ((ch->tremor_table[slot][chan].pos + 1) <= val / 16) {
                ch->tremor_table[slot][chan].pos++;
            } else {
                slide_volume_down(chan, 63);
                ch->tremor_table[slot][chan].pos = -1;
            }
        } else {
            if ((ch->tremor_table[slot][chan].pos - 1) >= -(val % 16)) {
                ch->tremor_table[slot][chan].pos--;
            } else {
                set_ins_volume(ch->tremor_table[slot][chan].volM, ch->tremor_table[slot][chan].volC, chan);
                ch->tremor_table[slot][chan].pos = 1;
            }
        }
        break;

    case ef_Extended2:
        switch (val / 16) {
        case ef_ex2_NoteDelay:
            if (ch->notedel_table[chan] == 0) {
                ch->notedel_table[chan] = BYTE_NULL;
                output_note(ch->event_table[chan].note, ch->event_table[chan].instr_def, chan, true, true);
            } else if (ch->notedel_table[chan] != BYTE_NULL) {
                ch->notedel_table[chan]--;
            }
            break;
        case ef_ex2_NoteCut:
            if (ch->notecut_table[chan] == 0) {
                ch->notecut_table[chan] = BYTE_NULL;
                key_off(chan);
            } else if (ch->notecut_table[chan] != BYTE_NULL) {
                ch->notecut_table[chan]--;
            }
            break;
        case ef_ex2_GlVolSlideUp: global_volume_slide(val & 0xf, BYTE_NULL); break;
        case ef_ex2_GlVolSlideDn: global_volume_slide(BYTE_NULL, val & 0xf); break;
        }
        break;
    }
}

void Ca2mv2Player::update_effects()
{
    for (int chan = 0; chan < songinfo->nm_tracks; chan++) {
        update_effects_slot(0, chan);
        update_effects_slot(1, chan);
    }
}

void Ca2mv2Player::update_fine_effects(int slot, int chan)
{
    uint8_t def = ch->effect_table[slot][chan].def;
    uint8_t val = ch->effect_table[slot][chan].val;

    switch (def) {
    case ef_ArpggVSlideFine:    volume_slide(chan, val / 16, val % 16); break;
    case ef_FSlideUpFine:       portamento_up(chan, val, nFreq(12*8+1)); break;
    case ef_FSlideDownFine:     portamento_down(chan, val, nFreq(0)); break;
    case ef_FSlUpVSlF:          volume_slide(chan, val / 16, val % 16); break;
    case ef_FSlDownVSlF:        volume_slide(chan, val / 16, val % 16); break;
    case ef_FSlUpFineVSlide:    portamento_up(chan, ch->fslide_table[slot][chan], nFreq(12*8+1)); break;
    case ef_FSlDownFineVSlide:  portamento_down(chan, ch->fslide_table[slot][chan], nFreq(0)); break;

    case ef_FSlUpFineVSlF:
        portamento_up(chan, ch->fslide_table[slot][chan], nFreq(12*8+1));
        volume_slide(chan, val / 16, val % 16);
        break;

    case ef_FSlDownFineVSlF:
        portamento_down(chan, ch->fslide_table[slot][chan], nFreq(0));
        volume_slide(chan, val / 16, val % 16);
        break;

    case ef_TPortamVSlideFine:  volume_slide(chan, val / 16, val % 16); break;
    case ef_Vibrato:            if (ch->vibr_table[slot][chan].fine) vibrato(slot, chan); break;
    case ef_Tremolo:            if (ch->trem_table[slot][chan].fine) tremolo(slot, chan); break;
    case ef_VibratoVolSlide:    if (ch->vibr_table[slot][chan].fine) vibrato(slot, chan); break;

    case ef_VibratoVSlideFine:
        volume_slide(chan, val / 16, val % 16);
        if (ch->vibr_table[slot][chan].fine)
            vibrato(slot, chan);
        break;

    case ef_VolSlideFine:       volume_slide(chan, val / 16, val % 16); break;

    case ef_Extended2:
        switch(val / 16) {
        case ef_ex2_GlVolSlideUpF: global_volume_slide(val & 0xf, BYTE_NULL); break;
        case ef_ex2_GlVolSlideDnF: global_volume_slide(BYTE_NULL, val & 0xf); break;
        }
        break;
    }
}

void Ca2mv2Player::update_extra_fine_effects_slot(int slot, int chan)
{
    uint8_t def = ch->effect_table[slot][chan].def;
    uint8_t val = ch->effect_table[slot][chan].val;

    switch (def) {
    case ef_Extended2:
        switch(val / 16) {
        case ef_ex2_GlVolSldUpXF:  global_volume_slide(val & 0xf, BYTE_NULL); break;
        case ef_ex2_GlVolSldDnXF:  global_volume_slide(BYTE_NULL, val & 0xf); break;
        case ef_ex2_VolSlideUpXF:  volume_slide(chan, val & 0xf, 0); break;
        case ef_ex2_VolSlideDnXF:  volume_slide(chan, 0, val & 0xf); break;
        case ef_ex2_FreqSlideUpXF: portamento_up(chan, val & 0xf, nFreq(12*8+1)); break;
        case ef_ex2_FreqSlideDnXF: portamento_down(chan, val & 0xf, nFreq(0)); break;
        }
        break;

    case ef_GlobalFreqSlideUpXF:    portamento_up(chan, val, nFreq(12*8+1)); break;
    case ef_GlobalFreqSlideDnXF:    portamento_down(chan, val, nFreq(0)); break;
    case ef_ExtraFineArpeggio:      arpeggio(slot, chan); break;
    case ef_ExtraFineVibrato:       if (!ch->vibr_table[slot][chan].fine) vibrato(slot, chan); break;
    case ef_ExtraFineTremolo:       if (!ch->trem_table[slot][chan].fine) tremolo(slot, chan); break;
    }
}

void Ca2mv2Player::update_extra_fine_effects()
{
    for (int chan = 0; chan < songinfo->nm_tracks; chan++) {
        update_extra_fine_effects_slot(0, chan);
        update_extra_fine_effects_slot(1, chan);
    }
}

void Ca2mv2Player::set_current_order(uint8_t new_order)
{
    if (new_order >= 0x80) {
        AdPlug_LogWrite("set_current_order parameter 0x%x is out of bounds, possibly corrupt file\n", new_order);
    }
    current_order = new_order < 0x80 ? new_order : 0;

    if (songinfo->pattern_order[current_order] < 0x80)
        return;

    // protect from circular jump to jump order command
    // if after 128 attempts of order jump we still land on jump command
    // then quit
    int i = 0;
    do {
        if (songinfo->pattern_order[current_order] > 0x7f) {
            uint8_t old_order = current_order;
            current_order = songinfo->pattern_order[current_order] - 0x80;
            if (current_order <= old_order)
                songend = true;
        }
        i++;
    } while (i < 128 && songinfo->pattern_order[current_order] > 0x7f);

    if (i >= 128) {
        AdPlug_LogWrite("set_current_order: Circular order jump detected, stopping playback\n");
        songend = true;
        a2t_stop();
    }
}

int Ca2mv2Player::calc_following_order(uint8_t order)
{
    int result;
    uint8_t index, jump_count;

    result = -1;
    index = order;
    jump_count = 0;

    do {
        if (songinfo->pattern_order[index] < 0x80) {
            result = index;
        } else {
            index = songinfo->pattern_order[index] - 0x80;
            jump_count++;
        }
    } while (!((jump_count > 0x7f) || (result != -1)));

    return result;
}

void Ca2mv2Player::update_song_position()
{
    if ((current_line < songinfo->patt_len - 1) && !pattern_break) {
        current_line++;
    } else {
        bool do_pattern_loop =  pattern_break && ((next_line & 0xf0) == pattern_loop_flag);
        bool do_position_jump = pattern_break && ((next_line & 0xf0) == pattern_break_flag);

        if (do_pattern_loop) {
            // ZCx, ZDx - loop back
            uint8_t chan = next_line - pattern_loop_flag;
            next_line = ch->loopbck_table[chan];

            if (ch->loop_table[chan][current_line] != 0)
                ch->loop_table[chan][current_line]--;
        } else {
            // A bit overkill to clean arrays here, better do it in the end of pattern loop
            memset(ch->loopbck_table, BYTE_NULL, sizeof(ch->loopbck_table));
            memset(ch->loop_table, BYTE_NULL, sizeof(ch->loop_table));

            if (do_position_jump) {
                // Bxx - order position jump
                uint8_t old_order = current_order;

                uint8_t chan = next_line - pattern_break_flag;
                int slot = ch->event_table[chan].eff[0].def == ef_PositionJump ? 0 : 1;
                uint8_t val = ch->event_table[chan].eff[slot].val;

                set_current_order(val);

                if (current_order <= old_order)
                    songend = true;
                /* Reset global volume when jumping back to the start order (order 0)
                    adt2play doesn't do it, but mmori.a2m has global volume fade at the end */
                if (val == 0 && old_order != 0) {
                    global_volume = 63;
                    set_global_volume();
                }
                pattern_break = false;
            } else {
                int new_order = current_order < 0x7f ? current_order + 1 : 0;
                uint8_t old_order = current_order;
                set_current_order(new_order);
                /* Reset global volume when jumping back to the start order (order 0)
                    adt2play doesn't do it, but mmori.a2m has global volume fade at the end */
                if (current_order == 0 && old_order != 0) {
                    global_volume = 63;
                    set_global_volume();
                }
            }
        }

        if ((songinfo->pattern_order[current_order] > 0x7f))
            return;

        current_pattern = songinfo->pattern_order[current_order];
        if (!pattern_break) {
            current_line = 0;
        } else {
            pattern_break = false;
            current_line = next_line;
        }
    }

    for (int chan = 0; chan < songinfo->nm_tracks; chan++) {
        ch->glfsld_table[0][chan].def = 0;
        ch->glfsld_table[0][chan].val = 0;
        ch->glfsld_table[1][chan].def = 0;
        ch->glfsld_table[1][chan].val = 0;
    }

    if (speed_update && current_line == 0 && current_order == calc_following_order(0)) {
        tempo = songinfo->tempo;
        speed = songinfo->speed;
        update_timer(tempo);
    }
}

void Ca2mv2Player::poll_proc()
{
    /* Pascal match: don't call a2t_stop() here. a2t_stop() is only called from
     * timer_poll_proc, but a2t_update_dump skips that path. If we a2t_stop() here,
     * subsequent IRQ ticks reprocess from reset state (order=0) and produce zeroed
     * frames that Pascal doesn't produce. The main loop exits on !songend instead. */
    if (pattern_delay) {
        update_effects();
        ticks++;
        if (tickD > 1) {
            tickD--;
        } else {
            tick0 = ticks;
            update_song_position();
            pattern_delay = false;
        }
    } else {
        if (ticks - tick0 + 1 >= speed) {
            play_line();
            update_effects();
            /* Pascal poll_proc: update_song_position only if NOT pattern_delay
             * (or fast_forward — not used in this port). Otherwise the row /
             * order advances immediately and tickD-based pattern delay never runs
             * (divergence vs adt2_dump, e.g. tunes/MLF/PINK.A2T). */
            if (!pattern_delay)
                update_song_position();
            tick0 = ticks;
        } else {
            update_effects();
            ticks++;
        }
    }

    tickXF++;
    if (tickXF % 4 == 0) {
        update_extra_fine_effects();
        tickXF -= 4;
    }
}

void Ca2mv2Player::macro_poll_proc()
{
#define  IDLE       0xfff
#define  FINISHED   0xffff
    uint16_t chan;
    uint16_t finished_flag;

    for (chan = 0; chan < songinfo->nm_tracks; chan++) {
        finished_flag = ch->keyoff_loop[chan] ? IDLE : FINISHED;

        tCH_MACRO_TABLE *mt = &ch->macro_table[chan];
        tFMREG_TABLE *rt = get_fmreg_table(mt->fmreg_ins);

        bool force_macro_keyon = false;

        /* Pascal: when an instrument has no fmreg allocated (src[0]==0 and no header
         * links) or length==0, macro_poll_proc immediately terminates with
         * fmreg_pos→finished_flag (0xffff), never processing cell data.
         * C must match so inactive/idle channels show FINISHED instead of 0. */
        if (mt->fmreg_ins != 0 && (!rt || rt->length == 0)) {
            mt->fmreg_pos = finished_flag;
        }

        if (rt && rt->length && speed != 0) {
            if (mt->fmreg_duration > 1) {
                mt->fmreg_duration--;
            } else {
                if (mt->fmreg_pos <= rt->length) {
                    if (rt->loop_begin && rt->loop_length) {
                        if (mt->fmreg_pos == rt->loop_begin + rt->loop_length - 1) {
                            mt->fmreg_pos = rt->loop_begin;
                        } else {
                            if (mt->fmreg_pos < rt->length) {
                                mt->fmreg_pos++;
                            } else {
                                mt->fmreg_pos = finished_flag;
                            }
                        }
                    } else {
                        if (mt->fmreg_pos < rt->length) {
                            mt->fmreg_pos++;
                        } else {
                            mt->fmreg_pos = finished_flag;
                        }
                    }
                } else {
                    mt->fmreg_pos = finished_flag;
                }

                if (((ch->freq_table[chan] | 0x2000) == ch->freq_table[chan]) &&
                     (rt->keyoff_pos != 0) &&
                     (mt->fmreg_pos >= rt->keyoff_pos)) {
                    mt->fmreg_pos = IDLE;
                } else {
                    if (((ch->freq_table[chan] | 0x2000) != ch->freq_table[chan]) &&
                         (mt->fmreg_pos != 0) && (rt->keyoff_pos != 0) &&
                        ((mt->fmreg_pos < rt->keyoff_pos) || (mt->fmreg_pos == IDLE)))
                        mt->fmreg_pos = rt->keyoff_pos;
                }

                if (mt->fmreg_pos && mt->fmreg_pos != IDLE && mt->fmreg_pos != finished_flag) {
                    mt->fmreg_duration = rt->data[mt->fmreg_pos - 1].duration;

                    if (mt->fmreg_duration) {
                        tREGISTER_TABLE_DEF *d = &rt->data[mt->fmreg_pos - 1];
                        // NOTE: if we are already here, no need to call get_instr()
                        uint32_t disabled = instrinfo->instruments[mt->fmreg_ins - 1].dis_fmreg_cols;

                        // force KEY-ON with missing ADSR instrument data
                        force_macro_keyon = false;
                        if (mt->fmreg_pos == 1) {
                            uint32_t adsr_disabled = disabled & ((1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) |
                                (1 << 12) | (1 << 13) | (1 << 14) | (1 << 15));
                            if (is_ins_adsr_data_empty(ch->voice_table[chan]) && !adsr_disabled) {
                                force_macro_keyon = true;
                            }
                        }

                        for (unsigned bit = 0; bit < 28; bit++) {
                            if (disabled & (1 << bit))
                                continue;

                            switch (bit) {
                            case 0:  ch->fmpar_table[chan].attckM = d->fm.attckM; break;
                            case 1:  ch->fmpar_table[chan].decM = d->fm.decM; break;
                            case 2:  ch->fmpar_table[chan].sustnM = d->fm.sustnM; break;
                            case 3:  ch->fmpar_table[chan].relM = d->fm.relM; break;
                            case 4:  ch->fmpar_table[chan].wformM = d->fm.wformM; break;
                            case 5:  set_ins_volume(63 - d->fm.volM, BYTE_NULL, chan); break;
                            case 6:  ch->fmpar_table[chan].kslM = d->fm.kslM; break;
                            case 7:  ch->fmpar_table[chan].multipM = d->fm.multipM; break;
                            case 8:  ch->fmpar_table[chan].tremM = d->fm.tremM; break;
                            case 9:  ch->fmpar_table[chan].vibrM = d->fm.vibrM; break;
                            case 10: ch->fmpar_table[chan].ksrM = d->fm.ksrM; break;
                            case 11: ch->fmpar_table[chan].sustM = d->fm.sustM; break;
                            case 12: ch->fmpar_table[chan].attckC = d->fm.attckC; break;
                            case 13: ch->fmpar_table[chan].decC = d->fm.decC; break;
                            case 14: ch->fmpar_table[chan].sustnC = d->fm.sustnC; break;
                            case 15: ch->fmpar_table[chan].relC = d->fm.relC; break;
                            case 16: ch->fmpar_table[chan].wformC = d->fm.wformC; break;
                            case 17: set_ins_volume(BYTE_NULL, 63 - d->fm.volC, chan); break;
                            case 18: ch->fmpar_table[chan].kslC = d->fm.kslC; break;
                            case 19: ch->fmpar_table[chan].multipC = d->fm.multipC; break;
                            case 20: ch->fmpar_table[chan].tremC = d->fm.tremC; break;
                            case 21: ch->fmpar_table[chan].vibrC = d->fm.vibrC; break;
                            case 22: ch->fmpar_table[chan].ksrC = d->fm.ksrC; break;
                            case 23: ch->fmpar_table[chan].sustC = d->fm.sustC; break;
                            case 24: ch->fmpar_table[chan].connect = d->fm.connect; break;
                            case 25: ch->fmpar_table[chan].feedb = d->fm.feedb; break;
                            case 27: if (!ch->pan_lock[chan]) ch->panning_table[chan] = d->panning; break;
                            }
                        }

                        update_modulator_adsrw(chan);
                        update_carrier_adsrw(chan);
                        update_fmpar(chan);

                        // TODO: check if those flags are really set by the editor
                        uint8_t macro_flags = d->macro_flags;

                        if (force_macro_keyon || (macro_flags & 0x80)) { // MACRO_NOTE_RETRIG_FLAG
                            if (!((is_4op_chan(chan) && is_4op_chan_hi(chan)))) {
                                output_note(ch->event_table[chan].note,
                                            ch->event_table[chan].instr_def, chan, false, true);
                                if (is_4op_chan(chan) && is_4op_chan_lo(chan))
                                    init_macro_table(chan - 1, 0, ch->voice_table[chan - 1], 0);
                            }
                        } else if (macro_flags & 0x40) { // MACRO_ENVELOPE_RESTART_FLAG
                            key_on(chan);
                            change_freq(chan, ch->freq_table[chan]);
                        } else if (macro_flags & 0x20) { // MACRO_ZERO_FREQ_FLAG
                            if (ch->freq_table[chan]) {
                                ch->zero_fq_table[chan] = ch->freq_table[chan];
                                ch->freq_table[chan] &= ~0x1fff;
                                change_freq(chan, ch->freq_table[chan]);
                            }
                        } else if (ch->zero_fq_table[chan]) {
                            ch->freq_table[chan] = ch->zero_fq_table[chan];
                            ch->zero_fq_table[chan] = 0;
                            change_freq(chan, ch->freq_table[chan]);
                        }

                        int freq_slide = d->freq_slide;

                        if (!(disabled & (1 << 26))) {
                            if (freq_slide > 0) {
                                portamento_up(chan, freq_slide, nFreq(12*8+1));
                            } else if (freq_slide < 0) {
                                portamento_down(chan, abs(freq_slide), nFreq(0));
                            }
                        }
                    }
                }
            }
        }

        tARPEGGIO_TABLE *at = get_arpeggio_table(mt->arpg_table);

        if (at && at->length && at->speed) {
            if (mt->arpg_count == at->speed) {
                mt->arpg_count = 1;

                if (mt->arpg_pos <= at->length) {
                    if ((at->loop_begin != 0) && (at->loop_length != 0)) {
                        if (mt->arpg_pos == at->loop_begin + (at->loop_length - 1)) {
                            mt->arpg_pos = at->loop_begin;
                        } else {
                            if (mt->arpg_pos < at->length)
                                mt->arpg_pos++;
                            else
                                mt->arpg_pos = finished_flag;
                        }
                    } else {
                        if (mt->arpg_pos < at->length)
                            mt->arpg_pos++;
                        else
                            mt->arpg_pos = finished_flag;
                    }
                } else {
                    mt->arpg_pos = finished_flag;
                }

                if (((ch->freq_table[chan] | 0x2000) == ch->freq_table[chan]) &&
                     (at->keyoff_pos != 0) &&
                     (mt->arpg_pos >= at->keyoff_pos)) {
                    mt->arpg_pos = IDLE;
                } else {
                    if (((ch->freq_table[chan] | 0x2000) != ch->freq_table[chan]) &&
                         (at->keyoff_pos != 0) && (at->keyoff_pos != 0) &&
                        ((mt->arpg_pos < at->keyoff_pos) || (mt->arpg_pos == IDLE)))
                        mt->arpg_pos = at->keyoff_pos;
                }

                if ((mt->arpg_pos != 0) &&
                    (mt->arpg_pos != IDLE) && (mt->arpg_pos != finished_flag)) {
                    int8_t fine_tune = get_instr_fine_tune(ch->event_table[chan].instr_def);
                    uint8_t d = at->data[mt->arpg_pos - 1];

                    if (d == 0) {
                        change_frequency(chan, nFreq(mt->arpg_note - 1) + fine_tune);
                    } else if (d <= 96) {
                        // 1 - 96:
                        change_frequency(chan, nFreq(max(mt->arpg_note + at->data[mt->arpg_pos], 97) - 1) +
                            fine_tune);
                    } else if (d >= 0x80 && d <= 0x80+12*8+1) {
                        // 0x80 - 0x80+12*8+1:
                        change_frequency(chan, nFreq(at->data[mt->arpg_pos - 1] - 0x80 - 1) +
                            fine_tune);
                    }
                }
            } else {
                mt->arpg_count++;
            }
        }

        tVIBRATO_TABLE *vt = get_vibrato_table(mt->vib_table);

        if (vt && vt->length && vt->speed && !mt->vib_paused) {
            if (mt->vib_count == vt->speed) {
                if (mt->vib_delay != 0) {
                    mt->vib_delay--;
                } else {
                    mt->vib_count = 1;

                    if (mt->vib_pos <= vt->length) {
                        if ((vt->loop_begin != 0) && (vt->loop_length != 0)) {
                            if (mt->vib_pos == vt->loop_begin + (vt->loop_length-1)) {
                                mt->vib_pos = vt->loop_begin;
                            } else {
                                if (mt->vib_pos < vt->length)
                                    mt->vib_pos++;
                                else
                                    mt->vib_pos = finished_flag;
                            }
                        } else {
                            if (mt->vib_pos < vt->length)
                                mt->vib_pos++;
                            else
                                mt->vib_pos = finished_flag;
                        }
                    } else {
                        mt->vib_pos = finished_flag;
                    }

                    if (((ch->freq_table[chan] | 0x2000) == ch->freq_table[chan]) &&
                         (vt->keyoff_pos != 0) &&
                         (mt->vib_pos >= vt->keyoff_pos)) {
                        mt->vib_pos = IDLE;
                    } else {
                        if (((ch->freq_table[chan] | 0x2000) != ch->freq_table[chan]) &&
                             (mt->vib_pos != 0) && (vt->keyoff_pos != 0) &&
                            ((mt->vib_pos < vt->keyoff_pos) || (mt->vib_pos == IDLE)))
                            mt->vib_pos = vt->keyoff_pos;
                    }

                    if ((mt->vib_pos != 0) &&
                        (mt->vib_pos != IDLE) && (mt->vib_pos != finished_flag)) {
                        if (vt->data[mt->vib_pos - 1] > 0)
                            macro_vibrato__porta_up(chan, vt->data[mt->vib_pos - 1]);
                        else if (vt->data[mt->vib_pos - 1] < 0)
                            macro_vibrato__porta_down(chan, abs(vt->data[mt->vib_pos - 1]));
                        else
                            change_freq(chan, mt->vib_freq);
                    }
                }
            } else {
                mt->vib_count++;
            }
        }
    }
}

void Ca2mv2Player::newtimer()
{
    if ((ticklooper == 0) && (irq_mode)) {
        poll_proc();
        if (IRQ_freq != tempo * _macro_speedup()) {
            IRQ_freq = (tempo < 18 ? 18 : tempo) * _macro_speedup();
        }
    }

    if ((macro_ticklooper == 0) && (irq_mode))
        macro_poll_proc();

    ticklooper++;
    if (ticklooper >= IRQ_freq / tempo)
        ticklooper = 0;

    macro_ticklooper++;
    if (macro_ticklooper >= IRQ_freq / (tempo * _macro_speedup()))
        macro_ticklooper = 0;
}

void Ca2mv2Player::init_irq()
{
    if (irq_initialized)
        return;

    irq_initialized = true;
    update_timer(50);
}

void Ca2mv2Player::done_irq()
{
    if (!irq_initialized)
        return;

    irq_initialized = false;
    irq_mode = true;
    update_timer(0);
    irq_mode = false;
}

void Ca2mv2Player::init_buffers()
{
    memset(ch, 0, sizeof(*ch));

    if (!lockvol) {
        memset(ch->volume_lock, 0, sizeof(ch->volume_lock));
    } else {
        for (int i = 0; i < 20; i++)
            ch->volume_lock[i] = (bool)((songinfo->lock_flags[i] >> 4) & 1);
    }

    if (!panlock) {
        memset(ch->panning_table, 0, sizeof(ch->panning_table));
        memset(ch->pan_lock, 0, sizeof(ch->pan_lock));
    } else {
        for (int i = 0; i < 20; i++) {
              ch->panning_table[i] = songinfo->lock_flags[i] & 3;
              ch->pan_lock[i] = true;
        }
    }

    if (!lockVP) {
        memset(ch->peak_lock, 0, sizeof(ch->peak_lock));
    } else {
        for (int i = 0; i < 20; i++)
              ch->peak_lock[i] = (bool)((songinfo->lock_flags[i] >> 5) & 1);
    }

    static uint8_t _4op_main_chan[6] = { 1, 3, 5, 10, 12, 14 }; // 0-based

    memset(ch->vol4op_lock, false, sizeof(ch->vol4op_lock));
    for (int i = 0; i < 6; i++) {
        ch->vol4op_lock[_4op_main_chan[i]] =
            ((songinfo->lock_flags[_4op_main_chan[i]] | 0x40) == songinfo->lock_flags[_4op_main_chan[i]]);
        ch->vol4op_lock[_4op_main_chan[i] - 1] =
            ((songinfo->lock_flags[_4op_main_chan[i] - 1] | 0x40) == songinfo->lock_flags[_4op_main_chan[i] - 1]);
    }

    for (int i = 0; i < 20; i++)
        ch->volslide_type[i] = (songinfo->lock_flags[i] >> 2) & 3;

    memset(ch->notedel_table, BYTE_NULL, sizeof(ch->notedel_table));
    memset(ch->notecut_table, BYTE_NULL, sizeof(ch->notecut_table));
    memset(ch->loopbck_table, BYTE_NULL, sizeof(ch->loopbck_table));
    memset(ch->loop_table, BYTE_NULL, sizeof(ch->loop_table));
}

void Ca2mv2Player::init_player()
{
    opl2out(0x01, 0);

    for (int i = 0; i < 18; i++)
        opl2out(0xb0 + regoffs_n(i), 0);

    for (int i = 0x80; i <= 0x8d; i++)
        opl2out(i, BYTE_NULL);

    for (int i = 0x90; i <= 0x95; i++)
        opl2out(i, BYTE_NULL);

    misc_register = (tremolo_depth << 7) +
            (vibrato_depth << 6) +
            (percussion_mode << 5);

    opl2out(0x01, 0x20);
    opl2out(0x08, 0x40);
    opl3exp(0x0105);
    opl3exp(0x04 + (songinfo->flag_4op << 8));

    init_buffers();

    key_off(16);
    key_off(17);
    opl2out(0xbd, misc_register);

    current_tremolo_depth = tremolo_depth;
    current_vibrato_depth = vibrato_depth;
    global_volume = 63;

    vibtrem_speed_factor = def_vibtrem_speed_factor;
    vibtrem_table_size = def_vibtrem_table_size;
    memcpy(&vibtrem_table, &def_vibtrem_table, sizeof(vibtrem_table));

    for (int i = 0; i < 20; i++) {
        ch->arpgg_table[0][i].state = 1;
        ch->arpgg_table[1][i].state = 1;
        ch->voice_table[i] = i + 1;
    }
}

void Ca2mv2Player::a2t_stop()
{
    irq_mode = false;
    play_status = isStopped;
    global_volume = 63;
    current_tremolo_depth = tremolo_depth;
    current_vibrato_depth = vibrato_depth;
    pattern_break = false;
    current_order = 0;
    current_pattern = 0;
    current_line = 0;
    playback_speed_shift = 0;

    for (int i = 0; i < 20; i++)
        release_sustaining_sound(i);

    opl2out(0xbd, 0);
    opl3exp(0x0004);
    opl3exp(0x0005);
    lockvol = false;
    panlock = false;
    lockVP = false;
    init_buffers();

    speed = 4;
    update_timer(50);
}

/* Clean songinfo before importing a2t tune */
void Ca2mv2Player::init_songdata()
{
    memset(songinfo, 0, sizeof(*songinfo));
    memset(songinfo->pattern_order, 0x80, sizeof(songinfo->pattern_order));

    IRQ_freq_shift = 0;
    playback_speed_shift = 0;
    songinfo->patt_len = 64;
    songinfo->nm_tracks = 18;
    songinfo->tempo = tempo;
    songinfo->speed = speed;
    songinfo->macro_speedup = 1;
    speed_update = false;
    lockvol = false;
    panlock = false;
    lockVP  = false;
    tremolo_depth = 0;
    vibrato_depth = 0;
    volume_scaling = false;
    percussion_mode = false;
}

bool Ca2mv2Player::a2t_play(char *tune, unsigned long size) // start_playing()
{
    bool err = a2_import(tune, size);

    if (!err)
        return false;

    rewind (0);

    return true;
}

/* LOADER FOR A2M/A2T */

void Ca2mv2Player::a2t_depack(char *src, int srcsize, char *dst, int dstsize)
{
    switch (ffver) {
    case 1:
    case 5: // sixpack
        Sixdepak::decode((unsigned short *)src, srcsize, (unsigned char *)dst, dstsize);
        break;
    case 2:
    case 6: // lzw
        LZW_decompress(src, dst, srcsize, dstsize);
        break;
    case 3:
    case 7: // lzss
        LZSS_decompress(src, dst, srcsize, dstsize);
        break;
    case 4:
    case 8: // unpacked
        if (dstsize <= srcsize)
            memcpy(dst, src, srcsize);
        break;
    case 9:
    case 10:
    case 11: // apack (aPlib)
        aP_depack(src, dst, srcsize, dstsize);
        break;
    case 12:
    case 13:
    case 14: // lzh
        LZH_decompress(src, dst, srcsize, dstsize);
        break;
    }
}

// read the variable part of the header
int Ca2mv2Player::a2t_read_varheader(char *blockptr, unsigned long size)
{
    uint8_t *varheader = (uint8_t *)blockptr;

    switch (ffver) {
    case 1:
    case 2:
    case 3:
    case 4:
        if (A2T_VARHEADER_V1234_SIZE > size)
            return INT_MAX;
        for (int i = 0; i < 6; i++)
            len[i] = A2T_VARHEADER_V1234_LEN(varheader, i);
        return A2T_VARHEADER_V1234_SIZE;
    case 5:
    case 6:
    case 7:
    case 8:
        if (A2T_VARHEADER_V5678_SIZE > size)
            return INT_MAX;
        songinfo->common_flag = A2T_VARHEADER_V5678_COMMON_FLAG(varheader);
        for (int i = 0; i < 10; i++)
            len[i] = A2T_VARHEADER_V5678_LEN(varheader, i);
        return A2T_VARHEADER_V5678_SIZE;
    case 9:
        if (A2T_VARHEADER_V9_SIZE > size)
            return INT_MAX;
        songinfo->common_flag = A2T_VARHEADER_V9_COMMON_FLAG(varheader);
        songinfo->patt_len = A2T_VARHEADER_V9_PATT_LEN(varheader);
        songinfo->nm_tracks = A2T_VARHEADER_V9_NM_TRACKS(varheader);
        songinfo->macro_speedup = A2T_VARHEADER_V9_MACRO_SPEEDUP(varheader);
        for (int i = 0; i < 20; i++)
            len[i] = A2T_VARHEADER_V9_LEN(varheader, i);
        return A2T_VARHEADER_V9_SIZE;
    case 10:
        if (A2T_VARHEADER_V10_SIZE > size)
            return INT_MAX;
        songinfo->common_flag = A2T_VARHEADER_V10_COMMON_FLAG(varheader);
        songinfo->patt_len = A2T_VARHEADER_V10_PATT_LEN(varheader);
        songinfo->nm_tracks = A2T_VARHEADER_V10_NM_TRACKS(varheader);
        songinfo->macro_speedup = A2T_VARHEADER_V10_MACRO_SPEEDUP(varheader);
        songinfo->flag_4op = A2T_VARHEADER_V10_FLAG_4OP(varheader);
        for (int i = 0; i < 20; i++)
            songinfo->lock_flags[i] = A2T_VARHEADER_V10_LOCK_FLAGS(varheader, i);
        for (int i = 0; i < 20; i++)
            len[i] = A2T_VARHEADER_V10_LEN(varheader, i);
        return A2T_VARHEADER_V10_SIZE;
    case 11:
    case 12:
    case 13:
    case 14:
        if (A2T_VARHEADER_V11_SIZE > size)
            return INT_MAX;
        songinfo->common_flag = A2T_VARHEADER_V11_COMMON_FLAG(varheader);
        songinfo->patt_len = A2T_VARHEADER_V11_PATT_LEN(varheader);
        songinfo->nm_tracks = A2T_VARHEADER_V11_NM_TRACKS(varheader);
        songinfo->macro_speedup = A2T_VARHEADER_V11_MACRO_SPEEDUP(varheader);
        songinfo->flag_4op = A2T_VARHEADER_V11_FLAG_4OP(varheader);
        for (int i = 0; i < 20; i++)
            songinfo->lock_flags[i] = A2T_VARHEADER_V11_LOCK_FLAGS(varheader, i);
        for (int i = 0; i < 21; i++)
            len[i] = A2T_VARHEADER_V11_LEN(varheader, i);
        return A2T_VARHEADER_V11_SIZE;
    }

    return INT_MAX;
}

void Ca2mv2Player::instrument_import(int ins, uint8_t *srci)
{
    tINSTR_DATA *dsti = get_instr_data(ins);
    assert(dsti);

    fmdata_fill_from_raw(&dsti->fm, srci);

    dsti->panning    = srci[11] & 3;
    dsti->fine_tune  = srci[12];

    // Is 'perc_voice' even used anywhere except editor?
    dsti->perc_voice = (ffver >= 9 ? srci[13] : 0);

    if (dsti->panning >= 3) {
        AdPlug_LogWrite("instrument %d, panning out of range\n", ins);
        dsti->panning = 0;
    }
}

int Ca2mv2Player::a2t_read_instruments(char *packed, unsigned long size)
{
    if (len[0] > size) return INT_MAX;

    int instnum = (ffver < 9 ? 250 : 255);
    int instsize = (ffver < 9 ? tINSTR_DATA_V1_8_SIZE : tINSTR_DATA_V9_14_SIZE);
    int unpackedsize = (instnum * instsize) +
                  (ffver > 11 ? tBPM_DATA_SIZE + tINS_4OP_FLAGS_SIZE + tRESERVED_SIZE : 0);
    uint8_t *unpacked = (uint8_t *)calloc(1, unpackedsize);
    uint8_t *p = unpacked;

    a2t_depack(packed, len[0], (char *)unpacked, unpackedsize);

    if (ffver == 14) {
        //memcpy(&songinfo->bpm_data, dst, sizeof(songinfo->bpm_data));
        p += tBPM_DATA_SIZE;
    }

    if (ffver >= 12 && ffver <= 14) {
        //memcpy(&songinfo->ins_4op_flags, p, sizeof(songinfo->ins_4op_flags));
        p += tINS_4OP_FLAGS_SIZE;
        //memcpy(&songinfo->reserved_data, p, sizeof(songinfo->reserved_data));
        p += tRESERVED_SIZE;
    }

    // Calculate the real number of used instruments
    int count = instnum;
    while (count && is_data_empty((char *)p + (count - 1) * instsize, instsize))
        count--;

    instruments_allocate(count);

    for (int i = 0; i < count; i++, p += instsize) {
        instrument_import(i + 1, p);
    }

    free(unpacked);

    return len[0];
}

int Ca2mv2Player::a2t_read_fmregtable(char *packed, unsigned long size)
{
    if (ffver < 9) return 0;

    if (len[1] > size) return INT_MAX;

    uint8_t *unpacked = (uint8_t *)calloc(255, tFMREG_TABLE_V9_14_SIZE);
    a2t_depack(packed, len[1], (char *)unpacked, 255 * tFMREG_TABLE_V9_14_SIZE);

    int count = instrinfo->count;

    // Allocate fmreg macro tables
    fmreg_table_allocate(count, unpacked);

    for (int i = 0; i < count; i++) {
        // Instrument arpegio/vibrato references
        tINSTR_DATA_EXT *dst = get_instr(i + 1);
        assert(dst);

        if (!dst->fmreg) continue;
        dst->arpeggio = dst->fmreg->arpeggio_table;
        dst->vibrato = dst->fmreg->vibrato_table;
    }

    free(unpacked);

    return len[1];
}

int Ca2mv2Player::a2t_read_arpvibtable(char *packed, unsigned long size)
{
    if (ffver < 9) return 0;

    if (len[2] > size) return INT_MAX;

    uint8_t *unpacked = (uint8_t *)calloc(255, tARPVIB_TABLE_V9_14_SIZE);
    a2t_depack(packed, len[2], (char *)unpacked, 255 * tARPVIB_TABLE_V9_14_SIZE);

    // TODO: Calculate actual num of arp/vib tables
    arpvib_tables_allocate(255, unpacked);

    free(unpacked);

    return len[2];
}

int Ca2mv2Player::a2t_read_disabled_fmregs(char *packed, unsigned long size)
{
    if (ffver < 11) return 0;

    if (len[3] > size) return INT_MAX;

    bool (*dis_fmregs)[255][28] = (bool (*)[255][28])calloc(255, 28);

    a2t_depack(packed, len[3], (char *)*dis_fmregs, 255 * 28);

    disabled_fmregs_import(instrinfo->count, *dis_fmregs);

    free(dis_fmregs);

    return len[3];
}

int Ca2mv2Player::a2t_read_order(char *packed, unsigned long size)
{
    int blocknum[14] = {1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 4, 4, 4, 4};
    int i = blocknum[ffver - 1];

    if (len[i] > size) return INT_MAX;

    a2t_depack(packed, len[i], (char *)songinfo->pattern_order, 128);

    return len[i];
}

void Ca2mv2Player::convert_v1234_effects(tADTRACK2_EVENT *ev, int chan)
{
    // Old v1234 effects
    enum {
        fx_Arpeggio          = 0x00,
        fx_FSlideUp          = 0x01,
        fx_FSlideDown        = 0x02,
        fx_FSlideUpFine      = 0x03,
        fx_FSlideDownFine    = 0x04,
        fx_TonePortamento    = 0x05,
        fx_TPortamVolSlide   = 0x06,
        fx_Vibrato           = 0x07,
        fx_VibratoVolSlide   = 0x08,
        fx_SetOpIntensity    = 0x09,
        fx_SetInsVolume      = 0x0a,
        fx_PatternBreak      = 0x0b,
        fx_PatternJump       = 0x0c,
        fx_SetTempo          = 0x0d,
        fx_SetTimer          = 0x0e,
        fx_Extended          = 0x0f,
        fx_ex_DefAMdepth     = 0x00,
        fx_ex_DefVibDepth    = 0x01,
        fx_ex_DefWaveform    = 0x02,
        fx_ex_ManSlideUp     = 0x03,
        fx_ex_ManSlideDown   = 0x04,
        fx_ex_VSlideUp       = 0x05,
        fx_ex_VSlideDown     = 0x06,
        fx_ex_VSlideUpFine   = 0x07,
        fx_ex_VSlideDownFine = 0x08,
        fx_ex_RetrigNote     = 0x09,
        fx_ex_SetAttckRate   = 0x0a,
        fx_ex_SetDecayRate   = 0x0b,
        fx_ex_SetSustnLevel  = 0x0c,
        fx_ex_SetReleaseRate = 0x0d,
        fx_ex_SetFeedback    = 0x0e,
        fx_ex_ExtendedCmd    = 0x0f
    };

    switch (ev->eff[0].def) {
    case fx_Arpeggio:           ev->eff[0].def = 0/*ef_Arpeggio is 0x80 now*/;        break;
    case fx_FSlideUp:           ev->eff[0].def = ef_FSlideUp;        break;
    case fx_FSlideDown:         ev->eff[0].def = ef_FSlideDown;      break;
    case fx_FSlideUpFine:       ev->eff[0].def = ef_FSlideUpFine;    break;
    case fx_FSlideDownFine:     ev->eff[0].def = ef_FSlideDownFine;  break;
    case fx_TonePortamento:     ev->eff[0].def = ef_TonePortamento;  break;
    case fx_TPortamVolSlide:    ev->eff[0].def = ef_TPortamVolSlide; break;
    case fx_Vibrato:            ev->eff[0].def = ef_Vibrato;         break;
    case fx_VibratoVolSlide:    ev->eff[0].def = ef_VibratoVolSlide; break;
    case fx_SetInsVolume:       ev->eff[0].def = ef_SetInsVolume;    break;
    case fx_PatternJump:        ev->eff[0].def = ef_PositionJump;    break;
    case fx_PatternBreak:       ev->eff[0].def = ef_PatternBreak;    break;
    case fx_SetTempo:           ev->eff[0].def = ef_SetSpeed;        break;
    case fx_SetTimer:           ev->eff[0].def = ef_SetTempo;        break;
    case fx_SetOpIntensity: {
        if (ev->eff[0].val & 0xf0) {
            ev->eff[0].def = ef_SetCarrierVol;
            ev->eff[0].val = (ev->eff[0].val >> 4) * 4 + 3;
        } else if (ev->eff[0].val & 0x0f) {
            ev->eff[0].def = ef_SetModulatorVol;
            ev->eff[0].val = (ev->eff[0].val & 0x0f) * 4 + 3;
        } else ev->eff[0].def = 0;
        break;
    }
    case fx_Extended: {
        switch (ev->eff[0].val >> 4) {
        case fx_ex_DefAMdepth:
            ev->eff[0].def = ef_Extended;
            ev->eff[0].val = ef_ex_SetTremDepth << 4 | (ev->eff[0].val & 0x0f);
            break;
        case fx_ex_DefVibDepth:
            ev->eff[0].def = ef_Extended;
            ev->eff[0].val = ef_ex_SetVibDepth << 4 | (ev->eff[0].val & 0x0f);
            break;
        case fx_ex_DefWaveform:
            ev->eff[0].def = ef_SetWaveform;
            if ((ev->eff[0].val & 0x0f) < 4) {
                ev->eff[0].val = ((ev->eff[0].val & 0x0f) << 4) | 0x0f; // 0..3
            } else {
                ev->eff[0].val = ((ev->eff[0].val & 0x0f) - 4) | 0xf0; // 4..7
            }
            break;
        case fx_ex_VSlideUp:
            ev->eff[0].def = ef_VolSlide;
            ev->eff[0].val = (ev->eff[0].val & 0x0f) << 4;
            break;
        case fx_ex_VSlideDown:
            ev->eff[0].def = ef_VolSlide;
            ev->eff[0].val = ev->eff[0].val & 0x0f;
            break;
        case fx_ex_VSlideUpFine:
            ev->eff[0].def = ef_VolSlideFine;
            ev->eff[0].val = (ev->eff[0].val & 0x0f) << 4;
            break;
        case fx_ex_VSlideDownFine:
            ev->eff[0].def = ef_VolSlideFine;
            ev->eff[0].val = ev->eff[0].val & 0x0f;
            break;
        case fx_ex_ManSlideUp:
            ev->eff[0].def = ef_Extended2;
            ev->eff[0].val = (ef_ex2_FineTuneUp << 4) | (ev->eff[0].val & 0x0f);
            break;
        case fx_ex_ManSlideDown:
            ev->eff[0].def = ef_Extended2;
            ev->eff[0].val = (ef_ex2_FineTuneDown << 4) | (ev->eff[0].val & 0x0f);
            break;
        case fx_ex_RetrigNote:
            ev->eff[0].def = ef_RetrigNote;
            ev->eff[0].val = (ev->eff[0].val & 0x0f) + 1;
            break;
        case fx_ex_SetAttckRate:
            ev->eff[0].def = ef_Extended;
            ev->eff[0].val = ev->eff[0].val & 0x0f;
            if (!adsr_carrier[chan]) {
                ev->eff[0].val |= ef_ex_SetAttckRateM << 4;
            } else {
                ev->eff[0].val |= ef_ex_SetAttckRateC << 4;
            }
            break;
        case fx_ex_SetDecayRate:
            ev->eff[0].def = ef_Extended;
            ev->eff[0].val = ev->eff[0].val & 0x0f;
            if (!adsr_carrier[chan]) {
                ev->eff[0].val |= ef_ex_SetDecayRateM << 4;
            } else {
                ev->eff[0].val |= ef_ex_SetDecayRateC << 4;
            }
            break;
        case fx_ex_SetSustnLevel:
            ev->eff[0].def = ef_Extended;
            ev->eff[0].val = ev->eff[0].val & 0x0f;
            if (!adsr_carrier[chan]) {
                ev->eff[0].val |= ef_ex_SetSustnLevelM << 4;
            } else {
                ev->eff[0].val |= ef_ex_SetSustnLevelC << 4;
            }
            break;
        case fx_ex_SetReleaseRate:
            ev->eff[0].def = ef_Extended;
            ev->eff[0].val = ev->eff[0].val & 0x0f;
            if (!adsr_carrier[chan]) {
                ev->eff[0].val |= ef_ex_SetRelRateM << 4;
            } else {
                ev->eff[0].val |= ef_ex_SetRelRateC << 4;
            }
            break;
        case fx_ex_SetFeedback:
            ev->eff[0].def = ef_Extended;
            ev->eff[0].val = (ef_ex_SetFeedback << 4) | (ev->eff[0].val & 0x0f);
            break;
        case fx_ex_ExtendedCmd:
            ev->eff[0].def = ef_Extended;
            ev->eff[0].val = ef_ex_ExtendedCmd2 << 4;
            if ((ev->eff[0].val & 0x0f) < 10) {
                // FIXME: Should be a parameter
                const bool whole_song = false;

                switch (ev->eff[0].val & 0x0f) {
                case 0: ev->eff[0].val |= ef_ex_cmd2_RSS;       break;
                case 1: ev->eff[0].val |= ef_ex_cmd2_LockVol;   break;
                case 2: ev->eff[0].val |= ef_ex_cmd2_UnlockVol; break;
                case 3: ev->eff[0].val |= ef_ex_cmd2_LockVP;    break;
                case 4: ev->eff[0].val |= ef_ex_cmd2_UnlockVP;  break;
                case 5:
                    ev->eff[0].def = (whole_song ? 255 : 0);
                    ev->eff[0].val = 0;
                    adsr_carrier[chan] = true;
                    break;
                case 6:
                    ev->eff[0].def = (whole_song ? 255 : 0);
                    ev->eff[0].val = (whole_song ? 1 : 0);
                    adsr_carrier[chan] = false;
                    break;
                case 7: ev->eff[0].val |= ef_ex_cmd2_VSlide_car; break;
                case 8: ev->eff[0].val |= ef_ex_cmd2_VSlide_mod; break;
                case 9: ev->eff[0].val |= ef_ex_cmd2_VSlide_def; break;
                }
            } else {
                ev->eff[0].def = 0;
                ev->eff[0].val = 0;
            }
            break;
        }
        break;
    }
    default:
        ev->eff[0].def = 0;
        ev->eff[0].val = 0;
    }
}

// common for both a2t/a2m
int Ca2mv2Player::a2_read_patterns(char *src, int s, unsigned long size)
{
    int retval = 0;

    switch (ffver) {
    case 1:
    case 2:
    case 3:
    case 4: // [4][16][64][9][4]
        {
        uint8_t *old = (uint8_t *)calloc(16, tPATTERN_DATA_V1234_SIZE);

        memset(adsr_carrier, false, sizeof(adsr_carrier));

        for (int i = 0; i < 4; i++) {
            if (!len[i+s]) continue;

            if (len[i+s] > size) {
                free(old);
                return INT_MAX;
            }

            a2t_depack(src, len[i+s], (char *)old, 16 * tPATTERN_DATA_V1234_SIZE);

            for (int p = 0; p < 16; p++) { // pattern
                if (i * 8 + p >= eventsinfo->patterns)
                        break;
                for (int r = 0; r < 64; r++) // row
                for (int c = 0; c < 9; c++) { // channel
                    tADTRACK2_EVENT *dst = get_event_p(i * 16 + p, c, r);
                    uint8_t *src = old + (
                        p * tPATTERN_DATA_V1234_SIZE +
                        r * 9 * tADTRACK2_EVENT_V1_8_SIZE +
                        c * tADTRACK2_EVENT_V1_8_SIZE
                    );

                    dst->note       = src[0];
                    dst->instr_def  = src[1];
                    dst->eff[0].def = src[2];
                    dst->eff[0].val = src[3];

                    convert_v1234_effects(dst, c);
                }
            }

            src += len[i+s];
            size -= len[i+s];
            retval += len[i+s];
        }

        free(old);
        break;
        }
    case 5:
    case 6:
    case 7:
    case 8: // [8][8][18][64][4]
        {
        uint8_t *old = (uint8_t *)calloc(8, tPATTERN_DATA_V5678_SIZE);

        for (int i = 0; i < 8; i++) {
            if (!len[i+s]) continue;

            if (len[i+s] > size) {
                free(old);
                return INT_MAX;
            }

            a2t_depack(src, len[i+s], (char *)old, 8 * tPATTERN_DATA_V5678_SIZE);

            for (int p = 0; p < 8; p++) { // pattern
                if (i * 8 + p >= eventsinfo->patterns)
                    break;
                for (int c = 0; c < 18; c++) // channel
                for (int r = 0; r < 64; r++) { // row
                    tADTRACK2_EVENT *dst = get_event_p(i * 8 + p, c, r);
                    uint8_t *src = old + (
                        p * tPATTERN_DATA_V5678_SIZE +
                        c * 64 * tADTRACK2_EVENT_V1_8_SIZE +
                        r * tADTRACK2_EVENT_V1_8_SIZE
                    );

                    dst->note       = src[0];
                    dst->instr_def  = src[1];

                    /* Pascal (iloaders.inc import_old_a2m_event2): convert
                     * ef_ManualFSlide (22) to ef_Extended2 FineTuneUp/Down. */
                    if (src[2] == 22) {
                        if (src[3] / 16 != 0) {
                            dst->eff[0].def = ef_Extended2;
                            dst->eff[0].val = (ef_ex2_FineTuneUp << 4) | (src[3] / 16);
                        } else {
                            dst->eff[0].def = ef_Extended2;
                            dst->eff[0].val = (ef_ex2_FineTuneDown << 4) | (src[3] % 16);
                        }
                    } else {
                        dst->eff[0].def = src[2];
                        dst->eff[0].val = src[3];
                    }
                }
            }

            src += len[i+s];
            size -= len[i+s];
            retval += len[i+s];
        }

        free(old);
        break;
        }
    case 9:
    case 10:
    case 11:
    case 12:
    case 13:
    case 14: // [16][8][20][256][6]
        {
        uint8_t *old = (uint8_t *)calloc(8, tPATTERN_DATA_V9_14_SIZE);

        // 16 groups of 8 patterns
        for (int i = 0; i < 16; i++) {
            if (!len[i+s]) continue;
            if (len[i+s] > size) {
                free(old);
                return INT_MAX;
            }

            a2t_depack(src, len[i+s], (char *)old, 8 * tPATTERN_DATA_V9_14_SIZE);
            src += len[i+s];
            size -= len[i+s];
            retval += len[i+s];

            for (int p = 0; p < 8; p++) { // pattern
                if (i * 8 + p >= eventsinfo->patterns)
                        break;

                for (int c = 0; c < eventsinfo->channels; c++) // channel
                for (int r = 0; r < eventsinfo->rows; r++) { // row
                    tADTRACK2_EVENT *dst = get_event_p(i * 8 + p, c, r);
                    uint8_t *src = old + (
                            p * tPATTERN_DATA_V9_14_SIZE +
                            c * 256 * tADTRACK2_EVENT_V9_14_SIZE +
                            r * tADTRACK2_EVENT_V9_14_SIZE);

                    dst->note       = src[0];
                    dst->instr_def  = src[1];
                    dst->eff[0].def = src[2];
                    dst->eff[0].val = src[3];
                    dst->eff[1].def = src[4];
                    dst->eff[1].val = src[5];
                }
            }
        }

        free(old);
        break;
        }
    }

    return retval;
}

int Ca2mv2Player::a2t_read_patterns(char *src, unsigned long size)
{
    int blockstart[14] = {2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 5, 5, 5, 5};
    int s = blockstart[ffver - 1];

    return a2_read_patterns(src, s, size);
}

bool Ca2mv2Player::a2t_import(char *tune, unsigned long size)
{
    uint8_t *header = (uint8_t *)tune;
    char *blockptr = tune + A2T_HEADER_SIZE;
    int result;

    if (A2T_HEADER_SIZE > size)
        return false;

    if (strncmp((char *)header, "_A2tiny_module_", 15))
        return false;

    init_songdata();

    memset(len, 0, sizeof(len));

    ffver = A2T_HEADER_FFVER(header);
    type = 1;

    if (!ffver || ffver > 14)
        return false;

    songinfo->tempo = A2T_HEADER_TEMPO(header);
    songinfo->speed = A2T_HEADER_SPEED(header);

    // Read variable part after header, fill len[] with values
    result = a2t_read_varheader(blockptr, size - (blockptr - tune));
    if (result == INT_MAX) return false;
    blockptr += result;

    speed_update    = (songinfo->common_flag >> 0) & 1;
    lockvol         = (songinfo->common_flag >> 1) & 1;
    lockVP          = (songinfo->common_flag >> 2) & 1;
    tremolo_depth   = (songinfo->common_flag >> 3) & 1;
    vibrato_depth   = (songinfo->common_flag >> 4) & 1;
    panlock         = (songinfo->common_flag >> 5) & 1;
    percussion_mode = (songinfo->common_flag >> 6) & 1;
    volume_scaling  = (songinfo->common_flag >> 7) & 1;

    // Read instruments; all versions
    result = a2t_read_instruments(blockptr, size - (blockptr - tune));
    if (result == INT_MAX) return false;
    blockptr += result;

    // Read instrument macro (v >= 9,10,11)
    result = a2t_read_fmregtable(blockptr, size - (blockptr - tune));
    if (result == INT_MAX) return false;
    blockptr += result;

    // Read arpeggio/vibrato macro table (v >= 9,10,11)
    result = a2t_read_arpvibtable(blockptr, size - (blockptr - tune));
    if (result == INT_MAX) return false;
    blockptr += result;

    // Read disabled fm regs (v == 11)
    result = a2t_read_disabled_fmregs(blockptr, size - (blockptr - tune));
    if (result == INT_MAX) return false;
    blockptr += result;

    // Read pattern_order
    result = a2t_read_order(blockptr, size - (blockptr - tune));
    if (result == INT_MAX) return false;
    blockptr += result;

    // Allocate patterns
    patterns_allocate(A2T_HEADER_NPATT(header), songinfo->nm_tracks, songinfo->patt_len);

    // Read patterns
    result = a2t_read_patterns(blockptr, size - (blockptr - tune));
    if (result == INT_MAX) return false;

    return true;
}

int Ca2mv2Player::a2m_read_varheader(char *blockptr, int npatt, unsigned long size)
{
    unsigned int lensize;
    unsigned int maxblock = (ffver < 5 ? npatt / 16 : npatt / 8) + 1;

    uint8_t *src16 = (uint8_t *)blockptr;
    uint8_t *src32 = (uint8_t *)blockptr;

    if (ffver < 5) lensize = 5;         // 1,2,3,4 - uint16_t len[5];
    else if (ffver < 9) lensize = 9;    // 5,6,7,8 - uint16_t len[9];
    else lensize = 17;                  // 9,10,11 - uint32_t len[17];

    if (ffver >= 1 && ffver <= 8) { // 1 - 8
        if (lensize * 2 > size) return INT_MAX;

        // skip possible rubbish (MARIO.A2M)
        for (unsigned int i = 0; (i < lensize) && (i <= maxblock); i++)
            len[i] = UINT16LE(src16 + i * 2);

        return lensize * 2;
    } else if (ffver >= 9 && ffver <= 14) { // 9 - 14
        if (lensize * 4 > size) return INT_MAX;

        for (unsigned int i = 0; i < lensize; i++)
            len[i] = UINT32LE(src32 + i * 4);

        return lensize * 4;
    }

    return INT_MAX;
}

int Ca2mv2Player::a2m_read_songdata(char *packed, unsigned long size)
{
    if (ffver < 9) { // 1 - 8
        if (len[0] > size) return INT_MAX;

        uint8_t *unpacked = (uint8_t *)calloc(1, A2M_SONGDATA_V1_8_SIZE);
        a2t_depack(packed, len[0], (char *)unpacked, A2M_SONGDATA_V1_8_SIZE);

        memcpy(songinfo->songname, A2M_SONGDATA_V1_8_SONGNAME_P(unpacked) + 1, 42);
        memcpy(songinfo->composer, A2M_SONGDATA_V1_8_COMPOSER_P(unpacked) + 1, 42);

        // Calculate the real number of used instruments
        int count = 250;
        while (count && is_data_empty((char *)A2M_SONGDATA_V1_8_INSTR_DATA_P(unpacked, count - 1), tINSTR_DATA_V1_8_SIZE))
            count--;

        instruments_allocate(count);

        for (int i = 0; i < count; i++) {
            instrument_import(i + 1, A2M_SONGDATA_V1_8_INSTR_DATA_P(unpacked, i));
        }

        for (int i = 0; i < 250; i++)
            memcpy(songinfo->instr_names[i], A2M_SONGDATA_V1_8_INSTR_NAMES_P(unpacked, i) + 1, 32);

        memcpy(songinfo->pattern_order, A2M_SONGDATA_V1_8_PATTERN_ORDER_P(unpacked, 0), 128);

        songinfo->tempo = A2M_SONGDATA_V1_8_TEMPO(unpacked);
        songinfo->speed = A2M_SONGDATA_V1_8_SPEED(unpacked);

        if (ffver > 4) { // 5 - 8
            songinfo->common_flag = A2M_SONGDATA_V1_8_COMMON_FLAG(unpacked);
        }

        free(unpacked);
    } else { // 9 - 14
        if (len[0] > size) return INT_MAX;

        uint8_t *unpacked = (uint8_t *)calloc(1, A2M_SONGDATA_V9_14_SIZE);

        a2t_depack(packed, len[0], (char *)unpacked, A2M_SONGDATA_V9_14_SIZE);

        memcpy(songinfo->songname, A2M_SONGDATA_V9_14_SONGNAME_P(unpacked) + 1, 42);
        memcpy(songinfo->composer, A2M_SONGDATA_V9_14_COMPOSER_P(unpacked) + 1, 42);

        // Calculate the real number of used instruments
        int count = 255;
        while (count && is_data_empty((char *)A2M_SONGDATA_V9_14_INSTR_DATA_P(unpacked, count - 1), tINSTR_DATA_V9_14_SIZE))
            count--;

        instruments_allocate(count);

        for (int i = 0; i < count; i++) {
            instrument_import(i + 1, A2M_SONGDATA_V9_14_INSTR_DATA_P(unpacked, i));
        }

        for (int i = 0; i < 255; i++)
            memcpy(songinfo->instr_names[i], A2M_SONGDATA_V9_14_INSTR_NAMES_P(unpacked, i) + 1, 42);

        // Allocate fmreg macro tables
        fmreg_table_allocate(count, A2M_SONGDATA_V9_14_FMREG_TABLE_P(unpacked, 0));

        for (int i = 0; i < count; i++) {
            // Instrument arpegio/vibrato references
            tINSTR_DATA_EXT *dst = get_instr(i + 1);
            assert(dst);

            if (!dst->fmreg) continue;
            dst->arpeggio = dst->fmreg->arpeggio_table;
            dst->vibrato = dst->fmreg->vibrato_table;
        }

        // Allocate arpeggio/vibrato macro tables
        // TODO: Calculate actual num of arp/vib tables
        arpvib_tables_allocate(255, A2M_SONGDATA_V9_14_ARPVIB_TABLE_P(unpacked, 0));

        memcpy(songinfo->pattern_order, A2M_SONGDATA_V9_14_PATTERN_ORDER_P(unpacked, 0), 128);

        songinfo->tempo = A2M_SONGDATA_V9_14_TEMPO(unpacked);
        songinfo->speed = A2M_SONGDATA_V9_14_SPEED(unpacked);
        songinfo->common_flag = A2M_SONGDATA_V9_14_COMMON_FLAG(unpacked);
        songinfo->patt_len = A2M_SONGDATA_V9_14_PATT_LEN(unpacked);
        songinfo->nm_tracks = A2M_SONGDATA_V9_14_NM_TRACKS(unpacked);
        songinfo->macro_speedup = A2M_SONGDATA_V9_14_MACRO_SPEEDUP(unpacked);

        // v10
        songinfo->flag_4op = A2M_SONGDATA_V9_14_FLAG_4OP(unpacked);
        memcpy(songinfo->lock_flags, A2M_SONGDATA_V9_14_LOCK_FLAGS_P(unpacked, 0), 20);

        // v11
        disabled_fmregs_import(count, (bool (*)[28])A2M_SONGDATA_V9_14_DIS_FMREG_COL_P(unpacked, 0));

        // v12-13
        // NOTE: not used anywhere
        //songinfo->ins_4op_flags.num_4op = data->ins_4op_flags.num_4op;
        //memcpy(songinfo->ins_4op_flags.idx_4op, data->ins_4op_flags.idx_4op, 128);
        //memcpy(songinfo->reserved_data, data->reserved_data, 1024);

        // v14
        // TODO: Implement these in player
        songinfo->bpm_rows_per_beat = A2M_SONGDATA_V9_14_BPM_ROWS_PER_BEAT(unpacked);
        songinfo->bpm_tempo_finetune = A2M_SONGDATA_V9_14_BPM_TEMPO_FINETUNE(unpacked);

        free(unpacked);
    }

    speed_update    = (songinfo->common_flag >> 0) & 1;
    lockvol         = (songinfo->common_flag >> 1) & 1;
    lockVP          = (songinfo->common_flag >> 2) & 1;
    tremolo_depth   = (songinfo->common_flag >> 3) & 1;
    vibrato_depth   = (songinfo->common_flag >> 4) & 1;
    panlock         = (songinfo->common_flag >> 5) & 1;
    percussion_mode = (songinfo->common_flag >> 6) & 1;
    volume_scaling  = (songinfo->common_flag >> 7) & 1;

    return len[0];
}

int Ca2mv2Player::a2m_read_patterns(char *src, unsigned long size)
{
    return a2_read_patterns(src, 1, size);
}

bool Ca2mv2Player::a2m_import(char *tune, unsigned long size)
{
    uint8_t *header = (uint8_t *)tune;
    char *blockptr = tune + A2M_HEADER_SIZE;
    int result;

    if (A2M_HEADER_SIZE > size)
        return false;

    if (strncmp((const char *)header, "_A2module_", 10))
        return false;

    init_songdata();

    memset(len, 0, sizeof(len));

    ffver = A2M_HEADER_FFVER(header);

    if (!ffver || ffver > 14)
        return false;

    // Read variable part after header, fill len[] with values
    result = a2m_read_varheader(blockptr, A2M_HEADER_NPATT(header), size - (blockptr - tune));
    if (result == INT_MAX) return false;
    blockptr += result;

    // Read songdata
    result = a2m_read_songdata(blockptr, size - (blockptr - tune));
    if (result == INT_MAX) return false;
    blockptr += result;

    // Allocate patterns
    patterns_allocate(A2M_HEADER_NPATT(header), songinfo->nm_tracks, songinfo->patt_len);

    // Read patterns
    result = a2m_read_patterns(blockptr, size - (blockptr - tune));
    if (result == INT_MAX) return false;

    return true;
}

bool Ca2mv2Player::a2_import(char *tune, unsigned long size)
{
    if ((size > 10) && !strncmp(tune, "_A2module_", 10)) {
        return a2m_import(tune, size);
    }

    if ((size > 15) && !strncmp(tune, "_A2tiny_module_", 15)) {
        return a2t_import(tune, size);
    }

    return false;
}

static void translate_effect (uint8_t effect, CPlayer::TrackedCmds &command, uint8_t &volume, uint8_t &param)
{
    switch (effect) {
	case ef_Arpeggio:          if (param) command = CPlayer::TrackedCmdArpeggio;         break;
	case ef_FSlideUp:          command = CPlayer::TrackedCmdPitchSlideUp;                break;
	case ef_FSlideDown:        command = CPlayer::TrackedCmdPitchSlideDown;              break;
	case ef_TonePortamento:    command = CPlayer::TrackedCmdTonePortamento;              break;
	case ef_Vibrato:           command = CPlayer::TrackedCmdVibrato;                     break;
	case ef_TPortamVolSlide:   command = CPlayer::TrackedCmdTonePortamentoVolumeSlide;   break;
	case ef_VibratoVolSlide:   command = CPlayer::TrackedCmdVibratoVolumeSlide;          break;
	case ef_FSlideUpFine:      command = CPlayer::TrackedCmdVolumeFineSlideUp;           break;
	case ef_FSlideDownFine:    command = CPlayer::TrackedCmdVolumeFineSlideDown;         break;
	case ef_SetModulatorVol:   command = CPlayer::TrackedCmdOPLModulatorVolume;          break;
	case ef_VolSlide:          command = CPlayer::TrackedCmdVolumeSlideUpDown;           break;
	case ef_PositionJump:      command = CPlayer::TrackedCmdPatternJumpTo;               break;
	case ef_SetInsVolume:      volume = param;                                  break;
	case ef_PatternBreak:      command = CPlayer::TrackedCmdPatternBreak;                break;
	case ef_SetTempo:          command = CPlayer::TrackedCmdTempo;                       break;
	case ef_SetSpeed:          command = CPlayer::TrackedCmdSpeed;                       break;
	case ef_TPortamVSlideFine: command = CPlayer::TrackedCmdTonePortamentoVolumeSlide;   break; /* estimation */
	case ef_VibratoVSlideFine: command = CPlayer::TrackedCmdVibratoVolumeSlide;          break; /* estimation */
	case ef_SetCarrierVol:     command = CPlayer::TrackedCmdOPLCarrierVolume;            break;
	case ef_SetWaveform:       command = CPlayer::TrackedCmdOPLCarrierModulatorWaveform; break;
	case ef_VolSlideFine:      command = CPlayer::TrackedCmdVolumeSlideUpDown;           break; /* estimation */
	case ef_RetrigNote:        command = CPlayer::TrackedCmdRetrigger;                   break;
	case ef_Tremolo:           command = CPlayer::TrackedCmdOPLTremolo;                  break;
	case ef_Tremor:            command = CPlayer::TrackedCmdOPLTremoloVibrato;           break; /* guess */
	case ef_ArpggVSlide:       command = CPlayer::TrackedCmdArpeggio;                    break; /* missing volume slide */
	case ef_ArpggVSlideFine:   command = CPlayer::TrackedCmdArpeggio;                    break; /* missing fine volume slide */
	case ef_MultiRetrigNote:   command = CPlayer::TrackedCmdRetrigger;                   break; /* estimation */
	case ef_FSlideUpVSlide:    command = CPlayer::TrackedCmdPitchFineSlideUp;            break; /* missing volume slide */
	case ef_FSlideDownVSlide:  command = CPlayer::TrackedCmdPitchFineSlideDown;          break; /* missing volume slide */
	case ef_FSlUpFineVSlide:   command = CPlayer::TrackedCmdPitchFineSlideUp;            break; /* missing fine volume slide */
	case ef_FSlDownFineVSlide: command = CPlayer::TrackedCmdPitchFineSlideDown;          break; /* missing fine volume slide */
	case ef_FSlUpVSlF:         command = CPlayer::TrackedCmdPitchFineSlideUp;            break; /* missing xx volume slide */
	case ef_FSlDownVSlF:       command = CPlayer::TrackedCmdPitchFineSlideDown;          break; /* missing xx volume slide */
	case ef_FSlUpFineVSlF:     command = CPlayer::TrackedCmdPitchFineSlideUp;            break; /* missing xx volume slide */
	case ef_FSlDownFineVSlF:   command = CPlayer::TrackedCmdPitchFineSlideDown;          break; /* missing xx volume slide */

	case ef_Extended: /* we ignore most of these */
		 if ((param >> 4) == ef_ex_PatternLoop)    { command = CPlayer::TrackedCmdPatternDoLoop;  param &= 0x0f; }
	    else if ((param >> 4) == ef_ex_PatternLoopRec) { command = CPlayer::TrackedCmdPatternSetLoop; param &= 0x0f; }
	    break;
	case ef_Extended2: /* we ignore most of these */
		 if ((param >> 4) == ef_ex2_PatDelayRow)   { command = CPlayer::TrackedCmdPatternDelay; param &= 0x0f; }
	    else if ((param >> 4) == ef_ex2_NoteCut)       { command = CPlayer::TrackedCmdNoteCut; }
	    break;
	case ef_SetGlobalVolume:   command = CPlayer::TrackedCmdGlobalVolume;                break;
	case ef_SwapArpeggio:                                                                break; /* ignore */
	case ef_SwapVibrato:                                                                 break; /* ignore */
	case ef_ForceInsVolume:    volume = param;                                           break; /* estimation */
	case ef_Extended3: /* we ignore all of these */
	    break;
	case ef_ExtraFineArpeggio: command = CPlayer::TrackedCmdVibrato;                     break; /* estimation */
	case ef_ExtraFineVibrato:  command = CPlayer::TrackedCmdVibrato;                     break; /* estimation */
	case ef_ExtraFineTremolo:  command = CPlayer::TrackedCmdOPLTremolo;                  break; /* estimation */
	case ef_SetCustomSpeedTab:                                                           break; /* ignore */
	case ef_GlobalFSlideUp:                                                              break; /* ignore */
	case ef_GlobalFSlideDown:                                                            break; /* ignore */
	case ef_GlobalFreqSlideUpXF:                                                         break; /* ignore */
	case ef_GlobalFreqSlideDnXF:                                                         break; /* ignore */
    }
}

void Ca2mv2Player::gettrackdata(unsigned char pattern, void (*callback)(void *arg, unsigned char row, unsigned char channel, unsigned char note, TrackedCmds command, unsigned char inst, unsigned char volume, unsigned char param), void *arg)
{
    if (!eventsinfo) return;
    if (pattern >= eventsinfo->patterns) return;

    for (int row = 0; row < eventsinfo->rows; row++) {
        for (int ch = 0; ch < eventsinfo->channels; ch++) {
            tADTRACK2_EVENT e = *get_event_p (pattern, ch, row);
            TrackedCmds command = TrackedCmdNone;
            unsigned char param = e.eff[0].val;
            unsigned char volume = 0xff;
            TrackedCmds command2 = TrackedCmdNone;
            unsigned char param2 = e.eff[1].val;

            if (e.note == 0xff) {
                command = TrackedCmdNoteCut;
                e.note = 0;
            } else if (!note_in_range(e.note)) {
                e.note = 0;
            }

            translate_effect (e.eff[0].def, command,  volume, param);
            translate_effect (e.eff[1].def, command2, volume, param2);

            if ((command == TrackedCmdNone) && (command2 != TrackedCmdNone))
            {
               command = command2;
               param = param2;
            }

            if (e.note || e.instr_def || (command != TrackedCmdNone) || (volume != 0xff)) {
                callback (arg, row, ch, e.note, command, e.instr_def, volume, param);
            }
        }
    }
}
