diff --git a/README.md b/README.md index 0009cf1..fb5517c 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ Modplayer-js will fall back to the deprecated `ScriptProcessorNode` API in these - Amiga 4 channel Sountracker/Noisetracker mod files with 4 channels and 15-31 instruments - Stereo playback (channels 0 & 3 goes to the left chan, 1 & 2 to the right, just like on a real Amiga) - LowPass filter (not sure it sounds right) -- Left/Right Spectrum vizualizers -- Ability to mute any of the 4 module channels -- Includes 8 Module files from the Amiga-era +- Individual Spectrum vizualizers for each channel (AudioWorklet mode only) +- Ability to mute any of the 4 module channels (AudioWorklet only) +- 26 great modules of various styles and times are downloaded from [The Mod Archive](https://modarchive.org) -Most note effects should be supported, including extended ones. Only effect not implemented is `Vibrato (0x4 & 0x6)`. +Most note effects should be supported, including extended ones. # Dependencies @@ -42,19 +42,6 @@ ModPlayer JS makes use of the following piece of software: I also heavily used [MilkyTracker](https://milkytracker.titandemo.org/) and [webaudio-mod-player](https://mod.haxor.fi/) - which plays lot of module formats with high fidelity - to track down some timing bugs. -# Module files copyright - -ModPlayer-js includes the following module files: - - - agony.mod: music from Amiga game [Agony](https://www.youtube.com/watch?v=iRzIpghJeec) by [Tim Wright](https://twitter.com/CoLDSToRAGE) - - all_that_she_wants.mod: Ace of Base remix from 1993 by Crossair - - bigtime.mod: Björk Big Time Sensuality remix from 1994 by ISO from Axis group, appeared in the [Big Time Sensuality demo](https://www.youtube.com/watch?v=Y-2xUXk5F2w) - - cannonfodder.mod: music from [Cannon Fodder](https://www.youtube.com/watch?v=PiYuq6Ac3a0) Amiga game by [John Hare](https://twitter.com/johnhare) / [Richard Joseph](https://en.wikipedia.org/wiki/Richard_Joseph) - - desert_strike.mod: music from [Desert Strike](https://www.youtube.com/watch?v=hcJaph0D7UM) Amiga game by Jason Whitley - - LotusII.mod: music from [Lotus II](https://www.youtube.com/watch?v=vETonlaTZ4c) game by [Barry Leitch](https://en.wikipedia.org/wiki/Barry_Leitch) - - projectx.mod: music from [Project-X](https://www.youtube.com/watch?v=gjq-ONi3dZE) Amiga game by [Allister Brimble](https://twitter.com/allisterbrimble) - - silkworm.mod: music from [Silkworm](https://www.youtube.com/watch?v=4wNidIucUuc) Amiga game by [Barry Leitch](https://en.wikipedia.org/wiki/Barry_Leitch) - # Module background Modules are like MIDI files but with custom sound samples instead of builtin synth files. @@ -107,3 +94,15 @@ Then comes the patterns data: first the list of positions and then each patterns Last but not least, sample data is stored, uncompressed in LPCM 8bit format. More information can be found in the [original specs](https://github.com/cmatsuoka/tracker-history/blob/master/reference/amiga/soundtracker/Soundtracker_v1-v9/Soundtracker_v2.doc) file (which was written in 1988: ouch!). + +# Module files copyright + +No file is hosted in GitHub, all modules are downloaded from [The Mod Archive](https://modarchive.org) and are licensed under the [Mod Archive Distribution License](https://modarchive.org/index.php?terms-upload) + + - [agony.mod](https://api.modarchive.org/downloads.php?moduleid=124303#agony_intro.mod): music from Amiga game [Agony](https://www.youtube.com/watch?v=iRzIpghJeec) by [Tim Wright](https://twitter.com/CoLDSToRAGE) + - [bigtime.mod](https://api.modarchive.org/downloads.php?moduleid=98051#big_time_sensuality.mod): Björk Big Time Sensuality remix from 1994 by ISO from Axis group, appeared in the [Big Time Sensuality demo](https://www.youtube.com/watch?v=Y-2xUXk5F2w) + - [cannonfodder.mod](https://api.modarchive.org/downloads.php?moduleid=34568#CANNONFO.MOD): music from [Cannon Fodder](https://www.youtube.com/watch?v=PiYuq6Ac3a0) Amiga game by [John Hare](https://twitter.com/johnhare) / [Richard Joseph](https://en.wikipedia.org/wiki/Richard_Joseph) + - [desert_strike.mod](https://api.modarchive.org/downloads.php?moduleid=68835#desert_strike.mod): music from [Desert Strike](https://www.youtube.com/watch?v=hcJaph0D7UM) Amiga game by Jason Whitley + - [LotusII.mod](https://api.modarchive.org/downloads.php?moduleid=87180#lotus2-title.mod): music from [Lotus II](https://www.youtube.com/watch?v=vETonlaTZ4c) game by [Barry Leitch](https://en.wikipedia.org/wiki/Barry_Leitch) + - [projectx.mod](https://api.modarchive.org/downloads.php?moduleid=56660#projectx.mod): music from [Project-X](https://www.youtube.com/watch?v=gjq-ONi3dZE) Amiga game by [Allister Brimble](https://twitter.com/allisterbrimble) + - [silkworm.mod](https://api.modarchive.org/downloads.php?moduleid=83115#silkwormtitle.mod): music from [Silkworm](https://www.youtube.com/watch?v=4wNidIucUuc) Amiga game by [Barry Leitch](https://en.wikipedia.org/wiki/Barry_Leitch) \ No newline at end of file diff --git a/audio/LotusII.mod b/audio/LotusII.mod deleted file mode 100644 index 28470bf..0000000 Binary files a/audio/LotusII.mod and /dev/null differ diff --git a/audio/agony.mod b/audio/agony.mod deleted file mode 100644 index 46f4844..0000000 Binary files a/audio/agony.mod and /dev/null differ diff --git a/audio/all_that_she_wants.mod b/audio/all_that_she_wants.mod deleted file mode 100644 index 717cd08..0000000 Binary files a/audio/all_that_she_wants.mod and /dev/null differ diff --git a/audio/bigtime.mod b/audio/bigtime.mod deleted file mode 100644 index 66b6c85..0000000 Binary files a/audio/bigtime.mod and /dev/null differ diff --git a/audio/cannonfodder.mod b/audio/cannonfodder.mod deleted file mode 100644 index 53ea298..0000000 Binary files a/audio/cannonfodder.mod and /dev/null differ diff --git a/audio/desert_strike.mod b/audio/desert_strike.mod deleted file mode 100644 index 4961e5c..0000000 Binary files a/audio/desert_strike.mod and /dev/null differ diff --git a/audio/projectx.mod b/audio/projectx.mod deleted file mode 100644 index f33d2e8..0000000 Binary files a/audio/projectx.mod and /dev/null differ diff --git a/audio/silkworm.mod b/audio/silkworm.mod deleted file mode 100755 index 9ffa01b..0000000 Binary files a/audio/silkworm.mod and /dev/null differ diff --git a/index.html b/index.html index 8a08a14..4dd1a22 100644 --- a/index.html +++ b/index.html @@ -52,7 +52,7 @@ } .mdl-mini-footer--link-list li, .mdl-mini-footer__link-list li{ - margin-right: 32px; + margin: 0 16px; } .author{ @@ -61,6 +61,8 @@ canvas{ width:100%; + height:100px; + cursor:pointer; } @@ -125,35 +127,12 @@ .mdl-card__title{ position:relative; } - - .channel_control{ - position:absolute; - top:10px; - }

audiotrack

by 

-
- - - - -
diff --git a/js/main.js b/js/main.js index d051758..29424b6 100644 --- a/js/main.js +++ b/js/main.js @@ -1,12 +1,29 @@ var moduleList = [ - { file: 'agony.mod', author: 'Tim Wright' }, - { file: 'all_that_she_wants.mod', author: 'Crossair' }, - { file: 'bigtime.mod', author: 'ISO/Axis Group' }, - { file: 'cannonfodder.mod', author: 'John Hare' }, - { file: 'desert_strike.mod', author: 'Jason Whitley' }, - { file: 'LotusII.mod', author: 'Barry Leitch' }, - { file: 'projectx.mod', author: 'Allister Brimble' }, - { file: 'silkworm.mod', author: 'Barry Leitch' } + { file: 'https://api.modarchive.org/downloads.php?moduleid=91286#faggots_universe.mod', author: 'Deelite' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=168739#neurodancer_-_quasar.mod', author: 'Neurodancer' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=183672#tax_haven_dry_hump.mod', author: 'Curt Cool' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=182057#h0ffman_-_drop_the_panic.mod', author: 'h0ffman' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=168122#prodigy_-_downtown.mod', author: 'prodigy of oops' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=172266#hoffman_-_the_hunter.mod', author: 'h0ffman' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=65280#variations.mod', author: 'jogeir-liljedahl' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=166686#wiklund_-_bonfire.mod', author: 'Wiklund' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=55058#pinball_illusions.mod', author: 'Olof Gustafsson' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=167668#vinnie_-_sweet_dreams.mod', author: 'vinnie/spaceballs' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=172271#subi-king_of_boggle.mod', author: 'Subi/DESiRE' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=171416#bass-1107.mod', author: 'Noiseless (cm/ao)' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=168110#punnik_-_drum_bass.mod', author: 'punnik' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=171616#dan_-_childs_philozophy.mod', author: 'dan / picco' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=119303#boesendorfer_p_s_s.mod', author: 'romeoknight' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=170637#ghost_in_the_cli.mod', author: 'h0ffman' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=158057#alf_-_no-mercy.mod', author: 'alf/vtl' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=105709#trans_atlantic.mod', author: 'Lizardking'}, + { file: 'https://api.modarchive.org/downloads.php?moduleid=124303#agony_intro.mod', author: 'Tim Wright' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=98051#big_time_sensuality.mod', author: 'ISO/Axis Group' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=34568#CANNONFO.MOD', author: 'John Hare' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=68835#desert_strike.mod', author: 'Jason Whitley' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=87180#lotus2-title.mod', author: 'Barry Leitch' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=56660#projectx.mod', author: 'Allister Brimble' }, + { file: 'https://api.modarchive.org/downloads.php?moduleid=83115#silkwormtitle.mod', author: 'Barry Leitch' } ], selectedMod = 0, prefix = 'audio/', @@ -14,17 +31,16 @@ var moduleList = [ window.onload = function () { var canvas = document.getElementById('visualizer'), - effect = 1, - effects = [], - ctx = canvas.getContext('2d'), - canvasWidth = canvas.width, - canvasHeight = canvas.height, - channelsPlaying = [true, true, true, true]; + channelsPlaying = [true, true, true, true], + audioWorkletSupport = !!AudioWorkletNode.toString().match(/native code/); toast = new Toast('info-snackbar'); document.addEventListener('moduleLoaded', (event) => { - toast.show(`Module loaded: ${moduleList[selectedMod].file}`); + const split = moduleList[selectedMod].file.split('#'), + name = split.length > 1 && split[1] || split[0]; + + toast.show(`Module loaded: ${name}`); const samples = event.data.samples; let str = ''; @@ -37,7 +53,7 @@ window.onload = function () { document.querySelector('.sample-list').innerHTML = str; document.querySelector('.song-title').innerText = event.data.title; - document.querySelector('.title').innerText = moduleList[selectedMod].file; + document.querySelector('.title').innerText = name; document.querySelector('.author').innerText = moduleList[selectedMod].author; document.querySelector('.song-length').innerText = event.data.length; document.querySelector('.song-samples').innerText = event.data.samples.length; @@ -65,7 +81,9 @@ window.onload = function () { if (i === selectedMod) { options += ' selected'; } - options += `">${module.file}`; + const split = module.file.split('#'), + name = split.length > 1 && split[1] || split[0]; + options += `">${name}`; }); modNav.innerHTML = options; @@ -78,97 +96,61 @@ window.onload = function () { document.querySelector('.mdl-layout__obfuscator').click(); }); - document.addEventListener('analyzer_ready', (event) => { - requestAnimationFrame(() => { - effects[effect](event.data); - }); - }); - - document.querySelector('.channel_control').addEventListener('click', (event) => { - if (event.target.id && event.target.id.match(/channel-toggle/)) { - var channel = event.target.id.substr(-1, 1), - checked = event.target.hasAttribute('checked'); - - channelsPlaying[channel - 1] = !channelsPlaying[channel - 1]; - - ModPlayer.setPlayingChannels(channelsPlaying); - } - }); - - canvas.onclick = () => { - effect++; - if (effect >= effects.length) { - effect = 0; - } - }; - - function drawBars(amplitudeArray) { - var bufferLength = amplitudeArray.length; - ctx.fillStyle = 'rgb(0, 0, 0)'; - ctx.fillRect(0, 0, canvasWidth, canvasHeight); - - var barWidth = (canvasWidth / bufferLength) * 2.5 - 1; - barWidth *= 2; - var barHeight; - var x = 0; - - for (var i = 0; i < bufferLength; i++) { - barHeight = amplitudeArray[i]; - - ctx.fillStyle = 'rgb(' + (barHeight + 100) + ',50,50)'; - ctx.fillRect(x, canvasHeight - barHeight / 2, barWidth, barHeight / 2); - - x += barWidth; - } - } - - function drawOscillo(amplitudeArray) { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - - for (var i = 0; i < amplitudeArray.length; i++) { - var value = amplitudeArray[i] / 256; - var y = canvasHeight - (canvasHeight * value) - 1; - ctx.fillStyle = '#000000'; - ctx.fillRect(i, y, 1, 1); + canvas.addEventListener('click', (event) => { + const width = canvas.width / 4, + channel = Math.floor(event.offsetX / width); + + // audioworklet mode shows the four channels + // scriptprocessor fallback groups 0-3 and 1-2 channels visually + if (audioWorkletSupport) { + channelsPlaying[channel] = !channelsPlaying[channel]; + } else { + if (!channel) { + channelsPlaying[0] = !channelsPlaying[0]; + channelsPlaying[3] = !channelsPlaying[3]; + } else if (channel === 3) { + channelsPlaying[1] = !channelsPlaying[1]; + channelsPlaying[2] = !channelsPlaying[2]; + } } - } - function drawOscillo2(amplitudeArray) { - var bufferLength = amplitudeArray.length; - - ctx.fillStyle = "rgb(200, 200, 200)"; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - ctx.lineWidth = 2; - ctx.strokeStyle = "rgb(0, 0, 0)"; + ModPlayer.setPlayingChannels(channelsPlaying); + }); - ctx.beginPath(); + // function drawBars(amplitudeArray) { + // var bufferLength = amplitudeArray.length; + // ctx.fillStyle = 'rgb(0, 0, 0)'; + // ctx.fillRect(0, 0, canvasWidth, canvasHeight); - var sliceWidth = canvas.width * 1.0 / bufferLength; - var x = 0; + // var barWidth = (canvasWidth / bufferLength) * 2.5 - 1; + // barWidth *= 2; + // var barHeight; + // var x = 0; - for (var i = 0; i < bufferLength; i++) { + // for (var i = 0; i < bufferLength; i++) { + // barHeight = amplitudeArray[i]; - var v = amplitudeArray[i] / 128.0; - var y = v * canvas.height / 2; + // ctx.fillStyle = 'rgb(' + (barHeight + 100) + ',50,50)'; + // ctx.fillRect(x, canvasHeight - barHeight / 2, barWidth, barHeight / 2); - if (i === 0) { - ctx.moveTo(x, y); - } else { - ctx.lineTo(x, y); - } + // x += barWidth; + // } + // } - x += sliceWidth; - } - - ctx.lineTo(canvas.width, canvas.height / 2); - ctx.stroke(); - } + // function drawOscillo(amplitudeArray) { + // ctx.clearRect(0, 0, canvasWidth, canvasHeight); - effects.push(drawBars, drawOscillo); + // for (var i = 0; i < amplitudeArray.length; i++) { + // var value = amplitudeArray[i] / 256; + // var y = canvasHeight - (canvasHeight * value) - 1; + // ctx.fillStyle = '#000000'; + // ctx.fillRect(i, y, 1, 1); + // } + // } ModPlayer.init({ - canvas: canvas + canvas: canvas, + audioWorkletSupport: audioWorkletSupport }).then(() => { loadModule(selectedMod, false); }).catch((err) => { @@ -211,7 +193,7 @@ function loadModule(moduleIndex, hideDrawer = true) { document.querySelector('a.mdl-navigation__link.selected').classList.toggle('selected'); document.querySelector(`a.mdl-navigation__link.mod_${moduleIndex}`).classList.add('selected'); - ModPlayer.loadModule(prefix + moduleName) + ModPlayer.loadModule(moduleName.match(/^http/) ? moduleName : prefix + moduleName) .catch(err => { toast.show(`Error loading module: ${err}`); }); diff --git a/js/mod-processor-es5.js b/js/mod-processor-es5.js index 81f5f32..fbe4c99 100644 --- a/js/mod-processor-es5.js +++ b/js/mod-processor-es5.js @@ -1,2 +1,2 @@ -var t=function(t,e,s){void 0===s&&(s=0);for(var i=new Uint8Array(t),o="",a=!1,n=0;!a;++n){var r=i[s+n];(a=0===r)||(o+=String.fromCharCode(r))}return o},e=function(t,e,s){return void 0===e&&(e=0),void 0===s&&(s=!1),new DataView(t).getUint16(e,s)},s=new Array(4);s[0]=new Float32Array(64);for(var i=0;i-1&&!a.done&&this.ticks>=a.delay){var n=this.samples[a.sample];t[i][s]+=n.data[Math.floor(a.samplePos)]*a.volume/64,a.samplePos+=7093789.2/(2*a.period*this.mixingRate),a.done||(n.repeatLength||n.repeatStart?a.samplePos>=n.repeatStart+n.repeatLength&&(a.samplePos=n.repeatStart):a.samplePos>n.length&&(a.samplePos=0,a.done=!0))}}this.filledSamples++,this.newTick=!1}},i.prototype.tick=function(){this.filledSamples>this.samplesPerTick&&(this.newTick=!0,this.ticks++,this.filledSamples=0,this.ticks>this.speed-1&&(this.ticks=0,this.rowRepeat<=0&&this.row++,(this.row>63||this.skipPattern)&&(this.skipPattern=!1,this.jumpPattern>-1?(this.position=this.jumpPattern,this.jumpPattern=-1,this.getNextPattern()):this.getNextPattern(!0)),this.rowJump>-1&&(this.row=this.rowJump,this.rowJump=-1),this.row>63&&(this.row=0),this.decodeRow(),console.log("** next row !",this.row.toString(16).padStart(2,"0"))))},i.prototype.getNextPattern=function(t){t&&this.position++,this.position>this.positions.length-1&&(console.log("Warning: last position reached, going back to 0"),this.position=0);for(var e=0;e>4)-1,a=15&t[2+s],n=t[3+s],r=this.channels[e];r.delay=0,a?14===a?(r.cmd=224+(n>>4),r.data=15&n):(r.cmd=a,r.data=n):r.cmd=0,o>-1&&(3!==r.cmd&&5!==r.cmd&&(r.samplePos=0),r.done=!1,r.sample=o,r.volume=this.samples[o].volume),i&&(r.done=!1,3!==r.cmd&&5!==r.cmd?(r.period=i,r.samplePos=0):r.slideTo=i)}},i.prototype.executeEffect=function(t){try{a[t.cmd](this,t)}catch(e){console.warn("effect not implemented: "+t.cmd.toString(16).padStart(2,"0")+"/"+t.data.toString(16).padStart(2,"0"))}},i.prototype.getInstruments=function(){this.detectMaxSamples(),this.samples=new Array;for(var s=20,i=new Uint8Array(this.buffer),o=0;oa.length&&(a.repeatLength=0,a.repeatStart=0),this.samples.push(a),s+=30}},i.prototype.getPatternData=function(){var t=new Uint8Array(this.buffer,950);this.songLength=t[0];for(var e=2,s=0,i=0;is&&(s=o)}e=this.patternOffset;for(var a=0;a<=s;++a)this.patterns.push(this.buffer.slice(e,e+this.patternLength)),e+=this.patternLength},i.prototype.getSampleData=function(){for(var t=this.patternOffset+this.patterns.length*this.patternLength,e=0;e856&&(e.period=856))},3:function(t,e,s){t.ticks?e.slideTo&&t.ticks?e.periode.slideTo&&(e.period=e.slideTo)):e.period>e.slideTo&&(e.period-=e.slideSpeed,e.period>4;o&&i&&(e.vdepth=i,e.vspeed=o),e.vform>3&&(e.vpos=0)}e.period+=e.vdepth*s[3&e.vform][e.vpos]/63,e.vpos+=e.vspeed,e.vpos>63&&(e.vpos=e.vpos-64)},5:function(t,e){this[3](t,e),this[10](t,e)},6:function(t,e){this[4](t,e),this[10](t,e)},9:function(t,e){t.ticks||(e.samplePos=256*e.data)},10:function(t,e){if(t.ticks){var s=e.data>>4,i=15&e.data;i?s||(e.volume-=i):e.volume+=s,e.volume>63?e.volume=63:e.volume<0&&(e.volume=0)}},11:function(t,e){e.data>=0&&e.data<=t.patterns.length-1&&(t.skipPattern=!0,t.jumpPattern=e.data,t.rowJump=0)},12:function(t,e){t.ticks||(e.volume=e.data,e.volume>63&&(e.volume=63))},13:function(t,e){t.ticks||(t.rowJump=10*((240&e.data)>>4)+(15&e.data),t.skipPattern=!0)},224:function(t,e){t.ticks||(console.log("need to toggle lowPass",!!e.data),t.toggleLowPass(!!e.data))},228:function(t,e){},230:function(t,e){t.ticks||(0===e.data?e.loopInitiated?e.loops===e.loopCount&&(e.loopInitiated=!1):(e.loopInitiated=!0,e.loopStart=t.row,e.loopCount=0):e.loopInitiated&&(t.rowJump=e.loopStart,e.loopCount?e.loops++:(e.loopCount=e.data,e.loops=1)))},233:function(t,e){(t.ticks+1)%e.data||(console.log("retriggering note!",t.ticks+1),e.samplePos=0,e.done=!1)},234:function(t,e){t.ticks||(e.volume+=e.data,e.volume>63&&(e.volume=63))},235:function(t,e){t.ticks||(e.volume-=e.data,e.volume<0&&(e.volume=0))},237:function(t,e){t.ticks||(e.delay=e.data)},238:function(t,e){t.ticks||(t.rowRepeat?t.rowRepeat&&t.rowRepeat--:(t.rowRepeat=e.data,console.log("setting repeat to",t.rowRepeat)))},15:function(t,e){t.ticks||(e.data<32?t.speed=e.data:(t.bpm=e.data,t.calcTickSpeed()))},4095:function(){}};console.time(""); +var t=function(t,e,s){void 0===s&&(s=0);for(var i=new Uint8Array(t),o="",a=!1,n=0;!a;++n){var r=i[s+n];(a=0===r)||(o+=String.fromCharCode(r))}return o},e=function(t,e,s){return void 0===e&&(e=0),void 0===s&&(s=!1),new DataView(t).getUint16(e,s)},s=new Array(4);s[0]=new Float32Array(64);for(var i=0;i-1&&!a.done&&this.ticks>=a.delay){var r=this.samples[a.sample];n[s]+=r.data[Math.floor(a.samplePos)]*a.volume/64,a.samplePos+=7093789.2/(2*a.period*this.mixingRate),a.done||(r.repeatLength||r.repeatStart?a.samplePos>=r.repeatStart+r.repeatLength&&(a.samplePos=r.repeatStart):a.samplePos>r.length&&(a.samplePos=0,a.done=!0))}}this.filledSamples++,this.newTick=!1}},i.prototype.tick=function(){this.filledSamples>this.samplesPerTick&&(this.newTick=!0,this.ticks++,this.filledSamples=0,this.ticks>this.speed-1&&(this.ticks=0,this.rowRepeat<=0&&this.row++,(this.row>63||this.skipPattern)&&(this.skipPattern=!1,this.jumpPattern>-1?(this.position=this.jumpPattern,this.jumpPattern=-1,this.getNextPattern()):this.getNextPattern(!0)),this.rowJump>-1&&(this.row=this.rowJump,this.rowJump=-1),this.row>63&&(this.row=0),this.decodeRow()))},i.prototype.getNextPattern=function(t){t&&this.position++,this.position>this.positions.length-1&&(console.log("Warning: last position reached, going back to 0"),this.position=0);for(var e=0;e>4)-1,a=15&t[2+s],n=t[3+s],r=this.channels[e];r.delay=0,a?14===a?(r.cmd=224+(n>>4),r.data=15&n):(r.cmd=a,r.data=n):r.cmd=0,o>-1&&(3!==r.cmd&&5!==r.cmd&&(r.samplePos=0),r.done=!1,r.sample=o,r.volume=this.samples[o].volume),i&&(r.done=!1,3!==r.cmd&&5!==r.cmd?(r.period=i,r.samplePos=0):r.slideTo=i)}},i.prototype.executeEffect=function(t){try{a[t.cmd](this,t)}catch(e){console.warn("effect not implemented: "+t.cmd.toString(16).padStart(2,"0")+"/"+t.data.toString(16).padStart(2,"0"))}},i.prototype.getInstruments=function(){this.detectMaxSamples(),this.samples=new Array;for(var s=20,i=new Uint8Array(this.buffer),o=0;oa.length&&(a.repeatLength=0,a.repeatStart=0),this.samples.push(a),s+=30}},i.prototype.getPatternData=function(){var t=new Uint8Array(this.buffer,950);this.songLength=t[0];for(var e=2,s=0,i=0;is&&(s=o)}e=this.patternOffset;for(var a=0;a<=s;++a)this.patterns.push(this.buffer.slice(e,e+this.patternLength)),e+=this.patternLength},i.prototype.getSampleData=function(){for(var t=this.patternOffset+this.patterns.length*this.patternLength,e=0;e856&&(e.period=856))},3:function(t,e,s){t.ticks?e.slideTo&&t.ticks?e.periode.slideTo&&(e.period=e.slideTo)):e.period>e.slideTo&&(e.period-=e.slideSpeed,e.period>4;o&&i&&(e.vdepth=i,e.vspeed=o),e.vform>3&&(e.vpos=0)}e.period+=e.vdepth*s[3&e.vform][e.vpos]/63,e.vpos+=e.vspeed,e.vpos>63&&(e.vpos=e.vpos-64)},5:function(t,e){this[3](t,e),this[10](t,e)},6:function(t,e){this[4](t,e),this[10](t,e)},9:function(t,e){t.ticks||(e.samplePos=256*e.data)},10:function(t,e){if(t.ticks){var s=e.data>>4,i=15&e.data;i?s||(e.volume-=i):e.volume+=s,e.volume>63?e.volume=63:e.volume<0&&(e.volume=0)}},11:function(t,e){e.data>=0&&e.data<=t.patterns.length-1&&(t.skipPattern=!0,t.jumpPattern=e.data,t.rowJump=0)},12:function(t,e){t.ticks||(e.volume=e.data,e.volume>63&&(e.volume=63))},13:function(t,e){t.ticks||(t.rowJump=10*((240&e.data)>>4)+(15&e.data),t.skipPattern=!0)},224:function(t,e){t.ticks||(console.log("need to toggle lowPass",!!e.data),t.toggleLowPass(!!e.data))},228:function(t,e){},230:function(t,e){t.ticks||(0===e.data?e.loopInitiated?e.loops===e.loopCount&&(e.loopInitiated=!1):(e.loopInitiated=!0,e.loopStart=t.row,e.loopCount=0):e.loopInitiated&&(t.rowJump=e.loopStart,e.loopCount?e.loops++:(e.loopCount=e.data,e.loops=1)))},233:function(t,e){(t.ticks+1)%e.data||(console.log("retriggering note!",t.ticks+1),e.samplePos=0,e.done=!1)},234:function(t,e){t.ticks||(e.volume+=e.data,e.volume>63&&(e.volume=63))},235:function(t,e){t.ticks||(e.volume-=e.data,e.volume<0&&(e.volume=0))},237:function(t,e){t.ticks||(e.delay=e.data)},238:function(t,e){t.ticks||(t.rowRepeat?t.rowRepeat&&t.rowRepeat--:(t.rowRepeat=e.data,console.log("setting repeat to",t.rowRepeat)))},15:function(t,e){t.ticks||(e.data<32?t.speed=e.data:(t.bpm=e.data,t.calcTickSpeed()))}}; //# sourceMappingURL=modplayer-js.js.map diff --git a/js/mod-processor.js b/js/mod-processor.js index 07e572d..55d4713 100644 --- a/js/mod-processor.js +++ b/js/mod-processor.js @@ -54,6 +54,7 @@ class PTModuleProcessor extends AudioWorkletProcessor{ switch (event.data.message) { case 'init': this.mixingRate = event.data.mixingRate; + this.audioWorkletSupport = event.data.audioWorkletSupport; break; case 'loadModule': @@ -81,6 +82,12 @@ class PTModuleProcessor extends AudioWorkletProcessor{ } }); break; + + case 'speedUp': + console.log('speed up', event.data.speedUp); + this.speedUp = event.data.speedUp; + this.calcTickSpeed(); + break; } } @@ -90,21 +97,31 @@ class PTModuleProcessor extends AudioWorkletProcessor{ process(inputs, outputs, params) { if (this.ready && this.playing) { - this.mix(outputs[0]); + this.mix(outputs); } else { - this.emptyOutputBuffer(outputs[0]); + this.emptyOutputBuffer(outputs); } return true; } - emptyOutputBuffer(buffers) { - const length = buffers[0].length, - chans = buffers.length; + emptyOutputBuffer(outputs) { + if (this.audioWorkletSupport) { + const chans = outputs.length, + bufLength = outputs[0][0].length; - for (let i = 0; i < length; ++i) { for (let chan = 0; chan < chans; ++chan) { - buffers[chan][i] = 0.0; + for (let i = 0; i < bufLength; ++i) { + outputs[chan][0][i] = 0.0; + } + } + } else { + const chans = outputs[0].length, + bufLength = outputs[0][0].length; + for (let chan = 0; chan < chans; ++chan) { + for (let i = 0; i < bufLength; ++i) { + outputs[0][chan][i] = 0.0; + } } } } @@ -123,6 +140,7 @@ class PTModuleProcessor extends AudioWorkletProcessor{ this.bpm = 125; // number of ticks before playing next pattern row this.speed = 6; + this.speedUp = 1; this.position = 0; this.pattern = 0; this.row = 0; @@ -228,29 +246,39 @@ class PTModuleProcessor extends AudioWorkletProcessor{ * Calculates the number of samples needed */ calcTickSpeed() { - this.samplesPerTick = ((this.mixingRate * 60) / this.bpm) / 24; + this.samplesPerTick = ((this.mixingRate * 60) / (this.bpm * this.speedUp)) / 24; } /** * ProTracker audio mixer * - * @param {Float32Array} buffer Output buffer that should be filled with PCM data + * @param {Float32Array} outputs Output buffer that should be filled with PCM data * * This method is called each time the buffer should be filled with data */ - mix(buffers) { - const length = buffers[0].length; + mix(outputs) { + const length = this.audioWorkletSupport && outputs[0][0].length || outputs[0][0].length; for (let i = 0; i < length; ++i) { - buffers[0][i] = 0.0; - buffers[1][i] = 0.0; - + // buffers[0][i] = 0.0; + // buffers[1][i] = 0.0; let outputChannel = 0; + if (this.audioWorkletSupport) { + outputs[0][0][i] = 0.0; + outputs[1][0][i] = 0.0; + outputs[2][0][i] = 0.0; + outputs[3][0][i] = 0.0; + } else { + outputs[0][0][i] = 0.0; + outputs[0][1][i] = 0.0; + } + // playing speed test this.tick(); for (let chan = 0; chan < this.channels.length; ++chan) { const channel = this.channels[chan]; + const buffer = this.audioWorkletSupport ? outputs[chan][0] : outputs[0][outputChannel]; // select left/right output depending on module channel: // voices 0,3 go to left channel, 1,2 go to right channel outputChannel = outputChannel ^ (chan & 1); @@ -266,7 +294,7 @@ class PTModuleProcessor extends AudioWorkletProcessor{ const sample = this.samples[channel.sample]; // actually mix audio - buffers[outputChannel][i] += (sample.data[Math.floor(channel.samplePos)] * channel.volume) / 64.0; + buffer[i] += (sample.data[Math.floor(channel.samplePos)] * channel.volume) / 64.0; const sampleSpeed = 7093789.2 / ((channel.period * 2) * this.mixingRate); channel.samplePos += sampleSpeed; @@ -327,7 +355,7 @@ class PTModuleProcessor extends AudioWorkletProcessor{ this.decodeRow(); - console.log('** next row !', this.row.toString(16).padStart(2, "0")); + // console.log('** next row !', this.row.toString(16).padStart(2, "0")); } } } @@ -634,10 +662,8 @@ const Effects = { y = channel.data & 0x0F; if (!y) { - // console.log('volume slide', x); channel.volume += x; } else if (!x) { - // console.log('volume slide', -y); channel.volume -= y; } @@ -666,9 +692,7 @@ const Effects = { channel.volume = channel.data; if (channel.volume > 63) { channel.volume = 63; - }/* else { - channel.id === 2 && console.log('volume set to', channel.volume); - }*/ + } } }, /** @@ -686,7 +710,6 @@ const Effects = { 0xE0(Module, channel) { if (!Module.ticks) { console.log('need to toggle lowPass', !!channel.data); - // TODO: handle this message in modplayer.js to activate the filter Module.toggleLowPass(!!channel.data); } }, @@ -795,6 +818,4 @@ const Effects = { } } } -} - -console.time('') +} \ No newline at end of file diff --git a/js/modplayer.js b/js/modplayer.js index 3ce7aea..75c1ac5 100644 --- a/js/modplayer.js +++ b/js/modplayer.js @@ -12,8 +12,10 @@ const ModPlayer = { init(options) { this.canvas = options.canvas; this.ctx = this.canvas.getContext('2d'); - this.canvasWidth = (this.canvas.width) / 2; + this.audioWorkletSupport = options.audioWorkletSupport; + this.canvasWidth = (this.canvas.width) / 4; this.canvasHeight = this.canvas.height; + this.channels = [true, true, true, true]; return this.createContext(); }, @@ -59,42 +61,67 @@ const ModPlayer = { const soundProcessor = this.isXbox && 'mod-processor-es5.js' || 'mod-processor.js'; return this.context.audioWorklet.addModule(`js/${soundProcessor}`).then(() => { + const numAnalysers = this.audioWorkletSupport && 4 || 2; + + // apply a filter + this.filterNode = this.context.createBiquadFilter(); + this.filterNode.frequency.value = 22050; + + // Use 4 inputs that will be used to send each track's data to a separate analyser + // NOTE: what should we do if we support more channels (and different mod formats)? this.workletNode = new AudioWorkletNode(this.context, 'mod-processor', { - outputChannelCount:[2] + outputChannelCount: [1, 1, 1, 1], + numberOfInputs: 0, + numberOfOutputs: 4 }); + + if (!this.audioWorkletSupport) { + this.splitter = this.context.createChannelSplitter(numAnalysers); + this.filterNode.connect(this.splitter); + } + this.workletNode.port.onmessage = this.handleMessage.bind(this); this.postMessage({ message: 'init', - mixingRate: this.mixingRate + mixingRate: this.mixingRate, + audioWorkletSupport: this.audioWorkletSupport }); this.workletNode.port.start(); - this.filterNode = this.context.createBiquadFilter(); - this.filterNode.frequency.value = 22050; - - this.workletNode.connect(this.filterNode); - this.filterNode.connect(this.context.destination); + // create four analysers and connect each worklet's input to one + this.analysers = new Array(); + + for (let i = 0; i < numAnalysers; ++i) { + const analyser = this.context.createAnalyser(); + analyser.fftSize = 256;// Math.pow(2, 11); + analyser.minDecibels = -90; + analyser.maxDecibels = -10; + analyser.smoothingTimeConstant = 0.65; + if (this.audioWorkletSupport) { + this.workletNode.connect(analyser, i, 0); + } else { + this.splitter.connect(analyser, i); + } + this.analysers.push(analyser); + } - // split channels and connect each channel's output - // to a separate analyzer - this.analysisSplitter = this.context.createChannelSplitter(2); - this.filterNode.connect(this.analysisSplitter); + if (this.audioWorkletSupport) { + this.merger = this.context.createChannelMerger(4); - this.analyserLeft = this.context.createAnalyser(); + // merge the channel 0+3 in left channel, 1+2 in right channel + this.workletNode.connect(this.merger, 0, 0); + this.workletNode.connect(this.merger, 1, 1); + this.workletNode.connect(this.merger, 2, 1); + this.workletNode.connect(this.merger, 3, 0); - this.analyserLeft.fftSize = Math.pow(2, 11); - this.analyserLeft.minDecibels = -96; - this.analyserLeft.maxDecibels = 0; - this.analyserLeft.smoothingTimeConstant = 0.85; + // finally apply the lowpass filter and send audio to destination + this.merger.connect(this.filterNode); + } else { + this.workletNode.connect(this.filterNode); + } - this.analyserRight = this.context.createAnalyser(); - this.analyserRight.fftSize = Math.pow(2, 11); - this.analyserRight.minDecibels = -96; - this.analyserRight.maxDecibels = 0; - this.analyserRight.smoothingTimeConstant = 0.85; - this.analysisSplitter.connect(this.analyserLeft, 0); - this.analysisSplitter.connect(this.analyserRight, 1); + this.filterNode.connect(this.context.destination); }); }, @@ -102,6 +129,13 @@ const ModPlayer = { this.filterNode.frequency.value = activate && 6000 || 22050; }, + setSpeed(speedUp) { + this.postMessage({ + message: 'speedUp', + speedUp: speedUp + }); + }, + handleMessage(message) { switch (message.data.message) { case 'moduleLoaded': @@ -110,6 +144,9 @@ const ModPlayer = { event.data = message.data.data; event.data.wasPlaying = this.wasPlaying; document.dispatchEvent(event); + if (!this.playing) { + this.renderScope(); + } break; case 'toggleLowPass': @@ -174,6 +211,12 @@ const ModPlayer = { message: 'setPlayingChannels', channels: channels }); + + this.channels = channels; + + if (!this.playing) { + this.renderScope(); + } }, render() { @@ -187,37 +230,84 @@ const ModPlayer = { * render adapted from https://github.com/acarabott/audio-dsp-playground (MIT Licence) */ renderScope() { - const toRender = [ - { - label: "Left", - analyser: this.analyserLeft, - style: "rgba(53, 233, 255, 1)", - edgeThreshold: 0, - active: true - }, - { - label: "Right", - analyser: this.analyserRight, - style: "rgba(53, 233, 255, 1)", - edgeThreshold: 0, - active: true - }]; + let toRender; + + if (this.audioWorkletSupport) { + toRender = [ + { + label: "chan 1", + analyser: this.analysers[0], + style: "rgba(53, 233, 255, 1)", + edgeThreshold: 0, + pos: 0, + }, + { + label: "chan 2", + analyser: this.analysers[1], + style: "rgba(53, 233, 255, 1)", + edgeThreshold: 0, + pos: 1 + }, + { + label: "chan 3", + analyser: this.analysers[2], + style: "rgba(53, 233, 255, 1)", + edgeThreshold: 0, + pos: 2 + }, + { + label: "chan 4", + analyser: this.analysers[3], + style: "rgba(53, 233, 255, 1)", + edgeThreshold: 0, + pos: 3 + }]; + } else { + toRender = [ + { + label: "left", + analyser: this.analysers[0], + style: "rgba(53, 233, 255, 1)", + edgeThreshold: 0, + pos: 0 + }, + { + label: "right", + analyser: this.analysers[1], + style: "rgba(53, 233, 255, 1)", + edgeThreshold: 0, + pos: 3 + }]; + } this.ctx.fillStyle = "transparent"; - this.ctx.clearRect(0, 0, this.canvasWidth * 2, this.canvasHeight); + this.ctx.clearRect(0, 0, this.canvasWidth * 4, this.canvasHeight); - toRender.forEach(({ analyser, style = "rgb(43, 156, 212)", edgeThreshold = 0 }, i) => { + toRender.forEach(({ analyser, label, style = "rgb(43, 156, 212)", edgeThreshold = 0, pos }, i) => { if (analyser === undefined) { return; } + + this.ctx.font = "12px Verdana"; + this.ctx.fillStyle = "rgba(255,255,255,0.8)"; + this.ctx.textAlign = "left"; + // Chan number + this.ctx.fillText(label, 46 + pos * this.canvasWidth, 15); + + if (!this.channels[i]) { + this.ctx.font = "18px Arial"; + this.ctx.fillText('MUTE', 40 + pos * this.canvasWidth, 70); + return; + } + const timeData = new Float32Array(analyser.frequencyBinCount); let risingEdge = 0; analyser.getFloatTimeDomainData(timeData); - this.ctx.lineWidth = 2; this.ctx.strokeStyle = style; + this.ctx.fillStyle = style; - this.ctx.beginPath(); + // this.ctx.beginPath(); while (timeData[risingEdge] > 0 && risingEdge <= this.canvasWidth && @@ -238,17 +328,12 @@ const ModPlayer = { for (let x = risingEdge; x < timeData.length && x - risingEdge < this.canvasWidth; x++) { const y = this.canvasHeight - (((timeData[x] + 1) / 2) * this.canvasHeight); - this.ctx.lineTo(x - risingEdge + i * this.canvasWidth, y); + // this.ctx.moveTo(x - risingEdge + i * this.canvasWidth, y-1); + // this.ctx.lineTo(x - risingEdge + i * this.canvasWidth, y); + this.ctx.fillRect(x - risingEdge + pos * this.canvasWidth, y, 1, 1); } - this.ctx.stroke(); + // this.ctx.stroke(); }); - - // L/R - this.ctx.fillStyle = "rgba(255,255,255,0.7)"; - this.ctx.font = "11px Verdana"; - this.ctx.textAlign = "left"; - this.ctx.fillText("L", 5, 15); - this.ctx.fillText("R", 496, 15); } } \ No newline at end of file