diff --git a/build/premake5.lua b/build/premake5.lua index a9cba09..b38b670 100644 --- a/build/premake5.lua +++ b/build/premake5.lua @@ -202,6 +202,19 @@ if (downloadRaylib) then files {"../src/*.rc", "../src/*.ico"} files {"../resources/**"} + -- Exclude platform-specific files on each target + filter {"system:linux"} + excludedfiles { "../src/platform_win32.c", "../src/platform_web.c" } + + filter {"system:windows"} + excludedfiles { "../src/platform_linux.c", "../src/platform_web.c" } + + filter {"system:macosx"} + excludedfiles { "../src/platform_win32.c", "../src/platform_web.c" } + + filter {"action:emscripten"} + excludedfiles { "../src/platform_linux.c", "../src/platform_win32.c" } + filter{} includedirs { "../src" } diff --git a/build_web.sh b/build_web.sh index 22e4ade..74c689d 100755 --- a/build_web.sh +++ b/build_web.sh @@ -42,9 +42,22 @@ cd "$RAYLIB_BUILD" emar rcs libraylib.a rcore.o rshapes.o rtextures.o rtext.o rmodels.o raudio.o cd "$SCRIPT_DIR" -echo "=== Step 2: Compiling spectrogram for web ===" +# Compile spectrogram (web version — no subprocess support) +# platform_web.c provides stub implementations for spawn/error functions +echo "=== Step 2: Compiling spectrogram + platform for web ===" cd "$SCRIPT_DIR" +# Compile platform_web.o (handles fork/execvp stubs for web) +emcc -c -Os -Wall -DPLATFORM_WEB "$SRC_DIR/platform_web.c" -o "$RAYLIB_BUILD/platform_web.o" + +# Compile spectrogram +emcc -c -Os -Wall -DPLATFORM_WEB "$SRC_DIR/spectrogram.c" \ + -I"$RAYLIB_DIR/src" \ + -I"$RAYLIB_DIR/src/external" \ + -I"$RAYLIB_DIR/src/external/glfw/include" \ + -Iinclude \ + -o "$RAYLIB_BUILD/spectrogram.o" + # Linker flags LDFLAGS="-s USE_GLFW=3 -s ASYNCIFY -s TOTAL_MEMORY=67108864 -s FORCE_FILESYSTEM=1 --preload-file resources@resources --preload-file fonts/DejaVuSansMono.ttf@fonts/DejaVuSansMono.ttf --shell-file $SCRIPT_DIR/web_shell.html -s NO_EXIT_RUNTIME=1" @@ -56,7 +69,8 @@ fi # Compile and link emcc -o "$OUTPUT" \ - "$SRC_DIR/spectrogram.c" \ + "$RAYLIB_BUILD/platform_web.o" \ + "$RAYLIB_BUILD/spectrogram.o" \ -I"$RAYLIB_DIR/src" \ -I"$RAYLIB_DIR/src/external" \ -I"$RAYLIB_DIR/src/external/glfw/include" \ diff --git a/rspektrum.make b/rspektrum.make index d9d1e68..b07652b 100644 --- a/rspektrum.make +++ b/rspektrum.make @@ -119,7 +119,9 @@ GENERATED := OBJECTS := GENERATED += $(OBJDIR)/spectrogram.o +GENERATED += $(OBJDIR)/platform_linux.o OBJECTS += $(OBJDIR)/spectrogram.o +OBJECTS += $(OBJDIR)/platform_linux.o # Rules # ############################################# @@ -187,6 +189,10 @@ $(OBJDIR)/spectrogram.o: src/spectrogram.c @echo "$(notdir $<)" $(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/platform_linux.o: src/platform_linux.c + @echo "$(notdir $<)" + $(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" + -include $(OBJECTS:%.o=%.d) ifneq (,$(PCH)) -include $(PCH_PLACEHOLDER).d diff --git a/src/platform.h b/src/platform.h new file mode 100644 index 0000000..645934e --- /dev/null +++ b/src/platform.h @@ -0,0 +1,96 @@ +#pragma once + +#include + +/* ── Public API ─────────────────────────────────────────────────────────── */ + +/** + * Platform identification (for build-time selection). + * Only one should be defined: PLATFORM_LINUX, PLATFORM_WIN32, PLATFORM_WEB. + * Undefined means unknown / desktop fallback. + */ +enum Platform { + PLATFORM_UNKNOWN = 0, + PLATFORM_LINUX, + PLATFORM_WIN32, + PLATFORM_WEB, +}; + +/** + * Error codes returned by platform functions. + */ +enum PlatformError { + PLATFORM_OK = 0, /**< Success */ + PLATFORM_GENERAL = -1, /**< Non-specific failure */ + PLATFORM_NOT_SUPPORTED = -2, /**< Function not available on this platform */ +}; +typedef int PlatformError; + +/* ── Process / spawning ─────────────────────────────────────────────────── */ + +/** + * Result of waiting for a spawned process. + */ +enum SpawnStatus { + SPAWN_WAITING, /**< Process still running */ + SPAWN_EXITED, /**< Process has exited, exitCode is valid */ + SPAWN_EXIT_CODE_UNKNOWN, /**< Exited but we could not determine code */ +}; +typedef int SpawnStatus; + +/** + * Opaque handle to a spawned child process. + */ +typedef struct { + void *handle; + enum SpawnStatus status; + int exit_code; + int last_error; +} PlatformSpawnHandle; + +/** + * Spawn a child process synchronously. Blocks until the child + * exits — without going through a shell, so user-controlled + * arguments cannot inject commands. + * + * @param program Program to run (passed directly to execvp / + * CreateProcess, never split by a shell). + * @param argv NULL-terminated argument array. argv[0] is the + * program name, argv[1..n] are arguments. + * @param[out] out_handle Fill with a handle for status queries. + * @return PLATFORM_OK on success, PLATFORM_GENERAL on failure. + */ +PlatformError Platform_SpawnChild(const char *program, const char **argv, + PlatformSpawnHandle *out_handle); + +/** + * Wait for a spawned process to exit. + */ +PlatformError Platform_WaitForChild(PlatformSpawnHandle *handle); + +/** + * Get the exit status of a spawned process. + */ +SpawnStatus Platform_GetExitStatus(const PlatformSpawnHandle *handle, + int *code); + +/** + * Clean up resources associated with a spawn handle. + */ +void Platform_CloseSpawnHandle(PlatformSpawnHandle *handle); + +/* ── Error description ─────────────────────────────────────────────────── */ + +/** + * Human-readable description of the last platform error. + * Returns a static string (caller does not free). + */ +const char *Platform_GetLastErrorMessage(void); + +/* ── Paths ──────────────────────────────────────────────────────────────── */ + +/** + * Temporary directory for the platform. + * Static string — do not modify or free. + */ +const char *Platform_GetTempDir(void); diff --git a/src/platform_linux.c b/src/platform_linux.c new file mode 100644 index 0000000..8ee4b89 --- /dev/null +++ b/src/platform_linux.c @@ -0,0 +1,102 @@ +/* Platform layer: Linux — fork/execvp, path helpers, error strings */ + +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE +#endif + +#include "platform.h" +#include +#include +#include +#include +#include +#include +#include + +#define STATIC static + +STATIC char last_error_msg[256] = { 0 }; +STATIC PlatformSpawnHandle current_handle = { 0 }; + +/* ── Helpers ─────────────────────────────────────────────────────────── */ + +STATIC void set_last_error(const char *fmt, ...) { + // Simple snprintf to a local buffer — fine for error paths + char buf[256] = { 0 }; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + strncpy(last_error_msg, buf, sizeof(last_error_msg) - 1); +} + +/* ── Platform API ────────────────────────────────────────────────────── */ + +PlatformError Platform_SpawnChild(const char *program, const char **argv, + PlatformSpawnHandle *out_handle) { + last_error_msg[0] = '\0'; + + memset(out_handle, 0, sizeof(*out_handle)); + pid_t pid = fork(); + + if (pid < 0) { + snprintf(last_error_msg, sizeof(last_error_msg), + "fork() failed: %s", strerror(errno)); + out_handle->last_error = errno; + return PLATFORM_GENERAL; + } + + if (pid == 0) { + // Child process — replace with target + execvp(program, (char **)argv); + _exit(127); // execvp only returns on error + } + + // Parent + out_handle->handle = (void *)(intptr_t)pid; + out_handle->status = SPAWN_WAITING; + return PLATFORM_OK; +} + +PlatformError Platform_WaitForChild(PlatformSpawnHandle *handle) { + if (!handle || !handle->handle) return PLATFORM_GENERAL; + + pid_t pid = (pid_t)(intptr_t)handle->handle; + int status = 0; + waitpid(pid, &status, 0); + + if (WIFEXITED(status)) { + handle->status = SPAWN_EXITED; + handle->exit_code = WEXITSTATUS(status); + } else if (WIFSIGNALED(status)) { + handle->status = SPAWN_EXITED; + handle->exit_code = WTERMSIG(status); + } else { + handle->status = SPAWN_EXIT_CODE_UNKNOWN; + } + + handle->handle = NULL; // Consumed + return PLATFORM_OK; +} + +SpawnStatus Platform_GetExitStatus(const PlatformSpawnHandle *handle, int *code) { + if (!handle) return SPAWN_WAITING; + if (code) *code = handle->exit_code; + return handle->status; +} + +void Platform_CloseSpawnHandle(PlatformSpawnHandle *handle) { + if (!handle) return; + handle->handle = NULL; + handle->status = SPAWN_WAITING; + handle->exit_code = 0; + handle->last_error = 0; +} + +const char *Platform_GetLastErrorMessage(void) { + return last_error_msg; +} + +const char *Platform_GetTempDir(void) { + return "/tmp"; +} diff --git a/src/platform_web.c b/src/platform_web.c new file mode 100644 index 0000000..32bd3f3 --- /dev/null +++ b/src/platform_web.c @@ -0,0 +1,65 @@ +/* + * platform_web.c — Emscripten/WebAssembly stub implementation + * + * The web version cannot spawn child processes, so these functions + * return "not supported". A real implementation would use Emscripten's + * pthreads or asyncify if ffmpeg integration is needed. + */ + +#include "platform.h" +#include +#include +#include + +/* ── Helpers ─────────────────────────────────────────────────────────── */ + +static char last_error_msg[256] = { 0 }; + +static void set_last_error(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + vsnprintf(last_error_msg, sizeof(last_error_msg), fmt, args); + va_end(args); +} + +/* ── Platform API ────────────────────────────────────────────────────── */ + +PlatformError Platform_SpawnChild(const char *program, const char **argv, + PlatformSpawnHandle *out_handle) { + (void)program; + (void)argv; + set_last_error("Spawning child processes is not supported on the web"); + out_handle->last_error = ENOSYS; + return PLATFORM_NOT_SUPPORTED; +} + +PlatformError Platform_WaitForChild(PlatformSpawnHandle *handle) { + (void)handle; + return PLATFORM_NOT_SUPPORTED; +} + +SpawnStatus Platform_GetExitStatus(const PlatformSpawnHandle *handle, int *code) { + (void)handle; + (void)code; + return SPAWN_WAITING; +} + +void Platform_CloseSpawnHandle(PlatformSpawnHandle *handle) { + if (!handle) return; + handle->handle = NULL; + handle->status = SPAWN_WAITING; + handle->exit_code = 0; + handle->last_error = 0; +} + +const char *Platform_GetLastErrorMessage(void) { + return last_error_msg; +} + +const char *Platform_GetTempDir(void) { + /* + * Emscripten's virtual filesystem: "/" works as root. + * For persistent downloads/uploads, consider IDBFS mount. + */ + return "/"; +} diff --git a/src/platform_win32.c b/src/platform_win32.c new file mode 100644 index 0000000..7315902 --- /dev/null +++ b/src/platform_win32.c @@ -0,0 +1,129 @@ +/* + * platform_win32.c — Windows implementation + * + * Requires: Windows.h, mincore.lib (for CreateProcessA) + */ + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include "platform.h" +#include +#include +#include + +/* ── Helpers ─────────────────────────────────────────────────────────── */ + +static char last_error_msg[256] = { 0 }; + +static void set_last_error(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + vsnprintf(last_error_msg, sizeof(last_error_msg), fmt, args); + va_end(args); +} + +/* ── Platform API ────────────────────────────────────────────────────── */ + +PlatformError Platform_SpawnChild(const char *program, const char **argv, + PlatformSpawnHandle *out_handle) { + last_error_msg[0] = '\0'; + + /* Build a single command line from argv */ + char cmdline[2048] = { 0 }; + for (int i = 0; argv[i]; i++) { + if (i > 0) strncat(cmdline, " ", sizeof(cmdline) - strlen(cmdline) - 1); + /* Quote arguments that contain spaces */ + if (strchr(argv[i], ' ')) { + strncat(cmdline, "\"", sizeof(cmdline) - strlen(cmdline) - 1); + strncat(cmdline, argv[i], sizeof(cmdline) - strlen(cmdline) - 1); + strncat(cmdline, "\"", sizeof(cmdline) - strlen(cmdline) - 1); + } else { + strncat(cmdline, argv[i], sizeof(cmdline) - strlen(cmdline) - 1); + } + } + + /* CreateProcess needs a mutable buffer */ + char cmd_copy[2048]; + strncpy(cmd_copy, cmdline, sizeof(cmd_copy) - 1); + cmd_copy[sizeof(cmd_copy) - 1] = '\0'; + + STARTUPINFOA si; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + PROCESS_INFORMATION pi; + ZeroMemory(&pi, sizeof(pi)); + + if (!CreateProcessA(NULL, cmd_copy, NULL, NULL, FALSE, + 0, NULL, NULL, &si, &pi)) { + DWORD err = GetLastError(); + char msg[256]; + sprintf(msg, "CreateProcess failed (code %lu)", err); + strncpy(last_error_msg, msg, sizeof(last_error_msg) - 1); + out_handle->last_error = (int)err; + return PLATFORM_GENERAL; + } + + out_handle->handle = (void *)pi.hProcess; + out_handle->status = SPAWN_WAITING; + /* Don't close the thread handle here — CloseSpawnHandle will */ + CloseHandle(pi.hThread); + + return PLATFORM_OK; +} + +PlatformError Platform_WaitForChild(PlatformSpawnHandle *handle) { + if (!handle || !handle->handle) return PLATFORM_GENERAL; + + DWORD wait_result = WaitForSingleObject((HANDLE)handle->handle, INFINITE); + if (wait_result != WAIT_OBJECT_0) { + handle->last_error = (int)GetLastError(); + return PLATFORM_GENERAL; + } + + DWORD exit_code = 0; + if (!GetExitCodeProcess((HANDLE)handle->handle, &exit_code)) { + handle->status = SPAWN_EXIT_CODE_UNKNOWN; + handle->last_error = (int)GetLastError(); + return PLATFORM_GENERAL; + } + + handle->status = SPAWN_EXITED; + handle->exit_code = (int)exit_code; + handle->handle = NULL; + return PLATFORM_OK; +} + +SpawnStatus Platform_GetExitStatus(const PlatformSpawnHandle *handle, int *code) { + if (!handle) return SPAWN_WAITING; + if (code) *code = handle->exit_code; + return handle->status; +} + +void Platform_CloseSpawnHandle(PlatformSpawnHandle *handle) { + if (!handle) return; + if (handle->handle) { + CloseHandle((HANDLE)handle->handle); + handle->handle = NULL; + } + handle->status = SPAWN_WAITING; + handle->exit_code = 0; + handle->last_error = 0; +} + +const char *Platform_GetLastErrorMessage(void) { + return last_error_msg; +} + +const char *Platform_GetTempDir(void) { + /* Get temp directory path once and cache it */ + static char temp_path[MAX_PATH] = { 0 }; + if (temp_path[0] == '\0') { + DWORD len = GetTempPathA(sizeof(temp_path), temp_path); + if (len == 0 || len >= sizeof(temp_path)) { + strncpy(temp_path, "C:\\Windows\\Temp", sizeof(temp_path) - 1); + } + } + return temp_path; +} diff --git a/src/spectrogram.c b/src/spectrogram.c index c0026f5..cea1ceb 100644 --- a/src/spectrogram.c +++ b/src/spectrogram.c @@ -1,11 +1,9 @@ // spectrogram.c - Spectrogram viewer with region selection and playback // Based on Unity Audio-Experiments project -#define _POSIX_C_SOURCE 200809L -#define _DEFAULT_SOURCE - #include "raylib.h" #include "resource_dir.h" +#include "platform.h" #include #include @@ -418,21 +416,34 @@ static void FreeSTFT(StftResult* result) // Convert audio file to WAV using ffmpeg (if available) static bool ConvertToFFmpegWAV(const char* inputPath, char* outputPath, size_t outputSize) { - // Create temp WAV path - snprintf(outputPath, outputSize, "/tmp/rspektrum_temp_converted.wav"); - - // Build ffmpeg command - char cmd[1024]; - snprintf(cmd, sizeof(cmd), "ffmpeg -y -loglevel quiet -i \"%s\" -ar 48000 -ac 1 -f wav \"%s\" 2>&1", inputPath, outputPath); - - int result = system(cmd); - - if (result == 0 && FileExists(outputPath)) { + snprintf(outputPath, outputSize, "%s/rspektrum_temp_converted.wav", + Platform_GetTempDir()); + + /* Build argv array — no shell = no injection risk */ + const char* argv[] = { + "ffmpeg", "-y", "-loglevel", "quiet", + "-i", inputPath, + "-ar", "48000", "-ac", "1", "-f", "wav", + outputPath, NULL + }; + + PlatformSpawnHandle handle = { 0 }; + PlatformError rc = Platform_SpawnChild("ffmpeg", argv, &handle); + if (rc != PLATFORM_OK) { + TraceLog(LOG_WARNING, "Failed to spawn ffmpeg: %s", Platform_GetLastErrorMessage()); + return false; + } + + Platform_WaitForChild(&handle); + + int exit_code = 0; + SpawnStatus status = Platform_GetExitStatus(&handle, &exit_code); + if (status == SPAWN_EXITED && exit_code == 0 && FileExists(outputPath)) { TraceLog(LOG_INFO, "FFmpeg conversion successful: %s", outputPath); return true; } - - TraceLog(LOG_WARNING, "FFmpeg conversion failed or not available"); + + TraceLog(LOG_WARNING, "FFmpeg conversion failed (exit code %d)", exit_code); return false; } @@ -457,10 +468,10 @@ static bool LoadWavFile(const char* filepath, AudioSignal* signal) } Wave wave = LoadWave(filepath); - if (wave.data == NULL) { - TraceLog(LOG_ERROR, "Failed to open WAV file: %s", filepath); - if (isConverted) remove(convertedPath); - return false; + if (wave.data == NULL) { + TraceLog(LOG_ERROR, "Failed to open WAV file: %s", filepath); + if (isConverted) FileRemove(convertedPath); + return false; } signal->sampleRate = wave.sampleRate; @@ -493,7 +504,7 @@ static bool LoadWavFile(const char* filepath, AudioSignal* signal) // Clean up temp file if converted if (isConverted) { - remove(convertedPath); + FileRemove(convertedPath); TraceLog(LOG_INFO, "Cleaned up temp file: %s", convertedPath); } @@ -768,9 +779,13 @@ static void ScanDirectory(const char* path) const char* name = GetFileName(files.paths[i]); if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; if (DirectoryExists(files.paths[i])) { - app.browserFiles[app.browserFileCount] = strdup(name); - app.browserIsDir[app.browserFileCount] = true; - app.browserFileCount++; + size_t len = strlen(name) + 1; + app.browserFiles[app.browserFileCount] = (char*)malloc(len); + if (app.browserFiles[app.browserFileCount]) { + memcpy(app.browserFiles[app.browserFileCount], name, len); + app.browserIsDir[app.browserFileCount] = true; + app.browserFileCount++; + } } } for (int i = 0; i < files.count; i++) { @@ -780,9 +795,13 @@ static void ScanDirectory(const char* path) const char* ext = GetFileExtension(files.paths[i]); if (ext && (strcmp(ext, ".wav") == 0 || strcmp(ext, ".WAV") == 0 || strcmp(ext, ".Wave") == 0 || strcmp(ext, ".Wav") == 0)) { - app.browserFiles[app.browserFileCount] = strdup(name); - app.browserIsDir[app.browserFileCount] = false; - app.browserFileCount++; + size_t len = strlen(name) + 1; + app.browserFiles[app.browserFileCount] = (char*)malloc(len); + if (app.browserFiles[app.browserFileCount]) { + memcpy(app.browserFiles[app.browserFileCount], name, len); + app.browserIsDir[app.browserFileCount] = false; + app.browserFileCount++; + } } } }