← Back to all projects

LEARN MUSIC PROGRAMMING C DEEP DIVE

Learn Music Programming in C: From Silence to Symphony

Goal: Deeply understand digital audio—from raw PCM samples to synthesizers, effects, and complete music production software—all built from scratch in C.


Why Create Music with Code?

Creating music programmatically is one of the most rewarding applications of low-level programming. You’ll understand:

  • How sound is represented digitally (it’s just numbers!)
  • The physics of audio: frequency, amplitude, harmonics
  • Real-time systems programming (audio can’t skip!)
  • Digital Signal Processing (DSP) fundamentals
  • How professional audio software works under the hood

After completing these projects, you will:

  • Generate any waveform from first principles
  • Build synthesizers that create sounds from oscillators
  • Implement audio effects (reverb, delay, filters)
  • Create MIDI sequencers and music composition tools
  • Understand how DAWs (Digital Audio Workstations) work

Core Concept Analysis

Digital Audio Fundamentals

                    ANALOG TO DIGITAL

    Continuous Sound Wave              Digital Samples
          ╭───╮                        ┌─┐
         ╱     ╲                       │ │ ┌─┐
        ╱       ╲       Sampling      │ │ │ │ ┌─┐
    ───╱         ╲───  ─────────►  ───┴─┴─┴─┴─┴─┴───
                  ╲   @44100 Hz              │ │
                   ╲                         └─┘
                    ╰───╯

    Each sample is a number representing amplitude at that instant
    44100 samples per second = CD quality

Key Concepts You Must Master

  1. PCM (Pulse Code Modulation)
    • Audio is just an array of numbers (samples)
    • Sample rate: How many samples per second (44100 Hz = CD quality)
    • Bit depth: Range of values per sample (16-bit = -32768 to 32767)
    • Channels: Mono (1), Stereo (2), etc.
  2. Waveforms - The Building Blocks
    Sine Wave:        Square Wave:      Sawtooth:         Triangle:
         ╭─╮              ┌──┐              /|              /\
        /   \             │  │             / |             /  \
    ───/     \───     ────┘  └────     ───/  |───      ───/    \───
                                          |  /              \  /
                                          |/                 \/
    
    • Sine: Pure tone, single frequency
    • Square: Hollow sound, odd harmonics
    • Sawtooth: Bright/buzzy, all harmonics
    • Triangle: Mellow, odd harmonics (weaker)
  3. Frequency and Pitch
    • Frequency (Hz) = vibrations per second
    • A4 = 440 Hz (standard tuning reference)
    • Octave = doubling/halving frequency
    • Musical notes follow: freq = 440 * 2^((note - 69) / 12) (MIDI note number)
  4. Amplitude and Volume
    • Amplitude = sample value magnitude
    • Volume often measured in decibels (dB)
    • dB = 20 * log10(amplitude ratio)
    • Linear amplitude: 0.0 to 1.0 (or -1.0 to 1.0 for signed)
  5. Envelopes (ADSR)
    Amplitude
        │    ╱╲
        │   ╱  ╲___________
        │  ╱              ╲
        │ ╱                ╲
        └──────────────────────► Time
          A   D    S       R
    
    Attack:  Time to reach peak
    Decay:   Time to fall to sustain level
    Sustain: Level held while note is held
    Release: Time to fade to silence after note ends
    
  6. Effects Processing
    • Delay: Store samples, play back later
    • Reverb: Many delays simulating room reflections
    • Filter: Remove certain frequencies (low-pass, high-pass, band-pass)
    • Distortion: Clip or transform the waveform
  7. MIDI (Musical Instrument Digital Interface)
    • Not audio—it’s control messages
    • Note On/Off, velocity, pitch bend, control changes
    • Standard MIDI Files (.mid) store sequences of MIDI events
  8. Audio File Formats
    • WAV/PCM: Uncompressed, raw samples with header
    • AIFF: Apple’s uncompressed format
    • MP3/OGG/FLAC: Compressed formats (lossy/lossless)

Prerequisites

Before starting these projects, you should have:

  • Solid C programming skills - Pointers, arrays, structs, file I/O, memory management
  • Basic math - Trigonometry (sin, cos), logarithms, basic algebra
  • Understanding of binary/hex - For file format work
  • Linux/macOS environment - For audio APIs (Windows works too with different APIs)

Helpful but not required:

  • Basic music theory (notes, scales, chords)
  • Understanding of physics (waves, vibration)

Development Environment Setup

Required Tools

# On Ubuntu/Debian
sudo apt install build-essential libasound2-dev libportaudio2 libportaudiocpp0 \
    portaudio19-dev libsndfile1-dev audacity sox

# On macOS
brew install portaudio libsndfile sox audacity

# On Fedora
sudo dnf install @development-tools alsa-lib-devel portaudio-devel \
    libsndfile-devel audacity sox

Verification

# Test your audio setup
speaker-test -t sine -f 440  # Should hear A4 tone

# Check PortAudio installation
pkg-config --cflags --libs portaudio-2.0

Project List

Projects progress from understanding audio basics to building complete music software.


Project 1: WAV File Generator (Understanding PCM)

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Go, Python
  • Coolness Level: Level 2: Practical but Forgettable
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 1: Beginner
  • Knowledge Area: Audio File Formats / PCM Basics
  • Software or Tool: WAV format, file I/O
  • Main Book: “The Audio Programming Book” by Boulanger & Lazzarini

What you’ll build: A program that generates WAV files containing pure sine waves at specified frequencies—completely from scratch without any audio libraries. You’ll write the WAV header byte-by-byte.

Why it teaches music programming: This is the “Hello World” of audio. By writing raw PCM samples to a WAV file, you’ll understand exactly what digital audio IS—just numbers in an array. No magic, no black boxes.

Core challenges you’ll face:

  • Writing the WAV header correctly → maps to understanding audio file structure
  • Generating sine wave samples → maps to the relationship between frequency and samples
  • Handling little-endian byte order → maps to binary file formats
  • Calculating correct sample values → maps to amplitude and bit depth

Key Concepts:

  • WAV file format: WAV Format Tutorial
  • PCM fundamentals: “The Audio Programming Book” Chapter 1 - Boulanger & Lazzarini
  • Sine wave generation: Understanding sin(2 * PI * frequency * time)
  • Sample rate math: “Think DSP” Chapter 1 - Allen B. Downey

Difficulty: Beginner Time estimate: Weekend Prerequisites: Basic C programming, understanding of file I/O

Real world outcome:

$ ./wavgen --freq 440 --duration 2.0 --output a4.wav
Generating 440 Hz sine wave...
Sample rate: 44100 Hz
Bit depth: 16
Duration: 2.0 seconds
Samples: 88200
Written to: a4.wav (176444 bytes)

$ file a4.wav
a4.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 44100 Hz

$ play a4.wav  # Using sox
a4.wav:
 File Size: 176k      Bit Rate: 706k
  Encoding: Signed PCM
  Channels: 1 @ 16-bit
Samplerate: 44100Hz
Replaygain: off
  Duration: 00:00:02.00
# (You hear a pure A4 tone)

# Generate a chord
$ ./wavgen --freq 261.63,329.63,392.00 --duration 3.0 --output c_major.wav
Generating chord with 3 frequencies...
  261.63 Hz (C4)
  329.63 Hz (E4)
  392.00 Hz (G4)
Written to: c_major.wav

$ play c_major.wav
# (You hear a C major chord)

Implementation Hints:

The WAV file format (RIFF) structure:

Offset  Size  Description
0       4     "RIFF" (chunk ID)
4       4     File size - 8 (chunk size)
8       4     "WAVE" (format)
12      4     "fmt " (subchunk1 ID)
16      4     16 (subchunk1 size for PCM)
20      2     1 (audio format, 1 = PCM)
22      2     Number of channels
24      4     Sample rate
28      4     Byte rate (sample_rate * channels * bits_per_sample/8)
32      2     Block align (channels * bits_per_sample/8)
34      2     Bits per sample
36      4     "data" (subchunk2 ID)
40      4     Data size (num_samples * channels * bits_per_sample/8)
44      ...   Actual audio data

Key formulas:

// Generate sample at time t (in seconds)
double sample = sin(2.0 * M_PI * frequency * t);

// Convert to 16-bit signed integer
int16_t pcm_sample = (int16_t)(sample * 32767.0);

// For multiple frequencies (chord), sum them
double sample = 0;
for (int i = 0; i < num_freqs; i++) {
    sample += sin(2.0 * M_PI * frequencies[i] * t);
}
sample /= num_freqs;  // Normalize to prevent clipping

Questions to guide implementation:

  1. Why is the file size stored at offset 4 as “file size - 8”?
  2. What happens if you use 8-bit samples instead of 16-bit?
  3. How do you prevent clipping when mixing multiple frequencies?
  4. What’s the relationship between sample rate and maximum frequency (Nyquist)?

Learning milestones:

  1. Your WAV file plays in any audio player → You understand the WAV format
  2. Different frequencies produce correct pitches → You understand frequency/sample relationship
  3. You can generate chords → You understand audio mixing basics
  4. You can control duration precisely → You understand sample rate math

Project 2: Waveform Generator (Beyond Sine Waves)

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, C++
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 1: Beginner
  • Knowledge Area: Waveform Synthesis / Harmonics
  • Software or Tool: WAV output, waveform math
  • Main Book: “The Audio Programming Book” by Boulanger & Lazzarini

What you’ll build: Extend your WAV generator to produce square, sawtooth, and triangle waves. Add the ability to visualize waveforms in the terminal as ASCII art.

Why it teaches music programming: Different waveforms have different harmonic content, which is why they sound different. Understanding this relationship between waveform shape and timbre is fundamental to synthesis.

Core challenges you’ll face:

  • Generating non-sinusoidal waveforms → maps to understanding harmonic series
  • Anti-aliasing considerations → maps to the Nyquist theorem and aliasing
  • Implementing band-limited synthesis → maps to professional synthesizer techniques
  • Visualizing waveforms → maps to understanding what you’re hearing

Key Concepts:

  • Harmonic series: “The Audio Programming Book” Chapter 4 - Boulanger & Lazzarini
  • Aliasing: “Think DSP” Chapter 2 - Allen B. Downey
  • Band-limited oscillators: EarLevel Engineering Wavetable Oscillator
  • Fourier synthesis: Understanding sounds as sums of sine waves

Difficulty: Beginner Time estimate: 1 week Prerequisites: Project 1 completed

Real world outcome:

$ ./waveform --type sine --freq 440 --output sine.wav --visualize
Generating SINE wave at 440 Hz
         ╭────────╮
        ╱          ╲
       ╱            ╲
──────╱              ╲──────
                      ╲            ╱
                       ╲          ╱
                        ╰────────╯
Written to: sine.wav

$ ./waveform --type square --freq 440 --output square.wav --visualize
Generating SQUARE wave at 440 Hz
      ┌────────┐        ┌────────┐
      │        │        │        │
      │        │        │        │
──────┘        └────────┘        └────────
Written to: square.wav

$ ./waveform --type sawtooth --freq 440 --output saw.wav --visualize
Generating SAWTOOTH wave at 440 Hz
        ╱│      ╱│      ╱│
       ╱ │     ╱ │     ╱ │
      ╱  │    ╱  │    ╱  │
─────╱   │───╱   │───╱   │───
         │       │       │
Written to: saw.wav

$ ./waveform --type triangle --freq 440 --output tri.wav --visualize
Generating TRIANGLE wave at 440 Hz
        ╱╲        ╱╲        ╱╲
       ╱  ╲      ╱  ╲      ╱  ╲
      ╱    ╲    ╱    ╲    ╱    ╲
─────╱      ╲──╱      ╲──╱      ╲──
Written to: tri.wav

$ play sine.wav square.wav saw.wav tri.wav
# (Hear the distinct timbres of each waveform)

Implementation Hints:

Naive waveform generation (has aliasing issues at high frequencies):

// Phase goes from 0.0 to 1.0 per cycle
double phase = fmod(frequency * t, 1.0);

// Square wave
double square = (phase < 0.5) ? 1.0 : -1.0;

// Sawtooth wave
double sawtooth = 2.0 * phase - 1.0;  // -1 to +1

// Triangle wave
double triangle = 4.0 * fabs(phase - 0.5) - 1.0;

Band-limited synthesis using additive synthesis (no aliasing):

// Square wave = sum of odd harmonics: sin(f) + sin(3f)/3 + sin(5f)/5 + ...
double square_bl(double phase, double freq, double sample_rate) {
    double result = 0.0;
    double nyquist = sample_rate / 2.0;
    for (int n = 1; freq * n < nyquist; n += 2) {
        result += sin(2.0 * M_PI * phase * n) / n;
    }
    return result * (4.0 / M_PI);
}

Questions to guide implementation:

  1. Why does a square wave sound “hollow” compared to a sawtooth?
  2. What causes aliasing, and why is it worse at high frequencies?
  3. How many harmonics can you include before hitting the Nyquist limit?
  4. What’s the trade-off between naive and band-limited synthesis?

Learning milestones:

  1. All waveforms generate correctly → You understand basic waveform math
  2. You hear the timbral differences → You understand harmonics
  3. You implement band-limited synthesis → You understand aliasing
  4. Your visualization matches the audio → You can debug by looking AND listening

Project 3: Real-Time Audio Output (PortAudio)

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: C++, Rust
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Real-Time Audio / Callback Programming
  • Software or Tool: PortAudio, ALSA
  • Main Book: “The Audio Programming Book” by Boulanger & Lazzarini

What you’ll build: A program that generates audio in real-time, playing through your speakers immediately. You’ll implement a keyboard-controlled theremin where mouse/keyboard input changes the pitch.

Why it teaches music programming: Real-time audio is fundamentally different from file generation. You must generate samples fast enough to keep the audio buffer full, or you’ll hear glitches. This teaches the constraints of real-time systems.

Core challenges you’ll face:

  • Understanding audio callbacks → maps to real-time programming patterns
  • Managing audio latency → maps to buffer size trade-offs
  • Thread-safe parameter updates → maps to lock-free programming
  • Maintaining continuous audio → maps to real-time constraints

Key Concepts:

  • PortAudio callback model: PortAudio Tutorial
  • Audio latency: Buffer size × 2 / sample rate = roundtrip latency
  • Real-time constraints: “The Audio Programming Book” Chapter 2
  • Lock-free communication: Atomic operations for real-time safety

Difficulty: Intermediate Time estimate: 1 week Prerequisites: Project 1-2 completed, understanding of callbacks

Real world outcome:

$ ./theremin
PortAudio Theremin
==================
Using device: Built-in Audio (44100 Hz, 256 frames)
Latency: 5.8 ms

Controls:
  Mouse X: Frequency (100-2000 Hz)
  Mouse Y: Volume (0-100%)
  Q: Quit

[====|==========] Freq: 523 Hz (C5)  Vol: 67%

# (Moving mouse changes pitch and volume in real-time)
# (Sound plays continuously from speakers)

$ ./keyboard_synth
Keyboard Synthesizer
====================
Using device: Built-in Audio (44100 Hz, 512 frames)

Play notes with keyboard:
  A S D F G H J K = C D E F G A B C (one octave)
  W E   T Y U     = C# D#  F# G# A# (sharps)
  Z/X: Octave down/up
  1-4: Waveform (sine, square, saw, triangle)
  Q: Quit

Current: Octave 4, Sawtooth wave

> [Press 'D'] Playing E4 (329.63 Hz)...
> [Press 'H'] Playing A4 (440.00 Hz)...
> [Press multiple keys] Playing chord: C4 E4 G4

Implementation Hints:

PortAudio callback structure:

typedef struct {
    double phase;
    double frequency;
    double amplitude;
    int waveform_type;
} AudioData;

static int audioCallback(const void *input, void *output,
                         unsigned long frameCount,
                         const PaStreamCallbackTimeInfo *timeInfo,
                         PaStreamCallbackFlags statusFlags,
                         void *userData) {
    AudioData *data = (AudioData*)userData;
    float *out = (float*)output;
    double phaseIncrement = data->frequency / SAMPLE_RATE;

    for (unsigned long i = 0; i < frameCount; i++) {
        double sample = generate_waveform(data->phase, data->waveform_type);
        *out++ = (float)(sample * data->amplitude);  // Left channel
        *out++ = (float)(sample * data->amplitude);  // Right channel

        data->phase += phaseIncrement;
        if (data->phase >= 1.0) data->phase -= 1.0;
    }
    return paContinue;
}

Initialization:

Pa_Initialize();
Pa_OpenDefaultStream(&stream, 0, 2, paFloat32, SAMPLE_RATE,
                     FRAMES_PER_BUFFER, audioCallback, &audioData);
Pa_StartStream(stream);
// ... main loop handling input ...
Pa_StopStream(stream);
Pa_Terminate();

Questions to guide implementation:

  1. What happens if the callback takes too long to execute?
  2. Why can’t you use malloc() or printf() in the audio callback?
  3. How do you safely update frequency from the main thread?
  4. What’s the relationship between buffer size and latency?

Learning milestones:

  1. Audio plays without glitches → You understand real-time constraints
  2. Pitch changes smoothly → You understand phase continuity
  3. No clicks when changing frequency → You understand parameter smoothing
  4. You can play chords → You understand polyphonic audio

Project 4: ADSR Envelope Generator

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: C++, Rust
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Envelope Generation / Sound Shaping
  • Software or Tool: Real-time audio, state machines
  • Main Book: “Designing Software Synthesizer Plugins in C++” by Will Pirkle

What you’ll build: An ADSR (Attack-Decay-Sustain-Release) envelope generator that shapes the amplitude of sounds over time. When you press a key, the sound attacks, decays to sustain, and releases when you let go.

Why it teaches music programming: Envelopes are what make synthesizers sound like instruments instead of continuous tones. A piano has a fast attack and slow decay; a violin has a slow attack. Understanding envelopes is essential for expressive synthesis.

Core challenges you’ll face:

  • Implementing envelope state machine → maps to note lifecycle management
  • Smooth transitions between stages → maps to avoiding clicks and pops
  • Handling note on/off events → maps to MIDI-like control
  • Exponential vs linear curves → maps to perceptual audio curves

Key Concepts:

  • ADSR envelopes: “Designing Software Synthesizer Plugins” Chapter 5 - Will Pirkle
  • State machines: Envelope as idle → attack → decay → sustain → release → idle
  • Exponential curves: More natural-sounding than linear
  • Note stealing: What happens when you press a new note before release finishes

Difficulty: Intermediate Time estimate: 1 week Prerequisites: Project 3 completed

Real world outcome:

$ ./synth_adsr
ADSR Synthesizer
================
Attack:  50ms  │████░░░░░░│  [A/Z to adjust]
Decay:   100ms │██████░░░░│  [S/X to adjust]
Sustain: 70%   │███████░░░│  [D/C to adjust]
Release: 200ms │████████░░│  [F/V to adjust]

Waveform: Sawtooth [1-4 to change]
Octave: 4 [Q/W to change]

Press keys to play: A S D F G H J K

> [Press 'A'] Note ON: C4
  Envelope: [ATTACK ▓▓▓▓░░░░░░]

> [Hold 'A']
  Envelope: [SUSTAIN ▓▓▓▓▓▓▓░░░]

> [Release 'A'] Note OFF: C4
  Envelope: [RELEASE ▓▓▓▓░░░░░░]
  Envelope: [IDLE]

# Different envelope settings create different "instruments":
$ ./synth_adsr --preset piano
Attack: 5ms, Decay: 500ms, Sustain: 0%, Release: 100ms

$ ./synth_adsr --preset strings
Attack: 200ms, Decay: 100ms, Sustain: 80%, Release: 300ms

$ ./synth_adsr --preset organ
Attack: 10ms, Decay: 0ms, Sustain: 100%, Release: 10ms

Implementation Hints:

Envelope state machine:

typedef enum {
    ENV_IDLE,
    ENV_ATTACK,
    ENV_DECAY,
    ENV_SUSTAIN,
    ENV_RELEASE
} EnvelopeState;

typedef struct {
    EnvelopeState state;
    double level;           // Current envelope level (0.0 to 1.0)
    double attack_rate;     // Level increase per sample
    double decay_rate;      // Level decrease per sample
    double sustain_level;   // Target level for sustain
    double release_rate;    // Level decrease per sample
} Envelope;

double envelope_process(Envelope *env) {
    switch (env->state) {
        case ENV_ATTACK:
            env->level += env->attack_rate;
            if (env->level >= 1.0) {
                env->level = 1.0;
                env->state = ENV_DECAY;
            }
            break;
        case ENV_DECAY:
            env->level -= env->decay_rate;
            if (env->level <= env->sustain_level) {
                env->level = env->sustain_level;
                env->state = ENV_SUSTAIN;
            }
            break;
        case ENV_SUSTAIN:
            // Hold at sustain level
            break;
        case ENV_RELEASE:
            env->level -= env->release_rate;
            if (env->level <= 0.0) {
                env->level = 0.0;
                env->state = ENV_IDLE;
            }
            break;
        case ENV_IDLE:
            env->level = 0.0;
            break;
    }
    return env->level;
}

Exponential curves for more natural sound:

// Instead of linear: level += rate
// Use exponential: level *= multiplier
double attack_coef = exp(-log((1.0 + target) / target) / (attack_time * sample_rate));

Questions to guide implementation:

  1. What happens if you press a new note during the release phase?
  2. Why do exponential curves sound more natural than linear?
  3. How do you calculate rate from time (e.g., 100ms attack)?
  4. What’s the difference between an amplitude envelope and a filter envelope?

Learning milestones:

  1. Notes have proper attack/release → You understand envelope basics
  2. Preset “instruments” sound distinct → You understand shaping timbre with envelopes
  3. No clicks at note boundaries → You handle transitions correctly
  4. Polyphonic envelopes work → Each voice has independent envelope state

Project 5: MIDI File Parser and Player

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: C++, Rust
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: MIDI Protocol / File Formats
  • Software or Tool: Standard MIDI File format
  • Main Book: “The Audio Programming Book” by Boulanger & Lazzarini

What you’ll build: A parser for Standard MIDI Files (.mid) that can read any MIDI file and play it back using your synthesizer, showing notes as they play.

Why it teaches music programming: MIDI is the universal language of digital music. Every DAW, synthesizer, and music software speaks MIDI. Understanding the format teaches you how musical events are encoded and timed.

Core challenges you’ll face:

  • Parsing variable-length quantities → maps to MIDI’s compact encoding
  • Handling multiple tracks → maps to synchronizing parallel event streams
  • Delta time to absolute time conversion → maps to event scheduling
  • Implementing MIDI events → maps to note on/off, control changes, tempo

Key Concepts:

  • Standard MIDI File format: SMF Specification
  • Variable-length quantities: MIDI’s compact integer encoding
  • MIDI messages: Note on (0x90), note off (0x80), control change (0xB0)
  • Tempo and timing: Microseconds per quarter note, ticks per quarter note

Difficulty: Intermediate Time estimate: 2 weeks Prerequisites: Projects 3-4 completed

Real world outcome:

$ ./midiplay beethoven_ode_to_joy.mid
MIDI File Player
================
File: beethoven_ode_to_joy.mid
Format: 1 (multiple tracks)
Tracks: 4
Division: 480 ticks/quarter note

Track 0: Tempo track
  Tempo: 120 BPM

Track 1: Melody (Channel 1)
  Instrument: Acoustic Grand Piano
  Notes: 234

Track 2: Harmony (Channel 2)
  Instrument: String Ensemble
  Notes: 156

Track 3: Bass (Channel 3)
  Instrument: Acoustic Bass
  Notes: 89

Playing... (Ctrl-C to stop)

Time: 0:23 / 2:45  ████████████░░░░░░░░░░░░░░░░░░

╔═══════════════════════════════════════════════════════════╗
║ C5  │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
║ B4  │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
║ A4  │░░░░░░░░░░██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
║ G4  │░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
║ F4  │░░░░░░░░░░░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░░░░░│
║ E4  │░░░░░░██████░░░░░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░│
║ D4  │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
║ C4  │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
╚═══════════════════════════════════════════════════════════╝

$ ./midiinfo song.mid
MIDI File Analysis
==================
Duration: 3:45
Notes: 1,234
Tempo changes: 3
Time signature: 4/4
Key signature: C major

Events by type:
  Note On:        1,234
  Note Off:       1,234
  Control Change:   156
  Program Change:     4
  Tempo Change:       3

Implementation Hints:

MIDI file structure:

Header Chunk: "MThd" + length(6) + format + num_tracks + division
Track Chunk:  "MTrk" + length + events...

Each event: delta_time(variable) + event_data

Variable-length quantity parsing:

uint32_t read_variable_length(FILE *f) {
    uint32_t value = 0;
    uint8_t byte;
    do {
        byte = fgetc(f);
        value = (value << 7) | (byte & 0x7F);
    } while (byte & 0x80);  // Continue while high bit set
    return value;
}

MIDI event structure:

typedef struct {
    uint32_t delta_time;    // Ticks since last event
    uint32_t absolute_time; // Ticks from start
    uint8_t type;           // Event type
    uint8_t channel;        // MIDI channel (0-15)
    uint8_t data1;          // First data byte
    uint8_t data2;          // Second data byte (if present)
} MidiEvent;

// Note On: type=0x90, data1=note, data2=velocity
// Note Off: type=0x80, data1=note, data2=velocity

Questions to guide implementation:

  1. What’s “running status” and why does MIDI use it?
  2. How do you merge multiple tracks into a single timeline?
  3. How do you convert ticks to milliseconds given tempo?
  4. What’s the difference between MIDI Format 0, 1, and 2?

Learning milestones:

  1. You correctly parse the header → You understand the file structure
  2. You extract all events from tracks → You understand variable-length quantities
  3. Playback is correctly timed → You understand delta time and tempo
  4. Multi-track files play correctly → You can merge parallel event streams

Project 6: Digital Filter Implementation (Low-Pass, High-Pass, Band-Pass)

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: C++, Rust
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Digital Signal Processing / Filters
  • Software or Tool: Biquad filters, IIR/FIR
  • Main Book: “Designing Audio Effect Plugins in C++” by Will Pirkle

What you’ll build: Implement classic audio filters—low-pass, high-pass, band-pass, and a resonant filter—and use them to shape synthesizer tones in real-time with adjustable cutoff frequency and resonance.

Why it teaches music programming: Filters are fundamental to synthesis and audio processing. The “wah-wah” effect, the warmth of analog synths, the clarity of EQ—all come from filters. Understanding filter math opens the door to all audio effects.

Core challenges you’ll face:

  • Implementing biquad filters → maps to IIR filter fundamentals
  • Calculating filter coefficients → maps to the math behind filter design
  • Preventing instability → maps to numerical precision issues
  • Real-time parameter changes → maps to smoothing coefficient updates

Key Concepts:

  • Biquad filter: “Designing Audio Effect Plugins” Chapter 6 - Will Pirkle
  • Transfer function H(z): How filters are described mathematically
  • Cutoff frequency and Q/resonance: The two main filter parameters
  • Filter types: Audio EQ Cookbook by Robert Bristow-Johnson

Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 3-4 completed, basic understanding of complex numbers helpful

Real world outcome:

$ ./filter_synth
Filter Synthesizer
==================
Oscillator: Sawtooth 440 Hz

Filter: Low-Pass (Butterworth)
  Cutoff:    1000 Hz  ████████░░░░░░░░░░░░  [←/→ to adjust]
  Resonance: 1.0      ██░░░░░░░░░░░░░░░░░░  [↑/↓ to adjust]

Envelope → Filter: OFF [E to toggle]

[Playing... hear the bright sawtooth become muffled as cutoff decreases]

> [Press ←] Cutoff: 500 Hz
  # Sound becomes more muffled

> [Press ↑↑↑] Resonance: 4.0
  # Hear the resonant peak at cutoff frequency

> [Press 'E'] Filter envelope ON
  # Now filter opens on each note attack like an analog synth

Filter Types: [1-5]
  1. Low-Pass   - Removes highs (current)
  2. High-Pass  - Removes lows
  3. Band-Pass  - Keeps only a band
  4. Notch      - Removes a band
  5. Peak EQ    - Boosts/cuts a band

$ ./filter_analyze
Frequency Response Analyzer
===========================
Filter: Low-Pass, Cutoff: 1000 Hz, Q: 0.707

Frequency Response:
     │
  0dB├─────────────────╮
     │                  ╲
 -6dB│                   ╲
     │                    ╲
-12dB│                     ╲
     │                      ╲
-24dB│                       ╲
     │                        ╲_____
     └────────────────────────────────
     100    500   1k   2k   5k   10k   Hz

Implementation Hints:

Biquad filter difference equation:

y[n] = (b0/a0)*x[n] + (b1/a0)*x[n-1] + (b2/a0)*x[n-2]
                    - (a1/a0)*y[n-1] - (a2/a0)*y[n-2]

Biquad filter structure:

typedef struct {
    double b0, b1, b2;  // Numerator coefficients
    double a0, a1, a2;  // Denominator coefficients
    double x1, x2;      // Input history
    double y1, y2;      // Output history
} Biquad;

double biquad_process(Biquad *f, double input) {
    double output = (f->b0 * input + f->b1 * f->x1 + f->b2 * f->x2
                   - f->a1 * f->y1 - f->a2 * f->y2) / f->a0;
    f->x2 = f->x1;
    f->x1 = input;
    f->y2 = f->y1;
    f->y1 = output;
    return output;
}

Low-pass filter coefficients (from Audio EQ Cookbook):

void lowpass_coefficients(Biquad *f, double freq, double Q, double sample_rate) {
    double w0 = 2.0 * M_PI * freq / sample_rate;
    double alpha = sin(w0) / (2.0 * Q);

    f->b0 = (1.0 - cos(w0)) / 2.0;
    f->b1 = 1.0 - cos(w0);
    f->b2 = (1.0 - cos(w0)) / 2.0;
    f->a0 = 1.0 + alpha;
    f->a1 = -2.0 * cos(w0);
    f->a2 = 1.0 - alpha;
}

Questions to guide implementation:

  1. What happens to a filter when Q (resonance) gets very high?
  2. Why do we need to normalize by a0?
  3. What causes filter instability, and how do you prevent it?
  4. How do you smoothly change cutoff frequency without clicks?

Learning milestones:

  1. Low-pass filter works → You understand basic biquad implementation
  2. Resonance creates a peak → You understand Q parameter
  3. All filter types work → You can derive coefficients from the cookbook
  4. Real-time modulation is smooth → You understand coefficient interpolation

Project 7: Delay and Echo Effects

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: C++, Rust
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Audio Effects / Delay Lines
  • Software or Tool: Circular buffers, real-time audio
  • Main Book: “Designing Audio Effect Plugins in C++” by Will Pirkle

What you’ll build: A delay effect with adjustable delay time, feedback, and mix. Extend it to create ping-pong delay (bouncing between left and right) and multi-tap delay.

Why it teaches music programming: Delay is the foundation of many effects—chorus, flanger, reverb. Understanding delay lines teaches you about circular buffers, a fundamental DSP data structure. Plus, delay effects sound great!

Core challenges you’ll face:

  • Implementing circular buffers → maps to efficient memory-limited delay
  • Fractional delay interpolation → maps to sub-sample accuracy
  • Feedback control → maps to preventing runaway gain
  • Stereo processing → maps to multi-channel audio

Key Concepts:

  • Circular buffer: Fixed-size buffer with wrapping read/write pointers
  • Feedback: Output fed back to input (gain < 1 to prevent explosion)
  • Wet/dry mix: Blending processed and original signal
  • Interpolation: Linear or cubic for fractional delays

Difficulty: Intermediate Time estimate: 1 week Prerequisites: Projects 3-4 completed

Real world outcome:

$ ./delay_effect --input guitar.wav --output guitar_delay.wav
Delay Effect Processor
======================
Input: guitar.wav (stereo, 44100 Hz)

Delay Time:  250 ms  ████████████░░░░░░░░  [←/→]
Feedback:    50%     ██████████░░░░░░░░░░  [↑/↓]
Mix:         50%     ██████████░░░░░░░░░░  [+/-]
Mode:        Mono    [M] Toggle ping-pong

Processing... Done!
Written to: guitar_delay.wav

$ ./delay_realtime
Real-Time Delay
===============
Listening on default input...
Playing on default output...

Delay:    500 ms (tap SPACE to set from tempo)
Feedback: 40%
Mix:      50%

Mode: PING-PONG
  L ████████░░░░░░░░░░░░ (echo 1, 3, 5...)
  R ░░░░░░░░████████░░░░ (echo 2, 4, 6...)

[TAP] [TAP] [TAP] → Tempo: 120 BPM → Delay: 500 ms (quarter note)

$ ./multitap_delay
Multi-Tap Delay (4 taps)
========================
Tap 1:  125 ms,  80%, Pan: L30
Tap 2:  250 ms,  60%, Pan: R30
Tap 3:  375 ms,  40%, Pan: L60
Tap 4:  500 ms,  20%, Pan: R60

[Creates complex rhythmic echoes]

Implementation Hints:

Circular buffer delay line:

typedef struct {
    float *buffer;
    int size;           // Buffer size in samples
    int write_pos;      // Current write position
} DelayLine;

void delay_write(DelayLine *d, float sample) {
    d->buffer[d->write_pos] = sample;
    d->write_pos = (d->write_pos + 1) % d->size;
}

float delay_read(DelayLine *d, int delay_samples) {
    int read_pos = (d->write_pos - delay_samples + d->size) % d->size;
    return d->buffer[read_pos];
}

Delay effect with feedback:

float process_delay(DelayLine *d, float input, float delay_ms,
                    float feedback, float mix, float sample_rate) {
    int delay_samples = (int)(delay_ms * sample_rate / 1000.0);
    float delayed = delay_read(d, delay_samples);
    float output = input * (1.0 - mix) + delayed * mix;
    delay_write(d, input + delayed * feedback);
    return output;
}

Linear interpolation for fractional delays:

float delay_read_interp(DelayLine *d, float delay_samples) {
    int delay_int = (int)delay_samples;
    float frac = delay_samples - delay_int;
    float s1 = delay_read(d, delay_int);
    float s2 = delay_read(d, delay_int + 1);
    return s1 + frac * (s2 - s1);
}

Questions to guide implementation:

  1. What happens if feedback >= 1.0?
  2. Why do we need interpolation for delay time modulation?
  3. How do you calculate delay time from BPM?
  4. What’s the difference between pre-delay and post-delay mixing?

Learning milestones:

  1. Basic delay works → You understand circular buffers
  2. Feedback creates echoes that decay → You understand feedback loops
  3. Ping-pong creates stereo movement → You understand stereo processing
  4. Tempo sync works → You understand musical timing

Project 8: Simple Reverb (Schroeder Reverb)

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: C++, Rust
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Reverb Algorithms / Room Simulation
  • Software or Tool: Comb filters, allpass filters
  • Main Book: “Designing Audio Effect Plugins in C++” by Will Pirkle

What you’ll build: A reverb effect using the classic Schroeder algorithm—combining multiple comb filters and allpass filters to simulate room reflections.

Why it teaches music programming: Reverb is one of the most complex and rewarding effects to implement. It simulates the physical acoustics of spaces. The Schroeder reverb, while simple, teaches the fundamental building blocks used in all algorithmic reverbs.

Core challenges you’ll face:

  • Implementing comb filters → maps to creating repeated echoes
  • Implementing allpass filters → maps to diffusing the sound
  • Tuning delay times → maps to avoiding metallic coloration
  • Managing decay time (RT60) → maps to room size simulation

Key Concepts:

  • Schroeder reverb: Original paper “Natural Sounding Artificial Reverberation” (1962)
  • Comb filter: Delay + feedback creates echoes
  • Allpass filter: Delays without changing frequency response
  • RT60: Time for reverb to decay 60dB

Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Project 7 completed

Real world outcome:

$ ./reverb --input vocals.wav --output vocals_reverb.wav
Schroeder Reverb
================
Input: vocals.wav

Room Size:   Medium    [1-5: Small to Large]
Decay Time:  2.0s      [←/→ to adjust]
Damping:     50%       [↑/↓ to adjust]
Mix:         30%       [+/- to adjust]

Comb filters: 4
  Delay times: 29.7ms, 37.1ms, 41.1ms, 43.7ms

Allpass filters: 2
  Delay times: 5.0ms, 1.7ms

Processing... Done!
Written to: vocals_reverb.wav

$ ./reverb_realtime
Real-Time Reverb
================
Preset: Concert Hall
  Room size: Large
  Decay: 3.2s
  Damping: 40%
  Pre-delay: 25ms

Listening... [Audio passes through reverb in real-time]

Presets:
  1. Small Room     (0.5s decay)
  2. Medium Room    (1.2s decay)
  3. Large Hall     (2.5s decay)
  4. Concert Hall   (3.2s decay)
  5. Cathedral      (5.0s decay)
  6. Plate          (metallic character)

Implementation Hints:

Comb filter (FIR feedback comb):

typedef struct {
    float *buffer;
    int size;
    int pos;
    float feedback;
    float damping;      // Low-pass in feedback loop
    float damp_state;   // State for damping filter
} CombFilter;

float comb_process(CombFilter *c, float input) {
    float output = c->buffer[c->pos];

    // One-pole low-pass for damping
    c->damp_state = output * (1.0 - c->damping) + c->damp_state * c->damping;

    c->buffer[c->pos] = input + c->damp_state * c->feedback;
    c->pos = (c->pos + 1) % c->size;

    return output;
}

Allpass filter:

typedef struct {
    float *buffer;
    int size;
    int pos;
    float gain;
} AllpassFilter;

float allpass_process(AllpassFilter *a, float input) {
    float delayed = a->buffer[a->pos];
    float output = -input + delayed;
    a->buffer[a->pos] = input + delayed * a->gain;
    a->pos = (a->pos + 1) % a->size;
    return output;
}

Schroeder reverb structure:

// 4 parallel comb filters → 2 series allpass filters
float reverb_process(Reverb *r, float input) {
    float comb_sum = 0;
    for (int i = 0; i < 4; i++) {
        comb_sum += comb_process(&r->combs[i], input);
    }
    comb_sum *= 0.25;  // Average

    float output = comb_sum;
    for (int i = 0; i < 2; i++) {
        output = allpass_process(&r->allpasses[i], output);
    }
    return output;
}

Delay time selection (from Schroeder): Delay times should be mutually prime (no common factors) to avoid flutter:

  • Comb delays: 29.7, 37.1, 41.1, 43.7 ms
  • Allpass delays: 5.0, 1.7 ms

Questions to guide implementation:

  1. Why use parallel comb filters instead of series?
  2. How does damping affect the “brightness” of the reverb?
  3. Why must delay times be mutually prime?
  4. How do you calculate feedback gain from desired RT60?

Learning milestones:

  1. Single comb filter creates echoes → You understand basic reverb building blocks
  2. Four combs together sound more natural → You understand diffusion
  3. Allpass filters add density → You understand the full Schroeder structure
  4. Different room sizes sound correct → You understand parameter tuning

Project 9: Wavetable Synthesizer

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: C++, Rust
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Wavetable Synthesis / Advanced Oscillators
  • Software or Tool: Wavetables, interpolation
  • Main Book: “Designing Software Synthesizer Plugins in C++” by Will Pirkle

What you’ll build: A wavetable synthesizer that reads waveforms from tables, supports morphing between wavetables, and eliminates aliasing using band-limited wavetables.

Why it teaches music programming: Wavetable synthesis is how most professional soft synths work. It’s more efficient than additive synthesis and more flexible than simple oscillators. Understanding wavetables teaches you about anti-aliasing, interpolation, and modern synth architecture.

Core challenges you’ll face:

  • Building band-limited wavetables → maps to anti-aliasing techniques
  • Interpolation between samples → maps to audio quality
  • Wavetable morphing → maps to smooth timbral evolution
  • Multiple wavetables per octave → maps to mip-mapping for audio

Key Concepts:

  • Wavetable synthesis: WolfSound Wavetable Algorithm
  • Band-limiting: Pre-computing tables with harmonics removed
  • Interpolation: Linear, cubic, or sinc for reading between samples
  • Mip-mapping: Different tables for different octaves

Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Projects 2-4 completed

Real world outcome:

$ ./wavetable_synth
Wavetable Synthesizer
=====================

Wavetable: [1] Basic    [2] Analog    [3] Digital    [4] Vocal
Currently: Analog

Position:  50%   █████████░░░░░░░░░░░  [Mouse X or ←/→]
  Morphing between: Saw → Square

Wavetable visualization:
Frame 0 (Saw):    Frame 16 (Mix):   Frame 31 (Square):
    ╱│                 ╱─┐               ┌──┐
   ╱ │                ╱  │               │  │
  ╱  │               ╱   └──             │  └──
 ╱   │              ╱                    │

Filter:    1200 Hz
Envelope:  A:50ms D:200ms S:70% R:300ms

Voices: 8 (polyphonic)

Keys: A S D F G H J K (white keys)
      W E   T Y U     (black keys)

[Play notes and move wavetable position to morph the sound]

$ ./wavetable_editor
Wavetable Editor
================
Creating new wavetable: custom.wt

Drawing mode: Harmonic additive
  Fundamental: ████████████████████ (100%)
  2nd harmonic: ██████████░░░░░░░░░░ (50%)
  3rd harmonic: ████░░░░░░░░░░░░░░░░ (20%)
  4th harmonic: ░░░░░░░░░░░░░░░░░░░░ (0%)
  ...

Frame 1 of 32
[Draw harmonics for each frame to create evolving wavetable]

Preview: [SPACE to play current frame]
Save: [S to save wavetable]

Implementation Hints:

Wavetable structure:

#define WAVETABLE_SIZE 2048
#define NUM_OCTAVE_TABLES 10  // For anti-aliasing

typedef struct {
    float tables[NUM_OCTAVE_TABLES][WAVETABLE_SIZE];
    int num_frames;           // For morphing wavetables
    float frames[32][WAVETABLE_SIZE];  // Multiple frames to morph between
} Wavetable;

Basic wavetable playback with linear interpolation:

float wavetable_read(Wavetable *wt, double phase, int octave_table) {
    double index = phase * WAVETABLE_SIZE;
    int i0 = (int)index;
    int i1 = (i0 + 1) % WAVETABLE_SIZE;
    float frac = index - i0;

    return wt->tables[octave_table][i0] * (1.0 - frac)
         + wt->tables[octave_table][i1] * frac;
}

Generating band-limited wavetables:

void generate_bandlimited_saw(Wavetable *wt, float sample_rate) {
    for (int octave = 0; octave < NUM_OCTAVE_TABLES; octave++) {
        float base_freq = 20.0 * pow(2, octave);  // Frequency this table is for
        int max_harmonic = (int)(sample_rate / 2.0 / base_freq);

        // Additive synthesis with limited harmonics
        for (int i = 0; i < WAVETABLE_SIZE; i++) {
            double phase = (double)i / WAVETABLE_SIZE;
            double sample = 0;
            for (int h = 1; h <= max_harmonic; h++) {
                sample += sin(2.0 * M_PI * phase * h) / h;
            }
            wt->tables[octave][i] = sample * (2.0 / M_PI);
        }
    }
}

Wavetable morphing:

float wavetable_morph(Wavetable *wt, double phase, float morph_pos) {
    float frame_pos = morph_pos * (wt->num_frames - 1);
    int f0 = (int)frame_pos;
    int f1 = (f0 + 1) % wt->num_frames;
    float frac = frame_pos - f0;

    float s0 = wavetable_read_frame(wt, phase, f0);
    float s1 = wavetable_read_frame(wt, phase, f1);

    return s0 * (1.0 - frac) + s1 * frac;
}

Questions to guide implementation:

  1. Why do we need different wavetables for different octaves?
  2. How does wavetable size affect quality and CPU usage?
  3. What interpolation method sounds best for wavetables?
  4. How do you create interesting morph paths in a wavetable?

Learning milestones:

  1. Basic wavetable playback works → You understand table lookup synthesis
  2. No aliasing at high frequencies → You understand band-limiting
  3. Morphing sounds smooth → You understand frame interpolation
  4. Custom wavetables work → You can create your own sounds

Project 10: Polyphonic Synthesizer with Voice Allocation

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: C++, Rust
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Polyphony / Voice Management
  • Software or Tool: Voice allocation, note stealing
  • Main Book: “Designing Software Synthesizer Plugins in C++” by Will Pirkle

What you’ll build: A fully polyphonic synthesizer that can play multiple notes simultaneously, with intelligent voice allocation, note stealing, and per-voice envelopes and filters.

Why it teaches music programming: Real synthesizers must handle chords and fast playing. Voice allocation is a classic computer science problem applied to music—managing limited resources (voices) for unlimited demands (notes). This is how professional synths work.

Core challenges you’ll face:

  • Managing a pool of voices → maps to resource allocation
  • Voice stealing strategies → maps to what to do when voices run out
  • Per-voice state management → maps to independent processing per note
  • Efficient mixing → maps to summing multiple audio streams

Key Concepts:

  • Voice architecture: Each voice = oscillator + envelope + filter
  • Voice stealing: Oldest, quietest, or same-note strategies
  • Unison/detune: Multiple voices per note for thickness
  • Portamento/glide: Smooth pitch transitions

Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 4, 6, 9 completed

Real world outcome:

$ ./polysynth
Polyphonic Synthesizer
======================
Voices: 8 max

Oscillator: Wavetable (Analog Saw)
Filter: Low-pass 2000Hz, Q=2
Envelope: A:10ms D:100ms S:80% R:200ms
Filter Env: A:5ms D:50ms S:50% R:100ms (amount: 50%)

Voice Status:
  [1] C4  ████████░░ (sustain)
  [2] E4  ██████░░░░ (decay)
  [3] G4  ████░░░░░░ (release)
  [4] idle
  [5] idle
  [6] idle
  [7] idle
  [8] idle

Active: 3    Stealing: OFF    Mode: Poly

Controls:
  [Tab] Mono/Poly mode
  [U] Unison mode (stack voices)
  [P] Portamento on/off
  [L] Legato mode

$ ./polysynth --mode mono --portamento 100
Monophonic Synth with Portamento
================================
Playing single voice with 100ms glide between notes...

> [Play C4] Note: C4 (261.63 Hz)
> [Play E4] Gliding to E4... ████████████ 329.63 Hz
> [Play G4] Gliding to G4... ████████████ 392.00 Hz

$ ./polysynth --unison 4 --detune 0.1
Unison Mode (4 voices per note)
===============================
Detuning: ±10 cents
  Voice 1: -10 cents
  Voice 2: -3 cents
  Voice 3: +3 cents
  Voice 4: +10 cents

[Super thick "supersaw" sound]

Implementation Hints:

Voice structure:

typedef struct {
    int active;
    int note;               // MIDI note number
    float frequency;
    double phase;
    Envelope amp_env;
    Envelope filter_env;
    Biquad filter;
    int age;                // For voice stealing (samples since note on)
} Voice;

typedef struct {
    Voice voices[MAX_VOICES];
    int num_voices;
    int voice_stealing;     // 0=off, 1=oldest, 2=quietest
} Synth;

Voice allocation:

int allocate_voice(Synth *s, int note) {
    // First, look for a free voice
    for (int i = 0; i < s->num_voices; i++) {
        if (!s->voices[i].active) {
            return i;
        }
    }

    // No free voice - steal one
    if (s->voice_stealing == STEAL_OLDEST) {
        int oldest = 0;
        for (int i = 1; i < s->num_voices; i++) {
            if (s->voices[i].age > s->voices[oldest].age) {
                oldest = i;
            }
        }
        return oldest;
    }

    // ... other stealing strategies ...
    return -1;  // No voice available
}

Note on/off:

void note_on(Synth *s, int note, int velocity) {
    int v = allocate_voice(s, note);
    if (v < 0) return;

    Voice *voice = &s->voices[v];
    voice->active = 1;
    voice->note = note;
    voice->frequency = midi_to_freq(note);
    voice->age = 0;
    envelope_trigger(&voice->amp_env);
    envelope_trigger(&voice->filter_env);
}

void note_off(Synth *s, int note) {
    for (int i = 0; i < s->num_voices; i++) {
        if (s->voices[i].active && s->voices[i].note == note) {
            envelope_release(&s->voices[i].amp_env);
            envelope_release(&s->voices[i].filter_env);
        }
    }
}

Processing all voices:

float synth_process(Synth *s) {
    float output = 0;
    for (int i = 0; i < s->num_voices; i++) {
        if (s->voices[i].active) {
            output += voice_process(&s->voices[i]);
        }
    }
    return output / s->num_voices;  // Normalize
}

Questions to guide implementation:

  1. What happens when you press the same note twice?
  2. How do you handle releasing a note that was already stolen?
  3. What’s the difference between polyphonic and paraphonic?
  4. How does unison mode differ from normal polyphony?

Learning milestones:

  1. Multiple simultaneous notes work → You understand voice allocation
  2. Voice stealing sounds musical → You understand note priority
  3. Each voice has independent envelope → You understand per-voice state
  4. Unison/detune creates thick sound → You understand layering

Project 11: MIDI Sequencer and Step Sequencer

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: C++, Rust
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Sequencing / Music Composition
  • Software or Tool: MIDI, timing, patterns
  • Main Book: “The Audio Programming Book” by Boulanger & Lazzarini

What you’ll build: A step sequencer for creating rhythmic patterns and a piano roll sequencer for composing melodies. Support saving/loading patterns and exporting to MIDI.

Why it teaches music programming: Sequencers are at the heart of DAWs and drum machines. Building one teaches you about musical timing, pattern representation, and the relationship between visual representation and audio output.

Core challenges you’ll face:

  • Accurate timing at audio sample level → maps to sample-accurate sequencing
  • Pattern representation → maps to music data structures
  • Tempo and time signature → maps to musical time
  • MIDI output → maps to controlling external gear

Key Concepts:

  • PPQ (Pulses Per Quarter): Internal clock resolution
  • Step sequencer: Fixed grid, usually 16 steps
  • Piano roll: Free-form note placement on a grid
  • Quantization: Snapping notes to a grid

Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Projects 5, 10 completed

Real world outcome:

$ ./step_sequencer
Step Sequencer (16 steps)
=========================
BPM: 120  Time Sig: 4/4  Swing: 0%

          1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16
Kick    │ X │   │   │   │ X │   │   │   │ X │   │   │   │ X │   │   │   │
Snare   │   │   │   │   │ X │   │   │   │   │   │   │   │ X │   │   │   │
HiHat   │ X │   │ X │   │ X │   │ X │   │ X │   │ X │   │ X │   │ X │   │
OpenHH  │   │   │   │   │   │   │   │ X │   │   │   │   │   │   │   │ X │
        ───────────────────────────────────────────────────────────────
              ^
           [playing]

Controls:
  Arrow keys: Navigate
  Space: Toggle step
  +/-: Adjust BPM
  S: Add swing
  P: Play/Pause
  L: Pattern length

Patterns: A B C D [1-4 to select, Shift+1-4 to copy]
Song mode: [M] to enable

$ ./piano_roll
Piano Roll Sequencer
====================
BPM: 120  Grid: 1/16  Quantize: ON

     Bar 1           Bar 2           Bar 3           Bar 4
     ┌───────────────┬───────────────┬───────────────┬───────────────┐
  C5 │               │               │               │               │
  B4 │               │       ████    │               │               │
  A4 │       ████    │               │       ████    │               │
  G4 │   ████        │   ████        │   ████        │       ████████│
  F4 │               │               │               │               │
  E4 │████           │               │████           │               │
  D4 │               │               │               │               │
  C4 │────────────────────────────────────────────────────────────────│
     └───────────────┴───────────────┴───────────────┴───────────────┘
                         ^
                      [cursor]

Controls:
  Arrow keys: Move cursor
  Enter: Place/extend note
  Delete: Remove note
  [/]: Zoom in/out
  G: Change grid size (1/4, 1/8, 1/16, 1/32)

$ ./sequencer --export song.mid
Exporting to MIDI...
Written: song.mid
  Tracks: 2
  Events: 234
  Duration: 1:30

Implementation Hints:

Step sequencer data structure:

#define NUM_STEPS 16
#define NUM_TRACKS 8

typedef struct {
    int active;         // Is this step on?
    int velocity;       // 0-127
    int note;           // MIDI note (for melodic sequencer)
} Step;

typedef struct {
    Step steps[NUM_STEPS];
    int sound_index;    // Which sound to trigger
    int mute;
    int solo;
} Track;

typedef struct {
    Track tracks[NUM_TRACKS];
    int current_step;
    int playing;
    double bpm;
    int swing;          // 0-100, shifts even steps
    double samples_per_step;
    double sample_counter;
} StepSequencer;

Sample-accurate timing:

void sequencer_process(StepSequencer *seq, float *output, int num_samples) {
    for (int i = 0; i < num_samples; i++) {
        if (seq->playing) {
            seq->sample_counter += 1.0;

            double step_samples = seq->samples_per_step;
            // Apply swing to even steps
            if (seq->current_step % 2 == 1) {
                step_samples *= (1.0 + seq->swing / 200.0);
            }

            if (seq->sample_counter >= step_samples) {
                seq->sample_counter -= step_samples;
                advance_step(seq);
                trigger_active_sounds(seq);
            }
        }

        output[i] = mix_all_sounds();
    }
}

BPM to samples calculation:

void set_bpm(StepSequencer *seq, double bpm, double sample_rate) {
    // Steps per beat depends on time signature and step resolution
    // For 16 steps in 4/4, that's 4 steps per beat
    double beats_per_second = bpm / 60.0;
    double steps_per_second = beats_per_second * 4;  // 4 steps per beat
    seq->samples_per_step = sample_rate / steps_per_second;
}

Questions to guide implementation:

  1. How do you handle step triggers that fall between sample boundaries?
  2. What does “swing” mean musically, and how do you implement it?
  3. How do you synchronize a sequencer to external MIDI clock?
  4. What’s the difference between step recording and real-time recording?

Learning milestones:

  1. Steps trigger at correct tempo → You understand sample-accurate timing
  2. Swing sounds musical → You understand microtiming
  3. Piano roll notes have correct duration → You understand note on/off timing
  4. MIDI export works → You understand the MIDI file format

Project 12: Distortion and Waveshaping Effects

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: C++, Rust
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Distortion / Non-Linear Processing
  • Software or Tool: Waveshaping, oversampling
  • Main Book: “Designing Audio Effect Plugins in C++” by Will Pirkle

What you’ll build: A collection of distortion effects—hard clipping, soft clipping, tube saturation, bitcrushing—with oversampling to reduce aliasing artifacts.

Why it teaches music programming: Distortion is non-linear processing—the output isn’t a simple scaled version of the input. This creates harmonics (new frequencies). Understanding distortion teaches you about signal theory and why oversampling matters.

Core challenges you’ll face:

  • Implementing various waveshaping functions → maps to transfer functions
  • Oversampling to prevent aliasing → maps to anti-aliasing in non-linear processing
  • Mixing dry and wet signals → maps to parallel processing
  • Tone shaping → maps to pre/post filtering

Key Concepts:

  • Transfer function: Input→output mapping curve
  • Aliasing in distortion: Non-linearity creates harmonics that can alias
  • Oversampling: Process at higher sample rate, then downsample
  • Tube simulation: Asymmetric soft clipping

Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Project 6 completed

Real world outcome:

$ ./distortion
Distortion Effect
=================

Type: [1] Hard Clip  [2] Soft Clip  [3] Tube  [4] Fuzz  [5] Bitcrush
Currently: Tube Saturation

Drive:      80%  ████████████████░░░░  [←/→]
Tone:       60%  ████████████░░░░░░░░  [↑/↓]
Mix:        70%  ██████████████░░░░░░  [+/-]
Oversample: 4x   [O to cycle: 1x, 2x, 4x, 8x]

Transfer function visualization:
Output
   1 │           ___----
     │        _--
   0 │───────/──────────────
     │    __/
  -1 │___/
     └──────────────────────
    -1        0         1  Input

$ ./distortion --analyze
Harmonic Analysis (with distortion)
===================================
Input: 440 Hz sine wave

Without distortion:
  440 Hz: ████████████████████ (100%)

With tube saturation (80% drive):
  440 Hz:  ████████████████████ (100%)  Fundamental
  880 Hz:  ████████░░░░░░░░░░░░ (40%)   2nd harmonic
  1320 Hz: ████░░░░░░░░░░░░░░░░ (20%)   3rd harmonic
  1760 Hz: ██░░░░░░░░░░░░░░░░░░ (10%)   4th harmonic
  ...

Implementation Hints:

Common waveshaping functions:

// Hard clipping
float hard_clip(float x, float threshold) {
    if (x > threshold) return threshold;
    if (x < -threshold) return -threshold;
    return x;
}

// Soft clipping (tanh)
float soft_clip(float x, float drive) {
    return tanh(x * drive);
}

// Tube-style (asymmetric)
float tube_saturation(float x, float drive) {
    float input = x * drive;
    if (input >= 0) {
        return tanh(input);  // Soft clip positive
    } else {
        return tanh(input * 0.5) * 2.0;  // Different curve for negative
    }
}

// Fuzz (extreme clipping + filter)
float fuzz(float x, float drive) {
    float clipped = tanh(x * drive * 10.0);  // Heavy saturation
    return clipped;  // Usually followed by tone filter
}

// Bitcrusher
float bitcrush(float x, int bits, float sample_hold_factor) {
    float levels = pow(2, bits);
    return floor(x * levels) / levels;
}

Oversampling structure:

typedef struct {
    int factor;         // 2x, 4x, 8x
    Biquad upsample_filter;
    Biquad downsample_filter;
    float *buffer;
} Oversampler;

float process_with_oversampling(Oversampler *os, float input,
                                  float (*distort)(float, float), float drive) {
    // Upsample (insert zeros and filter)
    float upsampled[8];  // Max 8x
    for (int i = 0; i < os->factor; i++) {
        upsampled[i] = (i == 0) ? input * os->factor : 0;
        upsampled[i] = biquad_process(&os->upsample_filter, upsampled[i]);
    }

    // Apply distortion at higher sample rate
    for (int i = 0; i < os->factor; i++) {
        upsampled[i] = distort(upsampled[i], drive);
    }

    // Downsample (filter and decimate)
    float output = 0;
    for (int i = 0; i < os->factor; i++) {
        output = biquad_process(&os->downsample_filter, upsampled[i]);
    }
    return output;
}

Questions to guide implementation:

  1. Why does distortion create harmonics?
  2. Why is aliasing worse with distortion than with filtering?
  3. How does oversampling factor affect CPU usage vs quality?
  4. What’s the difference between pre-distortion and post-distortion EQ?

Learning milestones:

  1. Basic clipping works → You understand waveshaping
  2. Different algorithms sound different → You understand transfer functions
  3. Oversampling reduces harshness → You understand aliasing in non-linear processing
  4. Your distortion sounds “musical” → You understand tone shaping

Project 13: Modular Synthesizer Architecture

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: C++, Rust
  • Coolness Level: Level 5: Pure Magic
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 4: Expert
  • Knowledge Area: Modular Architecture / Audio Graphs
  • Software or Tool: Signal flow, patching
  • Main Book: “Designing Software Synthesizer Plugins in C++” by Will Pirkle

What you’ll build: A modular synthesizer system where you can create oscillators, filters, envelopes, LFOs, and effects as separate modules, then connect them together in any configuration—like a virtual modular synth.

Why it teaches music programming: This is how professional audio software is architected. Max/MSP, Pure Data, VCV Rack, and audio plugin hosts all use this model. Understanding audio graphs and signal flow is essential for building complex audio applications.

Core challenges you’ll face:

  • Designing a module interface → maps to audio plugin architecture
  • Connecting modules (patching) → maps to audio graph processing
  • Handling feedback loops → maps to delay compensation
  • Control rate vs audio rate → maps to efficiency optimization

Key Concepts:

  • Audio graph: Modules as nodes, connections as edges
  • Pull vs push model: Who triggers processing?
  • Control rate: Slower updates for envelopes/LFOs
  • Modular patching: CV (control voltage) and audio signals

Difficulty: Expert Time estimate: 4+ weeks Prerequisites: Projects 4, 6, 9, 10 completed

Real world outcome:

$ ./modular
Modular Synthesizer
===================

Modules:
  [1] OSC1  (Wavetable)
  [2] OSC2  (Saw)
  [3] LFO1  (Sine, 2 Hz)
  [4] ENV1  (ADSR)
  [5] FILT1 (LP)
  [6] VCA1
  [7] OUT

Patch Bay:
  OSC1.out  ─────────────────────►  FILT1.in
  OSC2.out  ────►  (+) ─────────►  FILT1.in
  LFO1.out  ─────────────────────►  OSC1.pitch (mod amount: 0.5)
  LFO1.out  ─────────────────────►  FILT1.cutoff (mod amount: 0.3)
  ENV1.out  ─────────────────────►  VCA1.gain
  ENV1.out  ─────────────────────►  FILT1.cutoff (mod amount: 0.7)
  FILT1.out ─────────────────────►  VCA1.in
  VCA1.out  ─────────────────────►  OUT.in

Commands:
  add <module_type>         - Add a module
  connect <from> <to>       - Create connection
  disconnect <from> <to>    - Remove connection
  param <module> <param> <value>  - Set parameter
  show                      - Show current patch

> add delay
Added: [8] DELAY1
> connect VCA1.out DELAY1.in
Connected.
> connect DELAY1.out OUT.in
Connected.
> param DELAY1 time 300
DELAY1.time = 300 ms
> param DELAY1 feedback 0.5
DELAY1.feedback = 0.5

[You hear the modular synth with delay added to the chain]

$ ./modular --load bass_patch.mod
Loading patch: bass_patch.mod
Modules: 8
Connections: 12
Ready to play!

Implementation Hints:

Module interface:

typedef enum {
    SIGNAL_AUDIO,   // Full audio rate
    SIGNAL_CONTROL  // Control rate (1/64 or similar)
} SignalType;

typedef struct {
    char name[32];
    SignalType type;
    float value;
    float *buffer;  // For audio rate
} Port;

typedef struct Module {
    char name[32];
    Port *inputs;
    int num_inputs;
    Port *outputs;
    int num_outputs;
    void *state;    // Module-specific state

    void (*process)(struct Module *, int num_samples);
    void (*reset)(struct Module *);
    void (*free)(struct Module *);
} Module;

Connection/patch structure:

typedef struct {
    Module *source_module;
    int source_port;
    Module *dest_module;
    int dest_port;
    float amount;   // Modulation amount (0-1, or can be negative)
} Connection;

typedef struct {
    Module **modules;
    int num_modules;
    Connection *connections;
    int num_connections;
    int *processing_order;  // Topologically sorted
} ModularSynth;

Topological sort for processing order:

void update_processing_order(ModularSynth *synth) {
    // Build adjacency list from connections
    // Perform topological sort (Kahn's algorithm or DFS)
    // Store result in synth->processing_order

    // This ensures modules are processed in correct order
    // (sources before destinations)
}

Processing the graph:

void modular_process(ModularSynth *synth, int num_samples) {
    // Clear all input buffers
    for (int i = 0; i < synth->num_modules; i++) {
        Module *m = synth->modules[i];
        for (int j = 0; j < m->num_inputs; j++) {
            memset(m->inputs[j].buffer, 0, num_samples * sizeof(float));
        }
    }

    // Apply connections (sum into inputs)
    for (int i = 0; i < synth->num_connections; i++) {
        Connection *c = &synth->connections[i];
        float *src = c->source_module->outputs[c->source_port].buffer;
        float *dst = c->dest_module->inputs[c->dest_port].buffer;
        for (int s = 0; s < num_samples; s++) {
            dst[s] += src[s] * c->amount;
        }
    }

    // Process modules in sorted order
    for (int i = 0; i < synth->num_modules; i++) {
        Module *m = synth->modules[synth->processing_order[i]];
        m->process(m, num_samples);
    }
}

Questions to guide implementation:

  1. How do you handle feedback loops in the audio graph?
  2. What’s the difference between audio-rate and control-rate processing?
  3. How do you implement “CV to pitch” conversion (1V/octave)?
  4. How do you save and load patches?

Learning milestones:

  1. Simple chain works (osc→filter→out) → You understand audio graphs
  2. Multiple sources mix correctly → You understand signal summing
  3. Modulation works (LFO→pitch) → You understand control signals
  4. Arbitrary patches work → You’ve built a true modular system

Project 14: Audio Spectrum Analyzer (FFT)

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: C++, Rust, Python
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Fourier Transform / Spectral Analysis
  • Software or Tool: FFT, FFTW library (or roll your own)
  • Main Book: “Think DSP” by Allen B. Downey

What you’ll build: A real-time spectrum analyzer that shows the frequency content of audio—like the visualizers in audio software. Display frequency on the X-axis and amplitude on the Y-axis.

Why it teaches music programming: The FFT is one of the most important algorithms in audio. It converts time-domain signals to frequency-domain, allowing you to see what frequencies are present. This is the foundation of EQs, vocoders, pitch detection, and more.

Core challenges you’ll face:

  • Implementing or using FFT → maps to Fourier transform understanding
  • Windowing functions → maps to reducing spectral leakage
  • Amplitude to dB conversion → maps to perceptual audio measurement
  • Real-time visualization → maps to efficient display updates

Key Concepts:

  • DFT/FFT: Converting samples to frequency bins
  • Windowing: Hann, Hamming, Blackman windows
  • Spectral leakage: Why we need windowing
  • Bin frequencies: freq = bin * sample_rate / fft_size

Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 3, 6 completed, understanding of complex numbers helpful

Real world outcome:

$ ./spectrum_analyzer
Real-Time Spectrum Analyzer
===========================
FFT Size: 2048  Window: Hann  Overlap: 50%

Listening to default input...

dB
  0│                          ╭─╮
-12│          ╭──╮           │  │
-24│      ╭───╯  ╰───╮  ╭────╯  ╰───────────────────
-36│  ╭───╯          ╰──╯
-48│──╯
-60│
   └────────────────────────────────────────────────────
   20    100   500   1k    2k    5k   10k   20k    Hz

Peak: 440 Hz (-6 dB)  Fundamental detected: A4

Controls:
  [F] FFT size (512, 1024, 2048, 4096)
  [W] Window type (Hann, Hamming, Blackman, None)
  [S] Smoothing
  [L] Log/Linear frequency scale
  [P] Peak hold on/off

$ ./spectrum_analyzer --input synth.wav --output spectrum.png
Analyzing: synth.wav
Generating spectrogram image...
Written: spectrum.png

$ ./spectrum_analyzer --3d
3D Spectrogram (waterfall)
==========================
[Shows frequency vs time vs amplitude as 3D surface]

Implementation Hints:

Simple DFT (for understanding, but slow):

void dft(float *input, float *real, float *imag, int N) {
    for (int k = 0; k < N; k++) {
        real[k] = 0;
        imag[k] = 0;
        for (int n = 0; n < N; n++) {
            float angle = 2.0 * M_PI * k * n / N;
            real[k] += input[n] * cos(angle);
            imag[k] -= input[n] * sin(angle);
        }
    }
}

Using FFTW (much faster):

#include <fftw3.h>

fftw_plan plan;
double *in;
fftw_complex *out;

// Setup
in = fftw_alloc_real(N);
out = fftw_alloc_complex(N/2 + 1);
plan = fftw_plan_dft_r2c_1d(N, in, out, FFTW_MEASURE);

// Process
memcpy(in, samples, N * sizeof(double));
fftw_execute(plan);

// Get magnitudes
for (int i = 0; i <= N/2; i++) {
    double mag = sqrt(out[i][0]*out[i][0] + out[i][1]*out[i][1]);
    magnitudes[i] = 20 * log10(mag / N);  // dB
}

Windowing:

void apply_hann_window(float *samples, int N) {
    for (int i = 0; i < N; i++) {
        float window = 0.5 * (1.0 - cos(2.0 * M_PI * i / (N - 1)));
        samples[i] *= window;
    }
}

Questions to guide implementation:

  1. Why does FFT size affect frequency resolution?
  2. What does “spectral leakage” look like, and how does windowing fix it?
  3. How do you convert bin index to frequency in Hz?
  4. Why use logarithmic frequency and dB scales for display?

Learning milestones:

  1. DFT produces correct frequencies → You understand Fourier theory
  2. Windowing reduces “smearing” → You understand spectral leakage
  3. Real-time display works → You understand efficient FFT workflow
  4. Peak detection works → You can extract musical information

Project 15: Complete DAW-Like Application

  • File: LEARN_MUSIC_PROGRAMMING_C_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: C++
  • Coolness Level: Level 5: Pure Magic
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 5: Master
  • Knowledge Area: Full Audio Application / Integration
  • Software or Tool: Everything combined
  • Main Book: All previous books combined

What you’ll build: A simplified but complete music production application combining: multi-track audio recording/playback, MIDI sequencing, your synthesizer, effects (EQ, compression, reverb), mixing console with faders, and export to WAV/MIDI.

Why it teaches music programming: This is the capstone—integrating everything you’ve learned into a cohesive application. You’ll understand how Ableton, FL Studio, and Logic work at a fundamental level.

Core challenges you’ll face:

  • Multi-track timing synchronization → maps to sample-accurate audio engine
  • Plugin/effect chain management → maps to audio graph processing
  • UI/audio thread communication → maps to real-time programming patterns
  • Project file format design → maps to data serialization

Key Concepts:

  • Session management: Tracks, clips, automation
  • Mixer architecture: Channels, buses, master
  • Transport control: Play, pause, seek, loop
  • Undo/redo: State management

Difficulty: Master Time estimate: 2-3 months Prerequisites: All previous projects completed

Real world outcome:

$ ./minidaw
╔═══════════════════════════════════════════════════════════════════════╗
║  MiniDAW v1.0                                              [−][□][×]  ║
╠═══════════════════════════════════════════════════════════════════════╣
║  Transport: [|◄] [►] [■] [○]  BPM: 120  Time: 00:00:00  Loop: OFF     ║
╠═══════════════════════════════════════════════════════════════════════╣
║  Timeline                                                              ║
║  ─────────────────────────────────────────────────────────────────────║
║       │1    2    3    4   │5    6    7    8   │9    10   11   12  │   ║
║  ─────┼───────────────────┼───────────────────┼───────────────────┼   ║
║  Drums│████████████████████████████████████████████████████████████   ║
║  Bass │    ████████    ████████    ████████    ████████              ║
║  Synth│████        ████        ████        ████        ████████████   ║
║  Audio│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓              ║
║        ^                                                               ║
║     [cursor]                                                          ║
╠═══════════════════════════════════════════════════════════════════════╣
║  Mixer                                                                ║
║  ──────────────────────────────────────────────────────────────────── ║
║    Drums     Bass      Synth     Audio     Master                     ║
║    ┌───┐    ┌───┐     ┌───┐     ┌───┐     ┌───┐                      ║
║    │▓▓▓│    │▓▓ │     │▓▓▓│     │▓▓ │     │▓▓▓│                      ║
║    │▓▓▓│    │▓▓ │     │▓▓ │     │▓▓ │     │▓▓▓│                      ║
║    │▓▓ │    │▓  │     │▓  │     │▓  │     │▓▓ │                      ║
║    │▓  │    │   │     │   │     │   │     │▓  │                      ║
║    └───┘    └───┘     └───┘     └───┘     └───┘                      ║
║    -6dB     -12dB     -6dB      -9dB       0dB                        ║
║    [M][S]   [M][S]    [M][S]    [M][S]     [M]                        ║
║                                                                        ║
║  FX: Drums→[Compressor]→[EQ]  |  Master→[Reverb]→[Limiter]           ║
╠═══════════════════════════════════════════════════════════════════════╣
║  [File] [Edit] [Track] [Insert] [View] [Help]                        ║
╚═══════════════════════════════════════════════════════════════════════╝

Commands:
  N: New project          O: Open project
  S: Save project         E: Export to WAV
  Space: Play/Pause       Enter: Record
  T: Add track            I: Insert instrument
  F: Add effect           M: Open mixer
  P: Open piano roll      D: Open drum editor

Implementation Hints:

Project structure:

typedef struct {
    char name[64];
    Track *tracks;
    int num_tracks;
    double bpm;
    int time_sig_num, time_sig_den;
    double sample_rate;
    double project_length;  // In beats or samples
    int loop_start, loop_end;
    int loop_enabled;
} Project;

typedef struct {
    char name[32];
    TrackType type;     // AUDIO, MIDI, INSTRUMENT
    Clip *clips;
    int num_clips;
    Module *instrument; // For instrument tracks
    Module *effects;    // Effect chain
    int num_effects;
    float volume;
    float pan;
    int mute, solo, arm;
} Track;

typedef struct {
    double start_time;  // In beats
    double length;
    void *data;         // AudioClip or MidiClip
} Clip;

Audio engine architecture:

// Separate audio thread
void *audio_thread(void *arg) {
    AudioEngine *engine = (AudioEngine *)arg;

    while (engine->running) {
        // Wait for audio callback signal
        sem_wait(&engine->audio_ready);

        if (engine->playing) {
            // Process all tracks at current position
            for (int t = 0; t < engine->project->num_tracks; t++) {
                process_track(&engine->project->tracks[t],
                             engine->position, BUFFER_SIZE);
            }

            // Mix to master
            mix_to_master(engine);

            // Apply master effects
            process_effects(engine->master_effects, engine->master_buffer);

            // Advance position
            engine->position += BUFFER_SIZE;
        }

        // Signal that buffer is ready
        sem_post(&engine->buffer_ready);
    }
    return NULL;
}

Lock-free UI/audio communication:

// Ring buffer for parameter changes
typedef struct {
    int track_id;
    int param_id;
    float value;
} ParamChange;

// UI thread writes
void set_track_volume(int track, float volume) {
    ParamChange change = {track, PARAM_VOLUME, volume};
    ringbuffer_write(&param_changes, &change);
}

// Audio thread reads (in process callback)
while (ringbuffer_read(&param_changes, &change)) {
    apply_param_change(&change);
}

Questions to guide implementation:

  1. How do you synchronize audio and MIDI tracks?
  2. How do you handle seeking to arbitrary positions?
  3. How do you implement undo/redo for audio editing?
  4. How do you manage memory for long audio recordings?

Learning milestones:

  1. Multi-track playback works → You understand audio mixing
  2. Recording works → You understand real-time audio capture
  3. Effects chains work → You understand modular processing
  4. Project save/load works → You’ve built a complete application

Project Comparison Table

Project Difficulty Time Depth of Understanding Fun Factor
1. WAV File Generator Beginner Weekend ⭐⭐ ⭐⭐⭐
2. Waveform Generator Beginner 1 week ⭐⭐⭐ ⭐⭐⭐
3. Real-Time Audio (PortAudio) Intermediate 1 week ⭐⭐⭐ ⭐⭐⭐⭐
4. ADSR Envelope Intermediate 1 week ⭐⭐⭐ ⭐⭐⭐⭐
5. MIDI Parser & Player Intermediate 2 weeks ⭐⭐⭐⭐ ⭐⭐⭐⭐
6. Digital Filters Advanced 2 weeks ⭐⭐⭐⭐⭐ ⭐⭐⭐
7. Delay Effects Intermediate 1 week ⭐⭐⭐ ⭐⭐⭐⭐⭐
8. Reverb (Schroeder) Advanced 2 weeks ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
9. Wavetable Synthesizer Advanced 2-3 weeks ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
10. Polyphonic Synth Advanced 2 weeks ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
11. MIDI Sequencer Advanced 2-3 weeks ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
12. Distortion Effects Intermediate 1-2 weeks ⭐⭐⭐ ⭐⭐⭐⭐
13. Modular Synth Expert 4+ weeks ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
14. Spectrum Analyzer Advanced 2 weeks ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
15. Mini DAW Master 2-3 months ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

Phase 1: Audio Fundamentals (3-4 weeks)

  1. Project 1: WAV File Generator - Understand digital audio
  2. Project 2: Waveform Generator - Understand synthesis basics
  3. Project 3: Real-Time Audio - Learn PortAudio and callbacks
  4. Project 4: ADSR Envelope - Shape sounds over time

Phase 2: MIDI and Synthesis (4-6 weeks)

  1. Project 5: MIDI Parser - Understand musical data
  2. Project 9: Wavetable Synth - Advanced oscillators
  3. Project 10: Polyphonic Synth - Voice management

Phase 3: Effects and DSP (4-6 weeks)

  1. Project 6: Digital Filters - Essential DSP
  2. Project 7: Delay Effects - Time-based effects
  3. Project 8: Reverb - Complex effect algorithms
  4. Project 12: Distortion - Non-linear processing

Phase 4: Advanced Topics (6-10 weeks)

  1. Project 11: MIDI Sequencer - Music composition
  2. Project 14: Spectrum Analyzer - Frequency analysis
  3. Project 13: Modular Synth - Flexible architecture

Phase 5: Integration (2-3 months)

  1. Project 15: Mini DAW - Complete application

Total estimated time: 6-9 months of focused learning


Essential Resources

Books (In Priority Order)

  1. “The Audio Programming Book” by Boulanger & Lazzarini - Comprehensive reference
  2. “Designing Audio Effect Plugins in C++” by Will Pirkle - Effects implementation
  3. “Designing Software Synthesizer Plugins in C++” by Will Pirkle - Synth architecture
  4. “Think DSP” by Allen B. Downey - DSP fundamentals (free online)
  5. “The Scientist and Engineer’s Guide to DSP” by Steven Smith - DSP theory (free at dspguide.com)

Online Resources

Libraries

  • PortAudio - Cross-platform audio I/O
  • libsndfile - Reading/writing audio files
  • FFTW - Fast Fourier Transform
  • RtMidi - Real-time MIDI I/O

Tools

  • Audacity - Audio editing and analysis
  • Sox - Command-line audio processing
  • VCV Rack - Open source modular synth (for inspiration)

Summary

# Project Main Language
1 WAV File Generator C
2 Waveform Generator (Square, Saw, Triangle) C
3 Real-Time Audio Output (PortAudio) C
4 ADSR Envelope Generator C
5 MIDI File Parser and Player C
6 Digital Filters (LP, HP, BP) C
7 Delay and Echo Effects C
8 Schroeder Reverb C
9 Wavetable Synthesizer C
10 Polyphonic Synthesizer C
11 MIDI Sequencer C
12 Distortion and Waveshaping C
13 Modular Synthesizer Architecture C
14 Audio Spectrum Analyzer (FFT) C
15 Complete DAW-Like Application C

Final Notes

Creating music with code is one of the most rewarding areas of programming because:

  • Immediate feedback: You hear your code instantly
  • Infinite creativity: No limits on what sounds you can create
  • Deep fundamentals: Teaches DSP, real-time systems, and math
  • Practical skills: Same techniques used in professional audio software

The journey from generating a simple sine wave to building a complete synthesizer is transformative. You’ll never listen to music the same way again—you’ll hear the filters, the envelopes, the reverb, and understand how they work.

Start simple with Project 1. When you hear that first sine wave you generated from scratch, you’ll be hooked.

Happy sound hacking! 🎵


Sources consulted: