DEV Community

Cover image for I taught my CPU to sing — in pure Assembly
Em'
Em'

Posted on Originally published at Medium

I taught my CPU to sing — in pure Assembly

I didn't come from computers.
My background is languages and translation — human grammar, the kind with conjugations and declensions.
I started coding in October 2025, with Rust, and somewhere between a table of verb endings and my first compiler error, something clicked: a programming language is just another grammar, one where intention becomes action.

Since then my curiosity has only pulled me lower.
Rust, then C++, then assembly — because assembly lets nothing hide. I'd already made my CPU count, sort, hash, and draw.
There was one thing left that isn't useful at all, and that's exactly why I wanted it: I wanted to make it sing.

So I built asm.fm — a chiptune synthesizer written entirely in x86-64 assembly.
No libc. No audio library. No sound card driver.
The rule was simple: syscalls or nothing.

Sound is just a list of numbers

Here's the whole secret, the thing that makes this possible at all.

A sound is a list of numbers.
44,100 of them per second, each one describing where a speaker's cone should sit at that instant.
Play those numbers back in order and the speaker moves; the air moves; you hear something.
That's it. That's all a .wav file is — a tiny header, then a very long list of positions.

So the entire synthesizer is one idea repeated: compute the right numbers, write them out.
Generate a square wave? Emit +amplitude for half a period, -amplitude for the other half, over and over.
Want it in tune? A note's frequency tells you how many samples one period lasts.
Everything else is arithmetic on that list.

There's no printf to lean on, no libsndfile to write the file. You build the 44-byte WAV header by hand — "RIFF", the sizes, "fmt ", the sample rate, "data" — and then you write() the bytes straight to stdout.
Redirect that to a file and you have music.

Four voices, then more

From there it grew the way synthesizers historically grew.

First the oscillators: square, sawtooth, triangle, and — for drums — an LFSR generating pseudo-random noise, the exact trick the NES used for its percussion.
Then polyphony: melody, bass, and drums at once, each voice computed and summed into one buffer, a tiny tracker in a few hundred instructions.

Then the notes stopped clicking.
A raw note starts and stops at full volume, and that hard edge clicks. ADSR envelopes fix it — attack, decay, sustain, release — so each note fades in, holds, and fades out. The note breathes.

The "fm" was never just about radio

The part I'm proudest of is FM synthesis — the technique behind the Yamaha DX7 and the Sega Genesis, the source of every bell-like, metallic, electric-piano tone of the 1980s.

The idea is deceptively small.
You take one oscillator (the carrier) and wobble its frequency using a second oscillator (the modulator), added right into its phase:

modulator = sin(f_mod · t)
carrier   = sin(f_car · t  +  index · modulator)
Enter fullscreen mode Exit fullscreen mode

That single addition — feeding one wave into another's phase — spawns a whole spectrum of new harmonics.
A pure tone becomes a shimmering bell.

The catch: there's no sin() without a math library.
So I build a 1,024-entry sine table at startup, by hand, using a 7th-century polynomial approximation (Bhaskara I's), and look values up from it.
The synthesizer computes its own trigonometry before it plays a note.

And then it kept going

Once the core was there, the effects followed, each one a small idea layered on the last.

First came modulation — making the sound move.
Vibrato and tremolo: a slow oscillator nudging the pitch, then the volume.
PWM, sliding the square wave's duty cycle so the timbre shivers while the pitch holds still.
A delay, which needed something new — memory.
You keep the last fraction of a second of output in the buffer and read it back, faded; reading your own faded output again is what makes the echo repeat and decay, all from one line of feedback.

Then came sculpting the timbre itself.
A low-pass filter — a one-pole smoother that softens a bright waveform into something warm and round.
A bitcrusher, snapping each sample to a handful of levels for that lo-fi, gritty crunch (a nice irony, degrading a synth that's already 8-bit at heart).
Ring modulation, which simply multiplies two waves — and out come their sum and difference frequencies, the originals gone entirely, leaving the metallic, clangorous voice famously used for the Daleks.

And the one I love most: a resonant filter sweep.
A state-variable filter that feeds a band-pass term back on itself to boost the frequencies right at the cutoff — that resonant peak is the squelch — while an LFO sweeps the cutoff up and down.
It's the acid "wah", opening and closing, and it makes the filter seem to sing.

Why do this?

None of this belongs in production. That was never the point.

The point is that after you write your own WAV header, you never quite hear a .wav the same way.
After you build FM synthesis from a hand-rolled sine table, you understand — in your fingers, not just your notes — what a synthesizer actually is.
Assembly doesn't forgive and it doesn't explain, which is exactly why it teaches so much.
There's nothing between the math and the speaker but you.

I made my processor count, sort, hash, and draw. Now it sings.


The repo is open, MIT-licensed: github.com/whispem/asm.fm — feedback on the assembly is very welcome.

Top comments (1)

Collapse
 
ailegend profile image
Talha Anwar

dev still use assembly. hats off