SDL-based Digital Audio Workstation Prototype

This document sketches a minimal yet extensible DAW written in pure C with SDL 2.30 (API 2025-02-04) for cross-platform audio I/O + windowing. It supports:

High-Level Architecture

Audio Thread

GUI / Main Thread

Folder Layout & Build
src/
 ├─ main.c             ▸ init SDL, event loop
 ├─ audio.c|h          ▸ device setup, callback, ring-buffer API
 ├─ gui.c|h            ▸ window, rendering, widgets
 ├─ track.c|h          ▸ Track objects (buffer, mute/solo, name, color)
 ├─ midi.c|h (opt)     ▸ RtMidi-C wrapper for piano keyboard
 └─ utils.c|h          ▸ circular buffer & math helpers

assets/
 ├─ fonts/RobotoMono.ttf
 └─ ui/play.png, stop.png …
# Linux / macOS (C11 + SDL2 + SDL2_ttf)
cc -std=c11 -Wall -O2 \
   src/*.c -o build/minidaw \
   `sdl2-config --cflags --libs` -lSDL2_ttf -lpthread
1 · Audio Engine Core (audio.c)
/* audio.c */
#include "audio.h"
#include <SDL.h>

static SDL_AudioDeviceID dev;
static float **trackBuffer;      /* [NUM_TRACKS][RING_FRAMES] */
static uint32_t writeIdx = 0;

static void SDLAudioCallback(void *userdata,
                             Uint8 *stream,
                             int    lenBytes) {
    float *out = (float *)stream;        /* interleaved stereo F32 */
    const int frames = lenBytes / (sizeof(float)*2);

    for(int i = 0; i < frames; ++i){
        float mixL = 0.f, mixR = 0.f;
        for(int t = 0; t < NUM_TRACKS; ++t){
            const float s = trackBuffer[t][(writeIdx+i) % RING_FRAMES];
            mixL += s;                /* naïve sum; add gain / pan per-track */
            mixR += s;
        }
        out[i*2]     = SDL_clamp(mixL, -1.f, 1.f);
        out[i*2 + 1] = SDL_clamp(mixR, -1.f, 1.f);
    }
    writeIdx = (writeIdx + frames) % RING_FRAMES;
}

bool audio_start(int sampleRate){
    SDL_AudioSpec want = {.freq = sampleRate,
                          .format = AUDIO_F32,
                          .channels = 2,
                          .samples = 512,
                          .callback = SDLAudioCallback };

    dev = SDL_OpenAudioDevice(NULL, 0, &want, NULL, 0);
    if(!dev){ SDL_Log("Audio open failed %s", SDL_GetError()); return false; }
    SDL_PauseAudioDevice(dev, 0);  /* start */
    return true;
}

Important parameters

2 · GUI Loop & Widgets (main.c / gui.c)
while(running){
    SDL_Event e;
    while(SDL_PollEvent(&e)){
        if(e.type == SDL_QUIT) running = false;

        if(e.type == SDL_DROPFILE){
            /* drag-n-drop .wav ⇒ new audio track */
            track_load_audio(e.drop.file);
        }
        if(e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_RETURN){
            transport_toggle_record();
        }
        if(e.type == SDL_TEXTINPUT){
            lyrics_append(e.text.text);  /* see section 3 */
        }
        /* …mouse wheel for zoom, etc. */
    }

    gui_render_tracks();   /* waveforms + piano roll */
    gui_render_transport();
    gui_render_lyrics();
    SDL_RenderPresent(renderer);
    SDL_Delay(1);          /* simple frame cap ~1 kHz */
}

Waveforms are drawn by fetching N samples from each track’s ring buffer, mapping amplitude ⇒ vertical pixels. For efficiency, pre-generate a min/max envelope array per block (i.e., track_build_peak_cache()).
The lyric pane is simply a semi-transparent rectangle at the bottom-left where SDL_ttf renders the current UTF-8 buffer.

3 · Realtime Lyrics Editor

SDL2 provides platform IME + key repeat via its text APIs.

/* lyrics.c */
static char textBuf[4096];
static size_t caret = 0;

void lyrics_init(void){ SDL_StartTextInput(); }

void lyrics_append(const char *utf8){
    size_t n = strlen(utf8);
    if(caret + n < sizeof textBuf){
        memcpy(textBuf + caret, utf8, n);
        caret += n;
    }
}

void lyrics_backspace(void){
    if(caret){
        do{          /* delete last UTF-8 codepoint */
            --caret;
        }while(caret && (textBuf[caret] & 0xC0) == 0x80);
        textBuf[caret] = '\0';
    }
}

void lyrics_render(SDL_Renderer *r, TTF_Font *font){
    SDL_Color c = {255,255,255,255};
    SDL_Surface *surf = TTF_RenderUTF8_Blended_Wrapped(font,textBuf,c, 400);
    SDL_Texture *tex  = SDL_CreateTextureFromSurface(r,surf);
    SDL_Rect dst = {12, SDL_GetWindowSurface(SDL_GetWindowFromID(1))->h - surf->h - 12,
                    surf->w, surf->h};
    SDL_SetTextureAlphaMod(tex, 200);
    SDL_RenderCopy(r,tex,NULL,&dst);
    SDL_DestroyTexture(tex);
    SDL_FreeSurface(surf);
}

Extended ideas: enable CTRL+S to save lyrics into a side-car .txt; support multi-track lyric lanes (verses, chorus).

4 · MIDI (Optional)

SDL 2.30 gained experimental SDL_midi. If still unavailable on your platform, wrap RtMidi-C:

void midi_poll(){
    double stamp;
    unsigned char msg[3];
    while(rtmidi_in_get_message(dev, msg, &stamp) > 0){
        if((msg[0] & 0xF0) == 0x90){ /* Note On */
            piano_track_note_on(msg[1], msg[2]/127.f);
        }
        if((msg[0] & 0xF0) == 0x80){
            piano_track_note_off(msg[1]);
        }
    }
}
5 · Next Steps

Minimal main.c Skeleton

#include <SDL.h>
#include <SDL_ttf.h>
#include "audio.h"
#include "gui.h"
#include "lyrics.h"

int main(int argc,char **argv){
    (void)argc; (void)argv;
    if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO|SDL_INIT_TIMER) != 0)
        return SDL_LogError(-1,"SDL init failed %s",SDL_GetError());

    if(TTF_Init()) return 1;

    SDL_Window   *win = SDL_CreateWindow("Mini-DAW",
                        SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
                        1280, 720, SDL_WINDOW_RESIZABLE);
    SDL_Renderer *ren = SDL_CreateRenderer(win,-1,
                        SDL_RENDERER_ACCELERATED|SDL_RENDERER_PRESENTVSYNC);

    audio_start(44100);
    gui_init(ren);
    lyrics_init();

    bool running = true;
    while(running){ running = gui_pump_events(); gui_render(); }
    SDL_Quit(); return 0;
}

Compilation Dependencies

License

The code snippets herein are released under MIT; feel free to copy, modify, and redistribute.