Skip to content

Commit 4d01be7

Browse files
committed
Merge branch 'dev' into main
2 parents 6fc8c27 + cbfc78a commit 4d01be7

File tree

15 files changed

+387
-212
lines changed

15 files changed

+387
-212
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ Note: The VRCOSC-Controls.unitypackage prefab is global controls for VRCOSC. It
5757
| Speech To Text | Run Speech To Text using any language model and display the result in your ChatBox | |
5858
| HypeRate | Connects to [HypeRate.io](https://www.hyperate.io/) to display your live heartrate in-game - [Supported Devices](https://www.hyperate.io/supported-devices) | VRCOSC-Heartrate.unitypackage |
5959
| Pulsoid | Connects to [Pulsoid](https://pulsoid.net/) to display your live heartrate in-game - [Supported Devices](https://www.blog.pulsoid.net/monitors) | VRCOSC-Heartrate.unitypackage |
60+
| PiShock | Allow you to control groups of PiShock shockers directly from your avatar | VRCOSC-PiShock.unitypackage |
61+
| Process Manager | Open and close apps on your PC using avatar parameters | |
6062
| Haptic Control | Allows for triggering controller haptics using variables from your avatar | |
6163
| OpenVR Statistics | Gets statistics from your OpenVR (SteamVR) session | VRCOSC-Trackers.unitypackage |
6264
| OpenVR Controller Statistics | Gets controller statistics from your OpenVR (SteamVR) session | |
@@ -66,6 +68,7 @@ Note: The VRCOSC-Controls.unitypackage prefab is global controls for VRCOSC. It
6668
| ChatBox Text | Displays custom text in the ChatBox that can also function like a ticker tape | |
6769
| Clock | Sends your local time as hours, minutes, and seconds to be displayed on a wrist watch | VRCOSC-Watch.unitypackage |
6870
| Discord | Allows for toggling of mute and deafen from the action menu | VRCOSC-Discord.unitypackage |
71+
| Exchange Rate | Retrieves exchange rate information for IRL currencies and displays them in the ChatBox | |
6972
| Random (Bool/Float/Int) | Sends a random value with adjustable update rate | |
7073

7174
## License

VRCOSC.Desktop/VRCOSC.Desktop.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
<ApplicationIcon>game.ico</ApplicationIcon>
77
<ApplicationManifest>app.manifest</ApplicationManifest>
88
<Version>0.0.0</Version>
9-
<FileVersion>2023.703.0</FileVersion>
9+
<FileVersion>2023.706.0</FileVersion>
1010
<Title>VRCOSC</Title>
1111
<Authors>VolcanicArts</Authors>
1212
<Company>VolcanicArts</Company>
1313
<Nullable>enable</Nullable>
14-
<AssemblyVersion>2023.703.0</AssemblyVersion>
14+
<AssemblyVersion>2023.706.0</AssemblyVersion>
1515
</PropertyGroup>
1616
<ItemGroup Label="Project References">
1717
<ProjectReference Include="..\VRCOSC.Game\VRCOSC.Game.csproj" />

VRCOSC.Game/App/AppManager.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Net;
78
using System.Threading.Tasks;
89
using osu.Framework.Allocation;
910
using osu.Framework.Bindables;
1011
using osu.Framework.Development;
12+
using osu.Framework.Extensions.IEnumerableExtensions;
1113
using osu.Framework.Graphics;
1214
using osu.Framework.Logging;
1315
using osu.Framework.Platform;
@@ -16,6 +18,7 @@
1618
using VRCOSC.Game.Config;
1719
using VRCOSC.Game.Graphics.Notifications;
1820
using VRCOSC.Game.Managers;
21+
using VRCOSC.Game.Modules;
1922
using VRCOSC.Game.OpenVR;
2023
using VRCOSC.Game.OpenVR.Metadata;
2124
using VRCOSC.Game.OSC;
@@ -42,6 +45,7 @@ public partial class AppManager : Component
4245
private static readonly TimeSpan vrchat_check_interval = TimeSpan.FromSeconds(5);
4346

4447
private readonly Queue<VRChatOscData> oscDataQueue = new();
48+
private ScheduledDelegate? runningModulesDelegate;
4549

4650
public readonly Bindable<AppManagerState> State = new(AppManagerState.Stopped);
4751
public readonly ModuleManager ModuleManager;
@@ -188,6 +192,26 @@ private void processControlParameters(VRChatOscData data)
188192
}
189193
}
190194

195+
private void scheduleModuleEnabledParameters()
196+
{
197+
runningModulesDelegate = Scheduler.AddDelayed(() =>
198+
{
199+
ModuleManager.Modules.ForEach(module => sendModuleRunningState(module, ModuleManager.GetRunningModules().Contains(module)));
200+
}, TimeSpan.FromSeconds(1).TotalMilliseconds, true);
201+
}
202+
203+
private void cancelRunningModulesDelegate()
204+
{
205+
runningModulesDelegate?.Cancel();
206+
runningModulesDelegate = null;
207+
ModuleManager.Modules.ForEach(module => sendModuleRunningState(module, false));
208+
}
209+
210+
private void sendModuleRunningState(Module module, bool running)
211+
{
212+
OSCClient.SendValue($"{VRChatOscConstants.ADDRESS_AVATAR_PARAMETERS_PREFIX}/VRCOSC/Modules/{module.GetType().Name.Replace("Module", string.Empty)}", running);
213+
}
214+
191215
#endregion
192216

193217
#region Start
@@ -205,6 +229,7 @@ public void Start()
205229
StartupManager.Start();
206230
enableOSCFlag(OscClientFlag.Send);
207231
ModuleManager.Start();
232+
scheduleModuleEnabledParameters();
208233
sendControlParameters();
209234
startOSCRouter();
210235
enableOSCFlag(OscClientFlag.Receive);
@@ -282,6 +307,7 @@ public async Task StopAsync()
282307

283308
await OSCClient.Disable(OscClientFlag.Receive);
284309
await OSCRouter.Disable();
310+
cancelRunningModulesDelegate();
285311
ModuleManager.Stop();
286312
await OSCClient.Disable(OscClientFlag.Send);
287313
ChatBoxManager.Teardown();

VRCOSC.Game/Managers/ModuleManager.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ public void PlayerUpdate()
199199

200200
public Module? GetModule(string serialisedName) => Modules.SingleOrDefault(module => module.SerialisedName == serialisedName);
201201

202+
public IEnumerable<Module> GetRunningModules() => runningModulesCache;
203+
202204
public IEnumerable<string> GetEnabledModuleNames() => Modules.Where(module => module.Enabled.Value).Select(module => module.SerialisedName);
203205

204206
public string GetModuleName(string serialisedName) => Modules.Single(module => module.SerialisedName == serialisedName).Title;

VRCOSC.Game/Modules/Module.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ protected List<T> GetSettingList<T>(Enum lookup)
396396
{
397397
var setting = Settings[lookup.ToLookup()];
398398

399-
if (setting.GetType().IsSubclassOf(typeof(ModuleAttributePrimitiveList<>)))
399+
if (setting.GetType().IsSubclassOf(typeof(ModuleAttributePrimitiveList<T>)))
400400
{
401401
if (setting is ModuleAttributeList<Bindable<T>> settingList)
402402
{
@@ -411,7 +411,7 @@ protected List<T> GetSettingList<T>(Enum lookup)
411411
}
412412
}
413413

414-
throw new InvalidCastException($"Setting with lookup '{lookup}' is not of type '{nameof(List<T>)}'");
414+
throw new InvalidCastException($"Setting with lookup '{lookup}' is not of type List<'{typeof(T)}>'");
415415
}
416416

417417
protected T GetSetting<T>(Enum lookup)

VRCOSC.Game/Providers/PiShock/PiShockProvider.cs

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,43 +17,38 @@ public class PiShockProvider
1717
private const string info_api_url = base_api_url + "/GetShockerInfo";
1818

1919
private readonly HttpClient client = new();
20-
private readonly string username;
21-
private readonly string apiKey;
2220

23-
public PiShockProvider(string username, string apiKey)
21+
public async Task<PiShockResponse> Execute(string username, string apiKey, string sharecode, PiShockMode mode, int duration, int intensity)
2422
{
25-
this.username = username;
26-
this.apiKey = apiKey;
27-
}
23+
if (string.IsNullOrEmpty(username)) return new PiShockResponse(false, "Invalid username");
24+
if (string.IsNullOrEmpty(apiKey)) return new PiShockResponse(false, "Invalid API key");
2825

29-
public async Task<PiShockResponse> Execute(string sharecode, PiShockMode mode, int duration, int intensity)
30-
{
3126
if (duration is < 1 or > 15) throw new InvalidOperationException($"{nameof(duration)} must be between 1 and 15");
3227
if (intensity is < 1 or > 100) throw new InvalidOperationException($"{nameof(intensity)} must be between 1 and 100");
3328

34-
var shocker = await RetrieveShockerInfo(sharecode);
29+
var shocker = await retrieveShockerInfo(username, apiKey, sharecode);
3530
if (shocker is null) return new PiShockResponse(false, "Shocker does not exist");
3631

3732
duration = Math.Min(duration, shocker.MaxDuration);
3833
intensity = Math.Min(intensity, shocker.MaxIntensity);
3934

4035
var request = getRequestForMode(mode, duration, intensity);
4136
request.AppName = app_name;
42-
request.APIKey = apiKey;
4337
request.Username = username;
38+
request.APIKey = apiKey;
4439
request.ShareCode = sharecode;
4540

4641
var response = await client.PostAsync(action_api_url, new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"));
4742
var responseString = await response.Content.ReadAsStringAsync();
4843
return new PiShockResponse(responseString == "Operation Succeeded.", responseString);
4944
}
5045

51-
public async Task<PiShockShocker?> RetrieveShockerInfo(string sharecode)
46+
private async Task<PiShockShocker?> retrieveShockerInfo(string username, string apiKey, string sharecode)
5247
{
5348
var request = new ShockerInfoPiShockRequest
5449
{
55-
APIKey = apiKey,
5650
Username = username,
51+
APIKey = apiKey,
5752
ShareCode = sharecode
5853
};
5954

VRCOSC.Modules/SpeechToText/MicrophoneInterface.cs renamed to VRCOSC.Game/Providers/SpeechToText/MicrophoneHook.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License.
22
// See the LICENSE file in the repository root for full license text.
33

4+
using System;
45
using NAudio.CoreAudioApi;
56
using NAudio.Wave;
6-
using VRCOSC.Game;
77

8-
namespace VRCOSC.Modules.SpeechToText;
8+
namespace VRCOSC.Game.Providers.SpeechToText;
99

10-
public class MicrophoneInterface
10+
public class MicrophoneHook
1111
{
1212
public WasapiCapture? AudioCapture;
1313

@@ -33,6 +33,7 @@ public class MicrophoneInterface
3333
public void UnHook()
3434
{
3535
AudioCapture?.StopRecording();
36+
AudioCapture = null;
3637
}
3738

3839
private void handleAudioCaptureBuffer(object? sender, WaveInEventArgs e)
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License.
2+
// See the LICENSE file in the repository root for full license text.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Newtonsoft.Json;
10+
using Vosk;
11+
12+
namespace VRCOSC.Game.Providers.SpeechToText;
13+
14+
public class SpeechToTextProvider
15+
{
16+
public Action? OnBeforeAnalysis;
17+
public Action<bool, string>? OnFinalResult;
18+
public Action<string>? OnPartialResult;
19+
public Action<string>? OnLog;
20+
21+
private readonly object analyseLock = new();
22+
private MicrophoneHook? microphoneHook;
23+
private Model? model;
24+
private VoskRecognizer? recogniser;
25+
private bool readyToAccept;
26+
27+
private string modelDirectoryPath = null!;
28+
29+
public bool AnalysisEnabled { get; set; } = true;
30+
public float RequiredConfidence { get; set; }
31+
32+
public SpeechToTextProvider()
33+
{
34+
Vosk.Vosk.SetLogLevel(-1);
35+
}
36+
37+
public void Initialise(string modelDirectoryPath)
38+
{
39+
if (!Directory.Exists(modelDirectoryPath))
40+
{
41+
OnLog?.Invoke("Model directory not found");
42+
return;
43+
}
44+
45+
this.modelDirectoryPath = modelDirectoryPath;
46+
47+
Task.Run(() =>
48+
{
49+
initialiseMicrophoneCapture();
50+
initialiseVosk();
51+
52+
readyToAccept = true;
53+
});
54+
}
55+
56+
public void Teardown()
57+
{
58+
readyToAccept = false;
59+
microphoneHook?.UnHook();
60+
microphoneHook = null;
61+
62+
lock (analyseLock)
63+
{
64+
model?.Dispose();
65+
recogniser?.Dispose();
66+
}
67+
}
68+
69+
private void initialiseMicrophoneCapture()
70+
{
71+
OnLog?.Invoke("Hooking into default microphone...");
72+
73+
microphoneHook = new MicrophoneHook();
74+
microphoneHook.BufferCallback += analyseAudio;
75+
var captureDevice = microphoneHook.Hook();
76+
77+
if (captureDevice is null)
78+
{
79+
OnLog?.Invoke("Failed to hook into default microphone. Please restart the module");
80+
return;
81+
}
82+
83+
OnLog?.Invoke($"Hooked into microphone {captureDevice.DeviceFriendlyName.Trim()}");
84+
}
85+
86+
private void initialiseVosk()
87+
{
88+
lock (analyseLock)
89+
{
90+
model = new Model(modelDirectoryPath);
91+
recogniser = new VoskRecognizer(model, microphoneHook!.AudioCapture!.WaveFormat.SampleRate);
92+
recogniser.SetWords(true);
93+
}
94+
}
95+
96+
private void analyseAudio(byte[] buffer, int bytesRecorded)
97+
{
98+
OnBeforeAnalysis?.Invoke();
99+
100+
if (!AnalysisEnabled || !readyToAccept) return;
101+
102+
lock (analyseLock)
103+
{
104+
if (recogniser is null) return;
105+
106+
var isFinalResult = recogniser.AcceptWaveform(buffer, bytesRecorded);
107+
108+
if (isFinalResult)
109+
handleFinalRecognition();
110+
else
111+
handlePartialRecognition();
112+
}
113+
}
114+
115+
private void handlePartialRecognition()
116+
{
117+
var partialResult = JsonConvert.DeserializeObject<PartialRecognition>(recogniser!.PartialResult())?.Text;
118+
if (string.IsNullOrEmpty(partialResult)) return;
119+
120+
OnPartialResult?.Invoke(partialResult);
121+
}
122+
123+
private void handleFinalRecognition()
124+
{
125+
var result = JsonConvert.DeserializeObject<Recognition>(recogniser!.Result());
126+
127+
if (result is not null)
128+
{
129+
if (result.IsValid && result.AverageConfidence >= RequiredConfidence)
130+
{
131+
OnLog?.Invoke($"Recognised '{result.Text}'");
132+
OnFinalResult?.Invoke(true, result.Text);
133+
}
134+
}
135+
else
136+
{
137+
OnFinalResult?.Invoke(false, string.Empty);
138+
}
139+
140+
recogniser?.Reset();
141+
}
142+
143+
private class Recognition
144+
{
145+
[JsonProperty("text")]
146+
public string Text = string.Empty;
147+
148+
[JsonProperty("result")]
149+
public List<WordResult>? Result;
150+
151+
public float AverageConfidence => Result is null || !Result.Any() ? 0f : Result.Average(wordResult => wordResult.Confidence);
152+
public bool IsValid => (AverageConfidence != 0f || !string.IsNullOrEmpty(Text)) && Text != "huh";
153+
}
154+
155+
private class WordResult
156+
{
157+
[JsonProperty("conf")]
158+
public float Confidence;
159+
}
160+
161+
private class PartialRecognition
162+
{
163+
[JsonProperty("partial")]
164+
public string Text = string.Empty;
165+
}
166+
}

VRCOSC.Game/VRCOSC.Game.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<Nullable>enable</Nullable>
55
<LangVersion>11</LangVersion>
66
<PackageId>VolcanicArts.VRCOSC.SDK</PackageId>
7-
<Version>2023.703.0</Version>
7+
<Version>2023.706.0</Version>
88
<Title>VRCOSC SDK</Title>
99
<Authors>VolcanicArts</Authors>
1010
<Description>SDK for creating custom modules with VRCOSC</Description>

0 commit comments

Comments
 (0)