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:
SDL_AudioDeviceID
(capture + playback)audio_callback
SDL_RENDERER_ACCELERATED
)SDL_StartTextInput
+ SDL_ttf)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
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
NUM_TRACKS
– compile-time constant ≤ 16 for simplicity.RING_FRAMES
– at least 10× 44 100 ≈ 0.25 s per buffer.SDL_OpenAudioDevice(NULL,1,…)
for capture if
you prefer separate input & output streams (lower latency).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.
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).
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]);
}
}
}
SDL_GetAudioDeviceSpec()
and
reduce samples
to 128 or 64 if the backend supports it.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;
}
brew install sdl2 sdl2_ttf
/ apt install libsdl2-dev libsdl2-ttf-dev
)brew install rtmidi
) for MIDI keyboards.The code snippets herein are released under MIT; feel free to copy, modify, and redistribute.