feat(web): proper viewport scaling, instant load, and file downloads
Make the Emscripten build behave like the desktop app for loading, window scaling, and exporting. All changes are #ifdef __EMSCRIPTEN__-gated or no-op on desktop, so native behavior is unchanged. Scaling: - Drop FLAG_WINDOW_HIGHDPI on web. The emscripten-GLFW shim forces a fixed pixel canvas style (!important) when HiDPI-aware, overriding the shell's 100vw/100vh CSS so the canvas can't fill the page; raylib's own resize/window callbacks also disagree about dividing by devicePixelRatio, desyncing the framebuffer from the reported screen size. - Sync raylib's window size to window.innerWidth/innerHeight each frame via SetWindowSize (guarded against no-op churn). This keeps screen size, GL viewport, and projection consistent, so the UI fills the viewport and reflows on resize like the desktop window. Loading: - Compute the full-resolution STFT synchronously when a file loads instead of the desktop overview-then-deferred-high-res path, which relied on many main-loop iterations yielding to the browser and appeared to stall partway. - Allow the wasm heap to grow (INITIAL_MEMORY + ALLOW_MEMORY_GROWTH) so longer recordings fit now that everything is computed up front. Exports: - Add Platform_OfferFileToUser(): no-op on desktop (file is already on disk); on web it reads the just-written file from MEMFS and triggers a browser download, then unlinks the temp copy. Wired into PNG and WAV export, which now show just the filename in the status message. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -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"
|
||||
|
||||
+4
-3
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+30
-1
@@ -10,7 +10,8 @@
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <errno.h> /* ENOSYS */
|
||||
#include <errno.h> /* ENOSYS */
|
||||
#include <emscripten.h> /* 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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,23 @@
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/emscripten.h>
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user