├── .gitignore ├── outside └── SDL2-2.0.6.tar.gz ├── README.md ├── Makefile └── src └── circular_buffer.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | # Build 2 | *.o 3 | build 4 | build/* 5 | 6 | # Emacs 7 | *~ 8 | \#*\# 9 | .\#* -------------------------------------------------------------------------------- /outside/SDL2-2.0.6.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etscrivner/sdl_audio_circular_buffer/HEAD/outside/SDL2-2.0.6.tar.gz -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SDL Circular Audio Buffer 2 | 3 | This repository contains an example of playing audio by writing raw data to a circular 4 | buffer. Playback is provided by the SDL audio callback that reads from this circular buffer. 5 | 6 | I couldn't find any good information on this online while debugging my own implementation, so I 7 | thought I'd put this together to help others in the future. 8 | 9 | ## Building 10 | 11 | The build has been tested on the following operating systems: 12 | 13 | * macOS 14 | * Linux 15 | 16 | To build you should simply have to clone the repository and then do the following: 17 | 18 | ``` 19 | make all 20 | ``` 21 | 22 | This will attempt to build the SDL dependency provided along with the code in the repository, and 23 | then build the application. If the build is successful, you can then run the example: 24 | 25 | ``` 26 | ./build/circular_buffer 27 | ``` 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Clean engine Makefile 2 | # 3 | 4 | default: all 5 | 6 | # Pick one of: 7 | # linux 8 | # macos 9 | # windows 10 | 11 | UNAME=$(shell uname) 12 | ifeq ($(UNAME),Darwin) 13 | OS=macos 14 | else ifeq ($(UNAME),Linux) 15 | OS=linux 16 | else ifeq ($(UNAME),FreeBSD) 17 | OS=bsd 18 | else ifeq ($(UNAME),OpenBSD) 19 | OS=bsd 20 | else ifneq (,$(findstring MINGW32_NT,$(UNAME))) 21 | OS=win 22 | else 23 | $(error unknown os $(UNAME)) 24 | endif 25 | 26 | # Build directory 27 | # 28 | 29 | BUILD=build 30 | INCLUDE=src 31 | 32 | RM=rm 33 | MAKE=make 34 | ifneq ($(UNAME),FreeBSD) 35 | CXX=g++ 36 | else 37 | CXX=c++ 38 | endif 39 | 40 | CXXFLAGS=-std=c++14 41 | CXXWFLAGS=-Wall -Wextra 42 | 43 | ifeq ($(DEBUG),1) 44 | CXXFLAGS+=--debug 45 | endif 46 | 47 | # Files to build 48 | # 49 | 50 | PROGRAM_NAME=circular_buffer 51 | PROGRAM_OFILES=src/circular_buffer.o 52 | 53 | # Libraries to link 54 | # 55 | 56 | SDL2_VER=SDL2-2.0.6 57 | 58 | ifeq ($(OS),$(filter $(OS),linux macos)) 59 | CXXFLAGS+=-Ioutside/$(SDL2_VER)/include # -Loutside/$(SDL2_VER)/build 60 | SDL2_LIB=outside/$(SDL2_VER)/build/libSDL2.la 61 | PROGRAM_OUTSIDE+=$(SDL2_LIB) 62 | PROGRAM_LIBS=-lSDL2 -lSDL2main 63 | endif 64 | 65 | ifeq ($(OS),linux) 66 | PROGRAM_LIBS+=-lX11 67 | endif 68 | 69 | # Make targets 70 | # 71 | 72 | all: $(PROGRAM_NAME) 73 | 74 | $(PROGRAM_NAME): clean $(BUILD)/$(PROGRAM_NAME) 75 | 76 | $(BUILD)/$(PROGRAM_NAME): $(PROGRAM_OUTSIDE) $(PROGRAM_OFILES) 77 | @echo " BUILD $(BUILD)/$(PROGRAM_NAME)" 78 | @mkdir -p $(BUILD) 79 | @$(CXX) $(CXXFLAGS) -o $(BUILD)/$(PROGRAM_NAME) $(PROGRAM_OFILES) $(PROGRAM_LIBS) 80 | 81 | %.o: %.cpp 82 | @echo " CXX $@" 83 | @$(CXX) -c $(CXXWFLAGS) $(CXXFLAGS) -o $@ $< 84 | 85 | $(SDL2_LIB): 86 | @echo " CXX $(SDL2_VER)" 87 | cd outside && tar -xzf $(SDL2_VER).tar.gz 88 | cd outside/$(SDL2_VER) && ./configure 89 | $(MAKE) -C outside/$(SDL2_VER) 90 | 91 | clean: 92 | $(RM) -f src/*.o build/* 93 | find . -name '*~' -delete 94 | # Misc 95 | # 96 | 97 | TAGS=.tags \ 98 | .etags 99 | 100 | tags: etags 101 | 102 | etags: 103 | etags -f .etags $$(find ./src -name '*.cpp' -or -name '*.hpp') || true 104 | 105 | .PHONY: clean full-clean etags tags 106 | -------------------------------------------------------------------------------- /src/circular_buffer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define internal static 4 | 5 | typedef float Real32; 6 | 7 | static Real32 PI = 3.14159265359; 8 | static Real32 TAU = 2*PI; 9 | static Uint32 SDL_AUDIO_TRANSPARENTLY_CONVERT_FORMAT = 0; 10 | 11 | /////////////////////////////////////////////////////////////////////////////// 12 | 13 | struct platform_program_state 14 | { 15 | bool IsRunning; 16 | SDL_Event LastEvent; 17 | }; 18 | 19 | struct platform_audio_config 20 | { 21 | int ToneHz; 22 | int ToneVolume; 23 | int WavePeriod; 24 | int SampleIndex; 25 | int SamplesPerSecond; 26 | int BytesPerSample; 27 | }; 28 | 29 | struct platform_audio_buffer 30 | { 31 | Uint8* Buffer; 32 | int Size; 33 | int ReadCursor; 34 | int WriteCursor; 35 | SDL_AudioDeviceID DeviceID; 36 | platform_audio_config* AudioConfig; 37 | }; 38 | 39 | struct platform_audio_thread_context 40 | { 41 | platform_audio_buffer* AudioBuffer; 42 | platform_program_state* ProgramState; 43 | }; 44 | 45 | /////////////////////////////////////////////////////////////////////////////// 46 | 47 | internal Sint16 48 | SampleSquareWave(platform_audio_config* AudioConfig) 49 | { 50 | int HalfSquareWaveCounter = AudioConfig->WavePeriod / 2; 51 | if ((AudioConfig->SampleIndex / HalfSquareWaveCounter) % 2 == 0) 52 | { 53 | return AudioConfig->ToneVolume; 54 | } 55 | 56 | return -AudioConfig->ToneVolume; 57 | } 58 | 59 | /////////////////////////////////////////////////////////////////////////////// 60 | 61 | internal Sint16 62 | SampleSineWave(platform_audio_config* AudioConfig) 63 | { 64 | int HalfWaveCounter = AudioConfig->WavePeriod / 2; 65 | return AudioConfig->ToneVolume * sin( 66 | TAU * AudioConfig->SampleIndex / HalfWaveCounter 67 | ); 68 | } 69 | 70 | /////////////////////////////////////////////////////////////////////////////// 71 | 72 | internal void 73 | SampleIntoAudioBuffer(platform_audio_buffer* AudioBuffer, 74 | Sint16 (*GetSample)(platform_audio_config*)) 75 | { 76 | int Region1Size = AudioBuffer->ReadCursor - AudioBuffer->WriteCursor; 77 | int Region2Size = 0; 78 | if (AudioBuffer->ReadCursor < AudioBuffer->WriteCursor) 79 | { 80 | // Fill to the end of the buffer and loop back around and fill to the read 81 | // cursor. 82 | Region1Size = AudioBuffer->Size - AudioBuffer->WriteCursor; 83 | Region2Size = AudioBuffer->ReadCursor; 84 | } 85 | 86 | platform_audio_config* AudioConfig = AudioBuffer->AudioConfig; 87 | 88 | int Region1Samples = Region1Size / AudioConfig->BytesPerSample; 89 | int Region2Samples = Region2Size / AudioConfig->BytesPerSample; 90 | int BytesWritten = Region1Size + Region2Size; 91 | 92 | Sint16* Buffer = (Sint16*)&AudioBuffer->Buffer[AudioBuffer->WriteCursor]; 93 | for (int SampleIndex = 0; 94 | SampleIndex < Region1Samples; 95 | SampleIndex++) 96 | { 97 | Sint16 SampleValue = (*GetSample)(AudioConfig); 98 | *Buffer++ = SampleValue; 99 | *Buffer++ = SampleValue; 100 | AudioConfig->SampleIndex++; 101 | } 102 | 103 | Buffer = (Sint16*)AudioBuffer->Buffer; 104 | for (int SampleIndex = 0; 105 | SampleIndex < Region2Samples; 106 | SampleIndex++) 107 | { 108 | Sint16 SampleValue = (*GetSample)(AudioConfig); 109 | *Buffer++ = SampleValue; 110 | *Buffer++ = SampleValue; 111 | AudioConfig->SampleIndex++; 112 | } 113 | 114 | AudioBuffer->WriteCursor = 115 | (AudioBuffer->WriteCursor + BytesWritten) % AudioBuffer->Size; 116 | } 117 | 118 | /////////////////////////////////////////////////////////////////////////////// 119 | 120 | internal void 121 | PlatformFillAudioDeviceBuffer(void* UserData, Uint8* DeviceBuffer, int Length) 122 | { 123 | platform_audio_buffer* AudioBuffer = (platform_audio_buffer*)UserData; 124 | 125 | // Keep track of two regions. Region1 contains everything from the current 126 | // PlayCursor up until, potentially, the end of the buffer. Region2 only 127 | // exists if we need to circle back around. It contains all the data from the 128 | // beginning of the buffer up until sufficient bytes are read to meet Length. 129 | int Region1Size = Length; 130 | int Region2Size = 0; 131 | if (AudioBuffer->ReadCursor + Length > AudioBuffer->Size) 132 | { 133 | // Handle looping back from the beginning. 134 | Region1Size = AudioBuffer->Size - AudioBuffer->ReadCursor; 135 | Region2Size = Length - Region1Size; 136 | } 137 | 138 | SDL_memcpy( 139 | DeviceBuffer, 140 | (AudioBuffer->Buffer + AudioBuffer->ReadCursor), 141 | Region1Size 142 | ); 143 | SDL_memcpy( 144 | &DeviceBuffer[Region1Size], 145 | AudioBuffer->Buffer, 146 | Region2Size 147 | ); 148 | 149 | AudioBuffer->ReadCursor = 150 | (AudioBuffer->ReadCursor + Length) % AudioBuffer->Size; 151 | } 152 | 153 | /////////////////////////////////////////////////////////////////////////////// 154 | 155 | internal void 156 | PlatformInitializeAudio(platform_audio_buffer* AudioBuffer) 157 | { 158 | SDL_AudioSpec AudioSettings = {}; 159 | AudioSettings.freq = AudioBuffer->AudioConfig->SamplesPerSecond; 160 | AudioSettings.format = AUDIO_S16LSB; 161 | AudioSettings.channels = 2; 162 | AudioSettings.samples = 4096; 163 | AudioSettings.callback = &PlatformFillAudioDeviceBuffer; 164 | AudioSettings.userdata = AudioBuffer; 165 | 166 | SDL_AudioSpec ObtainedSettings = {}; 167 | AudioBuffer->DeviceID = SDL_OpenAudioDevice( 168 | NULL, 0, &AudioSettings, &ObtainedSettings, 169 | SDL_AUDIO_TRANSPARENTLY_CONVERT_FORMAT 170 | ); 171 | 172 | if (AudioSettings.format != ObtainedSettings.format) 173 | { 174 | SDL_Log("Unable to obtain expected audio settings: %s", SDL_GetError()); 175 | exit(1); 176 | } 177 | 178 | // Start playing the audio buffer 179 | SDL_PauseAudioDevice(AudioBuffer->DeviceID, 0); 180 | } 181 | 182 | /////////////////////////////////////////////////////////////////////////////// 183 | 184 | internal void 185 | PlatformHandleEvent(platform_program_state* ProgramState) 186 | { 187 | if (ProgramState->LastEvent.type == SDL_QUIT) 188 | { 189 | ProgramState->IsRunning = false; 190 | } 191 | } 192 | 193 | /////////////////////////////////////////////////////////////////////////////// 194 | 195 | internal int 196 | PlatformAudioThread(void* UserData) 197 | { 198 | platform_audio_thread_context* AudioThread = 199 | (platform_audio_thread_context*)UserData; 200 | 201 | while (AudioThread->ProgramState->IsRunning) 202 | { 203 | SDL_LockAudioDevice(AudioThread->AudioBuffer->DeviceID); 204 | SampleIntoAudioBuffer(AudioThread->AudioBuffer, &SampleSineWave); 205 | SDL_UnlockAudioDevice(AudioThread->AudioBuffer->DeviceID); 206 | } 207 | 208 | return 0; 209 | } 210 | 211 | /////////////////////////////////////////////////////////////////////////////// 212 | 213 | int main() 214 | { 215 | if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) != 0) 216 | { 217 | SDL_Log("Unable to initialized SDL: %s", SDL_GetError()); 218 | return 1; 219 | } 220 | 221 | SDL_Window* Window = SDL_CreateWindow( 222 | "Circular Audio Buffer Example", 223 | SDL_WINDOWPOS_UNDEFINED, 224 | SDL_WINDOWPOS_UNDEFINED, 225 | 1280, 226 | 720, 227 | SDL_WINDOW_OPENGL 228 | ); 229 | 230 | if (!Window) 231 | { 232 | SDL_Log("Unable to initialize window: %s", SDL_GetError()); 233 | return 1; 234 | } 235 | 236 | SDL_Renderer* Renderer = SDL_CreateRenderer(Window, -1, 0); 237 | 238 | platform_audio_config AudioConfig = {}; 239 | AudioConfig.SamplesPerSecond = 44100; 240 | AudioConfig.BytesPerSample = 2 * sizeof(Sint16); 241 | AudioConfig.SampleIndex = 0; 242 | AudioConfig.ToneHz = 256; 243 | AudioConfig.ToneVolume = 3000; 244 | AudioConfig.WavePeriod = 245 | AudioConfig.SamplesPerSecond / AudioConfig.ToneHz; 246 | 247 | platform_audio_buffer AudioBuffer = {}; 248 | AudioBuffer.Size = 249 | AudioConfig.SamplesPerSecond * AudioConfig.BytesPerSample; 250 | AudioBuffer.Buffer = new Uint8[AudioBuffer.Size]; 251 | // Two data points per sample. One for the left speaker, one for the right. 252 | AudioBuffer.ReadCursor = 0; 253 | // NOTE: Offset by 1 sample in order to cause the circular buffer to 254 | // initially be filled. 255 | AudioBuffer.WriteCursor = AudioConfig.BytesPerSample; 256 | AudioBuffer.AudioConfig = &AudioConfig; 257 | memset(AudioBuffer.Buffer, 0, AudioBuffer.Size); 258 | 259 | PlatformInitializeAudio(&AudioBuffer); 260 | 261 | platform_program_state ProgramState = {}; 262 | ProgramState.IsRunning = true; 263 | 264 | platform_audio_thread_context AudioThreadContext = {}; 265 | AudioThreadContext.AudioBuffer = &AudioBuffer; 266 | AudioThreadContext.ProgramState = &ProgramState; 267 | SDL_Thread* AudioThread = SDL_CreateThread( 268 | PlatformAudioThread, "Audio", (void*)&AudioThreadContext 269 | ); 270 | 271 | while (ProgramState.IsRunning) 272 | { 273 | while (SDL_PollEvent(&ProgramState.LastEvent)) 274 | { 275 | PlatformHandleEvent(&ProgramState); 276 | } 277 | 278 | SDL_SetRenderDrawColor(Renderer, 0, 0, 0, 0); 279 | SDL_RenderClear(Renderer); 280 | SDL_RenderPresent(Renderer); 281 | } 282 | 283 | SDL_WaitThread(AudioThread, NULL); 284 | 285 | SDL_DestroyRenderer(Renderer); 286 | SDL_DestroyWindow(Window); 287 | SDL_CloseAudioDevice(AudioBuffer.DeviceID); 288 | SDL_Quit(); 289 | 290 | delete[] AudioBuffer.Buffer; 291 | return 0; 292 | } 293 | --------------------------------------------------------------------------------