Tests: split-track recording coverage (per-peer + own send)
Local checkpoint - NOT for public release. Closes the second test gap. - RecordingController gets an internal SettingsSourceForTest seam so a self-test can drive a split (multi-track) recording without writing to the real shared settings store. - New self-test "Recording split tracks": with SplitTracks on + one connected peer, starts the real controller, feeds the "your send" track through the tap it wires onto the sender, and asserts it wrote a FOLDER of tracks (one per peer plus your own) with content - Ed's multi-track feature, now proven on every build. Gate 24/24. Both previously-deferred test gaps (split-track + main-window tabs) now closed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c8fed26115
commit
499e9644d7
@@ -44,6 +44,11 @@ internal sealed class RecordingController
|
|||||||
/// recording starts, so one track file is created per peer. Set by MainForm.</summary>
|
/// recording starts, so one track file is created per peer. Set by MainForm.</summary>
|
||||||
public Func<IReadOnlyList<(IPAddress Address, string Name)>>? ConnectedPeersProvider { get; set; }
|
public Func<IReadOnlyList<(IPAddress Address, string Name)>>? ConnectedPeersProvider { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Test seam: overrides where <see cref="Start"/> reads the recording settings from, so a
|
||||||
|
/// self-test can drive a split (multi-track) recording without writing to the real shared settings
|
||||||
|
/// store. Null (the default) = read from the store as normal.</summary>
|
||||||
|
internal Func<RecordingSettings>? SettingsSourceForTest { get; set; }
|
||||||
|
|
||||||
public RecordingController(AudioSender sender, AudioReceiver receiver, RemSoundSettingsStore settings, Action<string> diagnostic)
|
public RecordingController(AudioSender sender, AudioReceiver receiver, RemSoundSettingsStore settings, Action<string> diagnostic)
|
||||||
{
|
{
|
||||||
this.sender = sender;
|
this.sender = sender;
|
||||||
@@ -71,7 +76,7 @@ internal sealed class RecordingController
|
|||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
if (IsRecording) return;
|
if (IsRecording) return;
|
||||||
var s = settings.LoadRecordingSettings();
|
var s = (SettingsSourceForTest ?? settings.LoadRecordingSettings)();
|
||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ internal static class SelfTest
|
|||||||
RunStep(results, "Service send host (headless stream + yield)", ServiceSendHostStream);
|
RunStep(results, "Service send host (headless stream + yield)", ServiceSendHostStream);
|
||||||
RunStep(results, "Service registration args", ServiceRegistrationArgs);
|
RunStep(results, "Service registration args", ServiceRegistrationArgs);
|
||||||
RunStep(results, "Recording engine (all formats + source gate + mono)", RecordingEngine);
|
RunStep(results, "Recording engine (all formats + source gate + mono)", RecordingEngine);
|
||||||
|
RunStep(results, "Recording split tracks (per-peer + own)", RecordingSplitTracks);
|
||||||
RunStep(results, "Recording churn / soak", RecordingChurn);
|
RunStep(results, "Recording churn / soak", RecordingChurn);
|
||||||
RunStep(results, "v5 settings and shaping round-trip", V5ConfigRoundTrip);
|
RunStep(results, "v5 settings and shaping round-trip", V5ConfigRoundTrip);
|
||||||
RunStep(results, "Profile save and reload", ProfileRoundTrip);
|
RunStep(results, "Profile save and reload", ProfileRoundTrip);
|
||||||
@@ -534,6 +535,63 @@ internal static class SelfTest
|
|||||||
finally { try { Directory.Delete(temp, recursive: true); } catch { /* best-effort */ } }
|
finally { try { Directory.Delete(temp, recursive: true); } catch { /* best-effort */ } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Split-track (multi-track) recording: with SplitTracks on and one connected peer, the
|
||||||
|
/// recorder must write a FOLDER of tracks — one per peer plus your own send — not a single mixed file.
|
||||||
|
/// Drives the real RecordingController via a settings-injection seam (so it never touches the shared
|
||||||
|
/// settings store), feeds the "your send" track through the tap the controller wires onto the sender,
|
||||||
|
/// and asserts the track files land with content.</summary>
|
||||||
|
private static string? RecordingSplitTracks()
|
||||||
|
{
|
||||||
|
var temp = Path.Combine(Path.GetTempPath(), "remsound-split-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(temp);
|
||||||
|
using var receiver = new AudioReceiver();
|
||||||
|
using var sender = new RemSound.Sender.AudioSender();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var controller = new RecordingController(sender, receiver, new RemSoundSettingsStore("RemSound"), _ => { })
|
||||||
|
{
|
||||||
|
SettingsSourceForTest = () => new RecordingSettings
|
||||||
|
{
|
||||||
|
SplitTracks = true,
|
||||||
|
Source = RecordingSource.Both,
|
||||||
|
FileFormat = RecordingFileFormat.Wav,
|
||||||
|
Folder = temp,
|
||||||
|
},
|
||||||
|
ConnectedPeersProvider = () => new[] { (IPAddress.Loopback, "TestPeer") },
|
||||||
|
};
|
||||||
|
|
||||||
|
controller.Start();
|
||||||
|
Check(controller.IsRecording, "split recording should be running after Start");
|
||||||
|
|
||||||
|
// Feed the "your send" track through the tap Start wired onto the sender.
|
||||||
|
var tap = sender.OnSentSamples;
|
||||||
|
if (tap is not null)
|
||||||
|
{
|
||||||
|
var chunk = new float[480 * 2];
|
||||||
|
var phase = 0.0;
|
||||||
|
for (var c = 0; c < 60; c++)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < chunk.Length; i += 2)
|
||||||
|
{
|
||||||
|
var s = (float)(0.2 * Math.Sin(phase));
|
||||||
|
phase += 2 * Math.PI * 440 / 48000;
|
||||||
|
chunk[i] = s; chunk[i + 1] = s;
|
||||||
|
}
|
||||||
|
tap(chunk.AsMemory(), RenderRoute.Mixed);
|
||||||
|
Thread.Sleep(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
controller.Stop();
|
||||||
|
for (var i = 0; i < 40 && Directory.GetFiles(temp, "*.wav", SearchOption.AllDirectories).Length == 0; i++) Thread.Sleep(25);
|
||||||
|
|
||||||
|
var files = Directory.GetFiles(temp, "*.wav", SearchOption.AllDirectories);
|
||||||
|
Check(files.Length >= 2, $"split recording must make one file per peer plus your own (found {files.Length})");
|
||||||
|
Check(files.Any(f => new FileInfo(f).Length > 200), "at least one split track (your own send) must have real content");
|
||||||
|
return $"split recording made {files.Length} track files, one with content";
|
||||||
|
}
|
||||||
|
finally { try { Directory.Delete(temp, recursive: true); } catch { /* best-effort */ } }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Load/soak: rapidly start, feed, stop and dispose recordings across every format, checking
|
/// <summary>Load/soak: rapidly start, feed, stop and dispose recordings across every format, checking
|
||||||
/// nothing leaks handles across the churn — catches recorder/encoder lifecycle leaks and races that a
|
/// nothing leaks handles across the churn — catches recorder/encoder lifecycle leaks and races that a
|
||||||
/// single recording wouldn't surface. Set the env var REMSOUND_TEST_SOAK=<seconds> to keep
|
/// single recording wouldn't surface. Set the env var REMSOUND_TEST_SOAK=<seconds> to keep
|
||||||
|
|||||||
Reference in New Issue
Block a user