Nimbin[12]?Games / Entropie / entropie/src/core/audio.c

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (18 hours ago) · synced 10 hours ago

C 88.5% Markdown 9.1% GLSL 2.3%
git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gz
entropie/src/core/audio.c 16.7 KB · 408 lines raw
#include "core/audio.h"

#include <SDL3/SDL.h>
#include <math.h>
#include <string.h>

#define RATE        44100
#define VOICES_MAX  96
#define CHUNK       512    /* frames mixed at a time */
#define MASTER      0.55f
#define BED_EASE    3.0f   /* 1/s: beds turn up and down this quickly */
#define TWO_PI      6.28318531f

typedef struct {            /* a one-pole lowpass (cascaded twice: steeper) */
    float a, y1, y2;
} Low;

typedef struct {
    Voice v;
    float t;                /* s since it started (negative: still waiting) */
    float phase;
    float click;            /* crackle: the click sounding now */
    Low   low, high;
    unsigned rng;
    bool  on;
} Playing;

typedef struct {
    float gain, pan, param;       /* where it's going ... */
    float g, p, k;                /* ... where it is */
    float phase, phase2, click;
    Low   low, low2, high;
    float mod, mod_v;             /* a slow wander (wind gusts, a fire breathing) */
    unsigned rng;
    double t;                     /* music: s it's been playing */
    float kick, osc[3], hiss;     /* ... the kick's phase, the other tones' phases, a noise burst's state */
    Low   tone, tone2;            /* ... their filters */
} BedState;

static SDL_AudioStream *g_stream;
static SDL_Mutex       *g_lock;
static Playing          g_voices[VOICES_MAX];
static BedState         g_beds[BED_COUNT];
static bool             g_muted, g_paused;
static float            g_fade = 1.0f; /* (muting fades, no click) */

static float coef(float hz) { return hz <= 0.0f ? 1.0f : 1.0f - expf(-TWO_PI * fminf(hz, RATE * 0.45f) / RATE); }
static float lowpass(Low *f, float x) { f->y1 += f->a * (x - f->y1); f->y2 += f->a * (f->y1 - f->y2); return f->y2; }
static float onePole(Low *f, float x) { f->y1 += f->a * (x - f->y1); return f->y1; }
static float noise(unsigned *r) {
    *r ^= *r << 13; *r ^= *r >> 17; *r ^= *r << 5;
    return (float)(*r & 0xFFFFFFu) / 8388608.0f - 1.0f;
}
static float chance(unsigned *r) { return 0.5f * (noise(r) + 1.0f); }
static void panGains(float pan, float *l, float *r) {
    float a = (fmaxf(-1.0f, fminf(1.0f, pan)) + 1.0f) * 0.25f * 3.14159265f;
    *l = cosf(a);
    *r = sinf(a);
}

/* ---------- Voices ---------- */

static float voiceSample(Playing *p) {
    const Voice *v = &p->v;
    float t = p->t, x = 0.0f;
    float f = v->freq;
    if (v->freq_end > 0.0f && v->freq > 0.0f) {
        float span = v->length > 0.0f ? v->length : v->attack + 3.0f * v->decay;
        f = v->freq * powf(v->freq_end / v->freq, fminf(t / fmaxf(span, 1e-3f), 1.0f));
    }
    switch (v->wave) {
    case WAVE_NOISE: x = noise(&p->rng); break;
    case WAVE_SINE:  p->phase += f / RATE; x = sinf(TWO_PI * p->phase); break;
    case WAVE_SAW:   p->phase += f / RATE; x = 2.0f * (p->phase - floorf(p->phase)) - 1.0f; break;
    case WAVE_CRACKLE:
        if (chance(&p->rng) < f / RATE) p->click = noise(&p->rng) > 0.0f ? 1.0f : -1.0f;
        x = p->click + 0.15f * noise(&p->rng);
        p->click *= 0.82f;
        break;
    }
    if (p->phase > 1.0f) p->phase -= floorf(p->phase);
    if (v->low > 0.0f) x = lowpass(&p->low, x);
    if (v->high > 0.0f) x -= onePole(&p->high, x);
    float env = t < v->attack ? t / fmaxf(v->attack, 1e-4f) : expf(-(t - v->attack) / fmaxf(v->decay, 1e-3f));
    return x * env * v->gain;
}

static void mixVoices(float *out, int frames) {
    for (int i = 0; i < VOICES_MAX; i++) {
        Playing *p = &g_voices[i];
        if (!p->on) continue;
        float l, r;
        panGains(p->v.pan, &l, &r);
        float end = p->v.length > 0.0f ? p->v.length : p->v.attack + 6.0f * p->v.decay;
        for (int k = 0; k < frames; k++) {
            p->t += 1.0f / RATE;
            if (p->t < 0.0f) continue;
            if (p->t > end) { p->on = false; break; }
            float s = voiceSample(p);
            out[2 * k] += s * l;
            out[2 * k + 1] += s * r;
        }
    }
}

/* ---------- Beds ---------- */

/* The floors' music: a drum machine each, its own groove (made up on the spot, like everything here).
   0 the Hofbuehne: a rock band - kick, snare on two and four, a chugging power chord;
   1 the Obstwiese: psytrance - four on the floor, a rolling bass on the sixteenths, open hats;
   2 the Kartoffelacker: hand drums - low and high toms in a pattern, a shaker;
   3 the hidden floor: techno - kick, an off-beat hat, a clap, an off-beat bass. */
static const float FLOOR_BPM[4] = { 118.0f, 144.0f, 100.0f, 130.0f }; /* (the dancers move to these: game_festival.c FLOOR_BPM) */

static float sweptKick(BedState *b, float since, float from, float to) { /* (a sine sweeping down fast: the thump) */
    float f = to + (from - to) * expf(-since * 28.0f);
    b->kick += f / RATE;
    if (b->kick > 1.0f) b->kick -= 1.0f;
    return sinf(TWO_PI * b->kick) * expf(-since * 7.0f);
}

static float sawAt(float *phase, float f) {
    *phase += f / RATE;
    if (*phase > 1.0f) *phase -= 1.0f;
    return 2.0f * *phase - 1.0f;
}

static float floorSample(int style, BedState *b) {
    float bpm = FLOOR_BPM[style], spb = 60.0f / bpm;
    b->t += 1.0 / RATE;
    if (b->t > 3600.0) b->t -= 3600.0 - fmod(3600.0, (double)(spb * 16.0f)); /* (whole bars: no jump in the groove) */
    float beats = (float)(b->t / spb);
    int beat = (int)beats % 16;                       /* (16 beats: four bars) */
    float in_beat = (beats - floorf(beats)) * spb;    /* s since this beat */
    float sixteenth = beats * 4.0f, in_six = (sixteenth - floorf(sixteenth)) * spb * 0.25f;
    int six = (int)sixteenth % 4;
    float noise_v = noise(&b->rng), x = 0.0f;
    switch (style) {
    case 0: { /* rock */
        bool kick = beat % 4 == 0 || beat % 4 == 2 || (beat % 8 == 7 && six == 2);
        if (kick) x += 0.9f * sweptKick(b, beat % 8 == 7 ? in_six : in_beat, 110.0f, 50.0f);
        if (beat % 2 == 1) { /* the snare: a crack of noise and a body */
            b->tone.a = coef(2500.0f);
            x += 0.5f * lowpass(&b->tone, noise_v) * expf(-in_beat * 14.0f) + 0.2f * sinf(TWO_PI * 190.0f * in_beat) * expf(-in_beat * 20.0f);
        }
        static const float ROOT[4] = { 82.4f, 82.4f, 98.0f, 73.4f }; /* (E, E, G, D) */
        float root = ROOT[(beat / 4) % 4];
        float chug = expf(-fmodf(in_beat, spb * 0.5f) * 6.0f) * 0.8f + 0.2f;
        float gtr = sawAt(&b->osc[0], root) + sawAt(&b->osc[1], root * 1.5f) + 0.6f * sawAt(&b->osc[2], root * 2.0f);
        gtr = tanhf(gtr * 2.5f) * chug;                         /* (overdriven) */
        b->tone2.a = coef(1800.0f);
        x += 0.28f * lowpass(&b->tone2, gtr);
        b->hiss = b->hiss * 0.5f + 0.5f * noise_v;              /* (the hi-hat on the eighths) */
        x += 0.06f * (noise_v - b->hiss) * expf(-fmodf(in_beat, spb * 0.5f) * 40.0f);
        break;
    }
    case 1: { /* psytrance */
        x += 1.0f * sweptKick(b, in_beat, 140.0f, 48.0f);
        if (six != 0) { /* the rolling bass: three on the sixteenths after each kick */
            static const float NOTE[4] = { 46.2f, 46.2f, 51.9f, 43.6f };
            float f = NOTE[(beat / 4) % 4];
            b->tone.a = coef(300.0f + 1400.0f * expf(-in_six * 18.0f));
            x += 0.55f * lowpass(&b->tone, sawAt(&b->osc[0], f)) * expf(-in_six * 10.0f);
        }
        if (six == 2) { /* the open hat on the off-beat */
            b->hiss = b->hiss * 0.3f + 0.7f * noise_v;
            x += 0.12f * (noise_v - b->hiss) * expf(-in_six * 18.0f);
        }
        float acid = sinf(TWO_PI * (float)fmod(b->t * 0.07, 1.0)); /* (a squelchy lead now and then, every other four bars) */
        if (beat >= 8 && six % 2 == 1) {
            b->tone2.a = coef(600.0f + 1800.0f * (0.5f + 0.5f * acid));
            x += 0.12f * lowpass(&b->tone2, sawAt(&b->osc[1], 185.0f * (six == 3 ? 1.5f : 1.0f))) * expf(-in_six * 12.0f);
        }
        break;
    }
    case 2: { /* hand drums */
        static const signed char LOW[16]  = { 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0 }; /* (on the sixteenths of a bar) */
        static const signed char HIGH[16] = { 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1 };
        int step = ((int)sixteenth) % 16;
        if (LOW[step]) x += 0.7f * sinf(TWO_PI * (95.0f + 40.0f * expf(-in_six * 30.0f)) * in_six) * expf(-in_six * 9.0f);
        if (HIGH[step]) {
            b->tone.a = coef(1800.0f);
            x += 0.4f * sinf(TWO_PI * 330.0f * in_six) * expf(-in_six * 16.0f) + 0.15f * lowpass(&b->tone, noise_v) * expf(-in_six * 40.0f);
        }
        b->hiss = b->hiss * 0.2f + 0.8f * noise_v; /* (the shaker on every sixteenth, the off ones louder) */
        x += (six % 2 ? 0.07f : 0.04f) * (noise_v - b->hiss) * expf(-in_six * 25.0f);
        float drone = sinf(TWO_PI * (float)fmod(b->t * 110.0, 1.0)) + 0.5f * sinf(TWO_PI * (float)fmod(b->t * 165.0, 1.0)); /* (a drone under it) */
        x += 0.05f * drone * (0.7f + 0.3f * sinf(TWO_PI * (float)fmod(b->t * 0.1, 1.0)));
        break;
    }
    default: { /* techno */
        x += 1.0f * sweptKick(b, in_beat, 120.0f, 46.0f);
        if (six == 2) {
            b->hiss = b->hiss * 0.4f + 0.6f * noise_v;
            x += 0.1f * (noise_v - b->hiss) * expf(-in_six * 30.0f);
            x += 0.4f * tanhf(1.5f * sawAt(&b->osc[0], 49.0f)) * expf(-in_six * 8.0f); /* (the off-beat bass) */
        }
        if (beat % 2 == 1) { /* the clap: noise in bursts */
            b->tone.a = coef(1600.0f);
            float burst = in_beat < 0.03f ? (fmodf(in_beat, 0.01f) < 0.004f ? 1.0f : 0.3f) : expf(-(in_beat - 0.03f) * 12.0f);
            x += 0.35f * lowpass(&b->tone, noise_v) * burst;
        }
        break;
    }
    }
    b->low.a = coef(9000.0f - 8300.0f * b->k); /* (far off: only the low end carries) */
    return lowpass(&b->low, x) * 1.2f;
}

static float bedSample(Bed which, BedState *b) {
    float x = 0.0f, k = b->k;
    b->mod_v += (noise(&b->rng) * 0.6f - b->mod * 0.4f - b->mod_v * 0.9f) / RATE; /* (a slow wander, -1..1) */
    b->mod = fmaxf(-1.0f, fminf(1.0f, b->mod + b->mod_v * 4.0f / RATE));
    switch (which) {
    case BED_WIND: { /* noise through a lowpass that opens with the gusts */
        float cut = 180.0f + 900.0f * k + 400.0f * k * (0.5f + 0.5f * b->mod);
        b->low.a = coef(cut);
        x = lowpass(&b->low, noise(&b->rng)) * (0.8f + 0.5f * (0.5f + 0.5f * b->mod) * k) * 2.2f;
        break;
    }
    case BED_RAIN: { /* hiss and drops; through a tent: dull, drumming */
        b->low.a = coef(7000.0f - 5500.0f * k);
        b->high.a = coef(600.0f - 400.0f * k);
        float hiss = noise(&b->rng);
        if (chance(&b->rng) < (400.0f + 900.0f * k) / RATE) b->click = noise(&b->rng);
        float drop = b->click;
        b->click *= 0.9f - 0.08f * k;
        x = lowpass(&b->low, hiss * (0.5f - 0.3f * k) + drop * (0.6f + 0.6f * k));
        x -= onePole(&b->high, x);
        x *= 1.4f;
        break;
    }
    case BED_FIRE: { /* a low roar, crackles and now and then a pop */
        b->low.a = coef(250.0f + 250.0f * k);
        float roar = lowpass(&b->low, noise(&b->rng)) * (0.6f + 0.3f * b->mod) * 2.0f;
        if (chance(&b->rng) < (6.0f + 30.0f * k) / RATE) b->click = noise(&b->rng) * (chance(&b->rng) < 0.1f ? 2.0f : 0.8f);
        b->high.a = coef(1500.0f);
        float c = b->click;
        b->click *= 0.7f;
        c -= onePole(&b->high, c);
        x = roar * (0.4f + 0.6f * k) + c;
        break;
    }
    case BED_SAW: { /* a two-stroke buzz, lower and rougher cutting */
        float f = 140.0f - 35.0f * k + 6.0f * b->mod;
        b->phase += f / RATE;
        if (b->phase > 1.0f) b->phase -= 1.0f;
        b->phase2 += 0.5f * f / RATE;
        if (b->phase2 > 1.0f) b->phase2 -= 1.0f;
        float saw = 2.0f * b->phase - 1.0f, chug = 0.75f + 0.25f * sinf(TWO_PI * b->phase2);
        b->low.a = coef(1800.0f + 1500.0f * k);
        x = lowpass(&b->low, saw * chug + 0.35f * k * noise(&b->rng)) * 1.3f;
        break;
    }
    case BED_ENGINE: { /* a low hum that rises with the speed */
        float f = 32.0f + 45.0f * k;
        b->phase += f / RATE;
        if (b->phase > 1.0f) b->phase -= 1.0f;
        b->low.a = coef(220.0f + 250.0f * k);
        x = lowpass(&b->low, (2.0f * b->phase - 1.0f) + 0.3f * noise(&b->rng)) * 1.8f;
        break;
    }
    case BED_CRICKETS: { /* a few crickets: bursts of a high tone, three pulses then a rest, each its own time */
        b->phase += 4500.0f / RATE;
        if (b->phase > 1.0f) b->phase -= 1.0f;
        b->phase2 += 1.0f / RATE;
        float s = 0.0f;
        for (int c = 0; c < 3; c++) {
            float cyc = 0.55f + 0.17f * (float)c, tt = fmodf(b->phase2 + 0.31f * (float)c, cyc);
            float pulse = tt < 0.12f ? fmaxf(0.0f, sinf(tt / 0.12f * 3.0f * TWO_PI * 0.5f)) : 0.0f;
            s += pulse * (c == 1 ? 0.7f : 1.0f);
        }
        x = sinf(TWO_PI * b->phase) * s * 0.5f;
        if (b->phase2 > 1000.0f) b->phase2 -= 1000.0f;
        break;
    }
    case BED_HOSE: { /* a steady spray */
        b->low.a = coef(6000.0f);
        b->high.a = coef(1500.0f);
        x = lowpass(&b->low, noise(&b->rng));
        x -= onePole(&b->high, x);
        x *= 1.6f;
        break;
    }
    default:
        if (which >= BED_FLOOR && which < BED_FLOOR + 4) x = floorSample(which - BED_FLOOR, b);
        break;
    }
    return x;
}

static void mixBeds(float *out, int frames) {
    for (int i = 0; i < BED_COUNT; i++) {
        BedState *b = &g_beds[i];
        float goal = g_paused ? 0.0f : b->gain;
        if (b->g < 1e-4f && goal < 1e-4f) continue;
        float ease = 1.0f - expf(-BED_EASE / RATE);
        for (int k = 0; k < frames; k++) {
            b->g += (goal - b->g) * ease;
            b->p += (b->pan - b->p) * ease;
            b->k += (b->param - b->k) * ease;
            float l, r, s = bedSample((Bed)i, b) * b->g;
            panGains(b->p, &l, &r);
            out[2 * k] += s * l;
            out[2 * k + 1] += s * r;
        }
    }
}

/* ---------- The device ---------- */

static void SDLCALL fill(void *user, SDL_AudioStream *stream, int additional, int total) {
    (void)user;
    (void)total;
    static float buf[CHUNK * 2];
    int frames = additional / (int)(2 * sizeof(float));
    while (frames > 0) {
        int n = frames < CHUNK ? frames : CHUNK;
        memset(buf, 0, sizeof(float) * 2 * (size_t)n);
        SDL_LockMutex(g_lock);
        mixVoices(buf, n);
        mixBeds(buf, n);
        bool muted = g_muted;
        SDL_UnlockMutex(g_lock);
        for (int k = 0; k < 2 * n; k++) {
            if (k % 2 == 0) g_fade += ((muted ? 0.0f : 1.0f) - g_fade) * 0.002f;
            float s = buf[k] * MASTER * g_fade;
            buf[k] = s / (1.0f + fabsf(s)); /* (soft: never clipping) */
        }
        SDL_PutAudioStreamData(stream, buf, (int)(sizeof(float) * 2 * (size_t)n));
        frames -= n;
    }
}

bool Audio_init(void) {
    if (g_stream) return true;
    if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
        SDL_Log("No sound: %s", SDL_GetError());
        return false;
    }
    g_lock = SDL_CreateMutex();
    for (int i = 0; i < BED_COUNT; i++) g_beds[i].rng = 0x9E3779B9u * (unsigned)(i + 1);
    SDL_AudioSpec spec = { SDL_AUDIO_F32, 2, RATE };
    g_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, fill, NULL);
    if (!g_stream) {
        SDL_Log("No sound device: %s", SDL_GetError());
        SDL_DestroyMutex(g_lock);
        g_lock = NULL;
        SDL_QuitSubSystem(SDL_INIT_AUDIO);
        return false;
    }
    SDL_ResumeAudioStreamDevice(g_stream);
    return true;
}

void Audio_shutdown(void) {
    if (!g_stream) return;
    SDL_DestroyAudioStream(g_stream);
    g_stream = NULL;
    SDL_DestroyMutex(g_lock);
    g_lock = NULL;
    SDL_QuitSubSystem(SDL_INIT_AUDIO);
}

void Audio_play(const Voice *voice) {
    if (!g_stream || voice->gain < 0.002f) return;
    static unsigned seed = 0x1234567u;
    SDL_LockMutex(g_lock);
    int pick = -1;
    float oldest = -1.0f;
    for (int i = 0; i < VOICES_MAX; i++) { /* (a free one, else the one playing longest) */
        if (!g_voices[i].on) { pick = i; break; }
        if (g_voices[i].t > oldest) { oldest = g_voices[i].t; pick = i; }
    }
    Playing *p = &g_voices[pick];
    memset(p, 0, sizeof(*p));
    p->v = *voice;
    p->t = -fmaxf(voice->delay, 0.0f);
    p->low.a = coef(voice->low);
    p->high.a = coef(voice->high);
    seed = seed * 1664525u + 1013904223u;
    p->rng = seed | 1u;
    p->on = true;
    SDL_UnlockMutex(g_lock);
}

void Audio_bed(Bed bed, float gain, float pan, float param) {
    if (!g_stream || bed < 0 || bed >= BED_COUNT) return;
    SDL_LockMutex(g_lock);
    g_beds[bed].gain = fmaxf(0.0f, fminf(1.0f, gain));
    g_beds[bed].pan = pan;
    g_beds[bed].param = fmaxf(0.0f, fminf(1.0f, param));
    SDL_UnlockMutex(g_lock);
}

void Audio_mute(bool mute) {
    if (g_lock) SDL_LockMutex(g_lock);
    g_muted = mute;
    if (g_lock) SDL_UnlockMutex(g_lock);
}

bool Audio_muted(void) { return g_muted; }

void Audio_pause(bool paused) {
    if (g_lock) SDL_LockMutex(g_lock);
    g_paused = paused;
    if (g_lock) SDL_UnlockMutex(g_lock);
}