From e4976e7c593f70e03736ada5186407270bc026f2 Mon Sep 17 00:00:00 2001 From: Charlie Malmqvist Date: Sat, 3 Aug 2024 13:11:07 +0200 Subject: [PATCH] Audio Playback speed, bugfixes & api improvements --- oogabooga/audio.c | 215 ++++++++++++++++++++------------ oogabooga/examples/audio_test.c | 58 ++++++--- 2 files changed, 176 insertions(+), 97 deletions(-) diff --git a/oogabooga/audio.c b/oogabooga/audio.c index 155491a..2576b63 100644 --- a/oogabooga/audio.c +++ b/oogabooga/audio.c @@ -11,24 +11,33 @@ void play_one_audio_clip_source(Audio_Source source); void play_one_audio_clip(string path); - void play_one_audio_clip_source_at_position(Audio_Source source, Vector3 pos); - void play_one_audio_clip_at_position(string path, Vector3 pos); + void play_one_audio_clip_source_config(Audio_Source source, Audio_Playback_Config config); + void play_one_audio_clip_config(string path, Audio_Playback_Config config); Playing audio (with players): Audio_Player * audio_player_get_one(); void audio_player_release(Audio_Player *p); - + + BEWARE! Most of these operations are potentially slow because they may need to synchronize with audio thread. + 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); bool audio_player_at_source_end(Audio_Player *p); 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_set_source(Audio_Player *p, Audio_Source src); void audio_player_clear_source(Audio_Player *p); void audio_player_set_looping(Audio_Player *p, bool looping); + Configuring playback: + + player->config.enable_spacialization = true/false; + player->config.position_ndc = v3(...); + player->config.volume = ...; // (1.0 by default) + player->config.playback_speed = ...; // (1.0 by default) + */ @@ -161,13 +170,6 @@ 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); @@ -214,8 +216,7 @@ wav_open_file(string path, Wav_Stream *wav, u64 sample_rate, u64 *number_of_fram sub_chunk_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") + // (yes, some wave 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"))) { @@ -284,6 +285,9 @@ wav_open_file(string path, Wav_Stream *wav, u64 sample_rate, u64 *number_of_fram } } + // This is a bit cheeky because format 0x0001 is actually 16-bit pcm, and 0x0003 16-bit float specifically + // so I'm just kinda mudding the wave spec here. I'm not sure why past Charlie did that. Present Charlie is + // superior and would never ever make a brain mistake like that. if (wav->format == 0xFFFE) { if (is_equal_wav_guid(&wav->sub_format, &WAV_SUBTYPE_PCM)) { wav->format = 0x0001; @@ -306,8 +310,8 @@ wav_open_file(string path, Wav_Stream *wav, u64 sample_rate, u64 *number_of_fram } 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); + f64 ratio = (f64)sample_rate/(f64)wav->sample_rate; + *number_of_frames = (u64)round((f64)wav->number_of_frames*ratio); } // Set the file position to the beginning of the PCM data @@ -326,8 +330,8 @@ wav_close(Wav_Stream *wav) { 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); + f64 ratio = (f64)wav->sample_rate/(f64)output_sample_rate; + frame_index = (u64)round(ratio*(f64)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); @@ -351,7 +355,7 @@ wav_read_frames(Wav_Stream *wav, Audio_Format format, void *frames, u64 remaining_frames = (end-pos)/frame_size; - f32 ratio = (f32)wav->sample_rate / (f32)format.sample_rate; + f64 ratio = (f64)wav->sample_rate / (f64)format.sample_rate; u64 frames_to_output = min(round(remaining_frames/ratio), number_of_frames); u64 frames_to_read = frames_to_output; @@ -508,7 +512,7 @@ wav_read_frames(Wav_Stream *wav, Audio_Format format, void *frames, format, convert_buffer, (Audio_Format){ format.bit_width, wav->channels, wav->sample_rate}, - frames_to_read + frames_to_output ); assert(converted == frames_to_output); @@ -727,7 +731,7 @@ audio_source_get_frames(Audio_Source *src, u64 first_frame_index, } break; // case AUDIO_DECODER_WAV: case AUDIO_DECODER_OGG: { - f32 ratio = (f32)src->ogg->sample_rate/(f32)src->format.sample_rate; + f64 ratio = (f64)src->ogg->sample_rate/(f64)src->format.sample_rate; third_party_allocator = src->allocator; bool seek_ok = stb_vorbis_seek(src->ogg, round(first_frame_index*ratio)); @@ -800,7 +804,7 @@ audio_source_get_frames(Audio_Source *src, u64 first_frame_index, src->format, convert_buffer, (Audio_Format){src->format.bit_width, src->ogg->channels, src->ogg->sample_rate}, - number_of_frames_to_sample + number_of_frames ); } @@ -827,9 +831,6 @@ audio_source_sample_next_frames(Audio_Source *src, u64 first_frame_index, u64 nu u64 new_index = first_frame_index; - // #Checkout - // first_frame_index = max(first_frame_index, 3); - int num_retrieved; switch (src->kind) { case AUDIO_SOURCE_FILE_STREAM: { @@ -903,11 +904,6 @@ mix_frames(void *dst, void *src, u64 frame_count, Audio_Format format) { u64 frame_size = comp_size * format.channels; u64 output_size = frame_count * frame_size; - // #Speed #Simd #Incomplete - // Quality: - // - Dithering - // - Clipping. Dynamic Range Compression? - for (u64 frame = 0; frame < frame_count; frame++) { for (u64 c = 0; c < format.channels; c++) { @@ -970,7 +966,7 @@ resample_frames(void *dst, Audio_Format dst_format, assert(dst_format.channels == src_format.channels, "Channel count must be the same for sample rate conversion"); assert(dst_format.bit_width == src_format.bit_width, "Types must be the same for sample rate conversion"); - f32 src_ratio = (f32)src_format.sample_rate / (f32)dst_format.sample_rate; + f64 src_ratio = (f64)src_format.sample_rate / (f64)dst_format.sample_rate; u64 dst_frame_count = (u64)round(src_frame_count / src_ratio); u64 dst_comp_size = get_audio_bit_width_byte_size(dst_format.bit_width); u64 dst_frame_size = dst_comp_size * dst_format.channels; @@ -1010,34 +1006,24 @@ resample_frames(void *dst, Audio_Format dst_format, } } - // 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( - dst, - dst_after_pad, - dst_frame_count * dst_frame_size - ); - memset((u8*)dst+dst_frame_count * dst_frame_size, 0, padding); - } + } // Assumes dst buffer is large enough int // Returns outputted number of frames convert_frames(void *dst, Audio_Format dst_format, - void *src, Audio_Format src_format, u64 src_frame_count) { + void *src, Audio_Format src_format, u64 output_frame_count) { u64 dst_comp_size = get_audio_bit_width_byte_size(dst_format.bit_width); u64 dst_frame_size = dst_comp_size * dst_format.channels; 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; + u64 src_frame_count = output_frame_count; if (dst_format.sample_rate != src_format.sample_rate) { - f32 ratio = (f32)src_format.sample_rate/(f32)dst_format.sample_rate; - output_frame_count = (u64)round((f32)src_frame_count/ratio); + f64 ratio = (f64)src_format.sample_rate/(f64)dst_format.sample_rate; + src_frame_count = (u64)round((f64)output_frame_count*ratio); } if (bytes_match(&dst_format, &src_format, sizeof(Audio_Format))) { @@ -1158,9 +1144,17 @@ typedef enum Audio_Player_State { AUDIO_PLAYER_STATE_PAUSED, AUDIO_PLAYER_STATE_PLAYING } Audio_Player_State; + +typedef struct Audio_Playback_Config { + Vector3 position_ndc; + bool enable_spacialization; + float32 volume; + float32 playback_speed; +} Audio_Playback_Config; + typedef struct Audio_Player { // You shouldn't set these directly. - // Configure players with the player_xxxxx procedures + // Set playback state with the player_xxxxx procedures Audio_Source source; bool has_source; bool allocated; @@ -1172,13 +1166,17 @@ typedef struct Audio_Player { 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. + // fairly quick and low contention, hence a spinlock. Spinlock sample_lock; - // These can be set safely - Vector3 position; // ndc space -1 to 1 - bool disable_spacialization; - float32 volume; + // #Cleanup + DEPRECATED(Vector3 position, "Use player->config.position_ndc instead"); // ndc space -1 to 1 + DEPRECATED(bool disable_spacialization, "Use player->config.enable_spacialization instead"); + DEPRECATED(float32 volume, "Use player->config.volume instead"); + DEPRECATED(float32 playback_speed, "Use player->config.playback_speed instead"); + + // This is safe to set whenever + Audio_Playback_Config config; } Audio_Player; #define AUDIO_PLAYERS_PER_BLOCK 128 @@ -1208,7 +1206,8 @@ audio_player_get_one() { memset(&block->players[i], 0, sizeof(block->players[i])); block->players[i].allocated = true; - block->players[i].volume = 1.0; + block->players[i].config.volume = 1.0; + block->players[i].config.playback_speed = 1.0; return &block->players[i]; } @@ -1229,7 +1228,8 @@ audio_player_get_one() { last->next = new_block; new_block->players[0].allocated = true; - new_block->players[0].volume = 1.0; + new_block->players[0].config.volume = 1.0; + block->players[0].config.playback_speed = 1.0; return &new_block->players[0]; } @@ -1322,7 +1322,7 @@ audio_player_get_current_progression_factor(Audio_Player *p) { return progression; } void -audio_player_set_source(Audio_Player *p, Audio_Source src, bool retain_progression_factor) { +audio_player_set_source(Audio_Player *p, Audio_Source src) { float64 last_progression = audio_player_get_current_progression_factor(p); @@ -1331,11 +1331,7 @@ audio_player_set_source(Audio_Player *p, Audio_Source src, bool retain_progressi 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; - } + p->frame_index = 0; spinlock_release(&p->sample_lock); } @@ -1373,20 +1369,33 @@ bool just_audio_clips_initted = false; #endif // NOT OOGABOOGA_LINK_EXTERNAL_INSTANCE void -play_one_audio_clip_source_at_position(Audio_Source source, Vector3 pos) { +DEPRECATED(play_one_audio_clip_source_at_position(Audio_Source source, Vector3 pos), "Use play_one_audio_clip_source_with_config() instead") { Audio_Player *p = audio_player_get_one(); - audio_player_set_source(p, source, false); + audio_player_set_source(p, source); audio_player_set_state(p, AUDIO_PLAYER_STATE_PLAYING); - p->position = pos; + p->config.position_ndc = pos; + p->config.enable_spacialization = true; + p->release_when_done = true; +} + +void +play_one_audio_clip_source_with_config(Audio_Source source, Audio_Playback_Config config) { + Audio_Player *p = audio_player_get_one(); + audio_player_set_source(p, source); + audio_player_set_state(p, AUDIO_PLAYER_STATE_PLAYING); + p->config = config; p->release_when_done = true; } void inline play_one_audio_clip_source(Audio_Source source) { - play_one_audio_clip_source_at_position(source, v3(0, 0, 0)); + Audio_Playback_Config config = {0}; + config.volume = 1.0; + config.playback_speed = 1.0; + play_one_audio_clip_source_with_config(source, config); } void -play_one_audio_clip_at_position(string path, Vector3 pos) { +DEPRECATED(play_one_audio_clip_at_position(string path, Vector3 pos), "Use play_one_audio_clip_with_config() instead") { if (!just_audio_clips_initted) { just_audio_clips_initted = true; just_audio_clips = make_hash_table(string, Audio_Source, get_heap_allocator()); @@ -1397,7 +1406,7 @@ play_one_audio_clip_at_position(string path, Vector3 pos) { play_one_audio_clip_source_at_position(*src_ptr, pos); } else { Audio_Source new_src; - bool ok = audio_open_source_load(&new_src, path, get_heap_allocator()); + bool ok = audio_open_source_stream(&new_src, path, get_heap_allocator()); if (!ok) { log_error("Could not load audio to play from %s", path); return; @@ -1407,9 +1416,33 @@ play_one_audio_clip_at_position(string path, Vector3 pos) { } } +void +play_one_audio_clip_with_config(string path, Audio_Playback_Config config) { + 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_with_config(*src_ptr, config); + } else { + Audio_Source new_src; + bool ok = audio_open_source_stream(&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_with_config(new_src, config); + } +} void inline play_one_audio_clip(string path) { - play_one_audio_clip_at_position(path, v3(0, 0, 0)); + Audio_Playback_Config config = {0}; + config.volume = 1.0; + config.playback_speed = 1.0; + play_one_audio_clip_with_config(path, config); } void @@ -1670,6 +1703,9 @@ do_program_audio_sample(u64 number_of_output_frames, Audio_Format out_format, if (p->fade_frames == 0) continue; } + // #Incomplete Reverse playback ? + if (p->config.playback_speed <= 0.0) continue; + if (p->frame_index >= p->source.number_of_frames && !p->looping) continue; spinlock_acquire_or_wait(&p->sample_lock); @@ -1677,21 +1713,24 @@ do_program_audio_sample(u64 number_of_output_frames, Audio_Format out_format, Audio_Source src = p->source; mutex_acquire_or_wait(&src.mutex_for_destroy); + + Audio_Format sample_format = src.format; + sample_format.sample_rate = sample_format.sample_rate*p->config.playback_speed; bool need_convert = !bytes_match( &out_format, - &src.format, + &sample_format, sizeof(Audio_Format) ); u64 in_comp_size - = get_audio_bit_width_byte_size(src.format.bit_width); + = get_audio_bit_width_byte_size(sample_format.bit_width); - u64 in_frame_size = in_comp_size * src.format.channels; + u64 in_frame_size = in_comp_size * sample_format.channels; u64 input_size = number_of_output_frames * in_frame_size; + // #Copypaste #Cleanup u64 biggest_size = max(input_size, output_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); @@ -1704,17 +1743,26 @@ do_program_audio_sample(u64 number_of_output_frames, Audio_Format out_format, 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; + if (sample_format.sample_rate != out_format.sample_rate) { + f64 src_ratio + = (f64)sample_format.sample_rate + / (f64)out_format.sample_rate; number_of_sample_frames = round(number_of_output_frames * src_ratio); input_size = number_of_sample_frames * in_frame_size; + + // #Copypaste #Cleanup we need to potentially grow the mix buffer again after we change input_size + u64 biggest_size = max(input_size, output_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); + } } 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); @@ -1738,12 +1786,14 @@ do_program_audio_sample(u64 number_of_output_frames, Audio_Format out_format, // For looping players, this means we have a slight offset between the players that start // playing at the exact same time. I'm not sure how else to deal with phase cancellation // in looping players. + // #Incomplete player->is_muted_for_phase_cancellation ? p->frame_index = src.number_of_frames; continue; } growing_array_add((void**)&started_this_frame, &src.uid); } + u64 last_frame_index = p->frame_index; p->frame_index = audio_source_sample_next_frames( &src, p->frame_index, @@ -1751,6 +1801,9 @@ do_program_audio_sample(u64 number_of_output_frames, Audio_Format out_format, target_buffer, p->looping ); + if (p->frame_index > last_frame_index && (p->looping || p->frame_index != src.number_of_frames)) { + assert(p->frame_index - last_frame_index == number_of_sample_frames); + } if (p->fade_frames > 0) { u64 frames_to_fade = min(p->fade_frames, number_of_sample_frames); @@ -1813,17 +1866,17 @@ do_program_audio_sample(u64 number_of_output_frames, Audio_Format out_format, mix_buffer, out_format, convert_buffer, - src.format, - number_of_sample_frames + sample_format, + number_of_output_frames ); assert(converted == number_of_output_frames); } - if (!p->disable_spacialization) { - apply_audio_spacialization(mix_buffer, out_format, number_of_output_frames, p->position); + if (p->config.enable_spacialization) { + apply_audio_spacialization(mix_buffer, out_format, number_of_output_frames, p->config.position_ndc); } - if (p->volume != 0.0) { - apply_audio_volume(mix_buffer, out_format, number_of_output_frames, p->volume); + if (p->config.volume != 0.0) { + apply_audio_volume(mix_buffer, out_format, number_of_output_frames, p->config.volume); } mix_frames(output, mix_buffer, number_of_output_frames, out_format); diff --git a/oogabooga/examples/audio_test.c b/oogabooga/examples/audio_test.c index 2d96973..1eed554 100644 --- a/oogabooga/examples/audio_test.c +++ b/oogabooga/examples/audio_test.c @@ -4,7 +4,13 @@ Gfx_Font *font; bool button(string label, Vector2 pos, Vector2 size, bool enabled); +/* + NOTE: + + In most cases you will probably just want to call play_one_clip(). + Using Audio Players is for when you need more control over the playback. +*/ int entry(int argc, char **argv) { @@ -25,7 +31,7 @@ int entry(int argc, char **argv) { 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_open_source_stream(&song, STR("oogabooga/examples/song.ogg"), heap); + bool song_ok = audio_open_source_stream_format(&song, STR("oogabooga/examples/song.ogg"), heap); assert(song_ok, "Could not load song.ogg"); // By default, audio sources will be converted to the same format as the output buffer. @@ -42,11 +48,11 @@ int entry(int argc, char **argv) { // 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_source(clip_player, bruh); + audio_player_set_source(song_player, song); audio_player_set_state(clip_player, AUDIO_PLAYER_STATE_PAUSED); - audio_player_set_state(song_player, AUDIO_PLAYER_STATE_PLAYING); + audio_player_set_state(song_player, AUDIO_PLAYER_STATE_PAUSED); audio_player_set_looping(clip_player, true); //play_one_audio_clip(STR("oogabooga/examples/block.wav")); @@ -57,13 +63,8 @@ int entry(int argc, char **argv) { 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)) { - float mx = input_frame.mouse_x; - float my = input_frame.mouse_y; // Easy mode (when you don't care and just want to play a clip) - Vector3 p = v3(mx/(f32)window.width*2.0-1, my/(f32)window.height*2.0-1, 0); - log("%f, %f", p.x, p.y); - play_one_audio_clip_at_position(STR("oogabooga/examples/block.wav"), p); - // Or just play_one_audio_clip if you don't care about spacialization + play_one_audio_clip(STR("oogabooga/examples/block.wav")); } @@ -89,7 +90,12 @@ int entry(int argc, char **argv) { } 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")); + Audio_Playback_Config config = {0}; + config.volume = 1.0; + config.playback_speed = get_random_float32_in_range(0.8, 1.2); + config.enable_spacialization = true; + config.position_ndc = v3(get_random_float32_in_range(-1, 1), get_random_float32_in_range(-1, 1), 0); + play_one_audio_clip_with_config(STR("oogabooga/examples/bruh.wav"), config); } rect.y -= FONT_HEIGHT*3; if (button(STR("Reset song"), rect.xy, rect.zw, false)) { @@ -99,16 +105,30 @@ int entry(int argc, char **argv) { rect.y = window.height/2-FONT_HEIGHT-40; rect.x += rect.z + FONT_HEIGHT; if (button(STR("Song vol up"), rect.xy, rect.zw, false)) { - song_player->volume += 0.05; + song_player->config.volume += 0.05; } rect.y -= FONT_HEIGHT*1.8; if (button(STR("Song vol down"), rect.xy, rect.zw, false)) { - song_player->volume -= 0.05; + song_player->config.volume -= 0.05; } - song_player->volume = clamp(song_player->volume, 0, 20); + song_player->config.volume = clamp(song_player->config.volume, 0, 20); rect.x += rect.z + FONT_HEIGHT; - draw_text(font, tprint("Song volume: %d%%", (s64)round(song_player->volume*100)), FONT_HEIGHT, v2_sub(rect.xy, v2(2, -2)), v2(1, 1), COLOR_BLACK); - draw_text(font, tprint("Song volume: %d%%", (s64)round(song_player->volume*100)), FONT_HEIGHT, rect.xy, v2(1, 1), COLOR_WHITE); + draw_text(font, tprint("Song volume: %d%%", (s64)round(song_player->config.volume*100)), FONT_HEIGHT, v2_sub(rect.xy, v2(2, -2)), v2(1, 1), COLOR_BLACK); + draw_text(font, tprint("Song volume: %d%%", (s64)round(song_player->config.volume*100)), FONT_HEIGHT, rect.xy, v2(1, 1), COLOR_WHITE); + rect.x -= rect.z + FONT_HEIGHT; + + rect.y -= FONT_HEIGHT*5; + if (button(STR("Speed up"), rect.xy, rect.zw, false)) { + song_player->config.playback_speed += 0.05; + } + rect.y -= FONT_HEIGHT*1.8; + if (button(STR("Speed down"), rect.xy, rect.zw, false)) { + song_player->config.playback_speed -= 0.05; + } + song_player->config.playback_speed = clamp(song_player->config.playback_speed, 0, 20); + rect.x += rect.z + FONT_HEIGHT; + draw_text(font, tprint("Speed: %d%%", (s64)round(song_player->config.playback_speed*100)), FONT_HEIGHT, v2_sub(rect.xy, v2(2, -2)), v2(1, 1), COLOR_BLACK); + draw_text(font, tprint("Speed: %d%%", (s64)round(song_player->config.playback_speed*100)), FONT_HEIGHT, rect.xy, v2(1, 1), COLOR_WHITE); rect.y -= FONT_HEIGHT*3; @@ -119,6 +139,12 @@ int entry(int argc, char **argv) { os_update(); gfx_update(); } + + // Don't actually do this on exit!! + // Your OS will clean up everything when program exits, so this is only slowing down the time it takes for the program to exit. + // This is just for testing purposes. + audio_source_destroy(&bruh); + audio_source_destroy(&song); return 0; }