Syncopation and a human touch

#retrocomputing #javascript #demoscene #web-audio

Written by Anders Marzi Tornblad

Published on dev.to

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 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;
Changes to performTick and effect methods of the Channel class

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;
        }
    }
Changes to the play method of the Channel class

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;
        // ...
    }
Changing the case SET_VOLUME in the effect method of the Channel class

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;
            // ...
        }
    }
Final implementation of Delay note

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
Storing the length of the song in the Mod class
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;
        }
Restarting from the beginning of the song

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: