Rayman 1 per-level soundtrack implemented within DOSBox

Discuss tools to aid in the modification and running of Rayman games.

Moderators: English moderators, Modding and utilities team

Forum rules
Please keep the forum rules and guidelines in mind when creating or replying to a topic.
DavidSosa
Ptizêtre Ninja
Posts: 219
Joined: Sun Apr 18, 2021 12:17 am
Location: Lima, Peru
Contact:
Tings: 3455

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by DavidSosa »

why is there so many versions of the tpls, the rayman control panel one can play two tracks at the same time, but the death sound is the short one, and in the tsr, it can´t play two songs at time, but it has the larger death sound, the rcp tpls can be updated so it can also play the larger death sound (or it can´t)?
i hate myself
DavidSosa
Ptizêtre Ninja
Posts: 219
Joined: Sun Apr 18, 2021 12:17 am
Location: Lima, Peru
Contact:
Tings: 3455

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by DavidSosa »

I have a question, in the tplstsr3.cue i founded the yeah sound effect complete, weird because it does not play, it was supposed to play but it cannot? it would be amazing to play it
i hate myself
PluMGMK
Aline Louïa
Posts: 37010
Joined: Fri Jul 31, 2009 9:00 pm
Location: https://www.youtube.com/watch?v=cErgMJSgpv0
Contact:
Tings: 102745

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by PluMGMK »

Well, firstly, the first 50 tracks are copied straight from the PS1 version, so naturally it's there, even if it's unused.

As for why it's unused, you can see the reason in one of my code comments:

Code: Select all

	; Hooks to make the PC version use CD audio where it normally doesn't
	; (but other versions normally do)
	; Actually, screw the exit-sign one. Not only does the fanfare always get
	; cut off for an actual exit sign, but it also gets engaged for certain
	; non-exit-sign events (e.g. beating Mister Sax, using the WINMAP cheat, etc.)
	; mov	edx,[pExitSign1]
	; call	set_hookpoint
	; mov	edx,[pExitSign2]
	; call	set_hookpoint
	mov	edx,[pPerdu]
	call	set_hookpoint
PluMGMK
Aline Louïa
Posts: 37010
Joined: Fri Jul 31, 2009 9:00 pm
Location: https://www.youtube.com/watch?v=cErgMJSgpv0
Contact:
Tings: 102745

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by PluMGMK »

New version of the patch, supporting the "UNPROTECTED" version 1.12:

Code: Select all

diff -Nur dosbox-0.74-3-vanilla/src/dosbox.cpp dosbox-0.74-3/src/dosbox.cpp
--- dosbox-0.74-3-vanilla/src/dosbox.cpp	2022-02-19 12:13:10.176703695 +0000
+++ dosbox-0.74-3/src/dosbox.cpp	2023-03-26 12:29:21.977752653 +0100
@@ -1,5 +1,6 @@
 /*
  *  Copyright (C) 2002-2010  The DOSBox Team
+ *  Modified 2019, 2023 by PluMGMK to include Rayman soundtrack code.
  *
  *  This program is free software; you can redistribute it and/or modify
  *  it under the terms of the GNU General Public License as published by
@@ -114,6 +115,10 @@
 
 void INT10_Init(Section*);
 
+/* PluM's soundtrack thingy */
+void RAYMAN_Init(Section*);
+bool HandleRaymanSoundtrack();
+
 static LoopHandler * loop;
 
 bool SDLNetInited;
@@ -142,6 +147,7 @@
 #endif
 		} else {
 			GFX_Events();
+			HandleRaymanSoundtrack();
 			if (ticksRemain>0) {
 				TIMER_AddTick();
 				ticksRemain--;
@@ -724,6 +730,19 @@
 	Pstring = Pmulti_remain->GetSection()->Add_string("parameters",Property::Changeable::WhenIdle,"");
 	Pmulti_remain->Set_help("see serial1");
 
+	// PluM's Rayman addition
+	secprop=control->AddSection_prop("rayman",&RAYMAN_Init,false);//done
+	const char* rayvers[] = { "auto", "1.00", "1.10", "1.12.0", "1.12.1", "1.12_Unprotected", "1.12.2", "1.20", "1.21", "1.21_Chinese",0};
+	Pstring = secprop->Add_string("gameversion",Property::Changeable::OnlyAtStart,"auto");
+	Pstring->Set_values(rayvers);
+	Pstring->Set_help(
+		"Rayman version you plan to run: auto (default),\n"
+		"1.00, 1.10, 1.12.0, 1.12.1, 1.12.2, 1.20, 1.21,\n"
+		"1.12_Unprotected or 1.21_Chinese.\n"
+		"auto can detect 1.12.0, 1.12_Unprotected, 1.20 or 1.21.\n");
+	Pstring = secprop->Add_path("musicfile",Property::Changeable::OnlyAtStart,"Music.dat");
+	Pstring->Set_help("Path (relative or absolute) to Music.dat file containing full Rayman soundtrack");
+
 
 	/* All the DOS Related stuff, which will eventually start up in the shell */
 	secprop=control->AddSection_prop("dos",&DOS_Init,false);//done
diff -Nur dosbox-0.74-3-vanilla/src/misc/Makefile.am dosbox-0.74-3/src/misc/Makefile.am
--- dosbox-0.74-3-vanilla/src/misc/Makefile.am	2022-02-19 12:13:10.209703826 +0000
+++ dosbox-0.74-3/src/misc/Makefile.am	2023-03-26 12:29:21.977752653 +0100
@@ -1,4 +1,4 @@
 AM_CPPFLAGS = -I$(top_srcdir)/include
 
 noinst_LIBRARIES = libmisc.a
-libmisc_a_SOURCES = cross.cpp messages.cpp programs.cpp setup.cpp support.cpp
+libmisc_a_SOURCES = cross.cpp messages.cpp programs.cpp setup.cpp support.cpp rayman_soundtrack.cpp
diff -Nur dosbox-0.74-3-vanilla/src/misc/Makefile.in dosbox-0.74-3/src/misc/Makefile.in
--- dosbox-0.74-3-vanilla/src/misc/Makefile.in	2022-02-19 12:13:10.209703826 +0000
+++ dosbox-0.74-3/src/misc/Makefile.in	2023-03-26 12:29:21.978752660 +0100
@@ -109,7 +109,7 @@
 libmisc_a_AR = $(AR) $(ARFLAGS)
 libmisc_a_LIBADD =
 am_libmisc_a_OBJECTS = cross.$(OBJEXT) messages.$(OBJEXT) \
-	programs.$(OBJEXT) setup.$(OBJEXT) support.$(OBJEXT)
+	programs.$(OBJEXT) setup.$(OBJEXT) support.$(OBJEXT) rayman_soundtrack.$(OBJEXT)
 libmisc_a_OBJECTS = $(am_libmisc_a_OBJECTS)
 AM_V_P = $(am__v_P_@AM_V@)
 am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
@@ -277,7 +277,7 @@
 top_srcdir = @top_srcdir@
 AM_CPPFLAGS = -I$(top_srcdir)/include
 noinst_LIBRARIES = libmisc.a
-libmisc_a_SOURCES = cross.cpp messages.cpp programs.cpp setup.cpp support.cpp
+libmisc_a_SOURCES = cross.cpp messages.cpp programs.cpp setup.cpp support.cpp rayman_soundtrack.cpp
 all: all-am
 
 .SUFFIXES:
@@ -331,6 +331,7 @@
 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/programs.Po@am__quote@
 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/setup.Po@am__quote@
 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/support.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rayman_soundtrack.Po@am__quote@
 
 .cpp.o:
 @am__fastdepCXX_TRUE@	$(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
diff -Nur dosbox-0.74-3-vanilla/src/misc/rayman_soundtrack.cpp dosbox-0.74-3/src/misc/rayman_soundtrack.cpp
--- dosbox-0.74-3-vanilla/src/misc/rayman_soundtrack.cpp	1970-01-01 01:00:00.000000000 +0100
+++ dosbox-0.74-3/src/misc/rayman_soundtrack.cpp	2023-03-26 13:28:56.413519224 +0100
@@ -0,0 +1,1219 @@
+/*
+ *  This Rayman soundtrack implementation code Copyright (C) 2019, 2023 PluMGMK
+ *  Based on DOSBox, Copyright (C) 2002-2010  The DOSBox Team
+ *  Incorporating LGPL code from SDL - Simple DirectMedia Layer,
+ *  	Copyright (C) 1997-2012 Sam Lantinga
+ *  Most logic based on TPLS, created by Snagglebee and included in
+ *  	MIT-licensed Rayman Control Panel, Copyright (c) 2019 RayCarrot
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include "dosbox.h"
+#include "mem.h"
+#include "mixer.h"
+#include "SDL.h"
+#include "SDL_thread.h"
+#include "SDL_sound.h"
+#include "setup.h" // For Section_prop
+#include "control.h" // For control->cmdline
+#include <cstring>
+#include <stdlib.h>
+
+// Rayman Versions
+#define RAY_AUTOVER 0
+#define RAY_1_00    1
+#define RAY_1_10    2
+#define RAY_1_12_0  3
+#define RAY_1_12_1  4
+#define RAY_1_12_2  5
+#define RAY_1_20    6
+#define RAY_1_21    7
+#define RAY_1_21_CN 8
+#define RAY_1_12_U  9	// The "UNPROTECTED" version
+
+static unsigned char gRayVer = 0;
+// Offsets associated with the current Rayman version...
+static PhysPt gRayWorldBase;
+static PhysPt gRayLevelOffset;
+static PhysPt gRayInLevelOffset;
+static PhysPt gRayMusOnOffset;
+static PhysPt gRayOptionsOnOffset;
+static PhysPt gRayOptionsOffOffset;
+static PhysPt gRayBossEventOffset;
+static PhysPt gRayXOffset;
+static PhysPt gRayYOffset;
+// Info on the actual game state
+static char gRayWorld[8];
+static char gRayLevel[8];
+static Bit8u gRayInLevel;
+static Bit8u gRayMusOn;
+static Bit8u gRayOptionsOn;
+static Bit8u gRayOptionsOff;
+static Bit8u gRayBossEvent;
+static Bit16u gRayX;
+static Bit16u gRayY;
+
+// Express world and level as integers to make things a bit easier...
+inline bool RaySanityCheck(char *name) { return ((name[0] == 'R') && (name[1] == 'A') && (name[2] == 'Y')); }
+inline int RayWorldNumber() { return RaySanityCheck(gRayWorld) ? std::atoi(gRayWorld+3) : 0; }
+inline int RayLevelNumber() { return RaySanityCheck(gRayLevel) ? std::atoi(gRayLevel+3) : 0; }
+
+// Location of Music.dat file
+static std::string gRayMusPath;
+// Offsets and lengths of the Ogg files for the current track, within the Music.dat file
+static int gRaySoundtrackOffsets[2];
+static int gRaySoundtrackLengths[2];
+// Other info about the currently-playing soundtrack(s)
+static unsigned char gRayNumCurSoundtracks = 0;
+static unsigned char gRayCurSoundtrackIdx = 0;
+static bool gRayPosDep = false;
+static bool gRaySoundtrackPaused = false;
+static int gRayFadeAfterSamples = 0;
+static int gRayFadeDurationSamples = 0;
+
+// Low-level sound stuff...
+#define RAY_MAX_STRACKS 2
+static MixerChannel *gRayChannel = NULL;
+static Sound_Sample *gRaySamples[RAY_MAX_STRACKS];
+static SDL_mutex *gRayMutex = NULL;
+
+// Additional MIDI soundtracks.
+static int gRayAddStrackOffset = 0;
+static int gRayAddStrackLength = 0;
+static MixerChannel *gRayAddChannel = NULL;
+static Sound_Sample *gRayAddSample = NULL;
+static SDL_mutex *gRayAddMutex = NULL;
+
+// Custom RWops functions for reading specific sections of Music.dat
+typedef struct RayWrapRWops {
+	SDL_RWops *underlying;
+	int offset;
+	int length;
+} RayWrapRWops;
+
+#define RAY_TROFFSET(ctx)	((RayWrapRWops*)(ctx)->hidden.unknown.data1)->offset
+#define RAY_TRLENGTH(ctx)	((RayWrapRWops*)(ctx)->hidden.unknown.data1)->length
+#define RAY_UNDERLYING(ctx)	((RayWrapRWops*)(ctx)->hidden.unknown.data1)->underlying
+
+static Uint32 gRayRWtype = ('P'<< 3)+('L'<< 2)+('U'<< 1)+'M';
+
+static int SDLCALL RaymanSeek(SDL_RWops *context, int offset, int whence) {
+	// Offset definitions...
+	int startpos = RAY_TROFFSET(context);
+	int endpos = startpos + RAY_TRLENGTH(context);
+	int newpos;
+	switch (whence) {
+		case RW_SEEK_SET:
+			// Relative to the start of our music "file".
+			newpos = startpos + offset;
+			break;
+		case RW_SEEK_CUR:
+			// No need to change anything.
+			newpos = SDL_RWtell(RAY_UNDERLYING(context)) + offset;
+			break;
+		case RW_SEEK_END:
+			// Relative to the end of our music "file".
+			newpos = endpos + offset;
+			break;
+		default:
+			SDL_SetError("Unknown value for 'whence'");
+			return(-1);
+	}
+	if ( (newpos >= startpos) && (newpos <= endpos) && // Sanity check first...
+		       	SDL_RWseek(RAY_UNDERLYING(context), newpos, RW_SEEK_SET) >= 0 ) {
+		return(SDL_RWtell(RAY_UNDERLYING(context)) - startpos);
+	} else {
+		SDL_Error(SDL_EFSEEK);
+		return(-1);
+	}
+}
+static int SDLCALL RaymanRead(SDL_RWops *context, void *ptr, int size, int maxnum)
+{
+	// Offset definitions...
+	int endpos = RAY_TROFFSET(context) + RAY_TRLENGTH(context);
+	int curpos = SDL_RWtell(RAY_UNDERLYING(context));
+	if((curpos + maxnum) > endpos)
+		// Clamp within the extent of our music "file".
+		maxnum = endpos - curpos;
+
+	size_t nread;
+
+	nread = SDL_RWread(RAY_UNDERLYING(context), ptr, size, maxnum); 
+	return(nread);
+}
+static int SDLCALL RaymanWrite(SDL_RWops *context, const void *ptr, int size, int maxnum)
+{
+	LOG_MSG("Something's trying to write to Music.dat - https://xkcd.com/2200/");
+	// Make the caller happy anyway...
+	return(maxnum);
+}
+static int SDLCALL RaymanClose(SDL_RWops *context)
+{
+	if ( context ) {
+		if(context->type != gRayRWtype) {
+			SDL_SetError("Wrong kind of SDL_RWops for RaymanClose()");
+			return 0;
+		}
+
+		SDL_FreeRW(RAY_UNDERLYING(context));
+		SDL_free(context->hidden.unknown.data1);
+		SDL_FreeRW(context);
+	}
+	return(0);
+}
+
+// Stuff for hijacking the CD player
+static float gRayDesiredVol[2] = {1, 1};
+void GagCDAudio() {
+	MixerChannel *CDchan = MIXER_FindChannel("CDAUDIO");
+
+	if (!CDchan)
+		return;
+
+	// Make sure it's not already gagged.
+	if ((CDchan->volmain[0] == 0) && (CDchan->volmain[1] == 0))
+		return;
+
+	// Save the volume.
+	gRayDesiredVol[0] = CDchan->volmain[0];
+	gRayDesiredVol[1] = CDchan->volmain[1];
+
+	// Mute the channel.
+	CDchan->SetVolume(0,0);
+}
+
+void UngagCDAudio() {
+	MixerChannel *CDchan = MIXER_FindChannel("CDAUDIO");
+
+	if (!CDchan)
+		return;
+
+	// Make sure it's actually gagged first.
+	if((CDchan->volmain[0] != 0) || (CDchan->volmain[1] != 0))
+		return;
+
+	// Restore the saved volume.
+	CDchan->SetVolume(gRayDesiredVol[0], gRayDesiredVol[1]);
+}
+
+// Main logic
+#define CUR_SAMPLE gRaySamples[gRayCurSoundtrackIdx-1]
+void RayAdvanceSoundtrack() {
+	if(gRayCurSoundtrackIdx < gRayNumCurSoundtracks)
+		// Advance to the next soundtrack.
+		gRayCurSoundtrackIdx++;
+	
+	// Move it to the start so we're ready to play it...
+	if (CUR_SAMPLE)
+		Sound_Seek(CUR_SAMPLE, 0);
+}
+
+void RayStopAddStrack() {
+	LOG_MSG("Stopping additional Rayman soundtrack");
+
+	// Stop our audio.
+	gRayAddChannel->Enable(false);
+
+	SDL_mutexP(gRayAddMutex);
+	Sound_FreeSample(gRayAddSample);
+	gRayAddSample = NULL;
+	SDL_mutexV(gRayAddMutex);
+}
+
+void RayStopSoundtrack() {
+	LOG_MSG("Stopping custom Rayman soundtrack");
+
+	// We're not paused, we're stopped!
+	gRaySoundtrackPaused = false;
+	gRayCurSoundtrackIdx = 0;
+
+	// Stop our audio and restore native CD audio.
+	gRayChannel->Enable(false);
+	UngagCDAudio();
+
+	// Iterate over all the non-null samples and delete them.
+	SDL_mutexP(gRayMutex);
+	for (int i=0; gRaySamples[i] && (i < RAY_MAX_STRACKS); i++) {
+		Sound_FreeSample(gRaySamples[i]);
+		gRaySamples[i] = NULL;
+	}
+	SDL_mutexV(gRayMutex);
+
+	// Also stop any additional soundtrack if necessary.
+	if(gRayAddSample)
+		RayStopAddStrack();
+}
+
+void RaySoundtrackCallBack(Bitu len)
+{
+	// Handle fading first
+	if(gRayFadeAfterSamples)
+		gRayFadeAfterSamples -= len;
+	else if (gRayFadeDurationSamples) {
+		float FadeStep = len / (1.0 * gRayFadeDurationSamples);
+		float VolSteps[2] = {FadeStep * gRayDesiredVol[0],
+			FadeStep * gRayDesiredVol[1]};
+		float NewVols[2] = {gRayChannel->volmain[0] - VolSteps[0],
+			gRayChannel->volmain[1] - VolSteps[0]};
+		if(NewVols[0] <= 0 || NewVols[1] <= 0) {
+			// Fade complete!
+			RayStopSoundtrack();
+			gRayFadeDurationSamples = 0;
+			return;
+		} else
+			gRayChannel->SetVolume(NewVols[0], NewVols[1]);
+	}
+
+	if(gRayFadeAfterSamples < 0)
+		gRayFadeAfterSamples = 0;
+
+	len *= 4;       // 16 bit, stereo
+	if (!len) return;
+	if (!gRayCurSoundtrackIdx || gRaySoundtrackPaused) {
+		gRayChannel->AddSilence();
+		return;
+	}
+
+	SDL_mutexP(gRayMutex);
+	if (CUR_SAMPLE) 
+	{
+		Sound_SetBufferSize(CUR_SAMPLE, len);
+		int bytes = Sound_Decode(CUR_SAMPLE);
+		bool success = (bytes == len);
+#if defined(WORDS_BIGENDIAN)
+		gRayChannel->AddSamples_s16_nonnative((success?len:bytes)/4,(Bit16s *)(CUR_SAMPLE->buffer));
+#else
+		gRayChannel->AddSamples_s16((success?len:bytes)/4,(Bit16s *)(CUR_SAMPLE->buffer));
+#endif
+		if(!success) {
+			RayAdvanceSoundtrack();
+			gRayChannel->AddSilence();
+		}
+	} else {
+		// Rudimentary error handling...
+		RayAdvanceSoundtrack();
+		gRayChannel->AddSilence();
+	}
+	SDL_mutexV(gRayMutex);
+}
+
+void RayAddStrackCallBack(Bitu len)
+{
+	// Handle fading first - piggy-back on main soundtrack
+	if (gRayFadeDurationSamples) {
+		gRayAddChannel->SetVolume(gRayChannel->volmain[0], gRayChannel->volmain[1]);
+	}
+
+	len *= 4;       // 16 bit, stereo
+	if (!len) return;
+	if (!gRayAddStrackLength || gRaySoundtrackPaused) {
+		gRayAddChannel->AddSilence();
+		return;
+	}
+
+	SDL_mutexP(gRayAddMutex);
+	if (gRayAddSample) 
+	{
+		Sound_SetBufferSize(gRayAddSample, len);
+		int bytes = Sound_Decode(gRayAddSample);
+		bool success = (bytes == len);
+#if defined(WORDS_BIGENDIAN)
+		gRayAddChannel->AddSamples_s16_nonnative((success?len:bytes)/4,(Bit16s *)(gRayAddSample->buffer));
+#else
+		gRayAddChannel->AddSamples_s16((success?len:bytes)/4,(Bit16s *)(gRayAddSample->buffer));
+#endif
+		if(!success) {
+			Sound_Seek(gRayAddSample, 0);
+			gRayAddChannel->AddSilence();
+		}
+	} else {
+		// Rudimentary error handling...
+		gRayAddChannel->AddSilence();
+	}
+	SDL_mutexV(gRayAddMutex);
+}
+
+static Sound_Sample *Sound_SampleFromRayMusic(int offset, int length) {
+	// Need a custom RWops to read only a specific part of Music.dat
+	// This approach involves multiple pointers to the same file when we've multiple active soundtracks...
+	SDL_RWops *underlying = SDL_RWFromFile(gRayMusPath.c_str(), "rb");
+	if(!underlying) {
+		LOG_MSG("Unable to open Music.dat (%s) - aborting", SDL_GetError());
+		return NULL;
+	}
+	// First seek to the beginning of our "file".
+	SDL_RWseek(underlying, offset, RW_SEEK_SET);
+
+	// Now create *another* RWops to wrap this one...
+	SDL_RWops *myrwops = SDL_AllocRW();
+	myrwops->type = gRayRWtype;
+
+	// Use the "unknown" part to define the offset and length of the current track.
+	myrwops->hidden.unknown.data1 = SDL_malloc(sizeof(RayWrapRWops));
+	RAY_TROFFSET(myrwops) = offset;
+	RAY_TRLENGTH(myrwops) = length;
+	RAY_UNDERLYING(myrwops) = underlying;
+
+	// Now we have to define custom read and seek functions...
+	myrwops->seek = RaymanSeek;
+	myrwops->read = RaymanRead;
+	myrwops->close = RaymanClose;
+	myrwops->write = RaymanWrite; // Shouldn't be needed...
+
+	Sound_AudioInfo desired = {AUDIO_S16, 2, 44100};
+	return Sound_NewSample(myrwops, "ogg", &desired, 2352);
+}
+
+void RayActivateAddStrack() {
+	LOG_MSG("Activating additional Rayman soundtrack");
+
+	Sound_Sample *mysample = Sound_SampleFromRayMusic(gRayAddStrackOffset, gRayAddStrackLength);
+	if(!mysample) 
+		return;
+
+	SDL_mutexP(gRayAddMutex);
+	gRayAddSample = mysample;
+	SDL_mutexV(gRayAddMutex);
+
+	// Enable our own audio and bring it up to the volume the CD audio had...
+	gRayAddChannel->Enable(true);
+	gRayAddChannel->SetVolume(gRayDesiredVol[0], gRayDesiredVol[1]);
+}
+
+void RayActivateSoundtrack() {
+	LOG_MSG("Activating custom Rayman soundtrack");
+
+	// Cut short any fades.
+	if(gRayFadeAfterSamples || gRayFadeDurationSamples) {
+		gRayFadeAfterSamples = gRayFadeDurationSamples = 0;
+		RayStopSoundtrack();
+	}
+
+	// Mute native CD audio...
+	GagCDAudio();
+
+	// If I'm already active, just interpret this as an unpause command...
+	if(gRayCurSoundtrackIdx) {
+		gRaySoundtrackPaused = false;
+		return;
+	}
+
+	for (int j=0; j<gRayNumCurSoundtracks; j++) {
+		Sound_Sample *mysample = Sound_SampleFromRayMusic(gRaySoundtrackOffsets[j], gRaySoundtrackLengths[j]);
+		if(!mysample) {
+			UngagCDAudio();
+			return;
+		}
+
+		SDL_mutexP(gRayMutex);
+		gRaySamples[j] = mysample;
+		SDL_mutexV(gRayMutex);
+	}
+
+	// Start with the first soundtrack
+	gRayCurSoundtrackIdx = 1;
+
+	// Enable our own audio and bring it up to the volume the CD audio had...
+	gRayChannel->Enable(true);
+	gRayChannel->SetVolume(gRayDesiredVol[0], gRayDesiredVol[1]);
+
+	// Also activate any additional soundtrack if necessary.
+	if(gRayAddStrackLength)
+		RayActivateAddStrack();
+}
+
+void RayPauseSoundtrack() {
+	LOG_MSG("Pausing custom Rayman soundtrack");
+	gRaySoundtrackPaused = true;
+	UngagCDAudio();
+}
+
+void RayFadeOutSoundtrack() {
+	LOG_MSG("Fading custom Rayman soundtrack");
+	gRayFadeAfterSamples = 44; // ~1 ms with 44100 Hz audio
+	gRayFadeDurationSamples = 44100; // 1 s
+	gRayPosDep = false;
+}
+
+void RayChooseSoundtrack() {
+	// Default for most levels:
+	gRayPosDep = false;
+	gRayAddStrackOffset = gRayAddStrackLength = 0;
+
+	switch (RayWorldNumber()) {
+		case 1:
+			switch (RayLevelNumber()) {
+				case 1:
+				case 5:
+				case 12:
+					gRaySoundtrackOffsets[0] = 31720237;
+					gRaySoundtrackOffsets[1] = 29542498;
+					gRaySoundtrackLengths[0] = 1222633;
+					gRaySoundtrackLengths[1] = 2177739;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 2:
+					gRayAddStrackOffset = 16214875;
+					gRayAddStrackLength = 1661628;
+					// Fallthrough since level 13 has same base soundtrack
+				case 13:
+					gRaySoundtrackOffsets[0] = 22786573;
+					gRaySoundtrackOffsets[1] = 20390262;
+					gRaySoundtrackLengths[0] = 656206;
+					gRaySoundtrackLengths[1] = 2396311;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 4:
+				case 10:
+				case 11:
+					gRaySoundtrackOffsets[0] = 28460009;
+					gRaySoundtrackOffsets[1] = 26510592;
+					gRaySoundtrackLengths[0] = 1082489;
+					gRaySoundtrackLengths[1] = 1949417;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 6:
+					gRayPosDep = true;
+					if(gRayY < 830) {
+						gRaySoundtrackOffsets[0] = 22786573;
+						gRaySoundtrackOffsets[1] = 20390262;
+						gRaySoundtrackLengths[0] = 656206;
+						gRaySoundtrackLengths[1] = 2396311;
+					} else if (gRayY > 830) {
+						gRaySoundtrackOffsets[0] = 25130666;
+						gRaySoundtrackOffsets[1] = 23442779;
+						gRaySoundtrackLengths[0] = 1379926;
+						gRaySoundtrackLengths[1] = 1687887;
+					}
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 7:
+					gRayPosDep = true;
+					if(gRayX < 4850 || gRayX > 9250) {
+						gRaySoundtrackOffsets[0] = 35162640;
+						gRaySoundtrackOffsets[1] = 32942870;
+						gRaySoundtrackLengths[0] = 469006;
+						gRaySoundtrackLengths[1] = 2219770;
+						gRayNumCurSoundtracks = 2;
+					} else if (gRayX > 4850) {
+						gRaySoundtrackOffsets[0] = 35631646;
+						gRaySoundtrackLengths[0] = 942193;
+						gRayNumCurSoundtracks = 1;
+					}
+					break;
+				case 9:
+					gRayPosDep = true;
+					if(gRayY < 2650) {
+						gRaySoundtrackOffsets[0] = 45334864;
+						gRaySoundtrackOffsets[1] = 43185671;
+						gRaySoundtrackLengths[0] = 993058;
+						gRaySoundtrackLengths[1] = 2149193;
+					} else if (gRayY > 2650) {
+						gRaySoundtrackOffsets[0] = 22786573;
+						gRaySoundtrackOffsets[1] = 20390262;
+						gRaySoundtrackLengths[0] = 656206;
+						gRaySoundtrackLengths[1] = 2396311;
+					}
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 14:
+					gRaySoundtrackOffsets[0] = 87789323;
+					gRaySoundtrackOffsets[1] = 85872167;
+					gRaySoundtrackLengths[0] = 560786;
+					gRaySoundtrackLengths[1] = 1917156;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 15:
+					gRaySoundtrackOffsets[0] = 35162640;
+					gRaySoundtrackOffsets[1] = 32942870;
+					gRaySoundtrackLengths[0] = 469006;
+					gRaySoundtrackLengths[1] = 2219770;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 16:
+					gRaySoundtrackOffsets[0] = 37576545;
+					gRaySoundtrackOffsets[1] = 36573839;
+					gRaySoundtrackLengths[0] = 1210122;
+					gRaySoundtrackLengths[1] = 1002706;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 3:
+				case 8:
+				case 17:
+				case 18:
+				case 19:
+				case 20:
+				case 21:
+				case 22:
+				default:
+					// Betilla/Magician level, so the normal game music is OK.
+					gRayNumCurSoundtracks = 0;
+			}
+			break;
+		case 2:
+			switch (RayLevelNumber()) {
+				case 1:
+				case 8:
+					gRaySoundtrackOffsets[0] = 63268113;
+					gRaySoundtrackOffsets[1] = 60977646;
+					gRaySoundtrackLengths[0] = 495197;
+					gRaySoundtrackLengths[1] = 2290467;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 2:
+				case 5:
+					gRaySoundtrackOffsets[0] = 54715946;
+					gRaySoundtrackOffsets[1] = 52302652;
+					gRaySoundtrackLengths[0] = 972212;
+					gRaySoundtrackLengths[1] = 2413294;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 3:
+					gRaySoundtrackOffsets[0] = 48840974;
+					gRaySoundtrackOffsets[1] = 46327922;
+					gRaySoundtrackLengths[0] = 931093;
+					gRaySoundtrackLengths[1] = 2513052;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 4:
+					gRayAddStrackOffset = 13763444;
+					gRayAddStrackLength = 634589;
+					gRaySoundtrackOffsets[0] = 72360982;
+					gRaySoundtrackOffsets[1] = 71234026;
+					gRaySoundtrackLengths[0] = 368425;
+					gRaySoundtrackLengths[1] = 1126956;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 6:
+					gRaySoundtrackOffsets[0] = 68284466;
+					gRaySoundtrackOffsets[1] = 66895587;
+					gRaySoundtrackLengths[0] = 999993;
+					gRaySoundtrackLengths[1] = 1388879;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 7:
+				case 9:
+					gRaySoundtrackOffsets[0] = 70729506;
+					gRaySoundtrackOffsets[1] = 69284459;
+					gRaySoundtrackLengths[0] = 504520;
+					gRaySoundtrackLengths[1] = 1445047;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 10:
+				case 14:
+					gRaySoundtrackOffsets[0] = 51971460;
+					gRaySoundtrackOffsets[1] = 49772067;
+					gRaySoundtrackLengths[0] = 331192;
+					gRaySoundtrackLengths[1] = 2199393;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 12:
+					gRaySoundtrackOffsets[0] = 57329229;
+					gRaySoundtrackOffsets[1] = 55688158;
+					gRaySoundtrackLengths[0] = 878792;
+					gRaySoundtrackLengths[1] = 1641071;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 13:
+					gRaySoundtrackOffsets[0] = 65467614;
+					gRaySoundtrackOffsets[1] = 63763310;
+					gRaySoundtrackLengths[0] = 1427973;
+					gRaySoundtrackLengths[1] = 1704304;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 15:
+				case 16:
+					gRaySoundtrackOffsets[0] = 60084178;
+					gRaySoundtrackOffsets[1] = 58208021;
+					gRaySoundtrackLengths[0] = 893468;
+					gRaySoundtrackLengths[1] = 1876157;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 11:
+				case 17:
+				case 18:
+				default:
+					// Betilla/Magician level, so the normal game music is OK.
+					gRayNumCurSoundtracks = 0;
+			}
+			break;
+		case 3:
+			switch (RayLevelNumber()) {
+				case 1:
+				case 5:
+					gRayAddStrackOffset = 17876503;
+					gRayAddStrackLength = 1569546;
+					// Fallthrough since level 6 has same base soundtrack
+				case 6:
+					gRaySoundtrackOffsets[0] = 82013734;
+					gRaySoundtrackOffsets[1] = 80248120;
+					gRaySoundtrackLengths[0] = 319362;
+					gRaySoundtrackLengths[1] = 1765614;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 2:
+					gRayPosDep = true;
+					if((1930 < gRayX && gRayX < 4525) || (5670 < gRayX && gRayX < 6670)) {
+						gRayAddStrackOffset = 19446049;
+						gRayAddStrackLength = 184365;
+					}
+					gRaySoundtrackOffsets[0] = 82013734;
+					gRaySoundtrackOffsets[1] = 80248120;
+					gRaySoundtrackLengths[0] = 319362;
+					gRaySoundtrackLengths[1] = 1765614;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 3:
+					gRaySoundtrackOffsets[0] = 75280276;
+					gRaySoundtrackOffsets[1] = 72729407;
+					gRaySoundtrackLengths[0] = 681754;
+					gRaySoundtrackLengths[1] = 2550869;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 4:
+					gRaySoundtrackOffsets[0] = 84951434;
+					gRaySoundtrackOffsets[1] = 82333096;
+					gRaySoundtrackLengths[0] = 920733;
+					gRaySoundtrackLengths[1] = 2618338;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 7:
+					gRaySoundtrackOffsets[0] = 87789323;
+					gRaySoundtrackOffsets[1] = 85872167;
+					gRaySoundtrackLengths[0] = 560786;
+					gRaySoundtrackLengths[1] = 1917156;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 8:
+					gRaySoundtrackOffsets[0] = 41987417;
+					gRaySoundtrackOffsets[1] = 38786667;
+					gRaySoundtrackLengths[0] = 1198254;
+					gRaySoundtrackLengths[1] = 3200750;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 9:
+					gRayAddStrackOffset = 17876503;
+					gRayAddStrackLength = 1569546;
+					gRaySoundtrackOffsets[0] = 89538037;
+					gRaySoundtrackOffsets[1] = 88350109;
+					gRaySoundtrackLengths[0] = 283812;
+					gRaySoundtrackLengths[1] = 1187928;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 10:
+					gRaySoundtrackOffsets[0] = 78535486;
+					gRaySoundtrackOffsets[1] = 77089354;
+					gRaySoundtrackLengths[0] = 1712634;
+					gRaySoundtrackLengths[1] = 1446132;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 11:
+				case 12:
+				case 13:
+				default:
+					// Betilla/Magician level, so the normal game music is OK.
+					gRayNumCurSoundtracks = 0;
+			}
+			break;
+		case 4:
+			switch (RayLevelNumber()) {
+				case 1:
+					gRayAddStrackOffset = 19630414;
+					gRayAddStrackLength = 759848;
+					gRaySoundtrackOffsets[0] = 94340731;
+					gRaySoundtrackOffsets[1] = 92766271;
+					gRaySoundtrackLengths[0] = 275891;
+					gRaySoundtrackLengths[1] = 1574460;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 2:
+				case 5:
+					gRaySoundtrackOffsets[0] = 99668617;
+					gRaySoundtrackOffsets[1] = 97779195;
+					gRaySoundtrackLengths[0] = 768697;
+					gRaySoundtrackLengths[1] = 1889422;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 3:
+				case 7:
+					gRaySoundtrackOffsets[0] = 91952950;
+					gRaySoundtrackOffsets[1] = 89821849;
+					gRaySoundtrackLengths[0] = 813321;
+					gRaySoundtrackLengths[1] = 2131101;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 4:
+					gRaySoundtrackOffsets[0] = 102810820;
+					gRaySoundtrackOffsets[1] = 100437314;
+					gRaySoundtrackLengths[0] = 311316;
+					gRaySoundtrackLengths[1] = 2373506;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 6:
+				case 8:
+					gRaySoundtrackOffsets[0] = 97313075;
+					gRaySoundtrackOffsets[1] = 94616622;
+					gRaySoundtrackLengths[0] = 466120;
+					gRaySoundtrackLengths[1] = 2696453;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 9:
+					gRayAddStrackOffset = 19630414;
+					gRayAddStrackLength = 759848;
+					gRaySoundtrackOffsets[0] = 72360982;
+					gRaySoundtrackOffsets[1] = 71234026;
+					gRaySoundtrackLengths[0] = 368425;
+					gRaySoundtrackLengths[1] = 1126956;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 10:
+					gRaySoundtrackOffsets[0] = 87789323;
+					gRaySoundtrackOffsets[1] = 85872167;
+					gRaySoundtrackLengths[0] = 560786;
+					gRaySoundtrackLengths[1] = 1917156;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 11:
+					gRaySoundtrackOffsets[0] = 105417855;
+					gRaySoundtrackOffsets[1] = 103122136;
+					gRaySoundtrackLengths[0] = 1364821;
+					gRaySoundtrackLengths[1] = 2295719;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 12:
+				case 13:
+				default:
+					// Magician level, so the normal game music is OK.
+					gRayNumCurSoundtracks = 0;
+			}
+			break;
+		case 5:
+			switch (RayLevelNumber()) {
+				case 1:
+					gRaySoundtrackOffsets[0] = 112108237;
+					gRaySoundtrackOffsets[1] = 109625356;
+					gRaySoundtrackLengths[0] = 315572;
+					gRaySoundtrackLengths[1] = 2482881;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 2:
+				case 5:
+				case 7:
+					gRaySoundtrackOffsets[0] = 2072694;
+					gRaySoundtrackOffsets[1] = 224859;
+					gRaySoundtrackLengths[0] = 533234;
+					gRaySoundtrackLengths[1] = 1847835;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 3:
+				case 8:
+					gRaySoundtrackOffsets[0] = 117543817;
+					gRaySoundtrackOffsets[1] = 115202576;
+					gRaySoundtrackLengths[0] = 522661;
+					gRaySoundtrackLengths[1] = 2341241;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 6:
+					gRayAddStrackOffset = 14398033;
+					gRayAddStrackLength = 1816842;
+					// Fallthrough since level 4 has same base soundtrack
+				case 4:
+					gRaySoundtrackOffsets[0] = 0; // (!)
+					gRaySoundtrackOffsets[1] = 118066478;
+					gRaySoundtrackLengths[0] = 224859;
+					gRaySoundtrackLengths[1] = 2257449;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 9:
+					gRaySoundtrackOffsets[0] = 108802337;
+					gRaySoundtrackOffsets[1] = 106782676;
+					gRaySoundtrackLengths[0] = 823019;
+					gRaySoundtrackLengths[1] = 2019661;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 10:
+				case 11:
+					gRaySoundtrackOffsets[0] = 114567259;
+					gRaySoundtrackOffsets[1] = 112423809;
+					gRaySoundtrackLengths[0] = 635317;
+					gRaySoundtrackLengths[1] = 2143450;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 12:
+				case 13:
+				default:
+					// Magician level, so the normal game music is OK.
+					gRayNumCurSoundtracks = 0;
+			}
+			break;
+		case 6:
+			switch (RayLevelNumber()) {
+				case 1:
+					gRaySoundtrackOffsets[0] = 8434183;
+					gRaySoundtrackOffsets[1] = 6531785;
+					gRaySoundtrackLengths[0] = 840804;
+					gRaySoundtrackLengths[1] = 1902398;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 2:
+					gRayAddStrackOffset = 14398033;
+					gRayAddStrackLength = 1816842;
+					gRaySoundtrackOffsets[0] = 6207028;
+					gRaySoundtrackOffsets[1] = 4923023;
+					gRaySoundtrackLengths[0] = 324757;
+					gRaySoundtrackLengths[1] = 1284005;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 3:
+					gRaySoundtrackOffsets[0] = 4804876;
+					gRaySoundtrackOffsets[1] = 2605928;
+					gRaySoundtrackLengths[0] = 118147;
+					gRaySoundtrackLengths[1] = 2198948;
+					gRayNumCurSoundtracks = 2;
+					break;
+				case 4:
+					gRaySoundtrackOffsets[0] = 11669736;
+					gRaySoundtrackOffsets[1] = 9274987;
+					gRaySoundtrackLengths[0] = 454217;
+					gRaySoundtrackLengths[1] = 2394749;
+					gRayNumCurSoundtracks = 2;
+					break;
+				default:
+					LOG_MSG("https://xkcd.com/2200/");
+					gRayNumCurSoundtracks = 0;
+			}
+			break;
+		default:
+			LOG_MSG("https://xkcd.com/2200/");
+			gRayNumCurSoundtracks = 0;
+	}
+}
+
+void SetRayVer(unsigned char curRayVer) {
+	// Is this version detection a change in state?
+	//
+	// Don't do anything if it becomes zero though,
+	// since that doesn't *necessarily* mean that
+	// the game has stopped...
+	if ((curRayVer != gRayVer) && curRayVer) {
+		gRayVer = curRayVer;
+		switch(gRayVer) {
+			case RAY_1_00:
+				LOG_MSG("Using Rayman 1.00");
+				gRayWorldBase = 0x16D310;
+				gRayLevelOffset = 0x00020;
+				gRayInLevelOffset = 0x02228;
+				gRayMusOnOffset = 0x02234;
+				gRayOptionsOnOffset = 0x174FB;
+				gRayOptionsOffOffset = 0x174FD;
+				gRayBossEventOffset = 0x02257;
+				gRayXOffset = 0x00E5C;
+				gRayYOffset = 0x00E60;
+				break;
+			case RAY_1_10:
+				LOG_MSG("Using Rayman 1.10");
+				gRayWorldBase = 0x16D7B4;
+				gRayLevelOffset = 0x0001C;
+				gRayInLevelOffset = 0x02278;
+				gRayMusOnOffset = 0x02232;
+				gRayOptionsOnOffset = 0x174E7;
+				gRayOptionsOffOffset = 0x174E9;
+				gRayBossEventOffset = 0x02256;
+				gRayXOffset = 0x00E54;
+				gRayYOffset = 0x00E58;
+				break;
+			case RAY_1_12_0:
+				LOG_MSG("Detected Rayman 1.12.0 - starting monitoring");
+				gRayWorldBase = 0x16D804;
+				gRayLevelOffset = 0x0001C;
+				gRayInLevelOffset = 0x02278;
+				gRayMusOnOffset = 0x02232;
+				gRayOptionsOnOffset = 0x174E7;
+				gRayOptionsOffOffset = 0x174E9;
+				gRayBossEventOffset = 0x02256;
+				gRayXOffset = 0x00E54;
+				gRayYOffset = 0x00E58;
+				break;
+			case RAY_1_12_1:
+				LOG_MSG("Using Rayman 1.12.1");
+				gRayWorldBase = 0x16D814;
+				gRayLevelOffset = 0x0001C;
+				gRayInLevelOffset = 0x02278;
+				gRayMusOnOffset = 0x02232;
+				gRayOptionsOnOffset = 0x174E7;
+				gRayOptionsOffOffset = 0x174E9;
+				gRayBossEventOffset = 0x02256;
+				gRayXOffset = 0x00E54;
+				gRayYOffset = 0x00E58;
+				break;
+			case RAY_1_12_2:
+				LOG_MSG("Using Rayman 1.12.2");
+				gRayWorldBase = 0x16D5B4;
+				gRayLevelOffset = 0x0001C;
+				gRayInLevelOffset = 0x02278;
+				gRayMusOnOffset = 0x02232;
+				gRayOptionsOnOffset = 0x174E7;
+				gRayOptionsOffOffset = 0x174E9;
+				gRayBossEventOffset = 0x02256;
+				gRayXOffset = 0x00E54;
+				gRayYOffset = 0x00E58;
+				break;
+			case RAY_1_12_U:
+				LOG_MSG("Detected Rayman 1.12 UNPROTECTED - starting monitoring");
+				//gRayWorldBase = 0x16E4B4;
+				gRayWorldBase = 0x16D5B4;
+				gRayLevelOffset = 0x0001C;
+				gRayInLevelOffset = 0x02278;
+				gRayMusOnOffset = 0x02232;
+				gRayOptionsOnOffset = 0x174E7;
+				gRayOptionsOffOffset = 0x174EA;
+				gRayBossEventOffset = 0x02256;
+				gRayXOffset = 0x00E54;
+				gRayYOffset = 0x00E58;
+				break;
+			case RAY_1_20:
+				LOG_MSG("Detected Rayman 1.20 - starting monitoring");
+				gRayWorldBase = 0x16E868;
+				gRayLevelOffset = 0x00034;
+				gRayInLevelOffset = 0x022C0;
+				gRayMusOnOffset = 0x02278;
+				gRayOptionsOnOffset = 0x17523;
+				gRayOptionsOffOffset = 0x17525;
+				gRayBossEventOffset = 0x022A0;
+				gRayXOffset = 0x00EA0;
+				gRayYOffset = 0x00EA4;
+				break;
+			case RAY_1_21:
+				LOG_MSG("Detected Rayman 1.21 - starting monitoring");
+				gRayWorldBase = 0x16E7D8;
+				gRayLevelOffset = 0x00034;
+				gRayInLevelOffset = 0x022C0;
+				gRayMusOnOffset = 0x02278;
+				gRayOptionsOnOffset = 0x17523;
+				gRayOptionsOffOffset = 0x17525;
+				gRayBossEventOffset = 0x022A0;
+				gRayXOffset = 0x00EA0;
+				gRayYOffset = 0x00EA4;
+				break;
+			case RAY_1_21_CN:
+				LOG_MSG("Using Rayman 1.21 (Chinese)");
+				gRayWorldBase = 0x16E9F0;
+				gRayLevelOffset = 0x00034;
+				gRayInLevelOffset = 0x022C0;
+				gRayMusOnOffset = 0x02278;
+				gRayOptionsOnOffset = 0x1752B;
+				gRayOptionsOffOffset = 0x1752D;
+				gRayBossEventOffset = 0x022A0;
+				gRayXOffset = 0x00EA0;
+				gRayYOffset = 0x00EA4;
+				break;
+			default:
+				LOG_MSG("Rayman seems to have stopped - stopping monitoring");
+		}
+	}
+}
+
+static void RAYMAN_Stop(Section *sec) {
+	SDL_DestroyMutex(gRayMutex);
+	SDL_DestroyMutex(gRayAddMutex);
+}
+
+// Function run when DOSBox starts...
+void RAYMAN_Init(Section* sec) {
+	sec->AddDestroyFunction(&RAYMAN_Stop);
+
+	Section_prop * section=static_cast<Section_prop *>(sec);
+	/* Read out config section */
+	std::string rayver;
+	// Prefer command-line specification.
+	if (!control->cmdline->FindString("-rayversion", rayver))
+		// Fall back to what's specified in the config file.
+		rayver=section->Get_string("gameversion");
+	if (rayver == "1.00")                  SetRayVer(RAY_1_00);
+	else if (rayver == "1.10")             SetRayVer(RAY_1_10);
+	else if (rayver == "1.12.0")           SetRayVer(RAY_1_12_0);
+	else if (rayver == "1.12.1")           SetRayVer(RAY_1_12_1);
+	else if (rayver == "1.12.2")           SetRayVer(RAY_1_12_2);
+	else if (rayver == "1.12_Unprotected") SetRayVer(RAY_1_12_U);
+	else if (rayver == "1.20")             SetRayVer(RAY_1_20);
+	else if (rayver == "1.21")             SetRayVer(RAY_1_21);
+	else if (rayver == "1.21_Chinese")     SetRayVer(RAY_1_21_CN);
+	// Default is auto, which is 0.
+
+	// Prefer command-line specification.
+	if (!control->cmdline->FindString("-raymusicdat", gRayMusPath))
+		// Fall back to what's specified in the config file.
+		gRayMusPath=section->Get_path("musicfile")->realpath;
+
+	/* Sanity check */
+	SDL_RWops *myrwops = SDL_RWFromFile(gRayMusPath.c_str(), "rb");
+	if(!myrwops) {
+		LOG_MSG("%s not accessible - Rayman soundtrack will be unavailable", gRayMusPath.c_str());
+		gRayMusPath = "";
+		return;
+	}
+	SDL_FreeRW(myrwops);
+
+	/* Initialize the internal stuff */
+	gRayMutex = SDL_CreateMutex();
+	gRayAddMutex = SDL_CreateMutex();
+	gRayChannel = MIXER_AddChannel(&RaySoundtrackCallBack, 44100, "PLUMRAY");
+	gRayAddChannel = MIXER_AddChannel(&RayAddStrackCallBack, 44100, "MIDIRAY");
+}
+
+// Looping function
+bool HandleRaymanSoundtrack() {
+	// If we've no Music.dat path, we can do nothing.
+	if(gRayMusPath=="")
+		return false;
+
+	if (!gRayVer)
+		// Detect Rayman version
+		if(mem_readd(0x16D7BC) == 320)
+			SetRayVer(RAY_1_12_0);
+		else if (mem_readd(/*0x16E46C*/ 0x16D56C) == 320)
+			SetRayVer(RAY_1_12_U);
+		else if (mem_readd(0x16E87C) == 320)
+			SetRayVer(RAY_1_20);
+		else if (mem_readd(0x16E7EC) == 320)
+			SetRayVer(RAY_1_21);
+
+	// Return immediately if Rayman isn't running.
+	if (!gRayVer)
+		return false;
+
+	// Get world name
+	char curWorld[8];
+	MEM_StrCopy(gRayWorldBase, curWorld, 8);
+	if(!RaySanityCheck(curWorld))
+		// Rayman's probably not running...
+		return false;
+	// Truncate
+	for(int i=0;i<8;i++)
+		if (curWorld[i] == '.')
+			curWorld[i] = 0;
+	// Is this a new world?
+	if(strncmp(curWorld,gRayWorld,8)) {
+		LOG_MSG("Entered new world: %s", curWorld);
+		strncpy(gRayWorld,curWorld,8);
+		// Clear the level buffer to make sure new soundtrack gets chosen below!
+		memset(gRayLevel,0,8);
+	}
+
+	// Get level name
+	char curLevel[8];
+	MEM_StrCopy(gRayWorldBase + gRayLevelOffset, curLevel, 8);
+	// Truncate
+	for(int i=0;i<8;i++)
+		if (curLevel[i] == '.')
+			curLevel[i] = 0;
+	// Is this a new level?
+	if(strncmp(curLevel,gRayLevel,8)) {
+		LOG_MSG("Entered new level: %s", curLevel);
+		strncpy(gRayLevel,curLevel,8);
+		// New level => new soundtrack
+		RayChooseSoundtrack();
+	}
+
+	// Other game state info
+	Bit8u RayInLevel = mem_readb(gRayWorldBase + gRayInLevelOffset);
+	Bit8u RayMusOn = mem_readb(gRayWorldBase + gRayMusOnOffset);
+	Bit8u RayOptionsOn = mem_readb(gRayWorldBase + gRayOptionsOnOffset);
+	Bit8u RayOptionsOff = mem_readb(gRayWorldBase + gRayOptionsOffOffset);
+	Bit8u RayBossEvent = mem_readb(gRayWorldBase + gRayBossEventOffset);
+
+	// Has any of it changed?
+	if(RayInLevel != gRayInLevel) {
+		if(RayInLevel) {
+			LOG_MSG("Now in level");
+			if (RayMusOn && RayOptionsOff && !RayOptionsOn && gRayNumCurSoundtracks)
+				RayActivateSoundtrack();
+			else
+				RayFadeOutSoundtrack();
+		} else {
+			LOG_MSG("No longer in level");
+			RayFadeOutSoundtrack();
+		}
+		gRayInLevel = RayInLevel;
+	}
+	if(RayMusOn != gRayMusOn) {
+		if(RayMusOn) {
+			LOG_MSG("Music now on");
+			if (RayInLevel && RayOptionsOff && !RayOptionsOn && gRayNumCurSoundtracks)
+				RayActivateSoundtrack();
+		} else {
+			LOG_MSG("Music no longer on");
+			if (RayInLevel && !RayOptionsOff && RayOptionsOn && gRayNumCurSoundtracks)
+				RayStopSoundtrack();
+		}
+		gRayMusOn = RayMusOn;
+	}
+	if(RayOptionsOn != gRayOptionsOn) {
+		if(RayOptionsOn) {
+			LOG_MSG("Options now on");
+			RayPauseSoundtrack();
+		} else
+			LOG_MSG("Options no longer on");
+		gRayOptionsOn = RayOptionsOn;
+	}
+	if(RayOptionsOff != gRayOptionsOff) {
+		if(RayOptionsOff) {
+			LOG_MSG("Options now off");
+			// LOG_MSG("We have %i soundtracks", gRayNumCurSoundtracks);
+			if (RayInLevel && RayMusOn && !RayOptionsOn && gRayNumCurSoundtracks)
+				RayActivateSoundtrack();
+		} else
+			LOG_MSG("Options no longer off");
+		gRayOptionsOff = RayOptionsOff;
+	}
+	if(RayBossEvent != gRayBossEvent) {
+		if(RayBossEvent) {
+			LOG_MSG("Boss event now");
+			RayFadeOutSoundtrack();
+		} else
+			LOG_MSG("Boss event over");
+		gRayBossEvent = RayBossEvent;
+	}
+
+	// Where is Rayman?
+	Bit16u RayX = mem_readw(gRayWorldBase + gRayXOffset);
+	Bit16u RayY = mem_readw(gRayWorldBase + gRayYOffset);
+	// Has he moved?
+	if ((RayX != gRayX) || (RayY != gRayY)) {
+		// No point in having log messages for this...
+		gRayX = RayX;
+		gRayY = RayY;
+		if (gRayMusOn && gRayNumCurSoundtracks && gRayPosDep) {
+			int curSoundtrack = gRaySoundtrackOffsets[0];
+			int curAddStrack = gRayAddStrackOffset;
+			RayChooseSoundtrack();
+			if (gRaySoundtrackOffsets[0] != curSoundtrack) {
+				LOG_MSG("Restarting custom Rayman soundtrack as Rayman has moved to a different part of the map");
+				RayStopSoundtrack();
+				RayActivateSoundtrack();
+			}
+			if (gRayAddStrackOffset != curAddStrack) {
+				RayStopAddStrack();
+				RayActivateAddStrack();
+			}
+		}
+	}
+
+	return true;
+}
I've tested it and it seems to work on Linux. I can't remember how to turn it into a release archive with Windows binaries though… :sad: I'm really tired right now, maybe I'll have better luck tomorrow!

Also, I was thinking that the Dosbox patch should be overhauled a bit, since it does weird things like determining the version by searching for an arbitrary integer in memory, and parsing the current WLD and LEV filenames instead of reading the numbers directly. I would envisage making it work a lot more like my TSR, but right now I don't have all the versions at my fingertips, so that's probably a longer-term thing…
Last edited by PluMGMK on Sun Mar 26, 2023 1:50 pm, edited 1 time in total.
Reason: Updated patch to fix a few bugs
dr_st
Eig
Posts: 1705
Joined: Sat Aug 25, 2012 5:52 pm
Tings: 40813

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by dr_st »

It sometimes horrifies me to realize that some of the best code in the universe is maintained in a kludge of 1000+ line patches inside forum threads by one dedicated person.

I think that even the Linux real-time patch is / was being handles the same way, so, Plum, you are in good company! :lol:
PluMGMK
Aline Louïa
Posts: 37010
Joined: Fri Jul 31, 2009 9:00 pm
Location: https://www.youtube.com/watch?v=cErgMJSgpv0
Contact:
Tings: 102745

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by PluMGMK »

dr_st wrote: Sun Mar 26, 2023 7:11 am It sometimes horrifies me to realize that some of the best code in the universe is maintained in a kludge of 1000+ line patches inside forum threads by one dedicated person.
Ah yes, and there's also this:
Image
:hap:

I don't think this is up there with the best code in the universe though :oops2: In fact, I think the TSR is overall a much better job (which I guess means I write better Assembly than I do C++! XD) since it ties directly into the game and makes no assumptions about physical memory layout. After all, physical memory layout can change if you run another DOS program, or if you've rebound a different version of PMODE/W, or if you want to run Rayman inside Windows 3.1 under Dosbox (yes, that can be done too!). The fact that the Dosbox patch isn't tied directly into the game also means you get some moments where things don't quite sync up, so you hear the Rayman Forever soundtrack for a second or two…

The two remaining advantages of the Dosbox patch over the TSR (apart from ease of configurability, which could be handled by RCP anyway) are that it supports more versions, and that it includes the extra "MIDI" soundtracks from the PS1 version. The version support is a very solvable problem (I just need all the EXEs I can find!), and, now that I think about it, the "MIDI" thing could also probably be solved by plugging into Rayman's sound system (which is already used as a delivery vector for the TSR's protected-mode code!). I could set a timer event to monitor Rayman's position in the map, then play the "MIDI" tracks as looping sounds… I'll just go and append that to my TODO list! :oops2:
dr_st wrote: Sun Mar 26, 2023 7:11 amI think that even the Linux real-time patch is / was being handles the same way, so, Plum, you are in good company! :lol:
I think that finally got mainlined recently though, didn't it? :fou:
dr_st
Eig
Posts: 1705
Joined: Sat Aug 25, 2012 5:52 pm
Tings: 40813

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by dr_st »

PluMGMK wrote: Sun Mar 26, 2023 9:44 amThe fact that the Dosbox patch isn't tied directly into the game also means you get some moments where things don't quite sync up, so you hear the Rayman Forever soundtrack for a second or two…
Ha, this reminds me of this weird thing I'm experiencing with DOSBox on one of my setups. It is a laptop on a dock, connected through DisplayPort to a monitor, and the audio goes through the DisplayPort to a pair of speakers connected to the monitor. Only on this particular setup, when I start some games, for ~1 second I hear the audio from the laptop speakers, and after that it works correctly through the monitor.
PluMGMK wrote: Sun Mar 26, 2023 9:44 amThe version support is a very solvable problem (I just need all the EXEs I can find!)
Well, as part of my no-CD investigation I obtained EU 1.12, US 1.12 (unprotected), IT-SP-DU 1.20, US 1.21 and FR 1.21. Need any of those?
PluMGMK wrote: Sun Mar 26, 2023 9:44 amI could set a timer event to monitor Rayman's position in the map, then play the "MIDI" tracks as looping sounds…
That's interesting. Will it require an exhaustive table of all relevant position in all maps, or is there a generic approach tied to certain objects?
PluMGMK
Aline Louïa
Posts: 37010
Joined: Fri Jul 31, 2009 9:00 pm
Location: https://www.youtube.com/watch?v=cErgMJSgpv0
Contact:
Tings: 102745

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by PluMGMK »

PluMGMK wrote: Sun Mar 26, 2023 12:58 am I can't remember how to turn it into a release archive with Windows binaries though… :sad: I'm really tired right now, maybe I'll have better luck tomorrow!
So it turns out I had documented that in my own README inside the zip archive! :fou: I've created a new release archive, but not before updating the patch to fix a few bugs (mainly affecting the demos on the main menu). The revised patch is in the above post, and the new archive is here: http://www.vigovproductions.net/patched-dosbox-v5.zip (first post updated too!) This archive only includes 32-bit Windows binaries because for some reason I don't have the folders I used to compile the 64-bit version anymore, and it would be a lot of hassle! :mrgreen: Microsoft aren't showing any signs of going the Apple route with 32-bit software support anyway, so this should be sufficient :hap:
dr_st wrote: Sun Mar 26, 2023 12:12 pm Ha, this reminds me of this weird thing I'm experiencing with DOSBox on one of my setups. It is a laptop on a dock, connected through DisplayPort to a monitor, and the audio goes through the DisplayPort to a pair of speakers connected to the monitor. Only on this particular setup, when I start some games, for ~1 second I hear the audio from the laptop speakers, and after that it works correctly through the monitor.
Weird… I guess it's the games using older sound APIs that aren't up to speed with how things should work! (I'm thinking of headaches on Linux with ALSA versus Pulse versus PipeWire etc, but I'd say there's some equivalent stuff on other OSes!)
dr_st wrote: Sun Mar 26, 2023 12:12 pmWell, as part of my no-CD investigation I obtained EU 1.12, US 1.12 (unprotected), IT-SP-DU 1.20, US 1.21 and FR 1.21. Need any of those?
I think the only one of those I don't have is FR 1.21… Snagglebee somehow managed to collect this entire list: 1.00, 1.10, 1.12 (three versions, albeit not including the Unprotected one :mefiant:), 1.20, 1.21, 1.21_Chinese; which is why they're all supported in the Dosbox patch, which was based on his work.
dr_st wrote: Sun Mar 26, 2023 12:12 pmThat's interesting. Will it require an exhaustive table of all relevant position in all maps, or is there a generic approach tied to certain objects?
In theory, it should be possible to use the "Ambient Starter" objects used for this purpose in the PS1 version. The code for that object is in the PC version – naturally it doesn't do much, but it's in a state that could easily be hooked by the TSR! Unfortunately, the PC maps don't actually include these objects, so there will need to be a table of maps…

On closer inspection though, the only place the Dosbox patch uses position dependence for the "MIDI" tracks is for the Mister Stone chase, which means it could, and probably should, be achieved by hooking the chase code instead. All the other "MIDI" tracks are only map-dependent, so in theory there's no need for a timer event. That said, the tracks in Music.dat contain a lot of silence to create the illusion that the "MIDI" tracks are only playing some of the time. It might be more efficient to use a timer event to start and stop them, instead of looping lots of silence. I could poll the current position of the CD head to ensure they're synchronized with the CD audio as expected.

I'm thinking I'd have a DAT file containing a header with a series of data structures something like this:

Code: Select all

AuxTrackData	struc
World		dw ?
Level		dw ?
CDTimeToStart	dd ?	; Minutes:Seconds:Frames relative to the start of the level's CD track (absolute position would be calculated at runtime)
Offset		dd ?	; offset to this track's data in the DAT file
Length		dd ?
AuxTrackData	ends
Then the rest of the file would be raw PCM data, based on what's in the current Music.dat, but downsampled to 10 kHz (which is what the PC version of Rayman runs the sound system at, for some peculiar reason, which is why it sounds lower-pitched than the spinoffs running at the standard 11.025 kHz).

I'm not going to work on this today, but since I've already gone to the trouble of writing a structure spec in MASM format, I'll most likely come back to it in the near future!
dr_st
Eig
Posts: 1705
Joined: Sat Aug 25, 2012 5:52 pm
Tings: 40813

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by dr_st »

PluMGMK wrote: Sun Mar 26, 2023 2:23 pm I think the only one of those I don't have is FR 1.21…
Attached.

This version was included in French Rayman Forever/Collector bundles, maybe also Rayman Gold (I never owned a non-English copy of 'Gold').
Attachments
RAYFR121.ZIP
Rayman 1.21 (French) executable
(392.93 KiB) Downloaded 23 times
Post Reply