Files
factorygame/util/audio.c

253 lines
7.6 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
// Created by bruno on 16.2.2025.
*/
#include "audio.h"
AudioData audioData;
#define MAX_MIDI_EVENTS 1024
MidiEvent midiEvents[MAX_MIDI_EVENTS];
int midiEventCount = 0;
int nextMidiEvent = 0;
uint16_t getAvailableChannel() {
for (uint16_t i = 0; i < NUM_SYNTH_VOICES; i++) {
if (audioData.synthVoices[i].volume == 0) {
return i;
}
}
return -1;
}
// Helper: compute left/right gains from a pan value in [1..+1]
// pan = 1.0 → full left (L=1, R=0)
// pan = +1.0 → full right (L=0, R=1)
// pan = 0.0 → center (L=R=1/sqrt(2) or just 0.707 to avoid clipping)
static void compute_stereo_gains(float pan, float *outL, float *outR) {
// Simple linear panning (no constantpower law).
// If you prefer constantpower, you could do:
// float angle = (pan + 1.0f) * (M_PI / 4.0f);
// *outL = cosf(angle);
// *outR = sinf(angle);
//
// Here well just do linear:
pan = fmaxf(-1.0f, fminf(+1.0f, pan));
if (pan <= 0.0f) {
*outL = 1.0f;
*outR = 1.0f + pan; // pan is negative, so R < 1
} else {
*outL = 1.0f - pan; // pan is positive, so L < 1
*outR = 1.0f;
}
// Optionally, scale down both so we never exceed 1.0f / sqrt(2)
// e.g. *outL *= 0.7071f; *outR *= 0.7071f;
}
// Improved audio callback with anti-clipping and smooth fade-out
void audio_callback(void *userdata, Uint8 *stream, int len) {
AudioData *audio = (AudioData *) userdata;
int frames = len / (2 * sizeof(float)); // Stereo frame count
float elapsedSec = audio->totalSamples / SAMPLE_RATE;
audio->totalSamples += frames;
while (nextMidiEvent < midiEventCount &&
midiEvents[nextMidiEvent].timeSec <= elapsedSec) {
MidiEvent *ev = &midiEvents[nextMidiEvent];
if (ev->type == 0 && ev->velocity > 0) {
// Note On
for (int i = NUM_SYNTH_VOICES - 4; i < NUM_SYNTH_VOICES; ++i) {
SynthVoice *v = &audio->synthVoices[i];
if (v->volume == 0) {
float freq = 440.0f * powf(2.0f, (ev->note - 69) / 12.0f);
v->frequency = (uint16_t) freq;
v->volume = ev->velocity * 2;
v->waveform = WAVE_SQUARE;
v->smoothedAmp = 0;
break;
}
}
} else {
// Note Off
for (int i = NUM_SYNTH_VOICES - 4; i < NUM_SYNTH_VOICES; ++i) {
SynthVoice *v = &audio->synthVoices[i];
float freq = 440.0f * powf(2.0f, (ev->note - 69) / 12.0f);
if ((uint16_t)freq == v->frequency) {
v->volume = 0;
}
}
}
nextMidiEvent++;
}
float *outBuf = (float *) stream;
for (int i = 0; i < 2 * frames; ++i) {
outBuf[i] = 0.0f;
}
float listenerCx = audio->playerRect->x + audio->playerRect->w * 0.5f;
int *voiceCounts = calloc(frames, sizeof(int));
for (int v = 0; v < NUM_SYNTH_VOICES; v++) {
SynthVoice *voice = &audio->synthVoices[v];
if ((voice->volume == 0 && voice->smoothedAmp < 0.001f) || voice->frequency == 0)
continue;
float sourceCx = voice->sourceRect.x + TILE_SIZE * 0.5f;
float dx = sourceCx - listenerCx;
float pan = fmaxf(-1.0f, fminf(+1.0f, dx / audio->maxPanDistance));
float gainL, gainR;
compute_stereo_gains(pan, &gainL, &gainR);
gainL *= 0.7071f;
gainR *= 0.7071f;
float dist = fabsf(dx);
float distanceAtten = 1.0f - fminf(dist / audio->maxPanDistance, 1.0f);
float targetAmp = (voice->volume / 255.0f) * distanceAtten;
double phaseInc = ((double) voice->frequency * 256.0) / (double) SAMPLE_RATE;
for (int i = 0; i < frames; i++) {
voice->smoothedAmp += (targetAmp - voice->smoothedAmp) * SMOOTHING_FACTOR;
float amp = voice->smoothedAmp;
double norm = voice->phase / 256.0;
double t = norm * 2.0 - 1.0;
float sample;
switch (voice->waveform) {
case WAVE_SQUARE:
sample = (t >= 0.0) ? 1.0f : -1.0f;
break;
case WAVE_SAWTOOTH:
sample = (float) t;
break;
case WAVE_TRIANGLE:
sample = (float) ((t < 0.0) ? -t : t);
break;
case WAVE_NOISE:
sample = ((float) rand() / (float) RAND_MAX) * 2.0f - 1.0f;
break;
default:
sample = (float) sin(norm * 2.0 * M_PI);
break;
}
voice->phase += phaseInc;
if (voice->phase >= 256.0) voice->phase -= 256.0;
else if (voice->phase < 0.0) voice->phase += 256.0;
int idxL = 2 * i;
int idxR = 2 * i + 1;
outBuf[idxL] += sample * amp * gainL;
outBuf[idxR] += sample * amp * gainR;
voiceCounts[i]++;
}
}
for (int i = 0; i < frames; ++i) {
int count = voiceCounts[i];
if (count > 0) {
outBuf[2 * i + 0] /= count;
outBuf[2 * i + 1] /= count;
}
}
free(voiceCounts);
}
static uint32_t read_be_uint32(const uint8_t *data) {
return (data[0]<<24) | (data[1]<<16) | (data[2]<<8) | data[3];
}
static uint16_t read_be_uint16(const uint8_t *data) {
return (data[0]<<8) | data[1];
}
static uint32_t read_vlq(const uint8_t **ptr) {
uint32_t value = 0;
const uint8_t *p = *ptr;
while (*p & 0x80) {
value = (value << 7) | (*p++ & 0x7F);
}
value = (value << 7) | (*p++ & 0x7F);
*ptr = p;
return value;
}
void load_midi_file(const char *path) {
FILE *f = fopen(path, "rb");
if (!f) return;
fseek(f, 0, SEEK_END);
long size = ftell(f);
rewind(f);
uint8_t *data = malloc(size);
fread(data, 1, size, f);
fclose(f);
const uint8_t *ptr = data;
if (memcmp(ptr, "MThd", 4) != 0) return;
ptr += 8; // skip header length
uint16_t format = read_be_uint16(ptr); ptr += 2;
uint16_t nTracks = read_be_uint16(ptr); ptr += 2;
uint16_t ppqn = read_be_uint16(ptr); ptr += 2;
if (format != 0 || nTracks != 1) {
printf("Only Type 0 MIDI supported\n");
free(data);
return;
}
if (memcmp(ptr, "MTrk", 4) != 0) return;
uint32_t trackLen = read_be_uint32(ptr+4);
ptr += 8;
const uint8_t *trackEnd = ptr + trackLen;
float curTime = 0.0f;
uint32_t tempo = 500000; // default: 120 BPM
uint8_t lastStatus = 0;
while (ptr < trackEnd && midiEventCount < MAX_MIDI_EVENTS) {
uint32_t delta = read_vlq(&ptr);
curTime += (delta * (tempo / 1000000.0f)) / ppqn;
uint8_t status = *ptr;
if (status < 0x80) status = lastStatus;
else ptr++;
lastStatus = status;
if (status == 0xFF) {
uint8_t metaType = *ptr++;
uint32_t len = read_vlq(&ptr);
if (metaType == 0x51 && len == 3) {
tempo = (ptr[0]<<16 | ptr[1]<<8 | ptr[2]);
}
ptr += len;
} else if ((status & 0xF0) == 0x90 || (status & 0xF0) == 0x80) {
uint8_t note = *ptr++;
uint8_t vel = *ptr++;
midiEvents[midiEventCount++] = (MidiEvent){
.timeSec = curTime,
.type = (status & 0xF0) == 0x90 ? 0 : 1,
.note = note,
.velocity = vel
};
} else {
ptr += 2; // skip unknown
}
}
free(data);
}