package micromod;
import micromod.resamplers.Resampler;
/**
A Channel attemps to do a bit of Amiga hardware emulation to play
ProTracker modules a bit better than most. In my experience, this implementation
can play some modules that even SurfSmurf can't manage. There are still one or
two tunes that won't sound entirely correct, but I believe this is one of the
better implementations.
The justification for the design of this class is that it provides a protracker
specific channel interface and allows the InstrumentPlayer,LoopDecoder,resampler
interfaces to be reused in other mod players.
In fact, InstrumentPlayer and LoopDecoder have been hacked to better emulate
ProTracker. This is an advantage of using different codebases for each module type.
*/
public class Channel {
protected static final int FIXED_POINT_SHIFT=16;
protected int period, volume, fineTune;
protected int amplitude, leftPan, rightPan, mixerStepPrecalc;
protected InstrumentPlayer instrumentPlayer;
/**
A log table for calculating fine tuned periods.
*/
protected int[] fineTuneTable = new int[] {
17358, 17233, 17109, 16986, 16864, 16743, 16622, 16503,
16384, 16266, 16149, 16033, 15918, 15803, 15689, 15576 };
/**
Constructor
*/
public Channel( int samplingRate, boolean pal ) {
instrumentPlayer = new InstrumentPlayer();
configure(samplingRate, pal);
reset();
}
/**
Configure the Channel to use the specified sampling
rate and PAL/NTSC mode.
*/
public void configure( int samplingRate, boolean pal ) {
// Set up a precalculated pitch constant
int cpuClock = Synthesizer.CBM_AMIGA_NTSC_CLOCK;
if(pal) cpuClock = Synthesizer.CBM_AMIGA_PAL_CLOCK;
mixerStepPrecalc = ((cpuClock<<8)/samplingRate) << 4;
}
/**
Overrides reset() to reinitialise the default volume and period.
*/
public void reset() {
instrumentPlayer.assignInstrument(new Instrument());
instrumentPlayer.setAssigned();
// Set default centre panning.
amplitude = 0;
leftPan = 65536;
rightPan = 65536;
// Set channel properties
volume=0;
period=0;
fineTune=0;
}
/**
Trigger an instrument on the channel to play at the specified pitch.
Call this for every row, even if the row is empty, since it resets the
volume and pitch modulations done by eg) vibrato/tremolo
ProTracker behaviour is currently as follows:
Valid instrument, period > 0 : Plays from instrument start as normal.
Null instrument, period > 0 : Assigned instrument is triggered at period.
Valid instrument, period = 0 : Instrument is "assigned" but not triggered.
Valid instrument, period =-1 : Instrument is "switched over" without retrig.
Anything else is ignored. This behaviour will probably change after some
testing, since I know it's not exactly correct.
*/
public void trigger( Instrument instrument, int period ) {
// If the volume or pitch was being modulated, reset the volume and pitch.
setVolume( this.volume, false );
setPeriod( this.period, false );
if( instrument!=null ) {
// Assign the specified instrument
instrumentPlayer.assignInstrument(instrument);
setVolume(instrument.volume, true);
if( period==-1 ) {
// Tone porta switch
if( this.period!=0 ) {
switchInstrument(false);
setPeriod( this.period, false );
}
}
else if( period>0 ) {
// A normal trigger.
switchInstrument(true);
setPeriod( period, true );
}
}
else {
// Trigger assigned
if( period>0 ) {
switchInstrument(true);
setPeriod( period, true );
}
}
}
/**
Get the current period of the channel.Used so the Modulator can, er, modulate
*/
public int getPeriod() {
return period;
}
/**
Get the current ProTracker volume of the channel, from 0 to 64.
*/
public int getVolume() {
return volume;
}
/**
Set the period using the conventional ProTracker period notation.
@param permanent if false, the period change only affects one subsequent call to output()
*/
public void setPeriod( int period, boolean permanent ) {
if( period == 0 ) return;
if( period < 113 ) period = 113;
if( period > 856 ) period = 856;
calculateMixerStep( period, fineTune );
if(permanent) this.period = period;
}
/**
Set the volume from 0 to 64.
@param permanent if false, the period change only affects one subsequent call to output()
*/
public void setVolume( int volume, boolean permanent ) {
if(volume>64) volume=64;
if(volume<0 ) volume=0;
amplitude = volume << 10;
if(permanent) this.volume = volume;
}
/**
Set the current sample position
*/
public void setSamplePosition( int samplePosition ) {
instrumentPlayer.setSamplePosition( samplePosition );
}
/**
Set the finetune. The finetune of the channel is set automatically
when an instrument is assigned. This method is used for the (useless)
set finetune ProTracker command.
*/
public void setFineTune( int fineTune ) {
this.fineTune = fineTune;
calculateMixerStep( period, fineTune );
}
/**
Set the leftAmp and rightAmp members to specify the panning. leftAmp and rightAmp
are signed integers from -65536 to 65536 to specify the value a sample on either
channel should be multiplied by. If one of these is negative, a "surround" effect can be achieved.
*/
public void setPanning( int left, int right ) {
leftPan = left;
rightPan = right;
}
/**
@return the current left-speaker amplitude of the channel, in 16 bit fixed point format.
This value can be negative, and if it is, the audio should be inverted for a surround effect.
*/
public int getLeftAmplitude() {
return amplitude*(leftPan>>1) >> FIXED_POINT_SHIFT-1;
}
/**
@return the current right-speaker amplitude of the channel, in 16 bit fixed point format.
This value can be negative, and if it is, the audio should be inverted for a surround effect.
*/
public int getRightAmplitude() {
return amplitude*(rightPan>>1) >> FIXED_POINT_SHIFT-1;
}
/**
@return true if a call to getAudio() would result only in a blank buffer.
*/
public boolean isSilent() {
return instrumentPlayer.hasFinished();
}
/**
Get resampled audio into the specified buffers. Volume and panning are not applied. You must
scale the amplitude according to getLeftAmplitude() and getRightAmplitude().
@param snapBack Do not update the sample-pointers, so the same audio can be retrieved again.
*/
public void getAudio( short[] buffer, int length, Resampler resampler, boolean snapBack ) {
// Dont attempt to generate audio when no mixer step calculated.
if(period==0) return;
// Obtain the resampled audio
instrumentPlayer.getAudio( buffer, length, resampler, snapBack );
// Idea for XM/IT player - update instrument envelopes here.
}
/**
Play the assigned instrument.
@param trig If true, will play instrument from start.
*/
protected void switchInstrument( boolean trig ) {
instrumentPlayer.setAssigned();
fineTune = instrumentPlayer.getInstrument().fineTune;
if(trig) instrumentPlayer.setSamplePosition(0);
}
/**
Calculate the step and subStep values for the resampler, from the
specified period and fineTune value.
*/
protected void calculateMixerStep( int period, int fineTune ) {
if( period == 0 ) return;
int ftPeriod = (period*fineTuneTable[fineTune+8]) >> 7;
instrumentPlayer.setResampleFactor( ((mixerStepPrecalc<<6)/ftPeriod) << 4 );
}
}