← Back to ASCII Techno

Technical Documentation

Audio Architecture & TR-909 Integration Guide

Overview

This document details the audio synthesis architecture behind ASCII Techno, a generative Berlin dark techno piece built entirely with the Web Audio API. No samples, no loops — pure oscillators, filters, and envelope shaping.

Key Decision: Web Audio API was chosen over MIDI or sample playback to enable real-time synthesis with precise timing control and zero external dependencies. Every sound is generated procedurally at playback time.

Sound Generation Architecture

Audio Context Setup

The foundation is a standard Web Audio API context with master gain control:

this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.audioContext.createGain();
this.masterGain.gain.value = 0.5;
this.masterGain.connect(this.audioContext.destination);

All sound sources route through masterGain for unified volume control.

Kick Drum Synthesis

The kick uses frequency modulation with an exponential pitch sweep to create a punchy, Berlin-style thump:

createKick(time, intensity = 1.0) {
    const osc = this.audioContext.createOscillator();
    const gain = this.audioContext.createGain();

    // Start at 150Hz, sweep down to 40Hz over 0.5 seconds
    osc.frequency.setValueAtTime(150 * intensity, time);
    osc.frequency.exponentialRampToValueAtTime(40, time + 0.5);

    // Amplitude envelope: instant attack, exponential decay
    gain.gain.setValueAtTime(1.0 * intensity, time);
    gain.gain.exponentialRampToValueAtTime(0.01, time + 0.5);

    osc.connect(gain);
    gain.connect(this.masterGain);

    osc.start(time);
    osc.stop(time + 0.5);
}

Parameters

Hi-Hat Synthesis

Hi-hats use white noise filtered through a high-pass filter to create metallic, industrial textures:

createHiHat(time, open = false) {
    // Generate white noise buffer
    const noise = this.audioContext.createBufferSource();
    const buffer = this.audioContext.createBuffer(1,
        this.audioContext.sampleRate * 0.1,
        this.audioContext.sampleRate);
    const data = buffer.getChannelData(0);

    for (let i = 0; i < data.length; i++) {
        data[i] = Math.random() * 2 - 1;
    }

    noise.buffer = buffer;

    // High-pass filter at 8kHz for brightness
    const filter = this.audioContext.createBiquadFilter();
    filter.type = 'highpass';
    filter.frequency.value = 8000;

    const gain = this.audioContext.createGain();
    gain.gain.setValueAtTime(0.3, time);
    gain.gain.exponentialRampToValueAtTime(0.01,
        time + (open ? 0.3 : 0.05));

    noise.connect(filter);
    filter.connect(gain);
    gain.connect(this.masterGain);
}

Two Variants

Bass Synth

Deep sub-bass uses a sawtooth oscillator for harmonic richness:

createBass(time, note = 40, duration = 0.5) {
    const osc = this.audioContext.createOscillator();
    const gain = this.audioContext.createGain();

    osc.type = 'sawtooth';
    osc.frequency.value = note;

    // Quick attack, exponential decay
    gain.gain.setValueAtTime(0, time);
    gain.gain.linearRampToValueAtTime(0.4, time + 0.01);
    gain.gain.exponentialRampToValueAtTime(0.01, time + duration);

    osc.connect(gain);
    gain.connect(this.masterGain);
}

Bass Line Notes

The sequencer cycles through four root notes:

Industrial Noise Stabs

Random percussive accents use bandpass-filtered noise for industrial texture:

createNoiseStab(time, duration = 0.1) {
    // Generate short noise burst
    const noise = this.audioContext.createBufferSource();
    const buffer = this.audioContext.createBuffer(1,
        this.audioContext.sampleRate * duration,
        this.audioContext.sampleRate);
    const data = buffer.getChannelData(0);

    for (let i = 0; i < data.length; i++) {
        data[i] = Math.random() * 2 - 1;
    }

    noise.buffer = buffer;

    // Bandpass filter at random frequency (2-5kHz)
    const filter = this.audioContext.createBiquadFilter();
    filter.type = 'bandpass';
    filter.frequency.value = 2000 + Math.random() * 3000;
    filter.Q.value = 10; // Narrow resonance

    const gain = this.audioContext.createGain();
    gain.gain.setValueAtTime(0.2, time);
    gain.gain.exponentialRampToValueAtTime(0.01, time + duration);

    noise.connect(filter);
    filter.connect(gain);
    gain.connect(this.masterGain);
}

Pattern Sequencing

All sounds are scheduled in advance using the audio context's high-precision timer:

schedulePattern(startTime, duration) {
    const bars = Math.ceil(duration / (this.beatDuration * 4));

    for (let bar = 0; bar < bars; bar++) {
        const barTime = startTime + (bar * this.beatDuration * 4);

        // Four-on-the-floor kick pattern
        for (let beat = 0; beat < 4; beat++) {
            const beatTime = barTime + (beat * this.beatDuration);
            const intensity = beat === 0 ? 1.2 : 1.0; // Accent downbeat
            this.createKick(beatTime, intensity);

            // Off-beat hi-hats
            this.createHiHat(beatTime + this.beatDuration / 2, false);
            if (beat % 2 === 1) {
                this.createHiHat(beatTime + this.beatDuration / 4, true);
            }
        }

        // Evolving bass line
        if (bar % 2 === 0) {
            this.createBass(barTime, bassNotes[bar % 4],
                this.beatDuration * 0.8);
            this.createBass(barTime + this.beatDuration * 1.5,
                bassNotes[(bar + 1) % 4],
                this.beatDuration * 0.4);
        }

        // Random noise stabs (60% probability)
        if (Math.random() > 0.6) {
            this.createNoiseStab(barTime + this.beatDuration *
                (Math.random() * 4), 0.15);
        }
    }
}
Timing Precision: Web Audio API's scheduling is sample-accurate. All events are scheduled relative to audioContext.currentTime, ensuring tight, sub-millisecond synchronization.

TR-909 Kick Integration

The current kick implementation uses pure synthesis. For authentic TR-909 character, here are three concrete approaches to integrate classic drum machine kicks into future tracks:

Approach 1: Authentic TR-909 Samples

Sample Sources

Implementation

// Load TR-909 kick sample
async loadKickSample() {
    const response = await fetch('/samples/tr909-kick.wav');
    const arrayBuffer = await response.arrayBuffer();
    this.kickBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
}

// Play TR-909 kick
createTR909Kick(time, intensity = 1.0) {
    const source = this.audioContext.createBufferSource();
    const gain = this.audioContext.createGain();

    source.buffer = this.kickBuffer;
    gain.gain.value = intensity;

    source.connect(gain);
    gain.connect(this.masterGain);

    source.start(time);
}

Pros & Cons

Approach 2: Physical Modeling Synthesis

TR-909 Kick Architecture

The TR-909 kick consists of:

  1. Bridged-T oscillator (60Hz → 40Hz sweep)
  2. VCA with exponential envelope
  3. Attack transient (click/noise at start)
  4. Tone control (low-pass filter sweep)

Enhanced Synthesis Implementation

createTR909StyleKick(time, intensity = 1.0) {
    // Main oscillator with TR-909 frequency sweep
    const osc = this.audioContext.createOscillator();
    const oscGain = this.audioContext.createGain();

    // TR-909 sweep: 60Hz → 40Hz over 0.4s
    osc.frequency.setValueAtTime(60 * intensity, time);
    osc.frequency.exponentialRampToValueAtTime(40, time + 0.4);

    // TR-909 envelope: fast attack, exponential decay
    oscGain.gain.setValueAtTime(1.0 * intensity, time);
    oscGain.gain.exponentialRampToValueAtTime(0.01, time + 0.5);

    // Attack transient (click)
    const clickOsc = this.audioContext.createOscillator();
    const clickGain = this.audioContext.createGain();

    clickOsc.frequency.value = 1000;
    clickGain.gain.setValueAtTime(0.5, time);
    clickGain.gain.exponentialRampToValueAtTime(0.01, time + 0.01);

    clickOsc.connect(clickGain);
    clickGain.connect(this.masterGain);
    clickOsc.start(time);
    clickOsc.stop(time + 0.01);

    // Tone control (low-pass sweep)
    const filter = this.audioContext.createBiquadFilter();
    filter.type = 'lowpass';
    filter.frequency.setValueAtTime(2000, time);
    filter.frequency.exponentialRampToValueAtTime(80, time + 0.4);
    filter.Q.value = 3; // Resonance for punch

    osc.connect(filter);
    filter.connect(oscGain);
    oscGain.connect(this.masterGain);

    osc.start(time);
    osc.stop(time + 0.5);
}

Pros & Cons

Approach 3: Hybrid (Sample + Synthesis)

Concept

Layer a short TR-909 kick sample (attack transient) with synthesized sub-bass tail. Combines authentic attack with flexible low-end control.

Implementation

createHybridKick(time, intensity = 1.0) {
    // Sample layer (attack transient, 0-50ms)
    const sample = this.audioContext.createBufferSource();
    const sampleGain = this.audioContext.createGain();

    sample.buffer = this.kickTransientBuffer; // Short 909 sample
    sampleGain.gain.value = intensity * 0.7;

    sample.connect(sampleGain);
    sampleGain.connect(this.masterGain);
    sample.start(time);

    // Synthesis layer (sub-bass tail)
    const osc = this.audioContext.createOscillator();
    const oscGain = this.audioContext.createGain();

    osc.frequency.setValueAtTime(60, time + 0.05); // Start after transient
    osc.frequency.exponentialRampToValueAtTime(40, time + 0.5);

    oscGain.gain.setValueAtTime(0, time);
    oscGain.gain.linearRampToValueAtTime(0.6 * intensity, time + 0.05);
    oscGain.gain.exponentialRampToValueAtTime(0.01, time + 0.5);

    osc.connect(oscGain);
    oscGain.connect(this.masterGain);

    osc.start(time);
    osc.stop(time + 0.5);
}

Pros & Cons

Compression & Mastering

For a polished, club-ready sound, add dynamic compression to the master output:

// Add to init() method
this.compressor = this.audioContext.createDynamicsCompressor();
this.compressor.threshold.value = -20;
this.compressor.knee.value = 10;
this.compressor.ratio.value = 8;
this.compressor.attack.value = 0.003;
this.compressor.release.value = 0.25;

this.masterGain.connect(this.compressor);
this.compressor.connect(this.audioContext.destination);
Berlin Techno Sound: Heavy compression (ratio 8:1) with fast attack creates the characteristic "pumping" effect where the kick drives the entire mix.

Next Steps

To implement TR-909 kicks in future generative techno tracks:

  1. Choose your approach based on project constraints:
    • Need authentic sound + no external files? → Physical modeling (Approach 2)
    • Want exact 909 character + okay with samples? → Authentic samples (Approach 1)
    • Need flexibility + willing to tune? → Hybrid (Approach 3)
  2. Add compression to the master chain for professional cohesion
  3. Tune kick-to-bass relationship: ensure kick fundamental (40-60Hz) doesn't clash with bass notes
  4. Experiment with variations: pitch accent (higher kick on downbeat), decay modulation, distortion
  5. Consider sidechain compression: duck bass/synths when kick hits for clarity

References

Technical documentation by Amber · January 2026