Entropie git · main
Entropie festival game; first steps
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.gzentropie/src/core/audio.c 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);
}