Add web build support with custom shell HTML

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-04-12 21:28:18 -07:00
parent ecbbba36db
commit da37cecb59
3 changed files with 310 additions and 51 deletions
Executable
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
# Build spectrogram viewer for the web using Emscripten
# Usage: ./build_web.sh [debug|release]
set -e
# Get script directory (project root)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
BUILD_TYPE=${1:-release}
RAYLIB_DIR="$SCRIPT_DIR/build/external/raylib-master"
SRC_DIR="$SCRIPT_DIR/src"
BUILD_DIR="$SCRIPT_DIR/bin/web"
RAYLIB_BUILD="$BUILD_DIR/raylib_build"
OUTPUT="$BUILD_DIR/rspektrum.html"
# Create build directories
mkdir -p "$BUILD_DIR" "$RAYLIB_BUILD"
echo "=== Building Spectrogram Viewer for Web ($BUILD_TYPE) ==="
echo "Raylib dir: $RAYLIB_DIR"
# Common compile flags for raylib
RAYLIB_CFLAGS="-Os -Wall -DPLATFORM_WEB -DGRAPHICS_API_OPENGL_ES2"
if [ "$BUILD_TYPE" = "debug" ]; then
RAYLIB_CFLAGS="-g -O0 -Wall -DPLATFORM_WEB -DGRAPHICS_API_OPENGL_ES2"
fi
echo "=== Step 1: Compiling raylib for web ==="
# Compile raylib source files
emcc -c "$RAYLIB_DIR/src/rcore.c" $RAYLIB_CFLAGS -o "$RAYLIB_BUILD/rcore.o"
emcc -c "$RAYLIB_DIR/src/rshapes.c" $RAYLIB_CFLAGS -o "$RAYLIB_BUILD/rshapes.o"
emcc -c "$RAYLIB_DIR/src/rtextures.c" $RAYLIB_CFLAGS -o "$RAYLIB_BUILD/rtextures.o"
emcc -c "$RAYLIB_DIR/src/rtext.c" $RAYLIB_CFLAGS -o "$RAYLIB_BUILD/rtext.o"
emcc -c "$RAYLIB_DIR/src/rmodels.c" $RAYLIB_CFLAGS -o "$RAYLIB_BUILD/rmodels.o"
emcc -c "$RAYLIB_DIR/src/raudio.c" $RAYLIB_CFLAGS -o "$RAYLIB_BUILD/raudio.o"
# Create static library
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 ==="
cd "$SCRIPT_DIR"
# Linker flags
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"
if [ "$BUILD_TYPE" = "debug" ]; then
LDFLAGS="$LDFLAGS -g -O0 -s ASSERTIONS=1"
else
LDFLAGS="$LDFLAGS -O2"
fi
# Compile and link
emcc -o "$OUTPUT" \
"$SRC_DIR/spectrogram.c" \
-I"$RAYLIB_DIR/src" \
-I"$RAYLIB_DIR/src/external" \
-I"$RAYLIB_DIR/src/external/glfw/include" \
-Iinclude \
-DPLATFORM_WEB \
"$RAYLIB_BUILD/libraylib.a" \
$LDFLAGS
echo ""
echo "=== Build complete ==="
echo "Output: $OUTPUT"
echo ""
echo "To test locally, run:"
echo " cd $BUILD_DIR && python3 -m http.server 8080"
echo "Then open http://localhost:8080/rspektrum.html in your browser"
+169 -36
View File
@@ -96,6 +96,11 @@ typedef struct {
float freqSelectionStart;
float freqSelectionEnd;
bool isFreqSelecting;
Vector2 selectStartPos; // For minimum drag distance check
bool isDraggingSelection; // Dragging existing selection box
Vector2 dragSelectionStartPos; // Mouse position when started dragging selection
float dragSelectionTimeStart; // Selection start time when dragging
float dragSelectionFreqStart; // Selection freq start when dragging
// Viewport/zoom controls
float viewStart; // 0-1, start of visible time region
@@ -941,26 +946,66 @@ static void DrawSidebar(void);
static void DrawSelection(Rectangle bounds)
{
Color overlayColor = Fade(BLACK, 0.5f);
float selStartX = bounds.x + app.timeSelectionStart * bounds.width;
float selEndX = bounds.x + app.timeSelectionEnd * bounds.width;
// Only draw if selection is not full range AND not currently dragging
bool hasSelection = (app.timeSelectionStart > 0.001f || app.timeSelectionEnd < 0.999f ||
app.freqSelectionStart > 0.001f || app.freqSelectionEnd < 0.999f);
if (!hasSelection || app.isTimeSelecting) return; // Don't draw overlay while dragging
if (selStartX > bounds.x) DrawRectangle(bounds.x, bounds.y, selStartX - bounds.x, bounds.height, overlayColor);
if (selEndX < bounds.x + bounds.width) DrawRectangle(selEndX, bounds.y, bounds.x + bounds.width - selEndX, bounds.height, overlayColor);
Color overlayColor = Fade(BLACK, 0.25f); // Lighter overlay
float selStartY = bounds.y + bounds.height - app.freqSelectionEnd * bounds.height;
float selEndY = bounds.y + bounds.height - app.freqSelectionStart * bounds.height;
// Convert signal coordinates to viewport coordinates
float viewWidth = app.viewEnd - app.viewStart;
float freqWidth = app.freqViewEnd - app.freqViewStart;
if (selStartY > bounds.y) DrawRectangle(selStartX, bounds.y, selEndX - selStartX, selStartY - bounds.y, overlayColor);
if (selEndY < bounds.y + bounds.height) DrawRectangle(selStartX, selEndY, selEndX - selStartX, bounds.y + bounds.height - selEndY, overlayColor);
float selStartX = bounds.x + ((app.timeSelectionStart - app.viewStart) / viewWidth) * bounds.width;
float selEndX = bounds.x + ((app.timeSelectionEnd - app.viewStart) / viewWidth) * bounds.width;
float selStartY = bounds.y + bounds.height - ((app.freqSelectionEnd - app.freqViewStart) / freqWidth) * bounds.height;
float selEndY = bounds.y + bounds.height - ((app.freqSelectionStart - app.freqViewStart) / freqWidth) * bounds.height;
DrawLineV((Vector2){ selStartX, bounds.y }, (Vector2){ selStartX, bounds.y + bounds.height }, YELLOW);
DrawLineV((Vector2){ selEndX, bounds.y }, (Vector2){ selEndX, bounds.y + bounds.height }, YELLOW);
// Clamp to viewport bounds
selStartX = fmaxf(bounds.x, fminf(bounds.x + bounds.width, selStartX));
selEndX = fmaxf(bounds.x, fminf(bounds.x + bounds.width, selEndX));
selStartY = fmaxf(bounds.y, fminf(bounds.y + bounds.height, selStartY));
selEndY = fmaxf(bounds.y, fminf(bounds.y + bounds.height, selEndY));
if (app.freqSelectionStart > 0.0f || app.freqSelectionEnd < 1.0f) {
DrawLineV((Vector2){ selStartX, selStartY }, (Vector2){ selEndX, selStartY }, CYAN);
DrawLineV((Vector2){ selStartX, selEndY }, (Vector2){ selEndX, selEndY }, CYAN);
// Draw overlay outside the selection box
DrawRectangle(bounds.x, bounds.y, selStartX - bounds.x, bounds.height, overlayColor);
DrawRectangle(selEndX, bounds.y, bounds.x + bounds.width - selEndX, bounds.height, overlayColor);
DrawRectangle(selStartX, bounds.y, selEndX - selStartX, selStartY - bounds.y, overlayColor);
DrawRectangle(selStartX, selEndY, selEndX - selStartX, bounds.y + bounds.height - selEndY, overlayColor);
// Draw selection box border
DrawRectangleLinesEx((Rectangle){ selStartX, selStartY, selEndX - selStartX, selEndY - selStartY }, 2, YELLOW);
}
static void DrawSelectionDrag(Rectangle bounds)
{
// Draw bounding box while dragging (no overlay)
if ((!app.isTimeSelecting && !app.isFreqSelecting && !app.isDraggingSelection) ||
(app.isDraggingSelection && !IsMouseButtonDown(MOUSE_LEFT_BUTTON))) return;
// Convert signal coordinates to viewport coordinates
float viewWidth = app.viewEnd - app.viewStart;
float freqWidth = app.freqViewEnd - app.freqViewStart;
float selStartX = bounds.x + ((app.timeSelectionStart - app.viewStart) / viewWidth) * bounds.width;
float selEndX = bounds.x + ((app.timeSelectionEnd - app.viewStart) / viewWidth) * bounds.width;
float selStartY = bounds.y + bounds.height - ((app.freqSelectionEnd - app.freqViewStart) / freqWidth) * bounds.height;
float selEndY = bounds.y + bounds.height - ((app.freqSelectionStart - app.freqViewStart) / freqWidth) * bounds.height;
// Clamp to viewport bounds
selStartX = fmaxf(bounds.x, fminf(bounds.x + bounds.width, selStartX));
selEndX = fmaxf(bounds.x, fminf(bounds.x + bounds.width, selEndX));
selStartY = fmaxf(bounds.y, fminf(bounds.y + bounds.height, selStartY));
selEndY = fmaxf(bounds.y, fminf(bounds.y + bounds.height, selEndY));
// Normalize coordinates for drawing
float x = selStartX < selEndX ? selStartX : selEndX;
float w = fabsf(selEndX - selStartX);
float y = selStartY < selEndY ? selStartY : selEndY;
float h = fabsf(selEndY - selStartY);
DrawRectangleLinesEx((Rectangle){ x, y, w, h }, 2, YELLOW);
}
static void DrawSidebar(void)
@@ -1139,7 +1184,7 @@ static bool UpdateSlider(Rectangle bounds, float* value)
int main(int argc, char* argv[])
{
SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_HIGHDPI);
SetConfigFlags(FLAG_VSYNC_HINT);
InitWindow(1280, 800, "Spectrogram Viewer");
SetTargetFPS(60);
SetTraceLogLevel(LOG_WARNING); // Suppress INFO texture logs
@@ -1227,6 +1272,11 @@ int main(int argc, char* argv[])
}
}
// Handle window resize
if (IsWindowResized()) {
app.visibleTextureValid = false;
}
// View controls
if (app.loaded && !app.showFileBrowser) {
// Spectrogram area fills remaining window space
@@ -1357,7 +1407,7 @@ int main(int argc, char* argv[])
}
}
// Selection (only when mouse is in spectrogram area, not sidebar)
// Selection: box select with LMB drag, right-click to clear
float selSidebarWidth = 320;
float selLabelHeight = 15;
float selScrollbarHeight = 18;
@@ -1374,46 +1424,128 @@ int main(int argc, char* argv[])
};
Vector2 mousePos = GetMousePosition();
if (app.loaded && !app.showFileBrowser && mousePos.x > 385 && CheckCollisionPointRec(mousePos, selBounds)) {
bool shiftHeld = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
// Right-click clears selection
if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) && CheckCollisionPointRec(mousePos, selBounds)) {
app.timeSelectionStart = 0.0f;
app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f;
app.freqSelectionEnd = 1.0f;
}
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !app.isPanning) {
if (shiftHeld) {
app.isFreqSelecting = true;
float t = 1.0f - (mousePos.y - selBounds.y) / selBounds.height;
app.freqSelectionStart = Clamp(t, 0.0f, 1.0f);
app.freqSelectionEnd = app.freqSelectionStart;
} else if (!IsKeyDown(KEY_LEFT_ALT) && !IsKeyDown(KEY_RIGHT_ALT)) {
// Check if click is inside existing selection (for dragging)
bool hasSelection = (app.timeSelectionStart > 0.001f || app.timeSelectionEnd < 0.999f ||
app.freqSelectionStart > 0.001f || app.freqSelectionEnd < 0.999f);
bool clickInsideSelection = false;
bool hoverInsideSelection = false;
if (hasSelection && CheckCollisionPointRec(mousePos, selBounds)) {
// Convert mouse position to signal coordinates
float viewWidth = app.viewEnd - app.viewStart;
float freqWidth = app.freqViewEnd - app.freqViewStart;
float mouseTime = app.viewStart + ((mousePos.x - selBounds.x) / selBounds.width) * viewWidth;
float mouseFreq = app.freqViewStart + (1.0f - (mousePos.y - selBounds.y) / selBounds.height) * freqWidth;
if (mouseTime >= app.timeSelectionStart && mouseTime <= app.timeSelectionEnd &&
mouseFreq >= app.freqSelectionStart && mouseFreq <= app.freqSelectionEnd) {
hoverInsideSelection = true;
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
clickInsideSelection = true;
}
}
}
// Set cursor based on context
if (app.isDraggingSelection) {
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); // 4-way arrow while dragging
} else if (hoverInsideSelection) {
SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); // Pointing hand on hover
} else {
SetMouseCursor(MOUSE_CURSOR_DEFAULT); // Normal arrow
}
// LMB drag = box select (time + frequency) OR drag existing selection
if (app.loaded && !app.showFileBrowser && CheckCollisionPointRec(mousePos, selBounds)) {
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
if (clickInsideSelection) {
// Start dragging existing selection
app.isDraggingSelection = true;
app.dragSelectionStartPos = mousePos;
app.dragSelectionTimeStart = app.timeSelectionStart;
app.dragSelectionFreqStart = app.freqSelectionStart;
} else {
// Start new box selection
app.isTimeSelecting = true;
app.timeSelectionStart = Clamp((mousePos.x - selBounds.x) / selBounds.width, 0.0f, 1.0f);
app.isFreqSelecting = true;
app.selectStartPos = mousePos;
// Convert screen position to signal coordinates (accounting for zoom)
float viewportT = (mousePos.x - selBounds.x) / selBounds.width;
float viewportF = 1.0f - (mousePos.y - selBounds.y) / selBounds.height;
app.timeSelectionStart = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f);
app.timeSelectionEnd = app.timeSelectionStart;
app.freqSelectionStart = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f);
app.freqSelectionEnd = app.freqSelectionStart;
}
}
if (app.isTimeSelecting && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
app.timeSelectionEnd = Clamp((mousePos.x - selBounds.x) / selBounds.width, 0.0f, 1.0f);
// Dragging existing selection
if (app.isDraggingSelection && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
float viewWidth = app.viewEnd - app.viewStart;
float freqWidth = app.freqViewEnd - app.freqViewStart;
float dx = (mousePos.x - app.dragSelectionStartPos.x) / selBounds.width;
float dy = (mousePos.y - app.dragSelectionStartPos.y) / selBounds.height;
float timeShift = dx * viewWidth;
float freqShift = -dy * freqWidth; // Y is inverted
float timeWidth = app.timeSelectionEnd - app.timeSelectionStart;
float freqHeight = app.freqSelectionEnd - app.freqSelectionStart;
app.timeSelectionStart = Clamp(app.dragSelectionTimeStart + timeShift, 0.0f, 1.0f - timeWidth);
app.timeSelectionEnd = app.timeSelectionStart + timeWidth;
app.freqSelectionStart = Clamp(app.dragSelectionFreqStart + freqShift, 0.0f, 1.0f - freqHeight);
app.freqSelectionEnd = app.freqSelectionStart + freqHeight;
}
if (app.isFreqSelecting && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
float t = 1.0f - (mousePos.y - selBounds.y) / selBounds.height;
app.freqSelectionEnd = Clamp(t, 0.0f, 1.0f);
// Creating new box selection
if ((app.isTimeSelecting || app.isFreqSelecting) && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
float viewportT = (mousePos.x - selBounds.x) / selBounds.width;
float viewportF = 1.0f - (mousePos.y - selBounds.y) / selBounds.height;
app.timeSelectionEnd = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f);
app.freqSelectionEnd = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f);
}
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
if (app.isTimeSelecting) {
app.isTimeSelecting = false;
if (app.isDraggingSelection) {
app.isDraggingSelection = false;
} else if (app.isTimeSelecting || app.isFreqSelecting) {
// Check if drag was large enough (minimum 5 pixels)
float dx = mousePos.x - app.selectStartPos.x;
float dy = mousePos.y - app.selectStartPos.y;
float dragDist = sqrtf(dx * dx + dy * dy);
if (dragDist > 5.0f) {
// Normalize so start < end
if (app.timeSelectionEnd < app.timeSelectionStart) {
float tmp = app.timeSelectionStart;
app.timeSelectionStart = app.timeSelectionEnd;
app.timeSelectionEnd = tmp;
}
}
if (app.isFreqSelecting) {
app.isFreqSelecting = false;
if (app.freqSelectionEnd < app.freqSelectionStart) {
float tmp = app.freqSelectionStart;
app.freqSelectionStart = app.freqSelectionEnd;
app.freqSelectionEnd = tmp;
}
} else {
// Drag too small - revert to full range
app.timeSelectionStart = 0.0f;
app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f;
app.freqSelectionEnd = 1.0f;
}
app.isTimeSelecting = false;
app.isFreqSelecting = false;
}
}
}
@@ -1566,6 +1698,7 @@ int main(int argc, char* argv[])
if (app.showGrid) DrawSpectrogramGrid(viewBounds, 10, 8, Fade(GRAY, 0.3f));
DrawSelection(viewBounds);
DrawSelectionDrag(viewBounds);
DrawLabels(viewBounds);
float maxFreq = (float)app.signal.sampleRate / 2.0f;
float freqMin = app.freqViewStart * maxFreq;
+52
View File
@@ -0,0 +1,52 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Spectrogram Viewer</title>
<meta name="viewport" content="width=device-width">
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: #1e1e1e;
}
canvas.emscripten {
width: 100vw;
height: 100vh;
display: block;
border: 0px none;
outline: none;
background-color: #1e1e1e;
}
</style>
</head>
<body>
<canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
<p id="output" />
<script>
var Module = {
print: (function() {
var element = document.getElementById('output');
if (element) element.value = '';
return function(text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.log(text);
if (element) {
element.value += text + "\n";
element.scrollTop = element.scrollHeight;
}
};
})(),
canvas: (function() {
var canvas = document.getElementById('canvas');
return canvas;
})()
};
</script>
{{{ SCRIPT }}}
</body>
</html>