libmodplug
libmodplug copied to clipboard
How to convert a "Set Global Volume" mod function to ULT function?
I'm reverse engineering an old DOS game's custom music file format which highly resembles the ULT format, including its functions.
However, the game file also uses the 0x10 function, Set Global Volume. (Essentially, the game's instruction format is note,instrument,func1,func2,func2_param,func1_param bytes.) However, this function code can't fit into an ULT instruction because func1 & func2 are nibbles in the byte. I know Fast Tracker ][ introduced this 0x10 command.
I managed to write a converter in C# that outputs an ULT file (playable by VLC player), however, it doesn't sound right as some tracks are louder or quieter than when the game plays it. Probably because I converted the 0x10 function into 0x0C (Set Volume) incorrectly. (Remembering the 0x10 parameter value and adding it to each subsequent 0x0C parameter use. I tried using the global volume as a maximum and rescaling 0x0C volume amounts relative to it, which resulted in very quiet music.)
My question is, how do I correctly interpret such 0x10 functions and turn them into 0x0C functions? In other terms, if I have 0x10(0x07) and 0x0C(0x20), what would be the equivalent argument y if there was just 0x0C(y).
Thank you for your time.
A couple of things:
- To the best of my knowledge, the ULT format does not support the Set Global Volume effect.
- libmodplug fundamentally can not properly support ULT or ULT-like formats because it does not have two full effects channels. libxmp and libopenmpt can, but that doesn't help with VLC.
- ULT also has some really weird behaviors, especially regarding finetune, and I don't recommend using it as an intermediate format for anything.
Which game format are you trying to convert?
Imperium Galactica 1 (1996) XMF.
The conversion to ULT does work 99%. I know it doesn't support set global volume; I'm trying to emulate it.
So the question still remains: how do I do it? How do other formats, which support 0x10 calculate the actual note volume?
Edit:
Looking through libxmp, I found these:
https://github.com/libxmp/libxmp/blob/master/src/player.c#L309-L310
https://github.com/libxmp/libxmp/blob/master/src/player.c#L989-L991
So either
volume_actual = volume_global * volume_current * other_factors >> 24volume_actual = volume_global * volume_current >> 12
After doing a bit of reverse engineering, it seems that every instance of effect 10h is only used with a parameter in the range from 0-f. Given this and what looks like a channel default panning table that starts at offset 1103h: this is probably not global volume, but rather Gravis UltraSound-style 4-bit panning.
This is pure speculation: the author's previous game Reunion uses MODs, so if this format was based on ULT, Tamas Kreiner may not have liked that Ultra Tracker uses the Bxx effect for panning, so he moved it to 10xx instead. That said, it doesn't seem like any of these modules use Bxx at all.
Since Ultra Tracker uses 4-bit panning, you should just be able to directly convert effect 10xx to Bxx if it really is panning.
Since Ultra Tracker uses 4-bit panning, you should just be able to directly convert effect
10xxtoBxxif it really is panning.
I tried that already. It resulted in an output where each track had its distinct pan position throughout the music, which was completely different how the same music sounds ingame - centered mono.
Reverse engineering the music code, it looked like when 10 is executed, the issues Gravis volume slide commands relative to the current and global volume.
Ghidra Decompiled Code
CX = xmf_sample_count + byte_5ac7
BP = channel index
For each channels
LAB_1010_31d7 XREF[1]: 1010:3251 (j)
1010:31d7 8b f5 MOV SI,BP
1010:31d9 d1 ee SHR SI,0x1
1010:31db 65 80 bc CMP byte ptr GS:[SI + smp_global_volume ],0xff
73 4f ff
1010:31e1 74 14 JZ LAB_1010_31f7
1010:31e3 65 8a 84 MOV AL,byte ptr GS:[SI + smp_volume_curr ]
93 4f
1010:31e8 65 2a 84 SUB AL,byte ptr GS:[SI + smp_global_volume ]
73 4f
1010:31ed 0a c0 OR AL,AL
1010:31ef 79 02 JNS LAB_1010_31f3
1010:31f1 f6 d8 NEG AL
LAB_1010_31f3 XREF[1]: 1010:31ef (j)
1010:31f3 3c 05 CMP AL,0x5
1010:31f5 72 56 JC LAB_1010_324d
LAB_1010_31f7 XREF[1]: 1010:31e1 (j)
1010:31f7 c1 e6 02 SHL SI,0x2
1010:31fa 66 65 83 CMP dword ptr GS:[SI + Smp_Play_Index ],-0x1
bc 33 4e ff
1010:3201 74 4a JZ LAB_1010_324d
1010:3203 65 8b 16 MOV DX,word ptr GS:[SB_Port ] = 240h
ab 5a
1010:3208 81 c2 02 01 ADD DX,0x102
1010:320c 8b c5 MOV AX,BP
1010:320e d1 e8 SHR AX,0x1
GUS Select Voice (BP >> 1)
Base + 0x102
1010:3210 ee OUT DX,AL
1010:3211 42 INC DX
1010:3212 b0 0d MOV AL,0xd
GUS Set Volume Control
Base + 0x103
1010:3214 ee OUT DX,AL
1010:3215 83 c2 02 ADD DX,0x2
1010:3218 b0 03 MOV AL,0x3
Base + 0x105 <- 0x3 (Volume Stopped | Stop Volume)
1010:321a ee OUT DX,AL
1010:321b 83 ea 02 SUB DX,0x2
1010:321e b0 07 MOV AL,0x7
Base + 0x103 <- Set Volume Start
1010:3220 ee OUT DX,AL
1010:3221 42 INC DX
1010:3222 65 a1 98 6b MOV AX,GS:[Volume_Start_W ]
Base + 0x104 <- Voice_Volume_Start Word
1010:3226 ef OUT DX,AX
1010:3227 4a DEC DX
1010:3228 b0 08 MOV AL,0x8
Gus Set Volume End
Base + 0x103 <- 0x08
1010:322a ee OUT DX,AL
1010:322b 42 INC DX
1010:322c 65 8b 86 MOV AX,word ptr GS:[BP + Voice_Volume_End_W ]
47 54
1010:3231 65 3b 06 CMP AX,word ptr GS:[Volume_Table ] = A00h
96 6b
1010:3236 74 15 JZ LAB_1010_324d
1010:3238 65 8b 1e MOV BX,word ptr GS:[Volume_Start_W ]
98 6b
1010:323d 65 89 9e MOV word ptr GS:[BP + Voice_Volume_End_W ],BX
47 54
Base + 0x104 <- Voice_Volume_End
1010:3242 ef OUT DX,AX
1010:3243 4a DEC DX
1010:3244 b0 0d MOV AL,0xd
Base + 0x103 <- Set Volume Control
1010:3246 ee OUT DX,AL
1010:3247 83 c2 02 ADD DX,0x2
1010:324a b0 40 MOV AL,0x40
base + 0x105 <- 0x40 (Volume_Direct)
1010:324c ee OUT DX,AL
LAB_1010_324d XREF[3]: 1010:31f5 (j) , 1010:3201 (j) , 1010:3236 (j)
1010:324d 83 c5 02 ADD BP,0x2
1010:3250 49 DEC CX
1010:3251 75 84 JNZ LAB_1010_31d7
1010:3253 61 POPA
1010:3254 0f a9 POP GS
1010:3256 cf IRET
So if it is for global volume, I'd have to try another mod format that accepts 10 natively. However, FastTracker's XM format looks too convoluted. What would be the closest format to ULT?
I think the following would work for global volume, at least to experiment with. The following would have to be applied to every event with a note or with a volume or 10xx:
if Cxx is used: volume = [ cxx_param * global_volume ] / global_volume_max
else: volume = [ instrument_default_volume * global_volume ] / global_volume_max
Cxx and instrument default volume both look like they're 0-255, so the same formula ought to work...
Reverse engineering the music code, it looked like when
10is executed, the issues Gravis volume slide commands relative to the current and global volume.
OK, I'll try to make sense of this in a little bit.
This disassembly looks like GUS volume ramping. There are a few things that aren't clear currently:
- Where is GUS voice register 0x6 (volume increment) being written? Is this what command
10changes? - How is
Volume_Start_Wand the initial value ofVoice_Volume_End_W[channel]set? - Where does
smp_global_volume[channel]come from?
This command plausibly might be meant to trigger the volume ramps, or to at least control the rate at which they're performed for the current channel.
I'm working on basic support for this format for libxmp, and I've noticed these modules are very quiet. Is that normal?
It's complicated. Here is the Ghidra Project I've been using. Open INTRO.EXE as it is only meant to play game videos, and surprisingly, can play XMF music along provided it is named URES.XMF.
I've noticed these modules are very quiet.
Not that quiet, volume is probably between 0x00 and 0x40. I added the global volume straight (not scaled). MAIN1.ZIP
Addendum to 0x10 and quietness: I'm 99% sure this is Set Global Volume command of FastTracker ][.
More effects info coming when I finish black box testing, but I figured out 10xx. It is Gravis UltraSound panning, and is not implemented for the Sound Blaster driver:

More typical MOD extended panning effects (8xx, E8x) do not work.
Effects testing module with all of the working effects, plus the original MOD and a recording of DOSBox's audio with the Gravis UltraSound driver: xmftest.zip
All of my other effects and sample findings, and the custom MOD to XMF converter I wrote for testing: https://gist.github.com/AliceLR/deeb651c7629c8ee4b8a079f62304827
Given that the official XMFs only use effects Axy, Cxx, EAx, EBx, Fxx, 10xy, and that 10xy is directly compatible with Ultra Tracker Bxy (or can be dropped if you don't want panning), converting to ULT will probably work.