Overview
gb-synth is a synthesis engine with a tracker-style song DSL for generating retro game music and sound effects. Define songs declaratively — chords, patterns, sections, instruments — and render to 16-bit mono PCM WAV at 22050 Hz.
Companion to gb-sprite (procedural 2D graphics).
Features:
- 5 waveforms — sine, square, triangle, sawtooth, noise
- ADSR envelopes with 4 presets (percussive, shortPluck, longPad, organ)
- Tracker-style step patterns with note sustain across rests
- Structured songs with sections (intro/verse/chorus/outro) and repeats
- 13 ready-to-use SFX presets (laser, explosion, coin, powerup, etc.)
- Programmatic chord construction and progressions
- Post-processing effects (bitCrush, echo, fade, reverse, mix)
- Pre-rendered drum samples (kick, snare, hihat)
- 16-bit mono PCM WAV output
Architecture
src/GBSynth/
├── Oscillator.hs Sine, square, triangle, sawtooth, noise
├── Envelope.hs ADSR (attack/decay/sustain/release)
├── Instrument.hs Synth (oscillator+ADSR) or Sample (pre-rendered buffer)
├── Pattern.hs Tracker-style step grid (MOD/XM/IT inspired)
├── Song.hs Sections + arrangement (intro/verse/chorus/outro)
├── Synthesis.hs Reusable SFX building blocks (sweeps, bursts, decays)
├── SFX.hs 13 ready-to-use sound effect presets
├── Chord.hs Programmatic chord construction and progressions
├── Effects.hs Post-processing (bitCrush, echo, fade, reverse, mix)
├── Render.hs Song → [Int16] pipeline
└── WAV.hs 16-bit mono PCM writer (22050 Hz)
Pipeline
Song → Sections → Tracks → Patterns → Notes → Oscillator + Envelope → SampleMap → Mix → Normalize → [Int16] → WAV
- Each
Section contains parallel Tracks
- Each
Track pairs an Instrument with a Pattern
Pattern steps are rendered: NoteOn triggers renderNote, Rest sustains the previous note, NoteOff silences
- Track audio is mixed into a sparse
SampleMap for efficient random-access
- Tracks are layered with per-track gain
- Section is repeated
secRepeats times
- All sections concatenated, normalized, converted to
[Int16]
Usage
As a dependency
Add to your .cabal file:
build-depends: gb-synth >= 0.2
Generating WAVs
import GBSynth.Render (renderSong)
import GBSynth.WAV (writeWav)
main :: IO ()
main = writeWav "music.wav" (renderSong mySong)
Quick SFX
import GBSynth.SFX (laser, explosion, coin)
import GBSynth.WAV (writeWav)
main :: IO ()
main = do
writeWav "laser.wav" laser
writeWav "explosion.wav" explosion
writeWav "coin.wav" coin
API
Oscillator
data Waveform = Sine | Square | Triangle | Sawtooth | Noise
oscillate :: Waveform -> Double -> Int -> [Double] -- waveform, freq Hz, duration samples
noteFreq :: Int -> Double -- MIDI note → Hz (A4 = 440)
Envelope
data ADSR = ADSR
{ adsrAttack :: !Double -- seconds, linear 0→1
, adsrDecay :: !Double -- seconds, 1→sustain
, adsrSustain :: !Double -- hold level (0.0–1.0)
, adsrRelease :: !Double -- seconds, sustain→0
}
renderEnvelope :: ADSR -> Int -> Int -> [Double] -- noteOn samples, total samples → curve
-- Presets
percussive :: ADSR -- drums, clicks
shortPluck :: ADSR -- bass, arpeggios
longPad :: ADSR -- pads, ambient
organ :: ADSR -- sustained tones
Instrument
data Instrument
= Synth !Waveform !ADSR !Double -- oscillator + envelope + gain
| Sample ![Double] !Double -- pre-rendered buffer + gain
renderNote :: Instrument -> Int -> Int -> [Double] -- instrument, MIDI note, duration
-- Presets
bass :: Instrument -- square + shortPluck
lead :: Instrument -- sine + shortPluck
pad :: Instrument -- sine + longPad
Pattern
data NoteEvent = NoteOn !Int !Double | NoteOff | Rest
data Pattern = Pattern { patSteps :: !Int, patEvents :: ![NoteEvent] }
fromNotes :: [Maybe Int] -> Pattern -- Nothing = rest, Just n = note on
fromHits :: Int -> [Int] -> Pattern -- percussion: total steps + hit positions
Song
data Track = Track { trkInstrument :: !Instrument, trkPattern :: !Pattern, trkGain :: !Double }
data Section = Section { secName :: !String, secRepeats :: !Int, secTracks :: ![Track] }
data Song = Song { songTempo :: !Int, songStepsPerBeat :: !Int, songSections :: ![Section] }
Render
renderSong :: Song -> [Int16] -- full song pipeline
renderSfx :: Double -> [(Double, [Double])] -> [Int16] -- layer + normalize SFX
layerWeighted :: [(Double, [Double])] -> [Double] -- mix with per-signal gain
Synthesis
Reusable SFX building blocks — combine these to create any sound effect:
sineSweep :: Double -> Double -> Double -> Int -> [Double] -- freq sweep
sineSweepAD :: Double -> Double -> Double -> Double -> Int -> [Double] -- sweep + decay
noiseBurst :: Double -> Int -> [Double] -- noise with decay
squareWaveDecay :: Double -> Double -> Int -> [Double] -- square + decay
expDecay :: Double -> Double -> Double -- exponential decay
attackDecay :: Double -> Double -> Double -> Double -- attack-decay curve
silence :: Int -> [Double] -- zero-filled gap
SFX
13 ready-to-use sound effect presets, pre-rendered as [Int16]:
laser :: [Int16] -- fire/shoot
explosion :: [Int16] -- kill/death
impact :: [Int16] -- structural hit
alert :: [Int16] -- wave start / alarm
click :: [Int16] -- UI button
powerup :: [Int16] -- upgrade/collect
coin :: [Int16] -- pickup/reward
jump :: [Int16] -- platformer jump
heal :: [Int16] -- healing/restore
defeat :: [Int16] -- game over
-- Drum samples
kickSample :: [Int16]
snareSample :: [Int16]
hihatSample :: [Int16]
Chord
Programmatic chord construction from MIDI root notes:
data Quality = Major | Minor | Diminished | Augmented | Sus2 | Sus4
chord :: Int -> Quality -> [Int] -- root MIDI + quality → note list
inversion :: Int -> [Int] -> [Int] -- nth inversion
chordProgression :: [(Int, Quality)] -> [(Int, [Int])] -- build full progression
-- Built-in progressions
pop1564 :: [(Int, [Int])] -- I-V-vi-IV in C
blues145 :: [(Int, [Int])] -- 12-bar blues in A
minorClassic :: [(Int, [Int])] -- i-VI-III-VII in Am
Effects
Post-processing for rendered audio signals:
bitCrush :: Int -> [Double] -> [Double] -- reduce bit depth for lo-fi crunch
echo :: Int -> Double -> [Double] -> [Double] -- delay line with decay
fadeIn :: Int -> [Double] -> [Double] -- linear fade in (N samples)
fadeOut :: Int -> [Double] -> [Double] -- linear fade out (N samples)
reverseSignal :: [Double] -> [Double] -- reverse audio
mix :: [[Double]] -> [Double] -- sum multiple signals
WAV
sampleRate :: Int -- 22050
msToSamples :: Int -> Int -- milliseconds → samples
toSample :: Double -> Int16 -- [-1,1] → Int16
writeWav :: FilePath -> [Int16] -> IO ()
Example
A complete song using the Chord module for progressions:
import GBSynth.Chord (Quality (..), chord, chordProgression)
import GBSynth.Instrument (Instrument (..), bass, lead, pad)
import GBSynth.Pattern (fromNotes, fromHits)
import GBSynth.Render (renderSong)
import GBSynth.SFX (kickSample)
import GBSynth.Song (Section (..), Song (..), Track (..))
import GBSynth.WAV (writeWav)
main :: IO ()
main = writeWav "song.wav" (renderSong mySong)
mySong :: Song
mySong = Song
{ songTempo = 120
, songStepsPerBeat = 4
, songSections = [intro, verse, chorus]
}
-- Am - F - C - G using Chord module
progression :: [(Int, [Int])]
progression = chordProgression [(57, Minor), (53, Major), (48, Major), (55, Major)]
-- Bass: root note sustained per chord (8 steps each)
bassPat :: Pattern
bassPat = fromNotes $ concatMap
(\(root, _) -> Just root : replicate 7 Nothing)
progression
-- Arpeggio: root, 3rd, 5th, 3rd
arpPat :: Pattern
arpPat = fromNotes $ concatMap
(\(_, notes) -> case notes of
(n0 : n1 : n2 : _) ->
[Just n0, Nothing, Just n1, Nothing,
Just n2, Nothing, Just n1, Nothing]
_ -> replicate 8 Nothing)
progression
-- Kick on beats 1 and 3
kickPat :: Pattern
kickPat = fromHits 32 [0, 8, 16, 24]
-- Pre-rendered kick from SFX module
kickInstr :: Instrument
kickInstr = Sample (map (\s -> fromIntegral s / 32768.0) kickSample) 1.0
intro :: Section
intro = Section "intro" 2
[ Track bass bassPat 0.35 ]
verse :: Section
verse = Section "verse" 4
[ Track bass bassPat 0.35
, Track lead arpPat 0.30
]
chorus :: Section
chorus = Section "chorus" 4
[ Track bass bassPat 0.35
, Track lead arpPat 0.35
, Track pad bassPat 0.20
, Track kickInstr kickPat 0.30
]
Build & Test
Requires GHCup with GHC >= 9.6.
cabal build # Build library
cabal test # Run all tests (109 pure tests)
cabal build --ghc-options="-Werror" # Warnings as errors
cabal haddock # Generate docs
MIT License · Gondola Bros Entertainment