wasm support
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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,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_);
|
||||
|
||||
@@ -75,6 +75,7 @@ public:
|
||||
|
||||
bool init(int argc, char** argv);
|
||||
void run();
|
||||
void mainLoopStep(); // single iteration (public for Emscripten callback)
|
||||
void shutdown();
|
||||
|
||||
private:
|
||||
|
||||
Reference in New Issue
Block a user