Downloads containing MayModUtils.asc

Downloads
Name Author Game Mode Rating
JJ2+ Only: Astral WitchcraftFeatured Download minmay Multiple 9 Download file

File preview

// by may
// 0.1

/*

******************************
* THIS IS NOT A REAL RELEASE *
******************************

This library is extremely unfinished. Interface and functionality are subject to
potentially drastic change. Volume estimates haven't really been implemented
yet, none of the song message features are really implemented yet, and only a
few select varieties of a few select file formats can even be loaded properly.

Use at your own risk.

*/

// TODO:
// - read instrument+sample info from XM and MO3 files
// - read volume commands properly from XM and MO3 files
// - don't just ignore portamento effects
// - support "sample mode" IT and MO3 files (no instruments)
// - get the tick at which an instrument stops playing exactly right, so that
//   retrigger note is correctly skipped when the note is already finished

// This script reads the song message for extra instructions. If there is a line
// consisting only of "INFO FOR JJ2+ SCRIPT" and whitespace, following lines
// will be read as extra instructions for the script. These lines can be:
//
// Line starting with "//": Ignored.
//
// Line of all whitespace: Ends the info section; remaining lines in the message
//   will be ignored. This way you can keep the original song message below the
//   info section. (You could also put it above the info section and not need
//   this, of course.)
//
// "AUTHOR <name of author>": Original author of song, for display to players.
//
// "ORIGINAL <filename>": Original filename of song, for display to players, so
//   they can easily search for it on Modland or whatever.
//
// "VOLUMEGUIDES": Indicates that the first non-enabled envelope
//   on each instrument is a guide to the script on what volume
//   to expect from that sample. This guide is time-normalized:
//   no matter the actual X position of the last envelope point,
//   it is interpreted as the volume at the end of the sample,
//   and the earlier points are interpreted accordingly (linear).
//   This has no effect on instruments that use all three
//   envelopes.
//   While this requires some extra work when adapting the
//   module for this script, it allows the script to skip sample
//   decoding entirely, improving loading times (and the
//   amount of code I have to write).
// 
// "PREGAME <order number>": This order will be jumped to when
//   the game is stopped, unless out-prioritized. Does not correspond to the
//   actual pregame mode in JJ2+. Sorry.
//
// "GAME <order number>": This order will be jumped to when the
//   game starts.
//
// "OVERTIME <order number>": This order will be jumped to when
//   the game enters overtime. Takes priority over GAME.
//
// ***NOT YET IMPLEMENTED***
// "WIN <order number>": This order will be jumped to when a
//   local player wins the game or ties. Takes priority over
//   PREGAME.
//
// ***NOT YET IMPLEMENTED***
// "LOSS <order number>": This order will be jumped to when
//   someone wins the game or ties but none of the local players
//   did. Takes priority over PREGAME.
//
// ***NOT YET IMPLEMENTED***
// "TIE <order number>": This order will be jumped to when a
//   local player ties the game. Takes priority over WIN.
//
// ***NOT YET IMPLEMENTED***
// "END <order number>": Shortcut for both WIN and LOSS. Lower
//   priority than WIN, LOSS, and TIE, but takes priority over
//   PREGAME.

// LIMITATIONS:
// - Only reads *some* IT/XM/MO3 files for now, since I use IT/XM
//   while working on the song then convert it to mo3 for release, and am too
//   lazy to support some niche features
// - Assumes generally clean files, IT version 2.14, FT2 version 2.08 or equivalent.
// - The note played hook and volume estimation will not consistently work in mods that play as many or more
//   rows/second than the player's frames/second. While this is fixable, even an FPS
//   of 30 is enough to avoid this problem with basically any real mod.
// - This script has no way of knowing or compensating for the player's audio latency.
// - Even ignoring audio latency, this script's timing resolution is limited due
//   to only ticking every ~14ms and not being able to get the exact playback
//   position on demand (only the current row).
//
// LIMITATIONS OF VOLUME ESTIMATES:
// - Volume estimates will be very poor for many instruments in modules
//   without the VOLUMEGUIDES thing explained above.
// - Volume estimates will be poor in the presence of loud or extensive virtual
//   channels. This is entirely fixable, I'm just lazy.
// - Volume estimates will be poor for varying-volume samples when the pitch
//   envelope is used. This is fixable, but nobody uses that envelope anyway.
// - Volume estimates will be poor for varying-volume samples in the presence of
//   portamento effects (or sufficiently wacky vibrato effects). This is fixable
//   too, I'm just lazy.
// - Volume estimates do not account for the effects of IT filters.
// - Volume estimates do not attempt to be perceptual.

namespace ModNote {
	enum Note {
		NONE,
		// This is "most trackers do this" notation, so middle C is C5, not C4.
		C0, CS0, D0, DS0, E0, F0, FS0, G0, GS0, A0, AS0, B0,
		C1, CS1, D1, DS1, E1, F1, FS1, G1, GS1, A1, AS1, B1,
		C2, CS2, D2, DS2, E2, F2, FS2, G2, GS2, A2, AS2, B2,
		C3, CS3, D3, DS3, E3, F3, FS3, G3, GS3, A3, AS3, B3,
		C4, CS4, D4, DS4, E4, F4, FS4, G4, GS4, A4, AS4, B4,
		C5, CS5, D5, DS5, E5, F5, FS5, G5, GS5, A5, AS5, B5, // heh heh "AS5"
		C6, CS6, D6, DS6, E6, F6, FS6, G6, GS6, A6, AS6, B6,
		C7, CS7, D7, DS7, E7, F7, FS7, G7, GS7, A7, AS7, B7,
		C8, CS8, D8, DS8, E8, F8, FS8, G8, GS8, A8, AS8, B8,
		C9, CS9, D9, DS9, E9, F9, FS9, G9, GS9, A9, AS9, B9,
		FADE = 0xFD,
		CUT = 0xFE,
		OFF = 0xFF,
	}
}

namespace MayModUtils {
	// Do not attempt to use massive files (>64 MB)
	const uint MAX_MOD_FILE_SIZE = 1 << 26;
	const array<string> NOTE_LETTERS = {"C-","C#","D-","D#","E-","F-","F#","G-","G#","A-","A#","B-"};
	const array<string> NOTE_LETTERS_SMALL = {"C","C'","D","D'","E","F","F'","G","G'","A","A'","B"};
	
	// currently, changing these WILL break everything
	const uint64 MAY_COMMAND_NOTE_MASK = 0xFF;
	const uint64 MAY_COMMAND_INSTRUMENT_MASK = 0xFF00;
	const uint64 MAY_COMMAND_VOLUME_MASK = 0xFF0000;
	const uint64 MAY_COMMAND_EFFECT_MASK = 0xFF000000;
	const uint64 MAY_COMMAND_EFFECT_PARAM_MASK = 0xFF00000000;
	
	const uint MAX_NOTE = ModNote::B9;
	// 
	const uint MAX_INSTRUMENTS = 256;
	
	// mo3 is not its own type, in fact this enum is mainly useful when
	// reading mo3 files :P
	enum ModType {
		MOD,
		S3M,
		XM,
		MTM,
		IT,
		J2B,
	}
	
	// Angelscript bug: if a function is named the same thing as a funcdef
	// type, script loading will sometimes AV/segfault, apparently looking
	// for some member of a null struct pointer.
	// To prevent the user from accidentally triggering this bug, the
	// funcdef type has a ridiculous name.
	funcdef void oNnOtEpLaYeD(uint note, uint instrument, uint volume, uint channel, uint reserved);

	// The returned value is:
	// 1 for C
	// 2 for C#
	// 3 for D
	// 4 for D#
	// 5 for E
	// 6 for F
	// 7 for F#
	// 8 for G
	// 9 for G#
	// 10 for A
	// 11 for A#
	// 12 for B
	//
	// It can also be compared to ModNote::C0 through ModNote::B0.
	int noteToneWithinOctave(uint8 note) {
		return (note-1)%12+1;
	}
	
	// We don't handle sustain loop in samples because nobody uses it.
	enum SampleFlag {
		FLAG_SAMPLE_LOOP,
		FLAG_SAMPLE_BIDI,
		// FLAG_SAMPLE_INVALID means the sample is messed up and we
		// shouldn't try to figure out anything about its playback from
		// its properties. Conditions for this are any of:
		// - Loop/bidi is on, length is not 0, and loopEnd is not
		//   higher than loopStart.
		// - Loop/bidi is on, length is not 0, and loopStart is not
		//   lower than length-1.
		// - Frequency is 0 and length is not 0.
		//
		// Weird properties that ARE acceptable:
		// - loopEnd is greater than length-1 (past the end of the
		//   sample). This just gets clamped to the end.
		// - length is 0. This is just treated the same as a null
		//   sample.
		FLAG_SAMPLE_INVALID,
	}
	
	class MayModDataSample {
		uint freq;
		uint length;
		uint loopStart;
		uint loopEnd;
		uint flags;
		uint globalVolume;
		uint defaultVolume;
		
		MayModDataSample(uint fr, uint s, uint ls, uint le, uint fl, uint gv, uint dv) {
			if (s != 0) {
				// Clamp loop end to end of sample
				if (le >= s) le = s-1;
				
				// Check for 0 frequency or invalid loop parameters
				if ((fr == 0) || ls >= le && (fl & (1 << FLAG_SAMPLE_LOOP | 1 << FLAG_SAMPLE_BIDI) != 0)) {
					flags = (1 << FLAG_SAMPLE_INVALID);
					return;
				}
			}
		
			freq = fr;
			length = s;
			loopStart = ls;
			loopEnd = le;
			flags = fl;
			globalVolume = gv;
			defaultVolume = dv;
		}
	}

	enum InstrumentFlag {
		FLAG_ENVELOPE_VOL,
		FLAG_ENVELOPE_VOL_CARRY,
		FLAG_ENVELOPE_PAN,
		FLAG_ENVELOPE_PAN_CARRY,
		FLAG_ENVELOPE_PITCH,
		FLAG_ENVELOPE_FILTER,
		FLAG_ENVELOPE_PITCHFILTER_CARRY,
	}
	
	enum InstrumentEnvelopeProperty {
		LOOP_START, // -1 means no loop
		LOOP_END,
		SUSTAIN_START, // -1 means no sustain loop
		SUSTAIN_END,
		NUM_ENVELOPE_PROPERTIES,
	}
		
	// envelopes can be efficiently stored as an array of uints with lower
	// bits used for Y and the rest for X
	const uint ENVELOPE_Y_BITS = 0x7F;
	const float ENVELOPE_Y_MAX_FLOAT_INV = 1.0f/64.0f;
	// use OpenMPT fade scale
	const float FADE_MULTIPLIER = 1.0f/32768.0f;
	class MayModDataInstrument {
		uint flags;
		uint globalVolume;
		uint defaultVolume;
		// fade is a 16-bit integer, but we only use it in a float computation
		float fade;
		array<uint> sampleMap(MAX_NOTE); // 0 = no sample, 1 = sample at samplesRef index 0
		array<uint> volumeEnvelope;
		array<int> volumeEnvelopeProps(NUM_ENVELOPE_PROPERTIES);
		array<uint> @intensityEnvelope; // can point to volume envelope
		array<MayModDataSample@> @samplesRef;
		
		MayModDataInstrument(array<MayModDataSample@> @samples) {
			@samplesRef = @samples;
		}
		
		// This is intentionally more precise/smooth than IT reference for some things (like fade), because we want visual effects based on it to be smooth.
		//
		// XXX: this should probably switch from float to double for all its tick-related measurements so that it
		// plays nicely with instruments that play for tens of thousands of ticks (90+ seconds even at fastest
		// tempo)
		float getVolumeAtTick(float tickFloat, uint note, float ticksSinceNoteOffFloat, uint samplesSinceStart) {
			uint sampleIndex1 = sampleMap[note-1];
			if (sampleIndex1 == 0 || (sampleIndex1-1) >= samplesRef.length() || samplesRef[sampleIndex1-1] is null || samplesRef[sampleIndex1-1].flags & (1 << FLAG_SAMPLE_INVALID) != 0) {
				return 0.0f;
			}
			MayModDataSample@ sample = samplesRef[sampleIndex1-1];
			bool sampleIsLooped = sample.flags & ((1 << FLAG_SAMPLE_BIDI) | (1 << FLAG_SAMPLE_LOOP)) != 0;
			if (!sampleIsLooped && samplesSinceStart > sample.length) {
				return 0.0f;
			}
		
			int tick = int(tickFloat);
			int ticksSinceNoteOff = int(ticksSinceNoteOffFloat);
			
			float volume = 1.0f;
			if (fade > 0) {
				volume = 1.0f-ticksSinceNoteOffFloat*FADE_MULTIPLIER*fade;
				if (volume <= 0.0f) return 0.0f; 
				//jjPrint("after fade "+volume);
			}
			volume = volume * (globalVolume/64.0f) * (sample.globalVolume/64.0f);
			// Apply intensity envelope
			if (intensityEnvelope !is null && intensityEnvelope.length() > 0) {
				uint samplePosition;
				// invalid sample check at the beginning of the function already
				// eliminated samples with screwed up loop parameters, so we
				// don't have to verify here that loopEnd > loopStart etc.
				if (sample.flags & (1 << FLAG_SAMPLE_BIDI) != 0) {
					if (samplesSinceStart > sample.loopEnd) {
						uint samplesSinceLoopStart = samplesSinceStart-sample.loopStart;
						uint loopLength = sample.loopEnd-sample.loopStart;
						uint samplesSinceLoopStartMod = samplesSinceLoopStart%(loopLength*2);
						if (samplesSinceLoopStartMod > loopLength) {
							// backwards portion of loop
							samplePosition = sample.loopEnd+loopLength-samplesSinceLoopStartMod;
						} else {
							samplePosition = sample.loopStart+samplesSinceLoopStartMod;
						}
					}
				} else if (sample.flags & (1 << FLAG_SAMPLE_LOOP) != 0) {
					if (samplesSinceStart > sample.loopEnd) {
						samplePosition = sample.loopStart+(samplesSinceStart-sample.loopStart)%(sample.loopEnd-sample.loopStart);
					}
				} else {
					if (samplesSinceStart >= sample.length) return 0.0f;
					samplePosition = samplesSinceStart;
				}
				
				if (intensityEnvelope.length() == 1) {
					volume *= (intensityEnvelope[0] & ~ENVELOPE_Y_BITS)*ENVELOPE_Y_MAX_FLOAT_INV;
				} else {
					// something is probably off by 1 here
					float samplePositionNormalized = samplePosition/float(sample.length);
					float lastEnvelopeX = float(intensityEnvelope[intensityEnvelope.length()-1] & ~ENVELOPE_Y_BITS);
					for (uint point = 0; point < intensityEnvelope.length(); point++) {
						float xNormalized = (intensityEnvelope[point] & ~ENVELOPE_Y_BITS)/lastEnvelopeX;
						if (xNormalized > samplePositionNormalized) {
							// if point == 0 this is an invalid envelope (first point was not at
							// x position 0) so we don't attempt to discern information from it
							if (point != 0) {
								float lastXNormalized = (intensityEnvelope[point-1] & ~ENVELOPE_Y_BITS)/lastEnvelopeX;
								float lastVolume = (intensityEnvelope[point-1] & ENVELOPE_Y_BITS)*ENVELOPE_Y_MAX_FLOAT_INV;
								float nextVolume = (intensityEnvelope[point] & ENVELOPE_Y_BITS)*ENVELOPE_Y_MAX_FLOAT_INV;
								float ratio = (samplePositionNormalized-lastXNormalized)/(xNormalized-lastXNormalized);
								volume *= nextVolume*ratio+lastVolume*(1.0f-ratio);
							}
							break;
						}
					}
				}
				//jjPrint("after intensity envelope "+volume);
			} else {
				// If no intensity envelope, assume constant 100% volume for looped samples
				// (i.e. do nothing here), and linear falloff for unlooped ones.
				if (!sampleIsLooped) {
					volume *= 1.0f-float(samplesSinceStart)/sample.length;
				}
			}
		
			// Apply volume envelope
			if (flags & (1 << FLAG_ENVELOPE_VOL) != 0 && volumeEnvelope.length() > 0) {
				// find the actual x position in the envelope 
				int loopStart = volumeEnvelopeProps[LOOP_START];
				int loopEnd = volumeEnvelopeProps[LOOP_END];
				int sustainStart = volumeEnvelopeProps[SUSTAIN_START];
				int sustainEnd = volumeEnvelopeProps[SUSTAIN_END];
				int ticksBeforeNoteOff = tick-ticksSinceNoteOff;
				
				// if sustain end is first, it needs to be applied first
				// if loop end is first, sustain end will never be reached
				// so sustain can be ignored!
				if (sustainStart != -1 && ticksBeforeNoteOff > sustainEnd && (loopStart == -1 || sustainEnd < loopEnd)) {
					if (sustainEnd > sustainStart) {
						tick = sustainStart+(ticksBeforeNoteOff-sustainStart)%(sustainEnd-sustainStart)+ticksSinceNoteOff;
					} else {
						tick = sustainEnd+ticksSinceNoteOff;
					}
				}
				
				if (loopStart != -1 && tick > loopEnd) {
					if (loopEnd > loopStart) {
						tick = loopStart+(tick-loopStart)%(loopEnd-loopStart);
					} else {
						tick = loopStart;
					}
				}
				
				tickFloat = tick+fraction(tickFloat);
				if (tickFloat == 0.0f) {
					volume *= (volumeEnvelope[0] & ENVELOPE_Y_BITS)*ENVELOPE_Y_MAX_FLOAT_INV;
				} else {
					for (uint point = 0; point < volumeEnvelope.length(); point++) {
						uint x = (volumeEnvelope[point] & ~ENVELOPE_Y_BITS) >> 7;
						if (int(x) > tickFloat) {
							// if point == 0 this is an invalid envelope (first point was not at
							// x position 0) so we don't attempt to discern information from it
							if (point != 0) {
								uint lastX = volumeEnvelope[point-1] & ~ENVELOPE_Y_BITS;
								float lastVolume = (volumeEnvelope[point-1] & ENVELOPE_Y_BITS)*ENVELOPE_Y_MAX_FLOAT_INV;
								float nextVolume = (volumeEnvelope[point] & ENVELOPE_Y_BITS)*ENVELOPE_Y_MAX_FLOAT_INV;
								float ratio = (tickFloat-lastX)/float(x-lastX);
								volume *= nextVolume*ratio+lastVolume*(1.0f-ratio);
							}
							break;
						}
					}
				}
				//jjPrint("after volume envelope "+volume);
			}
			
			return volume;
		}
	}
	
	class MayModDataChannelState {
		uint8 lastNote;
		uint8 lastInstrument;
		uint8 instrumentVolume;
		uint8 channelVolume;
		uint8 panning;
		uint8 effectParam;
		
		uint8 defaultVolume = 64;
		uint8 defaultPanning = 128;
		
		uint8 lastVolumeSlideParam;
		
		// in IT and derived MO3s, (fine) volume slides share
		// parameter memory with each other but not effect column
		uint8 lastITVolumeColumnSlideParam;
		
		// in IT and derived MO3s, volume column portamento
		// and vibrato share memory with effect column
		uint8 lastPortamentoParam;
		uint8 lastVibratoParam;
		
		uint8 lastRetriggerNoteParam;
		
		uint64 lastNoteStartTime;
		uint lastNoteStartTick;
		uint noteOffStartTick;
		
		// needed in addition to lastNoteStartTime due to portamento
		// and sample offset
		// this value is only updated on *tick*.
		uint instrumentSamplesElapsed;
		uint instrumentFrequency;
		
		// resets volume and pan to default, kills everything else
		void reset() {
			lastNote = 0;
			lastInstrument = 0;
			instrumentVolume = 0;
			channelVolume = defaultVolume;
			panning = defaultPanning;
			effectParam = 0;
			lastNoteStartTime = 0;
			lastNoteStartTick = 0;
			noteOffStartTick = 0;
			lastVolumeSlideParam = 0;
			lastITVolumeColumnSlideParam = 0;
			lastRetriggerNoteParam = 0;
			instrumentSamplesElapsed = 0;
			instrumentFrequency = 0;
		}
		
		void setInstrumentVolume(uint new) {
			instrumentVolume = new > 64 ? 64 : new;
		}
		
		void modifyInstrumentVolume(int delta) {
			if (-delta > int(instrumentVolume)) {
				instrumentVolume = 0;
			} else if (delta+int(instrumentVolume) > 64) {
				instrumentVolume = 64;
			} else {
				instrumentVolume += delta;
			}
		}
	}
	
	// Also used as our internal effect representation.
	enum IT_EFFECTS {
		IT_EFFECT_NONE,
		IT_EFFECT_SET_SPEED,
		IT_EFFECT_POSITION_JUMP,
		IT_EFFECT_PATTERN_BREAK,
		IT_EFFECT_VOLUME_SLIDE,
		IT_EFFECT_PORTAMENTO_DOWN,
		IT_EFFECT_PORTAMENTO_UP,
		IT_EFFECT_TONE_PORTAMENTO,
		IT_EFFECT_VIBRATO,
		IT_EFFECT_TREMOR,
		IT_EFFECT_ARPEGGIO,
		IT_EFFECT_VOLSLIDE_VIBRATO,
		IT_EFFECT_VOLSLIDE_TONE_PORTAMENTO,
		IT_EFFECT_SET_CHANNEL_VOLUME,
		IT_EFFECT_CHANNEL_VOLUME_SLIDE,
		IT_EFFECT_SET_OFFSET,
		IT_EFFECT_PANNING_SLIDE,
		IT_EFFECT_RETRIGGER_NOTE,
		IT_EFFECT_TREMOLO,
		IT_EFFECT_SXX,
		IT_EFFECT_SET_TEMPO,
		IT_EFFECT_FINE_VIBRATO,
		IT_EFFECT_SET_GLOBAL_VOLUME,
		IT_EFFECT_GLOBAL_VOLUME_SLIDE,
		IT_EFFECT_SET_PANNING,
		IT_EFFECT_PANBRELLO,
		IT_EFFECT_MIDI_MACRO,
	}
	
	// IT volume column commands are stored as 1 greater than the IT version
	// (so that 0 can mean no volume command)
	const uint8 MAY_VOLUME_COMMAND_NOTHING = 0;
	const uint8 MAY_VOLUME_COMMAND_SETVOL_START = 1;
	const uint8 MAY_VOLUME_COMMAND_SETVOL_END = 65;
	const uint8 MAY_VOLUME_COMMAND_FINEVOL_UP_START = 66;
	const uint8 MAY_VOLUME_COMMAND_FINEVOL_UP_END = 75;
	const uint8 MAY_VOLUME_COMMAND_FINEVOL_DOWN_START = 76;
	const uint8 MAY_VOLUME_COMMAND_FINEVOL_DOWN_END = 85;
	const uint8 MAY_VOLUME_COMMAND_VOLSLIDE_UP_START = 86;
	const uint8 MAY_VOLUME_COMMAND_VOLSLIDE_UP_END = 95;
	const uint8 MAY_VOLUME_COMMAND_VOLSLIDE_DOWN_START = 96;
	const uint8 MAY_VOLUME_COMMAND_VOLSLIDE_DOWN_END = 105;
	const uint8 MAY_VOLUME_COMMAND_PORTAMENTO_DOWN_START = 106;
	const uint8 MAY_VOLUME_COMMAND_PORTAMENTO_DOWN_END = 115;
	const uint8 MAY_VOLUME_COMMAND_PORTAMENTO_UP_START = 116;
	const uint8 MAY_VOLUME_COMMAND_PORTAMENTO_UP_END = 125;
	const uint8 MAY_VOLUME_COMMAND_TONE_PORTAMENTO_START = 194;
	const uint8 MAY_VOLUME_COMMAND_TONE_PORTAMENTO_END = 203;
	const uint8 MAY_VOLUME_COMMAND_VIBRATO_START = 204;
	const uint8 MAY_VOLUME_COMMAND_VIBRATO_END = 213;
	// XM volume column commands that lack IT equivalents, placed arbitrarily
	const uint8 MAY_VOLUME_COMMAND_SET_VIBRATO_SPEED_START = 126;
	const uint8 MAY_VOLUME_COMMAND_SET_VIBRATO_SPEED_END = 141;
	const uint8 MAY_VOLUME_COMMAND_PANNING_SLIDE_LEFT_START = 142;
	const uint8 MAY_VOLUME_COMMAND_PANNING_SLIDE_LEFT_END = 157;
	const uint8 MAY_VOLUME_COMMAND_PANNING_SLIDE_RIGHT_START = 158;
	const uint8 MAY_VOLUME_COMMAND_PANNING_SLIDE_RIGHT_END = 173;
	
	class MayModData {
		private string filename;
		private string author; // specified by song message
		private string originalFilename; // specified by song message
		private string songName;
		private string songMessage;
		private bool hasValidFile = false;
		private array<uint8> orders;
		private array<uint16> patternLengths;
		private array<MayModDataSample@> samples; // 0-indexed!
		private array<MayModDataInstrument@> instruments; // 0-indexed!
		
		private uint globalVolume;
		
		private ModType modType;
		
		// set to true while changing music files, stops hooks from
		// triggering or music data otherwise being read.
		// doesn't really mean anything right now because the current
		// code just blocks until it's done reading the file, but I can
		// fantasize about reading the file over several frames in a
		// future version.
		private bool suppressHooks = false;
		
		// If true, the script will use the first disabled envelope on
		// each instrument as a guide to the volume of its sample (see
		// the section about "VOLUMEGUIDES" below).
		//
		// This is false by default and only enabled for mods with the
		// "VOLUMEGUIDES" line in their song message, as it will provide
		// nonsensical results for normal modules.
		//
		// This does not affect how *enabled* volume envelopes are used.
		private bool useDisabledEnvelopesAsGuides;
		
		// represent patterns as arrays of rows, which are arrays of uint64s
		// 0x??????5544332211
		// 11: note (uint8)
		// 22: instrument (uint8)
		// 33: volume command (uint8)
		// 44: effect command (uint8)
		// 55: effect parameter (uint8)
		// ??: unused right now
		private array<array<array<uint64>>> patterns;
		
		// I pretend virtual channels don't exist because I don't want
		// to implement support for them.
		private array<MayModDataChannelState@> currentChannelStates;
		
		private int currentOrder = -1;
		private int currentRow = -1;
		private int currentRowTick = -1; // tick index within current row
		
		// The (Fine) Pattern Delay command can make a row last more ticks
		// than the mod speed parameter would suggest.
		private uint currentRowPatternDelayTicks = 0;
		
		private uint channelCount = 0;
		
		// fastest tempo is 102 ticks per second, so this gives us over
		// 10,000 hours of playtime before it wraps
		private uint elapsedTicks = 0;
		
		private uint64 lastUpdateTime = 0;
		private uint64 thisUpdateTime = 0;
		private uint64 lastRowStartTime = 0;
		private uint64 predictedNextRowTime = 0;
		
		private oNnOtEpLaYeD@ notePlayedHook;
		
		// order numbers for these can be specified in the song message
		private int pregameOrder = -1;
		private int gameOrder = -1;
		private int overtimeOrder = -1;
		private int winOrder = -1;
		private int lossOrder = -1;
		private int tieOrder = -1;
		private int endOrder = -1;
		
		private bool debugDisplay = false;
		private bool debugLoadingTime = true;
		
		private int lastGameState = -1;
		
		// I don't support "sample mode" ITs/MO3s yet anyway, but I also
		// don't load instruments from MO3s yet, so this is needed...
		private bool hasInstrumentInfo = false;
		
		MayModData() {
		
		}
	
		MayModData(const string &in inFilename) {
			loadMusicFile(inFilename);
		}
		
		private bool _substringEqual(const string &in a, const string &in b, uint start) {
			uint len = b.length();
			if (a.length() < start+len) return false;
			for (uint i = 0; i < len; i++) {
				if (a[start+i] != b[i]) return false;
			}
			return true;
		}
		
		private bool _hasITHeader(const string &in filedata) {
			// currently I only support "IMPM" IT files. There is at
			// least one other header in use but I do not wanna try
			// to support it.
			return _substringEqual(filedata, "IMPM", 0);
		}
		
		private bool _hasXMHeader(const string &in filedata) {
			return _substringEqual(filedata, "Extended Module: ", 0);
		}
		
		bool loadMusicFile(const string &in inFilename) {
			if (hasValidFile) {
				jjPrint("MayModData: A song is already loaded. Create a new MayModData to load a new song.");
				return false;
			}
			jjSTREAM stream = jjSTREAM(inFilename);
			uint filesize = stream.getSize();
			if (filesize > 0) { // File might not exist
				string data;
				if (!stream.get(data, stream.getSize())) return false;

				if (_substringEqual(data, "MO3", 0)) { // "MO3"
					if (_loadMO3(data)) {
						filename = inFilename;
						_onFileLoaded();
						return true;
					} else {
						jjPrint("MayModData: Error parsing MO3: " + inFilename);
						return false;
					}
				} else if (_hasITHeader(data)) {
					if (_loadIT(data)) {
						filename = inFilename;
						_onFileLoaded();
						return true;
					} else {
						jjPrint("MayModData: Error parsing IT: " + inFilename);
						return false;
					}					
				} else if (_hasXMHeader(data)) {
					if (_loadXM(data)) {
						filename = inFilename;
						_onFileLoaded();
						return true;
					} else {
						jjPrint("MayModData: Error parsing XM: " + inFilename);
						return false;
					}
				} else {
					jjPrint("MayModData: File format not supported: " + inFilename);
					return false;
				}
			} else {
				jjPrint("MayModData: File not found: " + inFilename);
				return false;
			}
		}
		
		private void _onFileLoaded() {
			hasValidFile = true;
			currentOrder = -1;
			currentRow = -1;
			currentRowTick = 0;
			elapsedTicks = 0;
			lastUpdateTime = 0;
		}
		
		// Call this once, every time, from your onMain().
		void updateOnMain() {
			thisUpdateTime = jjUnixTimeMs();
			if (hasValidFile) {
				int newOrder = jjGetModOrder();
				if (newOrder == -1) { // Music stopped playing, or hasn't started.
					currentOrder = -1;
					currentRow = -1;
					currentRowTick = 0;
					elapsedTicks = 0;
					predictedNextRowTime = 0;
				} else {
					int newRow = jjGetModRow();
					int ticksPerRow = jjGetModSpeed();
					int tempo = jjGetModTempo();
					bool isNewRow = (newOrder != currentOrder || newRow != currentRow);
					if (currentOrder == -1) {
						// We just started playing, reset stuff.
						_resetChannelStates();
						predictedNextRowTime = 0;
					} else {
						// Process new ticks of an already-started row?
						// (Tick duration is 2.5 seconds / tempo)
						int maxRowTick = ticksPerRow+currentRowPatternDelayTicks;
						// if we're moving to a new row, process all remaining ticks of the last row
						int predictedRowTick = isNewRow ? maxRowTick : (1+(thisUpdateTime-lastRowStartTime)*tempo/2500);
						if (predictedRowTick > maxRowTick) {
							predictedRowTick = maxRowTick;
						}
						while (currentRowTick < predictedRowTick) {
							_processTick();
						}
					}
					if (isNewRow && newOrder < int(orders.length())) {
						// This is where you'd put code to figure out if the mod advanced by
						// multiple rows between this update and the last update, and process the
						// rows it passed over if so, and decide you really hate the Pattern Loop command.
						//
						// However, that's totally unnecessary in practice. The fastest mod I have that
						// isn't just a gimmick is Substitutionology at 33.3 rows per second. onMain()
						// gets called more than twice per row of that fastest mod, unless JJ2's framerate
						// is seriously choking (in which case onMain() will still be called 70 times per
						// second but the time delay between onMain() calls becomes less stable). And in
						// that case, the player has bigger worries than the music based visual effects
						// not working perfectly.
						
						// The predicted next row time can be way off if jjSetModPosition() was called
						// without going through our wrapper, something weird happened with music playback,
						// the tempo/speed is changing, or the script just didn't interpret a row correctly.
						//
						// So if our predicted next row time is even later than the current time, or was
						// too early by more than 16 ms (a bit more than the JJ2 update interval), ignore
						// it and use the current time - it's too late by between 0 and like 15 ms, which
						// is tolerable.
						if (thisUpdateTime < predictedNextRowTime || (thisUpdateTime-predictedNextRowTime > 16)) {
							lastRowStartTime = thisUpdateTime;
						} else {
							lastRowStartTime = predictedNextRowTime;
						}
						
						_processNewRow(newOrder, newRow);
						currentOrder = newOrder;
						currentRow = newRow;
						currentRowTick = 0;
						// Process ticks of the new row (or at least one tick, anyway).
						int predictedRowTick = 1+(thisUpdateTime-lastRowStartTime)*tempo/2500;
						int maxRowTick = ticksPerRow+currentRowPatternDelayTicks;
						if (predictedRowTick > maxRowTick) {
							predictedRowTick = maxRowTick;
						}
						while (currentRowTick < predictedRowTick) {
							_processTick();
						}
					}
				}
				
				int jumpOrder = -1;
				if (jjGameState != lastGameState) {
					int wantedOrder = _determineOrderForGameState(jjGameState);
					int lastOrder = _determineOrderForGameState(lastGameState);
					if (wantedOrder != -1 && wantedOrder != lastOrder) {
						setModPosition(wantedOrder, 0, false);
					}
					
					lastGameState = jjGameState;
				}
			}
			lastUpdateTime = thisUpdateTime;
			if (debugDisplay) {
				_drawDebugDisplay();
			}
		}
		
		// Wrapper for jjSetModPosition that doesn't screw up the state
		// of the MayModData as much as a raw jjSetModPosition call.
		void setModPosition(int order, int row, bool reset) {
			if (isCurrentMusic()) {
				currentOrder = order;
				currentRow = row;
				currentRowTick = 0;
				if (reset) {
					_resetChannelStates();
				}
				lastRowStartTime = jjUnixTimeMs();
			}
			jjSetModPosition(order, row, reset);
		}
		
		// Set a function that will be called whenever a note command is
		// reached in the module. Will receive the note, instrument, and
		// channel numbers. The instrument and channel are 1-indexed.
		// The note could be NOTE_OFF/CUT/FADE (todo change how these are handled?)
		void setNotePlayedHook(oNnOtEpLaYeD@ f) {
			@notePlayedHook = f;
		}
		
		void exampleNotePlayedHookFlowers(uint note, uint instrument, uint volume, uint channel, uint reserved) {
			jjPARTICLE@ particle = jjAddParticle(PARTICLE::FLOWER);
			particle.xPos = jjP.cameraX + channel * jjResolutionWidth / 32;
			particle.yPos = jjP.cameraY + jjResolutionHeight * 2 / 3 + (note-60)*2;
			particle.ySpeed = -1;
			particle.flower.color = instrument+1;
			particle.flower.size = 128;			
		}
		
		bool isCurrentMusic() {
			return filename == jjMusicFileName;
		}
		
		float getEstimatedChannelVolume(uint channel) {
			// this fails silently because the PLAYER can make it
			// fail with /changemusic, -nosound, etc.
			if (!hasInstrumentInfo || jjGetModOrder() == -1 || !hasValidFile || channel < 1 || channel > channelCount) {
				return 0.0f;
			}
			MayModDataChannelState@ state = currentChannelStates[channel-1];
			uint8 note = state.lastNote;
			if (state.lastNote == 0 || state.lastInstrument == 0) {
				return 0.0f;
			}
			MayModDataInstrument@ instrument = instruments[state.lastInstrument-1];
			uint64 timeSinceStart = thisUpdateTime-state.lastNoteStartTime;
			uint ticksSinceStart = elapsedTicks-state.lastNoteStartTick;
			float tickDuration = 2.5f/jjGetModTempo();
			
			// No, you didn't read that wrong: it really doesn't check if our
			// assumed-from-time tick is even the correct tick.
			float tickFraction = fraction((thisUpdateTime-lastRowStartTime)*0.001f/tickDuration);
			
			float ticksSinceNoteOffFloat;
			if (state.noteOffStartTick <= state.lastNoteStartTick) {
				ticksSinceNoteOffFloat = 0.0f;
			} else {
				ticksSinceNoteOffFloat = elapsedTicks-state.lastNoteStartTick+tickFraction;
			}
			uint samplesSinceStart = state.instrumentSamplesElapsed;
			samplesSinceStart += uint(state.instrumentFrequency*tickDuration*tickFraction);
			return state.channelVolume*state.instrumentVolume/4096.0f*instrument.getVolumeAtTick(ticksSinceStart+tickFraction, note, ticksSinceNoteOffFloat, samplesSinceStart);
		}
		
		// This can be greater than 1 because the instrument could be
		// playing in several channels.
		float getEstimatedInstrumentVolume(uint instrument) {
			float rval = 0.0f;
			for (uint channel = 0; channel < channelCount; channel++) {
				if (currentChannelStates[channel].lastInstrument == instrument) {
					rval += getEstimatedChannelVolume(channel+1);
				}
			}
			return rval;
		}
		
		// Returns a handle to an array of all note values used by this instrument (all
		// instruments if passed instrument is 0) in this channel (all
		// chanenls is passed channel is 0), sorted by ascending order.
		//
		// This traverses all used patterns in the module. Call it once after loading
		// the mod and keep the returned array instead of calling it
		// on the fly.
		array<uint8>@ getNoteRange(uint onlyInstrument, uint onlyChannel) {
			array<uint8> rval(0);
			if (_warnInvalidFile()) return @rval;
			
			array<uint> usedPatternsKey(8, 0);
			for (uint i = 0; i < orders.length(); i++) {
				uint8 pattern = orders[i];
				if (pattern < patterns.length()) {
					usedPatternsKey[pattern >> 5] |= 1 << (pattern & 31);
				}
			}
			array<uint> usedNotesKey(8, 0);
			for (uint i = 0; i < patterns.length(); i++) {
				if (usedPatternsKey[i >> 5] & (1 << (i & 31)) != 0) {
					for (uint row = 0; row < patterns[i].length(); row++) {
						for (uint channel = 0; channel < patterns[i][row].length(); channel++) {
							if (onlyChannel == 0 || channel == onlyChannel-1) {
								uint instrument = (patterns[i][row][channel] & 0xFF00) >> 8;
								uint note = patterns[i][row][channel] & 0xFF;
								if (note > 0 && note <= MAX_NOTE && (onlyInstrument == 0 || instrument == onlyInstrument)) {
									usedNotesKey[note >> 5] |= 1 << (note & 31);
								}
							}
						}
					}
				}
			}
			for (uint8 note = 0; note <= MAX_NOTE; note++) {
				if (usedNotesKey[note >> 5] & (1 << (note & 31)) != 0) {
					rval.insertLast(note);
				}
			}
			return @rval;
		}
		
		// another extremely slow thing you'd never want to use in a level
		// (can obviously be made faster by duplicating some code or redesigning getNoteRange()
		// but i don't care)
		// can identify rarely used instruments
		void dumpNoteRanges() {
			for (uint instrument = 0; instrument < instruments.length(); instrument++) {
				array<uint8>@ notes = getNoteRange(instrument+1, 0);
							
				if (notes.length() == 0) {
					jjPrint("Instrument "+(instrument+1)+" unused");
				} else {
					array<uint8> instPatterns();
					for (uint i = 0; i < patterns.length(); i++) {
						for (uint row = 0; row < patterns[i].length(); row++) {
							bool found = false;
							for (uint channel = 0; channel < patterns[i][row].length(); channel++) {
								uint instrument2 = (patterns[i][row][channel] & 0xFF00) >> 8;
								uint note = patterns[i][row][channel] & 0xFF;
								if (note > 0 && note <= MAX_NOTE && instrument2 == instrument+1) {
									instPatterns.insertLast(i);
									found = true;
									break;
								}
							}
							if (found) break;
						}
					}
					string patternList = "";
					for (uint i = 0; i < instPatterns.length(); i++) {
						patternList += instPatterns[i]+" ";
					}
					jjPrint("Instrument "+(instrument+1)+" range: "+_noteToString(notes[0])+" to "+_noteToString(notes[notes.length()-1])+" Patterns: "+patternList);
				}
			}
		}
		
		private int _determineOrderForGameState(int gameState) {
			if (gameState == GAME::OVERTIME) {
				if (overtimeOrder != -1) return overtimeOrder;
				return gameOrder;
			} else if (gameState == GAME::STARTED || gameState == GAME::PAUSED) {
				return gameOrder;
			} else if (gameState == GAME::PREGAME || gameState == GAME::STOPPED) {
				// TODO: complicated logic for GAME::STOPPED to determine if the
				// game was won and who won it and this may not even be possible
				// to determine from a script with wins by timeout in some game modes?
				return pregameOrder;
			}
			// If we get here it's an unknown game state.
			return -1;
		}
		
		private void _drawDebugDisplay() {
			for (uint channelIndex = 1; channelIndex <= channelCount; channelIndex++) {
				float centerX = float(channelIndex-1) / (channelCount-1) * (jjResolutionWidth - 20)+10;
				int width = jjResolutionWidth/channelCount-2;
				int barHeight = jjResolutionHeight/2;
				if (width < 1) {
					width = 1;
				} else if (width > 16) {
					width = 16;
				}
				float vol = getEstimatedChannelVolume(channelIndex);
				float startX = jjLocalPlayers[0].cameraX;
				float startY = jjLocalPlayers[0].cameraY;
				if (vol > 0.0f) {
					jjDrawRectangle(startX+centerX-width/2, startY+barHeight*(1.0f-vol), width, int(barHeight*vol), 16);
					jjDrawString(startX+centerX-8, startY+barHeight+8, ""+channelIndex);
					jjDrawString(startX+centerX-8, startY+barHeight+18, ""+currentChannelStates[channelIndex-1].lastInstrument);

					string noteString = _noteToString(currentChannelStates[channelIndex-1].lastNote, true);
					jjDrawString(startX+centerX-12, startY+barHeight+28+(channelIndex & 1 == 1 ? 10 : 0), noteString);
				}
			}
		}
		
		private void _resetChannelStates() {
			for (uint i = 0; i < channelCount; i++) {
				currentChannelStates[i].reset();
			}
		}
		
		private void _processNewRow(uint order, uint row) {
			// If the music actually being played doesn't match the loaded file, these could be out of bounds.
			if (order >= orders.length()) return;
			uint8 patternIndex = orders[order];
			if (patternIndex >= patterns.length() || row >= patterns[patternIndex].length()) return;
			
			array<uint64> rowdata = patterns[patternIndex][row];
			
			int rowChannelCount = rowdata.length();
			bool gotPatternDelay = false; // only the first pattern delay effect (but not fine pattern delay!) is used
			for (int i = 0; i < rowChannelCount; i++) {
				uint64 channelCommand = rowdata[i];
				uint note = channelCommand & 0xFF;
				uint instrument = (channelCommand & 0xFF00) >> 8;
				uint volumeCommand = (channelCommand & 0xFF0000) >> 16;
				uint effect = (channelCommand & 0xFF000000) >> 24;
				uint effectParam = (channelCommand & 0xFF00000000) >> 32;
				
				// TODO: handle other formats' effects
				if (modType == IT) {
					switch(effect) {
						case IT_EFFECT_SET_CHANNEL_VOLUME:
							if (effectParam <= 64) currentChannelStates[i].channelVolume = effectParam;
							break;
						case IT_EFFECT_SET_GLOBAL_VOLUME:
							if (effectParam <= 128) globalVolume = effectParam;
							break;
						case IT_EFFECT_SXX:
							if (effectParam & 0xF0 == 0xE0) { // Pattern Delay
								if (!gotPatternDelay) {
									gotPatternDelay = true;
									currentRowPatternDelayTicks += (effectParam & 0x0F) * jjGetModSpeed();
								}
							} else if (effectParam & 0xF0 == 0x60) { // Fine Pattern Delay
								currentRowPatternDelayTicks += effectParam & 0x0F;
							}
							break;
						default:
							break;
					}
				}
				
				if (volumeCommand >= MAY_VOLUME_COMMAND_FINEVOL_UP_START
				&& volumeCommand <= MAY_VOLUME_COMMAND_VOLSLIDE_DOWN_END) {
					uint8 param = (volumeCommand-MAY_VOLUME_COMMAND_FINEVOL_UP_START) % 10;
					if (param != 0) {
						currentChannelStates[i].lastITVolumeColumnSlideParam = param;
					}
				}
			}
			
			predictedNextRowTime = thisUpdateTime+2500*(jjGetModSpeed()+currentRowPatternDelayTicks)/jjGetModTempo();
		}
		
		private bool _sampleExists(uint8 sample) {
			return sample != 0 && sample <= samples.length() && samples[sample-1].length != 0;
		}
		
		private bool _instrumentExists(uint8 instrument) {
			return !hasInstrumentInfo || (instrument != 0 && instrument <= instruments.length() && instruments[instrument-1] !is null);
		}
		
		private bool _instrumentCanPlayNote(uint8 instrument, uint8 note) {
			return !hasInstrumentInfo || (note != 0 && note <= MAX_NOTE && _instrumentExists(instrument) && _sampleExists(instruments[instrument-1].sampleMap[note-1]));
		}
		
		// XXX: this is probably off by 1 a lot
		private uint _getFrequencyForNote(uint8 instrument, uint8 note) {
			uint rval = hasInstrumentInfo ? samples[instruments[instrument-1].sampleMap[note-1]-1].freq : 44100;
			return uint(2.0f**((float(note)-ModNote::C5)/12.0f)*rval);
		}
		
		private void _triggerNote(uint8 note, uint channel, uint8 instrument, uint64 startTime) {
			if (note == 0) {
				note = currentChannelStates[channel].lastNote;
			}

			if (instrument == 0) {
				instrument = currentChannelStates[channel].lastInstrument;
			}

			if (note != 0 && instrument != 0 && _instrumentCanPlayNote(instrument, note)) {
				currentChannelStates[channel].lastInstrument = instrument;
				currentChannelStates[channel].lastNote = note;
				currentChannelStates[channel].lastNoteStartTime = startTime;
				currentChannelStates[channel].lastNoteStartTick = elapsedTicks;
				currentChannelStates[channel].instrumentSamplesElapsed = 0;
				currentChannelStates[channel].instrumentFrequency = _getFrequencyForNote(instrument, note);
			}
		}
		
		private void _processTick() {
			uint8 patternIndex = orders[currentOrder];
			if (patternIndex >= patterns.length() || currentRow >= int(patterns[patternIndex].length())) {
				currentRowTick++;
				elapsedTicks++;
				return;
			}
			int tempo = jjGetModTempo();
			uint64 tickTime = lastRowStartTime+currentRowTick*2500/tempo;
			
			array<uint64> rowdata = patterns[patternIndex][currentRow];
			
			uint rowChannelCount = rowdata.length();
			if (rowChannelCount > channelCount) rowChannelCount = channelCount;
			for (uint i = 0; i < rowChannelCount; i++) {
				uint64 channelCommand = rowdata[i];
				uint note = channelCommand & 0xFF;
				uint instrument = (channelCommand & 0xFF00) >> 8;
				uint volumeCommand = (channelCommand & 0xFF0000) >> 16;
				uint effect = (channelCommand & 0xFF000000) >> 24;
				uint effectParam = (channelCommand & 0xFF00000000) >> 32;
				
				if (note == ModNote::CUT) {
					// Yes, this is how it works! For example,
					// Retrigger Note after a note cut will do nothing.
					currentChannelStates[i].lastNote = 0;
				
				// There are cases where instrument command + no note command
				// is different from instrument command + last note command, but
				// I really don't wanna deal with them so let's pretend they
				// don't exist.
				} else if (note == 0 && instrument != 0) {
					note = currentChannelStates[i].lastNote;
				}
				
				// account for note delay effect. the rare and exciting logical xor
				bool isNoteTick = currentRowTick == 0 ^^ ((note > 0 && note <= MAX_NOTE) && effect == IT_EFFECT_SXX && (effectParam & 0xF0 == 0xD0) && (effectParam & 0x0F == uint(currentRowTick)));
				
				if (effect == IT_EFFECT_RETRIGGER_NOTE) {
					if (note == 0 || note > MAX_NOTE || currentRowTick != 0) {
						uint param = effectParam;
						if (param == 0) {
							param = currentChannelStates[i].lastRetriggerNoteParam;
						} else {
							currentChannelStates[i].lastRetriggerNoteParam = param;
						}
						uint delay = param & 0xF;
						if (delay == 0) delay = 1;
						if (currentRowTick % delay == 0) {
							uint volParam = (param & 0xF0) >> 4;
							if (volParam != 0 && volParam != 8) {
								if (volParam < 6) {
									currentChannelStates[i].modifyInstrumentVolume(-(2**(volParam-1)));
								} else if (volParam < 8) {
									currentChannelStates[i].setInstrumentVolume(currentChannelStates[i].instrumentVolume * 2 / (volParam == 6 ? 3 : 4));
								} else if (volParam < 14) {
									currentChannelStates[i].modifyInstrumentVolume(2**(volParam-9));
								} else {
									currentChannelStates[i].setInstrumentVolume(uint(currentChannelStates[i].instrumentVolume) * (volParam == 14 ? 3 : 4) / 2);
								}
							}
							_triggerNote(note, i, instrument, tickTime);
						}
					}
				} else if (effect == IT_EFFECT_VOLUME_SLIDE) {
					uint param = effectParam == 0 ? currentChannelStates[i].lastVolumeSlideParam : effectParam;
					currentChannelStates[i].lastVolumeSlideParam = param;
					uint up = (effectParam & 0xF0) >> 4;
					uint down = effectParam & 0x0F;				
					// volume slides without a 0 or F on either side are undefined
					// as is 0 on both sides without a previous param
					if (param == 0 || up == 0 || up == 0xF || down == 0 || down == 0xF) {
						bool fine = up == 0xF || down == 0xF;
						// Fine means the volume slide only happens on the first tick of the row.
						// Non-fine volume slides have no effect on the first tick of the row!
						// (How did this end up being the standard???)
						// also, the rare and exciting logical xor
						if (fine ^^ (currentRowTick != 0)) {
							int delta = (up != 0xF || up != 0) ? up : down;
							currentChannelStates[i].modifyInstrumentVolume(delta);
						}
					}
				}
				// TODO: handle portamento effects
				// TODO: handle volume column stuff (other than set volume which is handled)
				
				if (isNoteTick) {
					bool setVolume = volumeCommand >= MAY_VOLUME_COMMAND_SETVOL_START && volumeCommand <= MAY_VOLUME_COMMAND_SETVOL_END;
					if (setVolume) {
						currentChannelStates[i].instrumentVolume = volumeCommand-1;
					}
					if (note > 0 && note <= MAX_NOTE) {
						if (_instrumentCanPlayNote(instrument, note)) {
							if (!setVolume) {
								currentChannelStates[i].instrumentVolume = hasInstrumentInfo ? instruments[instrument-1].defaultVolume : 64;
							}
							_triggerNote(note, i, instrument, tickTime);
						}
					}
				}
				
				if (currentChannelStates[i].lastNoteStartTick != elapsedTicks) {
					currentChannelStates[i].instrumentSamplesElapsed += currentChannelStates[i].instrumentFrequency*25/(tempo*10);
				}
				
				if (note > 0 && note <= MAX_NOTE) {
					currentChannelStates[i].lastNote = note;
				}
			}
			
			// trigger note played hook after all channels have been
			// processed, otherwise users would get strange results
			// if they requested the volume of channel 8 from their
			// note hook that was triggered by channel 7
			for (uint i = 0; i < rowChannelCount; i++) {
				if (currentChannelStates[i].lastNoteStartTick == elapsedTicks && currentChannelStates[i].lastInstrument != 0 && currentChannelStates[i].lastNote != 0) {
					// sample volume, instrument "global" volume, and volume envelope are not included here
					// (in most applications you don't want them to be).
					//jjPrint("instrument volume: "+currentChannelStates[i].instrumentVolume+" channel volume: "+currentChannelStates[i].channelVolume+" global volume: "+globalVolume);
					uint volume = (currentChannelStates[i].instrumentVolume*currentChannelStates[i].channelVolume*globalVolume) >> 13;
					notePlayedHook(currentChannelStates[i].lastNote, currentChannelStates[i].lastInstrument, volume, i+1, 0);
				}
			}
			
			currentRowTick++;
			elapsedTicks++;
		}
		
		private bool _warnInvalidFile() {
			if (!hasValidFile) {
				jjPrint("Warning: MayModData does not have a valid module file loaded");
				return true;
			}
			return false;
		}
		
		private string _noteToString(uint note, bool tiny = false) {
			if (note == 0) return tiny ? "" : "---";
			if (note == ModNote::OFF) return tiny ? "=" : "== ";
			if (note == ModNote::CUT) return tiny ? "^" : "^^ ";
			if (note == ModNote::FADE) return tiny ? "~" : "~~ ";
			return (tiny ? NOTE_LETTERS_SMALL : NOTE_LETTERS)[(note-1) % 12] + ((note-1) / 12);
		}
		
		// For debugging, useful to verify a mod has been loaded correctly.
		// Not useful for anything else, printing this much text makes the
		// chatlog choke.
		void dumpPattern(uint patternIndex, bool compact) {
			if (_warnInvalidFile()) return;
			
			if (patternIndex >= patterns.length()) {
				jjPrint("Warning: dumpPattern index " + patternIndex + " out of range");
				return;
			}
			
			uint patternLength = patterns[patternIndex].length();
			for (uint rowi = 0; rowi < patternLength; rowi++) {
				array<uint64> row = patterns[patternIndex][rowi];
				string rowStr = "";
				for (uint channel = 0; channel < channelCount; channel++) {
					bool empty = channel >= row.length();
					uint note = empty ? 0 : (row[channel] & MAY_COMMAND_NOTE_MASK);
					uint instrument = empty ? 0 : ((row[channel] & MAY_COMMAND_INSTRUMENT_MASK) >> 8);
					uint volume = empty ? 0 : ((row[channel] & MAY_COMMAND_VOLUME_MASK) >> 16);
					if (compact) {
						if (note == 0 && volume > 0 && volume <= 64) {
							rowStr += " " + formatUInt(volume, "0", 2) + " ";
						} else {
							rowStr += _noteToString(note) + " ";
						}
					} else {
						rowStr += _noteToString(note) + " " + (instrument > 0 ? formatUInt(instrument, "0", 3) : "-- ") + " " + ((volume > 0 && volume <= 64) ? formatUInt(volume, "0", 2) : "--") + " ";
					}
				}
				jjPrint(rowStr);
			}
			jjPrint("Dumped "+patternLength+" rows");
		}
		
		private uint _nextLine(const string &in str, uint startIndex) {
			uint len = str.length();
			while (startIndex < len && str[startIndex] != 0xA) {
				startIndex++;
			}
			return startIndex+1;
		}
		private bool _allWhitespaceLine(const string &in str, uint startIndex) {
			uint len = str.length();
			while (startIndex < len && str[startIndex] != 0xA) {
				// tab, carriage return, space
				if (str[startIndex] == 0x9 || str[startIndex] == 0xD || str[startIndex] == 0x20) {
					startIndex++;
				} else {
					return false;
				}
			}
			return true;			
		}
		
		private bool _readMessageFieldString(const uint index, string &out dest, const string &in fieldName) {
			if (_substringEqual(songMessage, fieldName+" ", index)) {
				uint valueStartIndex = index+fieldName.length()+1;
				dest = songMessage.substr(valueStartIndex, _nextLine(songMessage, valueStartIndex)-valueStartIndex);
				return true;
			} else {
				return false;
			}
		}
		
		private bool _readMessageFieldOrder(const uint index, int &out dest, const string &in fieldName) {
			if (_substringEqual(songMessage, fieldName+" ", index)) {
				uint valueStartIndex = index+fieldName.length()+1;
				int order = parseInt(songMessage.substr(valueStartIndex, _nextLine(songMessage, valueStartIndex)-valueStartIndex));
				if (order < 0 || order >= int(orders.length())) {
					jjPrint("MayModData: invalid "+fieldName+" order number: "+order);
					// while we didn't actually assign, we still read the correct field;
					// do not return false here
				} else {
					dest = order;
				}
				return true;
			} else {
				return false;
			}
		}
		
		private void _processSongMessage() {
			if (songMessage.length() > 0) {
				uint index = 0;
				uint messageLength = songMessage.length();
				// skip lines until we find this one
				while (!_substringEqual(songMessage, "INFO FOR JJ2+ SCRIPT", index)) {
					index = _nextLine(songMessage, index);
					if (index >= messageLength) return;
				}
				
				// read remaining lines as instructions
				while ((index = _nextLine(songMessage, index)) < messageLength) {
					if (_substringEqual(songMessage, "VOLUMEGUIDES", index)) {
						useDisabledEnvelopesAsGuides = true;
					} else {
						if (_allWhitespaceLine(songMessage, index)) {
							// end info section
							break;
						} else if (_substringEqual(songMessage, "//", index) ||
						_readMessageFieldString(index, author, "AUTHOR") ||
						_readMessageFieldString(index, originalFilename, "ORIGINAL") ||
						_readMessageFieldOrder(index, gameOrder, "GAME") ||
						_readMessageFieldOrder(index, pregameOrder, "PREGAME") ||
						_readMessageFieldOrder(index, overtimeOrder, "OVERTIME") ||
						_readMessageFieldOrder(index, winOrder, "WIN") ||
						_readMessageFieldOrder(index, lossOrder, "LOSS") ||
						_readMessageFieldOrder(index, tieOrder, "TIE") ||
						_readMessageFieldOrder(index, endOrder, "END")) {
							// nothing
						} else {
							jjPrint("MayModData: invalid line: "+songMessage.substr(index, _nextLine(songMessage, index)-index));
						}
					}
				}
			}
		}
		
		private bool _loadXM(const string &in filedata) {
			suppressHooks = true;
			
			if (filedata[0x3a] != 4 || filedata[0x3b] != 1) {
				jjPrint("MayModData: Unsupported XM format version");
				return false;
			}
			
			songName = _readNullTerminatedString(filedata, 17, 20);
			//jjPrint("Song name: " + songName);
			
			uint32 headerSize = _readUint32(filedata, 60);
			
			uint16 numOrders = _readUint16(filedata, 64);
			
			//uint16 songRestartPos = _readUint16(filedata, 66);
			
			// XM initial global volume is always 64 (and we use a
			// 128 scale)
			globalVolume = 128;
			
			channelCount = _readUint16(filedata, 68);
			currentChannelStates.resize(channelCount);
			// XM does not have initial channel volumes/panning
			for (uint i = 0; i < channelCount; i++) {
				@currentChannelStates[i] = @MayModDataChannelState();
				currentChannelStates[i].reset();
			}
			
			uint16 numPatterns = _readUint16(filedata, 70);
			uint16 numInstruments = _readUint16(filedata, 72);
			
			//uint16 flags = _readUint16(filedata, 74);
			//uint16 defaultTempo = _readUint16(filedata, 76);
			//uint16 defaultSpeed = _readUint16(filedata, 78);
			
			orders.resize(numOrders);
			for (uint i = 0; i < numOrders; i++) {
				orders[i] = filedata[80+i];
			}
			patterns.resize(numPatterns);
			
			// Read patterns
			uint filedataIndex = 60+headerSize;
			for (uint patternIndex = 0; patternIndex < numPatterns; patternIndex++) {
				uint patternHeaderSize = _readUint32(filedata, filedataIndex);
				uint numRows = _readUint16(filedata, filedataIndex+5);
				if (numRows == 0) numRows = 64;
				uint packedSize = _readUint16(filedata, filedataIndex+7);
				
				patterns[patternIndex] = array<array<uint64>>(numRows);
				
				filedataIndex += patternHeaderSize;
				
				if (packedSize == 0) { // empty pattern
					for (uint i = 0; i < numRows; i++) {
						patterns[patternIndex][i] = array<uint64>(0);
					}
					continue;
				}

				// read packed pattern data
				for (uint row = 0; row < numRows; row++) {
					patterns[patternIndex][row] = array<uint64>(channelCount);
					for (uint channel = 0; channel < channelCount; channel++) {
						uint8 byte = filedata[filedataIndex++];
						if (byte & 0x80 == 0) { // note
							if (byte == 97) {
								patterns[patternIndex][row][channel] |= ModNote::OFF;
							} else {
								patterns[patternIndex][row][channel] |= byte + 12;
							}
						} else if (byte & 0x1 != 0) { // also note
							patterns[patternIndex][row][channel] |= filedata[filedataIndex++] + 12;
						}
						if (byte & 0x80 == 0 || byte & 0x2 != 0) { // instrument
							patterns[patternIndex][row][channel] |= (filedata[filedataIndex++] << 8);
						}
						if (byte & 0x80 == 0 || byte & 0x4 != 0) { // volume command
							patterns[patternIndex][row][channel] |= ((filedata[filedataIndex++]+1) << 16);
						}
						if (byte & 0x80 == 0 || byte & 0x8 != 0) { // effect command
							patterns[patternIndex][row][channel] |= (filedata[filedataIndex++] << 24);
						}
						if (byte & 0x80 == 0 || byte & 0x10 != 0) { // effect command parameter
							patterns[patternIndex][row][channel] |= (uint64(filedata[filedataIndex++]) << 32);
						}
					}
				}
			}
			
			// TODO: read samples and instruments. This just makes placeholders.
			samples = array<MayModDataSample@>(1);
			@samples[0] = @MayModDataSample(44100, 44100, 0, 0, 0, 64, 64);
			instruments = array<MayModDataInstrument@>(numInstruments);
			for (uint instrumentIndex = 0; instrumentIndex < numInstruments; instrumentIndex++) {
				MayModDataInstrument instrument(@samples);
				@instruments[instrumentIndex] = @instrument;
				instrument.defaultVolume = 64;
				instrument.fade = 0;
				instrument.globalVolume = 64;
				
				// sample map
				for (uint i = 0; i < MAX_NOTE; i += 1) {
					instrument.sampleMap[i] = 1;
				}
			}
			
			suppressHooks = false;
			hasInstrumentInfo = true;
			return true;
		}
		
		private bool _loadIT(const string &in filedata) {
			uint64 startTime = jjUnixTimeMs();
			suppressHooks = true;
			uint filedataIndex = 4; // skip header
			// next 26 bytes are song name
			songName = _readNullTerminatedString(filedata, filedataIndex, 26);
			//jjPrint("Song name: " + songName);
			
			filedataIndex = 0x20;
			uint16 numOrders = _readUint16(filedata, filedataIndex);
			filedataIndex += 2;
			//jjPrint("Orders: "+numOrders);
			
			uint16 numInstruments = _readUint16(filedata, filedataIndex);
			filedataIndex += 2;
			//jjPrint("Instruments: "+numInstruments);
			
			uint16 numSamples = _readUint16(filedata, filedataIndex);
			filedataIndex += 2;
			//jjPrint("Samples: "+numSamples);
			
			uint16 numPatterns = _readUint16(filedata, filedataIndex);
			filedataIndex += 2;
			//jjPrint("Patterns: "+numPatterns);
			
			//uint16 createdWithTracker;
			filedataIndex += 2;
			
			uint16 compatibleWith = _readUint16(filedata, filedataIndex);
			filedataIndex += 2;
			
			// we don't support ancient IT files
			if (compatibleWith < 0x200) {
				suppressHooks = false;
				return false;
			}
			
			//uint16 flags; // I don't care about these flags right now
			filedataIndex += 2;
			
			uint16 specialFlags = _readUint16(filedata, filedataIndex); // I do care about some of these, though.
			filedataIndex += 2;
			
			globalVolume = filedata[filedataIndex++];
			//uint8 mixVolume;
			//uint8 initialSpeed;
			//uint8 initialTempo;
			//uint8 panSeparation;
			//uint8 pitchWheelDepth;
			filedataIndex += 5;
			
			uint16 messageLength = _readUint16(filedata, filedataIndex);
			filedataIndex += 2;
			
			uint32 messageOffset = _readUint32(filedata, filedataIndex);
			filedataIndex += 4;
			
			if (specialFlags & 1 != 0) {
				songMessage = _readNullTerminatedString(filedata, messageOffset, messageLength);
				for (uint i = 0; i < songMessage.length(); i++) {
					// IT messages use carriage return instead of newline, fix that
					if (songMessage[i] == 0xD) songMessage[i] = 0xA;
				}
			}
			
			//uint32 reserved; // can be tracker version info
			filedataIndex += 4;
			
			// skip 64 bytes of channel volumes + 64 bytes of channel panning
			// we will come back to these later when we know how many channels
			// we actually have
			uint channelInfoOffset = filedataIndex;
			filedataIndex += 128;
			
			// read orders
			orders.resize(numOrders);
			for (uint i = 0; i < numOrders; i++) {
				orders[i] = filedata[filedataIndex+i];
			}
			filedataIndex += numOrders;
			
			_processSongMessage();
			
			uint64 curTime = jjUnixTimeMs();
			if (debugLoadingTime) jjPrint("IT header, orders, and message read in "+(curTime-startTime)+"ms");
			startTime = jjUnixTimeMs();
			
			// read instruments
			samples = array<MayModDataSample@>(numSamples);
			instruments = array<MayModDataInstrument@>(numInstruments);
			for (uint instrumentIndex = 0; instrumentIndex < numInstruments; instrumentIndex++) {
				uint instrumentOffset = _readUint32(filedata,filedataIndex);
				filedataIndex += 4;
				
				MayModDataInstrument instrument(@samples);
				@instruments[instrumentIndex] = @instrument;
				instrument.defaultVolume = filedata[instrumentOffset+0x13];
				// fade is 0-128 in file but 0-32768 in our representation
				instrument.fade = _readUint16(filedata, instrumentOffset+0x14) << 5;
				// global volume is 0-128 in file, 0-64 in our representation
				instrument.globalVolume = filedata[instrumentOffset+0x18] >> 1;
				
				// sample map
				for (uint mapOffset = 0; mapOffset < 240; mapOffset += 2) {
					instrument.sampleMap[filedata[instrumentOffset+0x40+mapOffset]] = filedata[instrumentOffset+0x41+mapOffset];
				}
				
				// volume envelope
				uint8 flags = filedata[instrumentOffset+0x130];
				if (flags & 1 != 0) {
					instrument.flags |= (1 << FLAG_ENVELOPE_VOL);
				}
				
				if (flags & 2 == 0) { // loop flag
					instrument.volumeEnvelopeProps[LOOP_START] = -1;
				} else {
					instrument.volumeEnvelopeProps[LOOP_START] = filedata[instrumentOffset+0x132];
					instrument.volumeEnvelopeProps[LOOP_END] = filedata[instrumentOffset+0x133];
				}
				
				if (flags & 4 == 0) { // sustain loop flag
					instrument.volumeEnvelopeProps[SUSTAIN_START] = -1;
				} else {
					instrument.volumeEnvelopeProps[SUSTAIN_START] = filedata[instrumentOffset+0x134];
					instrument.volumeEnvelopeProps[SUSTAIN_END] = filedata[instrumentOffset+0x135];
				}
				
				uint pointCount = filedata[instrumentOffset+0x131];
				instrument.volumeEnvelope.resize(pointCount);
				for (uint i = 0; i < pointCount; i++) {
					instrument.volumeEnvelope[i] = filedata[instrumentOffset+0x136+3*i] | (_readUint16(filedata, instrumentOffset+0x137+3*i) << 8);
				}
				
				if (useDisabledEnvelopesAsGuides) {
					if (flags & 1 == 0) {
						// volume envelope is disabled, use it as intensity envelope
						@instrument.intensityEnvelope = @instrument.volumeEnvelope;
					} else {
						// find next unused envelope
						uint intensityEnvelopeOffset = instrumentOffset+0x182; // panning envelope
						if (filedata[intensityEnvelopeOffset] & 1 == 1) {
							intensityEnvelopeOffset = instrumentOffset+0x1D4; // try pitch/filter envelope
						}
						// if all three envelopes are used, we just leave the instrument
						// with no intensity envelope. Could instead steal an envelope
						// from another instrument or something, but it's a smaller can
						// of worms to just not use all three envelopes on instruments
						// in JJ2 mod files - the pitch/filter envelope is rarely used
						// and our volume estimation doesn't handle it anyway.
						if (filedata[intensityEnvelopeOffset] & 1 == 0) {
							pointCount = filedata[intensityEnvelopeOffset+1];
							@instrument.intensityEnvelope = @array<uint>(pointCount);
							for (uint i = 0; i < pointCount; i++) {
								instrument.intensityEnvelope[i] = filedata[instrumentOffset+6+3*i] | (_readUint16(filedata, instrumentOffset+7+3*i) << 8);						
							}
						}
					}
				}
			}
			
			curTime = jjUnixTimeMs();
			if (debugLoadingTime) jjPrint("IT instruments read and processed in "+(curTime-startTime)+"ms");
			startTime = jjUnixTimeMs();
			
			// read sample properties (but not actual sample data)
			for (uint sampleIndex = 0; sampleIndex < numSamples; sampleIndex++) {
				uint sampleOffset = _readUint32(filedata, filedataIndex);
				filedataIndex += 4;
				
				uint8 globalVolume = filedata[sampleOffset+0x11];
				uint8 itFlags = filedata[sampleOffset+0x12];
				bool loop = (itFlags & 0x10 != 0);
				bool bidi = (itFlags & 0x40 != 0);
				uint8 defaultVolume = filedata[sampleOffset+0x13];
				uint length = _readUint32(filedata, sampleOffset+0x30);
				uint loopStart = _readUint32(filedata, sampleOffset+0x34);
				uint loopEnd = _readUint32(filedata, sampleOffset+0x38);
				uint freq = _readUint32(filedata,sampleOffset+0x3C);
				//jjPrint("Sample "+(sampleIndex+1)+" frequency "+freq);
				uint ourFlags = loop ? (bidi ? FLAG_SAMPLE_BIDI : FLAG_SAMPLE_LOOP) : 0;
				@samples[sampleIndex] = @MayModDataSample(freq, length, loopStart, loopEnd, ourFlags, globalVolume, defaultVolume);
			}
			
			curTime = jjUnixTimeMs();
			if (debugLoadingTime) jjPrint("IT sample headers read and processed in "+(curTime-startTime)+"ms");
			startTime = jjUnixTimeMs();
			
			// next are pattern offsets
			uint filedataSize = filedata.length();
			patterns.resize(numPatterns);
			
			uint numChannels = 0;
			array<uint64> lastValue(64);
			
			for (uint patternIndex = 0; patternIndex < numPatterns; patternIndex++) {
				uint patternOffset = _readUint32(filedata, filedataIndex+patternIndex*4);
				
				if (patternOffset == 0 || patternOffset > filedataSize) {
					// OpenMPT puts an empty 64-row pattern
					// here instead of like, complaining the
					// file is invalid. Guess they would
					// know better than me...
					patterns[patternIndex] = array<array<uint64>>(64);
					for (uint row = 0; row < 64; row++) {
						patterns[patternIndex][row] = array<uint64>(0);
					}
				} else {
					for (uint i = 0; i < 64; i++) {
						lastValue[i] = 0;
					}
					uint16 patternLengthBytes = _readUint16(filedata, patternOffset);
					patternOffset += 2;
					
					uint16 numRows = _readUint16(filedata, patternOffset);
					patternOffset += 2;
					
					// unused header bytes
					patternOffset += 4;
					
					patterns[patternIndex] = array<array<uint64>>(numRows);
					array<uint8> channelMask(64);
					for (uint row = 0; row < numRows; row++) {
						patterns[patternIndex][row] = array<uint64>(numChannels, 0);
					}
					uint row = 0;
					while (row < numRows && patternOffset < filedataSize) {
						uint8 b = filedata[patternOffset++];
						if (b == 0) {
							// row's over
							row++;
						} else {
							uint8 ch = b & 0x7f;
							if (ch != 0) ch = (ch-1) & 0x3f;
							
							if (ch >= patterns[patternIndex][row].length()) {
								patterns[patternIndex][row].resize(ch+1);
								if (ch > numChannels) {
									numChannels = ch+1;
								}
							}
							
							if (b & 0x80 != 0) {
								channelMask[ch] = filedata[patternOffset++];
							}
							
							// uncomment those &=s to support files that overwrite their own instructions
							// (such files hopefully do not exist)
							if (channelMask[ch] & 0x10 != 0) {
								//patterns[patternIndex][row][ch] &= ~MAY_COMMAND_NOTE_MASK;
								patterns[patternIndex][row][ch] |= (lastValue[ch] & MAY_COMMAND_NOTE_MASK);
							}
							if (channelMask[ch] & 0x20 != 0) {
								//patterns[patternIndex][row][ch] &= ~MAY_COMMAND_INSTRUMENT_MASK;
								patterns[patternIndex][row][ch] |= (lastValue[ch] & MAY_COMMAND_INSTRUMENT_MASK);
							}
							if (channelMask[ch] & 0x40 != 0) {
								//patterns[patternIndex][row][ch] &= ~MAY_COMMAND_VOLUME_MASK;
								patterns[patternIndex][row][ch] |= (lastValue[ch] & MAY_COMMAND_VOLUME_MASK);
							}
							if (channelMask[ch] & 0x80 != 0) {
								patterns[patternIndex][row][ch] |= (lastValue[ch] & MAY_COMMAND_EFFECT_MASK);
								patterns[patternIndex][row][ch] |= (lastValue[ch] & MAY_COMMAND_EFFECT_PARAM_MASK);
							}
							if (channelMask[ch] & 0x1 != 0) {
								uint8 note = filedata[patternOffset++];
								if (note < 0x80) {
									note += 1;
								}
								
								//patterns[patternIndex][row][ch] &= ~MAY_COMMAND_NOTE_MASK;
								patterns[patternIndex][row][ch] |= note;
								
								lastValue[ch] &= ~MAY_COMMAND_NOTE_MASK;
								lastValue[ch] |= note;
							}
							if (channelMask[ch] & 0x2 != 0) {
								uint8 instrument = filedata[patternOffset++];
								//patterns[patternIndex][row][ch] &= ~MAY_COMMAND_INSTRUMENT_MASK;
								patterns[patternIndex][row][ch] |= instrument << 8;
								
								lastValue[ch] &= ~MAY_COMMAND_INSTRUMENT_MASK;
								lastValue[ch] |= instrument << 8;
							}
							if (channelMask[ch] & 0x4 != 0) {
								uint8 volume = filedata[patternOffset++];
								//patterns[patternIndex][row][ch] &= ~MAY_COMMAND_VOLUME_MASK;
								patterns[patternIndex][row][ch] |= volume <= 212 ? ((volume+1) << 16) : 0;
								
								lastValue[ch] &= ~MAY_COMMAND_VOLUME_MASK;
								lastValue[ch] |= volume <= 212 ? ((volume+1) << 16) : 0;
							}
							if (channelMask[ch] & 0x8 != 0) {
								// includes effect param
								uint64 effect = (uint64(filedata[patternOffset++]) << 24) | ((uint64(filedata[patternOffset++])) << 32);
								patterns[patternIndex][row][ch] |= effect;

								lastValue[ch] &= ~MAY_COMMAND_EFFECT_MASK;
								lastValue[ch] &= ~MAY_COMMAND_EFFECT_PARAM_MASK;
								lastValue[ch] |= effect;
							}
						}
					}
				}
			}
			
			curTime = jjUnixTimeMs();
			if (debugLoadingTime) jjPrint("IT patterns read and processed in "+(curTime-startTime)+"ms");
			startTime = jjUnixTimeMs();
			
			channelCount = numChannels;
			
			// now that we know how many channels we have, read initial channel volumes and panning
			currentChannelStates.resize(numChannels);
			for (uint i = 0; i < numChannels; i++) {
				// first 64 bytes are panning, next 64 are volume
				@currentChannelStates[i] = @MayModDataChannelState();
				currentChannelStates[i].defaultPanning = filedata[channelInfoOffset+i];
				currentChannelStates[i].defaultVolume = filedata[channelInfoOffset+i+64];
				currentChannelStates[i].reset();
			}
			
			suppressHooks = false;
			hasInstrumentInfo = true;
			return true;
		}
		
		// This function is mostly copied from openmpt/soundlib/Load_mo3.cpp. I made it
		// worse in various ways.
		private bool _loadMO3(const string &in filedata) {
			uint filedataIndex = 3; // skip "MO3"
			uint8 version = filedata[filedataIndex++];
			uint32 musicSize = _readUint32(filedata, filedataIndex); // Uncompressed size
			//jjPrint("Music size: "+musicSize);
			filedataIndex += 4;
			// Make sure the mo3 isn't a zip bomb.
			if (musicSize > MAX_MOD_FILE_SIZE) return false;
			
			// We're committed to loading now
			suppressHooks = true;
			
			uint32 musicDataOffset = 0;
			if (version >= 5) {
				// Version 5 includes the size of the compressed chunk,
				// which would be convenient for us if we hadn't already
				// read the entire file into memory.
				//int compressedSize = _readUint32(filedata, filedataIndex);
				filedataIndex += 4;
			}
			
			// The song name, message, patterns, etc. are all in one compressed chunk.
			string uncompressed;
			uncompressed.resize(musicSize);
			
			uint16 data = 0;
			int8 carry = 0;
			int32 strLen = 0;
			int32 strOffset = 0;
			uint32 previousPtr = 0;
			
			uint8 firstByte = filedata[filedataIndex++];
			uncompressed[0] = firstByte;
			int32 remain = musicSize - 1;
			uint32 uncompressedIndex = 1;
			
			uint filedataSize = filedata.length();
			while (remain > 0) {
				// READ_CTRL_BIT
				data <<= 1;
				carry = (data > 0xFF) ? 1 : 0;
				data &= 0xFF;
				if (data == 0) {
					if (filedataIndex >= filedataSize) break;
					uint8 nextByte = filedata[filedataIndex++];
					data = nextByte;
					data = (data << 1) + 1;
					carry = (data > 0xFF) ? 1 : 0;
					data &= 0xFF;
				}
				// end READ_CTRL_BIT
				
				if (carry == 0) {
					// a 0 ctrl bit means 'copy', not compressed byte
					uint8 b;
					if (filedataIndex >= filedataSize) break;
					uncompressed[uncompressedIndex++] = filedata[filedataIndex++];
					remain--;
				} else {
					// a 1 ctrl bit means compressed bytes are following
					uint8 lengthAdjust = 0;  // length adjustment
					// DECODE_CTRL_BITS
					strLen++;
					do {
						// READ_CTRL_BIT
						data <<= 1;
						carry = (data > 0xFF) ? 1 : 0;
						data &= 0xFF;
						if (data == 0) {
							if (filedataIndex >= filedataSize) break;
							uint8 nextByte = filedata[filedataIndex++];
							data = nextByte;
							data = (data << 1) + 1;
							carry = (data > 0xFF) ? 1 : 0;
							data &= 0xFF;
						}
						// end READ_CTRL_BIT
						
						strLen = (strLen << 1) + carry;
						
						// READ_CTRL_BIT
						data <<= 1;
						carry = (data > 0xFF) ? 1 : 0;
						data &= 0xFF;
						if (data == 0) {
							if (filedataIndex >= filedataSize) break;
							uint8 nextByte = filedata[filedataIndex++];
							data = nextByte;
							data = (data << 1) + 1;
							carry = (data > 0xFF) ? 1 : 0;
							data &= 0xFF;
						}
						// end READ_CTRL_BIT
					} while(carry != 0);
					// end DECODE_CTRL_BITS
					
					strLen -= 3;
					if(strLen < 0) {
						// means LZ ptr with same previous relative LZ ptr (saved one)
						strOffset = previousPtr;  // restore previous Ptr
						strLen++;
					} else {
						// LZ ptr in ctrl stream
						if (filedataIndex >= filedataSize) break;
						uint8 b = filedata[filedataIndex++];
						strOffset = (strLen << 8) | b;  // read less significant offset byte from stream
						strLen = 0;
						strOffset = ~strOffset;
						if (strOffset < -1280) lengthAdjust++;
						lengthAdjust++; // length is always at least 1
						if (strOffset < -32000) lengthAdjust++;
						previousPtr = strOffset; // save current Ptr
					}
					
					// read the next 2 bits as part of strLen
					// READ_CTRL_BIT
					data <<= 1;
					carry = (data > 0xFF) ? 1 : 0;
					data &= 0xFF;
					if (data == 0) {
						if (filedataIndex >= filedataSize) break;
						uint8 nextByte = filedata[filedataIndex++];
						data = nextByte;
						data = (data << 1) + 1;
						carry = (data > 0xFF) ? 1 : 0;
						data &= 0xFF;
					}
					// end READ_CTRL_BIT
					
					strLen = (strLen << 1) + carry;
					
					// READ_CTRL_BIT
					data <<= 1;
					carry = (data > 0xFF) ? 1 : 0;
					data &= 0xFF;
					if (data == 0) {
						if (filedataIndex >= filedataSize) break;
						uint8 nextByte = filedata[filedataIndex++];
						data = nextByte;
						data = (data << 1) + 1;
						carry = (data > 0xFF) ? 1 : 0;
						data &= 0xFF;
					}
					// end READ_CTRL_BIT
					
					strLen = (strLen << 1) + carry;
					if(strLen == 0)
					{
						// length does not fit in 2 bits
						// decode length: 1 is the most significant bit,
						// DECODE_CTRL_BITS
						strLen++;
						do {
							// READ_CTRL_BIT
							data <<= 1;
							carry = (data > 0xFF) ? 1 : 0;
							data &= 0xFF;
							if (data == 0) {		
								if (filedataIndex >= filedataSize) break;
								uint8 nextByte = filedata[filedataIndex++];
								data = nextByte;
								data = (data << 1) + 1;
								carry = (data > 0xFF) ? 1 : 0;
								data &= 0xFF;
							}
							// end READ_CTRL_BIT
							
							strLen = (strLen << 1) + carry;
							
							// READ_CTRL_BIT
							data <<= 1;
							carry = (data > 0xFF) ? 1 : 0;
							data &= 0xFF;
							if (data == 0) {
								if (filedataIndex >= filedataSize) break;
								uint8 nextByte = filedata[filedataIndex++];
								data = nextByte;
								data = (data << 1) + 1;
								carry = (data > 0xFF) ? 1 : 0;
								data &= 0xFF;
							}
							// end READ_CTRL_BIT
						} while(carry != 0);
						// end DECODE_CTRL_BITS
						
						strLen += 2;       // then first bit of each bits pairs (noted n1), until n0.
					}
					strLen += lengthAdjust;  // length adjustment

					if(remain < strLen || strLen <= 0) break;
					if(strOffset >= 0 || -(uncompressedIndex+1) > strOffset) break;

					// Copy previous string, source and destination may overlap
					remain -= strLen;
					uint32 start = uncompressedIndex + strOffset;
					do {
						strLen--;
						uncompressed[uncompressedIndex++] = uncompressed[start++];
					} while (strLen > 0);
				}
			}
			
			// uncompressed now contains the uncompressed chunk, and we are done with the input stream.
			uint idx = musicDataOffset;
			string songName = _readNullTerminatedString(uncompressed, idx);
			idx += songName.length() + 1;
			//jjPrint("Song name: " + songName);
			songMessage = _readNullTerminatedString(uncompressed, idx);
			idx += songMessage.length() + 1;
			
			channelCount = uncompressed[idx];
			//jjPrint("Channel count: " + channelCount);
			idx += 1;
			
			uint16 numOrders = _readUint16(uncompressed, idx);
			//jjPrint("Order count: " + numOrders);
			idx += 2;
			
			//uint16 restartPos;
			idx += 2;
			
			uint16 numPatterns = _readUint16(uncompressed, idx);
			//jjPrint("Pattern count: " + numPatterns);
			idx += 2;
			
			uint16 numTracks = _readUint16(uncompressed, idx);
			//jjPrint("Track count: " + numTracks);
			idx += 2;
			
			uint16 numInstruments = _readUint16(uncompressed, idx);
			//jjPrint("Instrument count: " + numInstruments);
			idx += 2;
			
			uint16 numSamples = _readUint16(uncompressed, idx);
			idx += 2;
			
			//uint8  defaultSpeed;
			//uint8  defaultTempo;
			idx += 2;
			
			uint32 flags = _readUint32(uncompressed, idx);
			//jjPrint("Flags: "+flags);
			idx += 4;
			
			if (flags & 0x0100 != 0) {
				modType = IT;
				//jjPrint("IT");
			} else if (flags & 0x0002 != 0) {
				modType = S3M;
				//jjPrint("S3M");
			} else if (flags & 0x0080 != 0) {
				modType = MOD;
				//jjPrint("MOD");
			} else if (flags & 0x0008 != 0) {
				modType = MTM;
				//jjPrint("MTM");
			} else {
				modType = XM;
				//jjPrint("XM");
			}
			
			// 0...128 in IT, seems to be 0...64 for other stuff
			// XXX: famous last words up there, this is probably
			// wrong for MOD or something.
			globalVolume = uncompressed[idx];
			if (modType != IT) globalVolume *= 2;
			idx += 1;
			
			//uint8  panSeparation; // 0...128 in IT
			//int8   sampleVolume;  // Only used in IT
			idx += 2;
			
			// read initial channel volumes and panning
			currentChannelStates.resize(channelCount);
			for (uint i = 0; i < channelCount; i++) {
				@currentChannelStates[i] = @MayModDataChannelState();
				// 0...64
				currentChannelStates[i].channelVolume = uncompressed[idx+i];
				
				// 0...256, 127 = surround.
				currentChannelStates[i].panning = uncompressed[idx+64+i];
				currentChannelStates[i].reset();
			}
			// header always has room for 64 channels
			idx += 128;
			
			//uint8  sfxMacros[16];
			//uint8  fixedMacros[128][2];
			idx += 16+128*2;
			
			// Read orders
			orders.resize(numOrders);
			for (uint i = 0; i < numOrders; i++) {
				orders[i] = uncompressed[idx+i];
				//jjPrint("Order " + i + ":" + orders[i]);
			}
			idx += numOrders;
			
			// track assignments chunk, we use this when populating
			// pattern data but don't need it quite yet
			uint trackIndexIndex = idx;
			idx += numPatterns * channelCount * 2;
			
			array<uint> patternLengths(numPatterns);
			for (uint i = 0; i < numPatterns; i++) {
				patternLengths[i] = _readUint16(uncompressed, idx+i*2);
				//jjPrint("Pattern " + i + " length: " + patternLengths[i]);
			}
			idx += numPatterns * 2;
			
			array<uint> trackOffsets = array<uint>(numTracks);
			for (uint i = 0; i < numTracks; i++) {
				uint32 trackSize = _readUint32(uncompressed, idx);
				idx += 4;
				trackOffsets[i] = idx;
				idx += trackSize;
				//jjPrint("Track " + i + " size: "+trackSize);
			}
			
			uint8 noteOffset = 1;
			if (modType == MTM) {
				noteOffset = 14;
			} else if (modType != IT) {
				noteOffset = 13;
			}
			
			// read rows and translate commands to our format
			patterns.resize(numPatterns);
			for (uint patternIndex = 0; patternIndex < numPatterns; patternIndex++) {
				patterns[patternIndex] = array<array<uint64>>(patternLengths[patternIndex]);
				for (uint row = 0; row < patternLengths[patternIndex]; row++) {
					patterns[patternIndex][row] = array<uint64>(channelCount);
				}
				for (uint channelIndex = 0; channelIndex < channelCount; channelIndex++) {
					uint16 trackIndex = _readUint16(uncompressed, trackIndexIndex);
					trackIndexIndex += 2;
					
					if (trackIndex >= numTracks) continue;
					
					uint trackOffset = trackOffsets[trackIndex];
					uint row = 0;
					while (row < patternLengths[patternIndex]) {
						uint8 b = uncompressed[trackOffset];
						if (b == 0) break;
						trackOffset += 1;
						
						uint8 numCommands = (b & 0x0F);
						uint8 rep = (b >> 4);
						
						uint64 minCommand = 0;
						for (uint c = 0; c < numCommands; c++) {
							uint8 cmd0 = uncompressed[trackOffset];
							uint8 cmd1 = uncompressed[trackOffset+1];
							trackOffset += 2;
							
							switch(cmd0) {
								case 0x01: // note command
									minCommand |= cmd1 + (cmd1 < 120 ? noteOffset : 0);
									break;
								case 0x02: // instrument command
									minCommand |= ((cmd1+1) << 8);
									break;
									/*
								case 0x06: // tone portamento command
								case 0x07: // vibrato commmand
								case 0x0B: // panning command
									
									break;
									*/
								case 0x0F: // volume command
									minCommand |= (cmd1 << 16);
									break;
									/*
								case 0x10: // pattern break command
								case 0x12: // tempo+speed command
								case 0x14: // XM volume column volslide
								case 0x15: // XM volume column volslide
								case 0x1B: // XM volume column pan slide
								case 0x1D: // XM extra fine portamento up
								case 0x1E: // XM extra fine portamento down
								case 0x1F: // XM volume column vibrato speed
								case 0x20: // XM volume column vibrato depth
								case 0x22: // IT/S3M volume slide
								case 0x30: // IT volume column volslide
								case 0x31: // IT volume column potramento down
								case 0x32: // IT volume column portamento up
								case 0x34: // unrecognized IT volume command
									*/
								default:
									break;
							}
						}

						uint targetRow = row+rep;
						// XXX: invalid targetRow should probably not be tolerated like this...
						if (targetRow > patternLengths[patternIndex]) targetRow = patternLengths[patternIndex];
						while (row < targetRow) {
							patterns[patternIndex][row][channelIndex] = minCommand;
							row++;
						}
					}
				}
			}
			
			// TODO: read samples and instruments. This just makes placeholders.
			samples = array<MayModDataSample@>(1);
			@samples[0] = @MayModDataSample(44100, 44100, 0, 0, 0, 64, 64);
			instruments = array<MayModDataInstrument@>(numInstruments);
			for (uint instrumentIndex = 0; instrumentIndex < numInstruments; instrumentIndex++) {
				MayModDataInstrument instrument(@samples);
				@instruments[instrumentIndex] = @instrument;
				instrument.defaultVolume = 64;
				instrument.fade = 0;
				instrument.globalVolume = 64;
				
				// sample map
				for (uint i = 0; i < MAX_NOTE; i += 1) {
					instrument.sampleMap[i] = 1;
				}
			}
			
			suppressHooks = false;
			hasInstrumentInfo = true;
			return true;
		}
		
		private string _readNullTerminatedString(const string &in bytes, uint startIndex) {
			uint len = 0;
			while (bytes[startIndex+len] != 0) len++;
			string rval;
			rval.resize(len);
			for (uint i = 0; i < len; i++) rval[i] = bytes[startIndex+i];
			return rval;
		}
		
		private string _readNullTerminatedString(const string &in bytes, uint startIndex, uint maxLength) {
			uint len = 0;
			while (bytes[startIndex+len] != 0 && len < maxLength) len++;
			string rval;
			rval.resize(len);
			for (uint i = 0; i < len; i++) rval[i] = bytes[startIndex+i];
			return rval;
		}
		
		private uint16 _readUint16(const string &in bytes, uint startIndex) {
			return bytes[startIndex] | (bytes[startIndex+1] << 8);
		}
		
		private uint32 _readUint32(const string &in bytes, uint startIndex) {
			return bytes[startIndex] | (bytes[startIndex+1] << 8) | (bytes[startIndex+2] << 16) | (bytes[startIndex+3] << 24);
		}
	}
}