Skip to main content

Overview

LoFi Engine uses a sophisticated procedural generation system to create unique LoFi tracks in real-time. The system is based on music theory principles and implemented using Tone.js, a powerful Web Audio framework.
Every playback session generates a unique track—no two listening experiences are exactly the same.

Music Theory Foundation

The music generation system is built on three core concepts:

Major Scale

7-note diatonic scale: C, D, E, F, G, A, B

Chord Progressions

Movement between I, ii, iii, IV, V, vi, vii chords

Voicing

How chord notes are distributed across octaves

System Architecture

The music generation happens in three stages:
1

Chord Progression Generation

Generate a harmonic sequence using music theory rules
2

Voicing & Melodic Generation

Create piano patterns and drum rhythms
3

Audio Synthesis & Effects

Apply filters, stereo effects, and play samples

Chord Progression System

Major Scale Definition

The system uses the major scale as its foundation:
src/lib/engine/Chords/MajorScale.ts
export const singleOct = [0,2,4,5,7,9,11];
export const doubleOct = [...singleOct, ...singleOct.map(n=>n+12)];
export const tripleOct = [...doubleOct, ...singleOct.map(n=>n+24)];
export const fiveToFive = [
  ...singleOct.map(n=>n-12).slice(4), 
  ...singleOct, 
  ...singleOct.map(n=>n+12).slice(0,5)
];
  • singleOct: Major scale intervals in semitones [0,2,4,5,7,9,11]
  • doubleOct: Two octaves for wider range
  • tripleOct: Three octaves for bass to treble
  • fiveToFive: Five notes below to five above for voicing

Key Selection

LoFi Engine supports all 12 chromatic keys:
src/lib/engine/Chords/Keys.ts
const keys = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"];

Chord Definitions

Each chord in the major scale has specific intervals and allowed progressions:
src/lib/engine/Chords/Chords.ts
const I = new Chord(
    1,
    [0,4,7,11,14,17,21],  // Major 7th voicing
    toChordIdxs([2,3,4,5,6,7])  // Can go to ii, iii, IV, V, vi, vii
);

const ii = new Chord(
    2,
    [0,3,7,10,14,17,21],  // Minor 7th voicing
    toChordIdxs([3,5,7])  // Can go to iii, V, vii
);

const V = new Chord(
    5,
    [0,4,7,10,14,17,21],  // Dominant 7th voicing
    toChordIdxs([1,3,6])  // Can go to I, iii, vi
);
  • degree: Roman numeral position in scale (I = 1, V = 5, etc.)
  • intervals: Semitone offsets for each note in the chord
  • nextChordIdxs: Valid next chords (based on music theory)

Chord Progression Generation

The system generates progressions by randomly walking through valid chord changes:
src/lib/engine/Chords/ChordProgression.ts
class ChordProgression {
    static generate(length) {
        if(length < 2)
            return null;

        const progression = [];
        let chord = Chords[Math.floor(Math.random()*Chords.length)];
        
        for(let i = 0; i < length; i++) {
            progression.push(new Chord(
                chord.degree,
                [...chord.intervals],
                [...chord.nextChordIdxs]));
            chord = Chords[chord.nextChordIdx()];
        }
        
        return progression;
    }
}
1

Start with random chord

Pick any chord from I, ii, iii, IV, V, vi, vii
2

Follow music theory rules

Each chord knows which chords can come next
3

Build progression

Repeat for desired length (typically 4-8 chords)

Dynamic Voicing

Each chord generates a unique voicing (note arrangement):
src/lib/engine/Chords/Chord.ts
generateVoicing(size) {
    if(size<3)
        return this.intervals.slice(0,3);
    let voicing = this.intervals.slice(1,size);
    voicing.sort(() => Math.random()-0.5);  // Randomize
    for(let i = 1; i<voicing.length; i++) {
        while(voicing[i] < voicing[i-1]){
            voicing[i] += 12;  // Move up an octave
        }
    }
    voicing.unshift(0);  // Add root note
    return voicing;
}
This creates variation by rearranging chord notes across octaves while keeping the root note at the bottom.

Instrument System

Piano

The piano uses sampled audio files processed through effects:
src/lib/engine/Piano/Piano.ts
import * as Tone from 'tone';
import Samples from './Samples';

const lpf = new Tone.Filter(1000, "lowpass");
const sw = new Tone.StereoWidener(0.5);

class Piano {
    constructor(cb) {
        this.sampler = new Tone.Sampler(Samples, () => {
            cb();
        }).chain(lpf,sw,Tone.Master);
    }
}
  • Lowpass Filter (1000 Hz): Removes harsh high frequencies for warmer tone
  • Stereo Widener (0.5): Adds spatial width to the sound
  • Chain: Routes signal through effects to master output

Drums

Each drum element is a separate sampler with unique effects:
src/lib/engine/Drums/Kick.ts
const samplePath = `assets/engine/DrumSamples/kick.mp3`;
const samples = {C4: samplePath};
const vol = new Tone.Volume(-3);

class Kick {
    constructor(cb) {
        this.sampler = new Tone.Sampler(samples, () => {
            cb();
        }).chain(vol,Tone.Master);
    }
}
Low-end punch with -3dB volume control.

Melodic Generation

Interval Weighting

Not all chord tones are played equally—some intervals sound more “LoFi”:
src/lib/engine/Chords/IntervalWeights.ts
const intervalWeights = [0.10, 0.30, 0.20, 0.15, 0.15, 0.025, 0.025, 0.05];
  • Root (0.10): Stable but not overused
  • 3rd (0.30): Defines major/minor quality—played often
  • 7th (0.20): Adds jazzy color
  • Extensions (0.025): 9ths, 11ths for sophisticated harmony
  • 5th (0.15): Consonant support

Mode Generation

Each chord can generate its mode (scale) for melodic improvisation:
src/lib/engine/Chords/Chord.ts
generateMode() {
    return this.intervals.map(n => {
        if(n>=12)
            return n-12;  // Bring down to single octave
        else
            return n;
    });
}

Audio Processing Pipeline

The complete signal flow:
1

Sample Playback

Tone.Sampler loads and plays audio files
2

Filtering

Lowpass filters remove high frequencies
3

Volume Control

Individual volume nodes balance instruments
4

Stereo Processing

Stereo wideners create spatial depth
5

Master Output

Combined signal sent to speakers/headphones
SamplerLPF(1000Hz) → StereoWidth(0.5) → Master

Randomization & Variation

The system creates variety through:

Random Chord Selection

Each progression starts with a random chord

Weighted Progressions

Some progressions are more likely (V→I vs V→iii)

Dynamic Voicings

Notes rearranged across octaves each time

Interval Weighting

Melodic notes chosen by probability

Performance Optimization

Audio generation happens in real-time. The system is optimized to minimize CPU usage while maintaining smooth playback.

Efficient Scheduling

  • Tone.js schedules events ahead of time
  • Samples are preloaded (callback on load)
  • Minimal garbage collection during playback

Sample Management

All samples are loaded once at startup:
new Tone.Sampler(Samples, () => {
    cb();  // Callback when ready
})

Future Enhancements

Planned improvements to the music generation system:
  • More sophisticated chord voicing algorithms
  • Bass line generation
  • Additional instrument types
  • User-adjustable music parameters
  • Machine learning-based pattern variation

References

Source Code

View the engine implementation

Tone.js Docs

Learn about the audio framework

Build docs developers (and LLMs) love