Live wakeword loop: PortAudio input + queue + threshold detection
Replaces the Task 3 smoke-test Program.cs with the full inference loop: - PA_ALSA_PLUGHW=1 and ORT_LOGGING_LEVEL=3 via both Environment.SetEnvironmentVariable and Libc.setenv (P/Invoke) per the Test 1 findings.md env-var gotcha. - OrtEnv.CreateInstanceWithOptions with ORT_LOGGING_LEVEL_ERROR to suppress early GPU-discovery warnings from device_discovery.cc in ORT 1.26.0 (the env var alone does not silence these; CreateInstanceWithOptions does). - Callback-driven PortAudio input stream at 16 kHz / Int16 / 1280 frames/block. - BlockingCollection<short[]>(16) queue; per-call new short[1280] allocation (no races). - Main loop: Take() -> WakewordModel.Predict() -> threshold >= 0.5 with 1.0s cooldown -> DETECTED alexa score=... t=...s. No beep (Task 5 adds it). - Console.CancelKeyPress -> cts.Cancel() -> graceful stream.Stop() + model.Dispose() + PortAudio.Terminate().
This commit is contained in:
+126
-11
@@ -1,22 +1,137 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Microsoft.ML.OnnxRuntime;
|
||||||
|
using PortAudioSharp;
|
||||||
using WakewordProbe;
|
using WakewordProbe;
|
||||||
|
using Stream = PortAudioSharp.Stream;
|
||||||
|
|
||||||
|
const int SampleRate = WakewordModel.SampleRate;
|
||||||
|
const int Channels = 1;
|
||||||
|
const uint BlockFrames = WakewordModel.FrameSamples; // 1280
|
||||||
|
const float Threshold = 0.5f;
|
||||||
|
const double CooldownSeconds = 1.0;
|
||||||
|
|
||||||
|
// .NET-side mirror for any readers that go through Environment.GetEnvironmentVariable,
|
||||||
|
// AND libc setenv so PortAudio / onnxruntime (which use getenv()) see the values.
|
||||||
|
Environment.SetEnvironmentVariable("PA_ALSA_PLUGHW", "1");
|
||||||
|
Libc.setenv("PA_ALSA_PLUGHW", "1", 1);
|
||||||
|
Environment.SetEnvironmentVariable("ORT_LOGGING_LEVEL", "3");
|
||||||
|
Libc.setenv("ORT_LOGGING_LEVEL", "3", 1);
|
||||||
|
// ORT 1.26.0: the env var does NOT suppress early GPU-discovery warnings from device_discovery.cc.
|
||||||
|
// CreateInstanceWithOptions sets the log level at OrtEnv creation time, before EP discovery runs.
|
||||||
|
var ortEnvOpts = new EnvironmentCreationOptions { logId = "WakewordProbe", logLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_ERROR };
|
||||||
|
OrtEnv.CreateInstanceWithOptions(ref ortEnvOpts);
|
||||||
|
|
||||||
|
PortAudio.Initialize();
|
||||||
|
WakewordModel? model = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int device = FindUsbDevice();
|
||||||
|
Console.WriteLine($"Using device {device} ('{PortAudio.GetDeviceInfo(device).name}')");
|
||||||
|
|
||||||
string modelsDir = Path.Combine(AppContext.BaseDirectory, "models");
|
|
||||||
Console.WriteLine("Loading WakewordModel...");
|
Console.WriteLine("Loading WakewordModel...");
|
||||||
var t0 = DateTime.UtcNow;
|
var t0 = DateTime.UtcNow;
|
||||||
using var model = new WakewordModel(
|
string modelsDir = Path.Combine(AppContext.BaseDirectory, "models");
|
||||||
|
model = new WakewordModel(
|
||||||
Path.Combine(modelsDir, "melspectrogram.onnx"),
|
Path.Combine(modelsDir, "melspectrogram.onnx"),
|
||||||
Path.Combine(modelsDir, "embedding_model.onnx"),
|
Path.Combine(modelsDir, "embedding_model.onnx"),
|
||||||
Path.Combine(modelsDir, "alexa.onnx"));
|
Path.Combine(modelsDir, "alexa.onnx"));
|
||||||
Console.WriteLine($"Loaded in {(DateTime.UtcNow - t0).TotalSeconds:0.00}s.");
|
Console.WriteLine($"Loaded in {(DateTime.UtcNow - t0).TotalSeconds:0.00}s.");
|
||||||
|
|
||||||
// Smoke test: feed 50 frames of silence (1280 zero samples each).
|
var queue = new BlockingCollection<short[]>(boundedCapacity: 16);
|
||||||
// Expect every score to be 0f (warmup gate + no signal).
|
using var cts = new CancellationTokenSource();
|
||||||
var silentFrame = new short[WakewordModel.FrameSamples];
|
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
|
||||||
int nonZero = 0;
|
|
||||||
for (int i = 0; i < 50; i++)
|
var inParams = new StreamParameters
|
||||||
{
|
{
|
||||||
float s = model.Predict(silentFrame);
|
device = device,
|
||||||
if (s != 0f) nonZero++;
|
channelCount = Channels,
|
||||||
|
sampleFormat = SampleFormat.Int16,
|
||||||
|
suggestedLatency = PortAudio.GetDeviceInfo(device).defaultLowInputLatency,
|
||||||
|
hostApiSpecificStreamInfo = IntPtr.Zero,
|
||||||
|
};
|
||||||
|
|
||||||
|
Stream.Callback callback = (IntPtr input, IntPtr _, uint frameCount,
|
||||||
|
ref StreamCallbackTimeInfo _2, StreamCallbackFlags status, IntPtr _3) =>
|
||||||
|
{
|
||||||
|
if (status.HasFlag(StreamCallbackFlags.InputOverflow))
|
||||||
|
Console.Error.WriteLine("[status] input overflow");
|
||||||
|
|
||||||
|
if (frameCount != BlockFrames)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[status] unexpected callback frameCount={frameCount} (want {BlockFrames})");
|
||||||
|
return StreamCallbackResult.Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var frame = new short[BlockFrames];
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
short* src = (short*)input.ToPointer();
|
||||||
|
fixed (short* dst = &frame[0])
|
||||||
|
Buffer.MemoryCopy(src, dst, BlockFrames * sizeof(short), BlockFrames * sizeof(short));
|
||||||
|
}
|
||||||
|
if (!queue.TryAdd(frame, 0))
|
||||||
|
Console.Error.WriteLine("[status] consumer behind, dropping frame");
|
||||||
|
return StreamCallbackResult.Continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
using var stream = new Stream(
|
||||||
|
inParams, null, SampleRate, BlockFrames, StreamFlags.NoFlag, callback, IntPtr.Zero);
|
||||||
|
stream.Start();
|
||||||
|
Console.WriteLine("Listening. Say 'alexa'. Ctrl-C to exit.");
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
TimeSpan lastTrigger = TimeSpan.FromSeconds(-CooldownSeconds);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
short[] frame = queue.Take(cts.Token);
|
||||||
|
float score = model.Predict(frame);
|
||||||
|
var now = sw.Elapsed;
|
||||||
|
if (score >= Threshold && (now - lastTrigger).TotalSeconds >= CooldownSeconds)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"DETECTED alexa score={score:0.000} t={now.TotalSeconds:0.0}s");
|
||||||
|
lastTrigger = now;
|
||||||
|
// Beep is added in Task 5.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { /* Ctrl-C path */ }
|
||||||
|
|
||||||
|
stream.Stop();
|
||||||
|
Console.WriteLine("\nbye");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
model?.Dispose();
|
||||||
|
PortAudio.Terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int FindUsbDevice()
|
||||||
|
{
|
||||||
|
int count = PortAudio.DeviceCount;
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var info = PortAudio.GetDeviceInfo(i);
|
||||||
|
if (info.name.ToLowerInvariant().Contains("usb") && info.maxInputChannels >= 1)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
Console.Error.WriteLine("USB audio device not found. Devices:");
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var info = PortAudio.GetDeviceInfo(i);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$" [{i}] {info.name} in={info.maxInputChannels} out={info.maxOutputChannels}");
|
||||||
|
}
|
||||||
|
Environment.Exit(1);
|
||||||
|
return -1; // unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Libc
|
||||||
|
{
|
||||||
|
[DllImport("libc", EntryPoint = "setenv")]
|
||||||
|
public static extern int setenv(string name, string value, int overwrite);
|
||||||
}
|
}
|
||||||
Console.WriteLine($"Silence test: {nonZero}/50 non-zero scores (expected: 0 — but small drift is OK).");
|
|
||||||
Console.WriteLine("Predict pipeline ran without throwing.");
|
|
||||||
|
|||||||
Reference in New Issue
Block a user