DEV Community

Cover image for Syncopation and a human touch
Anders Marzi Tornblad
Anders Marzi Tornblad

Posted on • Originally published at atornblad.se

Syncopation and a human touch

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 slide
  • Effect 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;
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
Enter fullscreen mode Exit fullscreen mode

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;
        // ...
    }
Enter fullscreen mode Exit fullscreen mode

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;
            // ...
        }
    }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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;
        }
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)