diff --git a/build_web.sh b/build_web.sh index 03c5976..3b6fb14 100755 --- a/build_web.sh +++ b/build_web.sh @@ -79,7 +79,9 @@ done # Linker flags # The font lives at resources/fonts/DejaVuSansMono.ttf and is loaded relative to # the resources dir at runtime, so the resources@resources preload covers it. -LDFLAGS="-s USE_GLFW=3 -s ASYNCIFY -s TOTAL_MEMORY=67108864 -s FORCE_FILESYSTEM=1 --preload-file resources@resources --shell-file $SCRIPT_DIR/web_shell.html -s NO_EXIT_RUNTIME=1" +# INITIAL_MEMORY + ALLOW_MEMORY_GROWTH: the web build now computes the full STFT +# up front, so the heap must be able to grow for longer recordings. +LDFLAGS="-s USE_GLFW=3 -s ASYNCIFY -s INITIAL_MEMORY=67108864 -s ALLOW_MEMORY_GROWTH=1 -s FORCE_FILESYSTEM=1 --preload-file resources@resources --shell-file $SCRIPT_DIR/web_shell.html -s NO_EXIT_RUNTIME=1" if [ "$BUILD_TYPE" = "debug" ]; then LDFLAGS="$LDFLAGS -g -O0 -s ASSERTIONS=1" diff --git a/src/audio.c b/src/audio.c index 3259f22..817af56 100644 --- a/src/audio.c +++ b/src/audio.c @@ -253,9 +253,10 @@ void ExportSelectionWAV(const char* dirPath) Wave wave = { .data = samples, .frameCount = (unsigned int)numSamples, .sampleRate = (unsigned int)app.signal.sampleRate, .sampleSize = 32, .channels = 1 }; - if (ExportWave(wave, path)) - snprintf(app.exportMessage, sizeof(app.exportMessage), "Saved WAV: %.40s", path); - else + if (ExportWave(wave, path)) { + Platform_OfferFileToUser(path); // desktop: no-op; web: browser download + snprintf(app.exportMessage, sizeof(app.exportMessage), "Saved WAV: %.40s", GetFileName(path)); + } else snprintf(app.exportMessage, sizeof(app.exportMessage), "WAV export failed"); app.exportMessageTimer = 3.0f; diff --git a/src/platform.h b/src/platform.h index 645934e..6266638 100644 --- a/src/platform.h +++ b/src/platform.h @@ -94,3 +94,17 @@ const char *Platform_GetLastErrorMessage(void); * Static string — do not modify or free. */ const char *Platform_GetTempDir(void); + +/* ── File delivery ──────────────────────────────────────────────────────── */ + +/** + * Hand a just-written file to the user. + * + * On desktop this is a no-op: the file is already on disk at `path` where the + * user asked for it. On the web there is no local disk the user can see, so + * this reads the file back from the virtual filesystem, triggers a browser + * "save as" download named after the file, and removes the temporary copy. + * + * @param path Path the file was just written to. + */ +void Platform_OfferFileToUser(const char *path); diff --git a/src/platform_linux.c b/src/platform_linux.c index 8ee4b89..39063e9 100644 --- a/src/platform_linux.c +++ b/src/platform_linux.c @@ -100,3 +100,8 @@ const char *Platform_GetLastErrorMessage(void) { const char *Platform_GetTempDir(void) { return "/tmp"; } + +void Platform_OfferFileToUser(const char *path) { + /* Desktop: the file is already on disk where the user wanted it. */ + (void)path; +} diff --git a/src/platform_web.c b/src/platform_web.c index 242bb78..e936c5b 100644 --- a/src/platform_web.c +++ b/src/platform_web.c @@ -10,7 +10,8 @@ #include #include #include -#include /* ENOSYS */ +#include /* ENOSYS */ +#include /* EM_ASM */ /* ── Helpers ─────────────────────────────────────────────────────────── */ @@ -64,3 +65,31 @@ const char *Platform_GetTempDir(void) { */ return "/"; } + +void Platform_OfferFileToUser(const char *path) { + if (!path) return; + /* + * The file was just written into the in-memory (MEMFS) filesystem, which + * the user can't see. Read it back and hand it to the browser as a + * download, then unlink the temporary copy so we don't leak heap memory. + */ + EM_ASM({ + var p = UTF8ToString($0); + try { + var data = FS.readFile(p); // Uint8Array + var name = p.split('/').pop() || 'download'; + var blob = new Blob([data], { type: 'application/octet-stream' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = name; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(function() { URL.revokeObjectURL(url); }, 1000); + try { FS.unlink(p); } catch (e) {} + } catch (e) { + console.error('rspektrum: download failed for ' + p + ': ' + e); + } + }, path); +} diff --git a/src/platform_win32.c b/src/platform_win32.c index 7315902..54a83f9 100644 --- a/src/platform_win32.c +++ b/src/platform_win32.c @@ -127,3 +127,8 @@ const char *Platform_GetTempDir(void) { } return temp_path; } + +void Platform_OfferFileToUser(const char *path) { + /* Desktop: the file is already on disk where the user wanted it. */ + (void)path; +} diff --git a/src/spectrogram.c b/src/spectrogram.c index 93bea5e..f3ea47c 100644 --- a/src/spectrogram.c +++ b/src/spectrogram.c @@ -20,6 +20,23 @@ #include #include +#ifdef __EMSCRIPTEN__ +#include + +// Keep raylib's framebuffer/screen size matched to the browser viewport, so the +// (immediate-mode) UI fills the page and reflows on window resize the same way +// the desktop OS window does. Going through SetWindowSize keeps the screen size, +// GL viewport, and projection consistent; the != guard avoids per-frame churn. +static void SyncCanvasToWindow(void) +{ + int w = EM_ASM_INT({ return window.innerWidth; }); + int h = EM_ASM_INT({ return window.innerHeight; }); + if (w > 0 && h > 0 && (w != GetScreenWidth() || h != GetScreenHeight())) { + SetWindowSize(w, h); + } +} +#endif + // ============================================================================ // Global State (declared extern in spectrogram_types.h) // ============================================================================ @@ -217,7 +234,18 @@ static void DispatchKeymap(void) int main(int argc, char* argv[]) { +#ifdef __EMSCRIPTEN__ + // FLAG_WINDOW_HIGHDPI is buggy on the web backend: the Emscripten resize + // callback sets the screen size to window.innerWidth, but the GLFW window- + // size callback it triggers divides that by devicePixelRatio when HIGHDPI + // is set. On a HiDPI display the framebuffer and the reported screen size + // desync and the UI renders into a corner. UI scaling is handled by + // GetUIScale() regardless, so the flag is unnecessary here. raylib auto- + // resizes the canvas to the window when FLAG_WINDOW_RESIZABLE is set. + SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE); +#else SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI); +#endif InitWindow(1280, 800, "Spectrogram Viewer"); SetTargetFPS(60); SetTraceLogLevel(LOG_WARNING); // Suppress INFO texture logs @@ -320,6 +348,11 @@ int main(int argc, char* argv[]) while (!WindowShouldClose()) { +#ifdef __EMSCRIPTEN__ + // Track the browser viewport (fill + reflow on resize, like desktop). + SyncCanvasToWindow(); +#endif + // Drag & Drop if (IsFileDropped()) { FilePathList dropped = LoadDroppedFiles(); @@ -764,6 +797,27 @@ int main(int argc, char* argv[]) // Processing (incremental across frames) if (app.loaded && !app.stftComputed) { +#ifdef __EMSCRIPTEN__ + // Web build: there are no worker threads, and the desktop path's + // overview-then-deferred-high-res fill depends on many main-loop + // iterations yielding to the browser (which made loading appear to + // stall partway). Compute the full-resolution STFT in one shot so + // the spectrogram is completely ready as soon as the file loads. + ComputeSTFTInit(&app.signal, &app.stft, app.fftSize); + app.skipFactor = 1; // full resolution, no overview stride + ComputeSTFTIncremental(&app.signal, &app.stft, app.fftSize, 0); // computes every segment + AutoScaleAmplitude(&app.stft); + GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture); + app.currentSTFTSegment = app.stft.numSegments; + app.bgHighResSeg = app.stft.numSegments; + app.loadingProgress = 1.0f; + app.stftComputed = true; + app.highResFinished = true; + app.bgFinished = true; + app.isBgProcessing = false; + app.loadingPhase = 0; + SaveToCache(); +#else if (app.loadingPhase == 0) { // Initialize STFT once ComputeSTFTInit(&app.signal, &app.stft, app.fftSize); @@ -802,6 +856,7 @@ int main(int argc, char* argv[]) // Save the overview result to cache (will be overwritten when full-res completes) SaveToCache(); } +#endif // __EMSCRIPTEN__ } // Loading overlay (drawn during STFT computation) diff --git a/src/ui.c b/src/ui.c index 575634e..c1980dc 100644 --- a/src/ui.c +++ b/src/ui.c @@ -318,8 +318,9 @@ void ExportPNG(const SpectrogramApp* spa, const char* dirPath) } if (ExportImage(region, path)) { + Platform_OfferFileToUser(path); // desktop: no-op; web: browser download snprintf((char*)spa->exportMessage, sizeof(spa->exportMessage), - "Exported: %dx%d %.40s", region.width, region.height, path); + "Exported: %dx%d %.40s", region.width, region.height, GetFileName(path)); } else { snprintf((char*)spa->exportMessage, sizeof(spa->exportMessage), "Export failed"); }