Created
November 15, 2019 16:38
-
-
Save etscrivner/0fc546e1d69d0c2f984b4eb25de91940 to your computer and use it in GitHub Desktop.
Simple audio mixer in SDL2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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