Skip to content

Instantly share code, notes, and snippets.

@etscrivner
Created November 15, 2019 16:38
Show Gist options
  • Save etscrivner/0fc546e1d69d0c2f984b4eb25de91940 to your computer and use it in GitHub Desktop.
Save etscrivner/0fc546e1d69d0c2f984b4eb25de91940 to your computer and use it in GitHub Desktop.
Simple audio mixer in SDL2
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <SDL.h>
typedef uint32_t u32;
typedef int32_t i32;
typedef uint16_t u16;
typedef int16_t i16;
typedef uint8_t u8;
typedef int8_t i8;
typedef float f32;
typedef double f64;
typedef u32 b32;
#define Kilobytes(Amount) ((Amount)*1024L)
#define Megabytes(Amount) (Kilobytes(Amount)*1024L)
#define Clamp(X, Min, Max) (((((X) > Max) ? (Max) : (X)) < Min) ? (Min) : (X))
typedef struct memory_arena_t {
u8* Data;
u32 Size;
u32 Used;
} memory_arena;
typedef struct temporary_arena_t {
memory_arena* Arena;
u32 OldUsed;
} temporary_arena;
typedef struct audio_sound_t {
i16* LeftSamples;
i16* RightSamples;
u32 NumSamples;
} audio_sound;
typedef struct playing_sound_t {
audio_sound* Sound;
u32 NumSamplesPlayed;
b32 IsPlaying;
b32 Loop;
f32 CurrentVolume[2];
f32 TargetVolume[2];
f32 dCurrentVolume[2];
struct playing_sound_t* Next;
} playing_sound;
typedef struct audio_state_t {
f32 MasterVolume[2];
playing_sound* FirstPlayingSound;
playing_sound* FirstFreeSound;
memory_arena* PermanentArena;
memory_arena* TemporaryArena;
SDL_AudioDeviceID Device;
SDL_AudioSpec Spec;
} audio_state;
void* ArenaPush(memory_arena* Arena, u32 Size) {
assert(Arena->Used + Size < Arena->Size);
void* Result = (Arena->Data + Arena->Used);
Arena->Used += Size;
// Clear the area we're about to use
memset(Result, 0, Size);
return(Result);
}
void ArenaPop(memory_arena* Arena, u32 Size) {
assert(Arena->Used >= Size);
Arena->Used -= Size;
}
temporary_arena BeginTemporaryArena(memory_arena* Arena) {
temporary_arena Result;
Result.Arena = Arena;
Result.OldUsed = Arena->Used;
return(Result);
}
void EndTemporaryArena(temporary_arena TemporaryArena) {
memory_arena* Arena = TemporaryArena.Arena;
Arena->Used = TemporaryArena.OldUsed;
}
bool LoadAndUnweaveWAV(memory_arena* PermanentArena, audio_sound* Sound, const char* FileName) {
SDL_AudioSpec FileSpec;
u32 LengthBytes;
u8* Buffer;
bool Result = true;
if (SDL_LoadWAV(FileName, &FileSpec, &Buffer, &LengthBytes) == NULL) {
Result = false;
} else {
Sound->NumSamples = LengthBytes / (2 * sizeof(i16));
Sound->LeftSamples = (i16*)ArenaPush(PermanentArena, sizeof(i16)*Sound->NumSamples);
Sound->RightSamples = (i16*)ArenaPush(PermanentArena, sizeof(i16)*Sound->NumSamples);
i16* Samples = (i16*)Buffer;
for (u32 Index = 0; Index < Sound->NumSamples; ++Index)
{
Sound->LeftSamples[Index] = *Samples++;
Sound->RightSamples[Index] = *Samples++;
}
SDL_FreeWAV(Buffer);
}
return(Result);
}
void AudioLock(audio_state* State)
{
SDL_LockAudioDevice(State->Device);
}
void AudioUnlock(audio_state* State)
{
SDL_UnlockAudioDevice(State->Device);
}
void AudioMix(void* UserData, u8* Stream, i32 StreamSizeBytes)
{
memset(Stream, 0, StreamSizeBytes);
audio_state* State = (audio_state*)UserData;
if (State->FirstPlayingSound == NULL)
{
return;
}
u32 SamplesToPlay = StreamSizeBytes / (2 * sizeof(i16));
i16* Output = (i16*)Stream;
f32 SamplesPerSecond = 1.0f / 48000.0f;
for (u32 Sample = 0; Sample < SamplesToPlay; ++Sample)
{
playing_sound* Voice = State->FirstPlayingSound;
// We convert our 16-bit samples to 32-bit floating point values to make
// some of the math below easier and to delay clipping audio until the very
// end when the samples are converted back to 16-bit values.
f32 LeftSample = 0;
f32 RightSample = 0;
playing_sound* Prev = NULL;
while (Voice != NULL)
{
audio_sound* Sound = Voice->Sound;
LeftSample += Voice->CurrentVolume[0] * Sound->LeftSamples[Voice->NumSamplesPlayed];
RightSample += Voice->CurrentVolume[1] * Sound->RightSamples[Voice->NumSamplesPlayed];
for (u32 Channel = 0; Channel < 2; ++Channel)
{
Voice->CurrentVolume[Channel] += SamplesPerSecond * Voice->dCurrentVolume[Channel];
if (Voice->dCurrentVolume[Channel] > 0 && Voice->CurrentVolume[Channel] >= Voice->TargetVolume[Channel])
{
Voice->CurrentVolume[Channel] = Voice->TargetVolume[Channel];
Voice->dCurrentVolume[Channel] = 0.0f;
}
if (Voice->dCurrentVolume[Channel] < 0 && Voice->CurrentVolume[Channel] <= Voice->TargetVolume[Channel])
{
Voice->CurrentVolume[Channel] = Voice->TargetVolume[Channel];
Voice->dCurrentVolume[Channel] = 0.0f;
}
}
Voice->NumSamplesPlayed++;
if (Voice->NumSamplesPlayed >= Sound->NumSamples)
{
if (Voice->Loop) {
Voice->NumSamplesPlayed = 0;
Prev = Voice;
Voice = Voice->Next;
} else {
Voice->IsPlaying = false;
if (Prev) {
Prev->Next = Voice->Next;
} else {
State->FirstPlayingSound = Voice->Next;
}
if (State->FirstFreeSound) {
playing_sound* FreeVoice = Voice;
Voice = Voice->Next;
FreeVoice->Next = State->FirstFreeSound;
State->FirstFreeSound = FreeVoice;
} else {
State->FirstFreeSound = Voice;
Voice = Voice->Next;
State->FirstFreeSound->Next = NULL;
}
}
}
else
{
Prev = Voice;
Voice = Voice->Next;
}
}
LeftSample *= State->MasterVolume[0];
RightSample *= State->MasterVolume[1];
*Output++ = (i16)LeftSample;
*Output++ = (i16)RightSample;
}
}
playing_sound* PlaySound(audio_state* State, audio_sound* Sound, f32 Volume) {
AudioLock(State);
playing_sound* NewSound = {};
if (State->FirstFreeSound) {
NewSound = State->FirstFreeSound;
State->FirstFreeSound = NewSound->Next;
NewSound->Next = NULL;
} else {
NewSound = (playing_sound*)ArenaPush(State->PermanentArena, sizeof(playing_sound));
}
NewSound->Sound = Sound;
NewSound->NumSamplesPlayed = 0;
NewSound->CurrentVolume[0] = NewSound->CurrentVolume[1] = Volume;
NewSound->TargetVolume[0] = NewSound->TargetVolume[1] = 0.0f;
NewSound->dCurrentVolume[0] = NewSound->dCurrentVolume[1] = 0.0f;
NewSound->IsPlaying = true;
if (State->FirstPlayingSound == NULL) {
State->FirstPlayingSound = NewSound;
} else {
NewSound->Next = State->FirstPlayingSound;
State->FirstPlayingSound = NewSound;
}
AudioUnlock(State);
return(NewSound);
}
void ChangeVolume(audio_state* State, playing_sound* Sound,
f32 VolumeLeft, f32 VolumeRight, f32 FadeDurationInSeconds)
{
AudioLock(State);
if (!Sound->IsPlaying)
{
return;
}
if (FadeDurationInSeconds <= 0.0f)
{
Sound->CurrentVolume[0] = VolumeLeft;
Sound->CurrentVolume[1] = VolumeRight;
}
else
{
Sound->TargetVolume[0] = VolumeLeft;
Sound->TargetVolume[1] = VolumeRight;
Sound->dCurrentVolume[0] = (Sound->TargetVolume[0] - Sound->CurrentVolume[0]) / FadeDurationInSeconds;
Sound->dCurrentVolume[1] = (Sound->TargetVolume[1] - Sound->CurrentVolume[1]) / FadeDurationInSeconds;
}
AudioUnlock(State);
}
int main()
{
SDL_Init(SDL_INIT_EVERYTHING);
SDL_Window* Window = SDL_CreateWindow(
"Mixer",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
800,
600,
0
);
SDL_Renderer* Renderer = SDL_CreateRenderer(
Window,
-1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
);
audio_state State = {};
State.MasterVolume[0] = 1.0f;
State.MasterVolume[1] = 1.0f;
{
SDL_AudioSpec DesiredSpec = {};
DesiredSpec.freq = 48000;
DesiredSpec.format = AUDIO_S16;
DesiredSpec.channels = 2;
DesiredSpec.samples = 256;
DesiredSpec.callback = AudioMix;
DesiredSpec.userdata = &State;
State.Device = SDL_OpenAudioDevice(NULL, 0, &DesiredSpec, &State.Spec, 0);
if (State.Device == 0) {
fprintf(stderr, "Failed to open audio device: %s\n", SDL_GetError());
} else {
printf("SDL Audio Spec:\n");
printf(" * Frequency: %d\n", State.Spec.freq);
printf(" * Format: %d (%d)\n", State.Spec.format, AUDIO_S16);
printf(" * Chanels: %d\n", State.Spec.channels);
printf(" * Samples: %d\n", State.Spec.samples);
printf(" * Callback: %p\n", State.Spec.callback);
printf(" * Userdata: %p\n", State.Spec.userdata);
if (!memcmp(&DesiredSpec, &State.Spec, sizeof(SDL_AudioSpec)) != 0) {
fprintf(stderr, "Failed to get required audio spec from SDL.\n");
}
}
}
memory_arena PermanentArena = {};
memory_arena TemporaryArena = {};
{
// Here is where we set our memory budget for the entire application. We
// could further reduce the memory footprint for permanent storage by
// instead streaming audio files in from disk in chunks as required by the
// mixer.
PermanentArena.Size = Megabytes(25);
PermanentArena.Data = (u8*)calloc(1, PermanentArena.Size);
TemporaryArena.Size = Megabytes(1);
TemporaryArena.Data = (u8*)calloc(1, TemporaryArena.Size);
State.PermanentArena = &PermanentArena;
State.TemporaryArena = &TemporaryArena;
}
audio_sound Song = {};
audio_sound JumpSound = {};
playing_sound* MusicPlaying = {};
{
if (!LoadAndUnweaveWAV(&PermanentArena, &Song, "highlands.wav")) {
fprintf(stderr, "Failed to load 'highlands.wav'\n");
}
if (!LoadAndUnweaveWAV(&PermanentArena, &JumpSound, "jump.wav")) {
fprintf(stderr, "Failed to load 'jump.wav'\n");
}
MusicPlaying = PlaySound(&State, &Song, 0.05f);
}
SDL_PauseAudioDevice(State.Device, 0);
bool Running = true;
SDL_Event Event;
while (Running) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
Running = false;
}
if (Event.type == SDL_KEYDOWN) {
if (Event.key.keysym.sym == SDLK_ESCAPE) {
Running = false;
}
if (Event.key.keysym.sym == SDLK_SPACE) {
ChangeVolume(&State, MusicPlaying, 1.0f, 1.0f, 10.0f);
PlaySound(&State, &JumpSound, 0.25f);
}
if (Event.key.keysym.sym == SDLK_a) {
ChangeVolume(&State, MusicPlaying, 0.05f, 0.05f, 5.0f);
}
}
}
SDL_SetRenderDrawColor(Renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(Renderer);
SDL_RenderPresent(Renderer);
}
SDL_PauseAudioDevice(State.Device, 1);
free(PermanentArena.Data);
free(TemporaryArena.Data);
SDL_DestroyWindow(Window);
SDL_Quit();
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment