/* Extended Module Player
 * Copyright (C) 1996-2025 Claudio Matsuoka and Hipolito Carraro Jr
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "common.h"
#include "player.h"
#include "effects.h"
#include "period.h"
#include "virtual.h"
#include "mixer.h"
#ifndef LIBXMP_CORE_PLAYER
#include "extras.h"
#endif

#define NOT_IMPLEMENTED
#define HAS_QUIRK(x) (m->quirk & (x))

#define SET_LFO_NOTZERO(lfo, depth, rate) do { \
	if ((depth) != 0) libxmp_lfo_set_depth(lfo, depth); \
	if ((rate) != 0)  libxmp_lfo_set_rate(lfo, rate); \
} while (0)

#define EFFECT_MEMORY__(p, m) do { \
	if ((p) == 0) { (p) = (m); } else { (m) = (p); } \
} while (0)

/* ST3 effect memory is not a bug, but it's a weird implementation and it's
 * unlikely to be supported in anything other than ST3 (or OpenMPT).
 */
#define EFFECT_MEMORY(p, m) do { \
	if (HAS_QUIRK(QUIRK_ST3BUGS)) { \
		EFFECT_MEMORY__((p), xc->vol.memory); \
	} else { \
		EFFECT_MEMORY__((p), (m)); \
	} \
} while (0)

#define EFFECT_MEMORY_SETONLY(p, m) do { \
	EFFECT_MEMORY__((p), (m)); \
	if (HAS_QUIRK(QUIRK_ST3BUGS)) { \
		if ((p) != 0) { xc->vol.memory = (p); } \
	} \
} while (0)

#define EFFECT_MEMORY_S3M(p) do { \
	if (HAS_QUIRK(QUIRK_ST3BUGS)) { \
		EFFECT_MEMORY__((p), xc->vol.memory); \
	} \
} while (0)


static void do_toneporta(struct context_data *ctx,
                                struct channel_data *xc, int note)
{
	struct module_data *m = &ctx->m;
	struct xmp_instrument *instrument = &m->mod.xxi[xc->ins];
	struct xmp_subinstrument *sub;
	int mapped_xpo = 0;
	int mapped = 0;

	if (IS_VALID_NOTE(xc->key)) {
		mapped = instrument->map[xc->key].ins;
	}

	if (mapped >= instrument->nsm) {
		mapped = 0;
	}

	sub = &instrument->sub[mapped];

	if (IS_VALID_NOTE(note - 1) && (uint32)xc->ins < m->mod.ins) {
		note--;
		if (IS_VALID_NOTE(xc->key_porta)) {
			mapped_xpo = instrument->map[xc->key_porta].xpo;
		}
		xc->porta.target = libxmp_note_to_period(ctx, note + sub->xpo +
			mapped_xpo, xc->finetune, xc->per_adj);
	}
	xc->porta.dir = xc->period < xc->porta.target ? 1 : -1;
}

void libxmp_process_fx(struct context_data *ctx, struct channel_data *xc, int chn,
		struct xmp_event *e, int fnum)
{
	struct player_data *p = &ctx->p;
	struct module_data *m = &ctx->m;
	struct xmp_module *mod = &m->mod;
	struct flow_control *f = &p->flow;
	uint8 note, fxp, fxt;
	int h, l;

	/* key_porta is IT only */
	if (m->read_event_type != READ_EVENT_IT) {
		xc->key_porta = xc->key;
	}

	note = e->note;
	if (fnum == 0) {
		fxt = e->fxt;
		fxp = e->fxp;
	} else {
		fxt = e->f2t;
		fxp = e->f2p;
	}

	switch (fxt) {
	case FX_ARPEGGIO:
	      fx_arpeggio:
		if (!HAS_QUIRK(QUIRK_ARPMEM) || fxp != 0) {
			xc->arpeggio.val[0] = 0;
			xc->arpeggio.val[1] = MSN(fxp);
			xc->arpeggio.val[2] = LSN(fxp);
			xc->arpeggio.size = 3;
		}
		break;
	case FX_S3M_ARPEGGIO:
		EFFECT_MEMORY(fxp, xc->arpeggio.memory);
		goto fx_arpeggio;

#ifndef LIBXMP_CORE_PLAYER
	case FX_OKT_ARP3:
		if (fxp != 0) {
			xc->arpeggio.val[0] = -MSN(fxp);
			xc->arpeggio.val[1] = 0;
			xc->arpeggio.val[2] = LSN(fxp);
			xc->arpeggio.size = 3;
		}
		break;
	case FX_OKT_ARP4:
		if (fxp != 0) {
			xc->arpeggio.val[0] = 0;
			xc->arpeggio.val[1] = LSN(fxp);
			xc->arpeggio.val[2] = 0;
			xc->arpeggio.val[3] = -MSN(fxp);
			xc->arpeggio.size = 4;
		}
		break;
	case FX_OKT_ARP5:
		if (fxp != 0) {
			xc->arpeggio.val[0] = LSN(fxp);
			xc->arpeggio.val[1] = LSN(fxp);
			xc->arpeggio.val[2] = 0;
			xc->arpeggio.size = 3;
		}
		break;
#endif
	case FX_PORTA_UP:	/* Portamento up */
		EFFECT_MEMORY(fxp, xc->freq.memory);

		if (HAS_QUIRK(QUIRK_FINEFX)
		    && (fnum == 0 || !HAS_QUIRK(QUIRK_ITVPOR))) {
			switch (MSN(fxp)) {
			case 0xf:
				fxp &= 0x0f;
				goto fx_f_porta_up;
			case 0xe:
				fxp &= 0x0f;
				goto fx_xf_porta_up;
			}
		}

		if (fxp != 0) {
			SET(PITCHBEND);
			xc->freq.slide = -fxp;
			if (HAS_QUIRK(QUIRK_UNISLD))
				xc->porta.memory = fxp;
		}
		break;
	case FX_PORTA_DN:	/* Portamento down */
		/* FT2 has separate up and down memory. */
		if (HAS_QUIRK(QUIRK_FT2BUGS)) {
			EFFECT_MEMORY(fxp, xc->freq.down_memory);
		} else {
			EFFECT_MEMORY(fxp, xc->freq.memory);
		}

		if (HAS_QUIRK(QUIRK_FINEFX)
		    && (fnum == 0 || !HAS_QUIRK(QUIRK_ITVPOR))) {
			switch (MSN(fxp)) {
			case 0xf:
				fxp &= 0x0f;
				goto fx_f_porta_dn;
			case 0xe:
				fxp &= 0x0f;
				goto fx_xf_porta_dn;
			}
		}

		if (fxp != 0) {
			SET(PITCHBEND);
			xc->freq.slide = fxp;
			if (HAS_QUIRK(QUIRK_UNISLD))
				xc->porta.memory = fxp;
		}
		break;
	case FX_TONEPORTA:	/* Tone portamento */
		EFFECT_MEMORY_SETONLY(fxp, xc->porta.memory);

		if (fxp != 0) {
			if (HAS_QUIRK(QUIRK_UNISLD)) /* IT compatible Gxx off */
				xc->freq.memory = fxp;
			xc->porta.slide = fxp;
		}

		if (HAS_QUIRK(QUIRK_IGSTPOR)) {
			if (note == 0 && xc->porta.dir == 0)
				break;
		}

		if (!IS_VALID_INSTRUMENT(xc->ins))
			break;

		do_toneporta(ctx, xc, note);

		SET(TONEPORTA);
		break;

	case FX_VIBRATO:	/* Vibrato */
		EFFECT_MEMORY_SETONLY(fxp, xc->vibrato.memory);

		SET(VIBRATO);
		SET_LFO_NOTZERO(&xc->vibrato.lfo, LSN(fxp) << 2, MSN(fxp));
		break;
	case FX_FINE_VIBRATO:	/* Fine vibrato */
		EFFECT_MEMORY_SETONLY(fxp, xc->vibrato.memory);

		SET(VIBRATO);
		SET_LFO_NOTZERO(&xc->vibrato.lfo, LSN(fxp), MSN(fxp));
		break;

	case FX_TONE_VSLIDE:	/* Toneporta + vol slide */
		if (!IS_VALID_INSTRUMENT(xc->ins))
			break;
		do_toneporta(ctx, xc, note);
		SET(TONEPORTA);
		goto fx_volslide;
	case FX_VIBRA_VSLIDE:	/* Vibrato + vol slide */
		SET(VIBRATO);
		goto fx_volslide;

	case FX_TREMOLO:	/* Tremolo */
		EFFECT_MEMORY(fxp, xc->tremolo.memory);
		SET(TREMOLO);
		SET_LFO_NOTZERO(&xc->tremolo.lfo, LSN(fxp), MSN(fxp));
		break;

	case FX_SETPAN:		/* Set pan */
		if (HAS_QUIRK(QUIRK_PROTRACK)) {
			break;
		}
	    fx_setpan:
		/* From OpenMPT PanOff.xm:
		 * "Another chapter of weird FT2 bugs: Note-Off + Note Delay
		 *  + Volume Column Panning = Panning effect is ignored."
		 */
		if (!HAS_QUIRK(QUIRK_FT2BUGS)		/* If not FT2 */
		    || fnum == 0			/* or not vol column */
		    || e->note != XMP_KEY_OFF		/* or not keyoff */
		    || e->fxt != FX_EXTENDED		/* or not delay */
		    || MSN(e->fxp) != EX_DELAY) {
			xc->pan.val = fxp;
			xc->pan.surround = 0;
		}
		xc->rpv = 0;	/* storlek_20: set pan overrides random pan */
		xc->pan.surround = 0;
		break;
	case FX_OFFSET:		/* Set sample offset */
		EFFECT_MEMORY(fxp, xc->offset.memory);
		SET(OFFSET);
		if (note) {
			xc->offset.val &= xc->offset.val & ~0xffff;
			xc->offset.val |= fxp << 8;
			xc->offset.val2 = fxp << 8;
		}
		if (e->ins) {
			xc->offset.val2 = fxp << 8;
		}
		break;
	case FX_VOLSLIDE:	/* Volume slide */
	      fx_volslide:
		/* S3M file volume slide note:
		 * DFy  Fine volume down by y (...) If y is 0, the command will
		 *      be treated as a volume slide up with a value of f (15).
		 *      If a DFF command is specified, the volume will be slid
		 *      up.
		 */
		if (HAS_QUIRK(QUIRK_FINEFX)) {
			h = MSN(fxp);
			l = LSN(fxp);
			if (l == 0xf && h != 0) {
				xc->vol.memory = fxp;
				fxp >>= 4;
				goto fx_f_vslide_up;
			} else if (h == 0xf && l != 0) {
				xc->vol.memory = fxp;
				fxp &= 0x0f;
				goto fx_f_vslide_dn;
			}
		}

		/* recover memory */
		if (fxp == 0x00) {
			if ((fxp = xc->vol.memory) != 0)
				goto fx_volslide;
		}

		SET(VOL_SLIDE);

		/* Skaven's 2nd reality (S3M) has volslide parameter D7 => pri
		 * down. Other trackers only compute volumes if the other
		 * parameter is 0, Fall from sky.xm has 2C => do nothing.
		 * Also don't assign xc->vol.memory if fxp is 0, see Guild
		 * of Sounds.xm
		 */
		if (fxp) {
			xc->vol.memory = fxp;
			h = MSN(fxp);
			l = LSN(fxp);
			if (fxp) {
				if (HAS_QUIRK(QUIRK_VOLPDN)) {
					xc->vol.slide = l ? -l : h;
				} else {
					xc->vol.slide = h ? h : -l;
				}
			}
		}

		/* Mirko reports that a S3M with D0F effects created with ST321
		 * should process volume slides in all frames like ST300. I
		 * suspect ST3/IT could be handling D0F effects like this.
		 */
		if (HAS_QUIRK(QUIRK_FINEFX)) {
			if (MSN(xc->vol.memory) == 0xf
			    || LSN(xc->vol.memory) == 0xf) {
				SET(FINE_VOLS);
				xc->vol.fslide = xc->vol.slide;
			}
		}
		break;
	case FX_VOLSLIDE_2:	/* Secondary volume slide */
		SET(VOL_SLIDE_2);
		if (fxp) {
			h = MSN(fxp);
			l = LSN(fxp);
			xc->vol.slide2 = h ? h : -l;
		}
		break;
	case FX_JUMP:		/* Order jump */
		libxmp_process_pattern_jump(ctx, f, fxp);
		break;
	case FX_VOLSET:		/* Volume set */
		SET(NEW_VOL);
		xc->volume = fxp;
		if (xc->split) {
			p->xc_data[xc->pair].volume = xc->volume;
		}
		break;
	case FX_BREAK:		/* Pattern break */
		libxmp_process_pattern_break(ctx, f, 10 * MSN(fxp) + LSN(fxp));
		break;
	case FX_EXTENDED:	/* Extended effect */
		EFFECT_MEMORY_S3M(fxp);
		fxt = fxp >> 4;
		fxp &= 0x0f;
		switch (fxt) {
		case EX_FILTER:		/* Amiga led filter */
			if (IS_AMIGA_MOD()) {
				p->filter = !(fxp & 1);
			}
			break;
		case EX_F_PORTA_UP:	/* Fine portamento up */
			EFFECT_MEMORY(fxp, xc->fine_porta.up_memory);
			goto fx_f_porta_up;
		case EX_F_PORTA_DN:	/* Fine portamento down */
			EFFECT_MEMORY(fxp, xc->fine_porta.down_memory);
			goto fx_f_porta_dn;
		case EX_GLISS:		/* Glissando toggle */
			if (fxp) {
				SET_NOTE(NOTE_GLISSANDO);
			} else {
				RESET_NOTE(NOTE_GLISSANDO);
			}
			break;
		case EX_VIBRATO_WF:	/* Set vibrato waveform */
			fxp &= 3;
			libxmp_lfo_set_waveform(&xc->vibrato.lfo, fxp);
			break;
		case EX_FINETUNE:	/* Set finetune */
			if (!HAS_QUIRK(QUIRK_FT2BUGS) || note > 0) {
				xc->finetune = (int8)(fxp << 4);
			}
			break;
		case EX_PATTERN_LOOP:	/* Loop pattern */
			libxmp_process_pattern_loop(ctx, f, chn, p->row, fxp);
			break;
		case EX_TREMOLO_WF:	/* Set tremolo waveform */
			libxmp_lfo_set_waveform(&xc->tremolo.lfo, fxp & 3);
			break;
		case EX_SETPAN:
			fxp <<= 4;
			goto fx_setpan;
		case EX_RETRIG:		/* Retrig note */
#ifndef LIBXMP_CORE_PLAYER
		    fx_retrig:
#endif
			SET(RETRIG);
			xc->retrig.val = fxp;
			xc->retrig.count = fxp + 1;
			xc->retrig.type = 0;
			xc->retrig.limit = HAS_QUIRK(QUIRK_RTONCE) ? 1 : 0;
			break;
		case EX_F_VSLIDE_UP:	/* Fine volume slide up */
			EFFECT_MEMORY(fxp, xc->fine_vol.up_memory);
			goto fx_f_vslide_up;
		case EX_F_VSLIDE_DN:	/* Fine volume slide down */
			EFFECT_MEMORY(fxp, xc->fine_vol.down_memory);
			goto fx_f_vslide_dn;
		case EX_CUT:		/* Cut note */
			SET(RETRIG);
			SET_NOTE(NOTE_CUT);	/* for IT cut-carry */
			xc->retrig.val = fxp + 1;
			xc->retrig.count = xc->retrig.val;
			xc->retrig.type = 0x10;
			break;
		case EX_DELAY:		/* Note delay */
			/* computed at frame loop */
			break;
		case EX_PATT_DELAY:	/* Pattern delay */
			goto fx_patt_delay;
		case EX_INVLOOP:	/* Invert loop / funk repeat */
			xc->invloop.speed = fxp;
			break;
		}
		break;
	case FX_SPEED:		/* Set speed */
		if (HAS_QUIRK(QUIRK_NOBPM) || p->flags & XMP_FLAGS_VBLANK) {
			goto fx_s3m_speed;
		}

		/* speedup.xm needs BPM = 20 */
		if (fxp < 0x20) {
			goto fx_s3m_speed;
		}
		goto fx_s3m_bpm;

	case FX_FINETUNE:
		xc->finetune = (int16) (fxp - 0x80);
		break;

	case FX_F_VSLIDE_UP:	/* Fine volume slide up */
		EFFECT_MEMORY(fxp, xc->fine_vol.up_memory);
	    fx_f_vslide_up:
		SET(FINE_VOLS);
		xc->vol.fslide = fxp;
		break;
	case FX_F_VSLIDE_DN:	/* Fine volume slide down */
		EFFECT_MEMORY(fxp, xc->fine_vol.up_memory);
	    fx_f_vslide_dn:
		SET(FINE_VOLS);
		xc->vol.fslide = -fxp;
		break;

	case FX_F_PORTA_UP:	/* Fine portamento up */
	    fx_f_porta_up:
		if (fxp) {
			SET(FINE_BEND);
			xc->freq.fslide = -fxp;
		}
		break;
	case FX_F_PORTA_DN:	/* Fine portamento down */
	    fx_f_porta_dn:
		if (fxp) {
			SET(FINE_BEND);
			xc->freq.fslide = fxp;
		}
		break;
	case FX_PATT_DELAY:
	    fx_patt_delay:
		if (m->read_event_type != READ_EVENT_ST3 || !p->flow.delay) {
			p->flow.delay = fxp;
		}
		break;

	case FX_S3M_SPEED:	/* Set S3M speed */
		EFFECT_MEMORY_S3M(fxp);
	    fx_s3m_speed:
		if (fxp) {
			p->speed = fxp;
#ifndef LIBXMP_CORE_PLAYER
			p->st26_speed = 0;
#endif
		}
		break;
	case FX_S3M_BPM:	/* Set S3M BPM */
	    fx_s3m_bpm: {
		/* Lower time factor in MED allows lower BPM values */
		int min_bpm = (int)(0.5 + m->time_factor * XMP_MIN_BPM / 10);
		if (fxp < min_bpm)
			fxp = min_bpm;
		p->bpm = fxp;
		break;
	}

#ifndef LIBXMP_CORE_DISABLE_IT
	case FX_IT_BPM:		/* Set IT BPM */
		if (MSN(fxp) == 0) {
			SET(TEMPO_SLIDE);
			if (LSN(fxp))	/* T0x - Tempo slide down by x */
				xc->tempo.slide = -LSN(fxp);
			/* T00 - Repeat previous slide */
		} else if (MSN(fxp) == 1) {	/* T1x - Tempo slide up by x */
			SET(TEMPO_SLIDE);
			xc->tempo.slide = LSN(fxp);
		} else {
			if (fxp < XMP_MIN_BPM)
				fxp = XMP_MIN_BPM;
			p->bpm = fxp;
		}
		break;
	case FX_IT_ROWDELAY:
		if (!f->rowdelay_set) {
			f->rowdelay = fxp;
			f->rowdelay_set = ROWDELAY_ON | ROWDELAY_FIRST_FRAME;
		}
		break;

	/* From the OpenMPT VolColMemory.it test case:
	 * "Volume column commands a, b, c and d (volume slide) share one
	 *  effect memory, but it should not be shared with Dxy in the effect
	 *  column.
	 */
	case FX_VSLIDE_UP_2:	/* Fine volume slide up */
		EFFECT_MEMORY(fxp, xc->vol.memory2);
		SET(VOL_SLIDE_2);
		xc->vol.slide2 = fxp;
		break;
	case FX_VSLIDE_DN_2:	/* Fine volume slide down */
		EFFECT_MEMORY(fxp, xc->vol.memory2);
		SET(VOL_SLIDE_2);
		xc->vol.slide2 = -fxp;
		break;
	case FX_F_VSLIDE_UP_2:	/* Fine volume slide up */
		EFFECT_MEMORY(fxp, xc->vol.memory2);
		SET(FINE_VOLS_2);
		xc->vol.fslide2 = fxp;
		break;
	case FX_F_VSLIDE_DN_2:	/* Fine volume slide down */
		EFFECT_MEMORY(fxp, xc->vol.memory2);
		SET(FINE_VOLS_2);
		xc->vol.fslide2 = -fxp;
		break;
	case FX_IT_BREAK:	/* Pattern break with hex parameter */
		libxmp_process_pattern_break(ctx, f, fxp);
		break;

#endif

	case FX_GLOBALVOL:	/* Set global volume */
		if (fxp > m->gvolbase) {
			p->gvol = m->gvolbase;
		} else {
			p->gvol = fxp;
		}
		break;
	case FX_GVOL_SLIDE:	/* Global volume slide */
            fx_gvolslide:
		if (fxp) {
			SET(GVOL_SLIDE);
			xc->gvol.memory = fxp;
			h = MSN(fxp);
			l = LSN(fxp);

			if (HAS_QUIRK(QUIRK_FINEFX)) {
				if (l == 0xf && h != 0) {
					xc->gvol.slide = 0;
					xc->gvol.fslide = h;
				} else if (h == 0xf && l != 0) {
					xc->gvol.slide = 0;
					xc->gvol.fslide = -l;
				} else {
					xc->gvol.slide = h ? h : -l;
					xc->gvol.fslide = 0;
				}
			} else {
				xc->gvol.slide = h ? h : -l;
				xc->gvol.fslide = 0;
			}
		} else {
			if ((fxp = xc->gvol.memory) != 0) {
				goto fx_gvolslide;
			}
		}
		break;
	case FX_KEYOFF:		/* Key off */
		xc->keyoff = fxp + 1;
		break;
	case FX_ENVPOS:		/* Set envelope position */
		/* From OpenMPT SetEnvPos.xm:
		 * "When using the Lxx effect, Fasttracker 2 only sets the
		 *  panning envelope position if the volume envelope’s sustain
		 *  flag is set.
		 */
		if (HAS_QUIRK(QUIRK_FT2BUGS)) {
			struct xmp_instrument *instrument;
			instrument = libxmp_get_instrument(ctx, xc->ins);
			if (instrument != NULL) {
				if (instrument->aei.flg & XMP_ENVELOPE_SUS) {
					xc->p_idx = fxp;
				}
			}
		} else {
			xc->p_idx = fxp;
		}
		xc->v_idx = fxp;
		xc->f_idx = fxp;
		break;
	case FX_PANSLIDE:	/* Pan slide (XM) */
		EFFECT_MEMORY(fxp, xc->pan.memory);
		SET(PAN_SLIDE);
		xc->pan.slide = LSN(fxp) - MSN(fxp);
		break;
	case FX_PANSL_NOMEM:	/* Pan slide (XM volume column) */
		SET(PAN_SLIDE);
		xc->pan.slide = LSN(fxp) - MSN(fxp);
		break;

#ifndef LIBXMP_CORE_DISABLE_IT
	case FX_IT_PANSLIDE:	/* Pan slide w/ fine pan (IT) */
		SET(PAN_SLIDE);
		if (fxp) {
			if (MSN(fxp) == 0xf) {
				xc->pan.slide = 0;
				xc->pan.fslide = LSN(fxp);
			} else if (LSN(fxp) == 0xf) {
				xc->pan.slide = 0;
				xc->pan.fslide = -MSN(fxp);
			} else {
				SET(PAN_SLIDE);
				xc->pan.slide = LSN(fxp) - MSN(fxp);
				xc->pan.fslide = 0;
			}
		}
		break;
#endif

	case FX_MULTI_RETRIG:	/* Multi retrig */
		EFFECT_MEMORY_S3M(fxp);
		if (fxp) {
			xc->retrig.val = LSN(fxp);
			xc->retrig.type = MSN(fxp);
		}
		if (note) {
			xc->retrig.count = xc->retrig.val + 1;
		}
		xc->retrig.limit = 0;
		SET(RETRIG);
		break;
	case FX_TREMOR:			/* Tremor */
		EFFECT_MEMORY(fxp, xc->tremor.memory);
		xc->tremor.up = MSN(fxp);
		xc->tremor.down = LSN(fxp);
		if (IS_PLAYER_MODE_FT2()) {
			xc->tremor.count |= 0x80;
		} else {
			if (xc->tremor.up == 0) {
				xc->tremor.up++;
			}
			if (xc->tremor.down == 0) {
				xc->tremor.down++;
			}
		}
		SET(TREMOR);
		break;
	case FX_XF_PORTA:	/* Extra fine portamento */
		h = MSN(fxp);
		fxp &= 0x0f;
		switch (h) {
		case XX_XF_PORTA_UP:	/* Extra fine portamento up */
			EFFECT_MEMORY(fxp, xc->fine_porta.xf_up_memory);
		      fx_xf_porta_up:
			SET(FINE_BEND);
			xc->freq.fslide = -0.25 * fxp;
			break;
		case XX_XF_PORTA_DN:	/* Extra fine portamento down */
			EFFECT_MEMORY(fxp, xc->fine_porta.xf_down_memory);
		      fx_xf_porta_dn:
			SET(FINE_BEND);
			xc->freq.fslide = 0.25 * fxp;
			break;
		}
		break;
	case FX_SURROUND:
		xc->pan.surround = fxp;
		break;
	case FX_REVERSE:	/* Play forward/backward */
		libxmp_virt_reverse(ctx, chn, fxp);
		break;

#ifndef LIBXMP_CORE_DISABLE_IT
	case FX_TRK_VOL:	/* Track volume setting */
		if (fxp <= m->volbase) {
			xc->mastervol = fxp;
		}
		break;
	case FX_TRK_VSLIDE:	/* Track volume slide */
		if (fxp == 0) {
			if ((fxp = xc->trackvol.memory) == 0)
				break;
		}

		if (HAS_QUIRK(QUIRK_FINEFX)) {
			h = MSN(fxp);
			l = LSN(fxp);
			if (h == 0xf && l != 0) {
				xc->trackvol.memory = fxp;
				fxp &= 0x0f;
				goto fx_trk_fvslide;
			} else if (l == 0xf && h != 0) {
				xc->trackvol.memory = fxp;
				fxp &= 0xf0;
				goto fx_trk_fvslide;
			}
		}

		SET(TRK_VSLIDE);
		if (fxp) {
			h = MSN(fxp);
			l = LSN(fxp);

			xc->trackvol.memory = fxp;
			if (HAS_QUIRK(QUIRK_VOLPDN)) {
				xc->trackvol.slide = l ? -l : h;
			} else {
				xc->trackvol.slide = h ? h : -l;
			}
		}

		break;
	case FX_TRK_FVSLIDE:	/* Track fine volume slide */
	      fx_trk_fvslide:
		SET(TRK_FVSLIDE);
		if (fxp) {
			xc->trackvol.fslide = MSN(fxp) - LSN(fxp);
		}
		break;

	case FX_IT_INSTFUNC:
		switch (fxp) {
		case 0:	/* Past note cut */
			libxmp_virt_pastnote(ctx, chn, VIRT_ACTION_CUT);
			break;
		case 1:	/* Past note off */
			libxmp_virt_pastnote(ctx, chn, VIRT_ACTION_OFF);
			break;
		case 2:	/* Past note fade */
			libxmp_virt_pastnote(ctx, chn, VIRT_ACTION_FADE);
			break;
		case 3:	/* Set NNA to note cut */
			libxmp_virt_setnna(ctx, chn, XMP_INST_NNA_CUT);
			break;
		case 4:	/* Set NNA to continue */
			libxmp_virt_setnna(ctx, chn, XMP_INST_NNA_CONT);
			break;
		case 5:	/* Set NNA to note off */
			libxmp_virt_setnna(ctx, chn, XMP_INST_NNA_OFF);
			break;
		case 6:	/* Set NNA to note fade */
			libxmp_virt_setnna(ctx, chn, XMP_INST_NNA_FADE);
			break;
		case 7:	/* Turn off volume envelope */
			SET_PER(VENV_PAUSE);
			break;
		case 8:	/* Turn on volume envelope */
			RESET_PER(VENV_PAUSE);
			break;
		case 9:	/* Turn off pan envelope */
			SET_PER(PENV_PAUSE);
			break;
		case 0xa:	/* Turn on pan envelope */
			RESET_PER(PENV_PAUSE);
			break;
		case 0xb:	/* Turn off pitch envelope */
			SET_PER(FENV_PAUSE);
			break;
		case 0xc:	/* Turn on pitch envelope */
			RESET_PER(FENV_PAUSE);
			break;
		}
		break;
	case FX_FLT_CUTOFF:
		xc->filter.cutoff = fxp;
		break;
	case FX_FLT_RESN:
		xc->filter.resonance = fxp;
		break;
	case FX_MACRO_SET:
		xc->macro.active = LSN(fxp);
		break;
	case FX_MACRO:
		SET(MIDI_MACRO);
		xc->macro.val = fxp;
		xc->macro.slide = 0;
		break;
	case FX_MACROSMOOTH:
		if (ctx->p.speed && xc->macro.val < 0x80) {
			SET(MIDI_MACRO);
			xc->macro.target = fxp;
			xc->macro.slide = ((float)fxp - xc->macro.val) / ctx->p.speed;
		}
		break;
	case FX_PANBRELLO:	/* Panbrello */
		SET(PANBRELLO);
		SET_LFO_NOTZERO(&xc->panbrello.lfo, LSN(fxp) << 4, MSN(fxp));
		break;
	case FX_PANBRELLO_WF:	/* Panbrello waveform */
		libxmp_lfo_set_waveform(&xc->panbrello.lfo, fxp & 3);
		break;
	case FX_HIOFFSET:	/* High offset */
		xc->offset.val &= 0xffff;
		xc->offset.val |= fxp << 16;
		break;
#endif

#ifndef LIBXMP_CORE_PLAYER

	/* SFX effects */
	case FX_VOL_ADD:
		if (!IS_VALID_INSTRUMENT(xc->ins)) {
			break;
		}
		SET(NEW_VOL);
		xc->volume = m->mod.xxi[xc->ins].sub[0].vol + fxp;
		if (xc->volume > m->volbase) {
			xc->volume = m->volbase;
		}
		break;
	case FX_VOL_SUB:
		if (!IS_VALID_INSTRUMENT(xc->ins)) {
			break;
		}
		SET(NEW_VOL);
		xc->volume = m->mod.xxi[xc->ins].sub[0].vol - fxp;
		if (xc->volume < 0) {
			xc->volume =0;
		}
		break;
	case FX_PITCH_ADD:
		SET_PER(TONEPORTA);
		xc->porta.target = libxmp_note_to_period(ctx, note - 1, xc->finetune, 0)
			+ fxp;
		xc->porta.slide = 2;
		xc->porta.dir = 1;
		break;
	case FX_PITCH_SUB:
		SET_PER(TONEPORTA);
		xc->porta.target = libxmp_note_to_period(ctx, note - 1, xc->finetune, 0)
			- fxp;
		xc->porta.slide = 2;
		xc->porta.dir = -1;
		break;

	/* Saga Musix says:
	 *
	 * "When both nibbles of an Fxx command are set, SoundTracker 2.6
	 * applies the both values alternatingly, first the high nibble,
	 * then the low nibble on the next row, then the high nibble again...
	 * If only the high nibble is set, it should act like if only the low
	 * nibble is set (i.e. F30 is the same as F03).
	 */
	case FX_ICE_SPEED:
		if (fxp) {
			if (LSN(fxp)) {
				p->st26_speed = (MSN(fxp) << 8) | LSN(fxp);
			} else {
				p->st26_speed = MSN(fxp);
			}
		}
		break;

	case FX_VOLSLIDE_UP:	/* Vol slide with uint8 arg */
		if (HAS_QUIRK(QUIRK_FINEFX)) {
			h = MSN(fxp);
			l = LSN(fxp);
			if (h == 0xf && l != 0) {
				fxp &= 0x0f;
				goto fx_f_vslide_up;
			}
		}

		if (fxp)
			xc->vol.slide = fxp;
		SET(VOL_SLIDE);
		break;
	case FX_VOLSLIDE_DN:	/* Vol slide with uint8 arg */
		if (HAS_QUIRK(QUIRK_FINEFX)) {
			h = MSN(fxp);
			l = LSN(fxp);
			if (h == 0xf && l != 0) {
				fxp &= 0x0f;
				goto fx_f_vslide_dn;
			}
		}

		if (fxp)
			xc->vol.slide = -fxp;
		SET(VOL_SLIDE);
		break;
	case FX_F_VSLIDE:	/* Fine volume slide */
		SET(FINE_VOLS);
		if (fxp) {
			h = MSN(fxp);
			l = LSN(fxp);
			xc->vol.fslide = h ? h : -l;
		}
		break;
	case FX_NSLIDE_DN:
	case FX_NSLIDE_UP:
	case FX_NSLIDE_R_DN:
	case FX_NSLIDE_R_UP:
		if (fxp != 0) {
			if (fxt == FX_NSLIDE_R_DN || fxt == FX_NSLIDE_R_UP) {
				xc->retrig.val = MSN(fxp);
				xc->retrig.count = MSN(fxp) + 1;
				xc->retrig.type = 0;
				xc->retrig.limit = 0;
			}

			if (fxt == FX_NSLIDE_UP || fxt == FX_NSLIDE_R_UP)
				xc->noteslide.slide = LSN(fxp);
			else
				xc->noteslide.slide = -LSN(fxp);

			xc->noteslide.count = xc->noteslide.speed = MSN(fxp);
		}
		if (fxt == FX_NSLIDE_R_DN || fxt == FX_NSLIDE_R_UP)
			SET(RETRIG);
		SET(NOTE_SLIDE);
		break;
	case FX_NSLIDE2_DN:
		SET(NOTE_SLIDE);
		xc->noteslide.slide = -fxp;
		xc->noteslide.count = xc->noteslide.speed = 1;
		break;
	case FX_NSLIDE2_UP:
		SET(NOTE_SLIDE);
		xc->noteslide.slide = fxp;
		xc->noteslide.count = xc->noteslide.speed = 1;
		break;
	case FX_F_NSLIDE_DN:
		SET(FINE_NSLIDE);
		xc->noteslide.fslide = -fxp;
		break;
	case FX_F_NSLIDE_UP:
		SET(FINE_NSLIDE);
		xc->noteslide.fslide = fxp;
		break;

	case FX_PER_VIBRATO:	/* Persistent vibrato */
		if (LSN(fxp) != 0) {
			SET_PER(VIBRATO);
		} else {
			RESET_PER(VIBRATO);
		}
		SET_LFO_NOTZERO(&xc->vibrato.lfo, LSN(fxp) << 2, MSN(fxp));
		break;
	case FX_PER_PORTA_UP:	/* Persistent portamento up */
		SET_PER(PITCHBEND);
		xc->freq.slide = -fxp;
		if ((xc->freq.memory = fxp) == 0)
			RESET_PER(PITCHBEND);
		break;
	case FX_PER_PORTA_DN:	/* Persistent portamento down */
		SET_PER(PITCHBEND);
		xc->freq.slide = fxp;
		if ((xc->freq.memory = fxp) == 0)
			RESET_PER(PITCHBEND);
		break;
	case FX_PER_TPORTA:	/* Persistent tone portamento */
		if (!IS_VALID_INSTRUMENT(xc->ins))
			break;
		SET_PER(TONEPORTA);
		do_toneporta(ctx, xc, note);
		xc->porta.slide = fxp;
		if (fxp == 0)
			RESET_PER(TONEPORTA);
		break;
	case FX_PER_VSLD_UP:	/* Persistent volslide up */
		SET_PER(VOL_SLIDE);
		xc->vol.slide = fxp;
		if (fxp == 0)
			RESET_PER(VOL_SLIDE);
		break;
	case FX_PER_VSLD_DN:	/* Persistent volslide down */
		SET_PER(VOL_SLIDE);
		xc->vol.slide = -fxp;
		if (fxp == 0)
			RESET_PER(VOL_SLIDE);
		break;
	case FX_VIBRATO2:	/* Deep vibrato (2x) */
		SET(VIBRATO);
		SET_LFO_NOTZERO(&xc->vibrato.lfo, LSN(fxp) << 3, MSN(fxp));
		break;
	case FX_MED_RETRIG:	/* MED 1Fxy delay x, then retrig every y */
		/* initial delay is computed at frame loop */
		SET(RETRIG);
		xc->retrig.val = LSN(fxp);
		xc->retrig.count = LSN(fxp) + 1;
		xc->retrig.type = 0;
		xc->retrig.limit = 0;
		break;
	case FX_SPEED_CP:	/* Set speed and ... */
		if (fxp) {
			p->speed = fxp;
			p->st26_speed = 0;
		}
		/* fall through */
	case FX_PER_CANCEL:	/* Cancel persistent effects */
		xc->per_flags = 0;
		break;

	/* 669 effects */

	case FX_669_PORTA_UP:	/* 669 portamento up */
		SET_PER(PITCHBEND);
		xc->freq.slide = 80 * fxp;
		if ((xc->freq.memory = fxp) == 0)
			RESET_PER(PITCHBEND);
		break;
	case FX_669_PORTA_DN:	/* 669 portamento down */
		SET_PER(PITCHBEND);
		xc->freq.slide = -80 * fxp;
		if ((xc->freq.memory = fxp) == 0)
			RESET_PER(PITCHBEND);
		break;
	case FX_669_TPORTA:	/* 669 tone portamento */
		if (!IS_VALID_INSTRUMENT(xc->ins))
			break;
		SET_PER(TONEPORTA);
		do_toneporta(ctx, xc, note);
		xc->porta.slide = 40 * fxp;
		if (fxp == 0)
			RESET_PER(TONEPORTA);
		break;
	case FX_669_FINETUNE:	/* 669 finetune */
		xc->finetune = 80 * (int8)fxp;
		break;
	case FX_669_VIBRATO:	/* 669 vibrato */
		if (LSN(fxp) != 0) {
			libxmp_lfo_set_waveform(&xc->vibrato.lfo, 669);
			SET_PER(VIBRATO);
		} else {
			RESET_PER(VIBRATO);
		}
		SET_LFO_NOTZERO(&xc->vibrato.lfo, 669, 1);
		break;

	/* ULT effects */

	case FX_ULT_TEMPO:	/* ULT tempo */
		/* Has unusual semantics and is hard to split into multiple
		 * effects, due to ULT's two effects lanes per channel:
		 *
		 * 00:    reset both speed and BPM to the default 6/125.
		 * 01-2f: set speed
		 * 30-ff: set BPM (CIA compatible)
		 */
		if (fxp == 0) {
			p->speed = 6;
			p->st26_speed = 0;
			fxp = 125;
		} else if (fxp < 0x30) {
			goto fx_s3m_speed;
		}
		goto fx_s3m_bpm;
	case FX_ULT_TPORTA:	/* ULT tone portamento */
		/* Like normal persistent tone portamento, except:
		 *
		 * 1) Despite the documentation claiming 300 cancels tone
		 * portamento, it actually reuses the last parameter.
		 *
		 * 2) A 3xx without a note will reuse the last target note.
		 */
		if (!IS_VALID_INSTRUMENT(xc->ins))
			break;
		SET_PER(TONEPORTA);
		EFFECT_MEMORY(fxp, xc->porta.memory);
		EFFECT_MEMORY(note, xc->porta.note_memory);
		do_toneporta(ctx, xc, note);
		xc->porta.slide = fxp;
		if (fxp == 0)
			RESET_PER(TONEPORTA);
		break;

	/* Archimedes (!Tracker, Digital Symphony, et al.) effects */

	case FX_LINE_JUMP:	/* !Tracker and Digital Symphony "Line Jump" */
		libxmp_process_line_jump(ctx, f, p->ord, fxp);
		break;
	case FX_RETRIG:		/* Retrigger with extended range */
		goto fx_retrig;
#endif

	default:
#ifndef LIBXMP_CORE_PLAYER
		libxmp_extras_process_fx(ctx, xc, chn, note, fxt, fxp, fnum);
#endif
		break;
	}
}
