Skip to main content

blip8

Chiptune sound synthesis from code: the four voices of the NES and the Game Boy, generated from scratch. No samples, no recordings, no dependencies beyond NumPy. Every sound is arithmetic.

pip install blip8      # or: uv add blip8
from blip8 import melody, play, sfx

play(melody("E4 E4 F4 G4 G4 F4 E4 D4", bpm=140))   # a tune, in one line
play(sfx.coin())
play(sfx.kick() + sfx.hat())                        # adding arrays mixes them

Or build sounds yourself from the raw waveforms:

from blip8 import envelope, noise, play, square, triangle

play(square(freq=440, length=0.5))              # 1983 in one line
play(square(freq=440, length=0.5, duty=0.125))  # same note, thinner (the NES knob)
play(square(freq=440, length=0.5) + triangle(freq=220, length=0.5))  # melody + bass

play(square(freq=(1800, 200), length=0.25))     # a (start, end) pitch = laser
play(envelope(noise(length=1.5), decay=1.5, sustain=0.0))  # noise + fade = cymbal

Why I made this

Every time I built a game I ended up writing the same throwaway script again: forty lines of NumPy to make a coin sound, pasted into the repo, tweaked until it was close enough, then forgotten. The next game started from zero.

So I wrote the library I should have written the first time. Now it is pip install blip8 and sfx.coin().

It is also my first real Python project, coming from TypeScript and Go, and an excuse to explore how synthesis actually works. The NES had four voices and no way to play a recording, which makes it a small enough system to build from first principles and understand completely.

Hear it

uv run examples/first_beep.py     # two square waves, and the duty knob
uv run examples/four_voices.py    # square vs triangle vs noise
uv run examples/sound_design.py   # snare, crash, kick, laser, power-up, coin
uv run examples/sfx_menu.py       # every recipe, the wave channel, a drum groove
uv run examples/melody.py         # actual music: four voices, chords vs arpeggios
uv run examples/see_it.py         # print the waveforms instead of playing them

uv sets up Python and installs NumPy on first run, so there is nothing else to do. save() writes a .wav anywhere. play() shells out to whichever player the platform has (afplay on macOS, aplay, paplay or ffplay on Linux, winsound on Windows) and raises a clear error if none is present.

The voices

The NES sound chip had four, each locked to one waveform. The Game Boy's had four too, with a twist.

Voice Shape Sounds like Used for Status
Pulse square buzzy, electronic melody, harmony ✅ square()
Triangle ramp up/down soft, flute-ish basslines ✅ triangle()
Noise random static drums, explosions ✅ noise()
Wave anything you draw whatever you make it the Game Boy's trick ✅ wavetable()

Square waves have one knob, duty, which is how much of each cycle is spent "up". 0.5 sounds round and hollow, 0.125 thin and nasal. Same pitch, different character. That knob is most of the NES's personality.

Shaping

The waveforms are the raw material. Shaping them over time is what makes them recognisable as things.

envelope() controls volume over time, the ADSR curve every synth has. It's why a drum and a piano playing the same note are unmistakable. It also stops notes ending in a click, since they now finish at silence instead of mid-cycle.

Pitch sweeps come free: pass freq=(start, end) instead of one number and the note glides. Falling is a laser, rising is a power-up.

Between them, the same noise() becomes a snare (fast fade) or a cymbal (slow fade), and a triangle() sliding from 120 Hz to 40 Hz becomes a kick drum. examples/sound_design.py plays all of it.

crunch() throws precision away on purpose. bits=4 allows only 16 volume levels, which is what the Game Boy actually had. Producers call this bitcrushing and reach for it deliberately.

Recipes

sfx is the cookbook: 14 game sounds with the numbers already chosen.

sfx.blip()   sfx.select()  sfx.back()   sfx.coin()
sfx.jump()   sfx.powerup() sfx.laser()  sfx.hurt()
sfx.explosion()  sfx.chime()
sfx.kick()   sfx.snare()   sfx.hat()    sfx.crash()

Each returns an array rather than playing it, so they mix (+), chain, and save(). Every one is built only from the waveforms and shapers above. Open src/blip8/sfx.py to see exactly what any given sound is made of.

Music

Nobody thinks in Hz, so note("E5") converts names to frequencies. An octave up doubles the frequency, and a semitone is the twelfth root of two.

Patterns are strings, one token per step in time. A note name plays, . holds the note before it, - rests:

melody("C4 E4 G4 C5", bpm=120)                    # four sixteenth notes
melody("C2 . . . G2 . . .", voice=triangle)       # a bassline, notes held
melody("C5 - C5 -", duty=0.125)                   # extra kwargs reach the voice

That's a tracker, the text-grid format chiptune musicians actually use, arrived at because it's the obvious way to write music as a string.

chord("C4 E4 G4") plays notes together; arpeggio("C4 E4 G4") plays them one at a time, fast, until your ear fuses them into a chord. The second one exists because the NES only had four voices and couldn't spare three for one chord, which is why chiptune has that frantic bubbling sound.

Arrangement is two functions. at(time, sound) delays; layer(*sounds) mixes, padding to the longest:

layer(at(0.0, sfx.kick()), at(0.5, sfx.snare()))

See it

GitHub can't play audio, so show() draws the waveform in your terminal: min/max per column, the way an audio editor does. Zoom in on a few cycles to see the shape, or out to a whole sound to see its envelope.

from blip8 import show, square
show(square(freq=440, length=0.007), "three cycles")
square, zoomed in on three cycles
███████████         ███████████         ███████████           ██
          █         █         █         █         █           █ 
          █         █         █         █         █           █ 
          █         █         █         █         █           █ 
          █         █         █         █         █           █ 
──────────█─────────█─────────█─────────█─────────█───────────█─
          █         █         █         █         █           █ 
          █         █         █         █         █           █ 
          █         █         █         █         █           █ 
          █         █         █         █         █           █ 
          ███████████         ███████████         █████████████ 

triangle, the same three cycles
█                  ██                  ██                    ██ 
██                ████                ████                  ██ █
 ██              ██  ██              ██  ██                ██   
  ██            ██    ██            ██    ██              ██    
   ██          ██      ██          ██      ██           ██      
────██────────██────────██────────██────────██─────────██───────
     ██      ██          ██      ██          ██       ██        
      ██    ██            ██    ██            ██     ██         
       ██  ██              ██  ██              ██  ██           
        ████                ████                ████            
         ██                  ██                  ██             

a cymbal crash, zoomed out to 1.2 seconds
███                                                             
█████████████████                                               
███████████████████████████████                                 
████████████████████████████████████████████                    
██████████████████████████████████████████████████████████      
████████████████████████████████████████████████████████████████
██████████████████████████████████████████████████████████      
████████████████████████████████████████████                    
██████████████████████████████                                  
█████████████████                                               
███                                                             

uv run examples/see_it.py prints all four voices, the duty knob, envelopes, sweeps and bit crushing.

Develop

CI

uv run pytest         # 241 tests, all asserting things you can hear
uv run mypy src tests # type check, strict
uv run ruff check .   # lint
uv run ruff format .  # format

CI runs all four on Linux, macOS and Windows across Python 3.12 and 3.13, then builds the package and installs the wheel into a clean environment to prove it imports.

The package ships py.typed, so the annotations reach your editor rather than stopping at the package boundary. Samples and Pitch are exported for annotating your own code.

The test suite is worth a read if you want to know how you test a sound: every claim in the docstrings turns out to be a claim about numbers.

Releases go out from a version tag through PyPI Trusted Publishing, so no API token exists to leak. Design decisions with real tradeoffs are recorded in docs/decisions.

Status

All four voices, envelopes, sweeps, bit crushing, 14 ready-made game sounds, note names, patterns, chords, arpeggios and arrangement. It makes music now.

The roadmap ends with blip8 cover song.mid turning any MIDI file into an 8-bit cover.

Release files for blip8 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for blip8 0.1.0
File Size Uploaded
blip8-0.1.0.tar.gz 50.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for blip8 0.1.0
File Interpreter ABI Platform
blip8-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 65.6 kB

Release files / blip8-0.1.0.tar.gz

Download URL blip8-0.1.0.tar.gz
Size 50.4 kB
Tags Source
SHA-256 checksum
How to use checksums
9fda01c3a467c7b1c8cd6e586e2eec569101c990f9382998c10088494d62f209
BLAKE2b-256 checksum
How to use checksums
8e65f20967c9433d004e4924ec770dad79b14e0e07e107ff4c69bc4b6d53b812
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.

Transparency log

Release files / blip8-0.1.0-py3-none-any.whl

Download URL blip8-0.1.0-py3-none-any.whl
Size 15.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
be6e80a333e74923bcceea83fff975330c3c9d962081404de2adca7bdba006d3
BLAKE2b-256 checksum
How to use checksums
afa959d69a3c0106748ffa6d028c14fd7f03adc80386dc547637e7211b8ac565
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page