From c39902d6b1b33c3dca4c84dad8036c57dafb75f8 Mon Sep 17 00:00:00 2001 From: Charlie <66182434+asbott@users.noreply.github.com> Date: Mon, 15 Jul 2024 21:40:27 +0200 Subject: [PATCH] v0.01.000 - AUDIO! --- TODO | 30 +- build.bat | 2 +- build.c | 10 +- changelog.txt | 41 +- oogabooga/audio.c | 1359 ++++- oogabooga/base.c | 8 +- oogabooga/concurrency.c | 10 +- oogabooga/drawing.c | 8 +- oogabooga/examples/audio_test.c | 129 +- oogabooga/examples/block.wav | Bin 0 -> 44980 bytes oogabooga/examples/bruh.wav | Bin 2398616 -> 941196 bytes oogabooga/examples/text_rendering.c | 2 +- oogabooga/linmath.c | 123 +- oogabooga/memory.c | 43 +- oogabooga/oogabooga.c | 29 +- oogabooga/os_impl_windows.c | 253 +- oogabooga/os_interface.c | 37 +- oogabooga/string.c | 8 + oogabooga/string_format.c | 7 + oogabooga/tests.c | 5 +- oogabooga/third_party.c | 119 +- oogabooga/third_party/dr_wav.h | 8816 --------------------------- oogabooga/third_party/stb_vorbis.c | 146 +- 23 files changed, 1987 insertions(+), 9198 deletions(-) create mode 100644 oogabooga/examples/block.wav delete mode 100644 oogabooga/third_party/dr_wav.h diff --git a/TODO b/TODO index 19eea86..7afa339 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,33 @@ - Audio + - Player volume control - Optimize - Spam simd - - Concurrent jobs for players? \ No newline at end of file + - Concurrent jobs for players? + - Mega buffer for contiguous intermediate buffers + We definitely also want a limit to how much memory we want allocated to intermediate buffers. + - Avoid playing identical audio at the same time + - Custom audio mixing + - Fix vorbis >FILE< streaming (#StbVorbisFileStream) + - Handle audio sources which had their format defaulted to update when default device changes + For streamed audio sources this should be easy enough, because the conversion happens from raw format to source format as we stream it. + For loaded sources though, we would need to convert the source->pcm_frames. + - Bugs: + - Small fadeout on pause is slightly noisy + - Setting time stamp/progression causes noise (need fade transition like on pause/play) + - End of clip also causes noise if audio clip does not end smoothly + - Setting audio source to a format which differs from audio output format in both channels and bit_width at the same time will produce pure loud noise. + - 24-Bit audio conversion doesn't really work + +- Juice + - draw_line + - vx_length + - Occlude quads out of view + +- General bugs & issues + - Release freeze in run_tests + +- Needs testing: + - Audio format channel conversions + - sample rate downsampling + - non stereo or mono audio \ No newline at end of file diff --git a/build.bat b/build.bat index fecb595..ed21dba 100644 --- a/build.bat +++ b/build.bat @@ -6,6 +6,6 @@ mkdir build pushd build -clang -g -o cgame.exe ../build.c -O0 -std=c11 -D_CRT_SECURE_NO_WARNINGS -Wextra -Wno-incompatible-library-redeclaration -Wno-sign-compare -Wno-unused-parameter -Wno-builtin-requires-header -lkernel32 -lgdi32 -luser32 -lruntimeobject -lwinmm -ld3d11 -ldxguid -ld3dcompiler -lshlwapi -lole32 -lavrt -lksuser -femit-all-decls +clang -g -o cgame.exe ../build.c -O0 -std=c11 -D_CRT_SECURE_NO_WARNINGS -Wextra -Wno-incompatible-library-redeclaration -Wno-sign-compare -Wno-unused-parameter -Wno-builtin-requires-header -lkernel32 -lgdi32 -luser32 -lruntimeobject -lwinmm -ld3d11 -ldxguid -ld3dcompiler -lshlwapi -lole32 -lavrt -lksuser -ldbghelp -femit-all-decls popd \ No newline at end of file diff --git a/build.c b/build.c index 861eb33..1c5825e 100644 --- a/build.c +++ b/build.c @@ -3,8 +3,6 @@ /// // Build config stuff -#define OOGABOOGA_DEV 1 - #define INITIAL_PROGRAM_MEMORY_SIZE MB(5) typedef struct Context_Extra { @@ -14,7 +12,7 @@ typedef struct Context_Extra { #define CONTEXT_EXTRA Context_Extra // This defaults to "entry", but we can set it to anything (except "main" or other existing proc names" -#define ENTRY_PROC entry + #define ENTRY_PROC entry // Ooga booga needs to be included AFTER configuration and BEFORE the program code #include "oogabooga/oogabooga.c" @@ -27,14 +25,14 @@ typedef struct Context_Extra { // Comment & Uncomment these to swap projects (only include one at a time) // -// this is a minimal starting point for new projects. Copy & rename to get started -// #include "oogabooga/examples/minimal_game_loop.c" +// This is a minimal starting point for new projects. Copy & rename to get started +#include "oogabooga/examples/minimal_game_loop.c" // #include "oogabooga/examples/text_rendering.c" // #include "oogabooga/examples/custom_logger.c" // #include "oogabooga/examples/renderer_stress_test.c" // #include "oogabooga/examples/tile_game.c" -#include "oogabooga/examples/audio_test.c" +// #include "oogabooga/examples/audio_test.c" // This is where you swap in your own project! // #include "entry_yourepicgamename.c" diff --git a/changelog.txt b/changelog.txt index bd354c2..df40609 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,19 +1,50 @@ -## v0.01.001 - AUDIO! +## v0.01.000 - AUDIO! - Added audio sources - File stream sources & Fully decoded sources - 16-bit int & 32-bit float support - - WAV & OGG support + - WAV & OGG support (in-house wav decoder, stb_vorbis for ogg) - Usage: - bool audio_source_init_file_stream(*src, path, bit_width, allocator) - bool audio_source_init_file_decode(*src, path, bit_width, allocator) + bool audio_open_source_stream(*src, path, allocator) + bool audio_open_source_load(*src, path, allocator) void audio_source_destroy(*src) + - Added audio playback + - The simple way: + play_one_audio_clip_source(source); + play_one_audio_clip(path); + - With Audio_Player: + audio_player_get_one(); + audio_player_release(*player); + audio_player_set_state(*player, state); + audio_player_set_time_stamp(*player, time_in_seconds); + audio_player_set_progression_factor(*player, factor); + audio_player_get_time_stamp(*player); + audio_player_get_current_progression_factor(*player); + audio_player_set_source(*player, src, bool retain_progression_factor); + audio_player_clear_source(*player); + audio_player_set_looping(*player, bool looping); + - Added some basic features in the audio system + - Conversion & resampling between audio formats + - Automatically detects new default device + - Small fade in pause/play for less noise from sudden changes in playback buffer + - Added a audio_test.c example + - More OS procuedres + - os_file_set_pos(file, pos) + - os_file_get_pos(file) + - os_file_get_size(file) + - os_get_stack_trace(allocator) - Misc - Win32 audio impl + - Link to more win32 junk - Make default logger thread safe - Rename tm_scope_cycles & tm_scope_cycles_xxx -> tm_scope & tm_scope_xxx - Minor cleanups - + - Fix Vector types align woopsies + - Fix bug in heap allocator which caused internal heap corruption + - Maybe fixed spinlocks with memory barriers? + - Bunch of more asserts & better detection for heap allocation issues + - lerpf, lerpi, smerpf, smerpi + - Failed asserts now prints the stack trace. ## v0.00.005 - Z layers diff --git a/oogabooga/audio.c b/oogabooga/audio.c index e5d8e03..ddc1555 100644 --- a/oogabooga/audio.c +++ b/oogabooga/audio.c @@ -1,11 +1,35 @@ -bool check_wav_header(string data) { - return string_starts_with(data, STR("RIFF")); -} -bool check_ogg_header(string data) { - return string_starts_with(data, STR("OggS")); -} +/* + + Loading audio: + + bool audio_open_source_stream(Audio_Source *src, string path, Allocator allocator); + bool audio_open_source_load(Audio_Source *src, string path, Allocator allocator); + void audio_source_destroy(Audio_Source *src); + + Playing audio (the simple way): + + void play_one_audio_clip_source(Audio_Source source); + void play_one_audio_clip(string path); + + Playing audio (with players): + + Audio_Player * audio_player_get_one(); + void audio_player_release(Audio_Player *p); + + void audio_player_set_state(Audio_Player *p, Audio_Player_State state); + void audio_player_set_time_stamp(Audio_Player *p, float64 time_in_seconds); + void audio_player_set_progression_factor(Audio_Player *p, float64 factor); + float64 audio_player_get_time_stamp(Audio_Player *p); + float64 audio_player_get_current_progression_factor(Audio_Player *p); + void audio_player_set_source(Audio_Player *p, Audio_Source src, bool retain_progression_factor); + void audio_player_clear_source(Audio_Player *p); + void audio_player_set_looping(Audio_Player *p, bool looping); + +*/ + + // Supporting more than s16 and f32 // If it's a real thing that there's audio devices which support neither then I will be surprised @@ -27,11 +51,15 @@ typedef struct Audio_Format { int sample_rate; } Audio_Format; +// Implemented per OS +forward_global Audio_Format audio_output_format; +forward_global Mutex audio_init_mutex; + // I don't see a big reason for you to use anything else than WAV and OGG. // If you use mp3 that's just not very smart. // Ogg has better quality AND better compression AND you don't need any licensing (which you need for mp3) // https://convertio.co/mp3-ogg/ -// I will probably add mp3 support at some point for compatibility reasonavg. +// I will probably add mp3 support at some point for compatibility reasons. // - Charlie 2024-07-11 typedef enum Audio_Decoder_Kind { AUDIO_DECODER_WAV, @@ -43,107 +71,617 @@ typedef enum Audio_Source_Kind { AUDIO_SOURCE_MEMORY, // Raw pcm frames } Audio_Source_Kind; +typedef struct { + u32 data1; + u16 data2; + u16 data3; + u8 data4[8]; +} Wav_Subformat_Guid; +inline bool is_equal_wav_guid(const Wav_Subformat_Guid* a, const Wav_Subformat_Guid* b) { + return ( + a->data1 == b->data1 && + a->data2 == b->data2 && + a->data3 == b->data3 && + memcmp(a->data4, b->data4, 8) == 0 + ); +} + +const Wav_Subformat_Guid WAV_SUBTYPE_PCM = (Wav_Subformat_Guid){ 0x00000001, 0x0000, 0x0010, { 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } }; +const Wav_Subformat_Guid WAV_SUBTYPE_IEEE_FLOAT = (Wav_Subformat_Guid){ 0x00000003, 0x0000, 0x0010, { 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } }; + +typedef struct Wav_Stream { + File file; + int channels; + int sample_rate; + int format; + int bits_per_sample; + u64 number_of_frames; + u64 pcm_start; + u64 valid_bits_per_sample; + Wav_Subformat_Guid sub_format; +} Wav_Stream; + typedef struct Audio_Source { Audio_Source_Kind kind; Audio_Format format; u64 number_of_frames; Allocator allocator; - string compressed_data; // For file stream Audio_Decoder_Kind decoder; union { - drwav wav; + Wav_Stream wav; stb_vorbis *ogg; }; + // #Memory #Incomplete #StbVorbisFileStream + // I tried replacing the stdio stuff in stb_vorbis with oogabooga file api, but now + // stb_vorbis is shitting itself. + // So, for now, we're just reading the file into memory and then streaming from that + // memory. + // Luckily, ogg compression is pretty good so it's not going to completely butcher + // memory usage, but it's definitely suboptimal. + string ogg_raw; + // For memory source void *pcm_frames; + Mutex mutex_for_destroy; // This should ONLY be used so a source isnt sampled on audio thread while it's being destroyed + } Audio_Source; -int -_audio_file_stream_sample_frames(Audio_Source *src, u64 first_frame_index, - u64 number_of_frames, void *output_buffer); +int +convert_frames(void *dst, Audio_Format dst_format, + void *src, Audio_Format src_format, u64 src_frame_count); bool -audio_source_init_file_stream(Audio_Source *src, string path, Audio_Format_Bits bit_width, - Allocator allocator) { +check_wav_header(string data) { + return string_starts_with(data, STR("RIFF")); +} +bool +check_ogg_header(string data) { + return string_starts_with(data, STR("OggS")); +} + +bool +wav_open_file(string path, Wav_Stream *wav, u64 sample_rate, u64 *number_of_frames) { + + // https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html + // Seriously, why is everything Microsoft makes so stinky when it should be so simple? + // The above specification is pretty straight forward and to the point, but then I + // start finding nonsense in random WAV files because some people thought it was a + // good idea to try and add "extensions" to it and now there is LITERALLY junk in + // wave files that should just be so simple. + // Apparently, some wave files are just... mpeg audio... with a wave header... + // I JUST WANT SOME PCM FRAMES + + wav->file = os_file_open(path, O_READ); + + if (wav->file == OS_INVALID_FILE) return false; + + string header = talloc_string(12); + + u64 read; + bool ok = os_file_read(wav->file, header.data, 12, &read); + if (!ok || read != 12) { + os_file_close(wav->file); + return false; + } + + + if (!strings_match(string_view(header, 0, 4), STR("RIFF"))) return false; + if (!strings_match(string_view(header, 8, 4), STR("WAVE"))) { + log_error("Invalid header in wave file @ %s", path); + os_file_close(wav->file); + return false; + } + + u32 number_of_sub_chunk_bytes = *(u32*)(header.data+4); + number_of_sub_chunk_bytes -= 4; // ckID + + u32 avg_bytes_per_sec; + u16 block_align; + + string chunk_header = talloc_string(8); + const u64 NON_DATA_CHUNK_MAX_SIZE = 40; + string chunk = talloc_string(NON_DATA_CHUNK_MAX_SIZE); + + for (u64 byte_pos = 4; byte_pos < number_of_sub_chunk_bytes;) { + ok = os_file_read(wav->file, chunk_header.data, 8, &read); + if (!ok || read != 8) { + os_file_close(wav->file); + return false; + } + byte_pos += 8; + + string chunk_id = string_view(chunk_header, 0, 4); + u32 chunk_size = *(u32*)(chunk_header.data+4); + + byte_pos += chunk_size; + + // Ignored chunks + // THIS IS WHY WE CAN'T HAVE NICE THINGS + // (yes, some files actually has a chunk with id "junk") + if (strings_match(chunk_id, STR("bext")) + || strings_match(chunk_id, STR("fact")) + || strings_match(chunk_id, STR("junk"))) { + u64 pos = os_file_get_pos(wav->file); + os_file_set_pos(wav->file, pos+chunk_size); + continue; + } + + if (!strings_match(chunk_id, STR("data")) && chunk_size <= NON_DATA_CHUNK_MAX_SIZE) { + ok = os_file_read(wav->file, chunk.data, chunk_size, &read); + if (!ok || read != chunk_size) { + os_file_close(wav->file); + return false; + } + } + + if (strings_match(chunk_id, STR("fmt "))) { + + if (chunk_size != 16 && chunk_size != 18 && chunk_size != 40) { + log_error("Invalid wav fmt chunk, bad size %d", chunk_size); + os_file_close(wav->file); + return false; + } + + if (chunk_size >= 16) { + wav->format = *(u16*)(chunk.data+0); + wav->channels = *(u16*)(chunk.data+2); + wav->sample_rate = *(u32*)(chunk.data+4); + avg_bytes_per_sec = *(u32*)(chunk.data+8); + block_align = *(u16*)(chunk.data+12); + wav->bits_per_sample = *(u16*)(chunk.data+14); + + wav->valid_bits_per_sample = wav->bits_per_sample; + } + if (chunk_size == 40) { + /* cbSize @+16 */ + wav->valid_bits_per_sample = *(u16*)(chunk.data+18); + // u32 channel_mask = *(u16*)(chunk.data+20); + wav->sub_format = *(Wav_Subformat_Guid*)(chunk.data+24); + } + + } else if (strings_match(chunk_id, STR("data"))) { + + u64 number_of_bytes = chunk_size; + + if (number_of_bytes % 2 != 0) { + // Consume pad byte + u8 pad; + ok = os_file_read(wav->file, &pad, 1, &read); + if (!ok || read != 1) { + os_file_close(wav->file); + return false; + } + } + + wav->pcm_start = byte_pos - chunk_size; + + u64 number_of_samples + = number_of_bytes / (wav->bits_per_sample / 8); + + wav->number_of_frames = number_of_samples / wav->channels; + *number_of_frames = wav->number_of_frames; // If same sample rates... + } else { + log_warning("Unhandled chunk id '%s' in wave file @ %s", chunk_id, path); + + if (chunk_size > NON_DATA_CHUNK_MAX_SIZE) { + u64 pos = os_file_get_pos(wav->file); + os_file_set_pos(wav->file, pos+chunk_size); + } + } + } + + if (wav->format == 0xFFFE) { + if (is_equal_wav_guid(&wav->sub_format, &WAV_SUBTYPE_PCM)) { + wav->format = 0x0001; + } else if (is_equal_wav_guid(&wav->sub_format, &WAV_SUBTYPE_IEEE_FLOAT)) { + wav->format = 0x0003; + } else { + os_file_close(wav->file); + return false; + } + } + + if ((wav->format != 0x0001 && wav->format != 0x0003 + || wav->format == 0x0003 && wav->valid_bits_per_sample != 32)) { + log_error("Wav file @ '%s' format 0x%x (%d bits) is not supported.", path, wav->format, wav->valid_bits_per_sample); + return false; + } + + if (wav->valid_bits_per_sample == 24) { + log_warning("The current support for 24-bit wave audio is hit-or-miss. If the audio sounds weird, you should convert it to another bit-width (or vorbis)."); + } + + if (wav->sample_rate != sample_rate) { + f32 ratio = (f32)sample_rate/(f32)wav->sample_rate; + *number_of_frames = (u64)round((f32)wav->number_of_frames*ratio); + } + + // Set the file position to the beginning of the PCM data + ok = os_file_set_pos(wav->file, wav->pcm_start); + if (!ok) { + os_file_close(wav->file); + return false; + } + + return true; +} +void +wav_close(Wav_Stream *wav) { + os_file_close(wav->file); +} +bool +wav_set_frame_pos(Wav_Stream *wav, u64 output_sample_rate, u64 frame_index) { + + f32 ratio = (f32)wav->sample_rate/(f32)output_sample_rate; + frame_index = (u64)round(ratio*(f32)frame_index); + + u64 frame_size = wav->channels*(wav->bits_per_sample/8); + return os_file_set_pos(wav->file, wav->pcm_start + frame_index*frame_size); +} +u64 +wav_read_frames(Wav_Stream *wav, Audio_Format format, void *frames, + u64 number_of_frames) { + s64 pos = os_file_get_pos(wav->file); + if (pos < wav->pcm_start) return false; + + u64 comp_size = wav->bits_per_sample/8; + u64 frame_size = wav->channels*comp_size; + + u64 out_comp_size = get_audio_bit_width_byte_size(format.bit_width); + u64 out_frame_size = format.channels*out_comp_size; + + // We first convert bit width, and then channels & sample rate + u64 convert_frame_size = out_comp_size * wav->channels; + + u64 end = wav->pcm_start + frame_size*wav->number_of_frames; + + u64 remaining_frames = (end-pos)/frame_size; + + f32 ratio = (f32)wav->sample_rate / (f32)format.sample_rate; + + u64 frames_to_output = min(round(remaining_frames/ratio), number_of_frames); + u64 frames_to_read = frames_to_output; + + if (wav->sample_rate != format.sample_rate) { + + frames_to_read = (u64)round(ratio*frames_to_output); + } + + u64 required_size + = number_of_frames*max(format.channels,wav->channels)*4; + + // #Cleanup #Memory refactor intermediate buffers + thread_local local_persist void *raw_buffer = 0; + thread_local local_persist u64 raw_buffer_size = 0; + thread_local local_persist void *convert_buffer = 0; + thread_local local_persist u64 convert_buffer_size = 0; + if (!raw_buffer || required_size > raw_buffer_size) { + if (raw_buffer) dealloc(get_heap_allocator(), raw_buffer); + + u64 new_size = get_next_power_of_two(required_size); + + raw_buffer = alloc(get_heap_allocator(), new_size); + memset(raw_buffer, 0, new_size); + raw_buffer_size = new_size; + } + if (!convert_buffer || required_size > convert_buffer_size) { + if (convert_buffer) dealloc(get_heap_allocator(), convert_buffer); + + u64 new_size = get_next_power_of_two(required_size); + + convert_buffer = alloc(get_heap_allocator(), new_size); + memset(convert_buffer, 0, new_size); + convert_buffer_size = new_size; + } + + u64 frames_read; + bool ok = os_file_read(wav->file, raw_buffer, frames_to_read*frame_size, &frames_read); + if (!ok) return 0; + if (frames_read != frames_to_read*frame_size) { + os_file_set_pos(wav->file, pos); + return 0; + } + + bool raw_is_float = wav->format == 0x0003; + bool raw_is_f32 = raw_is_float && wav->valid_bits_per_sample == 32; + bool raw_is_int = wav->format == 0x0001; + bool raw_is_s16 = raw_is_int && wav->valid_bits_per_sample == 16; + + switch (format.bit_width) { + case AUDIO_BITS_32: { + if (raw_is_f32) { + memcpy(convert_buffer, raw_buffer, frames_to_read*frame_size); + } else { + assert(raw_is_int); + + // Convert any integer to f32 + for (u64 f = 0; f < frames_to_read; f++) { + void *raw_frame = (u8*)raw_buffer + f * frame_size; + void *dst_frame = (u8*)convert_buffer + f * convert_frame_size; + for (u64 c = 0; c < wav->channels; c++) { + void *raw_comp = (u8*)raw_frame + c * comp_size; + void *dst_comp = (u8*)dst_frame + c * out_comp_size; + + s64 i = 0; + if (wav->valid_bits_per_sample == 32) { + i = *(s32*)raw_comp; + } else if (wav->valid_bits_per_sample == 16) { + i = *(s16*)raw_comp; + } else if (wav->valid_bits_per_sample == 24) { + u8 bytes[3]; + memcpy(bytes, raw_comp, 3); + + u16 test = 0x1; + bool is_little_endian = *((u8*)&test) == 0x1; + + if (is_little_endian) { + i = (s32)((bytes[0] << 0) | (bytes[1] << 8) | (bytes[2] << 16)); + } else { + i = (s32)((bytes[2] << 0) | (bytes[1] << 8) | (bytes[0] << 16)); + } + if (i & 0x800000) { + i |= ~0xFFFFFF; // Sign extend the 24-bit value + } + } + s64 max = (1LL << (wav->valid_bits_per_sample - 1LL)) - 1LL; + + *(f32*)dst_comp = (f64)i / (f64)max; + } + } + } + break; + } + case AUDIO_BITS_16: { + if (raw_is_s16) { + memcpy(convert_buffer, raw_buffer, frames_to_read*frame_size); + } else if (raw_is_f32) { + // Convert f32 to s16 + for (u64 f = 0; f < frames_to_read; f++) { + void *raw_frame = (u8*)raw_buffer + f*frame_size; + void *dst_frame = (u8*)convert_buffer + f*convert_frame_size; + for (u64 c = 0; c < wav->channels; c++) { + void *raw_comp = (u8*)raw_frame + c*comp_size; + void *dst_comp = (u8*)dst_frame + c*out_comp_size; + + *(s16*)dst_comp = (s16)(*((f32*)raw_comp) * 32768.0f); + } + } + } else { + assert(raw_is_int); + // Convert any integer to s16 + for (u64 f = 0; f < frames_to_read; f++) { + void *raw_frame = (u8*)raw_buffer + f*frame_size; + void *dst_frame = (u8*)convert_buffer + f*convert_frame_size; + for (u64 c = 0; c < wav->channels; c++) { + void *raw_comp = (u8*)raw_frame + c*comp_size; + void *dst_comp = (u8*)dst_frame + c*out_comp_size; + + s64 i = 0; + if (wav->valid_bits_per_sample == 32) { + i = *(s32*)raw_comp; + } else if (wav->valid_bits_per_sample == 16) { + i = *(s16*)raw_comp; + } else if (wav->valid_bits_per_sample == 24) { + u8 bytes[3]; + memcpy(bytes, raw_comp, 3); + + u16 test = 0x1; + bool is_little_endian = *((u8*)&test) == 0x1; + + if (is_little_endian) { + i = (s32)((bytes[0] << 0) | (bytes[1] << 8) | (bytes[2] << 16)); + } else { + i = (s32)((bytes[2] << 0) | (bytes[1] << 8) | (bytes[0] << 16)); + } + if (i & 0x800000) { + i |= ~0xFFFFFF; // Sign extend the 24-bit value + } + } + s64 max = (1LL << (wav->valid_bits_per_sample - 1LL)) - 1LL; + + f32 factor = (f64)i * (1.0/(f64)max); + + *(s16*)dst_comp = (s16)(factor*32768.0f); + } + } + } + break; + } + } + + int converted = convert_frames( + frames, + format, + convert_buffer, + (Audio_Format){ format.bit_width, wav->channels, wav->sample_rate}, + frames_to_read + ); + assert(converted == frames_to_output); + + return frames_to_output; +} +bool +wav_load_file(string path, void **frames, Audio_Format format, u64 *number_of_frames, + Allocator allocator) { + Wav_Stream wav; + if (!wav_open_file(path, &wav, format.sample_rate, number_of_frames)) return false; + + u64 comp_size = get_audio_bit_width_byte_size(format.bit_width); + u64 frame_size = comp_size*format.channels; + + *frames = alloc(allocator, *number_of_frames*frame_size); + + u64 read = wav_read_frames(&wav, format, *frames, *number_of_frames); + if (read != *number_of_frames) { + if (read > 0) { + assert(*frames, "What's goin on here"); + } + return false; + } + + + return true; +} + + +int +audio_source_get_frames(Audio_Source *src, u64 first_frame_index, + u64 number_of_frames, void *output_buffer); + + +bool +audio_open_source_stream_format(Audio_Source *src, string path, Audio_Format format, + Allocator allocator) { *src = ZERO(Audio_Source); + mutex_init(&src->mutex_for_destroy); + src->allocator = allocator; src->kind = AUDIO_SOURCE_FILE_STREAM; - string data; - bool read_ok = os_read_entire_file(path, &data, allocator); - src->compressed_data = data; + mutex_acquire_or_wait(&audio_init_mutex); + src->format = format; + mutex_release(&audio_init_mutex); - third_party_allocator = allocator; + File file = os_file_open(path, O_READ); + if (file == OS_INVALID_FILE) return false; + string header = talloc_string(4); + memset(header.data, 0, 4); + u64 read; + bool ok = os_file_read(file, header.data, 4, &read); + if (read != 4) return false; + os_file_close(file); - if (!read_ok) { - third_party_allocator = ZERO(Allocator); - return false; - } - - if (check_wav_header(data)) { - drwav_bool32 init_ok = drwav_init_memory(&src->wav, data.data, data.count, null); - if (!init_ok) { - third_party_allocator = ZERO(Allocator); - return false; - } + if (check_wav_header(header)) { src->decoder = AUDIO_DECODER_WAV; - - src->format.channels = src->wav.fmt.channels; - src->format.sample_rate = src->wav.fmt.sampleRate; - src->number_of_frames = src->wav.totalPCMFrameCount; - } else if (check_ogg_header(data)) { - int err; - src->ogg = stb_vorbis_open_memory(data.data, data.count, &err, null); - if (!src->ogg) { - third_party_allocator = ZERO(Allocator); - return false; - } + ok = wav_open_file(path, &src->wav, src->format.sample_rate, &src->number_of_frames); + if (!ok) return false; + } else if (check_ogg_header(header)) { src->decoder = AUDIO_DECODER_OGG; - stb_vorbis_info info = stb_vorbis_get_info(src->ogg); - src->format.channels = info.channels; - src->format.sample_rate = info.sample_rate; - src->number_of_frames = stb_vorbis_stream_length_in_samples(src->ogg); - } else { - log_error("Error in init_audio_source_file_stream(): Unrecognized audio format in file '%s'. We currently support WAV and OGG (Vorbis).", path); + ok = os_read_entire_file(path, &src->ogg_raw, src->allocator); + if (!ok) return false; + + third_party_allocator = src->allocator; + int err = 0; + src->ogg = stb_vorbis_open_memory(src->ogg_raw.data, src->ogg_raw.count, &err, 0); third_party_allocator = ZERO(Allocator); + + if (err != 0 || src->ogg == 0) return false; + + third_party_allocator = src->allocator; + src->number_of_frames = stb_vorbis_stream_length_in_samples(src->ogg); + third_party_allocator = ZERO(Allocator); + } else { + log_error("Error in audio_open_source_stream(): Unrecognized audio format in file '%s'. We currently support WAV and OGG (Vorbis).", path); return false; } - src->format.bit_width = bit_width; + return true; +} +bool +audio_open_source_stream(Audio_Source *src, string path, Allocator allocator) { + return audio_open_source_stream_format(src, path, audio_output_format, allocator); +} +bool +audio_open_source_load_format(Audio_Source *src, string path, Audio_Format format, + Allocator allocator) { + *src = ZERO(Audio_Source); - third_party_allocator = ZERO(Allocator); + mutex_init(&src->mutex_for_destroy); + + src->allocator = allocator; + src->kind = AUDIO_SOURCE_MEMORY; + mutex_acquire_or_wait(&audio_init_mutex); + src->format = format; + mutex_release(&audio_init_mutex); + + File file = os_file_open(path, O_READ); + if (file == OS_INVALID_FILE) return false; + string header = talloc_string(4); + memset(header.data, 0, 4); + u64 read; + bool ok = os_file_read(file, header.data, 4, &read); + if (read != 4) return false; + os_file_close(file); + u64 frame_size + = src->format.channels*get_audio_bit_width_byte_size(src->format.bit_width); + + if (check_wav_header(header)) { + src->decoder = AUDIO_DECODER_WAV; + ok = wav_load_file(path, &src->pcm_frames, src->format, &src->number_of_frames, src->allocator); + if (!ok) return false; + } else if (check_ogg_header(header)) { + src->decoder = AUDIO_DECODER_OGG; + + ok = os_read_entire_file(path, &src->ogg_raw, src->allocator); + if (!ok) return false; + + third_party_allocator = src->allocator; + int err = 0; + src->ogg = stb_vorbis_open_memory(src->ogg_raw.data, src->ogg_raw.count, &err, 0); + third_party_allocator = ZERO(Allocator); + + if (err != 0 || src->ogg == 0) return false; + + third_party_allocator = src->allocator; + src->number_of_frames = stb_vorbis_stream_length_in_samples(src->ogg); + third_party_allocator = ZERO(Allocator); + + src->pcm_frames = alloc(src->allocator, src->number_of_frames*frame_size); + int retrieved = audio_source_get_frames( + src, + 0, + src->number_of_frames, + src->pcm_frames + ); + + + third_party_allocator = src->allocator; + stb_vorbis_close(src->ogg); + third_party_allocator = ZERO(Allocator); + + if (retrieved != src->number_of_frames) { + dealloc(src->allocator, src->pcm_frames); + return false; + } + } else { + log_error("Error in audio_open_source_load(): Unrecognized audio format in file '%s'. We currently support WAV and OGG (Vorbis).", path); + return false; + } return true; } - -bool -audio_source_init_file_decode(Audio_Source *src, string path, Audio_Format_Bits bit_width, - Allocator allocator) { - if (!audio_source_init_file_stream(src, path, bit_width, allocator)) return false; - src->kind = AUDIO_SOURCE_MEMORY; - - u64 comp_size = get_audio_bit_width_byte_size(src->format.bit_width); - u64 total_size = src->number_of_frames * src->format.channels * comp_size; - src->pcm_frames = alloc(allocator, total_size); - - int num_retrieved = _audio_file_stream_sample_frames(src, 0, src->number_of_frames, src->pcm_frames); - assert(num_retrieved == src->number_of_frames, "decoder failed failed"); - - return true; +bool +audio_open_source_load(Audio_Source *src, string path, Allocator allocator) { + return audio_open_source_load_format(src, path, audio_output_format, allocator); } void audio_source_destroy(Audio_Source *src) { + + mutex_acquire_or_wait(&src->mutex_for_destroy); + switch (src->kind) { case AUDIO_SOURCE_FILE_STREAM: { - if (src->pcm_frames) dealloc(src->allocator, src->pcm_frames); + third_party_allocator = src->allocator; + switch (src->decoder) { + case AUDIO_DECODER_WAV: { + wav_close(&src->wav); + break; + } + case AUDIO_DECODER_OGG: { + stb_vorbis_close(src->ogg); + dealloc_string(src->allocator, src->ogg_raw); + break; + } + } + third_party_allocator = ZERO(Allocator); break; } case AUDIO_SOURCE_MEMORY: { @@ -152,84 +690,113 @@ audio_source_destroy(Audio_Source *src) { } } - - third_party_allocator = src->allocator; - switch (src->decoder) { - case AUDIO_DECODER_WAV: { - drwav_uninit(&src->wav); - break; - } - case AUDIO_DECODER_OGG: { - stb_vorbis_close(src->ogg); - break; - } - } - third_party_allocator = ZERO(Allocator); - - dealloc_string(src->allocator, src->compressed_data); + mutex_release(&src->mutex_for_destroy); } int -_audio_file_stream_sample_frames(Audio_Source *src, u64 first_frame_index, - u64 number_of_frames, void *output_buffer) { - third_party_allocator = src->allocator; +audio_source_get_frames(Audio_Source *src, u64 first_frame_index, + u64 number_of_frames, void *output_buffer) { int retrieved = 0; switch (src->decoder) { - case AUDIO_DECODER_WAV: - bool seek_ok = drwav_seek_to_pcm_frame(&src->wav, first_frame_index); + case AUDIO_DECODER_WAV: { + bool seek_ok = wav_set_frame_pos(&src->wav, src->format.sample_rate, first_frame_index); assert(seek_ok); - switch(src->format.bit_width) { - case AUDIO_BITS_32: { - retrieved = drwav_read_pcm_frames_f32( - &src->wav, - number_of_frames, - (f32*)output_buffer - ); - break; - } - case AUDIO_BITS_16: { - retrieved = drwav_read_pcm_frames_s16( - &src->wav, - number_of_frames, - (s16*)output_buffer - ); - break; - } - default: panic("Invalid bits value"); + + retrieved = wav_read_frames( + &src->wav, + src->format, + output_buffer, + number_of_frames + ); + } break; // case AUDIO_DECODER_WAV: - case AUDIO_DECODER_OGG: - seek_ok = stb_vorbis_seek(src->ogg, first_frame_index); + case AUDIO_DECODER_OGG: { + f32 ratio = (f32)src->ogg->sample_rate/(f32)src->format.sample_rate; + + third_party_allocator = src->allocator; + bool seek_ok = stb_vorbis_seek(src->ogg, round(first_frame_index*ratio)); + third_party_allocator = ZERO(Allocator); assert(seek_ok); + + // We need to convert sample rate & channels for vorbis + + u64 comp_size = get_audio_bit_width_byte_size(src->format.bit_width); + u64 frame_size = src->format.channels*comp_size; + + u64 convert_frame_size = max(src->format.channels, src->ogg->channels)*comp_size; + u64 required_size = convert_frame_size*number_of_frames; + + // #Cleanup #Memory refactor intermediate buffers + thread_local local_persist void *convert_buffer = 0; + thread_local local_persist u64 convert_buffer_size = 0; + if (!convert_buffer || required_size > convert_buffer_size) { + if (convert_buffer) dealloc(get_heap_allocator(), convert_buffer); + + u64 new_size = get_next_power_of_two(required_size); + + convert_buffer = alloc(get_heap_allocator(), new_size); + memset(convert_buffer, 0, new_size); + convert_buffer_size = new_size; + } + + u64 number_of_frames_to_sample = number_of_frames; + void *target_buffer = output_buffer; + + if (src->ogg->sample_rate != src->format.sample_rate + || src->ogg->channels != src->format.channels) { + number_of_frames_to_sample = (u64)round(ratio * (f32)number_of_frames); + target_buffer = convert_buffer; + } + + third_party_allocator = src->allocator; + + // Unfortunately, vorbis only converts o a different channel count if that differing + // channel count is 2. So we might as well just deal with it ourselves. + switch(src->format.bit_width) { case AUDIO_BITS_32: { retrieved = stb_vorbis_get_samples_float_interleaved( src->ogg, - src->format.channels, - (f32*)output_buffer, - number_of_frames * src->format.channels + src->ogg->channels, + (f32*)target_buffer, + number_of_frames_to_sample * src->ogg->channels ); break; } case AUDIO_BITS_16: { retrieved = stb_vorbis_get_samples_short_interleaved( src->ogg, - src->format.channels, - (s16*)output_buffer, - number_of_frames * src->format.channels + src->ogg->channels, + (s16*)target_buffer, + number_of_frames_to_sample * src->ogg->channels ); break; } default: panic("Invalid bits value"); + } + third_party_allocator = ZERO(Allocator); + + if (src->ogg->sample_rate != src->format.sample_rate + || src->ogg->channels != src->format.channels) { + + retrieved = convert_frames( + output_buffer, + src->format, + convert_buffer, + (Audio_Format){src->format.bit_width, src->ogg->channels, src->ogg->sample_rate}, + number_of_frames_to_sample + ); + } + } break; // case AUDIO_DECODER_OGG: default: panic("Invalid decoder value"); } - third_party_allocator = ZERO(Allocator); return retrieved; } u64 // New frame index -audio_source_sample_frames(Audio_Source *src, u64 first_frame_index, u64 number_of_frames, +audio_source_sample_next_frames(Audio_Source *src, u64 first_frame_index, u64 number_of_frames, void *output_buffer, bool looping) { u64 comp_size = get_audio_bit_width_byte_size(src->format.bit_width); @@ -250,7 +817,7 @@ audio_source_sample_frames(Audio_Source *src, u64 first_frame_index, u64 number_ switch (src->kind) { case AUDIO_SOURCE_FILE_STREAM: { - num_retrieved = _audio_file_stream_sample_frames( + num_retrieved = audio_source_get_frames( src, first_frame_index, number_of_frames, @@ -263,7 +830,7 @@ audio_source_sample_frames(Audio_Source *src, u64 first_frame_index, u64 number_ if (num_retrieved < number_of_frames) { void *dst_remain = ((u8*)output_buffer) + num_retrieved*frame_size; if (looping) { - num_retrieved = _audio_file_stream_sample_frames( + num_retrieved = audio_source_get_frames( src, 0, number_of_frames-num_retrieved, @@ -377,8 +944,7 @@ convert_one_component(void *dst, Audio_Format_Bits dst_bits, } } -// Assume dst buffer is large enough -// in-place conversion is OK + void resample_frames(void *dst, Audio_Format dst_format, void *src, Audio_Format src_format, u64 src_frame_count) { @@ -392,8 +958,7 @@ resample_frames(void *dst, Audio_Format dst_format, u64 src_comp_size = get_audio_bit_width_byte_size(src_format.bit_width); u64 src_frame_size = src_comp_size * src_format.channels; - // Reverse in case dst == src (so we can do in-place conversion) - for (s64 dst_frame_index = dst_frame_count - 1; dst_frame_index >= 1; dst_frame_index--) { + for (s64 dst_frame_index = dst_frame_count - 1; dst_frame_index >= 0; dst_frame_index--) { f32 src_frame_index_f = dst_frame_index * src_ratio; u64 src_frame_index_1 = (u64)src_frame_index_f; u64 src_frame_index_2 = src_frame_index_1 + 1; @@ -406,12 +971,6 @@ resample_frames(void *dst, Audio_Format dst_format, void *dst_frame = (u8*)dst + dst_frame_index * dst_frame_size; for (int c = 0; c < src_format.channels; c++) { - union { - s16 s16_sample; - f32 f32_sample; - u8 data[4]; - } sample_dst; - void *src_comp_1 = (u8*)src_frame_1 + c * src_comp_size; void *src_comp_2 = (u8*)src_frame_2 + c * src_comp_size; void *dst_comp = (u8*)dst_frame + c * dst_comp_size; @@ -419,21 +978,21 @@ resample_frames(void *dst, Audio_Format dst_format, if (src_format.bit_width == AUDIO_BITS_32) { float sample_1 = *((f32*)src_comp_1); float sample_2 = *((f32*)src_comp_2); - sample_dst.f32_sample = sample_1 + lerp_factor * (sample_2 - sample_1); + f32 s = sample_1 + lerp_factor * (sample_2 - sample_1); + memcpy(dst_comp, &s, sizeof(f32)); } else if (src_format.bit_width == AUDIO_BITS_16) { s16 sample_1 = *((s16*)src_comp_1); s16 sample_2 = *((s16*)src_comp_2); - sample_dst.s16_sample = (s16)((f32)sample_1 + lerp_factor * ((f32)sample_2 - (f32)sample_1)); + s16 s = (s16)(sample_1 + lerp_factor * (sample_2 - sample_1)); + memcpy(dst_comp, &s, sizeof(s16)); } else { panic("Unhandled bit width"); } - - memcpy(dst_comp, sample_dst.data, dst_comp_size); } } - - // Correct padding if we downscaled (since we coverted in reverse) - if (src == dst && dst_format.sample_rate < src_format.sample_rate) { + + // Correct padding on downsampling since we downsample backwards + if (dst_format.sample_rate < src_format.sample_rate) { void *dst_after_pad = (u8*)dst + (src_frame_count - dst_frame_count) * dst_frame_size; u64 padding = (u64)dst_after_pad - (u64)dst; memcpy( @@ -443,11 +1002,10 @@ resample_frames(void *dst, Audio_Format dst_format, ); memset((u8*)dst+dst_frame_count * dst_frame_size, 0, padding); } - } // Assumes dst buffer is large enough -void +int // Returns outputted number of frames convert_frames(void *dst, Audio_Format dst_format, void *src, Audio_Format src_format, u64 src_frame_count) { @@ -456,31 +1014,40 @@ convert_frames(void *dst, Audio_Format dst_format, u64 src_comp_size = get_audio_bit_width_byte_size(src_format.bit_width); u64 src_frame_size = src_comp_size * src_format.channels; + u64 output_frame_count = src_frame_count; + if (dst_format.sample_rate != src_format.sample_rate) { f32 ratio = (f32)src_format.sample_rate/(f32)dst_format.sample_rate; - src_frame_count = (u64)round((f32)src_frame_count*ratio); + output_frame_count = (u64)round((f32)src_frame_count/ratio); } if (bytes_match(&dst_format, &src_format, sizeof(Audio_Format))) { memcpy(dst, src, src_frame_count*src_frame_size); - return; + return src_frame_count; } - u64 output_frame_count = src_frame_count; - + bool need_sample_conversion + = dst_format.channels != src_format.channels + || dst_format.bit_width != src_format.bit_width; // #Speed #Simd - if (dst_format.channels != src_format.channels || dst_format.bit_width != src_format.bit_width) { + if (need_sample_conversion) { for (u64 src_frame_index = 0; src_frame_index < src_frame_count; src_frame_index++) { void *src_frame = ((u8*)src) + src_frame_index*src_frame_size; void *dst_frame = ((u8*)dst) + src_frame_index*dst_frame_size; - // For getting average src sample union { s16 s16_sample; f32 f32_sample; u8 data[4]; } avg; - if (src_format.channels != dst_format.channels) { + + // We only need the average if downscaling or src format is more than 1 when + // upscaling. If source format is mono, the upscaling is just copying + // that one channel to all the channels in dst. + + if (src_format.channels != dst_format.channels && + src_format.channels > dst_format.channels || + src_format.channels > 1) { // This is where we get the average src sample f32 sum = 0; for (int c = 0; c < src_format.channels; c++) { @@ -497,12 +1064,11 @@ convert_frames(void *dst, Audio_Format dst_format, if (dst_format.bit_width == AUDIO_BITS_32) { avg.f32_sample = sum/(f32)src_format.channels; } else if (dst_format.bit_width == AUDIO_BITS_16) { - avg.s16_sample = (s16)round(sum/(f32)src_format.channels); + avg.s16_sample = (s16)round(sum/(f32)src_format.channels) * 32768.0; } else panic("Unhandled bit width"); } if (src_format.channels > dst_format.channels) { - // #Limitation #Audioquality // Here we are down-scaling the channel count. // So what we do is we get the average sample for all channels in src and then @@ -515,13 +1081,18 @@ convert_frames(void *dst, Audio_Format dst_format, memcpy(dst_comp, avg.data, dst_comp_size); } + } else if (src_format.channels == 1) { + for (int c = 0; c < dst_format.channels; c++) { + void *dst_comp = (u8*)dst_frame + c * dst_comp_size; + convert_one_component(dst_comp, dst_format.bit_width, + src_frame, src_format.bit_width); + } } else if (dst_format.channels > src_format.channels) { - // Here, we are upscaling to a higher channel count. - // I'm not sure what the best way to do this is, but for now I will try to just - // get the average in src and set that to the extra channels in dst. - // This is obviously fine for mono -> stereo but might be a problem for surround. - // Again, I'm not sure if surround will ever be on our list of worries. + // Here, we are upscaling to a higher channel count from a src channel count + // more than 1. + // I'm not sure what the best way to do this is, but for now I will try to + // just get the average in src and set that to the extra channels in dst. for (int c = 0; c < dst_format.channels; c++) { void *dst_comp = (u8*)dst_frame + c * dst_comp_size; @@ -547,82 +1118,472 @@ convert_frames(void *dst, Audio_Format dst_format, if (dst_format.sample_rate != src_format.sample_rate) { resample_frames( dst, - (Audio_Format){dst_format.bit_width, dst_format.channels, dst_format.sample_rate}, - dst, - (Audio_Format){dst_format.bit_width, dst_format.channels, src_format.sample_rate}, + dst_format, + need_sample_conversion ? dst : src, + need_sample_conversion ? + (Audio_Format){dst_format.bit_width, dst_format.channels, src_format.sample_rate} + : src_format, src_frame_count ); - + } + + return output_frame_count; } +#define AUDIO_STATE_FADE_TIME_MS 40 +typedef enum Audio_Player_State { + AUDIO_PLAYER_STATE_PAUSED, + AUDIO_PLAYER_STATE_PLAYING +} Audio_Player_State; +typedef struct Audio_Player { + // You shouldn't set these directly. + // Configure players with the player_xxxxx procedures + Audio_Source source; + bool has_source; + bool allocated; + bool marked_for_release; // We release on audio thread + Audio_Player_State state; + u64 frame_index; + bool looping; + u64 fade_frames; + u64 fade_frames_total; + bool release_when_done; + // I think we only need to sync when audio thread samples the source, which should be + // very quick and low contention, hence a spinlock. + Spinlock sample_lock; +} Audio_Player; +#define AUDIO_PLAYERS_PER_BLOCK 128 +typedef struct Audio_Player_Block { + Audio_Player players[AUDIO_PLAYERS_PER_BLOCK]; // Players need to be persistent in memory + + struct Audio_Player_Block *next; +} Audio_Player_Block; +Audio_Player_Block audio_player_block = {0}; -// #Temporary this is jsut for testing -Audio_Source *current_source = 0; -u64 current_index = 0; +Audio_Player * +audio_player_get_one() { + + Audio_Player_Block *block = &audio_player_block; + Audio_Player_Block *last = 0; + + while (block) { + + for (u64 i = 0; i < AUDIO_PLAYERS_PER_BLOCK; i++) { + if (!block->players[i].allocated) { + + memset(&block->players[i], 0, sizeof(block->players[i])); + block->players[i].allocated = true; + + return &block->players[i]; + } + } + + last = block; + block = block->next; + } + + // No free player found, make another block + // #Volatile can't assign to last->next before this is zero initialized + Audio_Player_Block *new_block = alloc(get_heap_allocator(), sizeof(Audio_Player_Block)); + +#if !DO_ZERO_INITIALIATION + memset(new_block, 0, sizeof(*new_block)); +#endif + + last->next = new_block; + + new_block->players[0].allocated = true; + return &new_block->players[0]; +} + +void +audio_player_release(Audio_Player *p) { + p->marked_for_release = true; +} +void +audio_player_set_state(Audio_Player *p, Audio_Player_State state) { + + if (p->state == state) return; + + spinlock_acquire_or_wait(&p->sample_lock); + assert(p->frame_index <= p->source.number_of_frames); + p->state = state; + + float64 full_duration + = (float64)p->source.number_of_frames/(float64)p->source.format.sample_rate; + float64 progression = (float64)p->frame_index / (float64)p->source.number_of_frames; + float64 remaining = (1.0-progression)*full_duration; + + float64 fade_seconds = min(AUDIO_STATE_FADE_TIME_MS/1000.0, remaining); + + float64 fade_factor = fade_seconds/full_duration; + + p->fade_frames = (u64)round(fade_factor*(float64)p->source.number_of_frames); + p->fade_frames_total = p->fade_frames; + + spinlock_release(&p->sample_lock); +} +void +audio_player_set_time_stamp(Audio_Player *p, float64 time_in_seconds) { + spinlock_acquire_or_wait(&p->sample_lock); + assert(p->frame_index <= p->source.number_of_frames); + + float64 full_duration + = (float64)p->source.number_of_frames/(float64)p->source.format.sample_rate; + time_in_seconds = clamp(time_in_seconds, 0, full_duration); + float64 progression = time_in_seconds/full_duration; + + p->frame_index = (u64)round((float64)p->source.number_of_frames*progression); + + spinlock_release(&p->sample_lock); +} +void // 0 - 1 +audio_player_set_progression_factor(Audio_Player *p, float64 factor) { + spinlock_acquire_or_wait(&p->sample_lock); + assert(p->frame_index <= p->source.number_of_frames); + + p->frame_index = (u64)round((float64)p->source.number_of_frames*factor); + + spinlock_release(&p->sample_lock); +} +float64 // seconds +audio_player_get_time_stamp(Audio_Player *p) { + spinlock_acquire_or_wait(&p->sample_lock); + assert(p->frame_index <= p->source.number_of_frames); + + float64 full_duration + = (float64)p->source.number_of_frames/(float64)p->source.format.sample_rate; + float64 progression = (float64)p->frame_index / (float64)p->source.number_of_frames; + + spinlock_release(&p->sample_lock); + + return progression*full_duration; +} +float64 +audio_player_get_current_progression_factor(Audio_Player *p) { + if (!p->has_source) return 0; + spinlock_acquire_or_wait(&p->sample_lock); + assert(p->frame_index <= p->source.number_of_frames); + + float64 progression = (float64)p->frame_index / (float64)p->source.number_of_frames; + + spinlock_release(&p->sample_lock); + + return progression; +} +void +audio_player_set_source(Audio_Player *p, Audio_Source src, bool retain_progression_factor) { + + float64 last_progression = audio_player_get_current_progression_factor(p); + + spinlock_acquire_or_wait(&p->sample_lock); + + p->source = src; + p->has_source = true; + + if (retain_progression_factor) { + p->frame_index = (u64)round((float64)p->source.number_of_frames*last_progression); + } else { + p->frame_index = 0; + } + + spinlock_release(&p->sample_lock); +} +void +audio_player_clear_source(Audio_Player *p) { + spinlock_acquire_or_wait(&p->sample_lock); + assert(p->frame_index <= p->source.number_of_frames); + + p->has_source = false; + p->state = AUDIO_PLAYER_STATE_PAUSED; + p->source = ZERO(Audio_Source); + + spinlock_release(&p->sample_lock); +} +void +audio_player_set_looping(Audio_Player *p, bool looping) { + spinlock_acquire_or_wait(&p->sample_lock); + + if (p->has_source && looping && !p->looping && p->frame_index == p->source.number_of_frames) { + p->frame_index = 0; + } + + p->looping = looping; + + spinlock_release(&p->sample_lock); +} + +Hash_Table just_audio_clips; +bool just_audio_clips_initted = false; + +void +play_one_audio_clip_source(Audio_Source source) { + Audio_Player *p = audio_player_get_one(); + audio_player_set_source(p, source, false); + audio_player_set_state(p, AUDIO_PLAYER_STATE_PLAYING); + p->release_when_done = true; +} +void +play_one_audio_clip(string path) { + if (!just_audio_clips_initted) { + just_audio_clips_initted = true; + just_audio_clips = make_hash_table(string, Audio_Source, get_heap_allocator()); + } + + Audio_Source *src_ptr = hash_table_find(&just_audio_clips, path); + if (src_ptr) { + play_one_audio_clip_source(*src_ptr); + } else { + Audio_Source new_src; + bool ok = audio_open_source_load(&new_src, path, get_heap_allocator()); + if (!ok) { + log_error("Could not load audio to play from %s", path); + return; + } + hash_table_add(&just_audio_clips, path, new_src); + play_one_audio_clip_source(new_src); + } +} + +void +audio_apply_fade_in(void *frames, u64 number_of_frames, Audio_Format format, + float64 fade_from, float64 fade_to) { + u64 comp_size = get_audio_bit_width_byte_size(format.bit_width); + u64 frame_size = comp_size * format.channels; + + for (u64 f = 0; f < number_of_frames; f++) { + f32 frame_t = (f32)f/(f32)number_of_frames; + f32 log_scale = log10(1.0 + 9.0 * frame_t) / log10(10.0); + for (u64 c = 0; c < format.channels; c++) { + void *p = ((u8*)frames)+frame_size*f+c*comp_size; + switch (format.bit_width) { + case AUDIO_BITS_32: { + f32 s = (*(f32*)p); + *(f32*)p = smerpf(s * fade_from, s * fade_to, log_scale); + break; + } + case AUDIO_BITS_16: { + s16 s = (*(s16*)p); + *(s16*)p = smerpi((f32)s * fade_from, (f32)s * fade_to, log_scale); + break; + } + } + } + } +} +void +audio_apply_fade_out(void *frames, u64 number_of_frames, Audio_Format format, + float64 fade_from, float64 fade_to) { + u64 comp_size = get_audio_bit_width_byte_size(format.bit_width); + u64 frame_size = comp_size * format.channels; + + for (u64 f = 0; f < number_of_frames; f++) { + f64 frame_t = 1.0-(f32)f/(f32)number_of_frames; + f64 log_scale = log10(1.0 + 9.0 * frame_t) / log10(10.0); + for (u64 c = 0; c < format.channels; c++) { + void *p = ((u8*)frames)+frame_size*f+c*comp_size; + switch (format.bit_width) { + case AUDIO_BITS_32: { + f32 s = (*(f32*)p); + *(f32*)p = smerpf(s * fade_from, s * fade_to, log_scale); + break; + } + case AUDIO_BITS_16: { + s16 s = (*(s16*)p); + *(s16*)p = smerpi((f64)s * fade_from, (f64)s * fade_to, log_scale); + break; + } + } + } + } +} // This is supposed to be called by OS layer audio thread whenever it wants more audio samples -void do_program_audio_sample(u64 number_of_output_frames, Audio_Format out_format, +void +do_program_audio_sample(u64 number_of_output_frames, Audio_Format out_format, void *output) { - u64 out_comp_size = get_audio_bit_width_byte_size(out_format.bit_width); + + reset_temporary_storage(); + + u64 out_comp_size = get_audio_bit_width_byte_size(out_format.bit_width); u64 out_frame_size = out_comp_size * out_format.channels; - u64 output_size = number_of_output_frames * out_frame_size; + u64 output_size = number_of_output_frames * out_frame_size; memset(output, 0, output_size); - if (current_source) { - - bool need_convert = !bytes_match(&out_format, ¤t_source->format, sizeof(Audio_Format)); + + Audio_Player_Block *block = &audio_player_block; + + // #Cleanup #Memory refactor intermediate buffers + thread_local local_persist void *mix_buffer = 0; + thread_local local_persist u64 mix_buffer_size; + thread_local local_persist void *convert_buffer = 0; + thread_local local_persist u64 convert_buffer_size; + + memset(mix_buffer, 0, mix_buffer_size); + + + while (block) { - u64 in_comp_size = get_audio_bit_width_byte_size(current_source->format.bit_width); - u64 in_frame_size = in_comp_size * current_source->format.channels; - u64 input_size = number_of_output_frames * in_frame_size; - - void *target_buffer = output; - u64 number_of_sample_frames = number_of_output_frames; - - thread_local local_persist void *convert_buffer = 0; - thread_local local_persist u64 convert_buffer_size; - if (need_convert) { - if (current_source->format.sample_rate != out_format.sample_rate) { - f32 src_ratio - = (f32)current_source->format.sample_rate / (f32)out_format.sample_rate; - number_of_sample_frames = round(number_of_output_frames * src_ratio); - input_size = number_of_sample_frames * in_frame_size; + for (u64 i = 0; i < AUDIO_PLAYERS_PER_BLOCK; i++) { + Audio_Player *p = &block->players[i]; + if (p->release_when_done && (p->frame_index >= p->source.number_of_frames + || !p->has_source)) { + p->allocated = false; } + if (!p->allocated) { + continue; + } + + if (p->marked_for_release) { + p->marked_for_release = false; + p->allocated = false; + continue; + } + + if (p->state != AUDIO_PLAYER_STATE_PLAYING) { + if (p->fade_frames == 0) continue; + } + + spinlock_acquire_or_wait(&p->sample_lock); + + Audio_Source src = p->source; + + mutex_acquire_or_wait(&src.mutex_for_destroy); + + bool need_convert = !bytes_match( + &out_format, + &src.format, + sizeof(Audio_Format) + ); + + u64 in_comp_size + = get_audio_bit_width_byte_size(src.format.bit_width); + + u64 in_frame_size = in_comp_size * src.format.channels; + u64 input_size = number_of_output_frames * in_frame_size; u64 biggest_size = max(input_size, output_size); - - if (!convert_buffer || convert_buffer_size < biggest_size) { - // #Speed - if (convert_buffer) dealloc(get_heap_allocator(), convert_buffer); - convert_buffer = alloc(get_heap_allocator(), biggest_size); - convert_buffer_size = biggest_size; + + if (!mix_buffer || mix_buffer_size < biggest_size) { + u64 new_size = get_next_power_of_two(biggest_size); + if (mix_buffer) dealloc(get_heap_allocator(), mix_buffer); + mix_buffer = alloc(get_heap_allocator(), new_size); + mix_buffer_size = new_size; + memset(mix_buffer, 0, new_size); } - target_buffer = convert_buffer; - memset(convert_buffer, 0, biggest_size); - } - - current_index = audio_source_sample_frames( - current_source, - current_index, - number_of_sample_frames, - target_buffer, - true - ); - if (need_convert) { - convert_frames( - output, - out_format, - convert_buffer, - current_source->format, - number_of_output_frames + void *target_buffer = mix_buffer; + u64 number_of_sample_frames = number_of_output_frames; + + if (need_convert) { + if (src.format.sample_rate != out_format.sample_rate) { + f32 src_ratio + = (f32)src.format.sample_rate + / (f32)out_format.sample_rate; + + number_of_sample_frames = round(number_of_output_frames * src_ratio); + input_size = number_of_sample_frames * in_frame_size; + } + + u64 biggest_size = max(input_size, output_size); + + if (!convert_buffer || convert_buffer_size < biggest_size) { + u64 new_size = get_next_power_of_two(biggest_size); + if (convert_buffer) dealloc(get_heap_allocator(), convert_buffer); + convert_buffer = alloc(get_heap_allocator(), new_size); + convert_buffer_size = new_size; + memset(convert_buffer, 0, new_size); + } + target_buffer = convert_buffer; + + } + + p->frame_index = audio_source_sample_next_frames( + &src, + p->frame_index, + number_of_sample_frames, + target_buffer, + p->looping ); + + if (p->fade_frames > 0) { + u64 frames_to_fade = min(p->fade_frames, number_of_sample_frames); + + u64 frames_faded_so_far = (p->fade_frames_total-p->fade_frames); + + switch (p->state) { + case AUDIO_PLAYER_STATE_PLAYING: { + // We need to fade in + float64 fade_from + = (f64)frames_faded_so_far / (f64)p->fade_frames_total; + + float64 fade_to + = (f64)(frames_faded_so_far + frames_to_fade) / (f64)p->fade_frames_total; + audio_apply_fade_in( + target_buffer, + frames_to_fade, + p->source.format, + fade_from, + fade_to + ); + break; + } + case AUDIO_PLAYER_STATE_PAUSED: { + // We need to fade out + // #Bug #Incomplete + // I can't get this to fade out without noise. + // I tried dithering but that didn't help. + float64 fade_from + = 1.0 - (f64)frames_faded_so_far / (f64)p->fade_frames_total; + + float64 fade_to + = 1.0 - (f64)(frames_faded_so_far + frames_to_fade) / (f64)p->fade_frames_total; + audio_apply_fade_out( + target_buffer, + frames_to_fade, + p->source.format, + fade_from, + fade_to + ); + break; + } + } + + p->fade_frames -= frames_to_fade; + + if (frames_to_fade < number_of_sample_frames) { + memset( + (u8*)target_buffer+frames_to_fade, + 0, + number_of_sample_frames-frames_to_fade + ); + } + } + + spinlock_release(&p->sample_lock); + + if (need_convert) { + int converted = convert_frames( + mix_buffer, + out_format, + convert_buffer, + src.format, + number_of_sample_frames + ); + assert(converted == number_of_output_frames); + } + + mix_frames(output, mix_buffer, number_of_output_frames, out_format); + + mutex_release(&src.mutex_for_destroy); } + + block = block->next; } } \ No newline at end of file diff --git a/oogabooga/base.c b/oogabooga/base.c index cc8425a..64da225 100644 --- a/oogabooga/base.c +++ b/oogabooga/base.c @@ -5,12 +5,15 @@ #define forward_global extern +#define alignas _Alignas + #define null 0 void printf(const char* fmt, ...); +void dump_stack_trace(); #define ASSERT_STR_HELPER(x) #x #define ASSERT_STR(x) ASSERT_STR_HELPER(x) -#define assert_line(line, cond, ...) {if(!(cond)) { printf("Assertion failed in file " __FILE__ " on line " ASSERT_STR(line) "\nFailed Condition: " #cond ". Message: " __VA_ARGS__); crash(); }} +#define assert_line(line, cond, ...) {if(!(cond)) { printf("Assertion failed in file " __FILE__ " on line " ASSERT_STR(line) "\nFailed Condition: " #cond ". Message: " __VA_ARGS__); dump_stack_trace(); crash(); }} #define assert(cond, ...) {assert_line(__LINE__, cond, __VA_ARGS__)} #define DEFER(start, end) for(int _i_ = ((start), 0); _i_ == 0; _i_ += 1, (end)) @@ -93,6 +96,7 @@ forward_global thread_local Allocator temp; void* memset(void* dest, int value, size_t amount); void* alloc(Allocator allocator, u64 size) { + assert(size > 0, "You requested an allocation of zero bytes. I'm not sure what you want with that."); void *p = allocator.proc(size, 0, ALLOCATOR_ALLOCATE, allocator.data); #if DO_ZERO_INITIALIZATION memset(p, 0, size); @@ -100,9 +104,11 @@ void* alloc(Allocator allocator, u64 size) { return p; } void* alloc_uninitialized(Allocator allocator, u64 size) { + assert(size > 0, "You requested an allocation of zero bytes. I'm not sure what you want with that."); return allocator.proc(size, 0, ALLOCATOR_ALLOCATE, allocator.data); } void dealloc(Allocator allocator, void *p) { + assert(p != 0, "You tried to deallocate a pointer at adress 0. That doesn't make sense!"); allocator.proc(0, p, ALLOCATOR_DEALLOCATE, allocator.data); } diff --git a/oogabooga/concurrency.c b/oogabooga/concurrency.c index 94c2ed7..8b5d760 100644 --- a/oogabooga/concurrency.c +++ b/oogabooga/concurrency.c @@ -46,13 +46,16 @@ void spinlock_init(Spinlock *l) { void spinlock_acquire_or_wait(Spinlock* l) { while (true) { bool expected = false; + MEMORY_BARRIER; if (compare_and_swap_bool(&l->locked, true, expected)) { MEMORY_BARRIER; return; } while (l->locked) { // spinny boi + MEMORY_BARRIER; } + MEMORY_BARRIER; } } // Returns true on aquired, false if timeout seconds reached @@ -60,6 +63,7 @@ bool spinlock_acquire_or_wait_timeout(Spinlock* l, f64 timeout_seconds) { f64 start = os_get_current_time_in_seconds(); while (true) { bool expected = false; + MEMORY_BARRIER; if (compare_and_swap_bool(&l->locked, true, expected)) { MEMORY_BARRIER; return true; @@ -67,15 +71,17 @@ bool spinlock_acquire_or_wait_timeout(Spinlock* l, f64 timeout_seconds) { while (l->locked) { // spinny boi if ((os_get_current_time_in_seconds()-start) >= timeout_seconds) return false; + MEMORY_BARRIER; } } return true; } void spinlock_release(Spinlock* l) { bool expected = true; - bool success = compare_and_swap_bool(&l->locked, false, expected); - assert(success, "This thread should have acquired the spinlock but compare_and_swap failed"); MEMORY_BARRIER; + bool success = compare_and_swap_bool(&l->locked, false, expected); + MEMORY_BARRIER; + assert(success, "This thread should have acquired the spinlock but compare_and_swap failed"); } diff --git a/oogabooga/drawing.c b/oogabooga/drawing.c index bfce8ff..0939845 100644 --- a/oogabooga/drawing.c +++ b/oogabooga/drawing.c @@ -48,6 +48,12 @@ Usage: API: + // !! IMPORTANT + // The Draw_Quad* returned from draw procedures is a temporary pointer and may be + // invalid after the next draw_xxxx call. This is because quads are stored in a + // resizing buffer (because that gave us a non-trivial performance boost). + // So the purpose of returning them is to customize the quad right after the draw proc. + Draw_Quad *draw_quad_projected(Draw_Quad quad, Matrix4 world_to_clip); Draw_Quad *draw_quad(Draw_Quad quad); Draw_Quad *draw_quad_xform(Draw_Quad quad, Matrix4 xform); @@ -62,7 +68,7 @@ Usage: */ -// We use radix sort so the exact bit count is of important +// We use radix sort so the exact bit count is of importance #define MAX_Z_BITS 21 #define MAX_Z ((1 << MAX_Z_BITS)/2) #define Z_STACK_MAX 4096 diff --git a/oogabooga/examples/audio_test.c b/oogabooga/examples/audio_test.c index f584b2a..8566706 100644 --- a/oogabooga/examples/audio_test.c +++ b/oogabooga/examples/audio_test.c @@ -1,3 +1,8 @@ +#define FONT_HEIGHT 48 + +Gfx_Font *font; + +bool button(string label, Vector2 pos, Vector2 size, bool enabled); @@ -12,30 +17,130 @@ int entry(int argc, char **argv) { Allocator heap = get_heap_allocator(); + font = load_font_from_disk(STR("C:/windows/fonts/arial.ttf"), heap); + assert(font, "Failed loading arial.ttf"); + Audio_Source bruh, song; - bool bruh_ok = audio_source_init_file_decode(&bruh, STR("oogabooga/examples/bruh.wav"), AUDIO_BITS_32, heap); + + bool bruh_ok = audio_open_source_load(&bruh, STR("oogabooga/examples/bruh.wav"), heap); assert(bruh_ok, "Could not load bruh.wav"); - bool song_ok = audio_source_init_file_stream(&song, STR("oogabooga/examples/song.ogg"), AUDIO_BITS_16, heap); - assert(bruh_ok, "Could not load song.ogg"); + bool song_ok = audio_open_source_stream(&song, STR("oogabooga/examples/song.ogg"), heap); + assert(song_ok, "Could not load song.ogg"); - // #Temporary This is not actually how it will work, I'm just testing audio source output. - current_source = &song; + // By default, audio sources will be converted to the same format as the output buffer. + // However, if you want it to be a specific format (or something smaller than the + // output format), then you can call: + // audio_open_source_load_format() + // audio_open_source_stream_format() + + Audio_Player *clip_player = audio_player_get_one(); + Audio_Player *song_player = audio_player_get_one(); + + // If you ever need it, you can give the player back to be reused somewhere else. + // audio_player_release(clip_player); + // But this is probably only something you would need to care about if you had a very + // complicated audio system. + + audio_player_set_source(clip_player, bruh, false); + audio_player_set_source(song_player, song, false); + + audio_player_set_state(clip_player, AUDIO_PLAYER_STATE_PAUSED); + audio_player_set_state(song_player, AUDIO_PLAYER_STATE_PLAYING); + + audio_player_set_looping(clip_player, true); + //play_one_audio_clip(STR("oogabooga/examples/block.wav")); while (!window.should_close) { reset_temporary_storage(); - - float64 now = os_get_current_time_in_seconds(); - Matrix4 rect_xform = m4_scalar(1.0); - rect_xform = m4_rotate_z(rect_xform, (f32)now); - rect_xform = m4_translate(rect_xform, v3(-.25f, -.25f, 0)); - draw_rect_xform(rect_xform, v2(.5f, .5f), COLOR_GREEN); - draw_rect(v2(sin(now), -.8), v2(.5, .25), COLOR_RED); + draw_frame.projection = m4_make_orthographic_projection(window.pixel_width * -0.5, window.pixel_width * 0.5, window.pixel_height * -0.5, window.pixel_height * 0.5, -1, 10); + + if (is_key_just_pressed(MOUSE_BUTTON_RIGHT)) { + // Easy mode (when you don't care and just want to play a clip) + play_one_audio_clip(STR("oogabooga/examples/block.wav")); + } + + + + Vector4 rect; + rect.x = -window.width/2+40; + rect.y = window.height/2-FONT_HEIGHT-40; + rect.z = FONT_HEIGHT*5; + rect.w = FONT_HEIGHT*1.5; + + bool clip_playing = clip_player->state == AUDIO_PLAYER_STATE_PLAYING; + bool song_playing = song_player->state == AUDIO_PLAYER_STATE_PLAYING; + + if (button(STR("Song"), rect.xy, rect.zw, song_playing)) { + if (song_playing) audio_player_set_state(song_player, AUDIO_PLAYER_STATE_PAUSED); + else audio_player_set_state(song_player, AUDIO_PLAYER_STATE_PLAYING); + } + + rect.y -= FONT_HEIGHT*1.8; + if (button(STR("Loop Bruh"), rect.xy, rect.zw, clip_playing)) { + if (clip_playing) audio_player_set_state(clip_player, AUDIO_PLAYER_STATE_PAUSED); + else audio_player_set_state(clip_player, AUDIO_PLAYER_STATE_PLAYING); + } + rect.y -= FONT_HEIGHT*1.8; + if (button(STR("One Bruh"), rect.xy, rect.zw, false)) { + play_one_audio_clip(STR("oogabooga/examples/bruh.wav")); + } + rect.y -= FONT_HEIGHT*3; + if (button(STR("Reset song"), rect.xy, rect.zw, false)) { + audio_player_set_progression_factor(song_player, 0); + } + + rect.y -= FONT_HEIGHT*3; + draw_text(font, STR("Right-click for thing"), FONT_HEIGHT, v2_sub(rect.xy, v2(2, -2)), v2(1, 1), COLOR_BLACK); + draw_text(font, STR("Right-click for thing"), FONT_HEIGHT, rect.xy, v2(1, 1), COLOR_WHITE); os_update(); gfx_update(); } return 0; +} + +bool button(string label, Vector2 pos, Vector2 size, bool enabled) { + + Vector4 color = v4(.25, .25, .25, 1); + + float L = pos.x; + float R = L + size.x; + float B = pos.y; + float T = B + size.y; + + float mx = input_frame.mouse_x - window.width/2; + float my = input_frame.mouse_y - window.height/2; + + bool pressed = false; + + if (mx >= L && mx < R && my >= B && my < T) { + color = v4(.15, .15, .15, 1); + if (is_key_down(MOUSE_BUTTON_LEFT)) { + color = v4(.05, .05, .05, 1); + } + + pressed = is_key_just_released(MOUSE_BUTTON_LEFT); + } + + if (enabled) { + color = v4_add(color, v4(.1, .1, .1, 0)); + } + + draw_rect(pos, size, color); + + Gfx_Text_Metrics m = measure_text(font, label, FONT_HEIGHT, v2(1, 1)); + + Vector2 bottom_left = v2_sub(pos, m.functional_pos_min); + bottom_left.x += size.x/2; + bottom_left.x -= m.functional_size.x/2; + + bottom_left.y += size.y/2; + bottom_left.y -= m.functional_size.y/2; + + draw_text(font, label, FONT_HEIGHT, bottom_left, v2(1, 1), COLOR_WHITE); + + return pressed; } \ No newline at end of file diff --git a/oogabooga/examples/block.wav b/oogabooga/examples/block.wav new file mode 100644 index 0000000000000000000000000000000000000000..1e8ef74ac1406fab52f8862d8f48e821ef381a1b GIT binary patch literal 44980 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 G|Naj&87H6s literal 0 HcmV?d00001 diff --git a/oogabooga/examples/bruh.wav b/oogabooga/examples/bruh.wav index e940a6e9d6252239d0b465a141a633cb0e2888ef..466ed907fdff8ceb1ad5f9faebb2c86ec6b05b44 100644 GIT binary patch literal 941196 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