Plan: C# probe re-implementing Test 1 (record/play) on the Pi
This commit is contained in:
@@ -0,0 +1,564 @@
|
||||
# Record-play C# probe — implementation plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Re-implement Test 1 (record 5 s from the USB Speaker Phone → live RMS meter → write `out.wav` → play it back) in C# on the Raspberry Pi, proving .NET-driven PortAudio works on linux-arm64 with the same pass criteria the Python version met.
|
||||
|
||||
**Architecture:** Single-folder throwaway probe under `tests/01-record-play-cs/`. One `Program.cs` driving PortAudio via the PortAudioSharp2 NuGet binding, plus a tiny hand-rolled `WavWriter.cs` for the PCM16 RIFF file. A new `bin/probe-cs` script builds it as a linux-arm64 self-contained binary, rsyncs to the Pi, and runs it over SSH. The Pi gets no .NET install. No automated tests — verification is the same listening test the Python probe uses (`findings.md`).
|
||||
|
||||
**Tech Stack:** .NET 9, C#, PortAudioSharp2 (NuGet), bash, sshpass, rsync. Target host: Pi at `192.168.50.115`, user `pi`, password `assistant` (see `CLAUDE.md`).
|
||||
|
||||
---
|
||||
|
||||
## File structure
|
||||
|
||||
| Path | Created in task | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `tests/01-record-play-cs/Probe.csproj` | Task 1 | net9.0 console project, linux-arm64 RID, PortAudioSharp2 reference |
|
||||
| `tests/01-record-play-cs/Program.cs` | Task 1 (sanity spike), expanded in Tasks 4 & 5 | Single entry point; mirrors the Python probe's structure |
|
||||
| `tests/01-record-play-cs/WavWriter.cs` | Task 3 | Static `Write(path, samples, sampleRate)` — 44-byte RIFF + raw PCM16 LE |
|
||||
| `bin/probe-cs` | Task 2 | `dotnet publish` → `rsync` → `ssh` flow |
|
||||
|
||||
`tests/01-record-play/` (Python) and the rest of the repo are untouched.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Scaffold project + PortAudio sanity spike on the Pi
|
||||
|
||||
**Goal:** Stand up the .NET project and prove on the Pi that `PortAudio.Initialize()` + device enumeration work end-to-end from a self-contained linux-arm64 publish. No audio I/O yet. This de-risks the unknown the spec called out: does PortAudioSharp2's NuGet ship a `libportaudio` for linux-arm64, or does it dlopen the system one?
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/01-record-play-cs/Probe.csproj`
|
||||
- Create: `tests/01-record-play-cs/Program.cs`
|
||||
|
||||
- [ ] **Step 1: Find the latest stable PortAudioSharp2 version**
|
||||
|
||||
Open `https://www.nuget.org/packages/PortAudioSharp2` in a browser (or `curl -s https://api.nuget.org/v3-flatcontainer/portaudiosharp2/index.json | jq -r '.versions[-1]'`). Note the latest non-prerelease version (call it `<PASHARP_VER>` below). Pin that exact version in the csproj — do **not** use a floating `*`. If `curl/jq` are not available, the NuGet web page lists "Latest version" at the top right.
|
||||
|
||||
- [ ] **Step 2: Write `tests/01-record-play-cs/Probe.csproj`**
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RootNamespace>RecordPlayProbe</RootNamespace>
|
||||
<AssemblyName>Probe</AssemblyName>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="PortAudioSharp2" Version="<PASHARP_VER>" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
Replace `<PASHARP_VER>` with the version from Step 1.
|
||||
|
||||
- [ ] **Step 3: Write `tests/01-record-play-cs/Program.cs` (spike version)**
|
||||
|
||||
```csharp
|
||||
using PortAudioSharp;
|
||||
|
||||
Environment.SetEnvironmentVariable("PA_ALSA_PLUGHW", "1");
|
||||
|
||||
PortAudio.Initialize();
|
||||
try
|
||||
{
|
||||
int count = PortAudio.DeviceCount;
|
||||
Console.WriteLine($"PortAudio version: {PortAudio.VersionInfo.versionText}");
|
||||
Console.WriteLine($"Devices ({count}):");
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var info = PortAudio.GetDeviceInfo(i);
|
||||
Console.WriteLine(
|
||||
$" [{i}] {info.name} in={info.maxInputChannels} out={info.maxOutputChannels} " +
|
||||
$"defaultSr={info.defaultSampleRate}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
PortAudio.Terminate();
|
||||
}
|
||||
```
|
||||
|
||||
**Note for the engineer:** The above uses the API surface as documented in PortAudioSharp2's README at time of writing (`PortAudio.Initialize/Terminate`, `PortAudio.DeviceCount`, `PortAudio.GetDeviceInfo(int)`, `PortAudio.VersionInfo`). If a compile error reveals the surface has changed, read PortAudioSharp2's README at the version you pinned in Step 1 and adjust the calls — the intent is "init, dump device table, terminate". Do **not** change to a different binding.
|
||||
|
||||
- [ ] **Step 4: Build and publish locally**
|
||||
|
||||
Run from the repo root:
|
||||
|
||||
```sh
|
||||
dotnet publish tests/01-record-play-cs -c Release -r linux-arm64 --self-contained \
|
||||
-o /tmp/probe-cs-out
|
||||
```
|
||||
|
||||
Expected: build succeeds. Verify the output contains the native PortAudio binary:
|
||||
|
||||
```sh
|
||||
ls /tmp/probe-cs-out | grep -E 'libportaudio|Probe'
|
||||
```
|
||||
|
||||
Expected: `Probe` (the binary) is present. `libportaudio.so*` may or may not be present — if absent, PortAudioSharp2 will dlopen the system `libportaudio2` already installed by `bin/bootstrap` on the Pi.
|
||||
|
||||
- [ ] **Step 5: Push the spike to the Pi and run it**
|
||||
|
||||
```sh
|
||||
sshpass -p 'assistant' rsync -az --delete /tmp/probe-cs-out/ \
|
||||
pi@192.168.50.115:~/probe-cs/
|
||||
sshpass -p 'assistant' ssh pi@192.168.50.115 'cd ~/probe-cs && ./Probe'
|
||||
```
|
||||
|
||||
Expected output: PortAudio version string + a device table that includes at least one entry whose name contains `USB` (the Anhui LISTENAI USB Speaker Phone — exposed as ALSA card 3 per `findings.md`). If you see `dlopen` failures for `libportaudio`, install it on the Pi: `sshpass -p 'assistant' ssh pi@192.168.50.115 'sudo apt-get install -y libportaudio2'` (the prototype `bin/bootstrap` already does this, but the Pi may have been wiped).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```sh
|
||||
git add tests/01-record-play-cs/Probe.csproj tests/01-record-play-cs/Program.cs
|
||||
git commit -m "Scaffold C# record-play probe + PortAudio device-list spike"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Wrap the deploy flow as `bin/probe-cs`
|
||||
|
||||
**Goal:** Capture the Task 1 build/rsync/ssh sequence as a single script so subsequent tasks just run `bin/probe-cs`.
|
||||
|
||||
**Files:**
|
||||
- Create: `bin/probe-cs`
|
||||
|
||||
- [ ] **Step 1: Write `bin/probe-cs`**
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PI_HOST="${PI_HOST:-192.168.50.115}"
|
||||
PI_USER="${PI_USER:-pi}"
|
||||
PI_PASS="${PI_PASS:-assistant}"
|
||||
PUBLISH_DIR="${PUBLISH_DIR:-/tmp/probe-cs-out}"
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$repo_root"
|
||||
|
||||
echo ">> dotnet publish (linux-arm64, self-contained)"
|
||||
dotnet publish tests/01-record-play-cs \
|
||||
-c Release -r linux-arm64 --self-contained \
|
||||
-o "$PUBLISH_DIR"
|
||||
|
||||
echo ">> rsync to $PI_USER@$PI_HOST:~/probe-cs/"
|
||||
sshpass -p "$PI_PASS" rsync -az --delete \
|
||||
"$PUBLISH_DIR"/ "$PI_USER@$PI_HOST:~/probe-cs/"
|
||||
|
||||
echo ">> ssh + run on Pi"
|
||||
sshpass -p "$PI_PASS" ssh -o StrictHostKeyChecking=accept-new \
|
||||
"$PI_USER@$PI_HOST" 'cd ~/probe-cs && ./Probe'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Mark it executable**
|
||||
|
||||
```sh
|
||||
chmod +x bin/probe-cs
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Smoke-test the script against Task 1's spike**
|
||||
|
||||
```sh
|
||||
bin/probe-cs
|
||||
```
|
||||
|
||||
Expected output: same device table as in Task 1 Step 5. If it errors, fix the script — do not work around it in later tasks.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```sh
|
||||
git add bin/probe-cs
|
||||
git commit -m "Add bin/probe-cs: build + rsync + run the C# probe on the Pi"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: `WavWriter.cs` — hand-rolled PCM16 RIFF writer
|
||||
|
||||
**Goal:** A static helper that writes a mono PCM16 WAV file. No external deps.
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/01-record-play-cs/WavWriter.cs`
|
||||
|
||||
- [ ] **Step 1: Write `tests/01-record-play-cs/WavWriter.cs`**
|
||||
|
||||
```csharp
|
||||
namespace RecordPlayProbe;
|
||||
|
||||
internal static class WavWriter
|
||||
{
|
||||
public static void Write(string path, ReadOnlySpan<short> samples, int sampleRate)
|
||||
{
|
||||
const short channels = 1;
|
||||
const short bitsPerSample = 16;
|
||||
const short pcmFormat = 1;
|
||||
|
||||
int byteRate = sampleRate * channels * bitsPerSample / 8;
|
||||
short blockAlign = (short)(channels * bitsPerSample / 8);
|
||||
int dataBytes = samples.Length * sizeof(short);
|
||||
int riffChunkSize = 36 + dataBytes;
|
||||
|
||||
using var fs = new FileStream(path, FileMode.Create, FileAccess.Write);
|
||||
using var bw = new BinaryWriter(fs);
|
||||
|
||||
// RIFF header
|
||||
bw.Write(new[] { (byte)'R', (byte)'I', (byte)'F', (byte)'F' });
|
||||
bw.Write(riffChunkSize);
|
||||
bw.Write(new[] { (byte)'W', (byte)'A', (byte)'V', (byte)'E' });
|
||||
|
||||
// fmt chunk
|
||||
bw.Write(new[] { (byte)'f', (byte)'m', (byte)'t', (byte)' ' });
|
||||
bw.Write(16); // fmt chunk size for PCM
|
||||
bw.Write(pcmFormat);
|
||||
bw.Write(channels);
|
||||
bw.Write(sampleRate);
|
||||
bw.Write(byteRate);
|
||||
bw.Write(blockAlign);
|
||||
bw.Write(bitsPerSample);
|
||||
|
||||
// data chunk
|
||||
bw.Write(new[] { (byte)'d', (byte)'a', (byte)'t', (byte)'a' });
|
||||
bw.Write(dataBytes);
|
||||
foreach (short s in samples)
|
||||
bw.Write(s);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`BinaryWriter` writes integers little-endian on every supported platform — that matches the WAV spec. No byte-order conversion needed.
|
||||
|
||||
- [ ] **Step 2: Build (no test step — spec says no automated tests)**
|
||||
|
||||
```sh
|
||||
dotnet build tests/01-record-play-cs -c Release
|
||||
```
|
||||
|
||||
Expected: success.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```sh
|
||||
git add tests/01-record-play-cs/WavWriter.cs
|
||||
git commit -m "Add WavWriter: mono PCM16 RIFF writer for the C# probe"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Recording with live RMS meter + WAV write
|
||||
|
||||
**Goal:** Replace the spike `Program.cs` with the real record + meter + write flow. No playback yet — we want to confirm recording independently of playback so a regression on either is unambiguous.
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/01-record-play-cs/Program.cs` (rewrite — the spike code is replaced)
|
||||
|
||||
- [ ] **Step 1: Rewrite `tests/01-record-play-cs/Program.cs`**
|
||||
|
||||
```csharp
|
||||
using PortAudioSharp;
|
||||
|
||||
const int SampleRate = 16_000;
|
||||
const int Channels = 1;
|
||||
const int DurationS = 5;
|
||||
const uint BlockFrames = 1024;
|
||||
const string OutWav = "out.wav";
|
||||
|
||||
Environment.SetEnvironmentVariable("PA_ALSA_PLUGHW", "1");
|
||||
|
||||
PortAudio.Initialize();
|
||||
try
|
||||
{
|
||||
int device = FindUsbDevice();
|
||||
Console.WriteLine(
|
||||
$"Recording {DurationS}s from device {device} " +
|
||||
$"('{PortAudio.GetDeviceInfo(device).name}')");
|
||||
|
||||
short[] buf = new short[SampleRate * DurationS];
|
||||
int writeOffset = 0;
|
||||
object gate = new();
|
||||
|
||||
var inParams = new StreamParameters
|
||||
{
|
||||
device = device,
|
||||
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");
|
||||
|
||||
lock (gate)
|
||||
{
|
||||
int remaining = buf.Length - writeOffset;
|
||||
int take = (int)Math.Min(frameCount, (uint)remaining);
|
||||
if (take > 0)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
short* src = (short*)input.ToPointer();
|
||||
fixed (short* dst = &buf[writeOffset])
|
||||
{
|
||||
Buffer.MemoryCopy(src, dst, take * sizeof(short), take * sizeof(short));
|
||||
}
|
||||
}
|
||||
writeOffset += take;
|
||||
}
|
||||
}
|
||||
return StreamCallbackResult.Continue;
|
||||
};
|
||||
|
||||
using var stream = new Stream(
|
||||
inParams, null, SampleRate, BlockFrames, StreamFlags.NoFlag, callback, IntPtr.Zero);
|
||||
stream.Start();
|
||||
|
||||
var start = DateTime.UtcNow;
|
||||
while (true)
|
||||
{
|
||||
int written;
|
||||
lock (gate) { written = writeOffset; }
|
||||
if (written >= buf.Length) break;
|
||||
|
||||
double level = Rms(buf, Math.Max(0, written - 1600), written);
|
||||
int bars = (int)(level * 60);
|
||||
double elapsed = (DateTime.UtcNow - start).TotalSeconds;
|
||||
Console.Write($"\r{elapsed,4:0.0}s |{new string('#', bars).PadRight(60)}|");
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
Console.WriteLine();
|
||||
stream.Stop();
|
||||
|
||||
WavWriter.Write(OutWav, buf, SampleRate);
|
||||
Console.WriteLine($"Wrote {OutWav} ({buf.Length} frames @ {SampleRate} Hz)");
|
||||
}
|
||||
finally
|
||||
{
|
||||
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 double Rms(short[] data, int from, int to)
|
||||
{
|
||||
if (to <= from) return 0.0;
|
||||
double sumSq = 0.0;
|
||||
for (int i = from; i < to; i++)
|
||||
{
|
||||
double x = data[i] / 32768.0;
|
||||
sumSq += x * x;
|
||||
}
|
||||
return Math.Sqrt(sumSq / (to - from) + 1e-12);
|
||||
}
|
||||
```
|
||||
|
||||
**RMS formula matches the Python probe** (`np.sqrt((x*x).mean() + 1e-12)`). **Tail window matches** (last 1600 samples, ~100 ms at 16 kHz). **Meter cadence matches** (50 ms sleep → ~20 Hz refresh).
|
||||
|
||||
The csproj also needs `<AllowUnsafeBlocks>true</AllowUnsafeBlocks>` for the `unsafe` block in the callback. Add it:
|
||||
|
||||
- [ ] **Step 2: Enable unsafe blocks in `Probe.csproj`**
|
||||
|
||||
Add `<AllowUnsafeBlocks>true</AllowUnsafeBlocks>` inside the existing `<PropertyGroup>`. The full PropertyGroup should now read:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RootNamespace>RecordPlayProbe</RootNamespace>
|
||||
<AssemblyName>Probe</AssemblyName>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build locally**
|
||||
|
||||
```sh
|
||||
dotnet build tests/01-record-play-cs -c Release
|
||||
```
|
||||
|
||||
Expected: success. If you hit a `StreamParameters` / `Stream` / `Callback` API mismatch, see PortAudioSharp2's README at the version you pinned in Task 1 and adjust the type names — the structure (struct of device + channelCount + sampleFormat + latency; Stream constructor that takes input params, output params, sample rate, frames-per-buffer, flags, callback, user data) is the conventional PortAudio shape and should map closely.
|
||||
|
||||
- [ ] **Step 4: Run on the Pi**
|
||||
|
||||
```sh
|
||||
bin/probe-cs
|
||||
```
|
||||
|
||||
Expected on the Pi:
|
||||
- A line `Recording 5s from device <N> ('USB...')`.
|
||||
- A live meter: `\r 0.0s |######...|` updating ~20×/sec for 5 seconds. The bar visibly responds to speech vs. silence.
|
||||
- A final line `Wrote out.wav (80000 frames @ 16000 Hz)`.
|
||||
- No sustained `[status] input overflow` messages (one at startup is acceptable).
|
||||
|
||||
Confirm the WAV is well-formed:
|
||||
|
||||
```sh
|
||||
sshpass -p 'assistant' ssh pi@192.168.50.115 'soxi ~/probe-cs/out.wav'
|
||||
```
|
||||
|
||||
Expected: `soxi` reports 1 channel, 16000 Hz, 16-bit PCM, ~5 seconds duration. (If `soxi` isn't installed: `sudo apt-get install -y sox` on the Pi, or use `file out.wav` which will at least say `RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 16000 Hz`.)
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```sh
|
||||
git add tests/01-record-play-cs/Probe.csproj tests/01-record-play-cs/Program.cs
|
||||
git commit -m "Record loop: callback InputStream + RMS meter + WAV write"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Playback + full pass-criteria verification
|
||||
|
||||
**Goal:** Add the playback step and verify all three pass criteria from the spec on real hardware.
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/01-record-play-cs/Program.cs` (append playback after the WAV write)
|
||||
|
||||
- [ ] **Step 1: Add playback to `Program.cs`**
|
||||
|
||||
Insert this block immediately after the `Console.WriteLine($"Wrote {OutWav}...");` line and before the `finally` block:
|
||||
|
||||
```csharp
|
||||
Console.WriteLine($"Playing back through device {device}");
|
||||
|
||||
int playOffset = 0;
|
||||
object playGate = new();
|
||||
var doneEvent = new ManualResetEventSlim(false);
|
||||
|
||||
var outParams = new StreamParameters
|
||||
{
|
||||
device = device,
|
||||
channelCount = Channels,
|
||||
sampleFormat = SampleFormat.Int16,
|
||||
suggestedLatency = PortAudio.GetDeviceInfo(device).defaultLowOutputLatency,
|
||||
hostApiSpecificStreamInfo = IntPtr.Zero,
|
||||
};
|
||||
|
||||
Stream.Callback playCb = (IntPtr _, IntPtr output, uint frameCount,
|
||||
ref StreamCallbackTimeInfo _2, StreamCallbackFlags _3, IntPtr _4) =>
|
||||
{
|
||||
lock (playGate)
|
||||
{
|
||||
int remaining = buf.Length - playOffset;
|
||||
int take = (int)Math.Min(frameCount, (uint)remaining);
|
||||
int silence = (int)frameCount - take;
|
||||
|
||||
unsafe
|
||||
{
|
||||
short* dst = (short*)output.ToPointer();
|
||||
if (take > 0)
|
||||
{
|
||||
fixed (short* src = &buf[playOffset])
|
||||
Buffer.MemoryCopy(src, dst, take * sizeof(short), take * sizeof(short));
|
||||
playOffset += take;
|
||||
}
|
||||
if (silence > 0)
|
||||
{
|
||||
for (int i = take; i < frameCount; i++) dst[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (playOffset >= buf.Length)
|
||||
{
|
||||
doneEvent.Set();
|
||||
return StreamCallbackResult.Complete;
|
||||
}
|
||||
}
|
||||
return StreamCallbackResult.Continue;
|
||||
};
|
||||
|
||||
using var playStream = new Stream(
|
||||
null, outParams, SampleRate, BlockFrames, StreamFlags.NoFlag, playCb, IntPtr.Zero);
|
||||
playStream.Start();
|
||||
doneEvent.Wait();
|
||||
// Give PortAudio a moment to drain the device buffer before Stop().
|
||||
Thread.Sleep(200);
|
||||
playStream.Stop();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build locally**
|
||||
|
||||
```sh
|
||||
dotnet build tests/01-record-play-cs -c Release
|
||||
```
|
||||
|
||||
Expected: success.
|
||||
|
||||
- [ ] **Step 3: Run on the Pi and verify all three pass criteria**
|
||||
|
||||
```sh
|
||||
bin/probe-cs
|
||||
```
|
||||
|
||||
Then **at the Pi's audio output** (the USB Speaker Phone is mic and speaker — playback comes out of the same device):
|
||||
|
||||
1. **Playback is recognisable as what was said into the mic.** Speak something distinct during the 5-second record window (e.g., "the quick brown fox jumps over the lazy dog") and confirm you can clearly understand the playback.
|
||||
2. **The meter visibly responds to speech and to silence.** During the recording, alternate between speaking and silence; the `####` bar should clearly grow and shrink.
|
||||
3. **No clipping, dropouts, or sustained PortAudio under/overrun warnings.** Scan the stderr output for `[status] input overflow` lines — a single one at start-up is acceptable, sustained ones are not. The playback should not have audible clicks, gaps, or chipmunk artefacts.
|
||||
|
||||
If any of the three fails: do not "fix" by tweaking thresholds or silencing logs — diagnose. Use `findings.md` ("Force PortAudio through ALSA's `plug` plugin") as the first reference; the `PA_ALSA_PLUGHW=1` env var must be in place before `PortAudio.Initialize()` runs.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```sh
|
||||
git add tests/01-record-play-cs/Program.cs
|
||||
git commit -m "Playback: append OutputStream playback of recorded buffer"
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Note the probe outcome**
|
||||
|
||||
The probe is done. Append a short note to `findings.md` under a new section `## C# probe outcome (2026-06-12)` recording: (a) which PortAudioSharp2 version you pinned, (b) whether the NuGet bundled `libportaudio.so` or you used the Pi's system `libportaudio2`, (c) any API drift you had to adjust for vs. the plan, (d) whether all three pass criteria held. This belongs in `findings.md` because the next probe (Test 2 in C#) will benefit from it.
|
||||
|
||||
Commit:
|
||||
|
||||
```sh
|
||||
git add findings.md
|
||||
git commit -m "Findings: C# record-play probe outcome"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes on what is intentionally **not** in this plan
|
||||
|
||||
- **No automated tests.** The spec is explicit; verification is the listening test on the Pi. WavWriter is verified transitively by the playback step in Task 5.
|
||||
- **No queue / consumer thread.** The Python probe has the same shape (callback writes, main thread reads). The findings doc's "callback queue for input" rework belongs to the eventual main client, not this probe.
|
||||
- **No cleanup on Ctrl-C mid-recording.** Probe. Crash trace is fine.
|
||||
- **No retry on PortAudio init failure.** Crash and surface.
|
||||
- **No abstraction over PortAudio.** No `IAudioDevice` interface, no DI. One file, one job.
|
||||
|
||||
If, during implementation, you feel the urge to add any of the above — stop and re-read the spec. The point of this probe is to be cheap and disposable.
|
||||
Reference in New Issue
Block a user