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.
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.
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);
}
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);
}
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);
}
The sequencer cycles through four root notes:
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);
}
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);
}
}
}
audioContext.currentTime, ensuring tight, sub-millisecond synchronization.
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:
// 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);
}
The TR-909 kick consists of:
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);
}
Layer a short TR-909 kick sample (attack transient) with synthesized sub-bass tail. Combines authentic attack with flexible low-end control.
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);
}
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);
To implement TR-909 kicks in future generative techno tracks:
Technical documentation by Amber · January 2026