Bound captured PCM to the ASR normalized amplitude range

This commit is contained in:
baketnk committed 2026-09-24 15:14:09 -04:00
1 parent 3190877587
commit 019b8132bb
6 files changed
+51 -5

No files matched your search

+5 -2
View File
@@ -15,8 +15,11 @@ cancellation; this component does not implement focus or delivery policy.
The adapter requires an existing absolute, owner-private `$XDG_RUNTIME_DIR` The adapter requires an existing absolute, owner-private `$XDG_RUNTIME_DIR`
(no symlink at the final component), creates its own 0700 `mkdtemp` directory, (no symlink at the final component), creates its own 0700 `mkdtemp` directory,
and writes only `clip.raw` with `O_EXCL|O_NOFOLLOW`, mode 0600. Clips are and writes only `clip.raw` with `O_EXCL|O_NOFOLLOW`, mode 0600. Clips are
3200..320000 finite float samples, mono 16 kHz, stored as little-endian IEEE 3200..320000 finite float samples in `[-1, 1]`, mono 16 kHz, stored as
float32 (0.2..20 s). Files are unlinked after replies or shutdown, and the little-endian IEEE float32 (0.2..20 s). The microphone adapter saturates finite
capture/resampler overshoot to this range without rescaling the rest of the
clip; NaN/infinity are rejected. `Worker::submit` independently rejects samples
outside this contract before writing a clip or request. Files are unlinked after replies or shutdown, and the
private directory is removed. Private clips are not encrypted against the private directory is removed. Private clips are not encrypted against the
account owner/root; do not use an untrusted runtime directory. The caller account owner/root; do not use an untrusted runtime directory. The caller
should pass a trusted interpreter and script. By default audio/transcripts are should pass a trusted interpreter and script. By default audio/transcripts are
+6 -1
View File
@@ -47,8 +47,13 @@ void Audio::drain() {
if (bytes % sizeof(float)) throw std::runtime_error("Misaligned microphone samples"); if (bytes % sizeof(float)) throw std::runtime_error("Misaligned microphone samples");
if (recording_) { if (recording_) {
const auto retain = std::min(std::size_t(bytes) / sizeof(float), 320000 - pcm_.size()); const auto retain = std::min(std::size_t(bytes) / sizeof(float), 320000 - pcm_.size());
for (std::size_t i = 0; i < retain; ++i) for (std::size_t i = 0; i < retain; ++i) {
if (!std::isfinite(chunk[i])) throw std::runtime_error("Invalid microphone samples"); if (!std::isfinite(chunk[i])) throw std::runtime_error("Invalid microphone samples");
// Floating-point capture/resampling can exceed full scale. The
// ASR API requires normalized PCM; saturate peaks, never scale
// the whole clip or silently turn nonfinite input into speech.
chunk[i] = std::clamp(chunk[i], -1.0f, 1.0f);
}
pcm_.insert(pcm_.end(), chunk.begin(), chunk.begin() + retain); pcm_.insert(pcm_.end(), chunk.begin(), chunk.begin() + retain);
} }
last_data_ = std::chrono::steady_clock::now(); last_data_ = std::chrono::steady_clock::now();
+1 -1
View File
@@ -10,7 +10,7 @@ public:
void prepare(); // open/resume once, while app is ready; never retain idle samples void prepare(); // open/resume once, while app is ready; never retain idle samples
void start(); // arm the already-running stream without touching the device void start(); // arm the already-running stream without touching the device
bool poll(); // drain even while idle; true at 20s limit; throws on loss/stall bool poll(); // drain even while idle; true at 20s limit; throws on loss/stall
std::vector<float> finish(); std::vector<float> finish(); // finite PCM saturated to [-1, 1] for the ASR API
void cancel(); // discard current clip, leave the device running void cancel(); // discard current clip, leave the device running
void close(); // release device on shutdown or capture failure void close(); // release device on shutdown or capture failure
bool recording() const { return recording_; } bool recording() const { return recording_; }
+3 -1
View File
@@ -353,7 +353,9 @@ void Worker::submit(uint64_t id, const std::vector<float>& pcm) {
auto& s = *state_; auto& s = *state_;
if (!ready() || s.pending) throw std::logic_error("worker not ready or request already pending"); if (!ready() || s.pending) throw std::logic_error("worker not ready or request already pending");
if (pcm.size() < 3200 || pcm.size() > 320000) throw std::invalid_argument("clip must be 0.2..20 seconds at 16 kHz"); if (pcm.size() < 3200 || pcm.size() > 320000) throw std::invalid_argument("clip must be 0.2..20 seconds at 16 kHz");
for (float value : pcm) if (!std::isfinite(value)) throw std::invalid_argument("nonfinite PCM sample"); for (float value : pcm)
if (!std::isfinite(value) || value < -1.0f || value > 1.0f)
throw std::invalid_argument("PCM samples must be finite and in [-1, 1]");
const std::string path = s.dir + "/clip.raw"; const std::string path = s.dir + "/clip.raw";
int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (fd < 0) throw std::runtime_error("cannot create exclusive private clip"); if (fd < 0) throw std::runtime_error("cannot create exclusive private clip");
+26
View File
@@ -4,6 +4,7 @@
#include <cassert> #include <cassert>
#include <cstring> #include <cstring>
#include <iostream> #include <iostream>
#include <limits>
#include <stdexcept> #include <stdexcept>
namespace { namespace {
@@ -52,6 +53,31 @@ int main() {
feed(0.9f, 100); audio.start(); feed(0.4f, 3200); feed(0.9f, 100); audio.start(); feed(0.4f, 3200);
clip = audio.finish(); assert(clip.size() == 3200 && clip.front() == 0.4f); clip = audio.finish(); assert(clip.size() == 3200 && clip.front() == 0.4f);
assert(opens == 1 && closes == 0 && resumes == 1); assert(opens == 1 && closes == 0 && resumes == 1);
// Real float capture can exceed full scale. Both poll and finish must
// saturate finite peaks without changing ordinary samples or clip size.
audio.start();
feed(1.01f, 1600); feed(-1.25f, 1600); audio.poll();
feed(std::numeric_limits<float>::max(), 1);
feed(-std::numeric_limits<float>::max(), 1);
feed(1.f, 1); feed(-1.f, 1); feed(.25f, 1); feed(0.f, 1);
clip = audio.finish();
assert(clip.size() == 3206);
assert(std::all_of(clip.begin(), clip.begin() + 1600, [](float x) { return x == 1.f; }));
assert(std::all_of(clip.begin() + 1600, clip.begin() + 3200, [](float x) { return x == -1.f; }));
assert(clip[3200] == 1.f && clip[3201] == -1.f && clip[3202] == 1.f && clip[3203] == -1.f);
assert(clip[3204] == .25f && clip[3205] == 0.f);
for (float invalid : {std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::quiet_NaN()}) {
audio.start(); feed(.2f, 3200); feed(invalid, 1);
bool rejected = false;
try { (void)audio.finish(); } catch (const std::runtime_error&) { rejected = true; }
assert(rejected && !audio.recording() && current->queued.empty());
audio.start(); feed(.3f, 3200);
clip = audio.finish();
assert(clip.size() == 3200 && clip.front() == .3f); // no failed clip tail leaks
}
assert(opens == 1 && closes == 0 && resumes == 1);
removed = true; removed = true;
bool failed = false; bool failed = false;
try { audio.poll(); } catch (const std::runtime_error&) { failed = true; } try { audio.poll(); } catch (const std::runtime_error&) { failed = true; }
+10
View File
@@ -6,6 +6,7 @@
#include <filesystem> #include <filesystem>
#include <functional> #include <functional>
#include <fstream> #include <fstream>
#include <limits>
#include <sys/stat.h> #include <sys/stat.h>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
@@ -40,6 +41,15 @@ int main(int argc, char** argv) {
try { worker.submit(1, std::vector<float>(3199)); } try { worker.submit(1, std::vector<float>(3199)); }
catch (const std::invalid_argument&) { rejected = true; } catch (const std::invalid_argument&) { rejected = true; }
assert(rejected); assert(rejected);
for (float value : {1.01f, -1.01f, std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::quiet_NaN()}) {
auto invalid = clip; invalid.back() = value;
rejected = false;
try { worker.submit(99, invalid); }
catch (const std::invalid_argument&) { rejected = true; }
assert(rejected && worker.ready()); // rejection happens before clip/IPC mutation
}
clip.front() = -1.f; clip.back() = 1.f; // full-scale boundaries are valid
worker.submit(100, clip); worker.submit(100, clip);
rejected = false; rejected = false;
try { worker.submit(101, clip); } try { worker.submit(101, clip); }