com.adrenak.univoice 4.3.0 → 4.5.0

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/README.md CHANGED
@@ -10,7 +10,7 @@ Some features of UniVoice:
10
10
  * 🌐 __Configurable Network__:
11
11
  - UniVoice is networking agnostic. Implement the `IAudioClient` and `IAudioServer` interfaces using the networking plugin of your choice to have it send audio data over any networking solution.
12
12
  - Built-in support for:
13
- - Mirror networking.
13
+ - Mirror networking
14
14
 
15
15
  * 🎤 __Configurable Audio Input__:
16
16
  - UniVoice is audio input agnostic. You can change the source of outgoing audio by implementing the `IAudioInput` interface.
@@ -50,13 +50,22 @@ Ensure you have the NPM registry in the `manifest.json` file of your Unity proje
50
50
  ```
51
51
  Then add `com.adrenak.univoice:x.y.z` to the `dependencies` in your `manifest.json` file (where x.y.z is the version you wish to install). The list of versions is available on [the UniVoice NPM page](https://www.npmjs.com/package/com.adrenak.univoice?activeTab=versions).
52
52
 
53
- ## Docs
54
- API reference is available here: http://www.vatsalambastha.com/univoice
53
+ ## Useful links
54
+ * API reference is available here: http://www.vatsalambastha.com/univoice
55
+ * UniVoice blog: https://blog.vatsalambastha.com/search/label/univoice
56
+ * Discord server: https://discord.gg/Un6Y2sQqqe
57
+
58
+ ## Integration
59
+ UniVoice isn't currently very drag-and-drop/low-code. The best way to integrate is to have some code perform a one time setup when your app starts and provides access to relevant objects that you can use throughout the rest of the apps runtime.
60
+
61
+ An example of this is the `UniVoiceMirrorSetupSample.cs` file that gives you access to an AudioServer that you can use in your server code and a ClientSession that you can use in your client code. For more see the "Samples" section below.
55
62
 
56
63
  ## Samples
57
- This repository contains a sample scene for the Mirror network, which is the best place to see how UniVoice can be integrated into your project.
64
+ This repository contains two samples:
65
+ * `UniVoiceMirrorSetupSample.cs` is a drag and drop component, a simple integration sample script. You can add it to your Mirror NetworkManager to get voice chat to work. No code required, it's as simple as that! It'll work as long as you have setup your project properly. For more instructions see the top of the `UniVoiceMirrorSetupSample.cs` file.
66
+ * A sample scene that shows the other clients in a UI as well as allows you to mute yourself/them. This sample is also Mirror based.
58
67
 
59
- > The sample relies on Mirror networking to work. Follow the instructions below for enabling Mirror in your project before trying it out.
68
+ > UniVoice currently only supports Mirror out of the box. All the samples rely on Mirror networking to work. Follow the instructions in the "Activating non-packaged dependencies" section below for enabling Mirror in your project before trying it out.
60
69
 
61
70
  ## Dependencies
62
71
  [com.adrenak.brw](https://www.github.com/adrenak/brw) for reading and writing messages for communication. See `MirrorServer.cs` and `MirrorClient.cs` where they're used.
@@ -0,0 +1,22 @@
1
+ using System;
2
+
3
+ namespace Adrenak.UniVoice {
4
+ /// <summary>
5
+ /// An audio input implementation that doesn't do anything.
6
+ /// Use this when the device doesn't have any input mode.
7
+ /// This is especially useful when setting up the ClientSession
8
+ /// object on a dedicated server that likely isn't going to have
9
+ /// and mic or other audio capture devices.
10
+ /// </summary>
11
+ public class EmptyAudioInput : IAudioInput {
12
+ public int Frequency => 1;
13
+
14
+ public int ChannelCount => 1;
15
+
16
+ public int SegmentRate => 1;
17
+
18
+ public event Action<AudioFrame> OnFrameReady;
19
+
20
+ public void Dispose() { }
21
+ }
22
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: f1db3d25938d78548877be424a98cdd5
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -29,27 +29,40 @@ namespace Adrenak.UniVoice.Networks {
29
29
  public event Action<int> OnPeerLeft;
30
30
  public event Action<int, AudioFrame> OnReceivedPeerAudioFrame;
31
31
 
32
+ readonly MirrorModeObserver mirrorEvents;
33
+
32
34
  public MirrorClient() {
33
35
  PeerIDs = new List<int>();
34
36
  YourVoiceSettings = new VoiceSettings();
35
37
 
36
- NetworkManager.singleton.transport.OnClientConnected += OnClientConnected;
37
- NetworkManager.singleton.transport.OnClientDisconnected += OnClientDisconnected;
38
+ mirrorEvents = MirrorModeObserver.New("for MirrorClient");
39
+ mirrorEvents.ModeChanged += OnModeChanged;
40
+
38
41
  NetworkClient.RegisterHandler<MirrorMessage>(OnReceivedMessage, false);
39
42
  }
40
43
 
41
44
  public void Dispose() {
42
- NetworkManager.singleton.transport.OnClientConnected -= OnClientConnected;
43
- NetworkManager.singleton.transport.OnClientDisconnected -= OnClientDisconnected;
44
- NetworkClient.UnregisterHandler<MirrorMessage>();
45
+ PeerIDs.Clear();
45
46
  }
46
47
 
47
- void OnClientConnected() {
48
+ void OnModeChanged(NetworkManagerMode oldMode, NetworkManagerMode newMode) {
49
+ // For some reason, handlers don't always work as expected when the connection mode changes
48
50
  NetworkClient.ReplaceHandler<MirrorMessage>(OnReceivedMessage);
51
+
52
+ bool clientOnlyToOffline = newMode == NetworkManagerMode.Offline && oldMode == NetworkManagerMode.ClientOnly;
53
+ bool hostToServerOnlyOrOffline = oldMode == NetworkManagerMode.Host;
54
+
55
+ if (clientOnlyToOffline || hostToServerOnlyOrOffline) {
56
+ // We unregister the handler only when the device was a client.
57
+ // If it was a Host that's now a ServerOnly, we still need the handler as it's used in MirrorServer
58
+ if (clientOnlyToOffline)
59
+ NetworkClient.UnregisterHandler<MirrorMessage>();
60
+
61
+ OnClientDisconnected();
62
+ }
49
63
  }
50
64
 
51
65
  void OnClientDisconnected() {
52
- NetworkClient.ReplaceHandler<MirrorMessage>(OnReceivedMessage);
53
66
  YourVoiceSettings = new VoiceSettings();
54
67
  var oldPeerIds = PeerIDs;
55
68
  PeerIDs.Clear();
@@ -59,7 +72,6 @@ namespace Adrenak.UniVoice.Networks {
59
72
  OnLeft?.Invoke();
60
73
  }
61
74
 
62
-
63
75
  void OnReceivedMessage(MirrorMessage msg) {
64
76
  var reader = new BytesReader(msg.data);
65
77
  var tag = reader.ReadString();
@@ -69,13 +81,16 @@ namespace Adrenak.UniVoice.Networks {
69
81
  // peers that are already connected to the server
70
82
  case MirrorMessageTags.PEER_INIT:
71
83
  ID = reader.ReadInt();
72
- Debug.unityLogger.Log(LogType.Log, TAG,
73
- $"Initialized with ID {ID}");
74
- OnJoined?.Invoke(ID, PeerIDs);
75
84
  PeerIDs = reader.ReadIntArray().ToList();
76
- if(PeerIDs.Count > 0)
77
- Debug.unityLogger.Log(LogType.Log, TAG,
78
- $"Peers {string.Join(", ", PeerIDs)}");
85
+
86
+ string log = $"Initialized with ID {ID}. ";
87
+ if (PeerIDs.Count > 0)
88
+ log += $"Peer list: {string.Join(", ", PeerIDs)}";
89
+ else
90
+ log += "There are currently no peers.";
91
+ Debug.unityLogger.Log(LogType.Log, TAG, log);
92
+
93
+ OnJoined?.Invoke(ID, PeerIDs);
79
94
  foreach (var peerId in PeerIDs)
80
95
  OnPeerJoined?.Invoke(peerId);
81
96
  break;
@@ -84,9 +99,9 @@ namespace Adrenak.UniVoice.Networks {
84
99
  case MirrorMessageTags.PEER_JOINED:
85
100
  var newPeerID = reader.ReadInt();
86
101
  if (!PeerIDs.Contains(newPeerID)) {
87
- Debug.unityLogger.Log(LogType.Log, TAG,
88
- $"Peer {newPeerID} has joined");
89
102
  PeerIDs.Add(newPeerID);
103
+ Debug.unityLogger.Log(LogType.Log, TAG,
104
+ $"Peer {newPeerID} joined. Peer list is now {string.Join(", ", PeerIDs)}");
90
105
  OnPeerJoined?.Invoke(newPeerID);
91
106
  }
92
107
  break;
@@ -95,9 +110,14 @@ namespace Adrenak.UniVoice.Networks {
95
110
  case MirrorMessageTags.PEER_LEFT:
96
111
  var leftPeerID = reader.ReadInt();
97
112
  if (PeerIDs.Contains(leftPeerID)) {
98
- Debug.unityLogger.Log(LogType.Log, TAG,
99
- $"Peer {leftPeerID} has left");
100
113
  PeerIDs.Remove(leftPeerID);
114
+ string log2 = $"Peer {leftPeerID} left. ";
115
+ if (PeerIDs.Count == 0)
116
+ log2 += "There are no peers anymore.";
117
+ else
118
+ log2 += $"Peer list is now {string.Join(", ", PeerIDs)}";
119
+
120
+ Debug.unityLogger.Log(LogType.Log, TAG, log2);
101
121
  OnPeerLeft?.Invoke(leftPeerID);
102
122
  }
103
123
  break;
@@ -11,6 +11,8 @@ namespace Adrenak.UniVoice.Networks {
11
11
  /// when it changes
12
12
  /// </summary>
13
13
  public class MirrorModeObserver : MonoBehaviour {
14
+ const string TAG = "[MirrorModeObserver]";
15
+
14
16
  /// <summary>
15
17
  /// Event fired when the Mirror NetworkManager changes the mode
16
18
  /// </summary>
@@ -25,8 +27,8 @@ namespace Adrenak.UniVoice.Networks {
25
27
  /// Creates a new instance of this class on a GameObject
26
28
  /// </summary>
27
29
  /// <returns></returns>
28
- public static MirrorModeObserver New() {
29
- var go = new GameObject("MirrorEventProvider");
30
+ public static MirrorModeObserver New(string name = "") {
31
+ var go = new GameObject($"MirrorEventProvider {name}");
30
32
  DontDestroyOnLoad(go);
31
33
  return go.AddComponent<MirrorModeObserver>();
32
34
  }
@@ -34,7 +36,12 @@ namespace Adrenak.UniVoice.Networks {
34
36
  void Update() {
35
37
  var newMode = NetworkManager.singleton.mode;
36
38
  if (lastMode != newMode) {
37
- ModeChanged?.Invoke(lastMode, newMode);
39
+ try {
40
+ ModeChanged?.Invoke(lastMode, newMode);
41
+ }
42
+ catch (Exception e) {
43
+ Debug.Log(LogType.Error, TAG, "Exception while handling Mirror Mode change: " + e);
44
+ }
38
45
  lastMode = newMode;
39
46
  }
40
47
  }
@@ -40,43 +40,64 @@ namespace Adrenak.UniVoice.Networks {
40
40
  ClientIDs = new List<int>();
41
41
  ClientVoiceSettings = new Dictionary<int, VoiceSettings>();
42
42
 
43
- mirrorEvents = MirrorModeObserver.New();
43
+ mirrorEvents = MirrorModeObserver.New("for MirrorServer");
44
44
  mirrorEvents.ModeChanged += OnModeChanged;
45
45
 
46
46
  NetworkServer.RegisterHandler<MirrorMessage>(OnReceivedMessage, false);
47
+ }
48
+
49
+ public void Dispose() {
50
+ mirrorEvents.ModeChanged -= OnModeChanged;
51
+ NetworkServer.UnregisterHandler<MirrorMessage>();
52
+ OnServerShutdown();
53
+ }
47
54
 
55
+ void OnServerStarted() {
48
56
  #if MIRROR_89_OR_NEWER
49
57
  NetworkManager.singleton.transport.OnServerConnectedWithAddress += OnServerConnected;
50
58
  #else
51
59
  NetworkManager.singleton.transport.OnServerConnected += OnServerConnected;
52
60
  #endif
53
61
  NetworkManager.singleton.transport.OnServerDisconnected += OnServerDisconnected;
62
+ OnServerStart?.Invoke();
54
63
  }
55
-
56
- void IDisposable.Dispose() {
57
- mirrorEvents.ModeChanged -= OnModeChanged;
58
-
59
- NetworkServer.UnregisterHandler<MirrorMessage>();
60
64
 
65
+ void OnServerShutdown() {
61
66
  #if MIRROR_89_OR_NEWER
62
67
  NetworkManager.singleton.transport.OnServerConnectedWithAddress -= OnServerConnected;
63
68
  #else
64
69
  NetworkManager.singleton.transport.OnServerConnected -= OnServerConnected;
65
- #endif
70
+ #endif
66
71
  NetworkManager.singleton.transport.OnServerDisconnected -= OnServerDisconnected;
72
+ ClientIDs.Clear();
73
+ ClientVoiceSettings.Clear();
74
+ OnServerStop?.Invoke();
67
75
  }
68
76
 
69
77
  void OnModeChanged(NetworkManagerMode oldMode, NetworkManagerMode newMode) {
78
+ // For some reason, handlers don't always work as expected when the connection mode changes
70
79
  NetworkServer.ReplaceHandler<MirrorMessage>(OnReceivedMessage, false);
71
80
 
72
- if((newMode == NetworkManagerMode.ServerOnly || newMode == NetworkManagerMode.Host)
73
- && (oldMode != NetworkManagerMode.ServerOnly && oldMode != NetworkManagerMode.Host)) {
74
- OnServerStart?.Invoke();
81
+ // If in Host mode, the server and internal client have both started and the client connects immediately.
82
+ // The host client seems to have ID 0 always, so we trigger a new client connection using id 0.
83
+ if(newMode == NetworkManagerMode.Host) {
84
+ OnServerStarted();
85
+ OnServerConnected(0, "localhost");
86
+ }
87
+ else if(newMode == NetworkManagerMode.ServerOnly) {
88
+ // If a Host changes to ServerOnly, we disconnect the internal client
89
+ if (oldMode == NetworkManagerMode.Host)
90
+ OnServerDisconnected(0);
91
+ // But if this machine is going from Offline to ServerOnly, only the server is starting
92
+ else if(oldMode == NetworkManagerMode.Offline)
93
+ OnServerStarted();
75
94
  }
76
- else if(newMode == NetworkManagerMode.Offline) {
77
- ClientIDs.Clear();
78
- ClientVoiceSettings.Clear();
79
- OnServerStop?.Invoke();
95
+ // If a Host or ServerOnly goes offline
96
+ else if(newMode == NetworkManagerMode.Offline && (oldMode == NetworkManagerMode.ServerOnly || oldMode == NetworkManagerMode.Host)) {
97
+ // We check if it was a Host before and disconnect the internal client
98
+ if (oldMode == NetworkManagerMode.Host)
99
+ OnServerDisconnected(0);
100
+ OnServerShutdown();
80
101
  }
81
102
  }
82
103
 
@@ -146,6 +167,7 @@ namespace Adrenak.UniVoice.Networks {
146
167
  #else
147
168
  void OnServerConnected(int connId) {
148
169
  #endif
170
+ // Not sure if this needs to be done, but being extra cautious here
149
171
  NetworkServer.ReplaceHandler<MirrorMessage>(OnReceivedMessage, false);
150
172
 
151
173
  Debug.unityLogger.Log(LogType.Log, TAG, $"Client {connId} connected");
@@ -172,9 +194,10 @@ namespace Adrenak.UniVoice.Networks {
172
194
  // required but I faced some issues with immediate initialization earlier.
173
195
  SendToClientDelayed(connId, newClientPacket.Bytes, Channels.Reliable, 100);
174
196
 
175
- string peerListString = string.Join(", ", otherPeerIDs);
176
- Debug.unityLogger.Log(LogType.Log, TAG,
177
- $"Initializing new client with ID {connId} and peer list {peerListString}");
197
+ string log = $"Initializing new client with ID {connId}";
198
+ if (otherPeerIDs.Length > 0)
199
+ log += $" and peer list {string.Join(", ", otherPeerIDs)}";
200
+ Debug.unityLogger.Log(LogType.Log, TAG, log);
178
201
  }
179
202
  // To the already existing peers, we let them know a new peer has joined
180
203
  // by sending the new peer ID to them.
@@ -191,11 +214,11 @@ namespace Adrenak.UniVoice.Networks {
191
214
  }
192
215
 
193
216
  void OnServerDisconnected(int connId) {
217
+ // Not sure if this needs to be done, but being extra cautious here
194
218
  NetworkServer.ReplaceHandler<MirrorMessage>(OnReceivedMessage, false);
195
219
 
196
220
  ClientIDs.Remove(connId);
197
- Debug.unityLogger.Log(LogType.Log, TAG,
198
- $"Client {connId} disconnected");
221
+ Debug.unityLogger.Log(LogType.Log, TAG, $"Client {connId} disconnected");
199
222
 
200
223
  // Notify all remaining peers that a peer has left
201
224
  foreach (var peerId in ClientIDs) {
@@ -0,0 +1,199 @@
1
+ using UnityEngine;
2
+
3
+ using Adrenak.UniMic;
4
+ using Adrenak.UniVoice.Networks;
5
+ using Adrenak.UniVoice.Outputs;
6
+ using Adrenak.UniVoice.Inputs;
7
+ using Adrenak.UniVoice.Filters;
8
+
9
+ namespace Adrenak.UniVoice.Samples {
10
+ /// <summary>
11
+ /// To get this setup sample to work, ensure that you have done the following:
12
+ /// - Import Mirror and add the UNIVOICE_NETWORK_MIRROR compilation symbol to your project
13
+ /// - If you want to use RNNoise filter, import RNNoise4Unity into your project and add UNIVOICE_FILTER_RNNOISE4UNITY
14
+ /// - Add this component to the first scene of your Unity project
15
+ ///
16
+ /// *** More info on adding and activating non packaged dependencies is here: https://github.com/adrenak/univoice?tab=readme-ov-file#activating-non-packaged-dependencies ***
17
+ ///
18
+ /// This is a basic integration script that uses the following to setup UniVoice:
19
+ /// - <see cref="MirrorServer"/>, an implementation of <see cref="IAudioServer{T}"/>
20
+ /// - <see cref="MirrorClient"/>, an implementation of <see cref="IAudioClient{T}"/>
21
+ /// - <see cref="UniMicInput"/>, an implementation of <see cref="IAudioInput"/> that captures audio from a mic
22
+ /// - <see cref="EmptyAudioInput"/>, an implementation of <see cref="IAudioInput"/> that is basically
23
+ /// an idle audio input used when there is no input device
24
+ /// - <see cref="RNNoiseFilter"/>, an implementation of <see cref="IAudioFilter"/> that removes noise from
25
+ /// captured audio.
26
+ /// - <see cref="ConcentusEncodeFilter"/>, an implementation of <see cref="IAudioFilter"/> that encodes captured audio
27
+ /// using Concentus (C# Opus) to reduce the size of audio frames
28
+ /// - <see cref="ConcentusDecodeFilter"/>, an implementation of <see cref="IAudioFilter"/> that decodes incoming audio
29
+ /// using Concentus to decode and make the audio frame playable.
30
+ /// </summary>
31
+ public class UniVoiceMirrorSetupSample : MonoBehaviour {
32
+ const string TAG = "[BasicUniVoiceSetupSample]";
33
+
34
+ /// <summary>
35
+ /// Whether UniVoice has been setup successfully. This field will return true if the setup was successful.
36
+ /// It runs on both server and client.
37
+ /// </summary>
38
+ public static bool HasSetUp { get; private set; }
39
+
40
+ /// <summary>
41
+ /// The server object.
42
+ /// </summary>
43
+ public static IAudioServer<int> AudioServer { get; private set; }
44
+
45
+ /// <summary>
46
+ /// The client session.
47
+ /// </summary>
48
+ public static ClientSession<int> ClientSession { get; private set; }
49
+
50
+ [SerializeField] bool useRNNoise4UnityIfAvailable = true;
51
+
52
+ [SerializeField] bool useConcentusEncodeAndDecode = true;
53
+
54
+ void Start() {
55
+ if (HasSetUp) {
56
+ Debug.unityLogger.Log(LogType.Log, TAG, "UniVoice is already set up. Ignoring...");
57
+ return;
58
+ }
59
+ HasSetUp = Setup();
60
+ }
61
+
62
+ bool Setup() {
63
+ Debug.unityLogger.Log(LogType.Log, TAG, "Trying to setup UniVoice");
64
+
65
+ bool failed = false;
66
+
67
+ // Set setup the AudioServer and ClientSession on ALL builds. This means that you'd
68
+ // have a ClientSession on a dedicated server, even though there's not much you can do with it.
69
+ // Similarly, a client would also have an AudioServer object. But it would just be inactive.
70
+ // This sample is for ease of use and to get something working quickly, so we don't bother
71
+ // with these minor details. Note that doing so does not have any performance implications
72
+ // so you can do this, so you could keep this approach without any tradeoffs.
73
+ var createdAudioServer = SetupAudioServer();
74
+ if (!createdAudioServer) {
75
+ Debug.unityLogger.Log(LogType.Error, TAG, "Could not setup UniVoice server.");
76
+ failed = true;
77
+ }
78
+
79
+ var setupAudioClient = SetupClientSession();
80
+ if (!setupAudioClient) {
81
+ Debug.unityLogger.Log(LogType.Error, TAG, "Could not setup UniVoice client.");
82
+ failed = true;
83
+ }
84
+
85
+ if (!failed)
86
+ Debug.unityLogger.Log(LogType.Log, TAG, "UniVoice successfully setup!");
87
+ else
88
+ Debug.unityLogger.Log(LogType.Error, TAG, $"Refer to the notes on top of {typeof(UniVoiceMirrorSetupSample).Name}.cs for setup instructions.");
89
+ return !failed;
90
+ }
91
+
92
+ bool SetupAudioServer() {
93
+ #if UNIVOICE_NETWORK_MIRROR
94
+ // ---- CREATE AUDIO SERVER AND SUBSCRIBE TO EVENTS TO PRINT LOGS ----
95
+ // We create a server. If this code runs in server mode, MirrorServer will take care
96
+ // or automatically handling all incoming messages. On a device connecting as a client,
97
+ // this code doesn't do anything.
98
+ AudioServer = new MirrorServer();
99
+ Debug.unityLogger.Log(LogType.Log, TAG, "Created MirrorServer object");
100
+
101
+ AudioServer.OnServerStart += () => {
102
+ Debug.unityLogger.Log(LogType.Log, TAG, "Server started");
103
+ };
104
+
105
+ AudioServer.OnServerStop += () => {
106
+ Debug.unityLogger.Log(LogType.Log, TAG, "Server stopped");
107
+ };
108
+ return true;
109
+ #else
110
+ Debug.unityLogger.Log(LogType.Error, TAG, "MirrorServer implementation not found!");
111
+ return false;
112
+ #endif
113
+ }
114
+
115
+ bool SetupClientSession() {
116
+ #if UNIVOICE_NETWORK_MIRROR
117
+ // ---- CREATE AUDIO CLIENT AND SUBSCRIBE TO EVENTS ----
118
+ IAudioClient<int> client = new MirrorClient();
119
+ client.OnJoined += (id, peerIds) => {
120
+ Debug.unityLogger.Log(LogType.Log, TAG, $"You are Peer ID {id}");
121
+ };
122
+
123
+ client.OnLeft += () => {
124
+ Debug.unityLogger.Log(LogType.Log, TAG, "You left the chatroom");
125
+ };
126
+
127
+ // When a peer joins, we instantiate a new peer view
128
+ client.OnPeerJoined += id => {
129
+ Debug.unityLogger.Log(LogType.Log, TAG, $"Peer {id} joined");
130
+ };
131
+
132
+ // When a peer leaves, destroy the UI representing them
133
+ client.OnPeerLeft += id => {
134
+ Debug.unityLogger.Log(LogType.Log, TAG, $"Peer {id} left");
135
+ };
136
+
137
+ Debug.unityLogger.Log(LogType.Log, TAG, "Created MirrorClient object");
138
+
139
+ // ---- CREATE AUDIO INPUT ----
140
+ IAudioInput input;
141
+ // Since in this sample we use microphone input via UniMic, we first check if there
142
+ // are any mic devices available.
143
+ Mic.Init(); // Must do this to use the Mic class
144
+ if (Mic.AvailableDevices.Count == 0) {
145
+ Debug.unityLogger.Log(LogType.Log, TAG, "Device has no microphones." +
146
+ "Will only be able to hear other clients, cannot send any audio.");
147
+ input = new EmptyAudioInput();
148
+ Debug.unityLogger.Log(LogType.Log, TAG, "Created EmptyAudioInput");
149
+ }
150
+ else {
151
+ // Get the first recording device that we have available and start it.
152
+ // Then we create a UniMicInput instance that requires the mic object
153
+ // For more info on UniMic refer to https://www.github.com/adrenak/unimic
154
+ var mic = Mic.AvailableDevices[0];
155
+ mic.StartRecording(60);
156
+ Debug.unityLogger.Log(LogType.Log, TAG, "Started recording with Mic device named." +
157
+ mic.Name + $" at frequency {mic.SamplingFrequency} with frame duration {mic.FrameDurationMS} ms.");
158
+ input = new UniMicInput(mic);
159
+ Debug.unityLogger.Log(LogType.Log, TAG, "Created UniMicInput");
160
+ }
161
+
162
+ // ---- CREATE AUDIO OUTPUT FACTORY ----
163
+ IAudioOutputFactory outputFactory;
164
+ // We want the incoming audio from peers to be played via the StreamedAudioSourceOutput
165
+ // implementation of IAudioSource interface. So we get the factory for it.
166
+ outputFactory = new StreamedAudioSourceOutput.Factory();
167
+ Debug.unityLogger.Log(LogType.Log, TAG, "Using StreamedAudioSourceOutput.Factory as output factory");
168
+
169
+ // ---- CREATE CLIENT SESSION AND ADD FILTERS TO IT ----
170
+ // With the client, input and output factory ready, we create create the client session
171
+ ClientSession = new ClientSession<int>(client, input, outputFactory);
172
+ Debug.unityLogger.Log(LogType.Log, TAG, "Created session");
173
+
174
+ #if UNIVOICE_FILTER_RNNOISE4UNITY
175
+ if(useRNNoise4UnityIfAvailable) {
176
+ // RNNoiseFilter to remove noise from captured audio
177
+ session.InputFilters.Add(new RNNoiseFilter());
178
+ Debug.unityLogger.Log(LogType.Log, TAG, "Registered RNNoiseFilter as an input filter");
179
+ }
180
+ #endif
181
+
182
+ if (useConcentusEncodeAndDecode) {
183
+ // ConcentureEncoder filter to encode captured audio that reduces the audio frame size
184
+ ClientSession.InputFilters.Add(new ConcentusEncodeFilter());
185
+ Debug.unityLogger.Log(LogType.Log, TAG, "Registered ConcentusEncodeFilter as an input filter");
186
+
187
+ // For incoming audio register the ConcentusDecodeFilter to decode the encoded audio received from other clients
188
+ ClientSession.OutputFilters.Add(new ConcentusDecodeFilter());
189
+ Debug.unityLogger.Log(LogType.Log, TAG, "Registered ConcentusDecodeFilter as an output filter");
190
+ }
191
+
192
+ return true;
193
+ #else
194
+ Debug.unityLogger.Log(LogType.Error, TAG, "MirrorClient implementation not found!");
195
+ return false;
196
+ #endif
197
+ }
198
+ }
199
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: d0dec1d2df214b44f8cb106b071695ce
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -0,0 +1,435 @@
1
+ %YAML 1.1
2
+ %TAG !u! tag:unity3d.com,2011:
3
+ --- !u!29 &1
4
+ OcclusionCullingSettings:
5
+ m_ObjectHideFlags: 0
6
+ serializedVersion: 2
7
+ m_OcclusionBakeSettings:
8
+ smallestOccluder: 5
9
+ smallestHole: 0.25
10
+ backfaceThreshold: 100
11
+ m_SceneGUID: 00000000000000000000000000000000
12
+ m_OcclusionCullingData: {fileID: 0}
13
+ --- !u!104 &2
14
+ RenderSettings:
15
+ m_ObjectHideFlags: 0
16
+ serializedVersion: 9
17
+ m_Fog: 0
18
+ m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
19
+ m_FogMode: 3
20
+ m_FogDensity: 0.01
21
+ m_LinearFogStart: 0
22
+ m_LinearFogEnd: 300
23
+ m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
24
+ m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
25
+ m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
26
+ m_AmbientIntensity: 1
27
+ m_AmbientMode: 0
28
+ m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
29
+ m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
30
+ m_HaloStrength: 0.5
31
+ m_FlareStrength: 1
32
+ m_FlareFadeSpeed: 3
33
+ m_HaloTexture: {fileID: 0}
34
+ m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
35
+ m_DefaultReflectionMode: 0
36
+ m_DefaultReflectionResolution: 128
37
+ m_ReflectionBounces: 1
38
+ m_ReflectionIntensity: 1
39
+ m_CustomReflection: {fileID: 0}
40
+ m_Sun: {fileID: 0}
41
+ m_IndirectSpecularColor: {r: 0.44657874, g: 0.49641275, b: 0.5748172, a: 1}
42
+ m_UseRadianceAmbientProbe: 0
43
+ --- !u!157 &3
44
+ LightmapSettings:
45
+ m_ObjectHideFlags: 0
46
+ serializedVersion: 12
47
+ m_GIWorkflowMode: 1
48
+ m_GISettings:
49
+ serializedVersion: 2
50
+ m_BounceScale: 1
51
+ m_IndirectOutputScale: 1
52
+ m_AlbedoBoost: 1
53
+ m_EnvironmentLightingMode: 0
54
+ m_EnableBakedLightmaps: 1
55
+ m_EnableRealtimeLightmaps: 0
56
+ m_LightmapEditorSettings:
57
+ serializedVersion: 12
58
+ m_Resolution: 2
59
+ m_BakeResolution: 40
60
+ m_AtlasSize: 1024
61
+ m_AO: 0
62
+ m_AOMaxDistance: 1
63
+ m_CompAOExponent: 1
64
+ m_CompAOExponentDirect: 0
65
+ m_ExtractAmbientOcclusion: 0
66
+ m_Padding: 2
67
+ m_LightmapParameters: {fileID: 0}
68
+ m_LightmapsBakeMode: 1
69
+ m_TextureCompression: 1
70
+ m_FinalGather: 0
71
+ m_FinalGatherFiltering: 1
72
+ m_FinalGatherRayCount: 256
73
+ m_ReflectionCompression: 2
74
+ m_MixedBakeMode: 2
75
+ m_BakeBackend: 1
76
+ m_PVRSampling: 1
77
+ m_PVRDirectSampleCount: 32
78
+ m_PVRSampleCount: 512
79
+ m_PVRBounces: 2
80
+ m_PVREnvironmentSampleCount: 256
81
+ m_PVREnvironmentReferencePointCount: 2048
82
+ m_PVRFilteringMode: 1
83
+ m_PVRDenoiserTypeDirect: 1
84
+ m_PVRDenoiserTypeIndirect: 1
85
+ m_PVRDenoiserTypeAO: 1
86
+ m_PVRFilterTypeDirect: 0
87
+ m_PVRFilterTypeIndirect: 0
88
+ m_PVRFilterTypeAO: 0
89
+ m_PVREnvironmentMIS: 1
90
+ m_PVRCulling: 1
91
+ m_PVRFilteringGaussRadiusDirect: 1
92
+ m_PVRFilteringGaussRadiusIndirect: 5
93
+ m_PVRFilteringGaussRadiusAO: 2
94
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
95
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
96
+ m_PVRFilteringAtrousPositionSigmaAO: 1
97
+ m_ExportTrainingData: 0
98
+ m_TrainingDataDestination: TrainingData
99
+ m_LightProbeSampleCountMultiplier: 4
100
+ m_LightingDataAsset: {fileID: 0}
101
+ m_LightingSettings: {fileID: 0}
102
+ --- !u!196 &4
103
+ NavMeshSettings:
104
+ serializedVersion: 2
105
+ m_ObjectHideFlags: 0
106
+ m_BuildSettings:
107
+ serializedVersion: 2
108
+ agentTypeID: 0
109
+ agentRadius: 0.5
110
+ agentHeight: 2
111
+ agentSlope: 45
112
+ agentClimb: 0.4
113
+ ledgeDropHeight: 0
114
+ maxJumpAcrossDistance: 0
115
+ minRegionArea: 2
116
+ manualCellSize: 0
117
+ cellSize: 0.16666667
118
+ manualTileSize: 0
119
+ tileSize: 256
120
+ accuratePlacement: 0
121
+ maxJobWorkers: 0
122
+ preserveTilesOutsideBounds: 0
123
+ debug:
124
+ m_Flags: 0
125
+ m_NavMeshData: {fileID: 0}
126
+ --- !u!1 &169645566
127
+ GameObject:
128
+ m_ObjectHideFlags: 0
129
+ m_CorrespondingSourceObject: {fileID: 0}
130
+ m_PrefabInstance: {fileID: 0}
131
+ m_PrefabAsset: {fileID: 0}
132
+ serializedVersion: 6
133
+ m_Component:
134
+ - component: {fileID: 169645568}
135
+ - component: {fileID: 169645567}
136
+ m_Layer: 0
137
+ m_Name: Directional Light
138
+ m_TagString: Untagged
139
+ m_Icon: {fileID: 0}
140
+ m_NavMeshLayer: 0
141
+ m_StaticEditorFlags: 0
142
+ m_IsActive: 1
143
+ --- !u!108 &169645567
144
+ Light:
145
+ m_ObjectHideFlags: 0
146
+ m_CorrespondingSourceObject: {fileID: 0}
147
+ m_PrefabInstance: {fileID: 0}
148
+ m_PrefabAsset: {fileID: 0}
149
+ m_GameObject: {fileID: 169645566}
150
+ m_Enabled: 1
151
+ serializedVersion: 10
152
+ m_Type: 1
153
+ m_Shape: 0
154
+ m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
155
+ m_Intensity: 1
156
+ m_Range: 10
157
+ m_SpotAngle: 30
158
+ m_InnerSpotAngle: 21.80208
159
+ m_CookieSize: 10
160
+ m_Shadows:
161
+ m_Type: 2
162
+ m_Resolution: -1
163
+ m_CustomResolution: -1
164
+ m_Strength: 1
165
+ m_Bias: 0.05
166
+ m_NormalBias: 0.4
167
+ m_NearPlane: 0.2
168
+ m_CullingMatrixOverride:
169
+ e00: 1
170
+ e01: 0
171
+ e02: 0
172
+ e03: 0
173
+ e10: 0
174
+ e11: 1
175
+ e12: 0
176
+ e13: 0
177
+ e20: 0
178
+ e21: 0
179
+ e22: 1
180
+ e23: 0
181
+ e30: 0
182
+ e31: 0
183
+ e32: 0
184
+ e33: 1
185
+ m_UseCullingMatrixOverride: 0
186
+ m_Cookie: {fileID: 0}
187
+ m_DrawHalo: 0
188
+ m_Flare: {fileID: 0}
189
+ m_RenderMode: 0
190
+ m_CullingMask:
191
+ serializedVersion: 2
192
+ m_Bits: 4294967295
193
+ m_RenderingLayerMask: 1
194
+ m_Lightmapping: 4
195
+ m_LightShadowCasterMode: 0
196
+ m_AreaSize: {x: 1, y: 1}
197
+ m_BounceIntensity: 1
198
+ m_ColorTemperature: 6570
199
+ m_UseColorTemperature: 0
200
+ m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
201
+ m_UseBoundingSphereOverride: 0
202
+ m_UseViewFrustumForShadowCasterCull: 1
203
+ m_ShadowRadius: 0
204
+ m_ShadowAngle: 0
205
+ --- !u!4 &169645568
206
+ Transform:
207
+ m_ObjectHideFlags: 0
208
+ m_CorrespondingSourceObject: {fileID: 0}
209
+ m_PrefabInstance: {fileID: 0}
210
+ m_PrefabAsset: {fileID: 0}
211
+ m_GameObject: {fileID: 169645566}
212
+ m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
213
+ m_LocalPosition: {x: 0, y: 3, z: 0}
214
+ m_LocalScale: {x: 1, y: 1, z: 1}
215
+ m_ConstrainProportionsScale: 0
216
+ m_Children: []
217
+ m_Father: {fileID: 0}
218
+ m_RootOrder: 1
219
+ m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
220
+ --- !u!1 &419491085
221
+ GameObject:
222
+ m_ObjectHideFlags: 0
223
+ m_CorrespondingSourceObject: {fileID: 0}
224
+ m_PrefabInstance: {fileID: 0}
225
+ m_PrefabAsset: {fileID: 0}
226
+ serializedVersion: 6
227
+ m_Component:
228
+ - component: {fileID: 419491088}
229
+ - component: {fileID: 419491087}
230
+ - component: {fileID: 419491086}
231
+ m_Layer: 0
232
+ m_Name: Main Camera
233
+ m_TagString: MainCamera
234
+ m_Icon: {fileID: 0}
235
+ m_NavMeshLayer: 0
236
+ m_StaticEditorFlags: 0
237
+ m_IsActive: 1
238
+ --- !u!81 &419491086
239
+ AudioListener:
240
+ m_ObjectHideFlags: 0
241
+ m_CorrespondingSourceObject: {fileID: 0}
242
+ m_PrefabInstance: {fileID: 0}
243
+ m_PrefabAsset: {fileID: 0}
244
+ m_GameObject: {fileID: 419491085}
245
+ m_Enabled: 1
246
+ --- !u!20 &419491087
247
+ Camera:
248
+ m_ObjectHideFlags: 0
249
+ m_CorrespondingSourceObject: {fileID: 0}
250
+ m_PrefabInstance: {fileID: 0}
251
+ m_PrefabAsset: {fileID: 0}
252
+ m_GameObject: {fileID: 419491085}
253
+ m_Enabled: 1
254
+ serializedVersion: 2
255
+ m_ClearFlags: 1
256
+ m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
257
+ m_projectionMatrixMode: 1
258
+ m_GateFitMode: 2
259
+ m_FOVAxisMode: 0
260
+ m_SensorSize: {x: 36, y: 24}
261
+ m_LensShift: {x: 0, y: 0}
262
+ m_FocalLength: 50
263
+ m_NormalizedViewPortRect:
264
+ serializedVersion: 2
265
+ x: 0
266
+ y: 0
267
+ width: 1
268
+ height: 1
269
+ near clip plane: 0.3
270
+ far clip plane: 1000
271
+ field of view: 60
272
+ orthographic: 0
273
+ orthographic size: 5
274
+ m_Depth: -1
275
+ m_CullingMask:
276
+ serializedVersion: 2
277
+ m_Bits: 4294967295
278
+ m_RenderingPath: -1
279
+ m_TargetTexture: {fileID: 0}
280
+ m_TargetDisplay: 0
281
+ m_TargetEye: 3
282
+ m_HDR: 1
283
+ m_AllowMSAA: 1
284
+ m_AllowDynamicResolution: 0
285
+ m_ForceIntoRT: 0
286
+ m_OcclusionCulling: 1
287
+ m_StereoConvergence: 10
288
+ m_StereoSeparation: 0.022
289
+ --- !u!4 &419491088
290
+ Transform:
291
+ m_ObjectHideFlags: 0
292
+ m_CorrespondingSourceObject: {fileID: 0}
293
+ m_PrefabInstance: {fileID: 0}
294
+ m_PrefabAsset: {fileID: 0}
295
+ m_GameObject: {fileID: 419491085}
296
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
297
+ m_LocalPosition: {x: 0, y: 1, z: -10}
298
+ m_LocalScale: {x: 1, y: 1, z: 1}
299
+ m_ConstrainProportionsScale: 0
300
+ m_Children: []
301
+ m_Father: {fileID: 0}
302
+ m_RootOrder: 0
303
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
304
+ --- !u!1 &1902381426
305
+ GameObject:
306
+ m_ObjectHideFlags: 0
307
+ m_CorrespondingSourceObject: {fileID: 0}
308
+ m_PrefabInstance: {fileID: 0}
309
+ m_PrefabAsset: {fileID: 0}
310
+ serializedVersion: 6
311
+ m_Component:
312
+ - component: {fileID: 1902381428}
313
+ - component: {fileID: 1902381427}
314
+ - component: {fileID: 1902381429}
315
+ - component: {fileID: 1902381430}
316
+ - component: {fileID: 1902381431}
317
+ m_Layer: 0
318
+ m_Name: NetworkManager
319
+ m_TagString: Untagged
320
+ m_Icon: {fileID: 0}
321
+ m_NavMeshLayer: 0
322
+ m_StaticEditorFlags: 0
323
+ m_IsActive: 1
324
+ --- !u!114 &1902381427
325
+ MonoBehaviour:
326
+ m_ObjectHideFlags: 0
327
+ m_CorrespondingSourceObject: {fileID: 0}
328
+ m_PrefabInstance: {fileID: 0}
329
+ m_PrefabAsset: {fileID: 0}
330
+ m_GameObject: {fileID: 1902381426}
331
+ m_Enabled: 1
332
+ m_EditorHideFlags: 0
333
+ m_Script: {fileID: 11500000, guid: 8aab4c8111b7c411b9b92cf3dbc5bd4e, type: 3}
334
+ m_Name:
335
+ m_EditorClassIdentifier:
336
+ dontDestroyOnLoad: 1
337
+ runInBackground: 1
338
+ headlessStartMode: 1
339
+ editorAutoStart: 0
340
+ sendRate: 60
341
+ offlineScene:
342
+ onlineScene:
343
+ offlineSceneLoadDelay: 0
344
+ transport: {fileID: 1902381429}
345
+ networkAddress: localhost
346
+ maxConnections: 100
347
+ disconnectInactiveConnections: 0
348
+ disconnectInactiveTimeout: 60
349
+ authenticator: {fileID: 0}
350
+ playerPrefab: {fileID: 0}
351
+ autoCreatePlayer: 0
352
+ playerSpawnMethod: 0
353
+ spawnPrefabs: []
354
+ exceptionsDisconnect: 1
355
+ snapshotSettings:
356
+ bufferTimeMultiplier: 2
357
+ bufferLimit: 32
358
+ catchupNegativeThreshold: -1
359
+ catchupPositiveThreshold: 1
360
+ catchupSpeed: 0.019999999552965164
361
+ slowdownSpeed: 0.03999999910593033
362
+ driftEmaDuration: 1
363
+ dynamicAdjustment: 1
364
+ dynamicAdjustmentTolerance: 1
365
+ deliveryTimeEmaDuration: 2
366
+ evaluationMethod: 0
367
+ evaluationInterval: 3
368
+ timeInterpolationGui: 0
369
+ --- !u!4 &1902381428
370
+ Transform:
371
+ m_ObjectHideFlags: 0
372
+ m_CorrespondingSourceObject: {fileID: 0}
373
+ m_PrefabInstance: {fileID: 0}
374
+ m_PrefabAsset: {fileID: 0}
375
+ m_GameObject: {fileID: 1902381426}
376
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
377
+ m_LocalPosition: {x: 0, y: 0, z: 0}
378
+ m_LocalScale: {x: 1, y: 1, z: 1}
379
+ m_ConstrainProportionsScale: 0
380
+ m_Children: []
381
+ m_Father: {fileID: 0}
382
+ m_RootOrder: 2
383
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
384
+ --- !u!114 &1902381429
385
+ MonoBehaviour:
386
+ m_ObjectHideFlags: 0
387
+ m_CorrespondingSourceObject: {fileID: 0}
388
+ m_PrefabInstance: {fileID: 0}
389
+ m_PrefabAsset: {fileID: 0}
390
+ m_GameObject: {fileID: 1902381426}
391
+ m_Enabled: 1
392
+ m_EditorHideFlags: 0
393
+ m_Script: {fileID: 11500000, guid: c7424c1070fad4ba2a7a96b02fbeb4bb, type: 3}
394
+ m_Name:
395
+ m_EditorClassIdentifier:
396
+ port: 7777
397
+ NoDelay: 1
398
+ SendTimeout: 5000
399
+ ReceiveTimeout: 30000
400
+ serverMaxMessageSize: 16384
401
+ serverMaxReceivesPerTick: 10000
402
+ serverSendQueueLimitPerConnection: 10000
403
+ serverReceiveQueueLimitPerConnection: 10000
404
+ clientMaxMessageSize: 16384
405
+ clientMaxReceivesPerTick: 1000
406
+ clientSendQueueLimit: 10000
407
+ clientReceiveQueueLimit: 10000
408
+ --- !u!114 &1902381430
409
+ MonoBehaviour:
410
+ m_ObjectHideFlags: 0
411
+ m_CorrespondingSourceObject: {fileID: 0}
412
+ m_PrefabInstance: {fileID: 0}
413
+ m_PrefabAsset: {fileID: 0}
414
+ m_GameObject: {fileID: 1902381426}
415
+ m_Enabled: 1
416
+ m_EditorHideFlags: 0
417
+ m_Script: {fileID: 11500000, guid: d0dec1d2df214b44f8cb106b071695ce, type: 3}
418
+ m_Name:
419
+ m_EditorClassIdentifier:
420
+ useRNNoise4UnityIfAvailable: 1
421
+ useConcentusEncodeAndDecode: 1
422
+ --- !u!114 &1902381431
423
+ MonoBehaviour:
424
+ m_ObjectHideFlags: 0
425
+ m_CorrespondingSourceObject: {fileID: 0}
426
+ m_PrefabInstance: {fileID: 0}
427
+ m_PrefabAsset: {fileID: 0}
428
+ m_GameObject: {fileID: 1902381426}
429
+ m_Enabled: 1
430
+ m_EditorHideFlags: 0
431
+ m_Script: {fileID: 11500000, guid: 6442dc8070ceb41f094e44de0bf87274, type: 3}
432
+ m_Name:
433
+ m_EditorClassIdentifier:
434
+ offsetX: 0
435
+ offsetY: 0
@@ -0,0 +1,7 @@
1
+ fileFormatVersion: 2
2
+ guid: e2397deac1a1ddc489d2e846a22708fa
3
+ DefaultImporter:
4
+ externalObjects: {}
5
+ userData:
6
+ assetBundleName:
7
+ assetBundleVariant:
@@ -0,0 +1,8 @@
1
+ fileFormatVersion: 2
2
+ guid: 7974694d7a30f894db7efac8d8d46573
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.adrenak.univoice",
3
- "version": "4.3.0",
3
+ "version": "4.5.0",
4
4
  "displayName": "Adrenak.UniVoice",
5
5
  "description": "Voice chat/VoIP framework for Unity.",
6
6
  "unity": "2021.2",