Giter Site home page Giter Site logo

webaudio-mod-player's Introduction

webaudio-mod-player

Screenshot

This is a MOD/S3M/XM module player implemented in Javascript using the Web Audio API and runs fully within the browser. It has been tested and confirmed to work on Chrome 14+, Firefox 24+, Safari 6+ and Edge 20+. The Javascript performance of the browsers varies significantly, so some modules may stutter on one browser while the same module can play flawlessly on other ones. YMMV.

Although internally each file format is handled by a format specific player class, a front-end wrapper class is used to provide a common programming interface for the player.

All player classes use 32-bit floating point arithmetic in the channel mixing code, as well as a wide dynamic range. The output is scaled down to [-1, 1] domain using a "soft clipping" algorithm to roll off any audio peaks without harsh-sounding limiting. This should - in most cases - produce a reasonably constant audio volume for all modules.

Additionally, S3M and XM player classes use linear sample interpolation and volume ramping to produce a smooth Gravis Ultrasound -like sound quality. The MOD player class attempts to sound more like an Amiga by allowing audio aliasing and applying a low pass filter.

None of the player classes fully implement all the features and effects in each file format, but all the major ones should be implemented. In addition, there most certainly will be some playback bugs in each player class - let me know if you run into some bad ones.

You can test the player here:

https://mod.haxor.fi/

To install on your own server, clone the repo to the document root and edit+rename example.htaccess to match your domain. Then create a directory 'mods' alongside index.php and structure is like this (note that both PC-style and Amiga-style filenames are supported but extension must always be in lowercase):

/mods
/mods/Jugi
/mogs/Jugi/we_go.xm
/mods/Mantronix_and_Tip
/mods/Mantronix_and_Tip/mod.overload
/mods/Necros
/mods/Necros/point.s3m
/mods/mod.saf

Copyrights:

  • MOD/S3M/XM module player for Web Audio (c) 2012-2021 Noora Halme et al (see AUTHORS)
  • Topaz TTF font (c) 2009 dMG of Trueschool and Divine Stylers
  • "overload" (c) 1991 by Mantronix and Tip of Phenomena
  • "Point of Departure" (c) 1995 Necros / FM
  • "we go..." (c) 1996 by Jugi / Complex

webaudio-mod-player's People

Contributors

electronoora avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

webaudio-mod-player's Issues

Better integration in other environments plus minor improvements

File : player.js
Changes are identified with "@@@ JLB"
See my integration at https://webaudiostudio.jlbee.fr

/*
front end wrapper class for format-specific player classes
(c) 2015-2017 firehawk/tda
*/

// @@@ JLB : added 2 optionnal parameters
function Modplayer(audioContext, bConnectToDest)
{
this.supportedformats=new Array('mod', 's3m', 'xm');

this.url="";
this.format="s3m";

this.state="standby";
this.request=null;

this.loading=false;
this.playing=false;
this.paused=false;
this.repeat=false;

this.separation=1;
this.mixval=8.0;

this.amiga500=false;

this.filter=false;
this.endofsong=false;

this.autostart=false;
this.bufferstodelay=4; // adjust this if you get stutter after loading new song
this.delayfirst=0;
this.delayload=0;

this.onReady=function(){};
this.onPlay=function(){};
this.onStop=function(){};

this.buffer=0;
this.mixerNode=0;

// @@@ JLB
this.bConnectToDest = bConnectToDest;
this.context=audioContext;

this.samplerate=44100;
this.bufferlen=4096;

this.chvu=new Float32Array(32);

// format-specific player
this.player=null;

// read-only data from player class
this.title="";
this.signature="....";
this.songlen=0;
this.channels=0;
this.patterns=0;
this.samplenames=new Array();

// @@@ JLB
this.createContext();
}

// load module from url into local buffer
Modplayer.prototype.load = function(url)
{
// try to identify file format from url and create a new
// player class for it
this.url=url;
var ext=url.split('.').pop().toLowerCase().trim();
if (this.supportedformats.indexOf(ext)==-1) {
// unknown extension, maybe amiga-style prefix?
ext=url.split('/').pop().split('.').shift().toLowerCase().trim();
if (this.supportedformats.indexOf(ext)==-1) {
// ok, give up
return false;
}
}
this.format=ext;

switch (ext) {
case 'mod':
this.player=new Protracker();
break;
case 's3m':
this.player=new Screamtracker();
break;
case 'xm':
this.player=new Fasttracker();
break;
}

this.player.onReady=this.loadSuccess;

this.state="loading..";
var request = new XMLHttpRequest();
request.open("GET", this.url, true);
request.responseType = "arraybuffer";
this.request = request;
this.loading=true;
var asset = this;
request.onprogress = function(oe) {
asset.state="loading ("+Math.floor(100*oe.loaded/oe.total)+"%)..";
};
request.onload = function() {
var buffer=new Uint8Array(request.response);
this.state="parsing..";
if (asset.player.parse(buffer)) {
// copy static data from player
asset.title=asset.player.title
asset.signature=asset.player.signature;
asset.songlen=asset.player.songlen;
asset.channels=asset.player.channels;
asset.patterns=asset.player.patterns;
asset.filter=asset.player.filter;
if (asset.context) asset.setfilter(asset.filter);
asset.mixval=asset.player.mixval; // usually 8.0, though
asset.samplenames=new Array(32)
for(i=0;i<32;i++) asset.samplenames[i]="";
if (asset.format=='xm' || asset.format=='it') {
for(i=0;i<asset.player.instrument.length;i++) asset.samplenames[i]=asset.player.instrument[i].name;
} else {
for(i=0;i<asset.player.sample.length;i++) asset.samplenames[i]=asset.player.sample[i].name;
}

  asset.state="ready";
  asset.loading=false;
  asset.onReady();
  if (asset.autostart) asset.play();
} else {
  asset.state="error";
  asset.loading=false;
}

}
request.send();
return true;
}

// play loaded and parsed module with webaudio context
Modplayer.prototype.play = function()
{
if (this.loading) return false;
if (this.player) {
// @@@ JLB if (this.context == null) this.createContext();
this.player.samplerate=this.samplerate;
if (this.context) this.setfilter(this.player.filter);

if (this.player.paused) {
  this.player.paused=false;
  return true;
}

this.endofsong=false;
this.player.endofsong=false;
this.player.paused=false;
this.player.initialize();
this.player.flags=1+2;
this.player.playing=true;
this.playing=true;

this.chvu=new Float32Array(this.player.channels);
for(i=0;i<this.player.channels;i++) this.chvu[i]=0.0;

this.onPlay();

this.player.delayfirst=this.bufferstodelay;
return true;

} else {
return false;
}
}

// pause playback
Modplayer.prototype.pause = function()
{
if (this.player) {
if (!this.player.paused) {
this.player.paused=true;
} else {
this.player.paused=false;
}
}
}

// stop playback
Modplayer.prototype.stop = function()
{
this.paused=false;
this.playing=false;
if (this.player) {
this.player.paused=false;
this.player.playing=false;
this.player.delayload=1;
}
this.onStop();
}

// stop playing but don't call callbacks
Modplayer.prototype.stopaudio = function(st)
{
if (this.player) {
this.player.playing=st;
}
}

// jump positions forward/back
Modplayer.prototype.jump = function(step)
{
if (this.player) {
this.player.tick=0;
this.player.row=0;
this.player.position+=step;
this.player.flags=1+2;
if (this.player.position<0) this.player.position=0;
if (this.player.position >= this.player.songlen) this.stop();
}
this.position=this.player.position;
this.row=this.player.row;
}

// @@@ JLB
Modplayer.prototype.getPercentPlayed = function() {
if (this.player === null) return 0;
var totalstep = this.player.songlen * 64;
var curstep = this.player.position * 64 + this.player.row;
if (totalstep === 0) return 0;
return curstep * 100 / totalstep;
}

// set whether module repeats after songlen
Modplayer.prototype.setrepeat = function(rep)
{
this.repeat=rep;
if (this.player) this.player.repeat=rep;
}

// set stereo separation mode (0=standard, 1=65/35 mix, 2=mono)
Modplayer.prototype.setseparation = function(sep)
{
this.separation=sep;
if (this.player) this.player.separation=sep;
}

// set autostart to play immediately after loading
Modplayer.prototype.setautostart = function(st)
{
this.autostart=st;
}

// set amiga model - changes lowpass filter state
Modplayer.prototype.setamigamodel = function(amiga)
{
if (amiga=="600" || amiga=="1200" || amiga=="4000") {
this.amiga500=false;
if (this.filterNode) this.filterNode.frequency.value=22050;
} else {
this.amiga500=true;
if (this.filterNode) this.filterNode.frequency.value=6000;
}
}

// amiga "LED" filter
Modplayer.prototype.setfilter = function(f)
{
if (f) {
this.lowpassNode.frequency.value=3275;
} else {
this.lowpassNode.frequency.value=28867;
}
this.filter=f;
if (this.player) this.player.filter=f;
}

// are there E8x sync events queued?
Modplayer.prototype.hassyncevents = function()
{
if (this.player) return (this.player.syncqueue.length != 0);
return false;
}

// pop oldest sync event nybble from the FIFO queue
Modplayer.prototype.popsyncevent = function()
{
if (this.player) return this.player.syncqueue.pop();
}

// ger current pattern number
Modplayer.prototype.currentpattern = function()
{
if (this.player) return this.player.patterntable[this.player.position];
// @@@ JLB
return 0;
}

// get current pattern in standard unpacked format (note, sample, volume, command, data)
// note: 254=noteoff, 255=no note
// sample: 0=no instrument, 1..255=sample number
// volume: 255=no volume set, 0..64=set volume, 65..239=ft2 volume commands
// command: 0x2e=no command, 0..0x24=effect command
// data: 0..255
Modplayer.prototype.patterndata = function(pn)
{
var i, c, patt;
if (this.format=='mod') {
patt=new Uint8Array(this.player.pattern_unpack[pn]);
for(i=0;i<64;i++) for(c=0;c<this.player.channels;c++) {
if (patt[i5this.channels+c5+3]==0 && patt[i5this.channels+c5+4]==0) {
patt[i5this.channels+c5+3]=0x2e;
} else {
patt[i
5this.channels+c5+3]+=0x37;
if (patt[i5this.channels+c5+3]<0x41) patt[i5this.channels+c5+3]-=0x07;
}
}
} else if (this.format=='s3m') {
patt=new Uint8Array(this.player.pattern[pn]);
for(i=0;i<64;i++) for(c=0;c<this.player.channels;c++) {
if (patt[i5this.channels+c5+3]==255) patt[i5this.channels+c5+3]=0x2e;
else patt[i5this.channels+c5+3]+=0x40;
}
} else if (this.format=='xm') {
patt=new Uint8Array(this.player.pattern[pn]);
for(i=0;i<this.player.patternlen[pn];i++) for(c=0;c<this.player.channels;c++) {
if (patt[i
5this.channels+c5+0]<97)
patt[i5this.channels+c5+0]=(patt[i5this.channels+c5+0]%12)|(Math.floor(patt[i5this.channels+c5+0]/12)<<4);
if (patt[i
5this.channels+c5+3]==255) patt[i5this.channels+c5+3]=0x2e;
else {
if (patt[i
5this.channels+c5+3]<0x0a) {
patt[i5this.channels+c5+3]+=0x30;
} else {
patt[i
5this.channels+c5+3]+=0x41-0x0a;
}
}
}
}
return patt;
}

// check if a channel has a note on
Modplayer.prototype.noteon = function(ch)
{
if (ch>=this.channels) return 0;
return this.player.channel[ch].noteon;
}

// get currently active sample on channel
Modplayer.prototype.currentsample = function(ch)
{
if (ch>=this.channels) return 0;
if (this.format=="xm" || this.format=="it") return this.player.channel[ch].instrument;
return this.player.channel[ch].sample;
}

// get length of currently playing pattern
Modplayer.prototype.currentpattlen = function()
{
if (this.format=="mod" || this.format=="s3m") return 64;
return this.player.patternlen[this.player.patterntable[this.player.position]];
}

// create the web audio context
Modplayer.prototype.createContext = function()
{
// @@@ JLB
if (this.context === null || this.context === undefined) {
if ( typeof AudioContext !== 'undefined') {
this.context = new AudioContext();
} else {
this.context = new webkitAudioContext();
}
}

this.samplerate=this.context.sampleRate;
this.bufferlen=(this.samplerate > 44100) ? 4096 : 2048;

// Amiga 500 fixed filter at 6kHz. WebAudio lowpass is 12dB/oct, whereas
// older Amigas had a 6dB/oct filter at 4900Hz.
this.filterNode=this.context.createBiquadFilter();
if (this.amiga500) {
this.filterNode.frequency.value=6000;
} else {
this.filterNode.frequency.value=22050;
}

// "LED filter" at 3275kHz - off by default
this.lowpassNode=this.context.createBiquadFilter();
this.setfilter(this.filter);

// mixer
if ( typeof this.context.createJavaScriptNode === 'function') {
this.mixerNode=this.context.createJavaScriptNode(this.bufferlen, 1, 2);
} else {
this.mixerNode=this.context.createScriptProcessor(this.bufferlen, 1, 2);
}
this.mixerNode.module=this;
this.mixerNode.onaudioprocess=Modplayer.prototype.mix;

// patch up some cables :)
this.mixerNode.connect(this.filterNode);
this.filterNode.connect(this.lowpassNode);

if (this.bConnectToDest === true) { // @@@ JLB
this.lowpassNode.connect(this.context.destination);
}
}

// @@@ JLB
Modplayer.prototype.getOutputNode = function() {
return this.lowpassNode;
}

// scriptnode callback - pass through to player class
Modplayer.prototype.mix = function(ape) {
var mod;

if (ape.srcElement) {
mod=ape.srcElement.module;
} else {
mod=this.module;
}

if (mod.player && mod.delayfirst==0) {
mod.player.repeat=mod.repeat;

var bufs=new Array(ape.outputBuffer.getChannelData(0), ape.outputBuffer.getChannelData(1));
var buflen=ape.outputBuffer.length;
mod.player.mix(mod.player, bufs, buflen);

// apply stereo separation and soft clipping
var outp=new Float32Array(2);
for(var s=0;s<buflen;s++) {
  outp[0]=bufs[0][s];
  outp[1]=bufs[1][s];

  // a more headphone-friendly stereo separation
  if (mod.separation) {
    t=outp[0];
    if (mod.separation==2) { // mono
      outp[0]=outp[0]*0.5 + outp[1]*0.5;
      outp[1]=outp[1]*0.5 + t*0.5;
    } else { // narrow stereo
      outp[0]=outp[0]*0.65 + outp[1]*0.35;
      outp[1]=outp[1]*0.65 + t*0.35;
    }
  }

  // scale down and soft clip
  outp[0]/=mod.mixval; outp[0]=0.5*(Math.abs(outp[0]+0.975)-Math.abs(outp[0]-0.975));
  outp[1]/=mod.mixval; outp[1]=0.5*(Math.abs(outp[1]+0.975)-Math.abs(outp[1]-0.975));
  
  bufs[0][s]=outp[0];
  bufs[1][s]=outp[1];
}

mod.row=mod.player.row;
mod.position=mod.player.position;
mod.speed=mod.player.speed;
mod.bpm=mod.player.bpm;
mod.endofsong=mod.player.endofsong;

if (mod.player.filter != mod.filter) {
  mod.setfilter(mod.player.filter);
}

if (mod.endofsong && mod.playing) mod.stop();

if (mod.delayfirst>0) mod.delayfirst--;
mod.delayload=0;

// update this.chvu from player channel vu
if (mod.playing && ! mod.paused) { // @@@ JLB
	for(var i=0;i<mod.player.channels;i++) {
	  mod.chvu[i]=mod.chvu[i]*0.25 + mod.player.chvu[i]*0.75;    
	  mod.player.chvu[i]=0.0;
	}
  } else {
	for(var i=0;i<mod.player.channels;i++) {
		mod.chvu[i] = 0;
	}
  }

}

}

Not work with Raspbian on Rpi 3

The rewrite rules not works with apache2 on Raspbian 9.2.
If i load .mod from anywhere and later i load other scripts, then it not work; just hanging on loading....

results the following errors in second load .mod(inspect):
TypeError: undefined is not an object (evaluating 's[i].selected=false')

http://sample.domain.com:90/original_mod/mods/original_mod/humanoids.modFailed to load resource: the server responded with a status of 404 (Not Found)

duplicated path in url...

Pattern visualisation missing after manual load

After playing a module and then manually loading another one, the pattern visualisation is not shown until position changes from 00 to 01.

This issue doesn't seem to always occur when playlist is active.

UI Performance Issues

(FYI: I'm currently working on a PR for this)

I've taken a closer look at the UI code and I have some ideas on how to improve performance. Here are some of the things I'm considering addressing:

  • using setInterval for regular updates instead of the more efficient requestAnimationFrame
  • querying the DOM every time an element is mutated instead of caching DOM references
  • using jQuery's replaceWith to overwrite element markup rather than mutating the existing element
  • scrolling patterns by modifying scrollTop rather than using CSS transforms (scroll seems to cause unnecessary repaints)

Note that I don't have a precise way of measuring the mod player's performance. My current method is measuring general JS performance with Chrome dev tools (though that's not a direct indicator of perceived WebAudio performance). I also have several mods that play very slowly (with a lot of hitching) and so hopefully those will play back smoother as UI performance improves.

Standalone player

Hi,
I'm trying to use the library as a standalone player, without the UI part.
Since it's not documented, all I'm trying to do is:

var player = new Modplayer();
player.load('chiptune.mod');
player.onReady = function () {
    player.play();
}

It starts playing but after a short while (a couple of seconds) the song gets stuck in a loop (like repeating the same bar) and in the console I see this error:

Uncaught TypeError: Cannot read property 'player' of undefined

being thrown over and over.

General cleanup

I've been wanting to to better understand the implementation details of this repo and maybe make some improvements or experiment with related ideas. For example, I would love to clearly separate the modplayer code from the UI code (as mentioned in #5 ) and use that to experiment with a simple web synth/keyboard.

As I read through the code, I've been noticing some small style inconsistencies. Would it be okay with you if I did a general style cleanup on the repo?

Here are some simple changes I'm thinking of for a first pass:

  • fix typos
  • remove unnecessary spaces
  • standardize tab width
  • add an .editorconfig to help enforce basic style consistency

Here are some other ideas for a second pass:

  • change function names to be more descriptive
  • perhaps add standardized comment blocks for functions (JSDocs style perhaps? not sure)

I haven't done any of this yet--I wanted to ask first to make sure I'm not offending you or anything. I know this is a personal project of yours so I would hate for you to think I'm meddling. It's definitely a cool project and I hope I can find more time to work on it! ๐Ÿ˜„

Curious about the source of the last value in note period table.

Hi,

I'm curious where you found or derived the values in the last column of this table,
https://github.com/jhalme/webaudio-mod-player/blob/91b7e730db23c26e27e350b5cb29a1634939202b/js/st3.js#L31

I'm writing some tracker routines myself, and found 3628 (or a multiple thereof) used instead of 3624 everywhere I looked.

See https://github.com/schismtracker/schismtracker/blob/master/player/tables.c for instance. It has 1016, 960, 907 as the last three values. Yours has 1016, 960, 906 (off by one).

I doubt you'd actually be able to perceive such a small error. But just curious how come? Did you derive these values yourself and is it the result of rounding?

Cheers,
Marcel

Refactor to use AudioWorkletNode instead of ScriptProcessorNode

ScriptProcessorNode has been deprecated and replaced by AudioWorkletNode, which by now is supported by all the major browsers. Refactor the player code to use an AudioWorkletNode.

Since this also results in the mixing to happen in an AudioWorkletProcessor running in the Web Audio rendering thread, this should yield more stable audio performance.

Rewrite the UI to render entirely into a Canvas

Rendering the player UI using a complex DOM and requiring pixel-perfect positioning causes various performance and compatibility issues. Using a pre-rendered fontmap image and rendering the UI fully into a Canvas first, and then to the screen would likely improve performance, and simplify the code.

Clickable UI elements would need a mechanism for registering event callbacks to provide hover/click functionality.

As an additional bonus, this would also rewrite out the dependency to jQuery.

Publish modules to npm with TypeScript types

This library is great! Thanks for all the work you've put in to it.

I needed a TypeScript module to play mods in the browser and was slightly surprised to find very few options out there.

I've converted the Protracker class to TypeScript and published it as an npm module, along with a loadUrl helper:

If this is something you were hoping to do yourself, let me know :)

Similar modules:

Ui improvements

Hey there, I've some ideas to improve the ui.

  • Multiple Track selections with Ctrl (select multiple tracks) and Shift (select a bunch of tracks).
  • Support playlists
  • Share playlists

module.setamigatype() is not a function

Thanks again for your fantastic work on this project!

I decided to drop my Node port and stick with PHP so that I could play with all the new features you've recently added.

Unfortunately, the mod player was not working properly when I deployed it. The JS console reads Uncaught TypeError: module.setamigatype is not a function(โ€ฆ) and none of the player functionality works.

Suggestion: rewrite file format handlers in Rust

Hi @JHalme, first I want to say thank you for an incredibly nice little thing here. ๐Ÿ˜„ And thanks for making it FOSS!

I guess I'm feeling a bit nostalgic right now, recorded an .avi of an old MSDOS intro I made 20 years ago and put up on YouTube the other day. And now I'm looking into making a web port of this intro, just for the sake of it...

I integrated the player in my web app quite easily, using this code (documenting this since it might help others who also want to integrate the player into their intros/whatever). Only the files below are needed if you don't want the UI; no jQuery etc is necessary.

<script src="webaudio-mod-player/utils.js" type="text/javascript"></script>
<script src="webaudio-mod-player/player.js" type="text/javascript"></script>
<script src="webaudio-mod-player/pt.js" type="text/javascript"></script>

And then in a button event handler I kick it live like this:

window.module = new Modplayer();
window.module.setrepeat(true);

// Because the file gets loaded asynchronously, we need to wait until it's ready before we
// start playing the mod file.
window.module.onReady = () => {
  window.module.play();

  // ...start my animation etc, which is handled by some Rust code.
}
window.module.load("music.mod");

This all works quite well, and the performance is satisfying (only tested on Chrome on desktop yet.) With the https://mod.haxor.fi/ player, I noted some files (probably more complex ones) were stuttering when I played them on my iPhone - like you write on the web page, the Javascript runtimes can very greatly in speed.


I think we should consider rewriting the inner loops here in Rust, targetting WebAssembly. As you probably already know, webassembly is supported in all the bigger browsers these days and it's providing a lot better performance than JavaScript will ever be able to deliver, because of its compiled and statically typed nature.

So, I think we should think about this. Maybe this is something you have already thought about? Or what do you think? This demo from hellorust.com is what inspired me to start playing around a bit with WebAssembly and Rust, and it's actually really fun and works well. You have to use a nightly Rust compiler at the moment (webassembly is not supported by the stable releases yet) but apart from that, it works pretty much flawlessly and provides a really nice & capable platform for "low-level web stuff" - which .mod playing happens to be.

If you have a particular file handler (S3M? XM?) that is more performance-demanding than the others, this could perhaps be a suitable candidate for trying this idea out.

All in all, thanks for a great tool now already! ๐Ÿ‘

I can help you with the "spaghetti code"!

After reading this, I thought that I could help you with that.

I could clean up the code, following good practices, adding spaces, etc. and then, I'll add a JavaScript linter. At the same time, that would be without changing too much the structure of the code, so you can still find it familiar. After that, I'll send you a pull request.

Are you interested? (I'm asking first because it will take some time to do it; so it would be a waste if you are not interested)

XM player bugs and missing features

  • something causing sample sequences to play off sync in te-2rx.xm, little_man.xm and yuki_satellites.xm (was incorrect envelope loop length)
  • panning sounds kind of wrong but not always
  • noise/crackle just before a song ends.
  • occasional clicks (sounds like broken interpolation over sample loop point?)
  • some volume column effect commands are unimplemented
  • some FT2-specific effect commands are unimplemented
  • instrument vibrato is unimplemented
  • compatibility for versions other than 0x0104 is uncertain

Doesn't work if deployed under a directory

I have tried to deploy this under a directory on Ubuntu Linux (/var/www/html/modplayer) but I only get 404 errors

"GET /js/pt.js HTTP/1.1" 404
"GET /style.css HTTP/1.1" 404
"GET /js/ft2.js HTTP/1.1" 404
etc...

Is there an easy way to deploy this under a directory?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.