Wohlstand / libADLMIDI

A Software MIDI Synthesizer library with OPL3 (YMF262) emulator
GNU Lesser General Public License v3.0
171 stars 17 forks source link

Dead air at the end of tracks in System Shock's thm1.xmi file #134

Closed Interrupt closed 9 months ago

Interrupt commented 6 years ago

I've been working on adding XMI music playback to the Shockolate source port of System Shock and after the last week of commits things are super close to working! The main issue in playback is the ~10 seconds of dead air that plays at the end of the tracks before looping again.

System Shock plays music by muting / unmuting different tracks in the XMI files at times designated by the 119 Controller callback, usually layering multiple tracks at once. With the latest changes things should just work, if the tracks would end and start looping at the correct times.

I've got this work on a branch right now that displays the problem, at https://github.com/Interrupt/systemshock/tree/adlmidi-xmi

jpcima commented 6 years ago

In my few tries, the tracks I played were properly looping.

Example adlmidiplay ~/Documents/Projects/systemshock/res/sound/genmidi/thm1.xmi --solo 3

Also, the CC119 has official support in the master branch since recently. https://github.com/Wohlstand/libADLMIDI/commit/4ac22738e50e20cf00e5b2d0efead4ba3aadb04e

Interrupt commented 6 years ago

The tracks appear to loop just fine when playing in Solo mode, but you can hear the issue with the following setup that only is playing tracks 0, 5, and 33

for(int i = 0; i < 64; i++) {
    adl_setTrackOptions(adlDevice, i, ADLMIDI_TrackOption_Off);
}

adl_setTrackOptions(adlDevice, 0, ADLMIDI_TrackOption_On);
adl_setTrackOptions(adlDevice, 5, ADLMIDI_TrackOption_On);
adl_setTrackOptions(adlDevice, 33, ADLMIDI_TrackOption_On);

Most music in SS1 is played like this with the master track on and a few others layered on, changing over time.

jpcima commented 6 years ago

The problem is reproducible under adlmidiplay with some changes. Here's some information collected so far.

This is a MIDI conversion of the 3 tracks concerned, for analysis. thm1ex.zip

From this, it is seen there are 3 tracks of unequal lengths, which have each an infinite loop (116/117) covering the entirety of tracks. In playback, there is to be a conflict of these loops. (in effective playback, the 3 of these loops register but none is applied, for a reason I don't know)

AIL documentation has this to say:

* Multi-track MIDI Format 1 files are converted to MIDI Format 0 during
  the MIDIFORM program's XMIDI compilation process.  This implies that
  loops placed on a single track will actually cause events in all other
  tracks to be repeated as well.  In situations where it is desired to
  loop tracks independently of each other, a possible solution is to
  split the tracks into multiple XMIDI sequences and have the application
  play each sequence simultaneously.

Suggesting that multiple looping tracks cannot play in a single sequence.

Making a hypothesis, either it's a special handling on software side, or an independent sequencing in the same kind as SMF format 2. If it's the latter case, there is no current support in libADLMIDI's sequencer unfortunately.

For now I will give a look at systemshock music logic, for an attempt to get a better grasp of the whole context.

Interrupt commented 6 years ago

Thanks for looking into this! In SS's case I think it would be fine for loop events on one of the tracks to cause the whole file to loop, since the shortest loop should probably win anyway to avoid causing dead air between sequences.

For context you can look at the AIL calls in https://github.com/Interrupt/systemshock/blob/master/src/GameSrc/mlimbs.c to see how things were being set up originally.

Interrupt commented 6 years ago

Looking into this further it seems like AIL could use Channel Locking to play multiple XMIDI sequence at once, and the loop points in one channel wouldn't interfere with the loop points in another. SS was effectively playing multiple XMIDI songs at once using this then.

I might be able to init multiple ADL_MIDIPlayer instances and emulate that kind of behavior.

Wohlstand commented 6 years ago

I might be able to init multiple ADL_MIDIPlayer instances and emulate that kind of behavior.

Yes, that is possible, but looks "crutchy" (in Russian slang the "Crutch" it's a synonym of a workaround over anything). Alternatively, it's possible to don't use embedded MIDI sequencer and implement own that will use RT-API to pass events and play the result. Otherwise, for XMI playing it's probably going to have completely different sequencer as most of standard MIDI related stuff is not fits into XMI playing.

One note: when you creating multiple ADL_MIDIPlayer instances, you probably would set chip count to 1 per every instance to don't overload the system if you need to process multiple instances in parallel.

Wohlstand commented 5 years ago

Related to "Dead air": I have found similar issue with some of Warcraft 2 XMIs, and that appears randomly on some songs, then, after several tricks the crash happens... The huge empty space (20 minutes) appears at end... Possibly, some junk delta value suddenly appears at the end.

Wohlstand commented 1 year ago

Recently I updated XMI support, and now to switch between tracks you need to use the adl_selectSongNum() call instead of a solo track. Now it's no longer a workaround, it's a full-featured XMI support.

Wohlstand commented 1 year ago

And, at the tool you will need to play adlmidiplay ~/Documents/Projects/systemshock/res/sound/genmidi/thm1.xmi --song 3 Use the --song instead of --solo which means different:

Wohlstand commented 1 year ago

btw, @Interrupt, I see that you made the custom XMI sequencer on the game's side to make the music work properly. However, recently I started to rework the entire XMI support on my own side, and now I have the question: could you explain to me in short how System Shock deals with music? I want to make a very simple demo using this https://github.com/Wohlstand/ail2-sandbox to research the behaviour and implement a similar functionality at my MIDI sequencer (which I share between libADLMIDI, libOPNMIDI, and my MixerX library)

Interrupt commented 1 year ago

Sure! The current sequencer we have is a total hack and doesn't quite work at all, so it would be nice to be able to get rid of that eventually.

System Shock made its final music by layering small loops together, and there were some custom callback events used in the master loop that looked like they were there to sync up the other tracks, as the event always fired on the last note of the master loop if I remember right.

There is a file that is loaded that is used a sequencer for each level, it tells the game which loops to play in which order. There is also a more dynamic element for this as well, the game can switch between different 'alert' levels which will switch out which tracks play. The music generally starts pretty ambient, and then has different high energy loops layer on when the player starts taking damage or attacking an enemy. It will even switch out the action loops depending on which type of enemy you are fighting - fighting a robot in the medical level will play an action loop with more techy robot sounds mixed in, for example.

Wohlstand commented 1 year ago

Could you share some exact layouts/orders and conditions (for example, for the THM1.XMI)? Just, to let me implement the sequence switching mechanism (I'll simulate conditions by pressing buttons)

Interrupt commented 1 year ago

Possibly, I could record the specific loops that play into a log file that we could analyze.

Wohlstand commented 9 months ago

I'll close this as completed. The general improvement of XMI (AIL functionality) support needs a new issue.