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`
(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
3200..320000 finite float samples, mono 16 kHz, stored as little-endian IEEE
float32 (0.2..20 s). Files are unlinked after replies or shutdown, and the
3200..320000 finite float samples in `[-1, 1]`, mono 16 kHz, stored as
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
account owner/root; do not use an untrusted runtime directory. The caller
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 (recording_) {
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");
// 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);
}
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 start(); // arm the already-running stream without touching the device
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 close(); // release device on shutdown or capture failure
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_;
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");
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";
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");
+26
View File
@@ -4,6 +4,7 @@
#include <cassert>
#include <cstring>
#include <iostream>
#include <limits>
#include <stdexcept>
namespace {
@@ -52,6 +53,31 @@ int main() {
feed(0.9f, 100); audio.start(); feed(0.4f, 3200);
clip = audio.finish(); assert(clip.size() == 3200 && clip.front() == 0.4f);
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;
bool failed = false;
try { audio.poll(); } catch (const std::runtime_error&) { failed = true; }
+10
View File
@@ -6,6 +6,7 @@
#include <filesystem>
#include <functional>
#include <fstream>
#include <limits>
#include <sys/stat.h>
#include <stdexcept>
#include <string>
@@ -40,6 +41,15 @@ int main(int argc, char** argv) {
try { worker.submit(1, std::vector<float>(3199)); }
catch (const std::invalid_argument&) { rejected = true; }
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);
rejected = false;
try { worker.submit(101, clip); }