From 3c10cfddb67c6ea50c21ab87fd38afe0abce7c21 Mon Sep 17 00:00:00 2001 From: rexim Date: Tue, 11 Jun 2024 00:30:52 +0700 Subject: [PATCH] Outline the Music samples polling hack Incorporate some ideas from #104 Co-authored-by: Weypare --- src/arena.h | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/plug.c | 184 ++++++++++++++++++++++++++------ 2 files changed, 453 insertions(+), 33 deletions(-) create mode 100644 src/arena.h diff --git a/src/arena.h b/src/arena.h new file mode 100644 index 0000000..410bae6 --- /dev/null +++ b/src/arena.h @@ -0,0 +1,302 @@ +// Copyright 2022 Alexey Kutepov + +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: + +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#ifndef ARENA_H_ +#define ARENA_H_ + +#include +#include +#include + +#ifndef ARENA_NOSTDIO +#include +#include +#endif // ARENA_NOSTDIO + +#ifndef ARENA_ASSERT +#include +#define ARENA_ASSERT assert +#endif + +#define ARENA_BACKEND_LIBC_MALLOC 0 +#define ARENA_BACKEND_LINUX_MMAP 1 +#define ARENA_BACKEND_WIN32_VIRTUALALLOC 2 +#define ARENA_BACKEND_WASM_HEAPBASE 3 + +#ifndef ARENA_BACKEND +#define ARENA_BACKEND ARENA_BACKEND_LIBC_MALLOC +#endif // ARENA_BACKEND + +typedef struct Region Region; + +struct Region { + Region *next; + size_t count; + size_t capacity; + uintptr_t data[]; +}; + +typedef struct { + Region *begin, *end; +} Arena; + +#define REGION_DEFAULT_CAPACITY (8*1024) + +Region *new_region(size_t capacity); +void free_region(Region *r); + +// TODO: snapshot/rewind capability for the arena +// - Snapshot should be combination of a->end and a->end->count. +// - Rewinding should be restoring a->end and a->end->count from the snapshot and +// setting count-s of all the Region-s after the remembered a->end to 0. +void *arena_alloc(Arena *a, size_t size_bytes); +void *arena_realloc(Arena *a, void *oldptr, size_t oldsz, size_t newsz); +char *arena_strdup(Arena *a, const char *cstr); +void *arena_memdup(Arena *a, void *data, size_t size); +#ifndef ARENA_NOSTDIO +char *arena_sprintf(Arena *a, const char *format, ...); +#endif // ARENA_NOSTDIO + +void arena_reset(Arena *a); +void arena_free(Arena *a); + +#define ARENA_DA_INIT_CAP 256 + +#ifdef __cplusplus + #define cast_ptr(ptr) (decltype(ptr)) +#else + #define cast_ptr(...) +#endif + +#define arena_da_append(a, da, item) \ + do { \ + if ((da)->count >= (da)->capacity) { \ + size_t new_capacity = (da)->capacity == 0 ? ARENA_DA_INIT_CAP : (da)->capacity*2; \ + (da)->items = cast_ptr((da)->items)arena_realloc( \ + (a), (da)->items, \ + (da)->capacity*sizeof(*(da)->items), \ + new_capacity*sizeof(*(da)->items)); \ + (da)->capacity = new_capacity; \ + } \ + \ + (da)->items[(da)->count++] = (item); \ + } while (0) + +#endif // ARENA_H_ + +#ifdef ARENA_IMPLEMENTATION + +#if ARENA_BACKEND == ARENA_BACKEND_LIBC_MALLOC +#include + +// TODO: instead of accepting specific capacity new_region() should accept the size of the object we want to fit into the region +// It should be up to new_region() to decide the actual capacity to allocate +Region *new_region(size_t capacity) +{ + size_t size_bytes = sizeof(Region) + sizeof(uintptr_t)*capacity; + // TODO: it would be nice if we could guarantee that the regions are allocated by ARENA_BACKEND_LIBC_MALLOC are page aligned + Region *r = (Region*)malloc(size_bytes); + ARENA_ASSERT(r); + r->next = NULL; + r->count = 0; + r->capacity = capacity; + return r; +} + +void free_region(Region *r) +{ + free(r); +} +#elif ARENA_BACKEND == ARENA_BACKEND_LINUX_MMAP +#include +#include + +Region *new_region(size_t capacity) +{ + size_t size_bytes = sizeof(Region) + sizeof(uintptr_t) * capacity; + Region *r = mmap(NULL, size_bytes, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + ARENA_ASSERT(r != MAP_FAILED); + r->next = NULL; + r->count = 0; + r->capacity = capacity; + return r; +} + +void free_region(Region *r) +{ + size_t size_bytes = sizeof(Region) + sizeof(uintptr_t) * r->capacity; + int ret = munmap(r, size_bytes); + ARENA_ASSERT(ret == 0); +} + +#elif ARENA_BACKEND == ARENA_BACKEND_WIN32_VIRTUALALLOC + +#if !defined(_WIN32) +# error "Current platform is not Windows" +#endif + +#define WIN32_LEAN_AND_MEAN +#include + +#define INV_HANDLE(x) (((x) == NULL) || ((x) == INVALID_HANDLE_VALUE)) + +Region *new_region(size_t capacity) +{ + SIZE_T size_bytes = sizeof(Region) + sizeof(uintptr_t) * capacity; + Region *r = VirtualAllocEx( + GetCurrentProcess(), /* Allocate in current process address space */ + NULL, /* Unknown position */ + size_bytes, /* Bytes to allocate */ + MEM_COMMIT | MEM_RESERVE, /* Reserve and commit allocated page */ + PAGE_READWRITE /* Permissions ( Read/Write )*/ + ); + if (INV_HANDLE(r)) + ARENA_ASSERT(0 && "VirtualAllocEx() failed."); + + r->next = NULL; + r->count = 0; + r->capacity = capacity; + return r; +} + +void free_region(Region *r) +{ + if (INV_HANDLE(r)) + return; + + BOOL free_result = VirtualFreeEx( + GetCurrentProcess(), /* Deallocate from current process address space */ + (LPVOID)r, /* Address to deallocate */ + 0, /* Bytes to deallocate ( Unknown, deallocate entire page ) */ + MEM_RELEASE /* Release the page ( And implicitly decommit it ) */ + ); + + if (FALSE == free_result) + ARENA_ASSERT(0 && "VirtualFreeEx() failed."); +} + +#elif ARENA_BACKEND == ARENA_BACKEND_WASM_HEAPBASE +# error "TODO: WASM __heap_base backend is not implemented yet" +#else +# error "Unknown Arena backend" +#endif + +// TODO: add debug statistic collection mode for arena +// Should collect things like: +// - How many times new_region was called +// - How many times existing region was skipped +// - How many times allocation exceeded REGION_DEFAULT_CAPACITY + +void *arena_alloc(Arena *a, size_t size_bytes) +{ + size_t size = (size_bytes + sizeof(uintptr_t) - 1)/sizeof(uintptr_t); + + if (a->end == NULL) { + ARENA_ASSERT(a->begin == NULL); + size_t capacity = REGION_DEFAULT_CAPACITY; + if (capacity < size) capacity = size; + a->end = new_region(capacity); + a->begin = a->end; + } + + while (a->end->count + size > a->end->capacity && a->end->next != NULL) { + a->end = a->end->next; + } + + if (a->end->count + size > a->end->capacity) { + ARENA_ASSERT(a->end->next == NULL); + size_t capacity = REGION_DEFAULT_CAPACITY; + if (capacity < size) capacity = size; + a->end->next = new_region(capacity); + a->end = a->end->next; + } + + void *result = &a->end->data[a->end->count]; + a->end->count += size; + return result; +} + +void *arena_realloc(Arena *a, void *oldptr, size_t oldsz, size_t newsz) +{ + if (newsz <= oldsz) return oldptr; + void *newptr = arena_alloc(a, newsz); + char *newptr_char = (char*)newptr; + char *oldptr_char = (char*)oldptr; + for (size_t i = 0; i < oldsz; ++i) { + newptr_char[i] = oldptr_char[i]; + } + return newptr; +} + +char *arena_strdup(Arena *a, const char *cstr) +{ + size_t n = strlen(cstr); + char *dup = (char*)arena_alloc(a, n + 1); + memcpy(dup, cstr, n); + dup[n] = '\0'; + return dup; +} + +void *arena_memdup(Arena *a, void *data, size_t size) +{ + return memcpy(arena_alloc(a, size), data, size); +} + +#ifndef ARENA_NOSTDIO +char *arena_sprintf(Arena *a, const char *format, ...) +{ + va_list args; + va_start(args, format); + int n = vsnprintf(NULL, 0, format, args); + va_end(args); + + ARENA_ASSERT(n >= 0); + char *result = (char*)arena_alloc(a, n + 1); + va_start(args, format); + vsnprintf(result, n + 1, format, args); + va_end(args); + + return result; +} +#endif // ARENA_NOSTDIO + +void arena_reset(Arena *a) +{ + for (Region *r = a->begin; r != NULL; r = r->next) { + r->count = 0; + } + + a->end = a->begin; +} + +void arena_free(Arena *a) +{ + Region *r = a->begin; + while (r) { + Region *r0 = r; + r = r->next; + free_region(r0); + } + a->begin = NULL; + a->end = NULL; +} + +#endif // ARENA_IMPLEMENTATION diff --git a/src/plug.c b/src/plug.c index 55ed61d..f2ff496 100644 --- a/src/plug.c +++ b/src/plug.c @@ -12,6 +12,10 @@ #define NOB_IMPLEMENTATION #include "nob.h" +#define ARENA_IMPLEMENTATION +#include "arena.h" +#include "external/dr_wav.h" + #include #include @@ -188,11 +192,10 @@ typedef struct { // Renderer bool rendering; RenderTexture2D screen; - Wave wave; - float *wave_samples; size_t wave_cursor; FFMPEG *ffmpeg; bool cancel_rendering; + Arena rendering_arena; // FFT Analyzer float in_raw[FFT_SIZE]; @@ -1221,21 +1224,153 @@ static void toggle_track_playing(Track *track) } } +// Music context type +// NOTE: Depends on data structure provided by the library +// in charge of reading the different file types +typedef enum { + MUSIC_AUDIO_NONE = 0, // No audio context loaded + MUSIC_AUDIO_WAV, // WAV audio context + MUSIC_AUDIO_OGG, // OGG audio context + MUSIC_AUDIO_FLAC, // FLAC audio context + MUSIC_AUDIO_MP3, // MP3 audio context + MUSIC_AUDIO_QOA, // QOA audio context + MUSIC_MODULE_XM, // XM module audio context + MUSIC_MODULE_MOD // MOD module audio context +} MusicContextType; + +typedef struct stb_vorbis stb_vorbis; +extern int stb_vorbis_get_samples_short_interleaved(stb_vorbis *f, int channels, short *buffer, int num_shorts); + +#define SUPPORT_FILEFORMAT_WAV +#define SUPPORT_FILEFORMAT_OGG +int poll_samples_from_music(Arena *a, Music music, float *buffer, size_t num_floats) +{ + switch (music.ctxType) + { + #if defined(SUPPORT_FILEFORMAT_WAV) + case MUSIC_AUDIO_WAV: + { + size_t num_samples = num_floats/music.stream.channels; + if (music.stream.sampleSize == 16) + { + short *samples = (short *)arena_alloc(a, num_floats*sizeof(short)); + int frameCountRead = (int)drwav_read_pcm_frames_s16((drwav *)music.ctxData, num_samples, samples); + for (size_t i = 0; i < frameCountRead*music.stream.channels; ++i) { + buffer[i] = (float)(samples[i])/32767.0f; + } + return frameCountRead; + } + else if (music.stream.sampleSize == 32) + { + int frameCountRead = (int)drwav_read_pcm_frames_f32((drwav *)music.ctxData, num_samples, buffer); + return frameCountRead; + } + } break; + #endif + #if defined(SUPPORT_FILEFORMAT_OGG) + case MUSIC_AUDIO_OGG: + { + short *samples = (short *)arena_alloc(a, num_floats*sizeof(short)); + int frameCountRead = stb_vorbis_get_samples_short_interleaved((stb_vorbis *)music.ctxData, music.stream.channels, samples, num_floats); + for (size_t i = 0; i < frameCountRead*music.stream.channels; ++i) { + buffer[i] = (float)(samples[i])/32767.0f; + } + return frameCountRead; + } break; + #endif + #if defined(SUPPORT_FILEFORMAT_MP3) + case MUSIC_AUDIO_MP3: + { + while (true) + { + int frameCountRead = (int)drmp3_read_pcm_frames_f32((drmp3 *)music.ctxData, frameCountStillNeeded, (float *)((char *)AUDIO.System.pcmBuffer + frameCountReadTotal*frameSize)); + frameCountReadTotal += frameCountRead; + frameCountStillNeeded -= frameCountRead; + if (frameCountStillNeeded == 0) break; + else drmp3_seek_to_start_of_stream((drmp3 *)music.ctxData); + } + } break; + #endif + #if defined(SUPPORT_FILEFORMAT_QOA) + case MUSIC_AUDIO_QOA: + { + unsigned int frameCountRead = qoaplay_decode((qoaplay_desc *)music.ctxData, (float *)AUDIO.System.pcmBuffer, framesToStream); + frameCountReadTotal += frameCountRead; + /* + while (true) + { + int frameCountRead = (int)qoaplay_decode((qoaplay_desc *)music.ctxData, (float *)((char *)AUDIO.System.pcmBuffer + frameCountReadTotal*frameSize), frameCountStillNeeded); + frameCountReadTotal += frameCountRead; + frameCountStillNeeded -= frameCountRead; + if (frameCountStillNeeded == 0) break; + else qoaplay_rewind((qoaplay_desc *)music.ctxData); + } + */ + } break; + #endif + #if defined(SUPPORT_FILEFORMAT_FLAC) + case MUSIC_AUDIO_FLAC: + { + while (true) + { + int frameCountRead = (int)drflac_read_pcm_frames_s16((drflac *)music.ctxData, frameCountStillNeeded, (short *)((char *)AUDIO.System.pcmBuffer + frameCountReadTotal*frameSize)); + frameCountReadTotal += frameCountRead; + frameCountStillNeeded -= frameCountRead; + if (frameCountStillNeeded == 0) break; + else drflac__seek_to_first_frame((drflac *)music.ctxData); + } + } break; + #endif + #if defined(SUPPORT_FILEFORMAT_XM) + case MUSIC_MODULE_XM: + { + // NOTE: Internally we consider 2 channels generation, so sampleCount/2 + if (AUDIO_DEVICE_FORMAT == ma_format_f32) jar_xm_generate_samples((jar_xm_context_t *)music.ctxData, (float *)AUDIO.System.pcmBuffer, framesToStream); + else if (AUDIO_DEVICE_FORMAT == ma_format_s16) jar_xm_generate_samples_16bit((jar_xm_context_t *)music.ctxData, (short *)AUDIO.System.pcmBuffer, framesToStream); + else if (AUDIO_DEVICE_FORMAT == ma_format_u8) jar_xm_generate_samples_8bit((jar_xm_context_t *)music.ctxData, (char *)AUDIO.System.pcmBuffer, framesToStream); + //jar_xm_reset((jar_xm_context_t *)music.ctxData); + + } break; + #endif + #if defined(SUPPORT_FILEFORMAT_MOD) + case MUSIC_MODULE_MOD: + { + // NOTE: 3rd parameter (nbsample) specify the number of stereo 16bits samples you want, so sampleCount/2 + jar_mod_fillbuffer((jar_mod_context_t *)music.ctxData, (short *)AUDIO.System.pcmBuffer, framesToStream, 0); + //jar_mod_seek_start((jar_mod_context_t *)music.ctxData); + + } break; + #endif + default: break; + } + + // TODO: do something better with that + fprintf(stderr, "UNREACHABLE: Unsupported format!\n"); + abort(); +} + static void start_rendering_track(Track *track) { StopMusicStream(track->music); fft_clean(); - // TODO: LoadWave is pretty slow on big files - p->wave = LoadWave(track->file_path); p->wave_cursor = 0; - p->wave_samples = LoadWaveSamples(p->wave); // TODO: set the rendering output path based on the input path // Basically output into the same folder p->ffmpeg = ffmpeg_start_rendering(p->screen.texture.width, p->screen.texture.height, RENDER_FPS, track->file_path); p->rendering = true; p->cancel_rendering = false; SetTraceLogLevel(LOG_WARNING); + SetTargetFPS(0); +} + +static void finish_rendering_track(Track *track) +{ + SetTraceLogLevel(LOG_INFO); + p->rendering = false; + fft_clean(); + PlayMusicStream(track->music); + SetTargetFPS(60); } #ifdef MUSIALIZER_MICROPHONE @@ -1599,12 +1734,7 @@ static void rendering_screen(void) NOB_ASSERT(track != NULL); if (p->ffmpeg == NULL) { // Starting FFmpeg process has failed for some reason if (IsKeyPressed(KEY_ESCAPE)) { - SetTraceLogLevel(LOG_INFO); - UnloadWave(p->wave); - UnloadWaveSamples(p->wave_samples); - p->rendering = false; - fft_clean(); - PlayMusicStream(track->music); + finish_rendering_track(track); } const char *label = "FFmpeg Failure: Check the Logs"; @@ -1625,7 +1755,7 @@ static void rendering_screen(void) DrawTextEx(p->font, label, position, fontSize, 0, color); } else { // FFmpeg process is going // TODO: introduce a rendering mode that perfectly loops the video - if (p->wave_cursor >= p->wave.frameCount && fft_settled()) { // Rendering is finished + if (p->wave_cursor >= track->music.frameCount && fft_settled()) { // Rendering is finished if (!ffmpeg_end_rendering(p->ffmpeg, false)) { // NOTE: Ending FFmpeg process has failed, let's mark ffmpeg handle as NULL // which will be interpreted as "FFmpeg Failure" on the next frame. @@ -1634,23 +1764,12 @@ static void rendering_screen(void) // cause it should deallocate all the resources even in case of a failure. p->ffmpeg = NULL; } else { - SetTraceLogLevel(LOG_INFO); - UnloadWave(p->wave); - UnloadWaveSamples(p->wave_samples); - p->rendering = false; - fft_clean(); - PlayMusicStream(track->music); + finish_rendering_track(track); } } else if (IsKeyPressed(KEY_ESCAPE) || p->cancel_rendering) { // Rendering is cancelled ffmpeg_end_rendering(p->ffmpeg, true); p->ffmpeg = NULL; - - SetTraceLogLevel(LOG_INFO); - UnloadWave(p->wave); - UnloadWaveSamples(p->wave_samples); - p->rendering = false; - fft_clean(); - PlayMusicStream(track->music); + finish_rendering_track(track); } else { // Rendering is going... // Label const char *label = "Rendering video..."; @@ -1666,7 +1785,7 @@ static void rendering_screen(void) // Progress bar float bar_width = w*2/3; float bar_height = p->font.baseSize*0.25; - float bar_progress = (float)p->wave_cursor/p->wave.frameCount; + float bar_progress = (float)p->wave_cursor/track->music.frameCount; float bar_padding_top = p->font.baseSize*0.5; if (bar_progress > 1) bar_progress = 1; Rectangle bar_filling = { @@ -1700,14 +1819,13 @@ static void rendering_screen(void) // Rendering { - size_t chunk_size = p->wave.sampleRate/RENDER_FPS; - float *fs = (float*)p->wave_samples; + size_t channels = track->music.stream.channels; + size_t chunk_size = track->music.stream.sampleRate/RENDER_FPS; + float *buffer = arena_alloc(&p->rendering_arena, chunk_size*channels*sizeof(*buffer)); + memset(buffer, 0, chunk_size*channels*sizeof(*buffer)); + poll_samples_from_music(&p->rendering_arena, track->music, buffer, chunk_size*channels); for (size_t i = 0; i < chunk_size; ++i) { - if (p->wave_cursor < p->wave.frameCount) { - fft_push(fs[p->wave_cursor*p->wave.channels + 0]); - } else { - fft_push(0); - } + fft_push(buffer[i*channels + 0]); p->wave_cursor += 1; } }