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

View File

@@ -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<sf_count_t>(seconds * sampleRate_), SEEK_SET);
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) {
return static_cast<double>(sfInfo_.frames) / sfInfo_.samplerate;
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) {

View File

@@ -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
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 <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;
}

View File

@@ -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,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<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_);

View File

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