wasm support
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/build/
|
/build/
|
||||||
/build_arch/
|
/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)
|
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
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;
|
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) {
|
||||||
|
|||||||
@@ -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
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 "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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_);
|
||||||
|
|||||||
@@ -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
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