Syncopation and a human touch
This is part 6 of the JavaScript MOD Player series. If you haven't read the first part, here it is: Generating sound in modern Web Audio API
The last two effects that I want to implement have to do with note timing. The first is called Retrigger note, and can be used to restart the note from the beginning after a certain number of ticks, and keep repeating it during one row. The second is called Delay note, and is used to delay the start of a note by a certain number of ticks. Both of these effects are used to add some more feeling to the music, and are often added to drum sounds or bass notes.
Effect 0 – Arpeggio(done)Effect 1 – Slide up(done)Effect 2 – Slide down(done)Effect 3 – Tone portamento(done)Effect 4 – Vibrato(done)Effect 5 – Tone portamento + volume slide(done)Effect 6 – Vibrato + volume slideEffect 9 – Sample offset(done)Effect A – Volume slide(done)Effect C – Set volume(done)Effect D – Pattern break(done)- Effect E9 – Retrigger note
- Effect ED – Delay note
Effect F – Set speed(done)
Effect E9 – Retrigger note
To retrigger a note, I need to check each tick if it is divisible by the number of ticks specified in the effect parameter. When it is, I can start the sample from the beginning again by setting the sample position to 0:
performTick() {
if (this.volumeSlide && this.worklet.tick > 0) {
// Unchanged
}
if (this.vibrato) {
// Unchanged
}
else if (this.periodDelta) {
// Unchanged
}
else if (this.arpeggio) {
// Unchanged
}
else if (this.retrigger && (this.worklet.tick % this.retrigger) == 0) {
this.sampleIndex = 0;
}
// Rest of performTick unchanged
}
effect(raw) {
// Add one line of initialization:
this.retrigger = false;
// Everything unchanged apart from one new case in the switch:
switch (id) {
// ...
case RETRIGGER_NOTE:
this.retrigger = data;
break;
Effect ED – Delay note
When the Delay note effect is used, the currently playing note must not be interrupted until the specified number of ticks have passed. To implement this, I need to first prepare by storing the changes of instrument, volume and period in separate variables, and only apply them when the delay has passed. This makes the play(note)
method a bit more complicated, but it is still quite readable:
play(note) {
// Store the changes for later:
this.setInstrument = false;
this.setVolume = false;
this.setPeriod = false;
if (note.instrument) {
this.setInstrument = this.worklet.mod.instruments[note.instrument - 1];
this.setVolume = this.setInstrument.volume;
}
this.setSampleIndex = false;
this.setCurrentPeriod = false;
if (note.period) {
this.setPeriod = note.period - (this.setInstrument || this.instrument).finetune;
this.setCurrentPeriod = true;
this.setSampleIndex = 0;
}
this.effect(note.effect);
if (this.setInstrument) {
this.instrument = this.setInstrument;
}
if (this.setVolume !== false) {
this.volume = this.setVolume;
this.currentVolume = this.volume;
}
if (this.setPeriod) {
this.period = this.setPeriod;
}
if (this.setCurrentPeriod) {
this.currentPeriod = this.period;
}
if (this.setSampleIndex !== false) {
this.sampleIndex = this.setSampleIndex;
}
}
After this change, the volume control is a bit broken, but just a small change in the effect
method is needed to fix it:
effect(raw) {
// ...
case SET_VOLUME:
this.setVolume = data;
break;
// ...
}
To finally implement the delay, I need to do some small changes to the performTick
, play
and effect
methods:
performTick() {
if (this.volumeSlide && this.worklet.tick > 0) {
// Unchanged
}
if (this.vibrato) {
// Unchanged
}
else if (this.periodDelta) {
// Unchanged
}
else if (this.arpeggio) {
// Unchanged
}
else if (this.retrigger && (this.worklet.tick % this.retrigger) == 0) {
// Unchanged
}
else if (this.delayNote === this.worklet.tick) {
this.instrument = this.setInstrument;
this.volume = this.setVolume;
this.currentVolume = this.volume;
this.period = this.setPeriod;
this.currentPeriod = this.period;
this.sampleIndex = 0;
}
// Rest of method unchanged
}
play(note) {
this.setInstrument = false;
this.setVolume = false;
this.setPeriod = false;
this.delayNote = false;
if (note.instrument) {
// Unchanged
}
this.setSampleIndex = false;
this.setCurrentPeriod = false;
if (note.period) {
// Unchanged
}
this.effect(note.effect);
// If note is delayed, do nothing right now
if (this.delayNote) return;
// Rest of method unchanged
}
effect(raw) {
// Add one more line of initialization:
this.delayNote = false;
// Everything unchanged apart from one new case in the switch, and
// a small but important change to SAMPLE_OFFSET:
switch (id) {
// ...
case DELAY_NOTE:
this.delayNote = data;
break;
// ...
case SAMPLE_OFFSET:
this.setSampleIndex = data * 256;
break;
// ...
}
}
Fixing an end-of-song bug
When listening to a few songs from start to end, I noticed that some songs would repeat a single pattern over and over again at the end. This is because I haven't stored the length of the song anywhere, so the player doesn't know when to stop. To fix this, I need to add a length
member variable to the Mod
class, and check it in the nextRow
method of the PlayerWorklet
class:
export class Mod {
constructor(modfile) {
// Store the song length
this.length = new Uint8Array(modfile, 950, 1)[0];
// The rest of the class is unchanged
class PlayerWorklet extends AudioWorkletProcessor {
// ...
nextRow() {
++this.rowIndex;
if (this.patternBreak !== false) {
this.rowIndex = this.patternBreak;
++this.position;
this.patternBreak = false;
}
else if (this.rowIndex == 64) {
this.rowIndex = 0;
++this.position;
}
if (this.position >= this.mod.length) {
this.position = 0;
}
Try it out
You can try the final version of the player here
Conclusion
With this, the player is now able to play all the songs that I set out to support. I know there are lot of effects that I haven't implemented yet, but I'm happy with the progress so far. I've learned a lot about the inner workings of the MOD format, and I've also learned a lot about the Web Audio API. I'm looking forward to implementing more effects, but I'll take a break from that for now.
The main reason for making this player is to use it in Artsy, my JavaScript remake of Arte by Sanity. I will use the player for two things: First, I will use it to play the music in the demo. Second, I will use it as a timing source. Because of this, I will need to implement a way to make the player trigger events at certain points in time, driven by the music. That will be the focus of the next article, which will be the final one in this series. I hope to publish it in about a week. Until then, happy modding!
You can try this solution at atornblad.github.io/js-mod-player. The latest version of the code is always available in the GitHub repository.
Articles in this series: