Skip to content

Commit

Permalink
Implement flow-specific quirks system for Pattern Loop et. al. (libxm…
Browse files Browse the repository at this point in the history
…p#816)

This patch splits off Pattern Loop-only quirks from m->quirk into
a new flow-specific field, m->flow_mode, and implements several new
loop mode quirks. Due to the complexity of the Pattern Loop effect,
it has been split into its own function, and the module scan now
invokes this function (the alternative was two implementations).
This could probably be extended to all of the flow control effects
eventually, but effects.c would pull a bunch of other compilation
units into the development tests right now and I am not interested
in further scope creep for 4.6.1.

I've kept this patch limited to the formats implicated in the
discussion in libxmp#707, plus any format that was referencing `QUIRK_S3MLOOP`
(almost all of them erroneously).

I didn't bother splitting off FastTracker 2's shared loop target/
entry position field from `QUIRK_FT2BUGS` for the time being, as FT2 is
a mess and I didn't want to entangle this branch further with it.

Loop quirks implemented:

* Global loop target: Scream Tracker 3, Digital Tracker, Octalyser,
  Poly Tracker, and probably other trackers use a shared loop target
  between all tracks.
* Global loop count: Scream Tracker 3, Digital Tracker (until 1.9?),
  Octalyser, Poly Tracker, and probably other trackers use a shared
  loop count between all tracks.
* Loop end advances target (originally `QUIRK_S3MLOOP`): Scream
  Tracker 3 and Impulse Tracker (2.10+) will advance the loop target
  to the row after the loop end effect when a loop terminates.
  Scream Tracker 3.01b has a bug where it will instead set the loop
  end to the row of the effect, which I've chosen not to emulate.
* Loop end cancels jump: Liquid Tracker 0.80+ (and possibly some
  earlier versions) will cancel any jumps initiated by previous
  tracks on the same row when a loop terminates.
  This quirk is also applied to Octalyser to "fix" Dammed Illusion,
  though it is inaccurate and won't necessarily fix other similar
  cases.
* Reset loop vars on pattern change: Scream Tracker 3 and Imago
  Orpheus reset the loop variables every time the current position
  changes (except for one bug in Imago Orpheus). Liquid Tracker 1.03+
  will also do this if the "Scream Tracker compatibility" setting is
  enabled.
* SBx sets loop target if it is unset: Scream Tracker 3.01b SBx will
  set the loop target if no SB0 was used ever in the pattern.
* Only execute the first E60/E6x in a row: Digital Tracker (until 1.9?)
  will only handle the first Pattern Loop effect in a row, including
  E60.
* Only one loop can run at a time: Modplug Tracker 1.16 will prevent
  E6x/SBx from starting a loop if another loop is already running in
  a different channel.
* Ignore E60 when a loop is active: Liquid Tracker and Octalyser will
  not update the loop target when the loop count is non-zero.

Trackers this new system attempts to support:

* Scream Tracker 3.01b
* Scream Tracker 3.03b/3.20/3.21 (also applied to GDMs for now)
* Impulse Tracker 1.00
* Impulse Tracker 1.04
* Impulse Tracker 2.10 (also applied to some S3Ms)
* Modplug Tracker 1.16 (applied to some S3Ms, XMs, ITs)
* Imago Orpheus (also applied to some S3Ms)
* Liquid Tracker
* Poly Tracker
* Octalyser
* Digital Tracker 2.015/2.03/2.04 (also applied to Digital Tracker MODs)
* Digital Tracker 1.9

I've left `QUIRK_S3MLOOP` in place for now, but it doesn't do anything
anymore and its quirk slot can be repurposed for something else.
  • Loading branch information
AliceLR authored Dec 30, 2024
1 parent 4ec8603 commit 2b32c90
Show file tree
Hide file tree
Showing 86 changed files with 7,516 additions and 76 deletions.
2 changes: 2 additions & 0 deletions Makefile.vc
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ OBJS = \
src\med_extras.obj \
src\filter.obj \
src\effects.obj \
src\flow.obj \
src\mixer.obj \
src\mix_all.obj \
src\rng.obj \
Expand Down Expand Up @@ -239,6 +240,7 @@ LITE_OBJS = \
src\lite\lite-memio.obj \
src\lite\lite-rng.obj \
src\lite\lite-win32.obj \
src\lite\lite-flow.obj \
src\lite\lite-common.obj \
src\lite\lite-itsex.obj \
src\lite\lite-sample.obj \
Expand Down
2 changes: 2 additions & 0 deletions cmake/libxmp-sources.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ set(LIBXMP_SRC_LIST
src/med_extras.c
src/filter.c
src/effects.c
src/flow.c
src/mixer.c
src/mix_all.c
src/rng.c
Expand Down Expand Up @@ -204,6 +205,7 @@ set(LIBXMP_SRC_LIST_LITE
src/lite/lite-memio.c
src/lite/lite-rng.c
src/lite/lite-win32.c
src/lite/lite-flow.c
src/lite/lite-common.c
src/lite/lite-itsex.c
src/lite/lite-sample.c
Expand Down
5 changes: 5 additions & 0 deletions docs/Changelog
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ Stable versions
- Passing NULL to xmp_set_instrument_path() now unsets the instrument
path instead of crashing.
- Merge song file instrument path detection routines.
- Add compatibility for non-standard Pattern Loop implementations:
Scream Tracker 3.01b; Scream Tracker 3.03b+; Impulse Tracker 1.00;
Impulse Tracker 1.04 to 2.09; Modplug Tracker 1.16; Digital Tracker
<=2.04; Digital Tracker 1.9; Octalyser; Imago Orpheus; Liquid Tracker;
Poly Tracker. (MOD, FT2, and IT 2.10+ were already supported.)
Changes by Thomas Neumann:
- Fix XM envelope handling
- Bug fixes to DSMI loader
Expand Down
2 changes: 1 addition & 1 deletion lite/src/Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

SRC_OBJS = virtual.o format.o period.o player.o read_event.o \
misc.o dataio.o lfo.o scan.o control.o filter.o \
effects.o mixer.o mix_all.o load_helpers.o load.o \
effects.o flow.o mixer.o mix_all.o load_helpers.o load.o \
filetype.o hio.o smix.o memio.o rng.o win32.o

SRC_DFILES = Makefile $(SRC_OBJS:.o=.c) md5.c md5.h common.h effects.h \
Expand Down
2 changes: 1 addition & 1 deletion src/Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

SRC_OBJS = virtual.o format.o period.o player.o read_event.o dataio.o \
misc.o mkstemp.o md5.o lfo.o scan.o control.o far_extras.o \
med_extras.o filter.o effects.o mixer.o mix_all.o rng.o \
med_extras.o filter.o effects.o flow.o mixer.o mix_all.o rng.o \
load_helpers.o load.o hio.o hmn_extras.o extras.o smix.o \
filetype.o memio.o tempfile.o mix_paula.o miniz_tinfl.o win32.o

Expand Down
80 changes: 79 additions & 1 deletion src/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,79 @@ int libxmp_snprintf (char *, size_t, const char *, ...) LIBXMP_ATTRIB_PRINTF(3,4
QUIRK_VIRTUAL | QUIRK_FILTER | QUIRK_RSTCHN | \
QUIRK_IGSTPOR | QUIRK_S3MRTG | QUIRK_MARKER )

/* Quirks specific to flow effects, especially Pattern Loop. */
#define FLOW_LOOP_GLOBAL_TARGET (1 << 0) /* Global target for all tracks */
#define FLOW_LOOP_GLOBAL_COUNT (1 << 1) /* Global count for all tracks */
#define FLOW_LOOP_END_ADVANCES (1 << 2) /* Loop end advances target (S3M) */
#define FLOW_LOOP_END_CANCELS (1 << 3) /* Loop end cancels prev jumps on row (LIQ) */
#define FLOW_LOOP_PATTERN_RESET (1 << 4) /* Target/count reset on pattern change */
#define FLOW_LOOP_INIT_SAMEROW (1 << 5) /* SBx sets target if it isn't set (ST 3.01) */
#define FLOW_LOOP_FIRST_EFFECT (1 << 6) /* Only execute the first E60/E6x in a row */
#define FLOW_LOOP_ONE_AT_A_TIME (1 << 7) /* Init E6x if no other channel is looping (MPT) */
#define FLOW_LOOP_IGNORE_TARGET (1 << 8) /* Ignore E60 if count is >=1 (LIQ) */
/*#define FLOW_LOOP_TICK_0_JUMP */ /* Loop jump shortens row to one tick (DTM) */

#define HAS_FLOW_MODE(x) (m->flow_mode & (x))

#define FLOW_MODE_GENERIC 0
#define FLOW_LOOP_GLOBAL (FLOW_LOOP_GLOBAL_TARGET | FLOW_LOOP_GLOBAL_COUNT)

/* Scream Tracker 3. No S3Ms seem to rely on the earlier behavior mode.
* 3.01b has a bug where the end advancement sets the target to the same line
* instead of the next line; there's no way to make use of this without getting
* stuck, so it's not simulated.
*/
#define FLOW_MODE_ST3_301 (FLOW_MODE_ST3_321 | FLOW_LOOP_INIT_SAMEROW)
#define FLOW_MODE_ST3_321 (FLOW_LOOP_GLOBAL | FLOW_LOOP_PATTERN_RESET | \
FLOW_LOOP_END_ADVANCES)

/* Impulse Tracker. Not clear if anything relies on either old behavior type.
*/
#define FLOW_MODE_IT_100 (FLOW_LOOP_GLOBAL)
#define FLOW_MODE_IT_104 (FLOW_MODE_GENERIC)
#define FLOW_MODE_IT_210 (FLOW_LOOP_END_ADVANCES)

/* Modplug Tracker/OpenMPT */
#define FLOW_MODE_MPT_116 (FLOW_LOOP_ONE_AT_A_TIME)

/* Imago Orpheus. Pattern Jump actually does not reset target/count, but all
* other forms of pattern change do. Unclear if anything relies on it.
*/
#define FLOW_MODE_ORPHEUS (FLOW_LOOP_PATTERN_RESET)

/* Liquid Tracker uses generic MOD loops with an added behavior where
* the end of a loop will cancel any other jump in the row that preceded it.
* M60 is also ignored in channels that have started a loop for some reason.
* There is also a "Scream Tracker" compatibility mode (only detectable in the
* newer format) that adds LOOP_MODE_PATTERN_RESET.
*/
#define FLOW_MODE_LIQUID (FLOW_LOOP_END_CANCELS | FLOW_LOOP_IGNORE_TARGET)
#define FLOW_MODE_LIQUID_COMPAT (FLOW_MODE_LIQUID | FLOW_LOOP_PATTERN_RESET)

/* Octalyser (Atari). Looping jumps to the original position E60 was used in,
* which libxmp doesn't simulate for now since it mostly gets the player stuck.
* Octalyser ignores E60 if a loop is currently active; it's not clear if it's
* possible for a module to actually rely on this behavior.
*
* LOOP_MODE_END_CANCELS is inaccurate but needed to fix "Dammed Illusion",
* which has multiple E6x on one line that don't trigger because the module
* expects to play in 4 channel mode. This quirk only works for this module
* because it uses even loop counts, and doesn't break any other modules
* because multiple E6x on a row otherwise traps the player.
*/
#define FLOW_MODE_OCTALYSER (FLOW_LOOP_GLOBAL | FLOW_LOOP_IGNORE_TARGET | \
FLOW_LOOP_END_CANCELS)

/* Digital Tracker prior to shareware 1.02 doesn't use LOOP_MODE_FIRST_EFFECT,
* but any MOD that would rely on it is impossible to fingerprint.
* Commercial version 1.9(?) added per-track counters.
* Digital Home Studio added a bizarre tick-0 jump bug.
*/
#define FLOW_MODE_DTM_203 (FLOW_LOOP_GLOBAL | FLOW_LOOP_FIRST_EFFECT)
#define FLOW_MODE_DTM_19 (FLOW_LOOP_GLOBAL_TARGET)
#define FLOW_MODE_DTM_DHS (FLOW_LOOP_GLOBAL_TARGET | FLOW_LOOP_TICK_0_JUMP)


/* DSP effects */
#define DSP_EFFECT_CUTOFF 0x02
#define DSP_EFFECT_RESONANCE 0x03
Expand Down Expand Up @@ -395,6 +468,7 @@ struct module_data {
int mvol; /* Mix volume (S3M/IT) */
const int *vol_table; /* Volume translation table */
int quirk; /* player quirks */
int flow_mode; /* Flow quirks, esp. Pattern Loop */
#define READ_EVENT_MOD 0
#define READ_EVENT_FT2 1
#define READ_EVENT_ST3 2
Expand Down Expand Up @@ -429,7 +503,11 @@ struct flow_control {
int jump;
int delay;
int jumpline;
int loop_chn;
int loop_dest; /* Pattern loop destination, -1 for none */
int loop_param; /* Last loop param for Digital Tracker */
int loop_start; /* Global loop target for S3M et al. */
int loop_count; /* Global loop count for S3M et al. */
int loop_active_num; /* Number of active loops for scan */
#ifndef LIBXMP_CORE_PLAYER
int jump_in_pat;
#endif
Expand Down
24 changes: 2 additions & 22 deletions src/effects.c
Original file line number Diff line number Diff line change
Expand Up @@ -416,27 +416,7 @@ void libxmp_process_fx(struct context_data *ctx, struct channel_data *xc, int ch
}
break;
case EX_PATTERN_LOOP: /* Loop pattern */
if (fxp == 0) {
/* mark start of loop */
f->loop[chn].start = p->row;
if (HAS_QUIRK(QUIRK_FT2BUGS))
p->flow.jumpline = p->row;
} else {
/* end of loop */
if (f->loop[chn].count) {
if (--f->loop[chn].count) {
/* **** H:FIXME **** */
f->loop_chn = ++chn;
} else {
if (HAS_QUIRK(QUIRK_S3MLOOP))
f->loop[chn].start =
p->row + 1;
}
} else {
f->loop[chn].count = fxp;
f->loop_chn = ++chn;
}
}
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);
Expand Down Expand Up @@ -597,7 +577,7 @@ void libxmp_process_fx(struct context_data *ctx, struct channel_data *xc, int ch
case FX_IT_BREAK: /* Pattern break with hex parameter */
/* IT break is not applied if a lower channel looped (2.00+).
* (Labyrinth of Zeux ZX_11.it "Raceway"). */
if (f->loop_chn == 0) {
if (f->loop_dest < 0) {
p->flow.pbreak = 1;
p->flow.jumpline = fxp;
}
Expand Down
111 changes: 111 additions & 0 deletions src/flow.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/* Extended Module Player
* Copyright (C) 1996-2024 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"

/* Process a pattern loop effect with the parameter fxp. A parameter of 0
* will set the loop target, and a parameter of 1-15 (most formats) or
* 1-255 (OctaMED) will perform a loop.
*
* The compatibility logic for Pattern Loop is complex, so a flow_control
* argument is taken such that the scan can use this function directly.
*
* If the development tests ever start building against effects.c, this
* can be moved back to effects.c.
*/
void libxmp_process_pattern_loop(struct context_data *ctx,
struct flow_control *f, int chn, int row, int fxp)
{
struct module_data *m = &ctx->m;
struct xmp_module *mod = &m->mod;
int *start = &f->loop[chn].start;
int *count = &f->loop[chn].count;
int i;

/* Digital Tracker: only the first E60 or E6x is handled per row. */
if (HAS_FLOW_MODE(FLOW_LOOP_FIRST_EFFECT) && f->loop_param >= 0) {
return;
}
f->loop_param = fxp;

/* Scream Tracker 3, Digital Tracker, Octalyser, and probably others
* use global loop targets and counts. Later versions of Digital
* Tracker use a global target but per-track counts. */
if (HAS_FLOW_MODE(FLOW_LOOP_GLOBAL_TARGET)) {
start = &f->loop_start;
}
if (HAS_FLOW_MODE(FLOW_LOOP_GLOBAL_COUNT)) {
count = &f->loop_count;
}

if (fxp == 0) {
/* mark start of loop */
/* Liquid Tracker: M60 is ignored for channels with count >= 1 */
if (HAS_FLOW_MODE(FLOW_LOOP_IGNORE_TARGET) && *count >= 1) {
return;
}
*start = row;
if (HAS_QUIRK(QUIRK_FT2BUGS))
f->jumpline = row;
} else {
/* end of loop */
if (*start < 0) {
/* Scream Tracker 3.01b: if SB0 wasn't used, the first
* SBx used will set the loop target to its row. */
if (HAS_FLOW_MODE(FLOW_LOOP_INIT_SAMEROW)) {
*start = row;
} else {
*start = 0;
}
}

if (*count) {
if (--(*count)) {
f->loop_dest = *start;
} else {
/* S3M and IT: loop termination advances the
* loop target past SBx. */
if (HAS_FLOW_MODE(FLOW_LOOP_END_ADVANCES)) {
*start = row + 1;
}
/* Liquid Tracker cancels any other loop jumps
* this row started on loop termination. */
if (HAS_FLOW_MODE(FLOW_LOOP_END_CANCELS)) {
f->loop_dest = -1;
}
f->loop_active_num--;
}
} else {
/* Modplug Tracker: only begin a loop if no
* other channel is currently looping. */
if (HAS_FLOW_MODE(FLOW_LOOP_ONE_AT_A_TIME)) {
for (i = 0; i < mod->chn; i++) {
if (i != chn && f->loop[i].count != 0)
return;
}
}
*count = fxp;
f->loop_dest = *start;
f->loop_active_num++;
}
}
}
1 change: 1 addition & 0 deletions src/lite/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ LITE = lite-virtual.o lite-format.o lite-period.o lite-player.o lite-read_event
lite-misc.o lite-dataio.o lite-lfo.o lite-scan.o lite-control.o lite-filter.o \
lite-effects.o lite-mixer.o lite-mix_all.o lite-load_helpers.o lite-load.o \
lite-filetype.o lite-hio.o lite-smix.o lite-memio.o lite-rng.o lite-win32.o \
lite-flow.o \
\
lite-common.o lite-itsex.o lite-sample.o \
lite-xm_load.o lite-mod_load.o lite-s3m_load.o lite-it_load.o
Expand Down
4 changes: 4 additions & 0 deletions src/lite/lite-flow.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#ifndef LIBXMP_CORE_PLAYER
#define LIBXMP_CORE_PLAYER
#endif
#include "../flow.c"
Loading

0 comments on commit 2b32c90

Please sign in to comment.