wasm support

This commit is contained in:
2026-03-25 19:50:21 +01:00
parent 548db30cdd
commit ab52775ba7
12 changed files with 541 additions and 57 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/build/ /build/
/build_arch/ /build_arch/
/build_wasm/

View File

@@ -9,19 +9,8 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
if(NOT CMAKE_BUILD_TYPE) if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release) set(CMAKE_BUILD_TYPE Release)
endif() endif()
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -march=native -DNDEBUG")
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -fsanitize=address,undefined")
# ── Dependencies ────────────────────────────────────────────────────────────── # ── ImGui via FetchContent (shared by native and WASM) ───────────────────────
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 ────────────────────────────────────────────────────
include(FetchContent) include(FetchContent)
FetchContent_Declare( FetchContent_Declare(
@@ -44,9 +33,8 @@ target_include_directories(imgui PUBLIC
${imgui_SOURCE_DIR} ${imgui_SOURCE_DIR}
${imgui_SOURCE_DIR}/backends ${imgui_SOURCE_DIR}/backends
) )
target_link_libraries(imgui PUBLIC PkgConfig::SDL2 OpenGL::GL)
# ── Application ─────────────────────────────────────────────────────────────── # ── Common sources ───────────────────────────────────────────────────────────
set(SOURCES set(SOURCES
src/main.cpp src/main.cpp
@@ -64,18 +52,153 @@ set(SOURCES
src/ui/Application.cpp src/ui/Application.cpp
) )
add_executable(baudline ${SOURCES}) if(EMSCRIPTEN)
target_include_directories(baudline PRIVATE src) # ── WASM Build ───────────────────────────────────────────────────────────
target_link_libraries(baudline PRIVATE
# 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 imgui
PkgConfig::SDL2 PkgConfig::SDL2
PkgConfig::FFTW3F PkgConfig::FFTW3F
PkgConfig::SNDFILE PkgConfig::SNDFILE
OpenGL::GL OpenGL::GL
pthread pthread
) )
# Link math library and dl on Unix (dl needed by miniaudio for backend loading) # Link math library and dl on Unix (dl needed by miniaudio for backend loading)
if(UNIX) if(UNIX)
target_link_libraries(baudline PRIVATE m dl) target_link_libraries(baudline PRIVATE m dl)
endif()
endif() endif()

10
CMakeLists_wasm.cmake Normal file
View File

@@ -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 "")

18
build_wasm.sh Executable file
View File

@@ -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"

View File

@@ -24,11 +24,17 @@ bool FileSource::open() {
eof_ = false; eof_ = false;
if (format_ == InputFormat::WAV) { if (format_ == InputFormat::WAV) {
#ifndef __EMSCRIPTEN__
std::memset(&sfInfo_, 0, sizeof(sfInfo_)); std::memset(&sfInfo_, 0, sizeof(sfInfo_));
sndFile_ = sf_open(path_.c_str(), SFM_READ, &sfInfo_); sndFile_ = sf_open(path_.c_str(), SFM_READ, &sfInfo_);
if (!sndFile_) return false; if (!sndFile_) return false;
sampleRate_ = sfInfo_.samplerate; sampleRate_ = sfInfo_.samplerate;
channels_ = sfInfo_.channels; channels_ = sfInfo_.channels;
#else
if (!wavReader_.open(path_)) return false;
sampleRate_ = wavReader_.sampleRate();
channels_ = wavReader_.channels();
#endif
return true; return true;
} }
@@ -44,10 +50,14 @@ bool FileSource::open() {
} }
void FileSource::close() { void FileSource::close() {
#ifndef __EMSCRIPTEN__
if (sndFile_) { if (sndFile_) {
sf_close(sndFile_); sf_close(sndFile_);
sndFile_ = nullptr; sndFile_ = nullptr;
} }
#else
wavReader_.close();
#endif
if (rawFile_.is_open()) { if (rawFile_.is_open()) {
rawFile_.close(); rawFile_.close();
} }
@@ -81,8 +91,13 @@ size_t FileSource::read(float* buffer, size_t frames) {
void FileSource::seek(double seconds) { void FileSource::seek(double seconds) {
eof_ = false; eof_ = false;
if (format_ == InputFormat::WAV && sndFile_) { if (format_ == InputFormat::WAV) {
#ifndef __EMSCRIPTEN__
if (sndFile_)
sf_seek(sndFile_, static_cast<sf_count_t>(seconds * sampleRate_), SEEK_SET); sf_seek(sndFile_, static_cast<sf_count_t>(seconds * sampleRate_), SEEK_SET);
#else
wavReader_.seekFrame(static_cast<size_t>(seconds * sampleRate_));
#endif
} else if (rawFile_.is_open()) { } else if (rawFile_.is_open()) {
size_t bytesPerFrame = 0; size_t bytesPerFrame = 0;
switch (format_) { switch (format_) {
@@ -98,8 +113,14 @@ void FileSource::seek(double seconds) {
} }
double FileSource::duration() const { double FileSource::duration() const {
if (format_ == InputFormat::WAV && sfInfo_.samplerate > 0) { if (format_ == InputFormat::WAV) {
#ifndef __EMSCRIPTEN__
if (sfInfo_.samplerate > 0)
return static_cast<double>(sfInfo_.frames) / sfInfo_.samplerate; return static_cast<double>(sfInfo_.frames) / sfInfo_.samplerate;
#else
if (wavReader_.sampleRate() > 0 && wavReader_.totalFrames() > 0)
return static_cast<double>(wavReader_.totalFrames()) / wavReader_.sampleRate();
#endif
} }
if (rawFileSize_ > 0) { if (rawFileSize_ > 0) {
size_t bytesPerFrame = 0; size_t bytesPerFrame = 0;
@@ -118,9 +139,13 @@ double FileSource::duration() const {
// ── Format-specific readers ────────────────────────────────────────────────── // ── Format-specific readers ──────────────────────────────────────────────────
size_t FileSource::readWAV(float* buffer, size_t frames) { size_t FileSource::readWAV(float* buffer, size_t frames) {
#ifndef __EMSCRIPTEN__
if (!sndFile_) return 0; if (!sndFile_) return 0;
sf_count_t got = sf_readf_float(sndFile_, buffer, frames); sf_count_t got = sf_readf_float(sndFile_, buffer, frames);
return (got > 0) ? static_cast<size_t>(got) : 0; return (got > 0) ? static_cast<size_t>(got) : 0;
#else
return wavReader_.readFloat(buffer, frames);
#endif
} }
size_t FileSource::readRawFloat32(float* buffer, size_t frames) { size_t FileSource::readRawFloat32(float* buffer, size_t frames) {

View File

@@ -2,14 +2,19 @@
#include "audio/AudioSource.h" #include "audio/AudioSource.h"
#include "core/Types.h" #include "core/Types.h"
#ifndef __EMSCRIPTEN__
#include <sndfile.h> #include <sndfile.h>
#else
#include "audio/WavReader.h"
#endif
#include <fstream> #include <fstream>
#include <string> #include <string>
#include <vector> #include <vector>
namespace baudline { 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 { class FileSource : public AudioSource {
public: public:
// For WAV files: format is auto-detected, sampleRate/channels from file header. // For WAV files: format is auto-detected, sampleRate/channels from file header.
@@ -46,9 +51,14 @@ private:
bool loop_; bool loop_;
bool eof_ = false; bool eof_ = false;
// WAV via libsndfile #ifndef __EMSCRIPTEN__
// WAV via libsndfile (native)
SNDFILE* sndFile_ = nullptr; SNDFILE* sndFile_ = nullptr;
SF_INFO sfInfo_{}; SF_INFO sfInfo_{};
#else
// WAV via built-in reader (WASM)
WavReader wavReader_;
#endif
// Raw I/Q files // Raw I/Q files
std::ifstream rawFile_; std::ifstream rawFile_;

141
src/audio/WavReader.cpp Normal file
View File

@@ -0,0 +1,141 @@
#include "audio/WavReader.h"
#include <cstring>
#include <algorithm>
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<char*>(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<char*>(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<size_t>(chunkSize, sizeof(fmt));
file_.read(reinterpret_cast<char*>(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<int>(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<size_t>(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<char*>(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<int16_t>(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<std::streamoff>(byteOffset));
}
} // namespace baudline

41
src/audio/WavReader.h Normal file
View File

@@ -0,0 +1,41 @@
#pragma once
// Minimal WAV file reader — no external dependencies.
// Used for WASM builds where libsndfile is unavailable.
#include <cstddef>
#include <cstdint>
#include <fstream>
#include <string>
#include <vector>
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<uint8_t> readBuf_; // scratch for format conversion
};
} // namespace baudline

View File

@@ -1,15 +1,27 @@
#include "ui/Application.h" #include "ui/Application.h"
#include <cstdio> #include <cstdio>
#ifdef __EMSCRIPTEN__
// Keep app alive for the Emscripten main loop.
static baudline::Application* g_app = nullptr;
#endif
int main(int argc, char** argv) { int main(int argc, char** argv) {
baudline::Application app; static baudline::Application app;
if (!app.init(argc, argv)) { if (!app.init(argc, argv)) {
std::fprintf(stderr, "Failed to initialize application\n"); std::fprintf(stderr, "Failed to initialize application\n");
return 1; return 1;
} }
#ifdef __EMSCRIPTEN__
g_app = &app;
#endif
app.run(); app.run();
#ifndef __EMSCRIPTEN__
app.shutdown(); app.shutdown();
#endif
return 0; return 0;
} }

View File

@@ -5,7 +5,12 @@
#include <imgui_impl_sdl2.h> #include <imgui_impl_sdl2.h>
#include <imgui_impl_opengl3.h> #include <imgui_impl_opengl3.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <GLES2/gl2.h>
#else
#include <GL/gl.h> #include <GL/gl.h>
#endif
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <algorithm> #include <algorithm>
@@ -44,8 +49,14 @@ bool Application::init(int argc, char** argv) {
return false; 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_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
#endif
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
window_ = SDL_CreateWindow("Baudline Spectrum Analyzer", window_ = SDL_CreateWindow("Baudline Spectrum Analyzer",
@@ -75,7 +86,11 @@ bool Application::init(int argc, char** argv) {
style.GrabRounding = 2.0f; style.GrabRounding = 2.0f;
ImGui_ImplSDL2_InitForOpenGL(window_, glContext_); ImGui_ImplSDL2_InitForOpenGL(window_, glContext_);
#ifdef __EMSCRIPTEN__
ImGui_ImplOpenGL3_Init("#version 100");
#else
ImGui_ImplOpenGL3_Init("#version 120"); ImGui_ImplOpenGL3_Init("#version 120");
#endif
// Enumerate audio devices // Enumerate audio devices
paDevices_ = MiniAudioSource::listInputDevices(); paDevices_ = MiniAudioSource::listInputDevices();
@@ -110,8 +125,7 @@ bool Application::init(int argc, char** argv) {
return true; return true;
} }
void Application::run() { void Application::mainLoopStep() {
while (running_) {
SDL_Event event; SDL_Event event;
while (SDL_PollEvent(&event)) { while (SDL_PollEvent(&event)) {
ImGui_ImplSDL2_ProcessEvent(&event); ImGui_ImplSDL2_ProcessEvent(&event);
@@ -119,7 +133,9 @@ void Application::run() {
running_ = false; running_ = false;
if (event.type == SDL_KEYDOWN) { if (event.type == SDL_KEYDOWN) {
auto key = event.key.keysym.sym; auto key = event.key.keysym.sym;
#ifndef __EMSCRIPTEN__
if (key == SDLK_ESCAPE) running_ = false; if (key == SDLK_ESCAPE) running_ = false;
#endif
if (key == SDLK_SPACE) paused_ = !paused_; if (key == SDLK_SPACE) paused_ = !paused_;
if (key == SDLK_p) { if (key == SDLK_p) {
int pkCh = std::clamp(waterfallChannel_, 0, int pkCh = std::clamp(waterfallChannel_, 0,
@@ -135,7 +151,22 @@ void Application::run() {
processAudio(); processAudio();
render(); render();
}
#ifdef __EMSCRIPTEN__
static void emMainLoop(void* arg) {
static_cast<Application*>(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() { void Application::shutdown() {
@@ -1310,6 +1341,9 @@ void Application::renderMathPanel() {
} }
void Application::loadConfig() { void Application::loadConfig() {
#ifdef __EMSCRIPTEN__
return; // No filesystem config on WASM
#endif
config_.load(); config_.load();
fftSizeIdx_ = config_.getInt("fft_size_idx", fftSizeIdx_); fftSizeIdx_ = config_.getInt("fft_size_idx", fftSizeIdx_);
overlapPct_ = config_.getFloat("overlap_pct", overlapPct_); overlapPct_ = config_.getFloat("overlap_pct", overlapPct_);
@@ -1351,6 +1385,9 @@ void Application::loadConfig() {
} }
void Application::saveConfig() const { void Application::saveConfig() const {
#ifdef __EMSCRIPTEN__
return;
#endif
Config cfg; Config cfg;
cfg.setInt("fft_size_idx", fftSizeIdx_); cfg.setInt("fft_size_idx", fftSizeIdx_);
cfg.setFloat("overlap_pct", overlapPct_); cfg.setFloat("overlap_pct", overlapPct_);

View File

@@ -75,6 +75,7 @@ public:
bool init(int argc, char** argv); bool init(int argc, char** argv);
void run(); void run();
void mainLoopStep(); // single iteration (public for Emscripten callback)
void shutdown(); void shutdown();
private: private:

65
web/shell.html Normal file
View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Baudline Spectrum Analyzer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #111; }
#canvas {
display: block;
width: 100vw;
height: 100vh;
outline: none;
}
#status {
position: fixed;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
color: #888;
font: 13px monospace;
pointer-events: none;
z-index: 10;
}
#loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #ccc;
font: 18px monospace;
z-index: 20;
}
</style>
</head>
<body>
<div id="loading">Loading...</div>
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
<div id="status"></div>
<script>
var Module = {
canvas: (function() { return document.getElementById('canvas'); })(),
onRuntimeInitialized: function() {
document.getElementById('loading').style.display = 'none';
},
setStatus: function(text) {
document.getElementById('status').textContent = text;
},
// Auto-resize canvas to fill the window.
preRun: [function() {
function resize() {
var canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
}]
};
</script>
{{{ SCRIPT }}}
</body>
</html>