trueno/src/audio/mixer.jai

169 lines
5.0 KiB
Plaintext

#if OS != .WASM {
Thread :: #import "Thread";
}
Mixer_Bus :: enum {
MUSIC;
SOUND_EFFECT;
DIALOGUE;
}
Play_Mode :: enum {
ONESHOT;
REPEAT;
}
Mixer_Play_Task :: struct {
audio : *Audio_Data;
bus : Mixer_Bus;
mode : Play_Mode;
unique : bool = true;
curSample : s64 = 0; // output frames consumed
startTime : float64 = 0;
delay : float64 = 0;
silenceBeforeRepeat : float64 = 0;
}
Mixer_Config :: struct {
musicVolume : float = 0.5; @Slider,0,1,0.1
masterVolume : float = 0.5; @Slider,0,1,0.1
dialogueVolume : float = 0.5; @Slider,0,1,0.1
soundEffectVolume : float = 0.3; @Slider,0,1,0.1
}
g_mixer : Mixer;
Mixer :: struct {
paused : bool = false;
#if OS != .WASM {
// If we are on WASM, the Thread module mutex does not work.
// Fortunately we don't need a mutex for this, since sokol does
// not do multithreading for audio on WASM.
//
// Let's just have the mutex as a part of the struct on all platforms
// except WASM. It would probably be a good idea to have a mutex wrapper
// thing that would use the Thread module Mutex or a mutex that works with
// WASM. :NoMutexForWasm
mutex : Thread.Mutex;
}
config : Mixer_Config;
tasks : [..]Mixer_Play_Task;
}
mixer_add_task :: (task: Mixer_Play_Task) {
mixer_lock(*g_mixer);
defer mixer_unlock(*g_mixer);
if task.unique {
for g_mixer.tasks {
if it.audio == task.audio then return;
}
}
array_add(*g_mixer.tasks, task);
}
mixer_add_task :: (audio: *Audio_Data, bus: Mixer_Bus, mode: Play_Mode) {
mixer_add_task(.{
audio = audio,
bus = bus,
mode = mode
});
}
mixer_get_samples :: (buffer: *float, frame_count: s32, channel_count: s32) {
mixer_lock(*g_mixer);
defer mixer_unlock(*g_mixer);
total_samples := frame_count * channel_count;
memset(buffer, 0, total_samples * size_of(float));
if g_mixer.paused then return;
if g_mixer.tasks.count < 1 then return;
for < i : 0..g_mixer.tasks.count-1 {
task := *g_mixer.tasks[i];
if !task.audio {
array_unordered_remove_by_index(*g_mixer.tasks, i);
continue;
}
bus_vol := 1.0;
if task.bus == {
case .MUSIC; bus_vol = g_mixer.config.musicVolume;
case .SOUND_EFFECT; bus_vol = g_mixer.config.soundEffectVolume;
case .DIALOGUE; bus_vol = g_mixer.config.dialogueVolume;
}
vol := bus_vol * g_mixer.config.masterVolume;
source_data := task.audio.data;
source_channels := cast(s64) task.audio.channels;
src_rate := cast(s64) task.audio.sample_rate;
out_rate := cast(s64) saudio_sample_rate();
num_src_frames := source_data.count / source_channels;
task_finished := false;
for f : 0..frame_count-1 {
src_pos := cast(float64)(task.curSample + f) * cast(float64)src_rate / cast(float64)out_rate;
src_frame0 := cast(s64) src_pos;
src_frame1 := src_frame0 + 1;
t := cast(float)(src_pos - cast(float64)src_frame0);
if src_frame0 >= num_src_frames {
if task.mode == .ONESHOT {
task_finished = true;
break;
} else {
src_frame0 = src_frame0 % num_src_frames;
src_frame1 = src_frame1 % num_src_frames;
}
} else if src_frame1 >= num_src_frames {
// At the very last source frame: clamp for ONESHOT, wrap for REPEAT.
src_frame1 = ifx task.mode == .ONESHOT then src_frame0 else 0;
}
for ch : 0..channel_count-1 {
out_index := (f * channel_count) + ch;
src_ch := ifx source_channels == 1 then 0 else ch;
s0 := source_data[src_frame0 * source_channels + src_ch];
s1 := source_data[src_frame1 * source_channels + src_ch];
buffer[out_index] += (s0 + (s1 - s0) * t) * vol;
}
}
task.curSample += frame_count;
// Keep curSample bounded for REPEAT tracks so it never overflows.
if task.mode == .REPEAT && out_rate > 0 && src_rate > 0 {
loop_period := num_src_frames * out_rate / src_rate;
if loop_period > 0 then task.curSample = task.curSample % loop_period;
}
if task_finished {
array_unordered_remove_by_index(*g_mixer.tasks, i);
}
}
}
mixer_lock :: (mixer: *Mixer) {
#if OS != .WASM {
Thread.lock(*mixer.mutex);
} else {
return; // On WASM this is a no-op. See :NoMutexForWasm
}
}
mixer_unlock :: (mixer: *Mixer) {
#if OS != .WASM {
Thread.unlock(*mixer.mutex);
} else {
return; // On WASM this is a no-op. See :NoMutexForWasm
}
}