power_play/src/mixer/mixer_core.c
2025-07-30 15:58:38 -05:00

482 lines
15 KiB
C

/* TODO: Cap max sounds playing. */
/* Terminology:
*
* `Sample`: Once "PCM" data point representing the smallest unit of audio available for a single channel at a point in time.
* Examples:
* - Single 32 bit float output by mixer and consumed by playback API, that the API interprets as a sound sample for a single channel
* - Single 16 bit integer output by audio file decoder, that may represent a mono sound sample
*
* `Frame`: Represents a single data point of audio for all audio channels at a point in time.
* Examples:
* - Single 16 bit integer output by audio file decoder representing one mono sound sample
* - 2 16 bit integer samples output by audio file decoder representing two sound samples, one sample for each audio channel
* - 2 32 bit float samples output by mixer and consumed by playback API, one sample for each audio channel
*/
struct effect_data {
/* Spatialization */
f32 spatial_volume;
f32 spatial_pan;
};
struct mix {
M_Handle track_handle;
b32 track_finished;
M_TrackDesc desc;
struct effect_data effect_data;
SND_Sound *source;
u64 source_pos;
};
struct track {
u64 gen;
/* Controlled via interface */
SND_Sound *sound;
M_TrackDesc desc;
/* internal */
struct mix mix;
struct track *next;
struct track *prev;
};
Global struct {
P_Mutex mutex;
/* Listener */
Vec2 listener_pos;
Vec2 listener_dir;
/* Track list */
Arena *track_arena;
struct track *track_first_playing;
struct track *track_last_playing;
u64 track_playing_count;
struct track *track_first_free;
} G = ZI, DebugAlias(G, G_mixer);
/* ========================== *
* Startup
* ========================== */
M_StartupReceipt mixer_startup(void)
{
__prof;
G.track_arena = AllocArena(Gibi(64));
G.listener_pos = VEC2(0, 0);
G.listener_dir = VEC2(0, -1);
return (M_StartupReceipt) { 0 };
}
/* ========================== *
* Track
* ========================== */
internal M_Handle track_to_handle(struct track *track)
{
return (M_Handle) {
.gen = track->gen,
.data = track
};
}
internal struct track *track_from_handle(M_Handle handle)
{
struct track *track = (struct track *)handle.data;
if (track && track->gen == handle.gen) {
return track;
} else {
return 0;
}
}
internal struct track *track_alloc_locked(P_Lock *lock, SND_Sound *sound)
{
P_AssertLockedE(lock, &G.mutex);
(UNUSED)lock;
struct track *track = 0;
if (G.track_first_free) {
/* Take from free list */
track = G.track_first_free;
struct track *next_free = track->next;
G.track_first_free = next_free;
if (next_free) {
next_free->prev = 0;
}
*track = (struct track) { .gen = track->gen + 1 };
} else {
/* Allocate new */
track = PushStruct(G.track_arena, struct track);
track->gen = 1;
}
track->sound = sound;
track->mix.source = sound;
track->mix.track_handle = track_to_handle(track);
/* Append to playing list */
struct track *prev = G.track_last_playing;
if (prev) {
prev->next = track;
} else {
G.track_first_playing = track;
}
G.track_last_playing = track;
track->prev = prev;
++G.track_playing_count;
return track;
}
internal void track_release_locked(P_Lock *lock, struct track *track)
{
P_AssertLockedE(lock, &G.mutex);
(UNUSED)lock;
/* Remove from playing list */
struct track *prev = track->prev;
struct track *next = track->next;
if (prev) {
prev->next = next;
} else {
/* Track was first in list */
G.track_first_playing = next;
}
if (next) {
next->prev = prev;
} else {
/* Track was last in list */
G.track_last_playing = prev;
}
--G.track_playing_count;
++track->gen;
/* Add to free list */
track->prev = 0;
track->next = G.track_first_free;
if (G.track_first_free) {
G.track_first_free->prev = track;
}
G.track_first_free = track;
}
/* ========================== *
* Interface
* ========================== */
/* TODO: Rework interface to take "mixer_cmd"s instead of
* directly modifying tracks. */
M_Handle mixer_play(SND_Sound *sound)
{
return mixer_play_ex(sound, MIXER_DESC());
}
M_Handle mixer_play_ex(SND_Sound *sound, M_TrackDesc desc)
{
struct track *track;
{
P_Lock lock = P_LockE(&G.mutex);
{
track = track_alloc_locked(&lock, sound);
track->desc = desc;
}
P_Unlock(&lock);
}
return track_to_handle(track);
}
/* NOTE: This is quite inefficient. */
M_TrackDesc mixer_track_get(M_Handle handle)
{
M_TrackDesc result = ZI;
struct track *track = track_from_handle(handle);
if (track) {
/* TODO: Only lock mutex on track itself or something */
P_Lock lock = P_LockE(&G.mutex);
{
/* Confirm handle is still valid now that we're locked */
track = track_from_handle(handle);
if (track) {
result = track->desc;
}
}
P_Unlock(&lock);
}
return result;
}
/* NOTE: This is quite inefficient. */
void mixer_track_set(M_Handle handle, M_TrackDesc desc)
{
struct track *track = track_from_handle(handle);
if (track) {
/* TODO: Only lock mutex on track itself or something */
P_Lock lock = P_LockE(&G.mutex);
{
/* Confirm handle is still valid now that we're locked */
track = track_from_handle(handle);
if (track) {
track->desc = desc;
}
}
P_Unlock(&lock);
}
}
void mixer_set_listener(Vec2 pos, Vec2 dir)
{
P_Lock lock = P_LockE(&G.mutex);
{
G.listener_pos = pos;
G.listener_dir = NormVec2(dir);
}
P_Unlock(&lock);
}
/* ========================== *
* Update
* ========================== */
internal i16 sample_sound(SND_Sound *sound, u64 sample_pos, b32 wrap)
{
if (wrap) {
return sound->samples[sample_pos % sound->samples_count];
} else if (sample_pos < sound->samples_count) {
return sound->samples[sample_pos];
} else {
return 0;
}
}
/* To be called once per audio playback interval */
M_PcmF32 mixer_update(Arena *arena, u64 frame_count)
{
__prof;
TempArena scratch = BeginScratch(arena);
M_PcmF32 result = ZI;
result.count = frame_count * 2;
result.samples = PushStructs(arena, f32, result.count);
Vec2 listener_pos = VEC2(0, 0);
Vec2 listener_dir = VEC2(0, 0);
/* Create temp array of mixes */
struct mix **mixes = 0;
u64 mixes_count = 0;
{
P_Lock lock = P_LockE(&G.mutex);
/* Read listener info */
listener_pos = G.listener_pos;
listener_dir = G.listener_dir;
/* Update & read mixes */
mixes = PushStructsNoZero(scratch.arena, struct mix *, G.track_playing_count);
for (struct track *track = G.track_first_playing; track; track = track->next) {
__profn("Prepare track");
struct mix *mix = &track->mix;
mix->desc = track->desc;
mixes[mixes_count++] = mix;
}
P_Unlock(&lock);
}
for (u64 mix_index = 0; mix_index < mixes_count; ++mix_index) {
__profn("Mix track");
struct mix *mix = mixes[mix_index];
if (mix->source->samples_count <= 0) {
/* Skip empty sounds */
continue;
}
SND_Sound *source = mix->source;
M_TrackDesc desc = mix->desc;
struct effect_data *effect_data = &mix->effect_data;
b32 source_is_stereo = source->flags & SOUND_FLAG_STEREO;
f32 speed = MaxF32(0, desc.speed);
/* Determine sample range */
u64 source_samples_count = 0;
if (source_is_stereo) {
source_samples_count = frame_count * 2;
/* Round <samples_count * speed> to nearest frame boundary (nearest multiple of 2) */
source_samples_count = (u64)CeilF32ToI32((f32)source_samples_count * speed);
source_samples_count &= ~1;
} else {
source_samples_count = frame_count;
/* Round <samples_count * speed> to nearest sample */
source_samples_count = (u64)RoundF32ToI32((f32)source_samples_count * speed);
}
u64 source_sample_pos_start = mix->source_pos;
u64 source_sample_pos_end = source_sample_pos_start + source_samples_count;
if (source_sample_pos_end >= source->samples_count) {
if (desc.looping) {
source_sample_pos_end = source_sample_pos_end % source->samples_count;
} else {
source_sample_pos_end = source->samples_count;
mix->track_finished = 1;
}
}
u64 source_frames_count = source_is_stereo ? source_samples_count / 2 : source_samples_count;
u64 source_frame_pos_start = source_is_stereo ? source_sample_pos_start / 2 : source_sample_pos_start;
mix->source_pos = source_sample_pos_end;
M_PcmF32 mix_pcm = {
.count = result.count,
.samples = PushStructs(scratch.arena, f32, result.count)
};
/* ========================== *
* Resample
* ========================== */
/* Transform 16 bit source -> 32 bit stereo at output duration */
{
__profn("Resample");
f32 *out_samples = mix_pcm.samples;
u64 out_frames_count = mix_pcm.count / 2;
/* TODO: Fast path for 1:1 copy when speed = 1.0? */
/* TODO: Optimize */
if (source_is_stereo) {
/* 16 bit Stereo -> 32 bit Stereo */
for (u64 out_frame_pos = 0; out_frame_pos < out_frames_count; ++out_frame_pos) {
f32 in_frame_pos_exact = source_frame_pos_start + (((f32)out_frame_pos / (f32)out_frames_count) * (f32)source_frames_count);
u32 in_frame_pos_prev = FloorF32ToI32(in_frame_pos_exact);
u32 in_frame_pos_next = CeilF32ToI32(in_frame_pos_exact);
/* Sample source */
f32 sample1_prev = sample_sound(source, (in_frame_pos_prev * 2) + 0, desc.looping) * (1.f / 32768.f);
f32 sample1_next = sample_sound(source, (in_frame_pos_next * 2) + 0, desc.looping) * (1.f / 32768.f);
f32 sample2_prev = sample_sound(source, (in_frame_pos_prev * 2) + 1, desc.looping) * (1.f / 32768.f);
f32 sample2_next = sample_sound(source, (in_frame_pos_next * 2) + 1, desc.looping) * (1.f / 32768.f);
/* Lerp */
f32 t = in_frame_pos_exact - (f32)in_frame_pos_prev;
f32 sample1 = LerpF32(sample1_prev, sample1_next, t);
f32 sample2 = LerpF32(sample2_prev, sample2_next, t);
out_samples[(out_frame_pos * 2) + 0] += sample1;
out_samples[(out_frame_pos * 2) + 1] += sample2;
}
} else {
/* 16 bit Mono -> 32 bit Stereo */
for (u64 out_frame_pos = 0; out_frame_pos < out_frames_count; ++out_frame_pos) {
f32 in_frame_pos_exact = source_frame_pos_start + (((f32)out_frame_pos / (f32)out_frames_count) * (f32)source_frames_count);
u32 in_frame_pos_prev = FloorF32ToI32(in_frame_pos_exact);
u32 in_frame_pos_next = CeilF32ToI32(in_frame_pos_exact);
/* Sample source */
f32 sample_prev = sample_sound(source, in_frame_pos_prev, desc.looping) * (1.f / 32768.f);
f32 sample_next = sample_sound(source, in_frame_pos_next, desc.looping) * (1.f / 32768.f);
/* Lerp */
f32 t = (f32)in_frame_pos_exact - in_frame_pos_prev;
f32 sample = LerpF32(sample_prev, sample_next, t);
out_samples[(out_frame_pos * 2) + 0] += sample;
out_samples[(out_frame_pos * 2) + 1] += sample;
}
}
}
/* ========================== *
* Spatialize
* ========================== */
if (desc.flags & MIXER_FLAG_SPATIALIZE) {
__profn("Spatialize");
/* Algorithm constants */
const f32 rolloff_height = 1.2f;
const f32 rolloff_scale = 6.0f;
const f32 pan_scale = 0.75;
Vec2 pos = desc.pos;
/* If sound pos = listener pos, pretend sound is close in front of listener. */
if (EqVec2(listener_pos, pos)) {
pos = AddVec2(listener_pos, MulVec2(listener_dir, 0.001f));
}
Vec2 sound_rel = SubVec2(pos, listener_pos);
Vec2 sound_rel_dir = NormVec2(sound_rel);
/* Calculate volume */
f32 volume_start = effect_data->spatial_volume;
f32 volume_end;
{
/* https://www.desmos.com/calculator/c2h941hobz
* h = `rolloff_height`
* s = `rolloff_scale`
*/
f32 dist = Vec2Len(sound_rel);
f32 v = (dist / rolloff_scale) + 1.0f;
volume_end = rolloff_height * (1.0f / (v * v));
}
effect_data->spatial_volume = volume_end;
/* Calculate pan */
f32 pan_start = effect_data->spatial_pan;
f32 pan_end = WedgeVec2(listener_dir, sound_rel_dir) * pan_scale;
effect_data->spatial_pan = pan_end;
/* Spatialize samples */
for (u64 frame_pos = 0; frame_pos < frame_count; ++frame_pos) {
f32 t = (f32)frame_pos / (f32)(frame_count - 1);
f32 volume = LerpF32(volume_start, volume_end, t);
f32 pan = LerpF32(pan_start, pan_end, t);
u64 sample1_index = frame_pos * 2;
u64 sample2_index = sample1_index + 1;
f32 sample_mono = ((mix_pcm.samples[sample1_index + 0] / 2.0f) + (mix_pcm.samples[sample2_index] / 2.0f)) * volume;
mix_pcm.samples[sample1_index] = sample_mono * (1.0f - pan);
mix_pcm.samples[sample2_index] = sample_mono * (1.0f + pan);
}
}
/* ========================== *
* Mix into result
* ========================== */
for (u64 i = 0; i < mix_pcm.count; ++i) {
result.samples[i] += mix_pcm.samples[i] * desc.volume;
}
}
{
__profn("Update track effect data");
P_Lock lock = P_LockE(&G.mutex);
for (u64 i = 0; i < mixes_count; ++i) {
struct mix *mix = mixes[i];
struct track *track = track_from_handle(mix->track_handle);
if (track) {
if (mix->track_finished) {
/* Release finished tracks */
track_release_locked(&lock, track);
}
}
}
P_Unlock(&lock);
}
EndScratch(scratch);
return result;
}