deepseek-computer-use 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +16 -3
- package/lib/index.js +7 -0
- package/lib/update-client.js +426 -0
- package/native/Driver.cs +1906 -1906
- package/native/Json.cs +274 -274
- package/native/Overlay.cs +732 -732
- package/native/Program.cs +160 -160
- package/native/audio/Program.cs +95 -95
- package/native/wgcfw/Program.cs +249 -249
- package/package.json +20 -6
package/native/Program.cs
CHANGED
|
@@ -1,163 +1,163 @@
|
|
|
1
|
-
|
|
2
|
-
using System;
|
|
3
|
-
using System.Collections.Generic;
|
|
4
|
-
using System.Globalization;
|
|
5
|
-
using System.IO;
|
|
6
|
-
using System.Text;
|
|
7
|
-
using System.Threading;
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// stdio JSON-lines bridge: one request object per line in, one response object
|
|
11
|
-
// per line out. The DSH plugin owns the process lifetime; this helper owns the
|
|
12
|
-
// UI Automation / SendInput / capture state so element indexes stay valid.
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
public static class Program
|
|
15
|
-
{
|
|
16
|
-
static IPlatformDriver _driver;
|
|
17
|
-
/// Set by the close RPC: the loop stops after flushing the response.
|
|
18
|
-
public static bool CloseRequested;
|
|
19
|
-
/// 130 mirrors Codex's "the user pressed Esc" exit code.
|
|
20
|
-
public static int RequestedExitCode;
|
|
21
|
-
|
|
22
|
-
public static int Main(string[] args)
|
|
23
|
-
{
|
|
24
|
-
try
|
|
25
|
-
{
|
|
26
|
-
Console.OutputEncoding = new UTF8Encoding(false);
|
|
27
|
-
Console.InputEncoding = new UTF8Encoding(false);
|
|
28
|
-
}
|
|
29
|
-
catch { }
|
|
30
|
-
|
|
31
|
-
_driver = CreateDriver();
|
|
32
|
-
|
|
33
|
-
if (args.Length > 0 && args[0] == "--self-test")
|
|
34
|
-
{
|
|
35
|
-
return SelfTest();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
string line;
|
|
39
|
-
while (true)
|
|
40
|
-
{
|
|
41
|
-
try { line = Console.ReadLine(); }
|
|
42
|
-
catch (Exception) { break; }
|
|
43
|
-
if (line == null) break;
|
|
44
|
-
if (line.Trim().Length == 0) continue;
|
|
45
|
-
string response = Handle(line);
|
|
46
|
-
Console.Out.Write(response);
|
|
47
|
-
Console.Out.Write('\n');
|
|
1
|
+
|
|
2
|
+
using System;
|
|
3
|
+
using System.Collections.Generic;
|
|
4
|
+
using System.Globalization;
|
|
5
|
+
using System.IO;
|
|
6
|
+
using System.Text;
|
|
7
|
+
using System.Threading;
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// stdio JSON-lines bridge: one request object per line in, one response object
|
|
11
|
+
// per line out. The DSH plugin owns the process lifetime; this helper owns the
|
|
12
|
+
// UI Automation / SendInput / capture state so element indexes stay valid.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
public static class Program
|
|
15
|
+
{
|
|
16
|
+
static IPlatformDriver _driver;
|
|
17
|
+
/// Set by the close RPC: the loop stops after flushing the response.
|
|
18
|
+
public static bool CloseRequested;
|
|
19
|
+
/// 130 mirrors Codex's "the user pressed Esc" exit code.
|
|
20
|
+
public static int RequestedExitCode;
|
|
21
|
+
|
|
22
|
+
public static int Main(string[] args)
|
|
23
|
+
{
|
|
24
|
+
try
|
|
25
|
+
{
|
|
26
|
+
Console.OutputEncoding = new UTF8Encoding(false);
|
|
27
|
+
Console.InputEncoding = new UTF8Encoding(false);
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
|
|
31
|
+
_driver = CreateDriver();
|
|
32
|
+
|
|
33
|
+
if (args.Length > 0 && args[0] == "--self-test")
|
|
34
|
+
{
|
|
35
|
+
return SelfTest();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
string line;
|
|
39
|
+
while (true)
|
|
40
|
+
{
|
|
41
|
+
try { line = Console.ReadLine(); }
|
|
42
|
+
catch (Exception) { break; }
|
|
43
|
+
if (line == null) break;
|
|
44
|
+
if (line.Trim().Length == 0) continue;
|
|
45
|
+
string response = Handle(line);
|
|
46
|
+
Console.Out.Write(response);
|
|
47
|
+
Console.Out.Write('\n');
|
|
48
48
|
Console.Out.Flush();
|
|
49
49
|
if (CloseRequested || RequestedExitCode != 0) break;
|
|
50
50
|
}
|
|
51
|
-
try { if (_driver != null) _driver.Dispose(); } catch { }
|
|
52
|
-
return RequestedExitCode;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
static IPlatformDriver CreateDriver()
|
|
56
|
-
{
|
|
57
|
-
string forced = Environment.GetEnvironmentVariable("DSH_CU_PLATFORM");
|
|
58
|
-
if (!string.IsNullOrEmpty(forced) && forced != "windows")
|
|
59
|
-
throw new NotSupportedException("platform '" + forced + "' has no driver implementation yet; only windows is available");
|
|
60
|
-
return new WindowsDriver();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
static int SelfTest()
|
|
64
|
-
{
|
|
65
|
-
Dictionary<string, object> ping = _driver.Ping(new Dictionary<string, object>());
|
|
66
|
-
Dictionary<string, object> wins = _driver.ListWindows(new Dictionary<string, object>());
|
|
67
|
-
Dictionary<string, object> report = new Dictionary<string, object>();
|
|
68
|
-
report["ok"] = true;
|
|
69
|
-
report["platform"] = _driver.PlatformName;
|
|
70
|
-
report["windowCount"] = wins["count"];
|
|
71
|
-
report["driver"] = ping;
|
|
72
|
-
Console.Out.Write(Json.ToJson(report));
|
|
73
|
-
Console.Out.Write('\n');
|
|
74
|
-
Console.Out.Flush();
|
|
75
|
-
return 0;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
static string Handle(string line)
|
|
79
|
-
{
|
|
80
|
-
long id = 0;
|
|
81
|
-
try
|
|
82
|
-
{
|
|
83
|
-
Dictionary<string, object> req = Json.AsObject(Json.Parse(line));
|
|
84
|
-
if (req == null) throw new ToolError("bad_request", "request must be a JSON object");
|
|
85
|
-
id = Json.GetLong(req, "id", 0);
|
|
86
|
-
string method = Json.GetString(req, "method");
|
|
87
|
-
Dictionary<string, object> p = Json.AsObject(Json.Get(req, "params"));
|
|
88
|
-
if (p == null) p = new Dictionary<string, object>();
|
|
89
|
-
// Codex ships the budget with every call (x-oai-cua-request-budget-ms) and the
|
|
90
|
-
// helper honours it; here it bounds capture waits and marks overruns.
|
|
91
|
-
Budget.Begin(Json.GetLong(p, "__budgetMs", 0), Json.GetString(p, "__cancelFile"));
|
|
92
|
-
try { if (_driver != null) _driver.Maintenance(); } catch { }
|
|
93
|
-
object result = Dispatch(method, p);
|
|
94
|
-
return Ok(id, result);
|
|
95
|
-
}
|
|
96
|
-
catch (ToolError te)
|
|
97
|
-
{
|
|
98
|
-
try { _driver.InvalidateState(new Dictionary<string, object>()); } catch { }
|
|
99
|
-
return Fail(id, te.Code, te.Message);
|
|
100
|
-
}
|
|
101
|
-
catch (Exception ex)
|
|
102
|
-
{
|
|
103
|
-
try { _driver.InvalidateState(new Dictionary<string, object>()); } catch { }
|
|
104
|
-
return Fail(id, "internal_error", ex.GetType().Name + ": " + ex.Message);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
static object Dispatch(string method, Dictionary<string, object> p)
|
|
109
|
-
{
|
|
110
|
-
if (string.IsNullOrEmpty(method)) throw new ToolError("bad_request", "request.method is required");
|
|
111
|
-
switch (method)
|
|
112
|
-
{
|
|
113
|
-
case "ping": return _driver.Ping(p);
|
|
114
|
-
case "hello": return _driver.Hello(p);
|
|
115
|
-
case "restore_cursor": return _driver.RestoreCursor(p);
|
|
116
|
-
case "close": return _driver.Close(p);
|
|
117
|
-
case "list_windows": return _driver.ListWindows(p);
|
|
118
|
-
case "list_apps": return _driver.ListApps(p);
|
|
119
|
-
case "get_window": return _driver.GetWindow(p);
|
|
120
|
-
case "launch_app": return _driver.LaunchApp(p);
|
|
121
|
-
case "get_window_state": return _driver.GetWindowState(p);
|
|
122
|
-
case "click": return _driver.Click(p);
|
|
123
|
-
case "press_key": return _driver.PressKey(p);
|
|
124
|
-
case "type_text": return _driver.TypeText(p);
|
|
125
|
-
case "scroll": return _driver.Scroll(p);
|
|
126
|
-
case "drag": return _driver.Drag(p);
|
|
127
|
-
case "set_value": return _driver.SetValue(p);
|
|
128
|
-
case "perform_secondary_action": return _driver.PerformSecondaryAction(p);
|
|
129
|
-
case "activate_window": return _driver.ActivateWindow(p);
|
|
130
|
-
case "overlay_show": return _driver.OverlayShow(p);
|
|
131
|
-
case "overlay_hide": return _driver.OverlayHide(p);
|
|
132
|
-
case "configure": return _driver.Configure(p);
|
|
133
|
-
case "approve_app": return _driver.ApproveApp(p);
|
|
134
|
-
case "end_turn": return _driver.EndTurn(p);
|
|
135
|
-
case "invalidate_state": return _driver.InvalidateState(p);
|
|
136
|
-
case "start_audio_recording": return _driver.StartAudioRecording(p);
|
|
137
|
-
case "stop_audio_recording": return _driver.StopAudioRecording(p);
|
|
138
|
-
case "shutdown": return new Dictionary<string, object> { { "bye", true } };
|
|
139
|
-
default: throw new ToolError("unknown_method", "unknown method '" + method + "'");
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
static string Ok(long id, object result)
|
|
144
|
-
{
|
|
145
|
-
Dictionary<string, object> d = new Dictionary<string, object>();
|
|
146
|
-
d["id"] = id;
|
|
147
|
-
d["ok"] = true;
|
|
148
|
-
d["result"] = result;
|
|
149
|
-
return Json.ToJson(d);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
static string Fail(long id, string code, string message)
|
|
153
|
-
{
|
|
154
|
-
Dictionary<string, object> err = new Dictionary<string, object>();
|
|
155
|
-
err["code"] = code;
|
|
156
|
-
err["message"] = message;
|
|
157
|
-
Dictionary<string, object> d = new Dictionary<string, object>();
|
|
158
|
-
d["id"] = id;
|
|
159
|
-
d["ok"] = false;
|
|
160
|
-
d["error"] = err;
|
|
161
|
-
return Json.ToJson(d);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
51
|
+
try { if (_driver != null) _driver.Dispose(); } catch { }
|
|
52
|
+
return RequestedExitCode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static IPlatformDriver CreateDriver()
|
|
56
|
+
{
|
|
57
|
+
string forced = Environment.GetEnvironmentVariable("DSH_CU_PLATFORM");
|
|
58
|
+
if (!string.IsNullOrEmpty(forced) && forced != "windows")
|
|
59
|
+
throw new NotSupportedException("platform '" + forced + "' has no driver implementation yet; only windows is available");
|
|
60
|
+
return new WindowsDriver();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static int SelfTest()
|
|
64
|
+
{
|
|
65
|
+
Dictionary<string, object> ping = _driver.Ping(new Dictionary<string, object>());
|
|
66
|
+
Dictionary<string, object> wins = _driver.ListWindows(new Dictionary<string, object>());
|
|
67
|
+
Dictionary<string, object> report = new Dictionary<string, object>();
|
|
68
|
+
report["ok"] = true;
|
|
69
|
+
report["platform"] = _driver.PlatformName;
|
|
70
|
+
report["windowCount"] = wins["count"];
|
|
71
|
+
report["driver"] = ping;
|
|
72
|
+
Console.Out.Write(Json.ToJson(report));
|
|
73
|
+
Console.Out.Write('\n');
|
|
74
|
+
Console.Out.Flush();
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static string Handle(string line)
|
|
79
|
+
{
|
|
80
|
+
long id = 0;
|
|
81
|
+
try
|
|
82
|
+
{
|
|
83
|
+
Dictionary<string, object> req = Json.AsObject(Json.Parse(line));
|
|
84
|
+
if (req == null) throw new ToolError("bad_request", "request must be a JSON object");
|
|
85
|
+
id = Json.GetLong(req, "id", 0);
|
|
86
|
+
string method = Json.GetString(req, "method");
|
|
87
|
+
Dictionary<string, object> p = Json.AsObject(Json.Get(req, "params"));
|
|
88
|
+
if (p == null) p = new Dictionary<string, object>();
|
|
89
|
+
// Codex ships the budget with every call (x-oai-cua-request-budget-ms) and the
|
|
90
|
+
// helper honours it; here it bounds capture waits and marks overruns.
|
|
91
|
+
Budget.Begin(Json.GetLong(p, "__budgetMs", 0), Json.GetString(p, "__cancelFile"));
|
|
92
|
+
try { if (_driver != null) _driver.Maintenance(); } catch { }
|
|
93
|
+
object result = Dispatch(method, p);
|
|
94
|
+
return Ok(id, result);
|
|
95
|
+
}
|
|
96
|
+
catch (ToolError te)
|
|
97
|
+
{
|
|
98
|
+
try { _driver.InvalidateState(new Dictionary<string, object>()); } catch { }
|
|
99
|
+
return Fail(id, te.Code, te.Message);
|
|
100
|
+
}
|
|
101
|
+
catch (Exception ex)
|
|
102
|
+
{
|
|
103
|
+
try { _driver.InvalidateState(new Dictionary<string, object>()); } catch { }
|
|
104
|
+
return Fail(id, "internal_error", ex.GetType().Name + ": " + ex.Message);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static object Dispatch(string method, Dictionary<string, object> p)
|
|
109
|
+
{
|
|
110
|
+
if (string.IsNullOrEmpty(method)) throw new ToolError("bad_request", "request.method is required");
|
|
111
|
+
switch (method)
|
|
112
|
+
{
|
|
113
|
+
case "ping": return _driver.Ping(p);
|
|
114
|
+
case "hello": return _driver.Hello(p);
|
|
115
|
+
case "restore_cursor": return _driver.RestoreCursor(p);
|
|
116
|
+
case "close": return _driver.Close(p);
|
|
117
|
+
case "list_windows": return _driver.ListWindows(p);
|
|
118
|
+
case "list_apps": return _driver.ListApps(p);
|
|
119
|
+
case "get_window": return _driver.GetWindow(p);
|
|
120
|
+
case "launch_app": return _driver.LaunchApp(p);
|
|
121
|
+
case "get_window_state": return _driver.GetWindowState(p);
|
|
122
|
+
case "click": return _driver.Click(p);
|
|
123
|
+
case "press_key": return _driver.PressKey(p);
|
|
124
|
+
case "type_text": return _driver.TypeText(p);
|
|
125
|
+
case "scroll": return _driver.Scroll(p);
|
|
126
|
+
case "drag": return _driver.Drag(p);
|
|
127
|
+
case "set_value": return _driver.SetValue(p);
|
|
128
|
+
case "perform_secondary_action": return _driver.PerformSecondaryAction(p);
|
|
129
|
+
case "activate_window": return _driver.ActivateWindow(p);
|
|
130
|
+
case "overlay_show": return _driver.OverlayShow(p);
|
|
131
|
+
case "overlay_hide": return _driver.OverlayHide(p);
|
|
132
|
+
case "configure": return _driver.Configure(p);
|
|
133
|
+
case "approve_app": return _driver.ApproveApp(p);
|
|
134
|
+
case "end_turn": return _driver.EndTurn(p);
|
|
135
|
+
case "invalidate_state": return _driver.InvalidateState(p);
|
|
136
|
+
case "start_audio_recording": return _driver.StartAudioRecording(p);
|
|
137
|
+
case "stop_audio_recording": return _driver.StopAudioRecording(p);
|
|
138
|
+
case "shutdown": return new Dictionary<string, object> { { "bye", true } };
|
|
139
|
+
default: throw new ToolError("unknown_method", "unknown method '" + method + "'");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static string Ok(long id, object result)
|
|
144
|
+
{
|
|
145
|
+
Dictionary<string, object> d = new Dictionary<string, object>();
|
|
146
|
+
d["id"] = id;
|
|
147
|
+
d["ok"] = true;
|
|
148
|
+
d["result"] = result;
|
|
149
|
+
return Json.ToJson(d);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static string Fail(long id, string code, string message)
|
|
153
|
+
{
|
|
154
|
+
Dictionary<string, object> err = new Dictionary<string, object>();
|
|
155
|
+
err["code"] = code;
|
|
156
|
+
err["message"] = message;
|
|
157
|
+
Dictionary<string, object> d = new Dictionary<string, object>();
|
|
158
|
+
d["id"] = id;
|
|
159
|
+
d["ok"] = false;
|
|
160
|
+
d["error"] = err;
|
|
161
|
+
return Json.ToJson(d);
|
|
162
|
+
}
|
|
163
|
+
}
|
package/native/audio/Program.cs
CHANGED
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
using System;
|
|
2
|
-
using System.IO;
|
|
3
|
-
using System.Threading;
|
|
4
|
-
using NAudio.Wave;
|
|
5
|
-
using NAudio.Wave.SampleProviders;
|
|
6
|
-
|
|
7
|
-
namespace DshCuAudio
|
|
8
|
-
{
|
|
9
|
-
/// WASAPI loopback recorder: captures whatever the machine is playing, which is
|
|
10
|
-
/// what Codex's start/stop_audio_recording expose (gated behind SKY_ENABLE_AUDIO).
|
|
11
|
-
/// Stops on max duration, on the helper's stop file, or on Ctrl+C.
|
|
12
|
-
internal static class Program
|
|
13
|
-
{
|
|
14
|
-
private static int Main(string[] args)
|
|
15
|
-
{
|
|
16
|
-
string output = Path.Combine(Path.GetTempPath(), "dsh-cu-audio.wav");
|
|
17
|
-
int maxMs = 60000;
|
|
18
|
-
string stopFile = null;
|
|
19
|
-
for (int i = 0; i < args.Length - 1; i++)
|
|
20
|
-
{
|
|
21
|
-
if (args[i] == "--out") output = args[i + 1];
|
|
22
|
-
else if (args[i] == "--max-ms") int.TryParse(args[i + 1], out maxMs);
|
|
23
|
-
else if (args[i] == "--stop-file") stopFile = args[i + 1];
|
|
24
|
-
}
|
|
25
|
-
if (maxMs < 500) maxMs = 500;
|
|
26
|
-
|
|
27
|
-
try
|
|
28
|
-
{
|
|
29
|
-
// Record at the device mix format first, then normalise to the 24 kHz
|
|
30
|
-
// stereo 16-bit WAV that Codex returns.
|
|
31
|
-
string devicePath = output + ".device.wav";
|
|
32
|
-
try { if (File.Exists(devicePath)) File.Delete(devicePath); } catch { }
|
|
33
|
-
using (var capture = new WasapiLoopbackCapture())
|
|
34
|
-
using (var writer = new WaveFileWriter(devicePath, capture.WaveFormat))
|
|
35
|
-
{
|
|
36
|
-
var stopped = new ManualResetEventSlim(false);
|
|
37
|
-
Exception failure = null;
|
|
38
|
-
capture.DataAvailable += (sender, e) =>
|
|
39
|
-
{
|
|
40
|
-
try
|
|
41
|
-
{
|
|
42
|
-
if (e.BytesRecorded > 0) writer.Write(e.Buffer, 0, e.BytesRecorded);
|
|
43
|
-
}
|
|
44
|
-
catch (Exception ex) { failure = ex; stopped.Set(); }
|
|
45
|
-
};
|
|
46
|
-
capture.RecordingStopped += (sender, e) => { stopped.Set(); };
|
|
47
|
-
|
|
48
|
-
capture.StartRecording();
|
|
49
|
-
Console.WriteLine("recording started: " + capture.WaveFormat.SampleRate + "Hz " +
|
|
50
|
-
capture.WaveFormat.Channels + "ch -> " + output);
|
|
51
|
-
|
|
52
|
-
var deadline = DateTime.UtcNow.AddMilliseconds(maxMs);
|
|
53
|
-
while (DateTime.UtcNow < deadline && !stopped.IsSet)
|
|
54
|
-
{
|
|
55
|
-
if (stopFile != null && File.Exists(stopFile)) break;
|
|
56
|
-
Thread.Sleep(150);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
capture.StopRecording();
|
|
60
|
-
stopped.Wait(3000);
|
|
61
|
-
writer.Flush();
|
|
62
|
-
|
|
63
|
-
if (failure != null) { Console.Error.WriteLine(failure.GetType().Name + ": " + failure.Message); return 6; }
|
|
64
|
-
|
|
65
|
-
writer.Dispose();
|
|
66
|
-
Normalize(devicePath, output);
|
|
67
|
-
try { File.Delete(devicePath); } catch { }
|
|
68
|
-
long bytes = new FileInfo(output).Length;
|
|
69
|
-
Console.WriteLine("recorded 24kHz stereo, " + bytes + " bytes -> " + output);
|
|
70
|
-
return 0;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch (Exception ex)
|
|
74
|
-
{
|
|
75
|
-
Console.Error.WriteLine(ex.GetType().Name + ": " + ex.Message);
|
|
76
|
-
return 5;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/// Resample/convert the device-format capture into 24 kHz stereo 16-bit PCM.
|
|
81
|
-
static void Normalize(string source, string destination)
|
|
82
|
-
{
|
|
83
|
-
using (var reader = new WaveFileReader(source))
|
|
84
|
-
{
|
|
85
|
-
ISampleProvider samples = reader.ToSampleProvider();
|
|
86
|
-
if (samples.WaveFormat.Channels == 1)
|
|
87
|
-
samples = new MonoToStereoSampleProvider(samples);
|
|
88
|
-
ISampleProvider resampled = samples.WaveFormat.SampleRate == 24000
|
|
89
|
-
? samples
|
|
90
|
-
: new WdlResamplingSampleProvider(samples, 24000);
|
|
91
|
-
WaveFileWriter.CreateWaveFile16(destination, resampled);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Threading;
|
|
4
|
+
using NAudio.Wave;
|
|
5
|
+
using NAudio.Wave.SampleProviders;
|
|
6
|
+
|
|
7
|
+
namespace DshCuAudio
|
|
8
|
+
{
|
|
9
|
+
/// WASAPI loopback recorder: captures whatever the machine is playing, which is
|
|
10
|
+
/// what Codex's start/stop_audio_recording expose (gated behind SKY_ENABLE_AUDIO).
|
|
11
|
+
/// Stops on max duration, on the helper's stop file, or on Ctrl+C.
|
|
12
|
+
internal static class Program
|
|
13
|
+
{
|
|
14
|
+
private static int Main(string[] args)
|
|
15
|
+
{
|
|
16
|
+
string output = Path.Combine(Path.GetTempPath(), "dsh-cu-audio.wav");
|
|
17
|
+
int maxMs = 60000;
|
|
18
|
+
string stopFile = null;
|
|
19
|
+
for (int i = 0; i < args.Length - 1; i++)
|
|
20
|
+
{
|
|
21
|
+
if (args[i] == "--out") output = args[i + 1];
|
|
22
|
+
else if (args[i] == "--max-ms") int.TryParse(args[i + 1], out maxMs);
|
|
23
|
+
else if (args[i] == "--stop-file") stopFile = args[i + 1];
|
|
24
|
+
}
|
|
25
|
+
if (maxMs < 500) maxMs = 500;
|
|
26
|
+
|
|
27
|
+
try
|
|
28
|
+
{
|
|
29
|
+
// Record at the device mix format first, then normalise to the 24 kHz
|
|
30
|
+
// stereo 16-bit WAV that Codex returns.
|
|
31
|
+
string devicePath = output + ".device.wav";
|
|
32
|
+
try { if (File.Exists(devicePath)) File.Delete(devicePath); } catch { }
|
|
33
|
+
using (var capture = new WasapiLoopbackCapture())
|
|
34
|
+
using (var writer = new WaveFileWriter(devicePath, capture.WaveFormat))
|
|
35
|
+
{
|
|
36
|
+
var stopped = new ManualResetEventSlim(false);
|
|
37
|
+
Exception failure = null;
|
|
38
|
+
capture.DataAvailable += (sender, e) =>
|
|
39
|
+
{
|
|
40
|
+
try
|
|
41
|
+
{
|
|
42
|
+
if (e.BytesRecorded > 0) writer.Write(e.Buffer, 0, e.BytesRecorded);
|
|
43
|
+
}
|
|
44
|
+
catch (Exception ex) { failure = ex; stopped.Set(); }
|
|
45
|
+
};
|
|
46
|
+
capture.RecordingStopped += (sender, e) => { stopped.Set(); };
|
|
47
|
+
|
|
48
|
+
capture.StartRecording();
|
|
49
|
+
Console.WriteLine("recording started: " + capture.WaveFormat.SampleRate + "Hz " +
|
|
50
|
+
capture.WaveFormat.Channels + "ch -> " + output);
|
|
51
|
+
|
|
52
|
+
var deadline = DateTime.UtcNow.AddMilliseconds(maxMs);
|
|
53
|
+
while (DateTime.UtcNow < deadline && !stopped.IsSet)
|
|
54
|
+
{
|
|
55
|
+
if (stopFile != null && File.Exists(stopFile)) break;
|
|
56
|
+
Thread.Sleep(150);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
capture.StopRecording();
|
|
60
|
+
stopped.Wait(3000);
|
|
61
|
+
writer.Flush();
|
|
62
|
+
|
|
63
|
+
if (failure != null) { Console.Error.WriteLine(failure.GetType().Name + ": " + failure.Message); return 6; }
|
|
64
|
+
|
|
65
|
+
writer.Dispose();
|
|
66
|
+
Normalize(devicePath, output);
|
|
67
|
+
try { File.Delete(devicePath); } catch { }
|
|
68
|
+
long bytes = new FileInfo(output).Length;
|
|
69
|
+
Console.WriteLine("recorded 24kHz stereo, " + bytes + " bytes -> " + output);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (Exception ex)
|
|
74
|
+
{
|
|
75
|
+
Console.Error.WriteLine(ex.GetType().Name + ": " + ex.Message);
|
|
76
|
+
return 5;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Resample/convert the device-format capture into 24 kHz stereo 16-bit PCM.
|
|
81
|
+
static void Normalize(string source, string destination)
|
|
82
|
+
{
|
|
83
|
+
using (var reader = new WaveFileReader(source))
|
|
84
|
+
{
|
|
85
|
+
ISampleProvider samples = reader.ToSampleProvider();
|
|
86
|
+
if (samples.WaveFormat.Channels == 1)
|
|
87
|
+
samples = new MonoToStereoSampleProvider(samples);
|
|
88
|
+
ISampleProvider resampled = samples.WaveFormat.SampleRate == 24000
|
|
89
|
+
? samples
|
|
90
|
+
: new WdlResamplingSampleProvider(samples, 24000);
|
|
91
|
+
WaveFileWriter.CreateWaveFile16(destination, resampled);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|