Complete managed macOS platform bring-up
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s
.NET port / cpp-conformance (push) Canceled after 0s
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s
.NET port / cpp-conformance (push) Canceled after 0s
This commit is contained in:
@@ -174,3 +174,9 @@ Native `vccli --test-tone-ms 4000` runs finite external capture/playback, feeds
|
||||
and fails without decoded remote audio. Tests start two CLI processes in mono/stereo
|
||||
channels and also verify channel text. Normal `--voice` now explicitly subscribes before
|
||||
announcing its microphone stream. No C ABI or wire changes were needed.
|
||||
|
||||
The managed CLI also accepts `--test-tone-seconds N` (maximum 3600) for a finite, accurately
|
||||
paced peer used during device checks. Use a channel with Opus DTX disabled, such as the seeded
|
||||
Music Room: a steady sine is intentionally classified as non-speech by DTX and becomes comfort
|
||||
noise after its hangover period in Lobby. The finite peer prints received voice energy and exits
|
||||
normally when its requested duration completes.
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"sdk": { "version": "10.0.203", "rollForward": "latestFeature" }
|
||||
"sdk": { "version": "10.0.401", "rollForward": "latestFeature" }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using VoiceCat.Audio;
|
||||
using VoiceCat.Core;
|
||||
@@ -43,8 +44,14 @@ public static class CliCommand
|
||||
if (options.Delay > TimeSpan.Zero) await Task.Delay(options.Delay, stopped.Token);
|
||||
if (options.SendText is not null)
|
||||
client.Send(new() { TextMessage = new() { Scope = TextScope.TextChannel, TargetId = options.Channel, Body = options.SendText, ClientMsgId = Guid.NewGuid().ToString("N") } });
|
||||
Task? tone = options.Voice ? SendToneAsync(client, stopped.Token) : null;
|
||||
if (options.OneShot)
|
||||
Task? tone = options.Voice ? SendToneAsync(client, options.ToneDuration, stopped.Token) : null;
|
||||
if (options.ToneDuration > TimeSpan.Zero)
|
||||
{
|
||||
await tone!;
|
||||
Print("complete", options.Nickname, new { voiceEnergy = Interlocked.Read(ref energy) });
|
||||
stopped.Cancel();
|
||||
}
|
||||
else if (options.OneShot)
|
||||
{
|
||||
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(stopped.Token);
|
||||
deadline.CancelAfter(options.Timeout);
|
||||
@@ -75,15 +82,30 @@ public static class CliCommand
|
||||
if (message.UserEvent is { } user) Print("user", user.Kind.ToString(), new { userId = user.User?.Id ?? user.LeftId });
|
||||
}
|
||||
}
|
||||
private static async Task SendToneAsync(VoiceCatClient client, CancellationToken token)
|
||||
private static async Task SendToneAsync(VoiceCatClient client, TimeSpan requestedDuration, CancellationToken token)
|
||||
{
|
||||
TimeSpan duration = requestedDuration > TimeSpan.Zero ? requestedDuration : TimeSpan.FromSeconds(3);
|
||||
int frames = checked((int)Math.Ceiling(duration.TotalSeconds * 50));
|
||||
const int leadFrames = 8;
|
||||
long started = Stopwatch.GetTimestamp();
|
||||
short[] pcm = new short[960];
|
||||
for (int frame = 0; frame < 150 && !token.IsCancellationRequested; frame++)
|
||||
for (int frame = 0; frame < frames && !token.IsCancellationRequested; frame++)
|
||||
{
|
||||
if (frame >= leadFrames)
|
||||
{
|
||||
long target = started + (frame - leadFrames) * Stopwatch.Frequency / 50;
|
||||
while (true)
|
||||
{
|
||||
double remainingMilliseconds = (target - Stopwatch.GetTimestamp()) * 1000.0 / Stopwatch.Frequency;
|
||||
if (remainingMilliseconds <= 1) break;
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(remainingMilliseconds - 0.5), token);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < pcm.Length; i++) pcm[i] = (short)(Math.Sin((frame * 960 + i) * Math.PI * 880 / 48000) * 8000);
|
||||
foreach (StreamInfo stream in client.LocalStreams) client.Audio.FeedPcm(stream.StreamId, pcm, 1);
|
||||
await Task.Delay(20, token);
|
||||
foreach (StreamInfo stream in client.LocalStreams)
|
||||
while (!client.Audio.FeedPcm(stream.StreamId, pcm, 1)) await Task.Delay(1, token);
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(leadFrames * 20), token);
|
||||
}
|
||||
private static async Task InteractiveAsync(VoiceCatClient client, CancellationToken token)
|
||||
{
|
||||
@@ -96,20 +118,22 @@ public static class CliCommand
|
||||
}
|
||||
private static void Print(string type, string value, object? extra = null) => Console.WriteLine(JsonSerializer.Serialize(new { type, value, extra }));
|
||||
|
||||
private sealed record Options(string Host, ushort Port, string Nickname, string Pins, uint Channel, bool TrustFirst, bool Voice, bool ExpectVoice, string? SendText, string? ExpectText, TimeSpan Delay, TimeSpan Linger, TimeSpan Timeout)
|
||||
private sealed record Options(string Host, ushort Port, string Nickname, string Pins, uint Channel, bool TrustFirst, bool Voice, bool ExpectVoice, string? SendText, string? ExpectText, TimeSpan Delay, TimeSpan Linger, TimeSpan Timeout, TimeSpan ToneDuration)
|
||||
{
|
||||
internal bool OneShot => SendText is not null || ExpectText is not null || ExpectVoice;
|
||||
internal static Options Parse(string[] args)
|
||||
{
|
||||
string Value(string name, string fallback) { int i = Array.IndexOf(args, name); return i >= 0 && i + 1 < args.Length ? args[i + 1] : fallback; }
|
||||
bool Has(string name) => args.Contains(name, StringComparer.OrdinalIgnoreCase);
|
||||
if (Has("--help")) { Console.WriteLine("VoiceCat.Cli --host HOST --port PORT --nickname NAME [--trust-first] [--channel ID] [--voice] [--send-text TEXT] [--expect-text TEXT] [--expect-voice] [--start-delay-ms N]"); Environment.Exit(0); }
|
||||
if (Has("--help")) { Console.WriteLine("VoiceCat.Cli --host HOST --port PORT --nickname NAME [--trust-first] [--channel ID] [--voice] [--test-tone-seconds N] [--send-text TEXT] [--expect-text TEXT] [--expect-voice] [--start-delay-ms N]"); Environment.Exit(0); }
|
||||
TimeSpan toneDuration = TimeSpan.FromSeconds(int.Parse(Value("--test-tone-seconds", "0")));
|
||||
if (toneDuration < TimeSpan.Zero || toneDuration > TimeSpan.FromHours(1)) throw new ArgumentOutOfRangeException("--test-tone-seconds", "Tone duration must be between 0 and 3600 seconds.");
|
||||
return new(Value("--host", "127.0.0.1"), ushort.Parse(Value("--port", "8384")), Value("--nickname", Environment.UserName),
|
||||
Value("--pins", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "VoiceCat", "cli-tofu.txt")), uint.Parse(Value("--channel", "1")), Has("--trust-first"), Has("--voice") || Has("--expect-voice"), Has("--expect-voice"),
|
||||
Value("--pins", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "VoiceCat", "cli-tofu.txt")), uint.Parse(Value("--channel", "1")), Has("--trust-first"), Has("--voice") || Has("--expect-voice") || toneDuration > TimeSpan.Zero, Has("--expect-voice"),
|
||||
Array.IndexOf(args, "--send-text") is int send and >= 0 && send + 1 < args.Length ? args[send + 1] : null,
|
||||
Array.IndexOf(args, "--expect-text") is int expect and >= 0 && expect + 1 < args.Length ? args[expect + 1] : null,
|
||||
TimeSpan.FromMilliseconds(int.Parse(Value("--start-delay-ms", "0"))), TimeSpan.FromMilliseconds(int.Parse(Value("--linger-ms", "1000"))),
|
||||
TimeSpan.FromSeconds(int.Parse(Value("--timeout-seconds", "15"))));
|
||||
TimeSpan.FromSeconds(int.Parse(Value("--timeout-seconds", "15"))), toneDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ internal static class PrivateFiles
|
||||
public static void Write(string path, ReadOnlySpan<byte> data)
|
||||
{
|
||||
string destination = Path.GetFullPath(path);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
string temporary = destination + "." + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup Condition="'$(Protobuf_ProtocFullPath)' == '' and Exists('/opt/homebrew/bin/protoc')">
|
||||
<!-- Grpc.Tools currently ships an x64-only macOS protoc. Prefer Homebrew's native
|
||||
compiler on Apple Silicon hosts that do not have Rosetta installed. -->
|
||||
<Protobuf_ProtocFullPath>/opt/homebrew/bin/protoc</Protobuf_ProtocFullPath>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.36.1" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.83.0" PrivateAssets="all" />
|
||||
|
||||
@@ -4,6 +4,22 @@ namespace VoiceCat.Tests;
|
||||
|
||||
public class TofuTlsTests
|
||||
{
|
||||
[Fact]
|
||||
public void PinCreatesMissingParentDirectory()
|
||||
{
|
||||
string root = Path.Combine(Path.GetTempPath(), "voicecat-tofu-parent-" + Guid.NewGuid());
|
||||
string path = Path.Combine(root, "nested", "pins.txt");
|
||||
try
|
||||
{
|
||||
var store = new TofuStore(path);
|
||||
store.Pin("localhost", 8384, new string('a', 64));
|
||||
|
||||
Assert.True(File.Exists(path));
|
||||
Assert.Equal(TofuStatus.Matched, new TofuStore(path).Check("localhost", 8384, new string('A', 64)));
|
||||
}
|
||||
finally { if (Directory.Exists(root)) Directory.Delete(root, true); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RealHandshakesRequireAcceptanceAndRejectChangedCertificatesAfterRestart()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user