wasm support
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/build/
|
||||
/build_arch/
|
||||
/build_wasm/
|
||||
|
||||
163
CMakeLists.txt
163
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
|
||||
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)
|
||||
# Link math library and dl on Unix (dl needed by miniaudio for backend loading)
|
||||
if(UNIX)
|
||||
target_link_libraries(baudline PRIVATE m dl)
|
||||
endif()
|
||||
|
||||
endif()
|
||||
|
||||
10
CMakeLists_wasm.cmake
Normal file
10
CMakeLists_wasm.cmake
Normal 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
18
build_wasm.sh
Executable 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"
|
||||
@@ -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_) {
|
||||
if (format_ == InputFormat::WAV) {
|
||||
#ifndef __EMSCRIPTEN__
|
||||
if (sndFile_)
|
||||
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()) {
|
||||
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) {
|
||||
if (format_ == InputFormat::WAV) {
|
||||
#ifndef __EMSCRIPTEN__
|
||||
if (sfInfo_.samplerate > 0)
|
||||
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) {
|
||||
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<size_t>(got) : 0;
|
||||
#else
|
||||
return wavReader_.readFloat(buffer, frames);
|
||||
#endif
|
||||
}
|
||||
|
||||
size_t FileSource::readRawFloat32(float* buffer, size_t frames) {
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
#include "audio/AudioSource.h"
|
||||
#include "core/Types.h"
|
||||
#ifndef __EMSCRIPTEN__
|
||||
#include <sndfile.h>
|
||||
#else
|
||||
#include "audio/WavReader.h"
|
||||
#endif
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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_;
|
||||
|
||||
141
src/audio/WavReader.cpp
Normal file
141
src/audio/WavReader.cpp
Normal 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
41
src/audio/WavReader.h
Normal 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
|
||||
14
src/main.cpp
14
src/main.cpp
@@ -1,15 +1,27 @@
|
||||
#include "ui/Application.h"
|
||||
#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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
#include <imgui_impl_sdl2.h>
|
||||
#include <imgui_impl_opengl3.h>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#include <GLES2/gl2.h>
|
||||
#else
|
||||
#include <GL/gl.h>
|
||||
#endif
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
@@ -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,8 +125,7 @@ bool Application::init(int argc, char** argv) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void Application::run() {
|
||||
while (running_) {
|
||||
void Application::mainLoopStep() {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
ImGui_ImplSDL2_ProcessEvent(&event);
|
||||
@@ -119,7 +133,9 @@ void Application::run() {
|
||||
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,
|
||||
@@ -135,7 +151,22 @@ void Application::run() {
|
||||
processAudio();
|
||||
|
||||
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() {
|
||||
@@ -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_);
|
||||
|
||||
@@ -75,6 +75,7 @@ public:
|
||||
|
||||
bool init(int argc, char** argv);
|
||||
void run();
|
||||
void mainLoopStep(); // single iteration (public for Emscripten callback)
|
||||
void shutdown();
|
||||
|
||||
private:
|
||||
|
||||
65
web/shell.html
Normal file
65
web/shell.html
Normal 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>
|
||||
Reference in New Issue
Block a user