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
Sandra Misu
Posts: 30447
Joined: Fri Jul 31, 2009 9:00 pm
Location: Tír an "Tuisil Ghinidigh" agus an "Mhodha Choinníollaigh"
Contact:
Tings: 4768

Rayman 1 per-level soundtrack implemented within DOSBox

Post by PluMGMK » Sat Oct 19, 2019 3:01 pm

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.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 12:46:43.506726915 +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 12:46:06.257595245 +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 12:46:06.258595248 +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 = $(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-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	2019-11-02 12:46:55.071767991 +0000
@@ -0,0 +1,1190 @@
+/*
+ *  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 <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 */
+	const char * type=section->Get_string("gameversion");
+	if (!strcasecmp(type,"1.00"))              SetRayVer(RAY_1_00);
+	else if (!strcasecmp(type,"1.10"))         SetRayVer(RAY_1_10);
+	else if (!strcasecmp(type,"1.12.0"))       SetRayVer(RAY_1_12_0);
+	else if (!strcasecmp(type,"1.12.1"))       SetRayVer(RAY_1_12_1);
+	else if (!strcasecmp(type,"1.12.2"))       SetRayVer(RAY_1_12_2);
+	else if (!strcasecmp(type,"1.20"))         SetRayVer(RAY_1_20);
+	else if (!strcasecmp(type,"1.21"))         SetRayVer(RAY_1_21);
+	else if (!strcasecmp(type,"1.21_Chinese")) SetRayVer(RAY_1_21_CN);
+	// Default is auto, which is 0.
+
+	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);
+		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
Rayman 2
Posts: 26684
Joined: Sun Feb 25, 2018 3:57 pm
Location: na Corcaí Buafacha
Tings: 184333

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by Steo » Sat Oct 19, 2019 8:10 pm

Nice! I'll have to try this out during the week. :D
How much Ray may a Rayman man if a Rayman may Man Ray?
Image
FC: 40210 | CF: 103059 | BOM: 94388 | LOTLD: 120486 | DOTK: 110450 | LS: 40810 | SBTC: 99693 | HH: 100028 | TOTL: 100563

TOTAL: 809687

RayCarrot
Uglette
Posts: 1795
Joined: Sat Jan 11, 2014 5:46 pm
Location: Sweden
Contact:
Tings: 25324

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by RayCarrot » 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

PluMGMK
Sandra Misu
Posts: 30447
Joined: Fri Jul 31, 2009 9:00 pm
Location: Tír an "Tuisil Ghinidigh" agus an "Mhodha Choinníollaigh"
Contact:
Tings: 4768

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by PluMGMK » Sun Oct 20, 2019 2:05 pm

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
Sandra Misu
Posts: 30447
Joined: Fri Jul 31, 2009 9:00 pm
Location: Tír an "Tuisil Ghinidigh" agus an "Mhodha Choinníollaigh"
Contact:
Tings: 4768

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by PluMGMK » Sat Nov 02, 2019 4:17 pm

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: 11780
Joined: Tue Jan 08, 2013 6:22 pm
Location: Yes
Tings: 2695

Re: Rayman 1 per-level soundtrack implemented within DOSBox

Post by Snagglebee » Sun Nov 03, 2019 4:45 pm

Awesome work! It's awesome to see how my code gets ported into a different language :P
Image

Post Reply