View Issue Details

IDProjectCategoryView StatusLast Update
0001985OpenMPTlibopenmptpublic2026-08-16 10:38
Reporteryoyofr Assigned To 
PrioritynormalSeverityminorReproducibilityalways
Status newResolutionopen 
Product VersionOpenMPT 1.33.00.* (current testing) 
Summary0001985: 16-bit tempo XMs: header tempo clamped to 1000, and volume ramping causes loud whines
Description

Test module: "Jakob Bienenhalm – Haesslich" (scene.org, one-sample compo, winner of Evoke 2013 - One sample compo). Header tempo 39293, speed 16, 16 channels. Every row triggers a note on all 16 channels with EDx (x = channel index): one retrigger per tick, music in the volume column — the module is a PCM DAC at tick rate. Plays correctly in XMPlay, which I used as reference renderer (48 kHz WAV) for the measurements below.

Issue 1 — loader clamp. Load_xm.cpp clamps the header tempo to xmEx max (1000): the module plays 0000061:0000039× too slow. The XM saver has a comment saying FT2 accepts any 16-bit tempo. Suggested fix: clamp to uint16_max on load (safe as far as I can tell: the default tempo never goes through SetTempo(), and Fxx is 8-bit).

Issue 2 — volume ramp longer than a tick. With the tempo fixed, a row is 48 samples at 48 kHz but the FT2-style ramp is 5 ms (240 samples): retriggered every row, it never completes and modulates the whole mix at the row rate — a loud steady 0000006:0000001 kHz whine (−38 dBFS vs −86 in the XMPlay render). Disabling ramping instead leaves the tick steps unsmoothed: 16 kHz carrier at −32 dBFS (XMPlay: −54).

Suggested fix: cap the ramp length at the tick length when ticks are very short (e.g. samplesPerTick < 64). Measured result: whine −95 dBFS, carrier −56 — at or below the XMPlay reference, and every normal module stays bit-identical.

Note: fractional tick lengths in Classic mode make things worse (the inserted longer ticks form an audible 0000285:0000849 Hz-spaced comb); XMPlay appears to use integer ticks (its carrier sits at exactly 48000/3 Hz).

Steps To Reproduce

play https://files.scene.org/view/parties/2013/evoke13/mmul_onesample/jakob_bienenhalm_-_haesslich.xm

TagsNo tags attached.
Attached Files
0003-fix-xm-keep-full-16-bit-header-tempo.patch (1,100 bytes)   
diff --git a/soundlib/Load_xm.cpp b/soundlib/Load_xm.cpp
index b13143453..235e99d88 100644
--- a/soundlib/Load_xm.cpp
+++ b/soundlib/Load_xm.cpp
@@ -695,7 +695,15 @@ bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags)
 	if(fileHeader.speed)
 		Order().SetDefaultSpeed(fileHeader.speed);
 	if(fileHeader.tempo)
-		Order().SetDefaultTempo(Clamp(TEMPO(fileHeader.tempo, 0), ModSpecs::xmEx.GetTempoMin(), ModSpecs::xmEx.GetTempoMax()));
+	{
+		// rewamp: keep the FULL 16-bit tempo range. FT2 accepts any 16-bit
+		// BPM (this file's own saver notes it below), and sample-precise
+		// tempo tricks rely on it — "Haesslich" declares 39293 BPM, which the
+		// ModSpecs clamp (max 1000) silently rewrote into a wrong-speed song.
+		// Playback is safe: the default tempo never passes through
+		// SetTempo(), and XM tempo EFFECTS are 8-bit anyway.
+		Order().SetDefaultTempo(Clamp(TEMPO(fileHeader.tempo, 0), ModSpecs::xmEx.GetTempoMin(), TEMPO(uint16_max, 0)));
+	}
 
 	m_SongFlags.reset();
 	m_SongFlags.set(SONG_LINEARSLIDES, (fileHeader.flags & XMFileHeader::linearSlides) != 0);
0004-fix-cap-volume-ramp-at-tick-length-for-16-bit-tempo.patch (1,098 bytes)   
diff --git a/soundlib/Sndmix.cpp b/soundlib/Sndmix.cpp
index 34fbb1b6f..70363cb7f 100644
--- a/soundlib/Sndmix.cpp
+++ b/soundlib/Sndmix.cpp
@@ -1984,6 +1984,19 @@ void CSoundFile::ProcessRamping(ModChannel &chn) const
 		}
 		const bool enableCustomRamp = (instrRampLength > 0);
 
+		// rewamp: on 16-bit-tempo modules a tick is a HANDFUL of samples and
+		// rows re-trigger every channel, so any ramp longer than a tick never
+		// completes - it amplitude-modulates the whole mix at the row rate
+		// (a loud steady whine at ~1 kHz on "Haesslich"), while no ramp at
+		// all leaves the tick-rate steps unsmoothed (a 16 kHz whistle 20 dB
+		// above XMPlay's render). Capping the ramp to the tick keeps both
+		// numbers down: the ramp completes inside the tick that caused it.
+		// Gated on absurdly short ticks - a normal module never has any.
+		if(m_PlayState.m_nSamplesPerTick < 64 && rampLength > (int32)m_PlayState.m_nSamplesPerTick)
+		{
+			rampLength = globalRampLength = std::max((int32)m_PlayState.m_nSamplesPerTick, (int32)1);
+		}
+
 		if(!rampLength)
 		{
 			rampLength = 1;
Has the bug occurred in previous versions?probably
Tested code revision (in case you know it)

Activities

Saga Musix

Saga Musix

2026-08-16 10:36

administrator   ~0006712

I am aware of this module. JCO made it after I told him about the fact that this is possible at all.

The clamp at 1000 BPM is intentional and won't be changed. OpenMPT's renderer is really not set up to deal with 1-sample ticks properly, the overhead is too big, thus leading to situations that can easily cause excessive CPU consumption that will completely overload the tracker and its UI.

PS: Please don't use LLMs to write bug reports. It's obvious where the prose above is coming from.

Saga Musix

Saga Musix

2026-08-16 10:38

administrator   ~0006713

Also, AI-generated code contributions are not allowed as per our contribution guidelines, just saying.

Issue History

Date Modified Username Field Change
2026-08-16 09:55 yoyofr New Issue
2026-08-16 09:55 yoyofr File Added: 0003-fix-xm-keep-full-16-bit-header-tempo.patch
2026-08-16 09:55 yoyofr File Added: 0004-fix-cap-volume-ramp-at-tick-length-for-16-bit-tempo.patch
2026-08-16 10:36 Saga Musix Note Added: 0006712
2026-08-16 10:38 Saga Musix Note Added: 0006713