Implementing looping and the first effects
Tags: webaudio, javascript, retrocomputing
Published: 2023-06-14 05:45:00

## Looping samples

Currently, the player can only play "one-off" samples, which means that they are played once and then stopped. This means that looping samples don't work properly. In the instrument data, there are two fields called `repeatOffset` and `repeatLength`. These are used to define the loop points of the sample. For samples without looping, `repeatOffset` is set to 0 and `repeatLength` is set to 0 or 2, depending on which version of the MOD format is used. I add a new member variable to the `Instrument` class to check if the sample is looped: `isLooped`, and use that in the `Channel` class to determine if the sample should be looped or not.

```javascript
class Instrument {
    constructor(modfile, index, sampleStart) {
        const data = new Uint8Array(modfile, 20 + index * 30, 30);
        const nameBytes = data.slice(0, 21).filter(a => !!a);
        this.name = String.fromCodePoint(...nameBytes).trim();
        this.length = 2 * (data[22] * 256 + data[23]);
        this.finetune = data[24]; // Signed 4 bit integer
        if (this.finetune > 7) this.finetune -= 16;
        this.volume = data[25];
        this.repeatOffset = 2 * (data[26] * 256 + data[27]);
        this.repeatLength = 2 * (data[28] * 256 + data[29]);
        this.bytes = new Int8Array(modfile, sampleStart, this.length);
        this.isLooped = this.repeatOffset != 0 || this.repeatLength > 2;
    }
}
```


```javascript
    nextOutput() {
        if (!this.instrument || !this.period) return 0.0;
        const sample = this.instrument.bytes[this.sampleIndex | 0];

        this.sampleIndex += this.sampleSpeed;

        if (this.instrument.isLooped) {
            if (this.sampleIndex >= this.instrument.repeatOffset + this.instrument.repeatLength) {
                this.sampleIndex = this.instrument.repeatOffset;
            }
        }
        else if (this.sampleIndex >= this.instrument.length) {
            return 0.0;
        }

        return sample / 256.0;
    }
```

## Finding the most important effects

The MOD format defines a lot of effects, but some of them are not used very often in demo songs. My goal for this article series is to be able to play three songs:

- [Musiklinjen](https://modarchive.org/index.php?request=view_by_moduleid&query=101789): The song from Enigma by Phenomena
- [Livin' Instanity](https://modarchive.org/index.php?request=view_by_moduleid&query=48324): The first song from Arte by Sanity
- [Elekfunk](https://modarchive.org/index.php?request=view_by_moduleid&query=41529): The second song from Arte by Sanity

To find out which effects are used by those three songs, I refactor the `effect` method of the `Channel` class to log the effect id of each unimplemented effect. After reading some more about the different effect IDs, I also find out that the `0x0E` effect is used as a prefix for extended effects, so that is also added.

```javascript
// Declared at the top of the file, outside of the class definition
const unimplementedEffects = new Set();
const EXTENDED = 0x0e;
const SET_SPEED = 0x0f;

// Updated effect method of the Channel class
    effect(raw) {
        let id = raw >> 8;
        let data = raw & 0xff;

        if (id == EXTENDED) {
            // Effect ID can be E0..EF
            id = (id << 4) | (data >> 4);
            data = data & 0x0f;
        }

        if (id == SET_SPEED) {
            if (data >= 1 && data <= 31) {
                this.worklet.setTicksPerRow(data);
            }
            else {
                this.worklet.setBpm(data);
            }
        }
        else {
            if (!unimplementedEffects.has(id)) {
                unimplementedEffects.add(id);
                console.log('Unimplemented effect ' + id.toString(16));
            }
        }
    }
```

After running the player with the three songs, I get the following list of effects to implement, and I can add definitions for them to the player-worklet.js file:

- Effect 0 – Arpeggio
- Effect 1 – Slide up
- Effect 2 – Slide down
- Effect 3 – Tone portamento
- Effect 4 – Vibrato
- Effect 5 – Tone portamento + volume slide
- Effect 6 – Vibrato + volume slide
- Effect 9 – Sample offset
- Effect A – Volume slide
- Effect C – Set volume
- Effect D – Pattern break
- Effect E9 – Retrigger note
- Effect ED – Delay note
- ~~Effect F – Set speed~~ _(done)_

```javascript
const ARPEGGIO = 0x00;
const SLIDE_UP = 0x01;
const SLIDE_DOWN = 0x02;
const TONE_PORTAMENTO = 0x03;
const VIBRATO = 0x04;
const TONE_PORTAMENTO_WITH_VOLUME_SLIDE = 0x05;
const VIBRATO_WITH_VOLUME_SLIDE = 0x06;
const SAMPLE_OFFSET = 0x09;
const VOLUME_SLIDE = 0x0A;
const SET_VOLUME = 0x0C;
const PATTERN_BREAK = 0x0D;
const EXTENDED = 0x0e;
const SET_SPEED = 0x0f;
const RETRIGGER_NOTE = 0xe9;
const DELAY_NOTE = 0xed;
```

In the documentation I have found, the effects are described in a very technical way, but with a lot of details missing. Also, it is impossible to know exactly what the effects are supposed to sound like without hearing them in action. Fortunately, I have found a great source of information in the form of a [**YouTube playlist**](https://www.youtube.com/playlist?list=PLVoRT-Mqwas9gvmCRtOusCQSKNQNf6lTc) by demoscene musician [Alex Löfgren _(Wasp)_](https://demozoo.org/sceners/11697/) where a lot of the effects are explained in detail, and shown in action. Throughout the implementation of the effects, I'm' listening to the examples in the playlist to make sure that I get them right, and also making minimal test songs in ProTracker to try out the effects.

In this article, I implement four of the effects, and I leave the rest for future articles:

- Effect C – Set volume
- Effect 9 – Sample offset
- Effect D – Pattern break
- Effect A – Volume slide

## Effect C – Set volume

So far, all samples are playing at full volume, which is not correct. First of all, each instrument has a `volume` property, which is a value between 0 and 64 _(the maximum volume for audio in the Amiga Paula chip)_. Second, the **Set volume** effect can be used to override the volume of the instrument. It can be set when a note is triggered, or changed at any time during the note. To implement this, I add a `volume` property to the `Channel` class, update the `nextOutput` method to use the channel volume instead of the instrument volume, and update the `effect` method to set the volume when the effect is encountered.

```javascript
    constructor(worklet) {
        this.worklet = worklet;
        this.instrument = null;
        this.playing = false;
        this.period = 0;
        this.sampleSpeed = 0.0;
        this.sampleIndex = 0;
        this.volume = 64;
    }
```


```javascript
    nextOutput() {
        if (!this.instrument || !this.period) return 0.0;
        const sample = this.instrument.bytes[this.sampleIndex | 0];

        this.sampleIndex += this.sampleSpeed;

        if (this.instrument.isLooped) {
            if (this.sampleIndex >= this.instrument.repeatOffset + this.instrument.repeatLength) {
                this.sampleIndex = this.instrument.repeatOffset;
            }
        }
        else if (this.sampleIndex >= this.instrument.length) {
            return 0.0;
        }

        return sample / 256.0 * this.volume / 64;
    }
```


```javascript
        switch (id) {
            case SET_VOLUME:
                this.volume = data;
                break;
            case SET_SPEED:
                if (data >= 1 && data <= 31) {
                    this.worklet.setTicksPerRow(data);
                }
                else {
                    this.worklet.setBpm(data);
                }
                break;
            default:
                if (!unimplementedEffects.has(id)) {
                    unimplementedEffects.add(id);
                    console.log('Unimplemented effect ' + id.toString(16));
                }
                break;
        }
```

## Effect 9 – Sample offset

This effect makes the player set the position in the sample to a specific value. The value is the upper eight bits of the sample offset, so it must be multiplied by 256 to get the actual offset. The effect can be used to jump forward and backward in a sample while it is playing, or to start from a selected position when triggering a note. Implementing this is very simple, I just need to update the `effect` method to set the sample index when the effect is encountered.

```javascript
            case SAMPLE_OFFSET:
                this.sampleIndex = data * 256;
                break;
```

## Effect D – Pattern break

This effect makes the player jump to a specific row in the next pattern. The jump takes place _after the current row has finished playing_, so the effect is not triggered immediately. The value of the effect is the row number to start playing in the next pattern, in binary coded decimal notation. The first hexadecimal digit is the "tens" of the row number, and the second hexadecimal digit is the "ones". To make this work, I add a `patternBreak` member variable of the `PlayerWorklet` class that I initialize to `false` in the constructor, and I add a `setPatternBreak` method , that I call from the `effect` method when the effect is encountered.

```javascript
    constructor() {
        super();
        this.port.onmessage = this.onmessage.bind(this);
        this.mod = null;
        this.channels = [ new Channel(this), new Channel(this), new Channel(this), new Channel(this) ];
        this.patternBreak = false;
    }
```


```javascript
    setPatternBreak(row) {
        this.patternBreak = row;
    }
```


```javascript
    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;
        }

        // The rest of the nextRow method is unchanged
```


```javascript
            case PATTERN_BREAK:
                const row = (data >> 4) * 10 + (data & 0x0f);
                this.worklet.setPatternBreak(row);
                break;
```

## Effect A – Volume slide

The Volume slide effect works by repeatedly changing the volume of the channel, a number of times per row. Since the number of times is the same as the `ticksPerRow` value set by the Set speed effect, there is already an idea of "ticks" in the playback code. Unfortunately, the player doesn't keep track of the current tick, so I need to add a `tick` member variable and a `nextTick` method to the `PlayerWorklet` class. Instead of keeping track of when to move to the next row, I will keep track of when to move to the next tick, and call `nextRow` when the tick reaches the `ticksPerRow` value. When all this is done, I can implement the Volume slide effect, and other effects that use the tick count.

```javascript
// Only the changed methods are shown here
    onmessage(e) {
        if (e.data.type === 'play') {
            this.mod = e.data.mod;
            this.sampleRate = e.data.sampleRate;

            this.setBpm(125);
            this.setTicksPerRow(6);

            // Start at the last tick of the pattern "before the first pattern"
            this.position = -1;
            this.rowIndex = 63;
            this.tick = 5;
            this.ticksPerRow = 6;

            // Immediately move to the first row of the first pattern
            this.outputsUntilNextTick = 0;
        }
    }

    setTicksPerRow(ticksPerRow) {
        this.ticksPerRow = ticksPerRow;
    }

    setBpm(bpm) {
        this.bpm = bpm;
        this.outputsPerTick = this.sampleRate * 60 / this.bpm / 4 / 6;
    }

    nextTick() {
        ++this.tick;
        if (this.tick == this.ticksPerRow) {
            this.tick = 0;
            this.nextRow();
        }

        for (let i = 0; i < 4; ++i) {
            this.channels[i].performTick();
        }
    }

    nextOutput() {
        if (!this.mod) return 0.0;

        if (this.outputsUntilNextTick <= 0) {
            this.nextTick();
            this.outputsUntilNextTick += this.outputsPerTick;
        }

        this.outputsUntilNextTick--;

        const rawOutput = this.channels.reduce((acc, channel) => acc + channel.nextOutput(), 0.0);
        return Math.tanh(rawOutput);
    }
```


```javascript
// Only the changed methods are shown here
    nextOutput() {
        if (!this.instrument || !this.period) return 0.0;
        const sample = this.instrument.bytes[this.sampleIndex | 0];

        this.sampleIndex += this.sampleSpeed;

        if (this.instrument.isLooped) {
            if (this.sampleIndex >= this.instrument.repeatOffset + this.instrument.repeatLength) {
                this.sampleIndex = this.instrument.repeatOffset;
            }
        }
        else if (this.sampleIndex >= this.instrument.length) {
            return 0.0;
        }

        return sample / 256.0 * this.currentVolume / 64;
    }

    performTick() {
        if (this.volumeSlide) {
            this.currentVolume += this.volumeSlide;
            if (this.currentVolume < 0) this.currentVolume = 0;
            if (this.currentVolume > 64) this.currentVolume = 64;
        }
    }

    effect(raw) {
        this.volumeSlide = 0;

        if (!raw) return;

        let id = raw >> 8;
        let data = raw & 0xff;

        if (id == EXTENDED) {
            id = (id << 4) | (data >> 4);
            data = data & 0x0f;
        }

        switch (id) {
            case SET_VOLUME:
                this.volume = data;
                this.currentVolume = this.volume;
                break;
            case VOLUME_SLIDE:
                if (data & 0xf0) {
                    this.volumeSlide = data >> 4;
                }
                else if (data & 0x0f) {
                    this.volumeSlide = -(data & 0x0f);
                }
                break;
        // The rest of the effect method is unchanged
```

## Conclusion

Effects are a very important part of the musical experience of MOD files, and after just these four effects, the reference MODs are starting to sound much better. In the next couple of article, I will implement the remaining effects, and then I will move on to implementing more things that will help me use MOD files in a demo.

You can try this solution at [atornblad.github.io/js-mod-player](https://atornblad.github.io/js-mod-player).

The latest version of the code is always available in [the GitHub repository](https://github.com/atornblad/js-mod-player).