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

Post Reply
PluMGMK
Dora Dodemer
Posts: 32028
Joined: Fri Jul 31, 2009 9:00 pm
Location: An tír ina labhraítear (beagán ar fad) an teanga ina bhfuil sé seo scríofa
Contact:
Tings: 5520

Rayman 1 per-level soundtrack implemented within DOSBox

Post by PluMGMK »

Hello all! Here it is, the reason I haven't been posting much this week…

UPDATE: You can now download the patched source code and binaries at http://www.vigovproductions.net/patched-dosbox-v4.zip – this also includes Music.dat, which is why it's so big!

Here is a patch for DOSBox to make it implement per-level soundtracks in Rayman 1:

Code: Select all

diff -Nur dosbox-0.74-3-vanilla/src/dosbox.cpp dosbox-0.74-3-patched/src/dosbox.cpp
--- dosbox-0.74-3-vanilla/src/dosbox.cpp	2019-05-02 18:01:15.000000000 +0100
+++ dosbox-0.74-3-patched/src/dosbox.cpp	2019-11-02 13:28:14.000000000 +0000
@@ -1,5 +1,6 @@
 /*
  *  Copyright (C) 2002-2010  The DOSBox Team
+ *  Modified 2019 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.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"
+		"or 1.21_Chinese.\n"
+		"auto can detect 1.12.0, 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-patched/src/misc/Makefile.am
--- dosbox-0.74-3-vanilla/src/misc/Makefile.am	2019-05-02 18:01:19.000000000 +0100
+++ dosbox-0.74-3-patched/src/misc/Makefile.am	2019-11-02 13:28:14.000000000 +0000
@@ -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-patched/src/misc/Makefile.in
--- dosbox-0.74-3-vanilla/src/misc/Makefile.in	2019-06-26 15:55:12.000000000 +0100
+++ dosbox-0.74-3-patched/src/misc/Makefile.in	2019-11-02 13:28:14.000000000 +0000
@@ -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 = $([email protected][email protected])
 am__v_P_ = $([email protected][email protected])
@@ -277,7 +277,7 @@
 top_srcdir = @[email protected]
 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 @@
 @[email protected]@[email protected] @[email protected]/$(DEPDIR)/[email protected][email protected]
 @[email protected]@[email protected] @[email protected]/$(DEPDIR)/[email protected][email protected]
 @[email protected]@[email protected] @[email protected]/$(DEPDIR)/[email protected][email protected]
[email protected][email protected]@[email protected] @[email protected]/$(DEPDIR)/[email protected][email protected]
 
 .cpp.o:
 @[email protected]	$(AM_V_CXX)$(CXXCOMPILE) -MT [email protected] -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o [email protected] $<
diff -Nur dosbox-0.74-3-vanilla/src/misc/rayman_soundtrack.cpp dosbox-0.74-3-patched/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-patched/src/misc/rayman_soundtrack.cpp	2020-01-18 20:57:41.000000000 +0000
@@ -0,0 +1,1198 @@
+/*
+ *  This Rayman soundtrack implementation code Copyright (C) 2019  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
+
+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
+}
+
+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_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.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(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);
+	}
+
+	// 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) {
+				RayStopSoundtrack();
+				RayActivateSoundtrack();
+			}
+			if (gRayAddStrackOffset != curAddStrack) {
+				RayStopAddStrack();
+				RayActivateAddStrack();
+			}
+		}
+	}
+
+	return true;
+}
This performs the same function as TPLS in Rayman Plus and the Rayman Control Panel, but it is built right into DOSBox, and is hence not tied to Windows!

Very special thanks, of course, to Snagglebee and RayCarrot, who did the hard work of figuring out where in memory all the Rayman data is stored and how to deal with it, and for making the logic available in the open-source Control Panel!

Setup Instructions:
  • Obtain the DOSBox source code, e.g. from https://sourceforge.net/projects/dosbox ... z/download (whatever version you get, make sure it supports SDL_Sound!).
  • Apply this patch (on Linux this can be done by opening a terminal in the source directory, entering "patch -Np1", pasting in the above patch (between the code tags) and then pressing Ctrl+D).
  • Compile it. On Linux, you can use "./configure" (MAKE SURE that SDL_Sound is detected – you may need to install a dev package for it) and "make". On Windows, I think you can use the Visual C++ project included in the source directory – make sure to add the new file "misc/rayman_soundtrack.cpp" to the project before compiling.
  • You can optionally install with "make install", but there's no need since you can just run the executable from inside the src directory. Not sure what the equivalent is on Windows, but there should be an EXE there somewhere.
  • Obtain the Music.dat file used with TPLS. The way I did this was by installing Rayman Plus in a Windows VM and copying the file back out to Linux (there may be an easier way). Of course, on Windows it is easy!
  • Open up the DOSBox config file you're using with Rayman (e.g. the one from the GOG version of Rayman forever). Add a new section called "[rayman]" and specify the Music.dat location using the setting "musicfile". For example:

    Code: Select all

    [rayman]
    musicfile=../Music/Music.dat
    
  • Start up your new DOSBox executable using the config file and enjoy Rayman with per-level soundtrack on the OS of your choice!
Limitations:
  • Requires SDL_Sound, which is tied to the old SDL1. (But it's the same for reading CUE images usually used for the normal music...)
  • Windows binaries have only been tested under Wine. YMMV!
  • Doesn't play MIDI music for loading a level, or when you beat a boss.
  • The way I've used static global vars may upset people. Or not – I've scarcely written any C++ in five years! :lol:
Any feedback welcome! Enjoy! :D
Steo
Globox Origins
Posts: 30223
Joined: Sun Feb 25, 2018 3:57 pm
Location: Le Village des Globox
Tings: 225945

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by Steo »

Nice! I'll have to try this out during the week. :D
Image
FC: 40210 | CF: 103059 | BOM: 94388 | LOTLD: 120486 | DOTK: 110450 | LS: 40810 | SBTC: 99693 | HH: 100028 | TOTL: 100563

TOTAL: 809687
RayCarrot
Uglette
Posts: 2116
Joined: Sat Jan 11, 2014 5:46 pm
Contact:
Tings: 29039

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by RayCarrot »

This is really cool, good job! It's nice to have the functionality directly in DOSBox to avoid any external programs.
As for the versions, I have added support for all currently known versions in the Rayman Control Panel. You can check out the values here if it'd help: https://github.com/RayCarrot/Rayman-Con ... ensions.cs
Image
PluMGMK
Dora Dodemer
Posts: 32028
Joined: Fri Jul 31, 2009 9:00 pm
Location: An tír ina labhraítear (beagán ar fad) an teanga ina bhfuil sé seo scríofa
Contact:
Tings: 5520

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by PluMGMK »

Update: I've finished the game and I didn't find any problems. :) There is something that happens where arrow keys spontaneously get un-pressed if I've held them for a while, but I'm pretty sure that's a problem with my keyboard, not the patch…
RayCarrot wrote: Sat Oct 19, 2019 9:02 pm This is really cool, good job! It's nice to have the functionality directly in DOSBox to avoid any external programs.
As for the versions, I have added support for all currently known versions in the Rayman Control Panel. You can check out the values here if it'd help: https://github.com/RayCarrot/Rayman-Con ... ensions.cs
Thanks! Yeah, at first I didn't want to manually specify the game version, but I suppose now that I've code there for manually specifying the Music.dat location, there'd be no harm in adding that too…
PluMGMK
Dora Dodemer
Posts: 32028
Joined: Fri Jul 31, 2009 9:00 pm
Location: An tír ina labhraítear (beagán ar fad) an teanga ina bhfuil sé seo scríofa
Contact:
Tings: 5520

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by PluMGMK »

I've updated the patch so you can now specify any Rayman version supported by TPLS in your DOSBox config file. I also fixed the bug that made it crash when stopping the music on Windows (no idea why not on Linux!).

You can now download a zip file that includes Windows binaries and Music.dat itself: http://www.vigovproductions.net/patched-dosbox.zip :)
Snagglebee
Premier Grand Mini
Posts: 11786
Joined: Tue Jan 08, 2013 6:22 pm
Location: Yes
Tings: 2725

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by Snagglebee »

Awesome work! It's awesome to see how my code gets ported into a different language :P
Image
PluMGMK
Dora Dodemer
Posts: 32028
Joined: Fri Jul 31, 2009 9:00 pm
Location: An tír ina labhraítear (beagán ar fad) an teanga ina bhfuil sé seo scríofa
Contact:
Tings: 5520

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by PluMGMK »

Thanks! :P

Version 3 is now available, and can be downloaded from the first post! The Rayman version and Music.dat location can now be specified on the command line, using the "-rayversion" and "-raymusicdat" options respectively. Also, when Music.dat can't be opened, the console now actually shows the name of the file it's trying to open instead of some garbage – seems DOSBox's APIs are inconsistent about whether they prefer C-style or C++ strings. :mefiant:
RayCarrot
Uglette
Posts: 2116
Joined: Sat Jan 11, 2014 5:46 pm
Contact:
Tings: 29039

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by RayCarrot »

With the latest update to the Rayman Control Panel you can automatically use the modified DOSBox to get the per-level soundtrack to work.
Thanks to Plum for making this possible!
Image
Snagglebee
Premier Grand Mini
Posts: 11786
Joined: Tue Jan 08, 2013 6:22 pm
Location: Yes
Tings: 2725

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by Snagglebee »

I recently submitted an install script for Lutris for Rayman 1. That downloads and uses a modified DOSBox containing the TPLS code built in like in PluMGMK binary above.
The code also is updated to play the victory jingle, it has couple of the soundtrack-looping reworked and it handles the soundtrack for all the playable levels, including Betilla and the Magician Bonus levels. Also, instead of muting the CDDA channel from SDL Mixer, I hooked directly into MSCDEX and called the CDDA to stop. That would fix the sudden unwanted CDDA to reappear when you for example die in level.

Check the sources if you're intersted :)
https://github.com/Snaggly/Rayman1Dos-TPLS

https://github.com/Snaggly/LutrisScript ... src.tar.xz
Image
PluMGMK
Dora Dodemer
Posts: 32028
Joined: Fri Jul 31, 2009 9:00 pm
Location: An tír ina labhraítear (beagán ar fad) an teanga ina bhfuil sé seo scríofa
Contact:
Tings: 5520

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by PluMGMK »

I just realized that the Windows EXE I shipped in the download was 64-bit, even though I intended it to be 32-bit. :oops2: Not sure how that happened, let me look into it and I'll fix the download once I get a proper 32-bit binary built.
Steo
Globox Origins
Posts: 30223
Joined: Sun Feb 25, 2018 3:57 pm
Location: Le Village des Globox
Tings: 225945

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by Steo »

Nice, I'll have to look at this. I did try to run Rayman with the Linux version of Dosbox before, but I think some in game music and sound effects unrelated to TPLS weren't working for some reason. I never got back to trying to resolve it.
Image
FC: 40210 | CF: 103059 | BOM: 94388 | LOTLD: 120486 | DOTK: 110450 | LS: 40810 | SBTC: 99693 | HH: 100028 | TOTL: 100563

TOTAL: 809687
PluMGMK
Dora Dodemer
Posts: 32028
Joined: Fri Jul 31, 2009 9:00 pm
Location: An tír ina labhraítear (beagán ar fad) an teanga ina bhfuil sé seo scríofa
Contact:
Tings: 5520

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by PluMGMK »

I've put up a new zip file with the fake "bin-win32" folder renamed to "bin-win64" and with an actual 32-bit one added. The README has been updated to tell the truth about these folders. (In the old version I claim the files are 32-bit and then present instructions on how to cross-compile them, instructions which clearly produce 64-bit binaries. Globox moment, evidently! :oops2:)

I've also included a 32-bit Dosbox binary with GDB debugging symbols, which I may or may not be able to make Ray1Map read (see the conversation in the other thread) so it can automatically find the pointer. Not sure how useful that is, given that this will be one of the few Dosbox Windows binaries out there with these symbols. :?
Post Reply