You don't need to know how to solder. You don't need a Eurorack case, a Moog Sub37, a vintage Roland SH-101 in powder blue, or a room full of patch cables that cost more than a semester of community college. You don't need to have grown up in Düsseldorf or spent three years as Giorgio Moroder's assistant or been in a Brooklyn loft in 1983 with a beatbox and a Roland TR-808 that everyone thought sounded like a broken drum set.
What you need is curiosity about sound and the willingness to feel like an idiot for about two hours while you figure out why it makes noise at all.
Everything else follows from that.
A synthesizer is a machine for making sound from electricity. That's it. The complexity people associate with synthesizers — the patch cables, the knobs, the modular systems that cost more than a car — is all in service of that one idea: take electrical signals and shape them until they make the sound you want.
Sound is vibration. Vibration is waves. Waves have three properties that matter: frequency, which you hear as pitch; amplitude, which you hear as volume; and shape, which you hear as timbre — the difference between a piano and a violin playing the same note. A synthesizer generates waves and gives you tools to manipulate those three properties. That's the whole thing. Kraftwerk understood this. Moroder understood this. The engineers at Moog understood this. The kid in 1982 who figured out that an 808's bass drum could be tuned to a pitch and used as a bass line understood this, and that understanding changed popular music permanently.
You're going to understand it too.
The Building Blocks
Every synthesizer — from a 1970s Moog modular the size of a refrigerator to a softsynth plugin running in your DAW to the thing we're about to build — has the same core architecture. Learn these four pieces and you understand every synthesizer ever made.
The oscillator is the sound source. It generates the raw wave at a given frequency. The shape of that wave determines its character before you've touched anything else. A sine wave is a pure tone — single frequency, no harmonics, clean and slightly boring, what a tuning fork makes. A square wave flips between full-on and full-off, rich in odd harmonics, with that hollow reedy quality that runs through early video game music and most of post-punk. A sawtooth ramps up, drops instantly, ramps again — the most harmonically rich of the basic shapes, full of both odd and even harmonics, which is why it's the wave that makes synthesizers sound like synthesizers. The Kraftwerk leads. The Moroder strings. The MGMT arpeggios on Time to Pretend that sound like 1983 dreamed them and 2007 made them real. A triangle wave sits somewhere between sine and square, softer harmonics, gentler presence.
The filter is where personality lives. You take the harmonically rich oscillator output and cut or boost specific frequency ranges. The most important filter in synthesis history is the low-pass filter, which lets low frequencies through and cuts high ones. Sweep it open and you get that classic synthesizer swell — what the 303 bassline does, what the acid house producers discovered could make people lose their minds at 130 BPM. The filter has a cutoff frequency, which is where it starts cutting, and a resonance control, which emphasizes the frequencies right at the cutoff — at high settings, that's the characteristic synth squeal.
The amplifier controls volume over time. Not much to it on its own — it's the gain stage. What makes it interesting is what controls it.
The envelope is time. It's the shape of a sound from start to finish, described in four parameters: attack, how long it takes to reach full volume after a note triggers; decay, how long it takes to fall from the attack peak to the sustain level; sustain, the level it holds while the note is held; release, how long it takes to fade after the note is released. A piano has a fast attack, fast decay, no sustain, and a medium release. A string section has a slow attack, slow decay, high sustain, and a slow release. Change those four numbers and you change the entire character of a sound. The 808's iconic kick drum is a sine wave with a very fast attack, instant decay, no sustain, no release — plus a pitch envelope that drops fast. That's the whole secret. That's what changed hip hop.
ADSR Visualization:
Volume
|
| /\
| / \
| / \______
| / \
| / \
|/ \___
+--A---D---S--------R-----> Time
Let's Build One
Here's where we stop talking about synthesizers and start building one. In Python, in your browser, with no hardware required. The first version will make sound. The second version will make something you might actually want to listen to.
Step 1: The Oscillator in Python
import numpy as np
import sounddevice as sd
# Sample rate: 44100 Hz is CD quality
# This is how many samples per second we generate
SAMPLE_RATE = 44100
DURATION = 2.0 # seconds
def oscillator(frequency, waveform='sine', duration=DURATION, sample_rate=SAMPLE_RATE):
"""
Generate a basic waveform.
frequency: pitch in Hz (440 = A4, concert pitch)
waveform: 'sine', 'square', 'sawtooth', 'triangle'
"""
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
if waveform == 'sine':
wave = np.sin(2 * np.pi * frequency * t)
elif waveform == 'square':
wave = np.sign(np.sin(2 * np.pi * frequency * t))
elif waveform == 'sawtooth':
# Sawtooth: ramps from -1 to 1 each cycle
wave = 2 * (t * frequency - np.floor(t * frequency + 0.5))
elif waveform == 'triangle':
wave = 2 * np.abs(2 * (t * frequency - np.floor(t * frequency + 0.5))) - 1
return wave.astype(np.float32)
# Make some noise
wave = oscillator(440, waveform='sawtooth')
sd.play(wave, SAMPLE_RATE)
sd.wait()
Install the dependencies first:
pip install numpy sounddevice
Run that. You'll hear a raw sawtooth wave at A4 (440 Hz). It sounds harsh and buzzy. That's correct. That's the raw material. Everything else is shaping it.
Step 2: The Envelope
def adsr_envelope(duration, attack, decay, sustain_level, release,
sample_rate=SAMPLE_RATE):
"""
Returns an amplitude envelope array.
All time values in seconds. sustain_level is 0.0 to 1.0.
"""
total_samples = int(duration * sample_rate)
envelope = np.zeros(total_samples)
a_samples = int(attack * sample_rate)
d_samples = int(decay * sample_rate)
r_samples = int(release * sample_rate)
s_samples = total_samples - a_samples - d_samples - r_samples
s_samples = max(0, s_samples)
# Attack: ramp up
envelope[:a_samples] = np.linspace(0, 1, a_samples)
# Decay: ramp down to sustain
envelope[a_samples:a_samples+d_samples] = np.linspace(1, sustain_level, d_samples)
# Sustain: hold
envelope[a_samples+d_samples:a_samples+d_samples+s_samples] = sustain_level
# Release: ramp down to zero
envelope[total_samples-r_samples:] = np.linspace(sustain_level, 0, r_samples)
return envelope.astype(np.float32)
# Sawtooth wave shaped by an envelope
wave = oscillator(440, waveform='sawtooth', duration=2.0)
env = adsr_envelope(
duration=2.0,
attack=0.01, # fast attack
decay=0.1, # quick decay
sustain_level=0.6,
release=0.3 # smooth release
)
shaped = wave * env
sd.play(shaped, SAMPLE_RATE)
sd.wait()
Same frequency. Same waveform. Completely different character. The envelope is doing all of it.
Step 3: The Filter
from scipy import signal
def low_pass_filter(wave, cutoff_hz, resonance=0.7, sample_rate=SAMPLE_RATE):
"""
Simple low-pass filter.
cutoff_hz: filter cutoff frequency
resonance: Q factor (higher = more resonant peak at cutoff)
"""
# Normalize cutoff to Nyquist frequency
nyquist = sample_rate / 2
normalized_cutoff = cutoff_hz / nyquist
normalized_cutoff = np.clip(normalized_cutoff, 0.001, 0.999)
# Butterworth filter — smooth rolloff, no ripple
b, a = signal.butter(2, normalized_cutoff, btype='low', analog=False)
filtered = signal.lfilter(b, a, wave)
return filtered.astype(np.float32)
# Now: oscillator → envelope → filter
wave = oscillator(110, waveform='sawtooth', duration=3.0) # low A
env = adsr_envelope(3.0, attack=0.05, decay=0.2, sustain_level=0.7, release=0.5)
shaped = wave * env
# Apply filter — try changing cutoff_hz from 200 to 3000 and hear the difference
filtered = low_pass_filter(shaped, cutoff_hz=800)
sd.play(filtered, SAMPLE_RATE)
sd.wait()
Install scipy:
