package micromod;
import java.io.*;
public class ModuleLoader {
/**
@return true if stream contains a valid MOD file.
*/
public static boolean identify( DataInput modStream ) throws IOException {
// Get to the identifier
modStream.skipBytes(1080);
String info = DataReader.readText(modStream, 4);
System.out.println(" Checking MOD type : "+info);
// Check the identifier
try{ getFormatInfo(info); } catch (IllegalArgumentException e) { return false; }
return true;
}
public static Module read( DataInput dataInput ) throws IOException, IllegalArgumentException {
String name = DataReader.readText( dataInput, 20 );
// Read the instruments
Instrument[] instruments = new Instrument[32];
for( int n=1; n<32; n++ )
instruments[n] = readInstrument( dataInput );
// Set up the sequence
Sequence sequence = new Sequence();
sequence.songLengthPatterns = DataReader.readUnsigned7Bit(dataInput);
sequence.restartPosition = DataReader.readUnsigned7Bit(dataInput);
sequence.defaultBPM = 125;
sequence.defaultTempo = 6;
// Read in the actual pattern order.
for( int n=0; n<128; n++ ) {
sequence.patternOrder[n] = DataReader.readUnsigned7Bit(dataInput);
if( sequence.numberOfPatterns < sequence.patternOrder[n] ) sequence.numberOfPatterns = sequence.patternOrder[n];
}
sequence.numberOfPatterns++;
// Check module type
String id = DataReader.readText( dataInput, 4 );
ModFormatInfo fInfo = getFormatInfo( id );
sequence.numberOfChannels = fInfo.numChan;
// Read the patterns
for( int n=0; n<sequence.numberOfPatterns; n++ )
sequence.patterns[n] = readPattern( dataInput, sequence.numberOfChannels );
// Read instrumentdata. The module will still play even if some samples
// are missing, so only warn about this.
try {
for( int n=1; n<32; n++ )
if( instruments[n]!=null ) readSampleData( instruments[n], dataInput );
}
catch (EOFException e) {
System.out.println("Warning : Module is truncated!");
}
return new Module( name, id, fInfo.supportsPan, fInfo.pal, sequence, instruments );
}
/**
@return Bits 0-11 gives number of channels, bit 12 is set if format uses the panning
commands. Returns zero if mod is unrecognized.
*/
protected static ModFormatInfo getFormatInfo( String id ) throws IllegalArgumentException {
ModFormatInfo mfi = new ModFormatInfo();
if( id.equals("M.K.")||id.equals("M!K!")||id.equals("FLT4")||id.equals("N.T.")||id.equals("M&K!") ) {
// 4-Channel Karsten/Mahoney&Kaktus/Freelancers style MOD.
mfi.numChan=4;
mfi.pal=true;
mfi.supportsPan=false;
return mfi;
}
if( id.equals("CD81")||id.equals("OKTA")||id.equals("FLT8") ) {
// Oktalyzer-type module. Not so sure about CD81.
mfi.numChan=8;
mfi.pal=true;
mfi.supportsPan=false;
return mfi;
}
if( id.regionMatches( false, 1, "CHN", 0, 3 ) ) {
// FTK. Normally 6/8 channel.
mfi.numChan = Integer.parseInt( id.substring(0,1) );
mfi.pal=false;
mfi.supportsPan=true;
return mfi;
}
if( id.regionMatches( false, 0, "TDZ", 0, 3 ) ) {
// TDZ - No idea! 4-8 channels. Probably an Amiga/ST mod format.
mfi.numChan = Integer.parseInt( id.substring(3,4) );
mfi.pal=false;
mfi.supportsPan=false;
return mfi;
}
if( id.regionMatches( false, 2, "CH", 0, 2 )||id.regionMatches( false, 2, "CN", 0, 2 ) ) {
// Generic extended mod format. ModPlug saves multichannel mods with the "xxCH" id.
mfi.numChan = Integer.parseInt( id.substring(0,2) );
mfi.pal=false;
mfi.supportsPan=true;
return mfi;
}
throw new IllegalArgumentException("Module format \""+id+"\" unrecognised!");
}
/**
Read the data for a Pattern into a new Pattern object and return it.
*/
protected static Sequence.Pattern readPattern( DataInput dataInput, int numChannels ) throws IOException {
int note;
Sequence.Pattern patt = new Sequence.Pattern(numChannels);
for( int m=0; m<64; m++ ) {
for( int c=0; c<numChannels; c++ ) {
note = DataReader.readInt32(dataInput);
patt.instrument[m][c] = ((note&0xF000)>>12)|((note&0x10000000)>>24);
patt.period[m][c] = (note&0xFFF0000)>>16;
patt.effectCommand[m][c] = (note&0xF00)>>8;
if( patt.effectCommand[m][c] == 0xE ) {
patt.effectCommand[m][c] = 0xE0 | ((note&0xF0)>>4);
patt.effectValue[m][c] = note&0xF;
}
else {
patt.effectValue[m][c] = note&0xFF;
if( patt.effectCommand[m][c] == 0xD ) patt.effectValue[m][c] = nibbleDecimal2Bin((byte)patt.effectValue[m][c]);
}
}
}
return patt;
}
/**
Read in the data for an Instrument, and return a new Instrument containing that data.
*/
protected static Instrument readInstrument( DataInput dataInput ) throws IOException {
Instrument inst = new Instrument();
inst.name = DataReader.readText( dataInput, 22 );
inst.sampleLength = DataReader.readUnsigned16Bit(dataInput) << 1;
int fineTuneByte = DataReader.readUnsigned8Bit(dataInput) & 0x0F;
inst.fineTune = (fineTuneByte&0x07)-(fineTuneByte&0x08);
inst.volume = DataReader.readSigned8Bit(dataInput);
inst.loopStart = DataReader.readUnsigned16Bit(dataInput) << 1;
int loopLength = DataReader.readUnsigned16Bit(dataInput) << 1;
inst.data = new byte[inst.sampleLength + 3];
// Validate and configure the loop
inst.looped = true;
if(inst.loopStart+loopLength > inst.sampleLength)
loopLength=inst.sampleLength-inst.loopStart;
if(inst.loopStart >= inst.sampleLength) loopLength=0;
if( loopLength < 3 ) {
inst.looped = false;
inst.loopStart = 0;
inst.sampleEnd = inst.sampleLength;
} else {
inst.sampleEnd = inst.loopStart+loopLength;
}
return inst;
}
/**
Read the number of bytes specified by the Instrument's sampleLength field into the
Instrument's data array, and zero the first 2 samples, as ProTracker is supposed to
do (According to Lars Hamre's modspec.txt)
*/
protected static void readSampleData( Instrument instrument, DataInput dataInput ) throws IOException {
if( instrument.sampleEnd > 0 )
DataReader.readSigned8BitArray( dataInput, instrument.data, 0, instrument.sampleLength );
instrument.data[0] = 0;
instrument.data[1] = 0;
}
/**
Return a 2-digit decimal value written as hex as a binary number.
*/
protected static byte nibbleDecimal2Bin( byte decValue ) {
return (byte) ( ((decValue&0xf0)>>4)*10 + (decValue&0xf) );
}
/**
A type for the module format information.
*/
protected static class ModFormatInfo {
public int numChan;
public boolean pal, supportsPan;
}
}