You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1468 lines
53 KiB
1468 lines
53 KiB
|
|
function IBXMReplay( module, samplingRate ) {
|
|
/* Return a String representing the version of the replay. */
|
|
this.getVersion = function() {
|
|
return "20190513 (c)2019 mumart@gmail.com";
|
|
}
|
|
/* Return the sampling rate of playback. */
|
|
this.getSamplingRate = function() {
|
|
return samplingRate;
|
|
}
|
|
/* Set the sampling rate of playback.
|
|
This can be used with Module.c2Rate to adjust the tempo and pitch.*/
|
|
this.setSamplingRate = function( rate ) {
|
|
if( rate < 8000 || rate > 128000 ) {
|
|
throw "Unsupported sampling rate!";
|
|
}
|
|
samplingRate = rate;
|
|
}
|
|
/* Enable or disable the linear interpolation filter. */
|
|
this.setInterpolation = function( interp ) {
|
|
interpolation = interp;
|
|
}
|
|
/* Returns true if the specified channel is muted. */
|
|
this.isMuted = function( channel ) {
|
|
if( channel >= 0 && channel < module.numChannels ) {
|
|
return muted[ channel ];
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
/* Mute or unmute the specified channel.
|
|
If channel is negative, mute or unmute all channels. */
|
|
this.setMuted = function( channel, mute ) {
|
|
mute = mute ? true : false;
|
|
if( channel < 0 ) {
|
|
for( var idx = 0; idx < module.numChannels; idx++ ) {
|
|
muted[ idx ] = mute;
|
|
}
|
|
} else if( channel < module.numChannels ) {
|
|
muted[ channel ] = mute;
|
|
}
|
|
}
|
|
/* Get the current row position. */
|
|
this.getRow = function() {
|
|
return row;
|
|
}
|
|
/* Get the current pattern position in the sequence. */
|
|
this.getSequencePos = function() {
|
|
return seqPos;
|
|
}
|
|
/* Set the pattern in the sequence to play.
|
|
The tempo is reset to the default. */
|
|
this.setSequencePos = function( pos ) {
|
|
if( pos >= module.sequenceLength ) {
|
|
pos = 0;
|
|
}
|
|
breakSeqPos = pos;
|
|
nextRow = 0;
|
|
tick = 1;
|
|
this.globalVol = module.defaultGVol;
|
|
speed = module.defaultSpeed > 0 ? module.defaultSpeed : 6;
|
|
tempo = module.defaultTempo > 0 ? module.defaultTempo : 125;
|
|
plCount = plChannel = -1;
|
|
for( var idx = 0; idx < playCount.length; idx++ ) {
|
|
var patIdx = module.sequence[ idx ];
|
|
var numRows = ( patIdx < module.numPatterns ) ? module.patterns[ patIdx ].numRows : 0;
|
|
playCount[ idx ] = new Int8Array( numRows );
|
|
}
|
|
for( var idx = 0; idx < module.numChannels; idx++ ) {
|
|
channels[ idx ] = new IBXMChannel( this, idx );
|
|
}
|
|
for( var idx = 0; idx < 128; idx++ ) {
|
|
rampBuf[ idx ] = 0;
|
|
}
|
|
mixIdx = mixLen = 0;
|
|
seqTick();
|
|
}
|
|
/* Returns the song duration in samples at the current sampling rate. */
|
|
this.calculateSongDuration = function() {
|
|
var duration = 0;
|
|
this.setSequencePos( 0 );
|
|
var songEnd = false;
|
|
while( !songEnd ) {
|
|
duration += calculateTickLen( tempo, samplingRate );
|
|
songEnd = seqTick();
|
|
}
|
|
this.setSequencePos( 0 );
|
|
return duration;
|
|
}
|
|
/* Seek to approximately the specified sample position.
|
|
The actual sample position reached is returned. */
|
|
this.seek = function( samplePos ) {
|
|
this.setSequencePos( 0 );
|
|
var currentPos = 0;
|
|
var tickLen = calculateTickLen( tempo, samplingRate );
|
|
while( ( samplePos - currentPos ) >= tickLen ) {
|
|
for( var idx = 0; idx < module.numChannels; idx++ ) {
|
|
channels[ idx ].updateSampleIdx( tickLen * 2, samplingRate * 2 );
|
|
}
|
|
currentPos += tickLen;
|
|
seqTick();
|
|
tickLen = calculateTickLen( tempo, samplingRate );
|
|
}
|
|
return currentPos;
|
|
}
|
|
/* Seek to the specified position and row in the sequence. */
|
|
this.seekSequencePos = function( sequencePos, sequenceRow ) {
|
|
this.setSequencePos( 0 );
|
|
if( sequencePos < 0 || sequencePos >= module.sequenceLength ) {
|
|
sequencePos = 0;
|
|
}
|
|
if( sequenceRow >= module.patterns[ module.sequence[ sequencePos ] ].numRows ) {
|
|
sequenceRow = 0;
|
|
}
|
|
while( seqPos < sequencePos || row < sequenceRow ) {
|
|
var tickLen = calculateTickLen( tempo, samplingRate );
|
|
for( var idx = 0; idx < module.numChannels; idx++ ) {
|
|
channels[ idx ].updateSampleIdx( tickLen * 2, samplingRate * 2 );
|
|
}
|
|
if( seqTick() ) { /* Song end reached.*/
|
|
setSequencePos( sequencePos );
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
/* Write count floating-point stereo samples into the specified buffers. */
|
|
this.getAudio = function( leftBuf, rightBuf, count ) {
|
|
var outIdx = 0;
|
|
while( outIdx < count ) {
|
|
if( mixIdx >= mixLen ) {
|
|
mixLen = mixAudio();
|
|
mixIdx = 0;
|
|
}
|
|
var remain = mixLen - mixIdx;
|
|
if( ( outIdx + remain ) > count ) {
|
|
remain = count - outIdx;
|
|
}
|
|
for( var end = outIdx + remain; outIdx < end; outIdx++, mixIdx++ ) {
|
|
leftBuf[ outIdx ] = mixBuf[ mixIdx * 2 ];
|
|
rightBuf[ outIdx ] = mixBuf[ mixIdx * 2 + 1 ];
|
|
}
|
|
}
|
|
}
|
|
var mixAudio = function() {
|
|
/* Generate audio. The number of samples produced is returned.*/
|
|
var tickLen = calculateTickLen( tempo, samplingRate );
|
|
for( var idx = 0, end = ( tickLen + 65 ) * 4; idx < end; idx++ ) {
|
|
/* Clear mix buffer.*/
|
|
mixBuf[ idx ] = 0;
|
|
}
|
|
for( var idx = 0; idx < module.numChannels; idx++ ) {
|
|
/* Resample and mix each channel.*/
|
|
var chan = channels[ idx ];
|
|
if( !muted[ idx ] ) {
|
|
chan.resample( mixBuf, 0, ( tickLen + 65 ) * 2, samplingRate * 2, interpolation );
|
|
}
|
|
chan.updateSampleIdx( tickLen * 2, samplingRate * 2 );
|
|
}
|
|
downsample( mixBuf, tickLen + 64 );
|
|
volumeRamp( tickLen );
|
|
/* Update the sequencer.*/
|
|
seqTick();
|
|
return tickLen;
|
|
}
|
|
var calculateTickLen = function( tempo, sampleRate ) {
|
|
return ( ( sampleRate * 5 ) / ( tempo * 2 ) ) | 0;
|
|
}
|
|
var volumeRamp = function( tickLen ) {
|
|
var rampRate = 2048 / samplingRate;
|
|
for( var idx = 0, a1 = 0; a1 < 1; idx += 2, a1 += rampRate ) {
|
|
var a2 = 1 - a1;
|
|
mixBuf[ idx ] = mixBuf[ idx ] * a1 + rampBuf[ idx ] * a2;
|
|
mixBuf[ idx + 1 ] = mixBuf[ idx + 1 ] * a1 + rampBuf[ idx + 1 ] * a2;
|
|
}
|
|
rampBuf.set( mixBuf.subarray( tickLen * 2, ( tickLen + 64 ) * 2 ) );
|
|
}
|
|
var downsample = function( buf, count ) {
|
|
/* 2:1 downsampling with simple but effective anti-aliasing.*/
|
|
/* Buf must contain count * 2 + 1 stereo samples.*/
|
|
var outLen = count * 2;
|
|
for( inIdx = 0, outIdx = 0; outIdx < outLen; inIdx += 4, outIdx += 2 ) {
|
|
buf[ outIdx ] = buf[ inIdx ] * 0.25 + buf[ inIdx + 2 ] * 0.5 + buf[ inIdx + 4 ] * 0.25;
|
|
buf[ outIdx + 1 ] = buf[ inIdx + 1 ] * 0.25 + buf[ inIdx + 3 ] * 0.5 + buf[ inIdx + 5 ] * 0.25;
|
|
}
|
|
}
|
|
var seqTick = function() {
|
|
if( --tick <= 0 ) {
|
|
tick = speed;
|
|
seqRow();
|
|
} else {
|
|
for( var idx = 0; idx < module.numChannels; idx++ ) {
|
|
channels[ idx ].tick();
|
|
}
|
|
}
|
|
return playCount[ seqPos ][ row ] > 1;
|
|
}
|
|
var seqRow = function() {
|
|
if( nextRow < 0 ) {
|
|
breakSeqPos = seqPos + 1;
|
|
nextRow = 0;
|
|
}
|
|
if( breakSeqPos >= 0 ) {
|
|
if( breakSeqPos >= module.sequenceLength ) {
|
|
breakSeqPos = nextRow = 0;
|
|
}
|
|
while( module.sequence[ breakSeqPos ] >= module.numPatterns ) {
|
|
breakSeqPos++;
|
|
if( breakSeqPos >= module.sequenceLength ) {
|
|
breakSeqPos = nextRow = 0;
|
|
}
|
|
}
|
|
seqPos = breakSeqPos;
|
|
for( var idx = 0; idx < module.numChannels; idx++ ) {
|
|
channels[ idx ].plRow = 0;
|
|
}
|
|
breakSeqPos = -1;
|
|
}
|
|
var pattern = module.patterns[ module.sequence[ seqPos ] ];
|
|
row = nextRow;
|
|
if( row >= pattern.numRows ) row = 0;
|
|
var count = playCount[ seqPos ][ row ];
|
|
if( plCount < 0 && count < 127 ) {
|
|
playCount[ seqPos ][ row ] = count + 1;
|
|
}
|
|
nextRow = row + 1;
|
|
if( nextRow >= pattern.numRows ) {
|
|
nextRow = -1;
|
|
}
|
|
var noteIdx = row * module.numChannels;
|
|
for( var chanIdx = 0; chanIdx < module.numChannels; chanIdx++ ) {
|
|
var channel = channels[ chanIdx ];
|
|
pattern.getNote( noteIdx + chanIdx, note );
|
|
if( note.effect == 0xE ) {
|
|
note.effect = 0x70 | ( note.param >> 4 );
|
|
note.param &= 0xF;
|
|
}
|
|
if( note.effect == 0x93 ) {
|
|
note.effect = 0xF0 | ( note.param >> 4 );
|
|
note.param &= 0xF;
|
|
}
|
|
if( note.effect == 0 && note.param > 0 ) note.effect = 0x8A;
|
|
channel.row( note );
|
|
switch( note.effect ) {
|
|
case 0x81: /* Set Speed. */
|
|
if( note.param > 0 )
|
|
tick = speed = note.param;
|
|
break;
|
|
case 0xB: case 0x82: /* Pattern Jump.*/
|
|
if( plCount < 0 ) {
|
|
breakSeqPos = note.param;
|
|
nextRow = 0;
|
|
}
|
|
break;
|
|
case 0xD: case 0x83: /* Pattern Break.*/
|
|
if( plCount < 0 ) {
|
|
if( breakSeqPos < 0 )
|
|
breakSeqPos = seqPos + 1;
|
|
nextRow = ( note.param >> 4 ) * 10 + ( note.param & 0xF );
|
|
}
|
|
break;
|
|
case 0xF: /* Set Speed/Tempo.*/
|
|
if( note.param > 0 ) {
|
|
if( note.param < 32 )
|
|
tick = speed = note.param;
|
|
else
|
|
tempo = note.param;
|
|
}
|
|
break;
|
|
case 0x94: /* Set Tempo.*/
|
|
if( note.param > 32 )
|
|
tempo = note.param;
|
|
break;
|
|
case 0x76: case 0xFB : /* Pattern Loop.*/
|
|
if( note.param == 0 ) /* Set loop marker on this channel. */
|
|
channel.plRow = row;
|
|
if( channel.plRow < row && breakSeqPos < 0 ) { /* Marker valid. */
|
|
if( plCount < 0 ) { /* Not already looping, begin. */
|
|
plCount = note.param;
|
|
plChannel = chanIdx;
|
|
}
|
|
if( plChannel == chanIdx ) { /* Next Loop.*/
|
|
if( plCount == 0 ) { /* Loop finished. */
|
|
/* Invalidate current marker. */
|
|
channel.plRow = row + 1;
|
|
} else { /* Loop. */
|
|
nextRow = channel.plRow;
|
|
}
|
|
plCount--;
|
|
}
|
|
}
|
|
break;
|
|
case 0x7E: case 0xFE: /* Pattern Delay.*/
|
|
tick = speed + speed * note.param;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
var interpolation = true;
|
|
var rampBuf = new Float32Array( 64 * 2 );
|
|
var mixBuf = new Float32Array( ( calculateTickLen( 32, 128000 ) + 65 ) * 4 );
|
|
var mixIdx = 0, mixLen = 0;
|
|
var seqPos = 0, breakSeqPos = 0, row = 0, nextRow = 0, tick = 0;
|
|
var speed = 0, tempo = 0, plCount = 0, plChannel = 0;
|
|
var playCount = new Array( module.sequenceLength );
|
|
var channels = new Array( module.numChannels );
|
|
var muted = new Array( module.numChannels );
|
|
var note = new IBXMNote();
|
|
this.module = module;
|
|
this.globalVol = 0;
|
|
this.setSamplingRate( samplingRate );
|
|
this.setSequencePos( 0 );
|
|
}
|
|
|
|
function IBXMChannel( replay, id ) {
|
|
var sineTable = new Int16Array([
|
|
0, 24, 49, 74, 97, 120, 141, 161, 180, 197, 212, 224, 235, 244, 250, 253,
|
|
255, 253, 250, 244, 235, 224, 212, 197, 180, 161, 141, 120, 97, 74, 49, 24
|
|
]);
|
|
var instrument = new IBXMInstrument();
|
|
var sample = instrument.samples[ 0 ];
|
|
var keyOn = false;
|
|
var noteKey = 0, noteIns = 0, noteVol = 0, noteEffect = 0, noteParam = 0;
|
|
var sampleOffset = 0, sampleIdx = 0, freq = 0, ampl = 0, pann = 0;
|
|
var volume = 0, panning = replay.module.defaultPanning[ id ];
|
|
var fadeOutVol = 0, volEnvTick = 0, panEnvTick = 0;
|
|
var period = 0, portaPeriod = 0, retrigCount = 0, fxCount = 0, autoVibratoCount = 0;
|
|
var portaUpParam = 0, portaDownParam = 0, tonePortaParam = 0, offsetParam = 0;
|
|
var finePortaUpParam = 0, finePortaDownParam = 0, extraFinePortaParam = 0;
|
|
var arpeggioParam = 0, vslideParam = 0, globalVslideParam = 0, panningSlideParam = 0;
|
|
var fineVslideUpParam = 0, fineVslideDownParam = 0;
|
|
var retrigVolume = 0, retrigTicks = 0, tremorOnTicks = 0, tremorOffTicks = 0;
|
|
var vibratoType = 0, vibratoPhase = 0, vibratoSpeed = 0, vibratoDepth = 0;
|
|
var tremoloType = 0, tremoloPhase = 0, tremoloSpeed = 0, tremoloDepth = 0;
|
|
var tremoloAdd = 0, vibratoAdd = 0, arpeggioAdd = 0;
|
|
var randomSeed = ( id + 1 ) * 0xABCDEF;
|
|
this.plRow = 0;
|
|
this.resample = function( outBuf, offset, count, sampleRate, interpolate ) {
|
|
if( ampl <= 0 ) return;
|
|
var lGain = ampl * ( 1 - pann ) / 32768;
|
|
var rGain = ampl * pann / 32768;
|
|
var samIdx = sampleIdx;
|
|
var step = freq / sampleRate;
|
|
var loopLen = sample.loopLength;
|
|
var loopEnd = sample.loopStart + loopLen;
|
|
var sampleData = sample.sampleData;
|
|
var outIdx = offset * 2;
|
|
var outEnd = ( offset + count ) * 2;
|
|
if( interpolate ) {
|
|
while( outIdx < outEnd ) {
|
|
if( samIdx >= loopEnd ) {
|
|
if( loopLen <= 1 ) break;
|
|
while( samIdx >= loopEnd ) samIdx -= loopLen;
|
|
}
|
|
var x = samIdx | 0;
|
|
var c = sampleData[ x ];
|
|
var m = sampleData[ x + 1 ] - c;
|
|
var y = ( m * ( samIdx - x ) ) + c;
|
|
outBuf[ outIdx++ ] += y * lGain;
|
|
outBuf[ outIdx++ ] += y * rGain;
|
|
samIdx += step;
|
|
}
|
|
} else {
|
|
while( outIdx < outEnd ) {
|
|
if( samIdx >= loopEnd ) {
|
|
if( loopLen <= 1 ) break;
|
|
while( samIdx >= loopEnd ) samIdx -= loopLen;
|
|
}
|
|
var y = sampleData[ samIdx | 0 ];
|
|
outBuf[ outIdx++ ] += y * lGain;
|
|
outBuf[ outIdx++ ] += y * rGain;
|
|
samIdx += step;
|
|
}
|
|
}
|
|
}
|
|
this.updateSampleIdx = function( count, sampleRate ) {
|
|
sampleIdx += ( freq / sampleRate ) * count;
|
|
if( sampleIdx > sample.loopStart ) {
|
|
if( sample.loopLength > 1 ) {
|
|
sampleIdx = sample.loopStart + ( sampleIdx - sample.loopStart ) % sample.loopLength;
|
|
} else {
|
|
sampleIdx = sample.loopStart;
|
|
}
|
|
}
|
|
}
|
|
this.row = function( note ) {
|
|
noteKey = note.key;
|
|
noteIns = note.instrument;
|
|
noteVol = note.volume;
|
|
noteEffect = note.effect;
|
|
noteParam = note.param;
|
|
retrigCount++;
|
|
vibratoAdd = tremoloAdd = arpeggioAdd = fxCount = 0;
|
|
if( !( ( noteEffect == 0x7D || noteEffect == 0xFD ) && noteParam > 0 ) ) {
|
|
/* Not note delay.*/
|
|
trigger();
|
|
}
|
|
switch( noteEffect ) {
|
|
case 0x01: case 0x86: /* Porta Up. */
|
|
if( noteParam > 0 ) portaUpParam = noteParam;
|
|
portamentoUp( portaUpParam );
|
|
break;
|
|
case 0x02: case 0x85: /* Porta Down. */
|
|
if( noteParam > 0 ) portaDownParam = noteParam;
|
|
portamentoDown( portaDownParam );
|
|
break;
|
|
case 0x03: case 0x87: /* Tone Porta. */
|
|
if( noteParam > 0 ) tonePortaParam = noteParam;
|
|
break;
|
|
case 0x04: case 0x88: /* Vibrato. */
|
|
if( ( noteParam >> 4 ) > 0 ) vibratoSpeed = noteParam >> 4;
|
|
if( ( noteParam & 0xF ) > 0 ) vibratoDepth = noteParam & 0xF;
|
|
vibrato( false );
|
|
break;
|
|
case 0x05: case 0x8C: /* Tone Porta + Vol Slide. */
|
|
if( noteParam > 0 ) vslideParam = noteParam;
|
|
volumeSlide();
|
|
break;
|
|
case 0x06: case 0x8B: /* Vibrato + Vol Slide. */
|
|
if( noteParam > 0 ) vslideParam = noteParam;
|
|
vibrato( false );
|
|
volumeSlide();
|
|
break;
|
|
case 0x07: case 0x92: /* Tremolo. */
|
|
if( ( noteParam >> 4 ) > 0 ) tremoloSpeed = noteParam >> 4;
|
|
if( ( noteParam & 0xF ) > 0 ) tremoloDepth = noteParam & 0xF;
|
|
tremolo();
|
|
break;
|
|
case 0x08: /* Set Panning.*/
|
|
panning = noteParam & 0xFF;
|
|
break;
|
|
case 0x0A: case 0x84: /* Vol Slide. */
|
|
if( noteParam > 0 ) vslideParam = noteParam;
|
|
volumeSlide();
|
|
break;
|
|
case 0x0C: /* Set Volume. */
|
|
volume = noteParam >= 64 ? 64 : noteParam & 0x3F;
|
|
break;
|
|
case 0x10: case 0x96: /* Set Global Volume. */
|
|
replay.globalVol = noteParam >= 64 ? 64 : noteParam & 0x3F;
|
|
break;
|
|
case 0x11: /* Global Volume Slide. */
|
|
if( noteParam > 0 ) globalVslideParam = noteParam;
|
|
break;
|
|
case 0x14: /* Key Off. */
|
|
keyOn = false;
|
|
break;
|
|
case 0x15: /* Set Envelope Tick. */
|
|
volEnvTick = panEnvTick = noteParam & 0xFF;
|
|
break;
|
|
case 0x19: /* Panning Slide. */
|
|
if( noteParam > 0 ) panningSlideParam = noteParam;
|
|
break;
|
|
case 0x1B: case 0x91: /* Retrig + Vol Slide. */
|
|
if( ( noteParam >> 4 ) > 0 ) retrigVolume = noteParam >> 4;
|
|
if( ( noteParam & 0xF ) > 0 ) retrigTicks = noteParam & 0xF;
|
|
retrigVolSlide();
|
|
break;
|
|
case 0x1D: case 0x89: /* Tremor. */
|
|
if( ( noteParam >> 4 ) > 0 ) tremorOnTicks = noteParam >> 4;
|
|
if( ( noteParam & 0xF ) > 0 ) tremorOffTicks = noteParam & 0xF;
|
|
tremor();
|
|
break;
|
|
case 0x21: /* Extra Fine Porta. */
|
|
if( noteParam > 0 ) extraFinePortaParam = noteParam;
|
|
switch( extraFinePortaParam & 0xF0 ) {
|
|
case 0x10:
|
|
portamentoUp( 0xE0 | ( extraFinePortaParam & 0xF ) );
|
|
break;
|
|
case 0x20:
|
|
portamentoDown( 0xE0 | ( extraFinePortaParam & 0xF ) );
|
|
break;
|
|
}
|
|
break;
|
|
case 0x71: /* Fine Porta Up. */
|
|
if( noteParam > 0 ) finePortaUpParam = noteParam;
|
|
portamentoUp( 0xF0 | ( finePortaUpParam & 0xF ) );
|
|
break;
|
|
case 0x72: /* Fine Porta Down. */
|
|
if( noteParam > 0 ) finePortaDownParam = noteParam;
|
|
portamentoDown( 0xF0 | ( finePortaDownParam & 0xF ) );
|
|
break;
|
|
case 0x74: case 0xF3: /* Set Vibrato Waveform. */
|
|
if( noteParam < 8 ) vibratoType = noteParam;
|
|
break;
|
|
case 0x77: case 0xF4: /* Set Tremolo Waveform. */
|
|
if( noteParam < 8 ) tremoloType = noteParam;
|
|
break;
|
|
case 0x7A: /* Fine Vol Slide Up. */
|
|
if( noteParam > 0 ) fineVslideUpParam = noteParam;
|
|
volume += fineVslideUpParam;
|
|
if( volume > 64 ) volume = 64;
|
|
break;
|
|
case 0x7B: /* Fine Vol Slide Down. */
|
|
if( noteParam > 0 ) fineVslideDownParam = noteParam;
|
|
volume -= fineVslideDownParam;
|
|
if( volume < 0 ) volume = 0;
|
|
break;
|
|
case 0x7C: case 0xFC: /* Note Cut. */
|
|
if( noteParam <= 0 ) volume = 0;
|
|
break;
|
|
case 0x8A: /* Arpeggio. */
|
|
if( noteParam > 0 ) arpeggioParam = noteParam;
|
|
break;
|
|
case 0x95: /* Fine Vibrato.*/
|
|
if( ( noteParam >> 4 ) > 0 ) vibratoSpeed = noteParam >> 4;
|
|
if( ( noteParam & 0xF ) > 0 ) vibratoDepth = noteParam & 0xF;
|
|
vibrato( true );
|
|
break;
|
|
case 0xF8: /* Set Panning. */
|
|
panning = noteParam * 17;
|
|
break;
|
|
}
|
|
autoVibrato();
|
|
calculateFrequency();
|
|
calculateAmplitude();
|
|
updateEnvelopes();
|
|
}
|
|
this.tick = function() {
|
|
vibratoAdd = 0;
|
|
fxCount++;
|
|
retrigCount++;
|
|
if( !( noteEffect == 0x7D && fxCount <= noteParam ) ) {
|
|
switch( noteVol & 0xF0 ) {
|
|
case 0x60: /* Vol Slide Down.*/
|
|
volume -= noteVol & 0xF;
|
|
if( volume < 0 ) volume = 0;
|
|
break;
|
|
case 0x70: /* Vol Slide Up.*/
|
|
volume += noteVol & 0xF;
|
|
if( volume > 64 ) volume = 64;
|
|
break;
|
|
case 0xB0: /* Vibrato.*/
|
|
vibratoPhase += vibratoSpeed;
|
|
vibrato( false );
|
|
break;
|
|
case 0xD0: /* Pan Slide Left.*/
|
|
panning -= noteVol & 0xF;
|
|
if( panning < 0 ) panning = 0;
|
|
break;
|
|
case 0xE0: /* Pan Slide Right.*/
|
|
panning += noteVol & 0xF;
|
|
if( panning > 255 ) panning = 255;
|
|
break;
|
|
case 0xF0: /* Tone Porta.*/
|
|
tonePortamento();
|
|
break;
|
|
}
|
|
}
|
|
switch( noteEffect ) {
|
|
case 0x01: case 0x86: /* Porta Up. */
|
|
portamentoUp( portaUpParam );
|
|
break;
|
|
case 0x02: case 0x85: /* Porta Down. */
|
|
portamentoDown( portaDownParam );
|
|
break;
|
|
case 0x03: case 0x87: /* Tone Porta. */
|
|
tonePortamento();
|
|
break;
|
|
case 0x04: case 0x88: /* Vibrato. */
|
|
vibratoPhase += vibratoSpeed;
|
|
vibrato( false );
|
|
break;
|
|
case 0x05: case 0x8C: /* Tone Porta + Vol Slide. */
|
|
tonePortamento();
|
|
volumeSlide();
|
|
break;
|
|
case 0x06: case 0x8B: /* Vibrato + Vol Slide. */
|
|
vibratoPhase += vibratoSpeed;
|
|
vibrato( false );
|
|
volumeSlide();
|
|
break;
|
|
case 0x07: case 0x92: /* Tremolo. */
|
|
tremoloPhase += tremoloSpeed;
|
|
tremolo();
|
|
break;
|
|
case 0x0A: case 0x84: /* Vol Slide. */
|
|
volumeSlide();
|
|
break;
|
|
case 0x11: /* Global Volume Slide. */
|
|
replay.globalVol += ( globalVslideParam >> 4 ) - ( globalVslideParam & 0xF );
|
|
if( replay.globalVol < 0 ) replay.globalVol = 0;
|
|
if( replay.globalVol > 64 ) replay.globalVol = 64;
|
|
break;
|
|
case 0x19: /* Panning Slide. */
|
|
panning += ( panningSlideParam >> 4 ) - ( panningSlideParam & 0xF );
|
|
if( panning < 0 ) panning = 0;
|
|
if( panning > 255 ) panning = 255;
|
|
break;
|
|
case 0x1B: case 0x91: /* Retrig + Vol Slide. */
|
|
retrigVolSlide();
|
|
break;
|
|
case 0x1D: case 0x89: /* Tremor. */
|
|
tremor();
|
|
break;
|
|
case 0x79: /* Retrig. */
|
|
if( fxCount >= noteParam ) {
|
|
fxCount = 0;
|
|
sampleIdx = 0;
|
|
}
|
|
break;
|
|
case 0x7C: case 0xFC: /* Note Cut. */
|
|
if( noteParam == fxCount ) volume = 0;
|
|
break;
|
|
case 0x7D: case 0xFD: /* Note Delay. */
|
|
if( noteParam == fxCount ) trigger();
|
|
break;
|
|
case 0x8A: /* Arpeggio. */
|
|
if( fxCount > 2 ) fxCount = 0;
|
|
if( fxCount == 0 ) arpeggioAdd = 0;
|
|
if( fxCount == 1 ) arpeggioAdd = arpeggioParam >> 4;
|
|
if( fxCount == 2 ) arpeggioAdd = arpeggioParam & 0xF;
|
|
break;
|
|
case 0x95: /* Fine Vibrato. */
|
|
vibratoPhase += vibratoSpeed;
|
|
vibrato( true );
|
|
break;
|
|
}
|
|
autoVibrato();
|
|
calculateFrequency();
|
|
calculateAmplitude();
|
|
updateEnvelopes();
|
|
}
|
|
var updateEnvelopes = function() {
|
|
if( instrument.volumeEnvelope.enabled ) {
|
|
if( !keyOn ) {
|
|
fadeOutVol -= instrument.volumeFadeOut;
|
|
if( fadeOutVol < 0 ) fadeOutVol = 0;
|
|
}
|
|
volEnvTick = instrument.volumeEnvelope.nextTick( volEnvTick, keyOn );
|
|
}
|
|
if( instrument.panningEnvelope.enabled )
|
|
panEnvTick = instrument.panningEnvelope.nextTick( panEnvTick, keyOn );
|
|
}
|
|
var autoVibrato = function() {
|
|
var depth = instrument.vibratoDepth & 0x7F;
|
|
if( depth > 0 ) {
|
|
var sweep = instrument.vibratoSweep & 0x7F;
|
|
var rate = instrument.vibratoRate & 0x7F;
|
|
var type = instrument.vibratoType;
|
|
if( autoVibratoCount < sweep ) depth = depth * autoVibratoCount / sweep;
|
|
vibratoAdd += waveform( autoVibratoCount * rate >> 2, type + 4 ) * depth >> 8;
|
|
autoVibratoCount++;
|
|
}
|
|
}
|
|
var volumeSlide = function() {
|
|
var up = vslideParam >> 4;
|
|
var down = vslideParam & 0xF;
|
|
if( down == 0xF && up > 0 ) { /* Fine slide up.*/
|
|
if( fxCount == 0 ) volume += up;
|
|
} else if( up == 0xF && down > 0 ) { /* Fine slide down.*/
|
|
if( fxCount == 0 ) volume -= down;
|
|
} else if( fxCount > 0 || replay.module.fastVolSlides ) /* Normal.*/
|
|
volume += up - down;
|
|
if( volume > 64 ) volume = 64;
|
|
if( volume < 0 ) volume = 0;
|
|
}
|
|
var portamentoUp = function( param ) {
|
|
switch( param & 0xF0 ) {
|
|
case 0xE0: /* Extra-fine porta.*/
|
|
if( fxCount == 0 ) period -= param & 0xF;
|
|
break;
|
|
case 0xF0: /* Fine porta.*/
|
|
if( fxCount == 0 ) period -= ( param & 0xF ) << 2;
|
|
break;
|
|
default:/* Normal porta.*/
|
|
if( fxCount > 0 ) period -= param << 2;
|
|
break;
|
|
}
|
|
if( period < 0 ) period = 0;
|
|
}
|
|
var portamentoDown = function( param ) {
|
|
if( period > 0 ) {
|
|
switch( param & 0xF0 ) {
|
|
case 0xE0: /* Extra-fine porta.*/
|
|
if( fxCount == 0 ) period += param & 0xF;
|
|
break;
|
|
case 0xF0: /* Fine porta.*/
|
|
if( fxCount == 0 ) period += ( param & 0xF ) << 2;
|
|
break;
|
|
default:/* Normal porta.*/
|
|
if( fxCount > 0 ) period += param << 2;
|
|
break;
|
|
}
|
|
if( period > 65535 ) period = 65535;
|
|
}
|
|
}
|
|
var tonePortamento = function() {
|
|
if( period > 0 ) {
|
|
if( period < portaPeriod ) {
|
|
period += tonePortaParam << 2;
|
|
if( period > portaPeriod ) period = portaPeriod;
|
|
} else {
|
|
period -= tonePortaParam << 2;
|
|
if( period < portaPeriod ) period = portaPeriod;
|
|
}
|
|
}
|
|
}
|
|
var vibrato = function( fine ) {
|
|
vibratoAdd = waveform( vibratoPhase, vibratoType & 0x3 ) * vibratoDepth >> ( fine ? 7 : 5 );
|
|
}
|
|
var tremolo = function() {
|
|
tremoloAdd = waveform( tremoloPhase, tremoloType & 0x3 ) * tremoloDepth >> 6;
|
|
}
|
|
var waveform = function( phase, type ) {
|
|
var amplitude = 0;
|
|
switch( type ) {
|
|
default: /* Sine. */
|
|
amplitude = sineTable[ phase & 0x1F ];
|
|
if( ( phase & 0x20 ) > 0 ) amplitude = -amplitude;
|
|
break;
|
|
case 6: /* Saw Up.*/
|
|
amplitude = ( ( ( phase + 0x20 ) & 0x3F ) << 3 ) - 255;
|
|
break;
|
|
case 1: case 7: /* Saw Down. */
|
|
amplitude = 255 - ( ( ( phase + 0x20 ) & 0x3F ) << 3 );
|
|
break;
|
|
case 2: case 5: /* Square. */
|
|
amplitude = ( phase & 0x20 ) > 0 ? 255 : -255;
|
|
break;
|
|
case 3: case 8: /* Random. */
|
|
amplitude = ( randomSeed >> 20 ) - 255;
|
|
randomSeed = ( randomSeed * 65 + 17 ) & 0x1FFFFFFF;
|
|
break;
|
|
}
|
|
return amplitude;
|
|
}
|
|
var tremor = function() {
|
|
if( retrigCount >= tremorOnTicks ) tremoloAdd = -64;
|
|
if( retrigCount >= ( tremorOnTicks + tremorOffTicks ) )
|
|
tremoloAdd = retrigCount = 0;
|
|
}
|
|
var retrigVolSlide = function() {
|
|
if( retrigCount >= retrigTicks ) {
|
|
retrigCount = sampleIdx = 0;
|
|
switch( retrigVolume ) {
|
|
case 0x1: volume = volume - 1; break;
|
|
case 0x2: volume = volume - 2; break;
|
|
case 0x3: volume = volume - 4; break;
|
|
case 0x4: volume = volume - 8; break;
|
|
case 0x5: volume = volume - 16; break;
|
|
case 0x6: volume = ( volume * 2 / 3 ) | 0; break;
|
|
case 0x7: volume = volume >> 1; break;
|
|
case 0x8: /* ? */ break;
|
|
case 0x9: volume = volume + 1; break;
|
|
case 0xA: volume = volume + 2; break;
|
|
case 0xB: volume = volume + 4; break;
|
|
case 0xC: volume = volume + 8; break;
|
|
case 0xD: volume = volume + 16; break;
|
|
case 0xE: volume = ( volume * 3 / 2 ) | 0; break;
|
|
case 0xF: volume = volume << 1; break;
|
|
}
|
|
if( volume < 0 ) volume = 0;
|
|
if( volume > 64 ) volume = 64;
|
|
}
|
|
}
|
|
var calculateFrequency = function() {
|
|
var per = period + vibratoAdd;
|
|
if( replay.module.linearPeriods ) {
|
|
per = per - ( arpeggioAdd << 6 );
|
|
if( per < 28 || per > 7680 ) per = 7680;
|
|
freq = Math.round( replay.module.c2Rate * Math.pow( 2, ( 4608 - per ) / 768 ) );
|
|
} else {
|
|
per = per / Math.pow( 2, arpeggioAdd / 12 );
|
|
if( per < 28 ) per = 29021;
|
|
freq = Math.round( replay.module.c2Rate * 1712 / per );
|
|
}
|
|
}
|
|
var calculateAmplitude = function() {
|
|
var envVol = keyOn ? 64 : 0;
|
|
if( instrument.volumeEnvelope.enabled )
|
|
envVol = instrument.volumeEnvelope.calculateAmpl( volEnvTick );
|
|
var vol = volume + tremoloAdd;
|
|
if( vol > 64 ) vol = 64;
|
|
if( vol < 0 ) vol = 0;
|
|
vol = vol * fadeOutVol / 0x8000;
|
|
ampl = vol * envVol * replay.globalVol * replay.module.gain / 0x2000000;
|
|
var envPan = 32;
|
|
if( instrument.panningEnvelope.enabled )
|
|
envPan = instrument.panningEnvelope.calculateAmpl( panEnvTick );
|
|
var panRange = ( panning < 128 ) ? panning : ( 255 - panning );
|
|
pann = ( panning + ( panRange * ( envPan - 32 ) >> 5 ) ) / 255;
|
|
}
|
|
var trigger = function() {
|
|
if( noteIns > 0 && noteIns <= replay.module.numInstruments ) {
|
|
instrument = replay.module.instruments[ noteIns ];
|
|
var sam = instrument.samples[ instrument.keyToSample[ noteKey < 97 ? noteKey : 0 ] ];
|
|
volume = sam.volume >= 64 ? 64 : sam.volume & 0x3F;
|
|
if( sam.panning >= 0 ) panning = sam.panning & 0xFF;
|
|
if( period > 0 && sam.loopLength > 1 ) sample = sam; /* Amiga trigger.*/
|
|
sampleOffset = volEnvTick = panEnvTick = 0;
|
|
fadeOutVol = 32768;
|
|
keyOn = true;
|
|
}
|
|
if( noteEffect == 0x09 || noteEffect == 0x8F ) { /* Set Sample Offset. */
|
|
if( noteParam > 0 ) offsetParam = noteParam;
|
|
sampleOffset = offsetParam << 8;
|
|
}
|
|
if( noteVol >= 0x10 && noteVol < 0x60 )
|
|
volume = noteVol < 0x50 ? noteVol - 0x10 : 64;
|
|
switch( noteVol & 0xF0 ) {
|
|
case 0x80: /* Fine Vol Down.*/
|
|
volume -= noteVol & 0xF;
|
|
if( volume < 0 ) volume = 0;
|
|
break;
|
|
case 0x90: /* Fine Vol Up.*/
|
|
volume += noteVol & 0xF;
|
|
if( volume > 64 ) volume = 64;
|
|
break;
|
|
case 0xA0: /* Set Vibrato Speed.*/
|
|
if( ( noteVol & 0xF ) > 0 ) vibratoSpeed = noteVol & 0xF;
|
|
break;
|
|
case 0xB0: /* Vibrato.*/
|
|
if( ( noteVol & 0xF ) > 0 ) vibratoDepth = noteVol & 0xF;
|
|
vibrato( false );
|
|
break;
|
|
case 0xC0: /* Set Panning.*/
|
|
panning = ( noteVol & 0xF ) * 17;
|
|
break;
|
|
case 0xF0: /* Tone Porta.*/
|
|
if( ( noteVol & 0xF ) > 0 ) tonePortaParam = noteVol & 0xF;
|
|
break;
|
|
}
|
|
if( noteKey > 0 ) {
|
|
if( noteKey > 96 ) {
|
|
keyOn = false;
|
|
} else {
|
|
var isPorta = ( noteVol & 0xF0 ) == 0xF0 ||
|
|
noteEffect == 0x03 || noteEffect == 0x05 ||
|
|
noteEffect == 0x87 || noteEffect == 0x8C;
|
|
if( !isPorta ) sample = instrument.samples[ instrument.keyToSample[ noteKey ] ];
|
|
var fineTune = sample.fineTune;
|
|
if( noteEffect == 0x75 || noteEffect == 0xF2 ) { /* Set Fine Tune. */
|
|
fineTune = ( ( noteParam & 0xF ) << 4 ) - 128;
|
|
}
|
|
var key = noteKey + sample.relNote;
|
|
if( key < 1 ) key = 1;
|
|
if( key > 120 ) key = 120;
|
|
var per = ( key << 6 ) + ( fineTune >> 1 );
|
|
if( replay.module.linearPeriods ) {
|
|
portaPeriod = 7744 - per;
|
|
} else {
|
|
portaPeriod = 29021 * Math.pow( 2, -per / 768 );
|
|
}
|
|
if( !isPorta ) {
|
|
period = portaPeriod;
|
|
sampleIdx = sampleOffset;
|
|
if( vibratoType < 4 ) vibratoPhase = 0;
|
|
if( tremoloType < 4 ) tremoloPhase = 0;
|
|
retrigCount = autoVibratoCount = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function IBXMNote() {
|
|
this.key = 0;
|
|
this.instrument = 0;
|
|
this.volume = 0;
|
|
this.effect = 0;
|
|
this.param = 0;
|
|
this.toChars = function( chars ) {
|
|
const keyToString = "A-A#B-C-C#D-D#E-F-F#G-G#";
|
|
const b36ToString = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
chars[ 0 ] = ( this.key > 0 && this.key < 118 ) ? keyToString.codePointAt( ( ( this.key + 2 ) % 12 ) * 2 ) : 45;
|
|
chars[ 1 ] = ( this.key > 0 && this.key < 118 ) ? keyToString.codePointAt( ( ( this.key + 2 ) % 12 ) * 2 + 1 ) : 45;
|
|
chars[ 2 ] = ( this.key > 0 && this.key < 118 ) ? 48 + ( ( ( this.key + 2 ) / 12 ) | 0 ) : 45;
|
|
chars[ 3 ] = ( this.instrument > 0xF && this.instrument < 0xFF ) ? b36ToString.codePointAt( ( this.instrument >> 4 ) & 0xF ) : 45;
|
|
chars[ 4 ] = ( this.instrument > 0x0 && this.instrument < 0xFF ) ? b36ToString.codePointAt( this.instrument & 0xF ) : 45;
|
|
chars[ 5 ] = ( this.volume > 0xF && this.volume < 0xFF ) ? b36ToString.codePointAt( ( this.volume >> 4 ) & 0xF ) : 45;
|
|
chars[ 6 ] = ( this.volume > 0x0 && this.volume < 0xFF ) ? b36ToString.codePointAt( this.volume & 0xF ) : 45;
|
|
if( ( this.effect > 0 || this.param > 0 ) && this.effect < 36 ) {
|
|
chars[ 7 ] = b36ToString.codePointAt( this.effect );
|
|
} else if( this.effect > 0x80 && this.effect < 0x9F ) {
|
|
chars[ 7 ] = 96 + ( this.effect & 0x1F );
|
|
} else {
|
|
chars[ 7 ] = 45;
|
|
}
|
|
chars[ 8 ] = ( this.effect > 0 || this.param > 0 ) ? b36ToString.codePointAt( ( this.param >> 4 ) & 0xF ) : 45;
|
|
chars[ 9 ] = ( this.effect > 0 || this.param > 0 ) ? b36ToString.codePointAt( this.param & 0xF ) : 45;
|
|
return chars;
|
|
}
|
|
}
|
|
|
|
function IBXMPattern( numChannels, numRows ) {
|
|
this.numRows = numRows;
|
|
this.data = new Int8Array( numChannels * numRows * 5 );
|
|
this.getNote = function( index, note ) {
|
|
var offset = index * 5;
|
|
note.key = this.data[ offset ] & 0xFF;
|
|
note.instrument = this.data[ offset + 1 ] & 0xFF;
|
|
note.volume = this.data[ offset + 2 ] & 0xFF;
|
|
note.effect = this.data[ offset + 3 ] & 0xFF;
|
|
note.param = this.data[ offset + 4 ] & 0xFF;
|
|
return note;
|
|
}
|
|
}
|
|
|
|
function IBXMInstrument() {
|
|
this.name = "";
|
|
this.numSamples = 1;
|
|
this.vibratoType = 0;
|
|
this.vibratoSweep = 0;
|
|
this.vibratoDepth = 0;
|
|
this.vibratoRate = 0;
|
|
this.volumeFadeOut = 0;
|
|
this.volumeEnvelope = new IBXMEnvelope();
|
|
this.panningEnvelope = new IBXMEnvelope();
|
|
this.keyToSample = new Int8Array( 97 );
|
|
this.samples = [ new IBXMSample() ];
|
|
}
|
|
|
|
function IBXMEnvelope() {
|
|
this.enabled = false;
|
|
this.sustain = false;
|
|
this.looped = false;
|
|
this.sustainTick = 0;
|
|
this.loopStartTick = 0;
|
|
this.loopEndTick = 0;
|
|
this.numPoints = 1;
|
|
this.pointsTick = new Int32Array( 1 );
|
|
this.pointsAmpl = new Int32Array( 1 );
|
|
this.nextTick = function( tick, keyOn ) {
|
|
tick++;
|
|
if( this.looped && tick >= this.loopEndTick ) tick = this.loopStartTick;
|
|
if( this.sustain && keyOn && tick >= this.sustainTick ) tick = this.sustainTick;
|
|
return tick;
|
|
}
|
|
this.calculateAmpl = function( tick ) {
|
|
var ampl = this.pointsAmpl[ this.numPoints - 1 ];
|
|
if( tick < this.pointsTick[ this.numPoints - 1 ] ) {
|
|
var point = 0;
|
|
for( var idx = 1; idx < this.numPoints; idx++ )
|
|
if( this.pointsTick[ idx ] <= tick ) point = idx;
|
|
var dt = this.pointsTick[ point + 1 ] - this.pointsTick[ point ];
|
|
var da = this.pointsAmpl[ point + 1 ] - this.pointsAmpl[ point ];
|
|
ampl = this.pointsAmpl[ point ];
|
|
ampl += ( da / dt ) * ( tick - this.pointsTick[ point ] );
|
|
}
|
|
return ampl;
|
|
}
|
|
}
|
|
|
|
function IBXMSample() {
|
|
this.name = "";
|
|
this.volume = 0;
|
|
this.panning = -1;
|
|
this.relNote = 0;
|
|
this.fineTune = 0;
|
|
this.loopStart = 0;
|
|
this.loopLength = 0;
|
|
this.sampleData = new Int16Array( 1 );
|
|
this.setSampleData = function( sampleData, loopStart, loopLength, pingPong ) {
|
|
var sampleLength = sampleData.length;
|
|
/* Fix loop if necessary.*/
|
|
if( loopStart < 0 || loopStart > sampleLength )
|
|
loopStart = sampleLength;
|
|
if( loopLength < 0 || ( loopStart + loopLength ) > sampleLength )
|
|
loopLength = sampleLength - loopStart;
|
|
sampleLength = loopStart + loopLength;
|
|
/* Allocate new sample.*/
|
|
var newSampleData = new Int16Array( sampleLength + ( pingPong ? loopLength : 0 ) + 1 );
|
|
newSampleData.set( sampleData.subarray( 0, sampleLength ) );
|
|
sampleData = newSampleData;
|
|
if( pingPong ) {
|
|
/* Calculate reversed loop.*/
|
|
for( var idx = 0; idx < loopLength; idx++ )
|
|
sampleData[ sampleLength + idx ] = sampleData[ sampleLength - idx - 1 ];
|
|
loopLength *= 2;
|
|
}
|
|
/* Extend loop for linear interpolation.*/
|
|
sampleData[ loopStart + loopLength ] = sampleData[ loopStart ];
|
|
this.sampleData = sampleData;
|
|
this.loopStart = loopStart;
|
|
this.loopLength = loopLength;
|
|
}
|
|
}
|
|
|
|
function IBXMData( buffer ) {
|
|
this.sByte = function( offset ) {
|
|
return buffer[ offset ] | 0;
|
|
}
|
|
this.uByte = function( offset ) {
|
|
return buffer[ offset ] & 0xFF;
|
|
}
|
|
this.ubeShort = function( offset ) {
|
|
return ( ( buffer[ offset ] & 0xFF ) << 8 ) | ( buffer[ offset + 1 ] & 0xFF );
|
|
}
|
|
this.uleShort = function( offset ) {
|
|
return ( buffer[ offset ] & 0xFF ) | ( ( buffer[ offset + 1 ] & 0xFF ) << 8 );
|
|
}
|
|
this.uleInt = function( offset ) {
|
|
var value = buffer[ offset ] & 0xFF;
|
|
value = value | ( ( buffer[ offset + 1 ] & 0xFF ) << 8 );
|
|
value = value | ( ( buffer[ offset + 2 ] & 0xFF ) << 16 );
|
|
value = value | ( ( buffer[ offset + 3 ] & 0x7F ) << 24 );
|
|
return value;
|
|
}
|
|
this.strLatin1 = function( offset, length ) {
|
|
var str = new Array( length );
|
|
for( var idx = 0; idx < length; idx++ ) {
|
|
var chr = buffer[ offset + idx ] & 0xFF;
|
|
str[ idx ] = String.fromCharCode( chr < 32 ? 32 : chr );
|
|
}
|
|
return str.join('');
|
|
}
|
|
this.samS8 = function( offset, length ) {
|
|
var sampleData = new Int16Array( length );
|
|
for( var idx = 0; idx < length; idx++ ) {
|
|
sampleData[ idx ] = buffer[ offset + idx ] << 8;
|
|
}
|
|
return sampleData;
|
|
}
|
|
this.samS8D = function( offset, length ) {
|
|
var sampleData = new Int16Array( length );
|
|
var sam = 0;
|
|
for( var idx = 0; idx < length; idx++ ) {
|
|
sam += buffer[ offset + idx ] | 0;
|
|
sampleData[ idx ] = sam << 8;
|
|
}
|
|
return sampleData;
|
|
}
|
|
this.samU8 = function( offset, length ) {
|
|
var sampleData = new Int16Array( length );
|
|
for( var idx = 0; idx < length; idx++ ) {
|
|
sampleData[ idx ] = ( ( buffer[ offset + idx ] & 0xFF ) - 128 ) << 8;
|
|
}
|
|
return sampleData;
|
|
}
|
|
this.samS16 = function( offset, samples ) {
|
|
var sampleData = new Int16Array( samples );
|
|
for( var idx = 0; idx < samples; idx++ ) {
|
|
sampleData[ idx ] = ( buffer[ offset + idx * 2 ] & 0xFF ) | ( buffer[ offset + idx * 2 + 1 ] << 8 );
|
|
}
|
|
return sampleData;
|
|
}
|
|
this.samS16D = function( offset, samples ) {
|
|
var sampleData = new Int16Array( samples );
|
|
var sam = 0;
|
|
for( var idx = 0; idx < samples; idx++ ) {
|
|
sam += ( buffer[ offset + idx * 2 ] & 0xFF ) | ( buffer[ offset + idx * 2 + 1 ] << 8 );
|
|
sampleData[ idx ] = sam;
|
|
}
|
|
return sampleData;
|
|
}
|
|
this.samU16 = function( offset, samples ) {
|
|
var sampleData = new Int16Array( samples );
|
|
for( var idx = 0; idx < samples; idx++ ) {
|
|
var sam = ( buffer[ offset + idx * 2 ] & 0xFF ) | ( ( buffer[ offset + idx * 2 + 1 ] & 0xFF ) << 8 );
|
|
sampleData[ idx ] = sam - 32768;
|
|
}
|
|
return sampleData;
|
|
}
|
|
}
|
|
|
|
function IBXMModule( moduleData ) {
|
|
this.songName = "Blank";
|
|
this.numChannels = 4;
|
|
this.numInstruments = 1;
|
|
this.numPatterns = 1;
|
|
this.sequenceLength = 1;
|
|
this.restartPos = 0;
|
|
this.defaultGVol = 64;
|
|
this.defaultSpeed = 6;
|
|
this.defaultTempo = 125;
|
|
this.c2Rate = 8287;
|
|
this.gain = 64;
|
|
this.linearPeriods = false;
|
|
this.fastVolSlides = false;
|
|
this.defaultPanning = new Int32Array( [ 51, 204, 204, 51 ] );
|
|
this.sequence = new Int32Array( 1 );
|
|
this.patterns = [ new IBXMPattern( 4, 64 ) ];
|
|
this.instruments = [ new IBXMInstrument(), new IBXMInstrument() ];
|
|
this.loadXM = function( ibxmData ) {
|
|
if( ibxmData.uleShort( 58 ) != 0x0104 )
|
|
throw "XM format version must be 0x0104!";
|
|
this.songName = ibxmData.strLatin1( 17, 20 );
|
|
var deltaEnv = ibxmData.strLatin1( 38, 20 ).startsWith( "DigiBooster Pro" );
|
|
var dataOffset = 60 + ibxmData.uleInt( 60 );
|
|
this.sequenceLength = ibxmData.uleShort( 64 );
|
|
this.restartPos = ibxmData.uleShort( 66 );
|
|
this.numChannels = ibxmData.uleShort( 68 );
|
|
this.numPatterns = ibxmData.uleShort( 70 );
|
|
this.numInstruments = ibxmData.uleShort( 72 );
|
|
this.linearPeriods = ( ibxmData.uleShort( 74 ) & 0x1 ) > 0;
|
|
this.defaultGVol = 64;
|
|
this.defaultSpeed = ibxmData.uleShort( 76 );
|
|
this.defaultTempo = ibxmData.uleShort( 78 );
|
|
this.c2Rate = 8363;
|
|
this.gain = 64;
|
|
this.fastVolSlides = false;
|
|
this.defaultPanning = new Int32Array( this.numChannels );
|
|
for( var idx = 0; idx < this.numChannels; idx++ ) this.defaultPanning[ idx ] = 128;
|
|
this.sequence = new Int32Array( this.sequenceLength );
|
|
for( var seqIdx = 0; seqIdx < this.sequenceLength; seqIdx++ ) {
|
|
var entry = ibxmData.uByte( 80 + seqIdx );
|
|
this.sequence[ seqIdx ] = entry < this.numPatterns ? entry : 0;
|
|
}
|
|
this.patterns = new Array( this.numPatterns );
|
|
for( var patIdx = 0; patIdx < this.numPatterns; patIdx++ ) {
|
|
if( ibxmData.uByte( dataOffset + 4 ) != 0 )
|
|
throw "Unknown pattern packing type!";
|
|
var numRows = ibxmData.uleShort( dataOffset + 5 );
|
|
if( numRows < 1 ) {
|
|
numRows = 1;
|
|
}
|
|
var numNotes = numRows * this.numChannels;
|
|
var pattern = this.patterns[ patIdx ] = new IBXMPattern( this.numChannels, numRows );
|
|
var patternDataLength = ibxmData.uleShort( dataOffset + 7 );
|
|
dataOffset += ibxmData.uleInt( dataOffset );
|
|
var nextOffset = dataOffset + patternDataLength;
|
|
if( patternDataLength > 0 ) {
|
|
var patternDataOffset = 0;
|
|
for( var note = 0; note < numNotes; note++ ) {
|
|
var flags = ibxmData.uByte( dataOffset );
|
|
if( ( flags & 0x80 ) == 0 ) flags = 0x1F; else dataOffset++;
|
|
var key = ( flags & 0x01 ) > 0 ? ibxmData.sByte( dataOffset++ ) : 0;
|
|
pattern.data[ patternDataOffset++ ] = key;
|
|
var ins = ( flags & 0x02 ) > 0 ? ibxmData.sByte( dataOffset++ ) : 0;
|
|
pattern.data[ patternDataOffset++ ] = ins;
|
|
var vol = ( flags & 0x04 ) > 0 ? ibxmData.sByte( dataOffset++ ) : 0;
|
|
pattern.data[ patternDataOffset++ ] = vol;
|
|
var fxc = ( flags & 0x08 ) > 0 ? ibxmData.sByte( dataOffset++ ) : 0;
|
|
var fxp = ( flags & 0x10 ) > 0 ? ibxmData.sByte( dataOffset++ ) : 0;
|
|
if( fxc >= 0x40 ) fxc = fxp = 0;
|
|
pattern.data[ patternDataOffset++ ] = fxc;
|
|
pattern.data[ patternDataOffset++ ] = fxp;
|
|
}
|
|
}
|
|
dataOffset = nextOffset;
|
|
}
|
|
this.instruments = new Array( this.numInstruments + 1 );
|
|
this.instruments[ 0 ] = new IBXMInstrument();
|
|
for( var insIdx = 1; insIdx <= this.numInstruments; insIdx++ ) {
|
|
var instrument = this.instruments[ insIdx ] = new IBXMInstrument();
|
|
instrument.name = ibxmData.strLatin1( dataOffset + 4, 22 );
|
|
var numSamples = ibxmData.uleShort( dataOffset + 27 );
|
|
if( numSamples > 0 ) {
|
|
instrument.numSamples = numSamples;
|
|
instrument.samples = new Array( numSamples );
|
|
for( var keyIdx = 0; keyIdx < 96; keyIdx++ )
|
|
instrument.keyToSample[ keyIdx + 1 ] = ibxmData.uByte( dataOffset + 33 + keyIdx );
|
|
var volEnv = instrument.volumeEnvelope = new IBXMEnvelope();
|
|
volEnv.pointsTick = new Int32Array( 16 );
|
|
volEnv.pointsAmpl = new Int32Array( 16 );
|
|
var pointTick = 0;
|
|
for( var point = 0; point < 12; point++ ) {
|
|
var pointOffset = dataOffset + 129 + ( point * 4 );
|
|
pointTick = ( deltaEnv ? pointTick : 0 ) + ibxmData.uleShort( pointOffset );
|
|
volEnv.pointsTick[ point ] = pointTick;
|
|
volEnv.pointsAmpl[ point ] = ibxmData.uleShort( pointOffset + 2 );
|
|
}
|
|
var panEnv = instrument.panningEnvelope = new IBXMEnvelope();
|
|
panEnv.pointsTick = new Int32Array( 16 );
|
|
panEnv.pointsAmpl = new Int32Array( 16 );
|
|
pointTick = 0;
|
|
for( var point = 0; point < 12; point++ ) {
|
|
var pointOffset = dataOffset + 177 + ( point * 4 );
|
|
pointTick = ( deltaEnv ? pointTick : 0 ) + ibxmData.uleShort( pointOffset );
|
|
panEnv.pointsTick[ point ] = pointTick;
|
|
panEnv.pointsAmpl[ point ] = ibxmData.uleShort( pointOffset + 2 );
|
|
}
|
|
volEnv.numPoints = ibxmData.uByte( dataOffset + 225 );
|
|
if( volEnv.numPoints > 12 ) volEnv.numPoints = 0;
|
|
panEnv.numPoints = ibxmData.uByte( dataOffset + 226 );
|
|
if( panEnv.numPoints > 12 ) panEnv.numPoints = 0;
|
|
volEnv.sustainTick = volEnv.pointsTick[ ibxmData.uByte( dataOffset + 227 ) & 0xF ];
|
|
volEnv.loopStartTick = volEnv.pointsTick[ ibxmData.uByte( dataOffset + 228 ) & 0xF ];
|
|
volEnv.loopEndTick = volEnv.pointsTick[ ibxmData.uByte( dataOffset + 229 ) & 0xF ];
|
|
panEnv.sustainTick = panEnv.pointsTick[ ibxmData.uByte( dataOffset + 230 ) & 0xF ];
|
|
panEnv.loopStartTick = panEnv.pointsTick[ ibxmData.uByte( dataOffset + 231 ) & 0xF ];
|
|
panEnv.loopEndTick = panEnv.pointsTick[ ibxmData.uByte( dataOffset + 232 ) & 0xF ];
|
|
volEnv.enabled = volEnv.numPoints > 0 && ( ibxmData.uByte( dataOffset + 233 ) & 0x1 ) > 0;
|
|
volEnv.sustain = ( ibxmData.uByte( dataOffset + 233 ) & 0x2 ) > 0;
|
|
volEnv.looped = ( ibxmData.uByte( dataOffset + 233 ) & 0x4 ) > 0;
|
|
panEnv.enabled = panEnv.numPoints > 0 && ( ibxmData.uByte( dataOffset + 234 ) & 0x1 ) > 0;
|
|
panEnv.sustain = ( ibxmData.uByte( dataOffset + 234 ) & 0x2 ) > 0;
|
|
panEnv.looped = ( ibxmData.uByte( dataOffset + 234 ) & 0x4 ) > 0;
|
|
instrument.vibratoType = ibxmData.uByte( dataOffset + 235 );
|
|
instrument.vibratoSweep = ibxmData.uByte( dataOffset + 236 );
|
|
instrument.vibratoDepth = ibxmData.uByte( dataOffset + 237 );
|
|
instrument.vibratoRate = ibxmData.uByte( dataOffset + 238 );
|
|
instrument.volumeFadeOut = ibxmData.uleShort( dataOffset + 239 );
|
|
}
|
|
dataOffset += ibxmData.uleInt( dataOffset );
|
|
var sampleHeaderOffset = dataOffset;
|
|
dataOffset += numSamples * 40;
|
|
for( var samIdx = 0; samIdx < numSamples; samIdx++ ) {
|
|
var sample = instrument.samples[ samIdx ] = new IBXMSample();
|
|
var sampleDataBytes = ibxmData.uleInt( sampleHeaderOffset );
|
|
var sampleLoopStart = ibxmData.uleInt( sampleHeaderOffset + 4 );
|
|
var sampleLoopLength = ibxmData.uleInt( sampleHeaderOffset + 8 );
|
|
sample.volume = ibxmData.sByte( sampleHeaderOffset + 12 );
|
|
sample.fineTune = ibxmData.sByte( sampleHeaderOffset + 13 );
|
|
var looped = ( ibxmData.uByte( sampleHeaderOffset + 14 ) & 0x3 ) > 0;
|
|
var pingPong = ( ibxmData.uByte( sampleHeaderOffset + 14 ) & 0x2 ) > 0;
|
|
var sixteenBit = ( ibxmData.uByte( sampleHeaderOffset + 14 ) & 0x10 ) > 0;
|
|
sample.panning = ibxmData.uByte( sampleHeaderOffset + 15 );
|
|
sample.relNote = ibxmData.sByte( sampleHeaderOffset + 16 );
|
|
sample.name = ibxmData.strLatin1( sampleHeaderOffset + 18, 22 );
|
|
sampleHeaderOffset += 40;
|
|
if( !looped || ( sampleLoopStart + sampleLoopLength ) > sampleDataBytes ) {
|
|
sampleLoopStart = sampleDataBytes;
|
|
sampleLoopLength = 0;
|
|
}
|
|
if( sixteenBit ) {
|
|
sample.setSampleData( ibxmData.samS16D( dataOffset, sampleDataBytes >> 1 ), sampleLoopStart >> 1, sampleLoopLength >> 1, pingPong );
|
|
} else {
|
|
sample.setSampleData( ibxmData.samS8D( dataOffset, sampleDataBytes ), sampleLoopStart, sampleLoopLength, pingPong );
|
|
}
|
|
dataOffset += sampleDataBytes;
|
|
}
|
|
}
|
|
}
|
|
this.loadS3M = function( ibxmData ) {
|
|
this.songName = ibxmData.strLatin1( 0, 28 );
|
|
this.sequenceLength = ibxmData.uleShort( 32 );
|
|
this.numInstruments = ibxmData.uleShort( 34 );
|
|
this.numPatterns = ibxmData.uleShort( 36 );
|
|
var flags = ibxmData.uleShort( 38 );
|
|
var version = ibxmData.uleShort( 40 );
|
|
this.fastVolSlides = ( ( flags & 0x40 ) == 0x40 ) || version == 0x1300;
|
|
var signedSamples = ibxmData.uleShort( 42 ) == 1;
|
|
if( ibxmData.uleInt( 44 ) != 0x4d524353 ) throw "Not an S3M file!";
|
|
this.defaultGVol = ibxmData.uByte( 48 );
|
|
this.defaultSpeed = ibxmData.uByte( 49 );
|
|
this.defaultTempo = ibxmData.uByte( 50 );
|
|
this.c2Rate = 8363;
|
|
this.gain = ibxmData.uByte( 51 ) & 0x7F;
|
|
var stereoMode = ( ibxmData.uByte( 51 ) & 0x80 ) == 0x80;
|
|
var defaultPan = ibxmData.uByte( 53 ) == 0xFC;
|
|
var channelMap = new Int32Array( 32 );
|
|
this.numChannels = 0;
|
|
for( var chanIdx = 0; chanIdx < 32; chanIdx++ ) {
|
|
channelMap[ chanIdx ] = -1;
|
|
if( ibxmData.uByte( 64 + chanIdx ) < 16 )
|
|
channelMap[ chanIdx ] = this.numChannels++;
|
|
}
|
|
this.sequence = new Int32Array( this.sequenceLength );
|
|
for( var seqIdx = 0; seqIdx < this.sequenceLength; seqIdx++ )
|
|
this.sequence[ seqIdx ] = ibxmData.uByte( 96 + seqIdx );
|
|
var moduleDataIdx = 96 + this.sequenceLength;
|
|
this.instruments = new Array( this.numInstruments + 1 );
|
|
this.instruments[ 0 ] = new IBXMInstrument();
|
|
for( var instIdx = 1; instIdx <= this.numInstruments; instIdx++ ) {
|
|
var instrument = this.instruments[ instIdx ] = new IBXMInstrument();
|
|
var sample = instrument.samples[ 0 ];
|
|
var instOffset = ibxmData.uleShort( moduleDataIdx ) << 4;
|
|
moduleDataIdx += 2;
|
|
instrument.name = ibxmData.strLatin1( instOffset + 48, 28 );
|
|
if( ibxmData.uByte( instOffset ) != 1 ) continue;
|
|
if( ibxmData.uleShort( instOffset + 76 ) != 0x4353 ) continue;
|
|
var sampleOffset = ibxmData.uByte( instOffset + 13 ) << 20;
|
|
sampleOffset += ibxmData.uleShort( instOffset + 14 ) << 4;
|
|
var sampleLength = ibxmData.uleInt( instOffset + 16 );
|
|
var loopStart = ibxmData.uleInt( instOffset + 20 );
|
|
var loopLength = ibxmData.uleInt( instOffset + 24 ) - loopStart;
|
|
sample.volume = ibxmData.uByte( instOffset + 28 );
|
|
sample.panning = -1;
|
|
var packed = ibxmData.uByte( instOffset + 30 ) != 0;
|
|
var loopOn = ( ibxmData.uByte( instOffset + 31 ) & 0x1 ) == 0x1;
|
|
if( loopStart + loopLength > sampleLength )
|
|
loopLength = sampleLength - loopStart;
|
|
if( loopLength < 1 || !loopOn ) {
|
|
loopStart = sampleLength;
|
|
loopLength = 0;
|
|
}
|
|
var stereo = ( ibxmData.uByte( instOffset + 31 ) & 0x2 ) == 0x2;
|
|
var sixteenBit = ( ibxmData.uByte( instOffset + 31 ) & 0x4 ) == 0x4;
|
|
if( packed ) throw "Packed samples not supported!";
|
|
var c2Rate = ibxmData.uleInt( instOffset + 32 );
|
|
var tune = ( Math.log( c2Rate ) - Math.log( this.c2Rate ) ) * 12 / Math.log( 2 );
|
|
sample.relNote = Math.round( tune );
|
|
sample.fineTune = Math.round( ( tune - sample.relNote ) * 128 );
|
|
if( sixteenBit ) {
|
|
if( signedSamples ) {
|
|
sample.setSampleData( ibxmData.samS16( sampleOffset, sampleLength ), loopStart, loopLength, false );
|
|
} else {
|
|
sample.setSampleData( ibxmData.samU16( sampleOffset, sampleLength ), loopStart, loopLength, false );
|
|
}
|
|
} else {
|
|
if( signedSamples ) {
|
|
sample.setSampleData( ibxmData.samS8( sampleOffset, sampleLength ), loopStart, loopLength, false );
|
|
} else {
|
|
sample.setSampleData( ibxmData.samU8( sampleOffset, sampleLength ), loopStart, loopLength, false );
|
|
}
|
|
}
|
|
}
|
|
this.patterns = new Array( this.numPatterns );
|
|
for( var patIdx = 0; patIdx < this.numPatterns; patIdx++ ) {
|
|
var pattern = this.patterns[ patIdx ] = new IBXMPattern( this.numChannels, 64 );
|
|
var inOffset = ( ibxmData.uleShort( moduleDataIdx ) << 4 ) + 2;
|
|
var rowIdx = 0;
|
|
while( rowIdx < 64 ) {
|
|
var token = ibxmData.uByte( inOffset++ );
|
|
if( token == 0 ) {
|
|
rowIdx++;
|
|
continue;
|
|
}
|
|
var noteKey = 0;
|
|
var noteIns = 0;
|
|
if( ( token & 0x20 ) == 0x20 ) { /* Key + Instrument.*/
|
|
noteKey = ibxmData.uByte( inOffset++ );
|
|
noteIns = ibxmData.uByte( inOffset++ );
|
|
if( noteKey < 0xFE )
|
|
noteKey = ( noteKey >> 4 ) * 12 + ( noteKey & 0xF ) + 1;
|
|
if( noteKey == 0xFF ) noteKey = 0;
|
|
}
|
|
var noteVol = 0;
|
|
if( ( token & 0x40 ) == 0x40 ) { /* Volume Column.*/
|
|
noteVol = ( ibxmData.uByte( inOffset++ ) & 0x7F ) + 0x10;
|
|
if( noteVol > 0x50 ) noteVol = 0;
|
|
}
|
|
var noteEffect = 0;
|
|
var noteParam = 0;
|
|
if( ( token & 0x80 ) == 0x80 ) { /* Effect + Param.*/
|
|
noteEffect = ibxmData.uByte( inOffset++ );
|
|
noteParam = ibxmData.uByte( inOffset++ );
|
|
if( noteEffect < 1 || noteEffect >= 0x40 )
|
|
noteEffect = noteParam = 0;
|
|
if( noteEffect > 0 ) noteEffect += 0x80;
|
|
}
|
|
var chanIdx = channelMap[ token & 0x1F ];
|
|
if( chanIdx >= 0 ) {
|
|
var noteOffset = ( rowIdx * this.numChannels + chanIdx ) * 5;
|
|
pattern.data[ noteOffset ] = noteKey;
|
|
pattern.data[ noteOffset + 1 ] = noteIns;
|
|
pattern.data[ noteOffset + 2 ] = noteVol;
|
|
pattern.data[ noteOffset + 3 ] = noteEffect;
|
|
pattern.data[ noteOffset + 4 ] = noteParam;
|
|
}
|
|
}
|
|
moduleDataIdx += 2;
|
|
}
|
|
this.defaultPanning = new Int32Array( this.numChannels );
|
|
for( var chanIdx = 0; chanIdx < 32; chanIdx++ ) {
|
|
if( channelMap[ chanIdx ] < 0 ) continue;
|
|
var panning = 7;
|
|
if( stereoMode ) {
|
|
panning = 12;
|
|
if( ibxmData.uByte( 64 + chanIdx ) < 8 ) panning = 3;
|
|
}
|
|
if( defaultPan ) {
|
|
var panFlags = ibxmData.uByte( moduleDataIdx + chanIdx );
|
|
if( ( panFlags & 0x20 ) == 0x20 ) panning = panFlags & 0xF;
|
|
}
|
|
this.defaultPanning[ channelMap[ chanIdx ] ] = panning * 17;
|
|
}
|
|
}
|
|
this.loadMod = function( ibxmData ) {
|
|
this.songName = ibxmData.strLatin1( 0, 20 );
|
|
this.sequenceLength = ibxmData.uByte( 950 ) & 0x7F;
|
|
this.restartPos = ibxmData.uByte( 951 ) & 0x7F;
|
|
if( this.restartPos >= this.sequenceLength ) this.restartPos = 0;
|
|
this.sequence = new Int32Array( 128 );
|
|
for( var seqIdx = 0; seqIdx < 128; seqIdx++ ) {
|
|
var patIdx = ibxmData.uByte( 952 + seqIdx ) & 0x7F;
|
|
this.sequence[ seqIdx ] = patIdx;
|
|
if( patIdx >= this.numPatterns ) this.numPatterns = patIdx + 1;
|
|
}
|
|
switch( ibxmData.ubeShort( 1082 ) ) {
|
|
case 0x4b2e: /* M.K. */
|
|
case 0x4b21: /* M!K! */
|
|
case 0x5434: /* FLT4 */
|
|
this.numChannels = 4;
|
|
this.c2Rate = 8287; /* PAL */
|
|
this.gain = 64;
|
|
break;
|
|
case 0x484e: /* xCHN */
|
|
this.numChannels = ibxmData.uByte( 1080 ) - 48;
|
|
this.c2Rate = 8363; /* NTSC */
|
|
this.gain = 32;
|
|
break;
|
|
case 0x4348: /* xxCH */
|
|
this.numChannels = ( ibxmData.uByte( 1080 ) - 48 ) * 10;
|
|
this.numChannels += ibxmData.uByte( 1081 ) - 48;
|
|
this.c2Rate = 8363; /* NTSC */
|
|
this.gain = 32;
|
|
break;
|
|
default:
|
|
throw "MOD Format not recognised!";
|
|
}
|
|
this.defaultGVol = 64;
|
|
this.defaultSpeed = 6;
|
|
this.defaultTempo = 125;
|
|
this.linearPeriods = false;
|
|
this.fastVolSlides = false;
|
|
this.defaultPanning = new Int32Array( this.numChannels );
|
|
for( var idx = 0; idx < this.numChannels; idx++ ) {
|
|
this.defaultPanning[ idx ] = 51;
|
|
if( ( idx & 3 ) == 1 || ( idx & 3 ) == 2 )
|
|
this.defaultPanning[ idx ] = 204;
|
|
}
|
|
var moduleDataIdx = 1084;
|
|
this.patterns = new Array( this.numPatterns );
|
|
for( var patIdx = 0; patIdx < this.numPatterns; patIdx++ ) {
|
|
var pattern = this.patterns[ patIdx ] = new IBXMPattern( this.numChannels, 64 );
|
|
for( var patDataIdx = 0; patDataIdx < pattern.data.length; patDataIdx += 5 ) {
|
|
var period = ( ibxmData.uByte( moduleDataIdx ) & 0xF ) << 8;
|
|
period = ( period | ibxmData.uByte( moduleDataIdx + 1 ) ) * 4;
|
|
if( period >= 112 && period <= 6848 ) {
|
|
var key = Math.round( -12 * Math.log( period / 29021 ) / Math.log( 2 ) );
|
|
pattern.data[ patDataIdx ] = key;
|
|
}
|
|
var ins = ( ibxmData.uByte( moduleDataIdx + 2 ) & 0xF0 ) >> 4;
|
|
ins = ins | ibxmData.uByte( moduleDataIdx ) & 0x10;
|
|
pattern.data[ patDataIdx + 1 ] = ins;
|
|
var effect = ibxmData.uByte( moduleDataIdx + 2 ) & 0x0F;
|
|
var param = ibxmData.uByte( moduleDataIdx + 3 );
|
|
if( param == 0 && ( effect < 3 || effect == 0xA ) ) effect = 0;
|
|
if( param == 0 && ( effect == 5 || effect == 6 ) ) effect -= 2;
|
|
if( effect == 8 ) {
|
|
if( this.numChannels == 4 ) {
|
|
effect = param = 0;
|
|
} else if( param > 128 ) {
|
|
param = 128;
|
|
} else {
|
|
param = ( param * 255 ) >> 7;
|
|
}
|
|
}
|
|
pattern.data[ patDataIdx + 3 ] = effect;
|
|
pattern.data[ patDataIdx + 4 ] = param;
|
|
moduleDataIdx += 4;
|
|
}
|
|
}
|
|
this.numInstruments = 31;
|
|
this.instruments = new Array( this.numInstruments + 1 );
|
|
this.instruments[ 0 ] = new IBXMInstrument();
|
|
for( var instIdx = 1; instIdx <= this.numInstruments; instIdx++ ) {
|
|
var instrument = this.instruments[ instIdx ] = new IBXMInstrument();
|
|
var sample = instrument.samples[ 0 ];
|
|
instrument.name = ibxmData.strLatin1( instIdx * 30 - 10, 22 );
|
|
var sampleLength = ibxmData.ubeShort( instIdx * 30 + 12 ) * 2;
|
|
var fineTune = ( ibxmData.uByte( instIdx * 30 + 14 ) & 0xF ) << 4;
|
|
sample.fineTune = ( fineTune < 128 ) ? fineTune : fineTune - 256;
|
|
var volume = ibxmData.uByte( instIdx * 30 + 15 ) & 0x7F;
|
|
sample.volume = ( volume <= 64 ) ? volume : 64;
|
|
sample.panning = -1;
|
|
var loopStart = ibxmData.ubeShort( instIdx * 30 + 16 ) * 2;
|
|
var loopLength = ibxmData.ubeShort( instIdx * 30 + 18 ) * 2;
|
|
if( loopStart + loopLength > sampleLength ) {
|
|
if( loopStart / 2 + loopLength <= sampleLength ) {
|
|
/* Some old modules have loop start in bytes. */
|
|
loopStart = loopStart / 2;
|
|
} else {
|
|
loopLength = sampleLength - loopStart;
|
|
}
|
|
}
|
|
if( loopLength < 4 ) {
|
|
loopStart = sampleLength;
|
|
loopLength = 0;
|
|
}
|
|
sample.setSampleData( ibxmData.samS8( moduleDataIdx, sampleLength ), loopStart, loopLength, false );
|
|
moduleDataIdx += sampleLength;
|
|
}
|
|
}
|
|
if( moduleData ) {
|
|
var ibxmData = new IBXMData( moduleData );
|
|
if( ibxmData.strLatin1( 0, 17 ) == "Extended Module: " ) {
|
|
this.loadXM( ibxmData );
|
|
} else if( ibxmData.strLatin1( 44, 4 ) == "SCRM" ) {
|
|
this.loadS3M( ibxmData );
|
|
} else {
|
|
this.loadMod( ibxmData );
|
|
}
|
|
}
|
|
}
|