1 /**
  2  * This file is part of the Web Enabled Audio and Sound Enhancement Library (aka the Weasel audio library) Copyright 2011 - 2013 Warren Willmey. It is covered by the GNU General Public License version 3 as published by the Free Software Foundation, you should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
  3  */
  4 
  5 if( undefined == window.weasel ) window.weasel = {};
  6 
  7 // ---------------------------------------------------------------------------
  8 /** Create a DOC Soundtracker 9 module out of the provided data (which has already passed the module sniffer test).
  9  * DOC Soundtracker 9 is based upon Ultimate Soundtracker 1.8 with some additional effect commands. 
 10  * 
 11  * @constructor
 12  * @extends weasel.UltimateSoundTracker18
 13  * 
 14  * @param {Array|Uint8Array} aModuleData = The DOC Soundtracker 9 module as a byte array that MUST have passed the module sniffer test.
 15  * @param {int} iPlaybackFrequency = The playback frequency in hertz to use (e.g. 44100 ).
 16  * @param {weasel.Sample.prototype.SampleScannerMode} iSampleScannerMode = Scan for IFF Header corruption residue?.
 17  * 
 18  * @author Warren Willmey 2012
 19  */
 20 weasel.DOCSoundTracker9 = function( aModuleData, iPlaybackFrequency, iSampleScannerMode )
 21 {
 22 	this.parent = weasel.UltimateSoundTracker18;
 23 	
 24 	// Needed for prototype Inheritance.
 25 	//
 26 	if( aModuleData === undefined || !(( aModuleData instanceof Array ) || ( window.Uint8Array && aModuleData instanceof Uint8Array )) )
 27 		return;
 28 
 29 	this.parent( aModuleData, iPlaybackFrequency, iSampleScannerMode );
 30 	this.sModuleType = weasel.ModuleSniffer.prototype.SupportedModules.DOCSoundTracker9;
 31 
 32 	// Just to add some confusion to the mix Master Soundtracker 1.0 is identical to DOC Soundtracker 9
 33 	// but for some reason they removed the ability to adjust the BPM Speed from the editor
 34 	// and force the module to VBL speed whilst marking the file as 120BPM.
 35 	// However TIPs replay routine differs from the Editors and is identical to 
 36 	// Unknowns which honors the BPM speed just like Ultimate Soundtracker 1.8.
 37 	// SO we have a situation just like Ultimate Soundtracker 1.21 & 1.8...
 38 	// Solution, make all 120BPM speeds run at VBL 50hz. This can lead to DOC 9 modules
 39 	// playing back to fast (if they genuinely where meant for 120BPM).
 40 	// In which case you will have to force override yourself.
 41 	//
 42 	if( 120 == this.getSongSpeed() )
 43 	{
 44 		this.timingOverride( this.TimingOverrides.PAL );
 45 	}
 46 
 47 
 48 	// Due to the Tick Speed command it is not so simple to:
 49 	// a) Work out the (time) length of the song.
 50 	// b) Work out the current (time) position.
 51 	//
 52 	this.iSongLengthInTicks = 0;
 53 	this.aSequenceTableAccumulatedTicks = new Array( this.getSongLengthInPatterns() );
 54 	this._computeSequenceTableTicks();
 55 };
 56 
 57 weasel.DOCSoundTracker9.prototype = new weasel.UltimateSoundTracker18;
 58 
 59 // ---------------------------------------------------------------------------
 60 /** Scan all used patterns in sequence order to work out the length of the song
 61  * in ticks, recording each of the sequences starting tick.
 62  * 
 63  * @protected
 64  */
 65 weasel.DOCSoundTracker9.prototype._computeSequenceTableTicks = function( )
 66 {
 67 	for( var iLength = this.aSequenceTableAccumulatedTicks.length; --iLength >= 0; )
 68 	{
 69 		this.aSequenceTableAccumulatedTicks[ iLength ] = 0;
 70 	}
 71 
 72 	var iTickSpeed = weasel.FormatUltimateSoundTracker121.TicksPerRow;
 73 	var iTickTotal = 0;
 74 
 75 
 76 	for( var iSongSequenceLength = this.aSequenceTableAccumulatedTicks.length, iSongPosition = 0; iSongPosition < iSongSequenceLength; iSongPosition++ )
 77 	{
 78 		// Record the Total Ticks needed to get to the beginning of each pattern sequence.
 79 		//
 80 		this.aSequenceTableAccumulatedTicks[ iSongPosition ] = iTickTotal;
 81 
 82 		var iPatternNumber = this.getPatternNumber( iSongPosition );
 83 		var oPattern = this.getPattern( iPatternNumber );
 84 
 85 		for( var iRowsPerPattern = weasel.FormatUltimateSoundTracker121.NumberOfRowsPerPattern, iRow = 0; iRow < iRowsPerPattern; iRow++ )
 86 		{
 87 			// Order of channels important, there may be more than one Tick Speed effect in a single row.
 88 			//
 89 			for( var iChannels = this.aSoundChannels.length, iChannel = -1; ++iChannel < iChannels; )
 90 			{
 91 				var oColumn = oPattern.getColumn( iChannel );
 92 				var oPatternCell = oColumn.getCell( iRow );
 93 
 94 				if( weasel.FormatDOCSoundTracker9.Effects.TickSpeed == oPatternCell.getEffectNumber() )
 95 				{
 96 					// Tick Speed has changed.
 97 					//
 98 					var iWantedTickSpeed = oPatternCell.getEffectParameter() & 0xf;
 99 
100 					if( iWantedTickSpeed < 2 )
101 						iWantedTickSpeed = 2;
102 
103 					iTickSpeed = iWantedTickSpeed;
104 				}
105 			}
106 
107 			iTickTotal += iTickSpeed;
108 		}
109 	}
110 
111 	this.iSongLengthInTicks = iTickTotal;
112 };
113 
114 // ---------------------------------------------------------------------------
115 /**
116  * Get length of song in milliseconds.
117  * 
118  * @return {float} = The length of the song in ms.
119  * 
120  * @override
121  */
122 weasel.DOCSoundTracker9.prototype.getLengthOfSongInMilliSeconds = function( )
123 {
124 	return this.iSongLengthInTicks * ( 1000.0 / this.tickPlaybackRateInHz());
125 
126 };
127 
128 // ---------------------------------------------------------------------------
129 /**
130  * Get song position in milliseconds.
131  * 
132  * @return {float} = The song position from its beginning in ms.
133  * 
134  * @override
135  */
136 weasel.DOCSoundTracker9.prototype.getSongPositionInMilliSeconds = function( )
137 {
138 	return (this.aSequenceTableAccumulatedTicks[ this.getCurrentSequencePosition() ] + this.iTotalPatternTicks) * (1000.0 / this.tickPlaybackRateInHz());
139 };
140 
141 // ---------------------------------------------------------------------------
142 /** Set the row tick speed.
143  * 
144  * @param {int} iTickSpeed = The new row tick speed to use (2-15).
145  */
146 weasel.DOCSoundTracker9.prototype.setTickSpeed = function( iTickSpeed )
147 {
148 	this.iTickSpeed = iTickSpeed < 2 ? 2 : iTickSpeed > 15 ? 15 : iTickSpeed;
149 };
150 
151 
152 // ---------------------------------------------------------------------------
153 /** During pitch bends DOC Soundtracker 9 clamps the values to the 3 octave range.
154  * 
155  * @param {int} iNotePeriod = The note period to clamp.
156  * 
157  * @return {int} The clamped period value.
158  * 
159  * @private
160  */
161 weasel.DOCSoundTracker9.prototype._clampNotePeriod = function( iNotePeriod )
162 {
163 	if( iNotePeriod > weasel.FormatDOCSoundTracker9.MinNotePeriod )
164 	{
165 		return weasel.FormatDOCSoundTracker9.MinNotePeriod;
166 	}
167 	else if( iNotePeriod < weasel.FormatDOCSoundTracker9.MaxNotePeriod )
168 	{
169 		return weasel.FormatDOCSoundTracker9.MaxNotePeriod;
170 	}
171 
172 	return iNotePeriod;
173 };
174 
175 // ---------------------------------------------------------------------------
176 /** apply volume to immediately, this is because DOC soundtracker 9 applies 
177  * the new volume level of the pending instrument whether a note is present or not,
178  * allowing you to change the volume of the sample without actually changing the
179  * instrument or note period. The instrument volume is overruled by the Volume
180  * Effect Command.
181  * 
182  * @param {weasel.Channel} oChannel = The channel object to apply volume to immediately.
183  * 
184  * @protected
185  * @override
186  */
187 weasel.DOCSoundTracker9.prototype._applyVolumeImmediately = function( oChannel )
188 {
189 	// Volume change is applied immediately even if there is no new note.
190 	//
191 	var iVolume = this.getInstrument( oChannel.getPendingInstrumentNumber() ).getVolume();
192 	
193 	// Check volume column effect override.
194 	//
195 	if( weasel.FormatDOCSoundTracker9.Effects.Volume == oChannel.getEffectNumber() )
196 	{
197 		iVolume = oChannel.getEffectParameter();
198 	}
199 	
200 	oChannel.setVolume( iVolume );
201 
202 };
203 
204 // ---------------------------------------------------------------------------
205 /** Process a channel's effects.
206  * 
207  * @param {weasel.Channel} oChannel = The Channel to process for effects.
208  * 
209  * @protected
210  * @override
211  */
212 weasel.DOCSoundTracker9.prototype._processChannelEffect = function( oChannel )
213 {
214 	switch( oChannel.getEffectNumber() )
215 	{
216 		case weasel.FormatDOCSoundTracker9.Effects.Arpeggio :
217 
218 				if( oChannel.getEffectParameter() == 0 )
219 					break;
220 
221 				oChannel.arpeggio( this.iCurrentTick, weasel.Channel.prototype.ArpeggioMode.UltimateSoundtracker );
222 
223 			break;
224 
225 		case weasel.FormatDOCSoundTracker9.Effects.PitchbendUp :
226 
227 				oChannel.pitchBend( this.iCurrentTick, 0, oChannel.getEffectParameter() );
228 				oChannel.setShadowNotePeriod( this._clampNotePeriod( oChannel.getShadowNotePeriod() ) );
229 				oChannel.setNotePeriod( oChannel.getShadowNotePeriod() );
230 
231 			break;
232 
233 		case weasel.FormatDOCSoundTracker9.Effects.PitchbendDown :
234 
235 				oChannel.pitchBend( this.iCurrentTick, oChannel.getEffectParameter(), 0 );
236 				oChannel.setShadowNotePeriod( this._clampNotePeriod( oChannel.getShadowNotePeriod() ) );
237 				oChannel.setNotePeriod( oChannel.getShadowNotePeriod() );
238 
239 			break;
240 
241 		default :
242 			break;
243 	}
244 };
245 
246 
247 
248 // ---------------------------------------------------------------------------
249 /** Process a channel's effects that occur on Tick Zero.
250  * 
251  * @param {weasel.Channel} oChannel = The Channel to process for effects.
252  * 
253  * @protected
254  * @override
255  */
256 weasel.DOCSoundTracker9.prototype._processChannelTick0Effect = function( oChannel )
257 {
258 	switch( oChannel.getEffectNumber() )
259 	{
260 	
261 		case weasel.FormatDOCSoundTracker9.Effects.Volume :
262 
263 				var iVolume = oChannel.getEffectParameter();
264 
265 				if( iVolume > 64 )
266 				{
267 					iVolume = 64;
268 				}
269 
270 				oChannel.setVolume( iVolume );
271 
272 			break;
273 		case weasel.FormatDOCSoundTracker9.Effects.Filter :
274 
275 			// The original Amiga (the Amiga 1000) does not have a filter, the bit
276 			// that turns the filter on and off also controls the brightness of the Power LED.
277 			// Which resulted in a few tunes being written just to blink the light in time to the music.
278 			// The filter is non-programmable and is a low pass filter which according to the
279 			// Amiga Hardware Reference Manual page 152 section "Low-pass filter" says it "becomes
280 			// active at around 4 Khz and gradually begins to attenuate (cut off) the signal. Generally
281 			// you cannot clearly hear frequencies higher than 7 Khz."
282 			//
283 			// With the introduced of the Amiga 500, the Filter is enabled by default (on reset/power up)
284 			// resulting all software written for the Amiga 1000 having its music muffled.
285 			//
286 			var iFilter = oChannel.getEffectParameter() & 1;
287 			
288 			this.bFilterOn = iFilter == 0;	// Filter On = 0 (Amiga Power LED On), Filter Off = 1 (Amiga Power LED Off).
289 			
290 			break;
291 
292 		case weasel.FormatDOCSoundTracker9.Effects.TickSpeed :
293 
294 				var iWantedTickSpeed = oChannel.getEffectParameter() & 0xf;
295 
296 				if( iWantedTickSpeed < 2 )
297 					iWantedTickSpeed = 2;
298 
299 				this.setTickSpeed( iWantedTickSpeed );
300 
301 			break;
302 
303 		default :
304 			break;
305 	}
306 };
307 // ---------------------------------------------------------------------------
308 /**
309  * Process the effects column for all channels when current tick is zero (a new row has been fetched).
310  * 
311  * @override
312  */
313 weasel.DOCSoundTracker9.prototype.processTick0Effects = function( )
314 {
315 	for( var iChannels = this.aSoundChannels.length, iChannel = -1; ++iChannel < iChannels; )
316 	{
317 		var oChannel = this.aSoundChannels[ iChannel ];
318 
319 		this._processChannelTick0Effect( oChannel );
320 	}
321 };