468 lines
13 KiB
C
468 lines
13 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
|
|
|
|
MIX_SharedState M_shared_state = ZI;
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Bootstrap
|
|
|
|
void MIX_Bootstrap(void)
|
|
{
|
|
MIX_SharedState *g = &M_shared_state;
|
|
g->track_arena = AcquireArena(Gibi(64));
|
|
g->listener_pos = VEC2(0, 0);
|
|
g->listener_dir = VEC2(0, -1);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Track
|
|
|
|
MIX_Handle MIX_HandleFromTrack(MIX_Track *track)
|
|
{
|
|
MIX_Handle result = ZI;
|
|
result.gen = track->gen;
|
|
result.data = track;
|
|
return result;
|
|
}
|
|
|
|
MIX_Track *MIX_TrackFromHandle(MIX_Handle handle)
|
|
{
|
|
MIX_Track *track = (MIX_Track *)handle.data;
|
|
if (track && track->gen == handle.gen)
|
|
{
|
|
return track;
|
|
}
|
|
else
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
MIX_Track *MIX_AcquireTrackLocked(Lock *lock, SND_Sound *sound)
|
|
{
|
|
MIX_SharedState *g = &M_shared_state;
|
|
AssertLockedE(lock, &g->mutex);
|
|
|
|
MIX_Track *track = 0;
|
|
if (g->track_first_free)
|
|
{
|
|
// Take from free list
|
|
track = g->track_first_free;
|
|
MIX_Track *next_free = track->next;
|
|
g->track_first_free = next_free;
|
|
if (next_free)
|
|
{
|
|
next_free->prev = 0;
|
|
}
|
|
*track = (MIX_Track) { .gen = track->gen + 1 };
|
|
}
|
|
else
|
|
{
|
|
// Acquire new
|
|
track = PushStruct(g->track_arena, MIX_Track);
|
|
track->gen = 1;
|
|
}
|
|
|
|
track->sound = sound;
|
|
track->mix.source = sound;
|
|
track->mix.track_handle = MIX_HandleFromTrack(track);
|
|
|
|
// Append to playing list
|
|
MIX_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;
|
|
}
|
|
|
|
void MIX_ReleaseTrackLocked(Lock *lock, MIX_Track *track)
|
|
{
|
|
MIX_SharedState *g = &M_shared_state;
|
|
AssertLockedE(lock, &g->mutex);
|
|
|
|
// Remove from playing list
|
|
MIX_Track *prev = track->prev;
|
|
MIX_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;
|
|
}
|
|
|
|
// TODO: Rework interface to be command based instead of directly modifying tracks.
|
|
|
|
MIX_Handle MIX_PlaySound(SND_Sound *sound)
|
|
{
|
|
return MIX_PlaySoundEx(sound, M_TRACKDESC());
|
|
}
|
|
|
|
MIX_Handle MIX_PlaySoundEx(SND_Sound *sound, MIX_TrackDesc desc)
|
|
{
|
|
MIX_SharedState *g = &M_shared_state;
|
|
MIX_Track *track;
|
|
{
|
|
Lock lock = LockE(&g->mutex);
|
|
{
|
|
track = MIX_AcquireTrackLocked(&lock, sound);
|
|
track->desc = desc;
|
|
}
|
|
Unlock(&lock);
|
|
}
|
|
return MIX_HandleFromTrack(track);
|
|
}
|
|
|
|
// NOTE: This is quite inefficient.
|
|
MIX_TrackDesc MIX_TrackDescFromHandle(MIX_Handle handle)
|
|
{
|
|
MIX_SharedState *g = &M_shared_state;
|
|
MIX_TrackDesc result = ZI;
|
|
|
|
MIX_Track *track = MIX_TrackFromHandle(handle);
|
|
if (track)
|
|
{
|
|
// TODO: Only lock mutex on track itself or something
|
|
Lock lock = LockE(&g->mutex);
|
|
{
|
|
// Confirm handle is still valid now that we're locked
|
|
track = MIX_TrackFromHandle(handle);
|
|
if (track)
|
|
{
|
|
result = track->desc;
|
|
}
|
|
}
|
|
Unlock(&lock);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// NOTE: This is quite inefficient.
|
|
void MIX_UpdateTrack(MIX_Handle handle, MIX_TrackDesc desc)
|
|
{
|
|
MIX_SharedState *g = &M_shared_state;
|
|
MIX_Track *track = MIX_TrackFromHandle(handle);
|
|
if (track)
|
|
{
|
|
// TODO: Only lock mutex on track itself or something
|
|
Lock lock = LockE(&g->mutex);
|
|
{
|
|
// Confirm handle is still valid now that we're locked
|
|
track = MIX_TrackFromHandle(handle);
|
|
if (track)
|
|
{
|
|
track->desc = desc;
|
|
}
|
|
}
|
|
Unlock(&lock);
|
|
}
|
|
}
|
|
|
|
void MIX_UpdateListener(Vec2 pos, Vec2 dir)
|
|
{
|
|
MIX_SharedState *g = &M_shared_state;
|
|
Lock lock = LockE(&g->mutex);
|
|
{
|
|
g->listener_pos = pos;
|
|
g->listener_dir = NormVec2(dir);
|
|
}
|
|
Unlock(&lock);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Mix
|
|
|
|
i16 MIX_SampleSound(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
|
|
MIX_PcmF32 MIX_MixAllTracks(Arena *arena, u64 frame_count)
|
|
{
|
|
TempArena scratch = BeginScratch(arena);
|
|
MIX_SharedState *g = &M_shared_state;
|
|
|
|
MIX_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 mix array
|
|
|
|
MIX_MixData **mixes = 0;
|
|
u64 mixes_count = 0;
|
|
{
|
|
Lock lock = LockE(&g->mutex);
|
|
|
|
// Read listener info
|
|
listener_pos = g->listener_pos;
|
|
listener_dir = g->listener_dir;
|
|
|
|
// Update & read mixes
|
|
mixes = PushStructsNoZero(scratch.arena, MIX_MixData *, g->track_playing_count);
|
|
for (MIX_Track *track = g->track_first_playing; track; track = track->next)
|
|
{
|
|
MIX_MixData *mix = &track->mix;
|
|
mix->desc = track->desc;
|
|
mixes[mixes_count++] = mix;
|
|
}
|
|
|
|
Unlock(&lock);
|
|
}
|
|
|
|
//- Process mix data
|
|
|
|
for (u64 mix_index = 0; mix_index < mixes_count; ++mix_index)
|
|
{
|
|
MIX_MixData *mix = mixes[mix_index];
|
|
|
|
if (mix->source->samples_count <= 0)
|
|
{
|
|
// Skip empty sounds
|
|
continue;
|
|
}
|
|
|
|
SND_Sound *source = mix->source;
|
|
MIX_TrackDesc desc = mix->desc;
|
|
MIX_EffectData *effect_data = &mix->effect_data;
|
|
b32 source_is_stereo = source->flags & SND_SoundFlag_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;
|
|
|
|
MIX_PcmF32 mix_pcm = {
|
|
.count = result.count,
|
|
.samples = PushStructs(scratch.arena, f32, result.count)
|
|
};
|
|
|
|
//- Resample
|
|
// Transform 16 bit source -> 32 bit stereo at output duration
|
|
{
|
|
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 = MIX_SampleSound(source, (in_frame_pos_prev * 2) + 0, desc.looping) * (1.f / 32768.f);
|
|
f32 sample1_next = MIX_SampleSound(source, (in_frame_pos_next * 2) + 0, desc.looping) * (1.f / 32768.f);
|
|
f32 sample2_prev = MIX_SampleSound(source, (in_frame_pos_prev * 2) + 1, desc.looping) * (1.f / 32768.f);
|
|
f32 sample2_next = MIX_SampleSound(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 = MIX_SampleSound(source, in_frame_pos_prev, desc.looping) * (1.f / 32768.f);
|
|
f32 sample_next = MIX_SampleSound(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 & MIX_TrackFlag_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 (MatchVec2(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);
|
|
|
|
// Compute 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;
|
|
|
|
// Compute 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;
|
|
}
|
|
}
|
|
|
|
//- Update track effect data
|
|
{
|
|
Lock lock = LockE(&g->mutex);
|
|
for (u64 i = 0; i < mixes_count; ++i)
|
|
{
|
|
MIX_MixData *mix = mixes[i];
|
|
MIX_Track *track = MIX_TrackFromHandle(mix->track_handle);
|
|
if (track)
|
|
{
|
|
if (mix->track_finished)
|
|
{
|
|
// Release finished tracks
|
|
MIX_ReleaseTrackLocked(&lock, track);
|
|
}
|
|
}
|
|
}
|
|
Unlock(&lock);
|
|
}
|
|
|
|
EndScratch(scratch);
|
|
return result;
|
|
}
|