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 };