Skip to content

Commit

Permalink
FIXED: spectrum graph didn't work in Safari (#3)
Browse files Browse the repository at this point in the history
FIXED: modplayer-js now works in Edge on Xbox One (apparently Edge didn't like executing ES6 code using realm.js)
IMPROVED: moved main js code into a separate main.js file
  • Loading branch information
warpdesign authored Sep 10, 2018
1 parent 7d683d9 commit d6090f4
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 243 deletions.
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@ For a demo, head over [here](https://warpdesign.github.io/modplayer-js/).

modplayer-js requires a browser that supports the [ScriptProcessNode API](https://developer.mozilla.org/en-US/docs/Web/API/ScriptProcessorNode) and will make use of the new [AudioWorklet API](https://developers.google.com/web/updates/2017/12/audio-worklet]) if it's detected (as I'm publishing it only Chrome supports it).

modplayer-js has been tested on:
The native `AudioWorklet` API is used in these browsers:

- Safari 11 (OSX & iOS)
- Firefox
- Chrome
- Edge
- Chrome 70 (OSX, Windows)

Modplayer-js will fall back to the deprecated `ScriptProcessorNode` API in these browsers:

- Safari 11.1.2 (OSX & iOS)
- Firefox 62.0 (OSX)
- Edge 42.17134.4071.0 (Xbox One, stuttering audio)
- Edge 42 17134.1.0 (Windows 10, stuttering audio)

# What's implemented

- 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
Expand All @@ -31,8 +36,9 @@ Most note effects should be supported, including extended ones. Only effect not
ModPlayer JS makes use of the following piece of software:

- The User Interface is built using [Material Design Lite](https://getmdl.io)
- The [AudioWorklet polyfill](https://github.com/GoogleChromeLabs/audioworklet-polyfill) is used to stay compatible with browsers that do not support it yet
- The [AudioWorklet polyfill](https://github.com/GoogleChromeLabs/audioworklet-polyfill) is used to stay compatible with browsers that do not support the audio render thread API
- Spectrum display is based on [Audio DSP Background](https://github.com/acarabott/audio-dsp-playground) by [@Acarabott](https://github.com/acarabott)
- get-float-time-domain-data was written by [github.com/mohayonao](https://github.com/mohayonao/get-float-time-domain-data) (needed for Safari)

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.

Expand Down
241 changes: 5 additions & 236 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,243 +3,13 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8">
<!--webaudio polyfills-->
<script type="text/javascript" src="js/get-float-time-domain-data.min.js"></script>
<script type="text/javascript" src="js/audioworklet-polyfill.js"></script>
<!--some usefull js stuff-->
<script type="text/javascript" src="js/utils.js"></script>
<script type="text/javascript" src="js/modplayer.js"></script>
<script type="text/javascript">
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'}
],
selectedMod = 0,
prefix = 'audio/',
toast;

function Toast(id) {
this.container = document.getElementById(id);
}

Toast.prototype = {
show(message, timeout = 2750) {
this.container.MaterialSnackbar.showSnackbar({
timeout: timeout,
message: message
});
}
}

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

toast = new Toast('info-snackbar');

document.addEventListener('moduleLoaded', (event) => {
toast.show(`Module loaded: ${moduleList[selectedMod].file}`);

const samples = event.data.samples;
let str = '';
for (let i = 0; i < samples.length; ++i) {
if (samples[i].name.length) {
str += `<li>${samples[i].name}</li>`;
}
}

document.querySelector('.sample-list').innerHTML = str;

document.querySelector('.song-title').innerText = event.data.title;
document.querySelector('.title').innerText = moduleList[selectedMod].file;
document.querySelector('.author').innerText = moduleList[selectedMod].author;
document.querySelector('.song-length').innerText = event.data.length;
document.querySelector('.song-samples').innerText = event.data.samples.length;
document.querySelector('.song-positions').innerText = event.data.positions;
document.querySelector('.song-patterns').innerText = event.data.patterns;

document.querySelector('#loader').classList.remove('is-active');

document.querySelectorAll('.controls button').forEach((button) => {
button.style.display = 'inline-block';
});

togglePlayButton();

if (event.data.wasPlaying) {
togglePlay();
}
});

var modNav = document.querySelector('.nav-module'),
options = '';

moduleList.forEach((module, i) => {
options += `<a onclick="loadModule(${i});return false;" href="#" class="mdl-navigation__link mod_${i} `;
if (i === selectedMod) {
options += ' selected';
}
options += `">${module.file}</a>`;
});

modNav.innerHTML = options;

componentHandler.upgradeDom();

document.addEventListener('keyup', (e) => e.keyCode === 32 && togglePlay());

document.querySelector('.mdl-card__title-text').addEventListener('click', () => {
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);
}
}

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)";

ctx.beginPath();

var sliceWidth = canvas.width * 1.0 / bufferLength;
var x = 0;

for (var i = 0; i < bufferLength; i++) {

var v = amplitudeArray[i] / 128.0;
var y = v * canvas.height / 2;

if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}

x += sliceWidth;
}

ctx.lineTo(canvas.width, canvas.height / 2);
ctx.stroke();
}

effects.push(drawBars,drawOscillo);

ModPlayer.init({
canvas: canvas
}).then(() => {
loadModule(selectedMod, false);
}).catch((err) => {
toast.show(`Error loading module: ${err}`);
});
}

function togglePlayButton() {
document.querySelector('button.play i').innerText = ModPlayer.playing && 'pause' || 'play_arrow';
}

function togglePlay() {
ModPlayer.play();
togglePlayButton();
}

function stop() {
ModPlayer.stop();
togglePlayButton();
}

function loadModule(moduleIndex, hideDrawer = true) {
var moduleName = moduleList[moduleIndex].file;

selectedMod = moduleIndex;

if (ModPlayer.ready && moduleName) {
// I guess that's the best way to programmatically hide the drawer
// since MDL does not provide any API to do that
if (hideDrawer) {
document.querySelector('.mdl-layout__obfuscator').click();
}

document.querySelector('#loader').classList.add('is-active');

document.querySelectorAll('.controls button').forEach((button) => {
button.style.display = 'none';
});

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)
.catch(err => {
toast.show(`Error loading module: ${err}`);
});
}
}

</script>
<script type="text/javascript" src="js/main.js"></script>

<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-deep_purple.min.css">
Expand Down Expand Up @@ -294,7 +64,6 @@
}
</style>
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
<script src="js/audioworklet-polyfill.js"></script>
</head>
<body>
<!-- Always shows a header, even in smaller screens. -->
Expand Down
1 change: 1 addition & 0 deletions js/get-float-time-domain-data.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d6090f4

Please sign in to comment.