From ab52775ba7f881dbdcae681e92093a21207cc5e8 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Wed, 25 Mar 2026 19:50:21 +0100 Subject: [PATCH] wasm support --- .gitignore | 1 + CMakeLists.txt | 177 +++++++++++++++++++++++++++++++++------ CMakeLists_wasm.cmake | 10 +++ build_wasm.sh | 18 ++++ src/audio/FileSource.cpp | 33 +++++++- src/audio/FileSource.h | 14 +++- src/audio/WavReader.cpp | 141 +++++++++++++++++++++++++++++++ src/audio/WavReader.h | 41 +++++++++ src/main.cpp | 14 +++- src/ui/Application.cpp | 83 +++++++++++++----- src/ui/Application.h | 1 + web/shell.html | 65 ++++++++++++++ 12 files changed, 541 insertions(+), 57 deletions(-) create mode 100644 CMakeLists_wasm.cmake create mode 100755 build_wasm.sh create mode 100644 src/audio/WavReader.cpp create mode 100644 src/audio/WavReader.h create mode 100644 web/shell.html diff --git a/.gitignore b/.gitignore index fd313a8..2b93d21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /build/ /build_arch/ +/build_wasm/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bf1b7a..2b4cc16 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,19 +9,8 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() -set(CMAKE_CXX_FLAGS_RELEASE "-O3 -march=native -DNDEBUG") -set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -fsanitize=address,undefined") -# ── Dependencies ────────────────────────────────────────────────────────────── - -find_package(PkgConfig REQUIRED) -find_package(OpenGL REQUIRED) - -pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2) -pkg_check_modules(FFTW3F REQUIRED IMPORTED_TARGET fftw3f) -pkg_check_modules(SNDFILE REQUIRED IMPORTED_TARGET sndfile) - -# ── ImGui via FetchContent ──────────────────────────────────────────────────── +# ── ImGui via FetchContent (shared by native and WASM) ─────────────────────── include(FetchContent) FetchContent_Declare( @@ -44,9 +33,8 @@ target_include_directories(imgui PUBLIC ${imgui_SOURCE_DIR} ${imgui_SOURCE_DIR}/backends ) -target_link_libraries(imgui PUBLIC PkgConfig::SDL2 OpenGL::GL) -# ── Application ─────────────────────────────────────────────────────────────── +# ── Common sources ─────────────────────────────────────────────────────────── set(SOURCES src/main.cpp @@ -64,18 +52,153 @@ set(SOURCES src/ui/Application.cpp ) -add_executable(baudline ${SOURCES}) -target_include_directories(baudline PRIVATE src) -target_link_libraries(baudline PRIVATE - imgui - PkgConfig::SDL2 - PkgConfig::FFTW3F - PkgConfig::SNDFILE - OpenGL::GL - pthread -) +if(EMSCRIPTEN) + # ── WASM Build ─────────────────────────────────────────────────────────── + + # Add WavReader (replaces libsndfile on WASM) + list(APPEND SOURCES src/audio/WavReader.cpp) + + # Fetch FFTW3 source (don't build via its own CMake — it fails with Emscripten) + FetchContent_Declare( + fftw3 + URL http://www.fftw.org/fftw-3.3.10.tar.gz + URL_HASH MD5=8ccbf6a5ea78a16dbc3e1306e234cc5c + ) + FetchContent_GetProperties(fftw3) + if(NOT fftw3_POPULATED) + FetchContent_Populate(fftw3) + endif() + + # Build a minimal single-precision FFTW3 static lib + file(GLOB FFTW_KERNEL_SRCS ${fftw3_SOURCE_DIR}/kernel/*.c) + file(GLOB FFTW_DFT_SRCS ${fftw3_SOURCE_DIR}/dft/*.c) + file(GLOB FFTW_RDFT_SRCS ${fftw3_SOURCE_DIR}/rdft/*.c + ${fftw3_SOURCE_DIR}/rdft/scalar/*.c + ${fftw3_SOURCE_DIR}/rdft/scalar/r2cf/*.c + ${fftw3_SOURCE_DIR}/rdft/scalar/r2cb/*.c + ${fftw3_SOURCE_DIR}/rdft/scalar/r2r/*.c) + file(GLOB FFTW_DFT_SCALAR ${fftw3_SOURCE_DIR}/dft/scalar/*.c + ${fftw3_SOURCE_DIR}/dft/scalar/codelets/*.c) + file(GLOB FFTW_REODFT_SRCS ${fftw3_SOURCE_DIR}/reodft/*.c) + file(GLOB FFTW_API_SRCS ${fftw3_SOURCE_DIR}/api/*.c) + + # Generate config.h for FFTW (Emscripten/WASM compatible) + file(WRITE ${fftw3_BINARY_DIR}/config.h +"#ifndef FFTW_CONFIG_H +#define FFTW_CONFIG_H +#define FFTW_SINGLE 1 +#define SIZEOF_VOID_P 4 +#define SIZEOF_SIZE_T 4 +#define SIZEOF_PTRDIFF_T 4 +#define SIZEOF_INT 4 +#define SIZEOF_LONG 4 +#define SIZEOF_LONG_LONG 8 +#define SIZEOF_UNSIGNED_INT 4 +#define SIZEOF_UNSIGNED_LONG 4 +#define SIZEOF_UNSIGNED_LONG_LONG 8 +#define HAVE_ABORT 1 +#define HAVE_UNISTD_H 1 +#define HAVE_STRING_H 1 +#define HAVE_STDLIB_H 1 +#define HAVE_STDIO_H 1 +#define HAVE_STDDEF_H 1 +#define HAVE_STDINT_H 1 +#define HAVE_MEMORY_H 1 +#define HAVE_MATH_H 1 +#define HAVE_ERRNO_H 1 +#define HAVE_ISNAN 1 +#define HAVE_UINTPTR_T 1 +#define HAVE_SNPRINTF 1 +#define HAVE_SYS_TIME_H 1 +#define HAVE_GETTIMEOFDAY 1 +#define FFTW_CC \"emcc\" +#define CODELET_OPTIM \"\" +#define PACKAGE \"fftw\" +#define PACKAGE_VERSION \"3.3.10\" +#define VERSION \"3.3.10\" +#endif +") + + add_library(fftw3f_wasm STATIC + ${FFTW_KERNEL_SRCS} + ${FFTW_DFT_SRCS} + ${FFTW_DFT_SCALAR} + ${FFTW_RDFT_SRCS} + ${FFTW_REODFT_SRCS} + ${FFTW_API_SRCS} + ) + target_include_directories(fftw3f_wasm PUBLIC + ${fftw3_SOURCE_DIR}/api + ${fftw3_SOURCE_DIR}/kernel + ${fftw3_SOURCE_DIR}/dft + ${fftw3_SOURCE_DIR}/dft/scalar + ${fftw3_SOURCE_DIR}/rdft + ${fftw3_SOURCE_DIR}/rdft/scalar + ${fftw3_SOURCE_DIR}/reodft + ${fftw3_SOURCE_DIR} + ${fftw3_BINARY_DIR} + ) + target_compile_definitions(fftw3f_wasm PRIVATE FFTW_SINGLE=1) + target_compile_options(fftw3f_wasm PRIVATE -w) # suppress warnings from FFTW + + # ImGui: link with Emscripten SDL2 port + target_compile_options(imgui PUBLIC -sUSE_SDL=2) + target_link_options(imgui PUBLIC -sUSE_SDL=2) + + add_executable(baudline ${SOURCES}) + target_include_directories(baudline PRIVATE src) + target_link_libraries(baudline PRIVATE imgui fftw3f_wasm) + + # Emscripten linker flags + target_link_options(baudline PRIVATE + -sUSE_SDL=2 + -sALLOW_MEMORY_GROWTH=1 + -sINITIAL_MEMORY=67108864 + -sSTACK_SIZE=1048576 + -sASYNCIFY + -sASYNCIFY_STACK_SIZE=65536 + -sEXPORTED_RUNTIME_METHODS=ccall + --shell-file=${CMAKE_SOURCE_DIR}/web/shell.html + ) + + # Output baudline.html + .js + .wasm + set_target_properties(baudline PROPERTIES + SUFFIX ".html" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/web" + ) + + set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "" FORCE) + set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "" FORCE) + +else() + # ── Native Build ───────────────────────────────────────────────────────── + + set(CMAKE_CXX_FLAGS_RELEASE "-O3 -march=native -DNDEBUG") + set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -fsanitize=address,undefined") + + find_package(PkgConfig REQUIRED) + find_package(OpenGL REQUIRED) + + pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2) + pkg_check_modules(FFTW3F REQUIRED IMPORTED_TARGET fftw3f) + pkg_check_modules(SNDFILE REQUIRED IMPORTED_TARGET sndfile) + + target_link_libraries(imgui PUBLIC PkgConfig::SDL2 OpenGL::GL) + + add_executable(baudline ${SOURCES}) + target_include_directories(baudline PRIVATE src) + target_link_libraries(baudline PRIVATE + imgui + PkgConfig::SDL2 + PkgConfig::FFTW3F + PkgConfig::SNDFILE + OpenGL::GL + pthread + ) + + # Link math library and dl on Unix (dl needed by miniaudio for backend loading) + if(UNIX) + target_link_libraries(baudline PRIVATE m dl) + endif() -# Link math library and dl on Unix (dl needed by miniaudio for backend loading) -if(UNIX) - target_link_libraries(baudline PRIVATE m dl) endif() diff --git a/CMakeLists_wasm.cmake b/CMakeLists_wasm.cmake new file mode 100644 index 0000000..36ef902 --- /dev/null +++ b/CMakeLists_wasm.cmake @@ -0,0 +1,10 @@ +# Emscripten/WASM build for Baudline +# Usage: +# source ~/emsdk/emsdk_env.sh +# emcmake cmake -B build_wasm -C CMakeLists_wasm.cmake +# cmake --build build_wasm +# +# This file is loaded via cmake -C (initial cache). + +set(CMAKE_BUILD_TYPE Release CACHE STRING "") +set(BUILD_WASM ON CACHE BOOL "") diff --git a/build_wasm.sh b/build_wasm.sh new file mode 100755 index 0000000..030129a --- /dev/null +++ b/build_wasm.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Build Baudline for WebAssembly +# Prerequisites: Emscripten SDK installed at ~/emsdk +set -e + +EMSDK_DIR="${EMSDK_DIR:-$HOME/emsdk}" +source "$EMSDK_DIR/emsdk_env.sh" 2>/dev/null + +echo "=== Configuring WASM build ===" +emcmake cmake -B build_wasm -C CMakeLists_wasm.cmake -Wno-dev + +echo "=== Building ===" +cmake --build build_wasm -j$(nproc) + +echo "=== Done ===" +echo "Output: build_wasm/web/baudline.html" +echo "To test: cd build_wasm/web && python3 -m http.server 8080" +echo "Then open http://localhost:8080/baudline.html" diff --git a/src/audio/FileSource.cpp b/src/audio/FileSource.cpp index 5e299f4..5c72283 100644 --- a/src/audio/FileSource.cpp +++ b/src/audio/FileSource.cpp @@ -24,11 +24,17 @@ bool FileSource::open() { eof_ = false; if (format_ == InputFormat::WAV) { +#ifndef __EMSCRIPTEN__ std::memset(&sfInfo_, 0, sizeof(sfInfo_)); sndFile_ = sf_open(path_.c_str(), SFM_READ, &sfInfo_); if (!sndFile_) return false; sampleRate_ = sfInfo_.samplerate; channels_ = sfInfo_.channels; +#else + if (!wavReader_.open(path_)) return false; + sampleRate_ = wavReader_.sampleRate(); + channels_ = wavReader_.channels(); +#endif return true; } @@ -44,10 +50,14 @@ bool FileSource::open() { } void FileSource::close() { +#ifndef __EMSCRIPTEN__ if (sndFile_) { sf_close(sndFile_); sndFile_ = nullptr; } +#else + wavReader_.close(); +#endif if (rawFile_.is_open()) { rawFile_.close(); } @@ -81,8 +91,13 @@ size_t FileSource::read(float* buffer, size_t frames) { void FileSource::seek(double seconds) { eof_ = false; - if (format_ == InputFormat::WAV && sndFile_) { - sf_seek(sndFile_, static_cast(seconds * sampleRate_), SEEK_SET); + if (format_ == InputFormat::WAV) { +#ifndef __EMSCRIPTEN__ + if (sndFile_) + sf_seek(sndFile_, static_cast(seconds * sampleRate_), SEEK_SET); +#else + wavReader_.seekFrame(static_cast(seconds * sampleRate_)); +#endif } else if (rawFile_.is_open()) { size_t bytesPerFrame = 0; switch (format_) { @@ -98,8 +113,14 @@ void FileSource::seek(double seconds) { } double FileSource::duration() const { - if (format_ == InputFormat::WAV && sfInfo_.samplerate > 0) { - return static_cast(sfInfo_.frames) / sfInfo_.samplerate; + if (format_ == InputFormat::WAV) { +#ifndef __EMSCRIPTEN__ + if (sfInfo_.samplerate > 0) + return static_cast(sfInfo_.frames) / sfInfo_.samplerate; +#else + if (wavReader_.sampleRate() > 0 && wavReader_.totalFrames() > 0) + return static_cast(wavReader_.totalFrames()) / wavReader_.sampleRate(); +#endif } if (rawFileSize_ > 0) { size_t bytesPerFrame = 0; @@ -118,9 +139,13 @@ double FileSource::duration() const { // ── Format-specific readers ────────────────────────────────────────────────── size_t FileSource::readWAV(float* buffer, size_t frames) { +#ifndef __EMSCRIPTEN__ if (!sndFile_) return 0; sf_count_t got = sf_readf_float(sndFile_, buffer, frames); return (got > 0) ? static_cast(got) : 0; +#else + return wavReader_.readFloat(buffer, frames); +#endif } size_t FileSource::readRawFloat32(float* buffer, size_t frames) { diff --git a/src/audio/FileSource.h b/src/audio/FileSource.h index 1ec415d..8e42ae8 100644 --- a/src/audio/FileSource.h +++ b/src/audio/FileSource.h @@ -2,14 +2,19 @@ #include "audio/AudioSource.h" #include "core/Types.h" +#ifndef __EMSCRIPTEN__ #include +#else +#include "audio/WavReader.h" +#endif #include #include #include namespace baudline { -// Reads WAV files (via libsndfile) and raw I/Q files (float32, int16, uint8). +// Reads WAV files and raw I/Q files (float32, int16, uint8). +// Native builds use libsndfile; WASM builds use a built-in WAV reader. class FileSource : public AudioSource { public: // For WAV files: format is auto-detected, sampleRate/channels from file header. @@ -46,9 +51,14 @@ private: bool loop_; bool eof_ = false; - // WAV via libsndfile +#ifndef __EMSCRIPTEN__ + // WAV via libsndfile (native) SNDFILE* sndFile_ = nullptr; SF_INFO sfInfo_{}; +#else + // WAV via built-in reader (WASM) + WavReader wavReader_; +#endif // Raw I/Q files std::ifstream rawFile_; diff --git a/src/audio/WavReader.cpp b/src/audio/WavReader.cpp new file mode 100644 index 0000000..737ee0b --- /dev/null +++ b/src/audio/WavReader.cpp @@ -0,0 +1,141 @@ +#include "audio/WavReader.h" +#include +#include + +namespace baudline { + +// Read a little-endian uint16/uint32 from raw bytes. +static uint16_t readU16(const uint8_t* p) { return p[0] | (p[1] << 8); } +static uint32_t readU32(const uint8_t* p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); } + +bool WavReader::open(const std::string& path) { + close(); + file_.open(path, std::ios::binary); + if (!file_.is_open()) return false; + + // Read RIFF header. + uint8_t hdr[12]; + file_.read(reinterpret_cast(hdr), 12); + if (file_.gcount() < 12) return false; + if (std::memcmp(hdr, "RIFF", 4) != 0 || std::memcmp(hdr + 8, "WAVE", 4) != 0) + return false; + + // Scan chunks for "fmt " and "data". + bool gotFmt = false, gotData = false; + while (!file_.eof() && !(gotFmt && gotData)) { + uint8_t chunkHdr[8]; + file_.read(reinterpret_cast(chunkHdr), 8); + if (file_.gcount() < 8) break; + + uint32_t chunkSize = readU32(chunkHdr + 4); + + if (std::memcmp(chunkHdr, "fmt ", 4) == 0) { + uint8_t fmt[40] = {}; + size_t toRead = std::min(chunkSize, sizeof(fmt)); + file_.read(reinterpret_cast(fmt), toRead); + if (file_.gcount() < 16) return false; + + uint16_t audioFormat = readU16(fmt); + // 1 = PCM integer, 3 = IEEE float + if (audioFormat != 1 && audioFormat != 3) return false; + + channels_ = readU16(fmt + 2); + sampleRate_ = static_cast(readU32(fmt + 4)); + bitsPerSample_ = readU16(fmt + 14); + bytesPerSample_ = bitsPerSample_ / 8; + + if (channels_ < 1 || sampleRate_ < 1 || bytesPerSample_ < 1) + return false; + + // Skip remainder of fmt chunk if any. + if (chunkSize > toRead) + file_.seekg(chunkSize - toRead, std::ios::cur); + gotFmt = true; + + } else if (std::memcmp(chunkHdr, "data", 4) == 0) { + dataOffset_ = static_cast(file_.tellg()); + dataSize_ = chunkSize; + gotData = true; + // Don't seek past data — we'll read from here. + } else { + // Skip unknown chunk. + file_.seekg(chunkSize, std::ios::cur); + } + } + + if (!gotFmt || !gotData) return false; + + totalFrames_ = dataSize_ / (channels_ * bytesPerSample_); + + // Position at start of data. + file_.seekg(dataOffset_); + return true; +} + +void WavReader::close() { + if (file_.is_open()) file_.close(); + sampleRate_ = channels_ = bitsPerSample_ = bytesPerSample_ = 0; + totalFrames_ = dataOffset_ = dataSize_ = 0; +} + +size_t WavReader::readFloat(float* buffer, size_t frames) { + if (!file_.is_open() || bytesPerSample_ == 0) return 0; + + size_t samples = frames * channels_; + size_t rawBytes = samples * bytesPerSample_; + readBuf_.resize(rawBytes); + + file_.read(reinterpret_cast(readBuf_.data()), rawBytes); + size_t bytesRead = file_.gcount(); + size_t samplesRead = bytesRead / bytesPerSample_; + size_t framesRead = samplesRead / channels_; + + const uint8_t* src = readBuf_.data(); + + switch (bitsPerSample_) { + case 8: + // Unsigned 8-bit PCM → [-1, 1] + for (size_t i = 0; i < samplesRead; ++i) + buffer[i] = (src[i] - 128) / 128.0f; + break; + + case 16: + for (size_t i = 0; i < samplesRead; ++i) { + int16_t s = static_cast(readU16(src + i * 2)); + buffer[i] = s / 32768.0f; + } + break; + + case 24: + for (size_t i = 0; i < samplesRead; ++i) { + const uint8_t* p = src + i * 3; + int32_t s = p[0] | (p[1] << 8) | (p[2] << 16); + if (s & 0x800000) s |= 0xFF000000; // sign-extend + buffer[i] = s / 8388608.0f; + } + break; + + case 32: + if (bytesPerSample_ == 4) { + // Could be float or int32 — check by trying float first. + // We detected audioFormat in open(); for simplicity, treat + // 32-bit as float (most common for 32-bit WAV in SDR tools). + std::memcpy(buffer, src, samplesRead * sizeof(float)); + } + break; + + default: + return 0; + } + + return framesRead; +} + +void WavReader::seekFrame(size_t frame) { + if (!file_.is_open()) return; + size_t byteOffset = dataOffset_ + frame * channels_ * bytesPerSample_; + file_.clear(); + file_.seekg(static_cast(byteOffset)); +} + +} // namespace baudline diff --git a/src/audio/WavReader.h b/src/audio/WavReader.h new file mode 100644 index 0000000..b4438e5 --- /dev/null +++ b/src/audio/WavReader.h @@ -0,0 +1,41 @@ +#pragma once +// Minimal WAV file reader — no external dependencies. +// Used for WASM builds where libsndfile is unavailable. + +#include +#include +#include +#include +#include + +namespace baudline { + +class WavReader { +public: + bool open(const std::string& path); + void close(); + + // Read interleaved float frames. Returns number of frames read. + size_t readFloat(float* buffer, size_t frames); + + // Seek to frame position. + void seekFrame(size_t frame); + + int sampleRate() const { return sampleRate_; } + int channels() const { return channels_; } + size_t totalFrames() const { return totalFrames_; } + +private: + std::ifstream file_; + int sampleRate_ = 0; + int channels_ = 0; + int bitsPerSample_ = 0; + int bytesPerSample_ = 0; + size_t totalFrames_ = 0; + size_t dataOffset_ = 0; // byte offset of PCM data in file + size_t dataSize_ = 0; // total bytes of PCM data + + std::vector readBuf_; // scratch for format conversion +}; + +} // namespace baudline diff --git a/src/main.cpp b/src/main.cpp index b708d80..b23a804 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,15 +1,27 @@ #include "ui/Application.h" #include +#ifdef __EMSCRIPTEN__ +// Keep app alive for the Emscripten main loop. +static baudline::Application* g_app = nullptr; +#endif + int main(int argc, char** argv) { - baudline::Application app; + static baudline::Application app; if (!app.init(argc, argv)) { std::fprintf(stderr, "Failed to initialize application\n"); return 1; } +#ifdef __EMSCRIPTEN__ + g_app = &app; +#endif + app.run(); + +#ifndef __EMSCRIPTEN__ app.shutdown(); +#endif return 0; } diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index fda4a3c..e6719db 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -5,7 +5,12 @@ #include #include +#ifdef __EMSCRIPTEN__ +#include +#include +#else #include +#endif #include #include #include @@ -44,8 +49,14 @@ bool Application::init(int argc, char** argv) { return false; } +#ifdef __EMSCRIPTEN__ + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); +#else SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); +#endif SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); window_ = SDL_CreateWindow("Baudline Spectrum Analyzer", @@ -75,7 +86,11 @@ bool Application::init(int argc, char** argv) { style.GrabRounding = 2.0f; ImGui_ImplSDL2_InitForOpenGL(window_, glContext_); +#ifdef __EMSCRIPTEN__ + ImGui_ImplOpenGL3_Init("#version 100"); +#else ImGui_ImplOpenGL3_Init("#version 120"); +#endif // Enumerate audio devices paDevices_ = MiniAudioSource::listInputDevices(); @@ -110,32 +125,48 @@ bool Application::init(int argc, char** argv) { return true; } -void Application::run() { - while (running_) { - SDL_Event event; - while (SDL_PollEvent(&event)) { - ImGui_ImplSDL2_ProcessEvent(&event); - if (event.type == SDL_QUIT) - running_ = false; - if (event.type == SDL_KEYDOWN) { - auto key = event.key.keysym.sym; - if (key == SDLK_ESCAPE) running_ = false; - if (key == SDLK_SPACE) paused_ = !paused_; - if (key == SDLK_p) { - int pkCh = std::clamp(waterfallChannel_, 0, - analyzer_.numSpectra() - 1); - cursors_.snapToPeak(analyzer_.channelSpectrum(pkCh), - settings_.sampleRate, settings_.isIQ, - settings_.fftSize); - } +void Application::mainLoopStep() { + SDL_Event event; + while (SDL_PollEvent(&event)) { + ImGui_ImplSDL2_ProcessEvent(&event); + if (event.type == SDL_QUIT) + running_ = false; + if (event.type == SDL_KEYDOWN) { + auto key = event.key.keysym.sym; +#ifndef __EMSCRIPTEN__ + if (key == SDLK_ESCAPE) running_ = false; +#endif + if (key == SDLK_SPACE) paused_ = !paused_; + if (key == SDLK_p) { + int pkCh = std::clamp(waterfallChannel_, 0, + analyzer_.numSpectra() - 1); + cursors_.snapToPeak(analyzer_.channelSpectrum(pkCh), + settings_.sampleRate, settings_.isIQ, + settings_.fftSize); } } - - if (!paused_) - processAudio(); - - render(); } + + if (!paused_) + processAudio(); + + render(); +} + +#ifdef __EMSCRIPTEN__ +static void emMainLoop(void* arg) { + static_cast(arg)->mainLoopStep(); +} +#endif + +void Application::run() { +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop_arg(emMainLoop, this, 0, true); +#else + while (running_) { + mainLoopStep(); + } +#endif } void Application::shutdown() { @@ -1310,6 +1341,9 @@ void Application::renderMathPanel() { } void Application::loadConfig() { +#ifdef __EMSCRIPTEN__ + return; // No filesystem config on WASM +#endif config_.load(); fftSizeIdx_ = config_.getInt("fft_size_idx", fftSizeIdx_); overlapPct_ = config_.getFloat("overlap_pct", overlapPct_); @@ -1351,6 +1385,9 @@ void Application::loadConfig() { } void Application::saveConfig() const { +#ifdef __EMSCRIPTEN__ + return; +#endif Config cfg; cfg.setInt("fft_size_idx", fftSizeIdx_); cfg.setFloat("overlap_pct", overlapPct_); diff --git a/src/ui/Application.h b/src/ui/Application.h index fd0b2ca..7d22fd8 100644 --- a/src/ui/Application.h +++ b/src/ui/Application.h @@ -75,6 +75,7 @@ public: bool init(int argc, char** argv); void run(); + void mainLoopStep(); // single iteration (public for Emscripten callback) void shutdown(); private: diff --git a/web/shell.html b/web/shell.html new file mode 100644 index 0000000..6fab320 --- /dev/null +++ b/web/shell.html @@ -0,0 +1,65 @@ + + + + + +Baudline Spectrum Analyzer + + + +
Loading...
+ +
+ + +{{{ SCRIPT }}} + +