// 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_Ctx M = Zi; //////////////////////////////////////////////////////////// //~ Bootstrap void MIX_Bootstrap(void) { MIX.track_arena = AcquireArena(Gibi(64)); MIX.listener_pos = VEC2(0, 0); MIX.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) { AssertLockedE(lock, &MIX.mutex); MIX_Track *track = 0; if (MIX.track_first_free) { // Take from free list track = MIX.track_first_free; MIX_Track *next_free = track->next; MIX.track_first_free = next_free; if (next_free) { next_free->prev = 0; } *track = (MIX_Track) { .gen = track->gen + 1 }; } else { // Acquire new track = PushStruct(MIX.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 = MIX.track_last_playing; if (prev) { prev->next = track; } else { MIX.track_first_playing = track; } MIX.track_last_playing = track; track->prev = prev; ++MIX.track_playing_count; return track; } void MIX_ReleaseTrackLocked(Lock *lock, MIX_Track *track) { AssertLockedE(lock, &MIX.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 MIX.track_first_playing = next; } if (next) { next->prev = prev; } else { // Track was last in list MIX.track_last_playing = prev; } --MIX.track_playing_count; ++track->gen; // Add to free list track->prev = 0; track->next = MIX.track_first_free; if (MIX.track_first_free) { MIX.track_first_free->prev = track; } MIX.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, MIX_TRACKDESC()); } MIX_Handle MIX_PlaySoundEx(SND_Sound *sound, MIX_TrackDesc desc) { MIX_Track *track; { Lock lock = LockE(&MIX.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_TrackDesc result = Zi; MIX_Track *track = MIX_TrackFromHandle(handle); if (track) { // TODO: Only lock mutex on track itself or something Lock lock = LockE(&MIX.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_Track *track = MIX_TrackFromHandle(handle); if (track) { // TODO: Only lock mutex on track itself or something Lock lock = LockE(&MIX.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) { Lock lock = LockE(&MIX.mutex); { MIX.listener_pos = pos; MIX.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_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(&MIX.mutex); // Read listener info listener_pos = MIX.listener_pos; listener_dir = MIX.listener_dir; // Update & read mixes mixes = PushStructsNoZero(scratch.arena, MIX_MixData *, MIX.track_playing_count); for (MIX_Track *track = MIX.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 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 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(&MIX.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; }