├── .github └── workflows │ ├── ci.yml │ └── documentation.yml ├── .gitignore ├── .vsconfig ├── Assets ├── Adrenak.UniVoice.meta └── Adrenak.UniVoice │ ├── LICENSE │ ├── LICENSE.meta │ ├── README.md │ ├── README.md.meta │ ├── Runtime.meta │ ├── Runtime │ ├── Adrenak.UniVoice.Runtime.asmdef │ ├── Adrenak.UniVoice.Runtime.asmdef.meta │ ├── ClientSession.cs │ ├── ClientSession.cs.meta │ ├── Common.meta │ ├── Common │ │ ├── SimpleVad.cs │ │ ├── SimpleVad.cs.meta │ │ ├── Utils.cs │ │ ├── Utils.cs.meta │ │ ├── WavFileWriter.cs │ │ └── WavFileWriter.cs.meta │ ├── Impl.meta │ ├── Impl │ │ ├── Filters.meta │ │ ├── Filters │ │ │ ├── Concentus Opus Filters.meta │ │ │ ├── Concentus Opus Filters │ │ │ │ ├── ConcentusDecodeFilter.cs │ │ │ │ ├── ConcentusDecodeFilter.cs.meta │ │ │ │ ├── ConcentusEncodeFilter.cs │ │ │ │ ├── ConcentusEncodeFilter.cs.meta │ │ │ │ ├── ConcentusFrequencies.cs │ │ │ │ └── ConcentusFrequencies.cs.meta │ │ │ ├── GaussianAudioBlur.cs │ │ │ ├── GaussianAudioBlur.cs.meta │ │ │ ├── RNNoise.meta │ │ │ ├── RNNoise │ │ │ │ ├── RNNoiseFilter.cs │ │ │ │ └── RNNoiseFilter.cs.meta │ │ │ ├── SimpleVadFilter.cs │ │ │ └── SimpleVadFilter.cs.meta │ │ ├── Inputs.meta │ │ ├── Inputs │ │ │ ├── EmptyAudioInput.cs │ │ │ ├── EmptyAudioInput.cs.meta │ │ │ ├── UniMicInput.cs │ │ │ └── UniMicInput.cs.meta │ │ ├── Networks.meta │ │ ├── Networks │ │ │ ├── FakeNetwork.cs │ │ │ ├── FakeNetwork.cs.meta │ │ │ ├── FishNet.meta │ │ │ ├── FishNet │ │ │ │ ├── FishNetBroadcast.cs │ │ │ │ ├── FishNetBroadcast.cs.meta │ │ │ │ ├── FishNetBroadcastTags.cs │ │ │ │ ├── FishNetBroadcastTags.cs.meta │ │ │ │ ├── FishNetClient.cs │ │ │ │ ├── FishNetClient.cs.meta │ │ │ │ ├── FishNetServer.cs │ │ │ │ └── FishNetServer.cs.meta │ │ │ ├── Mirror.meta │ │ │ └── Mirror │ │ │ │ ├── MirrorClient.cs │ │ │ │ ├── MirrorClient.cs.meta │ │ │ │ ├── MirrorMessage.cs │ │ │ │ ├── MirrorMessage.cs.meta │ │ │ │ ├── MirrorMessageTags.cs │ │ │ │ ├── MirrorMessageTags.cs.meta │ │ │ │ ├── MirrorModeObserver.cs │ │ │ │ ├── MirrorModeObserver.cs.meta │ │ │ │ ├── MirrorServer.cs │ │ │ │ └── MirrorServer.cs.meta │ │ ├── Outputs.meta │ │ └── Outputs │ │ │ ├── StreamedAudioSourceOutput.cs │ │ │ └── StreamedAudioSourceOutput.cs.meta │ ├── Interfaces.meta │ ├── Interfaces │ │ ├── IAudioClient.cs │ │ ├── IAudioClient.cs.meta │ │ ├── IAudioFilter.cs │ │ ├── IAudioFilter.cs.meta │ │ ├── IAudioInput.cs │ │ ├── IAudioInput.cs.meta │ │ ├── IAudioOutput.cs │ │ ├── IAudioOutput.cs.meta │ │ ├── IAudioOutputFactory.cs │ │ ├── IAudioOutputFactory.cs.meta │ │ ├── IAudioServer.cs │ │ └── IAudioServer.cs.meta │ ├── Types.meta │ └── Types │ │ ├── AudioFrame.cs │ │ ├── AudioFrame.cs.meta │ │ ├── VoiceSettings.cs │ │ └── VoiceSettings.cs.meta │ ├── Samples.meta │ ├── Samples │ ├── Basic Setup Scripts.meta │ ├── Basic Setup Scripts │ │ ├── FishNet-SinglePrefabObjects.asset │ │ ├── FishNet-SinglePrefabObjects.asset.meta │ │ ├── UniVoiceFishNetSetupSample.cs │ │ ├── UniVoiceFishNetSetupSample.cs.meta │ │ ├── UniVoiceFishNetSetupSample.unity │ │ ├── UniVoiceFishNetSetupSample.unity.meta │ │ ├── UniVoiceMirrorSetupSample.cs │ │ ├── UniVoiceMirrorSetupSample.cs.meta │ │ ├── UniVoiceMirrorSetupSample.unity │ │ └── UniVoiceMirrorSetupSample.unity.meta │ ├── Group Chat Sample.meta │ └── Group Chat Sample │ │ ├── Prefabs.meta │ │ ├── Prefabs │ │ ├── Mic Toggle.prefab │ │ ├── Mic Toggle.prefab.meta │ │ ├── Peer View.prefab │ │ └── Peer View.prefab.meta │ │ ├── Scenes.meta │ │ ├── Scenes │ │ ├── GroupVoiceCallSample-Mirror.unity │ │ └── GroupVoiceCallSample-Mirror.unity.meta │ │ ├── Scripts.meta │ │ ├── Scripts │ │ ├── GroupVoiceCallMirrorSample.cs │ │ ├── GroupVoiceCallMirrorSample.cs.meta │ │ ├── PeerView.cs │ │ └── PeerView.cs.meta │ │ ├── Sprites.meta │ │ └── Sprites │ │ ├── mic.png │ │ ├── mic.png.meta │ │ ├── off.png │ │ ├── off.png.meta │ │ ├── on.png │ │ ├── on.png.meta │ │ ├── speaker.png │ │ └── speaker.png.meta │ ├── package.json │ └── package.json.meta ├── Documentation ├── docfx.json ├── filterConfig.yml └── toc.yml ├── LICENSE ├── Logs └── Packages-Update.log ├── Packages ├── manifest.json └── packages-lock.json ├── ProjectSettings ├── AudioManager.asset ├── ClusterInputManager.asset ├── DynamicsManager.asset ├── EditorBuildSettings.asset ├── EditorSettings.asset ├── GraphicsSettings.asset ├── InputManager.asset ├── MemorySettings.asset ├── NavMeshAreas.asset ├── NetworkManager.asset ├── PackageManagerSettings.asset ├── Physics2DSettings.asset ├── PresetManager.asset ├── ProjectSettings.asset ├── ProjectVersion.txt ├── QualitySettings.asset ├── TagManager.asset ├── TimeManager.asset ├── UnityConnectSettings.asset ├── VFXManager.asset ├── VersionControlSettings.asset ├── XRSettings.asset └── boot.config └── README.md /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | env: 11 | PKG_ROOT: "Assets/Adrenak.UniVoice" 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Check if README.md has changed 18 | id: readme_check 19 | run: | 20 | if git diff --name-only HEAD~1 HEAD | grep -q "README.md"; then 21 | echo "README_CHANGED=true" >> $GITHUB_ENV 22 | else 23 | echo "README_CHANGED=false" >> $GITHUB_ENV 24 | fi 25 | 26 | - name: Update package README 27 | if: env.README_CHANGED == 'true' 28 | run: | 29 | cp README.md "$PKG_ROOT/README.md" 30 | git config --global user.name 'github-bot' 31 | git config --global user.email 'github-bot@users.noreply.github.com' 32 | git commit -am "Updated package README from root README" 33 | git push -f -u origin master 34 | 35 | - name: Create UPM branch 36 | run: | 37 | git branch -d upm &> /dev/null || echo upm branch not found 38 | git subtree split -P "$PKG_ROOT" -b upm 39 | git checkout upm 40 | if [[ -d "Samples" ]]; then 41 | git mv Samples Samples~ 42 | rm -f Samples.meta 43 | git config --global user.name 'github-bot' 44 | git config --global user.email 'github-bot@users.noreply.github.com' 45 | git commit -am "fix: Samples => Samples~" 46 | fi 47 | git push -f -u origin upm -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | # Build the documentation 10 | build: 11 | runs-on: windows-latest # Required by DocFX 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 # Ensure full history is fetched for the reset to work next 17 | 18 | - name: Hard Reset 19 | run: | 20 | git reset --hard HEAD 21 | git clean -fdx # Remove untracked files and directories 22 | 23 | - name: Install DocFX 24 | run: choco install -y docfx 25 | 26 | - name: Use README.md as index.md 27 | run: cp README.md Documentation/index.md 28 | 29 | - name: Build DocFX 30 | run: docfx Documentation/docfx.json 31 | 32 | - name: List generated site files 33 | run: dir _site 34 | 35 | - name: Deploy site 36 | uses: peaceiris/actions-gh-pages@v4 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | publish_branch: gh-pages 40 | publish_dir: _site -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Ll]ibrary/ 2 | [Tt]emp/ 3 | [Oo]bj/ 4 | [Bb]uild/ 5 | [Bb]uilds/ 6 | Assets/AssetStoreTools* 7 | Assets/* 8 | !Assets/Adrenak.UniVoice 9 | !Assets/Adrenak.UniVoice.meta 10 | Logs/ 11 | Logs/* 12 | UserSettings/ 13 | 14 | # Visual Studio cache directory 15 | .vs/ 16 | 17 | # Autogenerated VS/MD/Consulo solution and project files 18 | ExportedObj/ 19 | .consulo/ 20 | *.csproj 21 | *.unityproj 22 | *.sln 23 | *.suo 24 | *.tmp 25 | *.user 26 | *.userprefs 27 | *.pidb 28 | *.booproj 29 | *.svd 30 | *.pdb 31 | *.opendb 32 | 33 | # Unity3D generated meta files 34 | *.pidb.meta 35 | *.pdb.meta 36 | 37 | # Unity3D Generated File On Crash Reports 38 | sysinfo.txt 39 | 40 | # Builds 41 | *.apk 42 | *.unitypackage 43 | -------------------------------------------------------------------------------- /.vsconfig: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "components": [ 4 | "Microsoft.VisualStudio.Workload.ManagedGame" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c309c926e41d02c4c9b1ccc930801f63 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Vatsal Ambastha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 789be013ec943714a826e42a9d7db452 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/README.md: -------------------------------------------------------------------------------- 1 | # UniVoice 2 | UniVoice is a voice chat/VoIP solution for Unity. 3 | 4 | Some features of UniVoice: 5 | - 🎨 Customize your audio input, output and networking layers. 6 | * 🌐 __Configurable Network__: 7 | - 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. 8 | - Built-in support for: 9 | - [Mirror networking](https://mirror-networking.com/) 10 | - [Fish Networking](https://fish-networking.gitbook.io/docs) 11 | 12 | * 🎤 __Configurable Audio Input__: 13 | - UniVoice is audio input agnostic. You can change the source of outgoing audio by implementing the `IAudioInput` interface. 14 | - Built-in support for: 15 | - Capturing Mic audio as device input. 16 | 17 | * 🔊 __Configurable Audio Output__: 18 | - UniVoice is audio output agnostic. You can divert incoming audio to anywhere you want by implementing the `IAudioOutput` interface. 19 | - Built-in support for: 20 | - Playing incoming audio using Unity AudioSource. 21 | 22 | * ✏️ __Audio Filters__: 23 | - Modify outgoing and incoming audio by implementing the `IAudioFilter` interface. 24 | - Built-in support for: 25 | - Opus (Concentus) encoding & decoding. 26 | - RNNoise based noise removal. 27 | - Energy based VAD (Voice Activity Detection) 28 | - Gaussian blurring for minor denoising. 29 | 30 | - 👥 Easy integration with your existing networking solution 31 | - Whether you're using Mirror or FishNet, UniVoice runs in the background in sync with your networking lifecycle 32 | - A basic integration involves just initializing it on start. 33 | - For advanced usage like teams, chatrooms, lobbies, you can use the UniVoice API to create runtime behaviour. 34 | 35 | - ⚙ Fine control over audio data flow. 36 | * Don't want to listen to a peer? Mute them. Don't want someone listening to you? Deafen them. 37 | * Group players using tags and control audio flow between them. For example: 38 | - "red", "blue" and "spectator" tags for two teams playing against each other. 39 | - Red and Blue teams can only hear each other 40 | - Spectators can hear everyone 41 | - clients with "contestant", "judge" and "audience" tags for a virtual talent show. 42 | - Contestant can be heard by everyone, but don't hear anyone else (for focus) 43 | - Judges can talk to and hear each other for discussions. They can hear the contestant. But not the audience (for less noise) 44 | - Audience can hear and talk to each other. They can hear the performer. But they cannot hear the judges. 45 | 46 | ## Installation 47 | ⚠️ [OpenUPM](https://openupm.com/packages/com.adrenak.univoice/?subPage=versions) may not have up to date releases. Install using NPM registry instead 👇 48 | 49 | Ensure you have the NPM registry in the `manifest.json` file of your Unity project with the following scopes: 50 | ``` 51 | "scopedRegistries": [ 52 | { 53 | "name": "npmjs", 54 | "url": "https://registry.npmjs.org", 55 | "scopes": [ 56 | "com.npmjs", 57 | "com.adrenak.univoice", 58 | "com.adrenak.brw", 59 | "com.adrenak.unimic", 60 | "com.adrenak.concentus-unity" 61 | ] 62 | } 63 | ] 64 | ``` 65 | 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). 66 | 67 | ## Useful links 68 | * API reference is available here: http://www.vatsalambastha.com/univoice 69 | * UniVoice blog: https://blog.vatsalambastha.com/search/label/univoice 70 | * Discord server: https://discord.gg/NGvkEVbdjQ 71 | 72 | ## Integration 73 | 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. 74 | 75 | ## Samples 76 | This repository contains two samples: 77 | * `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. 78 | * `UniVoiceFishNetSetypSample.cs` is also very similar. Just drag and drop and it should work! 79 | * A sample scene that shows the other clients in a UI as well as allows you to mute yourself/them. This sample is Mirror based. 80 | 81 | > UniVoice currently only supports Mirror and FishNetworking out of the box. Follow the instructions in the "Activating non-packaged dependencies" section below before trying it out the samples. 82 | 83 | ## Dependencies 84 | [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. 85 | 86 | [com.adrenak.unimic](https://www.github.com/adrenak/unimic) for easily capturing audio from any connected mic devices. See `UniMicInput.cs` for usage. Also used for streaming audio playback. See `StreamedAudioSourceOutput.cs` for usage. 87 | 88 | [com.adrenak.concentus-unity](https://www.github.com/adrenak/concentus-unity) for Opus encoding and decoding. See `ConcentusEncodeFilter.cs` and `ConcentusDecodeFilter.cs` for usage 89 | 90 | ## Activating non-packaged dependencies 91 | UniVoice includes and installs the dependencies mentioned above along with itself. The following implementations are available out of the box when you install it: 92 | * Opus encoding/decoding filter (via Contentus-Unity) 93 | * GaussianAudioBlur filter (plain C#, no dependencies used) 94 | * Mic audio capture input (via UniMic) 95 | * AudioSource based playback output (via UniMic) 96 | 97 | UniVoice has code that uses dependencies that you have to install and sometimes enable via compilation symbols as they are _not_ UniVoice dependencies and _don't_ get installed along with UniVoice. This is because they are either third party modules or based on native libraries (not plain C#) that can pose build issues. 98 | * RNNoise Noise removal filter: 99 | * To enable, ensure the [RNNoise4Unity](https://github.com/adrenak/RNNoise4Unity) package is in your project and add `UNIVOICE_FILTER_RNNOISE4UNITY` to activate it 100 | * Mirror network: 101 | * Just add the Mirror package to your project. UniVoice will detect it. 102 | * Fish Networking: 103 | * Just install FishNet package in your project. UniVoice will detect it. 104 | 105 | ## License and Support 106 | This project is under the [MIT license](https://github.com/adrenak/univoice/blob/master/LICENSE). 107 | 108 | Community contributions are welcome. 109 | 110 | Commercial engagements with the author can be arranged, subject to schedule and availability. 111 | 112 | ## Acknowledgements and contributors 113 | * [@metater](https://github.com/Metater/) for helping make improvements to audio streaming quality. [A related blog post](https://blog.vatsalambastha.com/2025/07/unimic-330-many-streamedaudiosource.html) 114 | * [@FrantisekHolubec](https://github.com/FrantisekHolubec) for [FishNet support code](https://github.com/adrenak/univoice/commit/fdc3424180d8991c92b3e092b3edb50b6110c863). Here's a [related blog post](https://blog.vatsalambastha.com/2025/09/univoice-480-fishnet-support.html) 115 | * [Masaryk University](https://www.muni.cz/en) for using UniVoice in their projects and providing helpful feedback 116 | 117 | ## Contact 118 | The author can be reached at the following links: 119 | 120 | [Website](http://www.vatsalambastha.com) 121 | [LinkedIn](https://www.linkedin.com/in/vatsalAmbastha) 122 | [GitHub](https://www.github.com/adrenak) 123 | [Twitter](https://www.twitter.com/vatsalAmbastha) 124 | 125 | Discord: `adrenak#1934` 126 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b497ccbc9f4029f4593611f820e24235 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e1b2e834bd331324e9157c87f86d1143 3 | folderAsset: yes 4 | timeCreated: 1547895114 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Adrenak.UniVoice.Runtime.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Adrenak.UniVoice.Runtime", 3 | "rootNamespace": "", 4 | "references": [ 5 | "GUID:1f776cd02c03a7b4280b6b649d7758e2", 6 | "GUID:f87ecb857e752164ab814a3de8eb0262", 7 | "GUID:b118fd5a40c85ad4e9b38e8c4a42bbb1", 8 | "GUID:4653938bfdb5cf8409322ce17219d5f7", 9 | "GUID:30817c1a0e6d646d99c048fc403f5979", 10 | "GUID:7c88a4a7926ee5145ad2dfa06f454c67" 11 | ], 12 | "includePlatforms": [], 13 | "excludePlatforms": [], 14 | "allowUnsafeCode": false, 15 | "overrideReferences": false, 16 | "precompiledReferences": [], 17 | "autoReferenced": true, 18 | "defineConstraints": [], 19 | "versionDefines": [], 20 | "noEngineReferences": false 21 | } -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Adrenak.UniVoice.Runtime.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6b289bb677194eb40b60db3a566aab7b 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/ClientSession.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 923d8509d2c05f2479d6af7331dabe58 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Common.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7e8f97d020fcd2740b34e8d69e4f882d 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Common/SimpleVad.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 016b8c73d9808cc49a425fb3d7acabed 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Common/Utils.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Net.Sockets; 4 | using System.Net; 5 | using System.Runtime.Serialization.Formatters.Binary; 6 | using System; 7 | using UnityEngine; 8 | 9 | namespace Adrenak.UniVoice { 10 | public class Utils { 11 | public class Bytes { 12 | public static byte[] FloatsToBytes(float[] floats) { 13 | int byteCount = sizeof(float) * floats.Length; 14 | byte[] byteArray = new byte[byteCount]; 15 | 16 | Buffer.BlockCopy(floats, 0, byteArray, 0, byteCount); 17 | 18 | return byteArray; 19 | } 20 | 21 | public static float[] BytesToFloats(byte[] bytes) { 22 | int floatCount = bytes.Length / sizeof(float); 23 | float[] floatArray = new float[floatCount]; 24 | 25 | Buffer.BlockCopy(bytes, 0, floatArray, 0, bytes.Length); 26 | 27 | return floatArray; 28 | } 29 | } 30 | 31 | public static class Audio { 32 | static float[] audioF; 33 | static float sumOfSquares; 34 | public static float CalculateRMS(byte[] audio) { 35 | audioF = Bytes.BytesToFloats(audio); 36 | 37 | foreach(var x in audioF) 38 | sumOfSquares += x * x; 39 | return Mathf.Sqrt(sumOfSquares / audioF.Length); 40 | } 41 | } 42 | 43 | public static class Network { 44 | public static string LocalIPv4Address { 45 | get { 46 | using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0)) { 47 | socket.Connect("8.8.8.8", 65530); 48 | IPEndPoint endPoint = socket.LocalEndPoint as IPEndPoint; 49 | return endPoint.Address.ToString(); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Common/Utils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a3530a416ca433448ab4c50826faefb4 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Common/WavFileWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | /* 6 | Example script for usage with UniMic. Just add it to a scene, 7 | play and then quit/exit play mode: 8 | 9 | public class WavFileWriterTest : MonoBehaviour { 10 | WavFileWriter writer; 11 | 12 | void Start() { 13 | string path = string.Empty; 14 | if (Application.isEditor) 15 | path = Application.dataPath.Replace("Assets", "output.wav"); 16 | else 17 | path = Path.Combine(Application.persistentDataPath, "output.wav"); 18 | 19 | writer = new WavFileWriter(path); 20 | 21 | Mic.Init(); 22 | 23 | Mic.AvailableDevices[0].OnFrameCollected += OnFrameCollected; 24 | Mic.AvailableDevices[0].StartRecording(20); 25 | } 26 | 27 | private void OnFrameCollected(int arg1, int arg2, float[] arg3) { 28 | writer.Write(arg1, arg2, arg3); 29 | } 30 | 31 | private void OnDestroy() { 32 | writer.Dispose(); 33 | } 34 | } 35 | */ 36 | 37 | namespace Adrenak.UniVoice { 38 | /// 39 | /// A utility to write audio samples to a file on disk. 40 | /// Construct using the path you want to store the audio file at. 41 | /// Invoke Write with the sampling frequency, channel count and PCM samples 42 | /// and it will lazily initialize. 43 | /// 44 | public class WavFileWriter : IDisposable { 45 | FileStream fileStream; 46 | int sampleRate; 47 | short channels; 48 | readonly short bitsPerSample = 16; 49 | 50 | long dataChunkPos; 51 | int totalSampleCount = 0; 52 | bool isInitialized = false; 53 | readonly string path; 54 | 55 | public WavFileWriter(string path) { 56 | this.path = path; 57 | } 58 | 59 | public void Write(int frequency, int channelCount, float[] samples) { 60 | if (!isInitialized) { 61 | sampleRate = frequency; 62 | channels = (short)channelCount; 63 | fileStream = new FileStream(path, FileMode.Create, FileAccess.Write); 64 | WriteWavHeader(); 65 | isInitialized = true; 66 | } 67 | else { 68 | if (frequency != sampleRate || channelCount != channels) 69 | throw new InvalidOperationException("Inconsistent frequency or channel count between calls."); 70 | } 71 | 72 | byte[] buffer = new byte[samples.Length * 2]; // 2 bytes per sample (16-bit PCM) 73 | for (int i = 0; i < samples.Length; i++) { 74 | short intSample = (short)Math.Clamp(samples[i] * short.MaxValue, short.MinValue, short.MaxValue); 75 | buffer[i * 2] = (byte)(intSample & 0xff); 76 | buffer[i * 2 + 1] = (byte)((intSample >> 8) & 0xff); 77 | } 78 | 79 | fileStream.Write(buffer, 0, buffer.Length); 80 | totalSampleCount += samples.Length; 81 | } 82 | 83 | void WriteWavHeader() { 84 | var writer = new BinaryWriter(fileStream, Encoding.ASCII, true); 85 | 86 | writer.Write(Encoding.ASCII.GetBytes("RIFF")); 87 | writer.Write(0); // placeholder for file size 88 | writer.Write(Encoding.ASCII.GetBytes("WAVE")); 89 | 90 | // fmt chunk 91 | writer.Write(Encoding.ASCII.GetBytes("fmt ")); 92 | writer.Write(16); // PCM header size 93 | writer.Write((short)1); // PCM format 94 | writer.Write(channels); 95 | writer.Write(sampleRate); 96 | int byteRate = sampleRate * channels * bitsPerSample / 8; 97 | writer.Write(byteRate); 98 | short blockAlign = (short)(channels * bitsPerSample / 8); 99 | writer.Write(blockAlign); 100 | writer.Write(bitsPerSample); 101 | 102 | // data chunk 103 | writer.Write(Encoding.ASCII.GetBytes("data")); 104 | dataChunkPos = fileStream.Position; 105 | writer.Write(0); // placeholder for data chunk size 106 | } 107 | 108 | public void Dispose() { 109 | if (!isInitialized) return; 110 | 111 | long dataSize = totalSampleCount * bitsPerSample / 8; 112 | 113 | fileStream.Seek(dataChunkPos, SeekOrigin.Begin); 114 | fileStream.Write(BitConverter.GetBytes((int)dataSize), 0, 4); 115 | 116 | fileStream.Seek(4, SeekOrigin.Begin); 117 | int fileSize = (int)(fileStream.Length - 8); 118 | fileStream.Write(BitConverter.GetBytes(fileSize), 0, 4); 119 | 120 | fileStream.Dispose(); 121 | isInitialized = false; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Common/WavFileWriter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0ae012123ed9d8640980ccfe6efbb4e5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dd9d2878ff4b11f47be8de3fe43f866b 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1c33181b4e7cd6f4588b07afff029c77 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/Concentus Opus Filters.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d3ce124588cebfb4e812cb9b91e46525 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/Concentus Opus Filters/ConcentusDecodeFilter.cs: -------------------------------------------------------------------------------- 1 | using Concentus; 2 | using System; 3 | 4 | /* 5 | * Opus encoding and decoding are VERY important for any real world use of UniVoice as without 6 | * encoding the size of audio data is much (over 10x) larger. 7 | * For more info see https://www.github.com/adrenak/concentus-unity 8 | */ 9 | namespace Adrenak.UniVoice.Filters { 10 | /// 11 | /// Decodes Opus encoded audio. Use this as a filter for incoming client audio. 12 | /// 13 | public class ConcentusDecodeFilter : IAudioFilter { 14 | IOpusDecoder decoder; 15 | float[] decodeBuffer; 16 | int inputChannelCount; 17 | int inputFrequency; 18 | byte[] floatsToBytes; 19 | 20 | /// 21 | /// Creates a Concentus decode filter. 22 | /// 23 | /// 24 | /// The length of the decode buffer. Default is 11520 to fit a large sample 25 | /// with frequency 48000, duration 120ms and 2 channels. This should be enough 26 | /// for almost all scenarios. Increase if you need more. 27 | /// 28 | public ConcentusDecodeFilter(int decodeBufferLength = 11520) { 29 | decodeBuffer = new float[decodeBufferLength]; 30 | } 31 | 32 | public AudioFrame Run(AudioFrame input) { 33 | inputChannelCount = input.channelCount; 34 | inputFrequency = input.frequency; 35 | 36 | CreateNewDecoderIfNeeded(); 37 | 38 | var decodeResult = Decode(input.samples, out Span decoded); 39 | if (decodeResult > 0) { 40 | floatsToBytes = Utils.Bytes.FloatsToBytes(decoded.ToArray()); 41 | return new AudioFrame { 42 | timestamp = input.timestamp, 43 | samples = floatsToBytes, 44 | channelCount = inputChannelCount, 45 | frequency = inputFrequency 46 | }; 47 | } 48 | else { 49 | return new AudioFrame { 50 | timestamp = input.timestamp, 51 | samples = new byte[0], 52 | channelCount = inputChannelCount, 53 | frequency = inputFrequency 54 | }; 55 | } 56 | } 57 | 58 | int Decode(Span toDecode, out Span decoded) { 59 | // Decode the Opus packet into preallocated buffer 60 | int samplesPerChannel = decoder.Decode(toDecode, decodeBuffer, decodeBuffer.Length); 61 | 62 | if (samplesPerChannel > 0) { 63 | int totalSamples = samplesPerChannel * inputChannelCount; // Total samples across all channels 64 | decoded = decodeBuffer.AsSpan(0, totalSamples); // Trim to valid samples 65 | } 66 | else { 67 | decoded = Span.Empty; 68 | } 69 | 70 | return samplesPerChannel; // Return number of samples per channel or 0 on failure 71 | } 72 | 73 | void CreateNewDecoderIfNeeded() { 74 | if (decoder == null || decoder.SampleRate != inputFrequency || decoder.NumChannels != inputChannelCount) { 75 | decoder?.Dispose(); 76 | decoder = OpusCodecFactory.CreateDecoder(inputFrequency, inputChannelCount); 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/Concentus Opus Filters/ConcentusDecodeFilter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 31124d8beda4bc045b5ee5696140f0d8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/Concentus Opus Filters/ConcentusEncodeFilter.cs: -------------------------------------------------------------------------------- 1 | using Concentus; 2 | using Concentus.Enums; 3 | using System; 4 | 5 | /* 6 | * Opus encoding and decoding are VERY important for any real world use of UniVoice as without 7 | * encoding the size of audio data is much (over 10x) larger. 8 | * For more info see https://www.github.com/adrenak/concentus-unity 9 | */ 10 | namespace Adrenak.UniVoice.Filters { 11 | /// 12 | /// A filter that encodes audio using Opus. Use this as an output filter 13 | /// to reduce the size of outgoing client audio 14 | /// 15 | public class ConcentusEncodeFilter : IAudioFilter { 16 | public ConcentusFrequencies SamplingFrequency { get; private set; } 17 | IOpusEncoder encoder; 18 | IResampler resampler; 19 | byte[] encodeBuffer; 20 | float[] resampleBuffer; 21 | int inputDuration; 22 | int inputChannels; 23 | int inputFrequency; 24 | int resamplerChannelCount; 25 | float[] bytesToFloats; 26 | byte[] floatsToBytes; 27 | int resamplerQuality; 28 | int encoderComplexity; 29 | int encoderBitrate; 30 | 31 | /// 32 | /// Creates a Concentus encode filter 33 | /// 34 | /// 35 | /// The frequency the encoder runs at. 36 | /// If the input audio frequency is different from this value, it will be resampled before encode. 37 | /// 38 | /// Resampler quality [1, 10] 39 | /// Encoder complexity [1, 10] 40 | /// Encoder bitrate [16000, 256000]. Set to -1 to enable variable bitrate. 41 | /// 42 | /// The length of the encode buffer. Default is 46080 to fit a large sample 43 | /// with frequency 48000, duration 120ms and 2 channels. This should be enough 44 | /// for almost all scenarios. Increase if you need more. 45 | /// 46 | public ConcentusEncodeFilter( 47 | ConcentusFrequencies encodeFrequency = ConcentusFrequencies.Frequency_16000, 48 | int resamplerQuality = 2, 49 | int encoderComplexity = 3, 50 | int encoderBitrate = 64000, 51 | int encodeBufferLength = 46080 52 | ) { 53 | SamplingFrequency = encodeFrequency; 54 | this.resamplerQuality = Math.Clamp(resamplerQuality, 1, 10); 55 | this.encoderComplexity = Math.Clamp(encoderComplexity, 1, 10); 56 | this.encoderBitrate = Math.Clamp(encoderBitrate, 16000, 256000); 57 | encodeBuffer = new byte[encodeBufferLength]; 58 | } 59 | 60 | public AudioFrame Run(AudioFrame input) { 61 | inputChannels = input.channelCount; 62 | inputFrequency = input.frequency; 63 | inputDuration = ((input.samples.Length / 4) * 1000) / (input.frequency * input.channelCount); 64 | 65 | CreateNewResamplerAndEncoderIfNeeded(); 66 | 67 | Span toEncode; 68 | bytesToFloats = Utils.Bytes.BytesToFloats(input.samples); 69 | toEncode = bytesToFloats; 70 | 71 | if (inputFrequency != (int)SamplingFrequency) 72 | toEncode = Resample(bytesToFloats); 73 | 74 | var encodeResult = Encode(toEncode, out Span encoded); 75 | if (encodeResult > 0) { 76 | floatsToBytes = encoded.ToArray(); 77 | return new AudioFrame { 78 | timestamp = input.timestamp, 79 | channelCount = inputChannels, 80 | samples = floatsToBytes, 81 | frequency = (int)SamplingFrequency 82 | }; 83 | } 84 | else { 85 | return new AudioFrame { 86 | timestamp = input.timestamp, 87 | channelCount = inputChannels, 88 | samples = new byte[0], 89 | frequency = (int)SamplingFrequency 90 | }; 91 | } 92 | } 93 | 94 | void CreateNewResamplerAndEncoderIfNeeded() { 95 | if (resampleBuffer == null || resampleBuffer.Length != (int)SamplingFrequency * inputDuration * inputChannels / 1000) 96 | resampleBuffer = new float[(int)SamplingFrequency * inputDuration * inputChannels / 1000]; 97 | 98 | if (resampler == null) { 99 | resamplerChannelCount = inputChannels; 100 | resampler = ResamplerFactory.CreateResampler(inputChannels, inputFrequency, (int)SamplingFrequency, resamplerQuality); 101 | } 102 | else { 103 | resampler.GetRates(out int in_rate, out int out_rate); 104 | if (in_rate != inputFrequency || out_rate != (int)SamplingFrequency || resamplerChannelCount != inputChannels) { 105 | resampler.Dispose(); 106 | resamplerChannelCount = inputChannels; 107 | resampler = ResamplerFactory.CreateResampler(inputChannels, inputFrequency, (int)SamplingFrequency, resamplerQuality); 108 | } 109 | } 110 | 111 | if (encoder == null || encoder.SampleRate != (int)SamplingFrequency || encoder.NumChannels != inputChannels) { 112 | encoder?.Dispose(); 113 | encoder = OpusCodecFactory.CreateEncoder((int)SamplingFrequency, inputChannels, OpusApplication.OPUS_APPLICATION_VOIP); 114 | encoder.Complexity = encoderComplexity; 115 | if(encoderBitrate == -1) 116 | encoder.UseVBR = true; 117 | else { 118 | encoder.UseVBR = false; 119 | encoder.Bitrate = encoderBitrate; 120 | } 121 | } 122 | } 123 | 124 | Span Resample(Span samples) { 125 | // Calculate input and output lengths 126 | int in_len = samples.Length / inputChannels; // Input samples per channel 127 | int out_len = (int)SamplingFrequency * inputDuration / 1000; // Output samples per channel 128 | 129 | // Perform resampling into preallocated buffer 130 | resampler.ProcessInterleaved(samples, ref in_len, resampleBuffer, ref out_len); 131 | 132 | // Return only the valid portion of resampled data 133 | return resampleBuffer.AsSpan(0, out_len * inputChannels); // Trim to valid samples 134 | } 135 | 136 | int Encode(Span toEncode, out Span encoded) { 137 | int frameSize = (int)SamplingFrequency * inputDuration / 1000; // Samples per channel 138 | int totalSamples = frameSize * inputChannels; // Total interleaved samples 139 | 140 | if (toEncode.Length < totalSamples) { 141 | encoded = Span.Empty; 142 | return 0; 143 | } 144 | 145 | // Use preallocated encodeBuffer 146 | int result = encoder.Encode(toEncode.Slice(0, totalSamples), frameSize, encodeBuffer, encodeBuffer.Length); 147 | 148 | if (result > 0) 149 | encoded = encodeBuffer.AsSpan(0, result); // Trim to actual encoded size 150 | else 151 | encoded = Span.Empty; 152 | 153 | return result; // Return number of bytes written or 0 on failure 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/Concentus Opus Filters/ConcentusEncodeFilter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7e548eaedfb8c1b49812a12b3fba2c8a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/Concentus Opus Filters/ConcentusFrequencies.cs: -------------------------------------------------------------------------------- 1 | namespace Adrenak.UniVoice.Filters { 2 | /// 3 | /// Defines supported sampling frequencies for the Opus codec via Concentus. 4 | /// 5 | public enum ConcentusFrequencies : int { 6 | /// 7 | /// 8 kHz sampling frequency, typically used for narrowband audio. 8 | /// 9 | Frequency_8000 = 8000, 10 | 11 | /// 12 | /// 12 kHz sampling frequency, suitable for medium-band audio. 13 | /// 14 | Frequency_12000 = 12000, 15 | 16 | /// 17 | /// 16 kHz sampling frequency, commonly used for wideband speech. 18 | /// 19 | Frequency_16000 = 16000, 20 | 21 | /// 22 | /// 24 kHz sampling frequency, providing good quality for music and audio. 23 | /// 24 | Frequency_24000 = 24000, 25 | 26 | /// 27 | /// 48 kHz sampling frequency, offering the highest audio quality. 28 | /// 29 | Frequency_48000 = 48000, 30 | } 31 | } -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/Concentus Opus Filters/ConcentusFrequencies.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 53ba54c62b14cf4439d3b71fe7b1d08c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/GaussianAudioBlur.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using UnityEngine; 4 | 5 | namespace Adrenak.UniVoice.Filters { 6 | /// 7 | /// A filter that applies Gaussian blur over audio data to smoothen it. 8 | /// This is somewhat effective in removing noise from the audio. 9 | /// 10 | public class GaussianAudioBlur : IAudioFilter { 11 | readonly float sigma; 12 | readonly int range; 13 | byte[] lastInput; 14 | 15 | public GaussianAudioBlur(float sigma = 2, int range = 2) { 16 | this.sigma = sigma; 17 | this.range = range; 18 | } 19 | 20 | public AudioFrame Run(AudioFrame frame) { 21 | var input = frame.samples; 22 | if (input == null || input.Length == 0) { 23 | frame.samples = null; 24 | return frame; 25 | } 26 | 27 | // If this is the first audio input we've received, we simply apply the gaussian filter 28 | // and return the result. 29 | if (lastInput == null) { 30 | lastInput = input; 31 | frame.samples = Utils.Bytes.FloatsToBytes( 32 | ApplyGaussianFilter(Utils.Bytes.BytesToFloats(input)) 33 | ); 34 | return frame; 35 | } 36 | 37 | // Else, if we've had some input before, we also consider the previously processed 38 | // audio. We make an array that has both the previous and the current input, smoothen 39 | // it, and then return the second half of the array. This reducing jittering by making 40 | // the smoothing a little more seamless. 41 | else { 42 | // Create an all input, that also has the input from the last time this filter ran. 43 | byte[] allInput = new byte[lastInput.Length + input.Length]; 44 | Buffer.BlockCopy(lastInput, 0, allInput, 0, lastInput.Length); 45 | Buffer.BlockCopy(input, 0, allInput, lastInput.Length, input.Length); 46 | 47 | // smoothen all input 48 | byte[] allInputSmooth = Utils.Bytes.FloatsToBytes( 49 | ApplyGaussianFilter(Utils.Bytes.BytesToFloats(allInput)) 50 | ); 51 | 52 | // get the second half of the smoothened values 53 | byte[] result = new byte[input.Length]; 54 | Buffer.BlockCopy(allInputSmooth, lastInput.Length, result, 0, input.Length); 55 | 56 | lastInput = input; 57 | frame.samples = result; 58 | return frame; 59 | } 60 | } 61 | 62 | float[] ApplyGaussianFilter(float[] inputArray) { 63 | int length = inputArray.Length; 64 | float[] smoothedArray = new float[length]; 65 | 66 | for (int i = 0; i < length; i++) { 67 | float sum = 0.0f; 68 | float weightSum = 0.0f; 69 | 70 | for (int j = -range; j <= range; j++) { 71 | int index = i + j; 72 | if (index >= 0 && index < length) { 73 | float weight = Gaussian(j, sigma); 74 | sum += inputArray[index] * weight; 75 | weightSum += weight; 76 | } 77 | } 78 | 79 | smoothedArray[i] = sum / weightSum; 80 | } 81 | 82 | return smoothedArray; 83 | } 84 | 85 | float Gaussian(int x, float sigma) { 86 | return (float)Mathf.Exp(-(x * x) / (2 * sigma * sigma)) 87 | / ((float)Mathf.Sqrt(2 * Mathf.PI) * sigma); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/GaussianAudioBlur.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5992899a0c52b09498191b477927699d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/RNNoise.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 046f57b349547674cb5b8c8e31c89467 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/RNNoise/RNNoiseFilter.cs: -------------------------------------------------------------------------------- 1 | #if UNIVOICE_FILTER_RNNOISE4UNITY 2 | using System; 3 | 4 | using Adrenak.RNNoise4Unity; 5 | 6 | namespace Adrenak.UniVoice.Filters { 7 | public class RNNoiseFilter : IAudioFilter { 8 | readonly Denoiser denoiser; 9 | 10 | public RNNoiseFilter() { 11 | denoiser = new Denoiser(); 12 | } 13 | 14 | public AudioFrame Run(AudioFrame input) { 15 | var data = Utils.Bytes.BytesToFloats(input.samples); 16 | 17 | denoiser.Denoise(data.AsSpan(), false); 18 | 19 | return new AudioFrame { 20 | timestamp = input.timestamp, 21 | channelCount = input.channelCount, 22 | frequency = input.frequency, 23 | samples = Utils.Bytes.FloatsToBytes(data) 24 | }; 25 | } 26 | } 27 | } 28 | #endif -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/RNNoise/RNNoiseFilter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bc1ad3167deb8cf4896b4f4b1180ec9a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/SimpleVadFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Adrenak.UniVoice.Filters { 2 | public class SimpleVadFilter : IAudioFilter { 3 | private readonly SimpleVad _vad; 4 | 5 | public SimpleVadFilter(SimpleVad vad) { 6 | _vad = vad; 7 | } 8 | 9 | public AudioFrame Run(AudioFrame input) { 10 | _vad.Process(input.frequency, input.channelCount, Utils.Bytes.BytesToFloats(input.samples)); 11 | if (_vad.IsSpeaking) { 12 | return input; 13 | } 14 | return default; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Filters/SimpleVadFilter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: abdb23219540abe46ba175163b23e00d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Inputs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 569cfdf74a194604aa8913c641f27024 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Inputs/EmptyAudioInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Adrenak.UniVoice { 4 | /// 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 | /// 11 | public class EmptyAudioInput : IAudioInput { 12 | public int Frequency => 1; 13 | 14 | public int ChannelCount => 1; 15 | 16 | public int SegmentRate => 1; 17 | 18 | #pragma warning disable CS0067 19 | public event Action OnFrameReady; 20 | #pragma warning restore 21 | 22 | public void Dispose() { } 23 | } 24 | } -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Inputs/EmptyAudioInput.cs.meta: -------------------------------------------------------------------------------- 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: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Inputs/UniMicInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Adrenak.UniMic; 4 | 5 | using UnityEngine; 6 | 7 | namespace Adrenak.UniVoice.Inputs { 8 | /// 9 | /// An implementation based on UniMic. 10 | /// For more on UniMic, visit https://www.github.com/adrenak/unimic 11 | /// 12 | public class UniMicInput : IAudioInput { 13 | const string TAG = "UniMicInput"; 14 | public event Action OnFrameReady; 15 | 16 | public Mic.Device device; 17 | public Mic.Device Device { 18 | get => device; 19 | set { 20 | if (device == value) 21 | return; 22 | if(device != null) 23 | device.OnFrameCollected -= OnFrameCollected; 24 | device = value; 25 | if(device != null) 26 | device.OnFrameCollected += OnFrameCollected; 27 | } 28 | } 29 | 30 | public UniMicInput(Mic.Device device) { 31 | Device = device; 32 | } 33 | 34 | private void OnFrameCollected(int frequency, int channels, float[] samples) { 35 | var frame = new AudioFrame { 36 | timestamp = 0, 37 | frequency = frequency, 38 | channelCount = channels, 39 | samples = Utils.Bytes.FloatsToBytes(samples) 40 | }; 41 | OnFrameReady?.Invoke(frame); 42 | } 43 | 44 | public void Dispose() { 45 | if(Device != null) 46 | Device.OnFrameCollected -= OnFrameCollected; 47 | Debug.unityLogger.Log(TAG, "Disposed"); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Inputs/UniMicInput.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6a98c26fdc5135842a8a651f2bd5f8e9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 94e11acf53928b445a24dd638c5d334a 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/FakeNetwork.cs: -------------------------------------------------------------------------------- 1 | namespace Adrenak.UniVoice.Networks { 2 | // Just an empty client to make sure that there's always 3 | // an Adrenak.UniVoice.Networks namespace even if there's 4 | // no network implementation in the project 5 | public class FakeNetwork { } 6 | } 7 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/FakeNetwork.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: af0bac6bd02832141b9897cc6b24529b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/FishNet.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 40d4cba94d8644408e9ebc8f61a470c3 3 | timeCreated: 1755189627 -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/FishNet/FishNetBroadcast.cs: -------------------------------------------------------------------------------- 1 | #if FISHNET 2 | using System; 3 | using FishNet.Broadcast; 4 | 5 | namespace Adrenak.UniVoice.Networks 6 | { 7 | /// 8 | /// The messages exchanged between the server and client. 9 | /// To see how the Mirror implementation of UniVoice uses this struct 10 | /// find the references to the object in the project. 11 | /// The gist is, it uses BRW (https://www.github.com/adrenak/brw) to 12 | /// write and read data. The data always starts with a tag. All the tags 13 | /// used for this UniVoice FishNet implementation are available in 14 | /// 15 | /// 16 | [Serializable] 17 | public struct FishNetBroadcast : IBroadcast 18 | { 19 | public byte[] data; 20 | } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/FishNet/FishNetBroadcast.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f0e5aa417fb8e6249bb05a011a5d7edf -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/FishNet/FishNetBroadcastTags.cs: -------------------------------------------------------------------------------- 1 | #if FISHNET 2 | namespace Adrenak.UniVoice.Networks 3 | { 4 | /// 5 | /// The different types of messages we send over FishNet 6 | /// to implement the and 7 | /// interfaces for FishNet 8 | /// 9 | public class FishNetBroadcastTags 10 | { 11 | public const string AUDIO_FRAME = "AUDIO_FRAME"; 12 | public const string VOICE_SETTINGS = "VOICE_SETTINGS"; 13 | } 14 | } 15 | #endif 16 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/FishNet/FishNetBroadcastTags.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fa1db30850448a14b840e4974d687a3b -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/FishNet/FishNetClient.cs: -------------------------------------------------------------------------------- 1 | #if FISHNET 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Adrenak.BRW; 6 | using FishNet; 7 | using FishNet.Managing; 8 | using FishNet.Transporting; 9 | using UnityEngine; 10 | 11 | namespace Adrenak.UniVoice.Networks 12 | { 13 | /// 14 | /// This is the implementation of interface for FishNet. 15 | /// It uses the FishNet to send and receive UniVoice data to the server. 16 | /// 17 | public class FishNetClient : IAudioClient 18 | { 19 | private const string TAG = "[FishNetClient]"; 20 | public int ID { get; private set; } = -1; 21 | 22 | public List PeerIDs { get; private set; } 23 | public VoiceSettings YourVoiceSettings { get; private set; } 24 | 25 | public event Action> OnJoined; 26 | public event Action OnLeft; 27 | public event Action OnPeerJoined; 28 | public event Action OnPeerLeft; 29 | public event Action OnReceivedPeerAudioFrame; 30 | 31 | private NetworkManager _networkManager; 32 | 33 | public FishNetClient() 34 | { 35 | PeerIDs = new List(); 36 | YourVoiceSettings = new VoiceSettings(); 37 | 38 | _networkManager = InstanceFinder.NetworkManager; 39 | _networkManager.ClientManager.OnClientConnectionState += OnClientConnectionStateChanged; 40 | _networkManager.ClientManager.OnAuthenticated += OnClientAuthenticated; 41 | _networkManager.ClientManager.OnRemoteConnectionState += OnRemoteConnectionStateChanged; 42 | _networkManager.ClientManager.RegisterBroadcast(OnReceivedMessage); 43 | } 44 | 45 | public void Dispose() 46 | { 47 | if (_networkManager) 48 | { 49 | _networkManager.ClientManager.OnClientConnectionState -= OnClientConnectionStateChanged; 50 | _networkManager.ClientManager.OnAuthenticated -= OnClientAuthenticated; 51 | _networkManager.ClientManager.OnRemoteConnectionState -= OnRemoteConnectionStateChanged; 52 | _networkManager.ClientManager.UnregisterBroadcast(OnReceivedMessage); 53 | } 54 | PeerIDs.Clear(); 55 | } 56 | 57 | private void OnRemoteConnectionStateChanged(RemoteConnectionStateArgs args) 58 | { 59 | // Don't process connection state changes before the client is authenticated 60 | if (_networkManager.ClientManager.Connection.ClientId < 0) 61 | return; 62 | 63 | if (args.ConnectionState == RemoteConnectionState.Started) 64 | { 65 | var newPeerID = args.ConnectionId; 66 | if (!PeerIDs.Contains(newPeerID)) 67 | { 68 | PeerIDs.Add(newPeerID); 69 | Debug.unityLogger.Log(LogType.Log, TAG, 70 | $"Peer {newPeerID} joined. Peer list is now {string.Join(", ", PeerIDs)}"); 71 | OnPeerJoined?.Invoke(newPeerID); 72 | } 73 | } 74 | else if (args.ConnectionState == RemoteConnectionState.Stopped) 75 | { 76 | var leftPeerID = args.ConnectionId; 77 | if (PeerIDs.Contains(leftPeerID)) 78 | { 79 | PeerIDs.Remove(leftPeerID); 80 | var log2 = $"Peer {leftPeerID} left. "; 81 | if (PeerIDs.Count == 0) 82 | log2 += "There are no peers anymore."; 83 | else 84 | log2 += $"Peer list is now {string.Join(", ", PeerIDs)}"; 85 | 86 | Debug.unityLogger.Log(LogType.Log, TAG, log2); 87 | OnPeerLeft?.Invoke(leftPeerID); 88 | } 89 | } 90 | } 91 | 92 | private void OnClientAuthenticated() 93 | { 94 | // We need to use OnClientAuthenticated to ensure the client does have ClientId set 95 | ID = _networkManager.ClientManager.Connection.ClientId; 96 | PeerIDs = _networkManager.ClientManager.Clients.Keys.Where(x => x != ID).ToList(); 97 | 98 | var log = $"Initialized with ID {ID}. "; 99 | if (PeerIDs.Count > 0) 100 | log += $"Peer list: {string.Join(", ", PeerIDs)}"; 101 | else 102 | log += "There are currently no peers."; 103 | Debug.unityLogger.Log(LogType.Log, TAG, log); 104 | 105 | OnJoined?.Invoke(ID, PeerIDs); 106 | foreach (var peerId in PeerIDs) 107 | OnPeerJoined?.Invoke(peerId); 108 | } 109 | 110 | private void OnClientConnectionStateChanged(ClientConnectionStateArgs args) 111 | { 112 | // We check only for the stopped state here, as the started state is handled in OnClientAuthenticated 113 | if (args.ConnectionState == LocalConnectionState.Stopped) 114 | { 115 | YourVoiceSettings = new VoiceSettings(); 116 | var oldPeerIds = PeerIDs.ToList(); 117 | PeerIDs.Clear(); 118 | ID = -1; 119 | foreach (var peerId in oldPeerIds) 120 | OnPeerLeft?.Invoke(peerId); 121 | OnLeft?.Invoke(); 122 | } 123 | } 124 | 125 | private void OnReceivedMessage(FishNetBroadcast msg, Channel channel) 126 | { 127 | var reader = new BytesReader(msg.data); 128 | var tag = reader.ReadString(); 129 | switch (tag) 130 | { 131 | // When the server sends audio from a peer meant for this client 132 | case FishNetBroadcastTags.AUDIO_FRAME: 133 | var sender = reader.ReadInt(); 134 | if (sender == ID || !PeerIDs.Contains(sender)) 135 | return; 136 | var frame = new AudioFrame 137 | { 138 | timestamp = reader.ReadLong(), 139 | frequency = reader.ReadInt(), 140 | channelCount = reader.ReadInt(), 141 | samples = reader.ReadByteArray() 142 | }; 143 | OnReceivedPeerAudioFrame?.Invoke(sender, frame); 144 | break; 145 | } 146 | } 147 | 148 | /// 149 | /// Sends an audio frame captured on this client to the server 150 | /// 151 | /// 152 | public void SendAudioFrame(AudioFrame frame) 153 | { 154 | if (ID == -1) 155 | return; 156 | var writer = new BytesWriter(); 157 | writer.WriteString(FishNetBroadcastTags.AUDIO_FRAME); 158 | writer.WriteInt(ID); 159 | writer.WriteLong(frame.timestamp); 160 | writer.WriteInt(frame.frequency); 161 | writer.WriteInt(frame.channelCount); 162 | writer.WriteByteArray(frame.samples); 163 | 164 | var message = new FishNetBroadcast 165 | { 166 | data = writer.Bytes 167 | }; 168 | 169 | if (_networkManager.ClientManager.Started) 170 | _networkManager.ClientManager.Broadcast(message, Channel.Unreliable); 171 | } 172 | 173 | /// 174 | /// Updates the server with the voice settings of this client 175 | /// 176 | public void SubmitVoiceSettings() 177 | { 178 | if (ID == -1) 179 | return; 180 | var writer = new BytesWriter(); 181 | writer.WriteString(FishNetBroadcastTags.VOICE_SETTINGS); 182 | writer.WriteInt(YourVoiceSettings.muteAll ? 1 : 0); 183 | writer.WriteIntArray(YourVoiceSettings.mutedPeers.ToArray()); 184 | writer.WriteInt(YourVoiceSettings.deafenAll ? 1 : 0); 185 | writer.WriteIntArray(YourVoiceSettings.deafenedPeers.ToArray()); 186 | writer.WriteString(string.Join(",", YourVoiceSettings.myTags)); 187 | writer.WriteString(string.Join(",", YourVoiceSettings.mutedTags)); 188 | writer.WriteString(string.Join(",", YourVoiceSettings.deafenedTags)); 189 | 190 | var message = new FishNetBroadcast() { 191 | data = writer.Bytes 192 | }; 193 | _networkManager.ClientManager.Broadcast(message); 194 | } 195 | } 196 | } 197 | #endif 198 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/FishNet/FishNetClient.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e49db0da30bc5fc479ca244d58f82481 -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/FishNet/FishNetServer.cs: -------------------------------------------------------------------------------- 1 | #if FISHNET 2 | using System; 3 | using System.Linq; 4 | using System.Collections.Generic; 5 | using UnityEngine; 6 | using Adrenak.BRW; 7 | using FishNet; 8 | using FishNet.Connection; 9 | using FishNet.Managing; 10 | using FishNet.Transporting; 11 | 12 | namespace Adrenak.UniVoice.Networks 13 | { 14 | /// 15 | /// This is an implementation of the interface for FishNet. 16 | /// It uses the FishNet to send and receive UniVoice audio data to and from clients. 17 | /// 18 | public class FishNetServer : IAudioServer 19 | { 20 | private const string TAG = "[FishNetServer]"; 21 | 22 | public event Action OnServerStart; 23 | public event Action OnServerStop; 24 | public event Action OnClientVoiceSettingsUpdated; 25 | 26 | public List ClientIDs { get; private set; } 27 | public Dictionary ClientVoiceSettings { get; private set; } 28 | 29 | private NetworkManager _networkManager; 30 | private List _startedTransports = new(); 31 | 32 | public FishNetServer() 33 | { 34 | ClientIDs = new List(); 35 | ClientVoiceSettings = new Dictionary(); 36 | 37 | _networkManager = InstanceFinder.NetworkManager; 38 | _networkManager.ServerManager.OnServerConnectionState += OnServerConnectionStateChanged; 39 | _networkManager.ServerManager.OnRemoteConnectionState += OnServerRemoteConnectionStateChanged; 40 | _networkManager.ClientManager.OnClientConnectionState += OnClientConnectionStateChanged; 41 | _networkManager.ServerManager.RegisterBroadcast(OnReceivedMessage, false); 42 | } 43 | 44 | public void Dispose() 45 | { 46 | if (_networkManager) 47 | { 48 | _networkManager.ServerManager.OnServerConnectionState -= OnServerConnectionStateChanged; 49 | _networkManager.ServerManager.OnRemoteConnectionState -= OnServerRemoteConnectionStateChanged; 50 | _networkManager.ClientManager.OnClientConnectionState -= OnClientConnectionStateChanged; 51 | _networkManager.ServerManager.UnregisterBroadcast(OnReceivedMessage); 52 | } 53 | OnServerShutdown(); 54 | } 55 | 56 | private void OnServerStarted() 57 | { 58 | OnServerStart?.Invoke(); 59 | } 60 | 61 | private void OnServerShutdown() 62 | { 63 | ClientIDs.Clear(); 64 | ClientVoiceSettings.Clear(); 65 | OnServerStop?.Invoke(); 66 | } 67 | 68 | private void OnServerRemoteConnectionStateChanged(NetworkConnection connection, RemoteConnectionStateArgs args) 69 | { 70 | if (args.ConnectionState == RemoteConnectionState.Started) 71 | { 72 | OnServerConnected(connection.ClientId); 73 | } 74 | else if (args.ConnectionState == RemoteConnectionState.Stopped) 75 | { 76 | OnServerDisconnected(connection.ClientId); 77 | } 78 | } 79 | 80 | private void OnServerConnectionStateChanged(ServerConnectionStateArgs args) 81 | { 82 | // Connection can change for each transport, so we need to track them 83 | if (args.ConnectionState == LocalConnectionState.Started) 84 | { 85 | var wasStarted = _startedTransports.Count != 0; 86 | _startedTransports.Add(args.TransportIndex); 87 | if (!wasStarted) 88 | OnServerStarted(); 89 | } 90 | else if (args.ConnectionState == LocalConnectionState.Stopped) 91 | { 92 | _startedTransports.Remove(args.TransportIndex); 93 | if(_startedTransports.Count == 0) 94 | OnServerShutdown(); 95 | } 96 | } 97 | 98 | private void OnClientConnectionStateChanged(ClientConnectionStateArgs args) 99 | { 100 | // TODO - do we need to check if host or is this enough? 101 | if (args.ConnectionState == LocalConnectionState.Started) 102 | { 103 | OnServerConnected(0); 104 | } 105 | else if (args.ConnectionState == LocalConnectionState.Stopped) 106 | { 107 | OnServerDisconnected(0); 108 | } 109 | } 110 | 111 | private void OnReceivedMessage(NetworkConnection connection, FishNetBroadcast message, Channel channel) 112 | { 113 | var clientId = connection.ClientId; 114 | var reader = new BytesReader(message.data); 115 | var tag = reader.ReadString(); 116 | 117 | if (tag.Equals(FishNetBroadcastTags.AUDIO_FRAME)) 118 | { 119 | // We start with all the peers except the one that's 120 | // sent the audio 121 | var peersToForwardAudioTo = ClientIDs 122 | .Where(x => x != clientId); 123 | 124 | // Check the voice settings of the sender and eliminate any peers the sender 125 | // may have deafened 126 | if (ClientVoiceSettings.TryGetValue(clientId, out var senderSettings)) 127 | { 128 | // If the client sending the audio has deafened everyone, 129 | // we simply return. Sender's audio should not be forwarded to anyone. 130 | if (senderSettings.deafenAll) 131 | return; 132 | 133 | // Filter the recipient list by removing all peers that the sender has 134 | // deafened using ID 135 | peersToForwardAudioTo = peersToForwardAudioTo 136 | .Where(x => !senderSettings.deafenedPeers.Contains(x)); 137 | 138 | // Further filter the recipient list by removing peers that the sender has 139 | // deafened using tags 140 | peersToForwardAudioTo = peersToForwardAudioTo.Where(peer => 141 | { 142 | // Get the voice settings of the peer 143 | if (ClientVoiceSettings.TryGetValue(peer, out var peerVoiceSettings)) 144 | { 145 | // Check if sender has not deafened peer using tag 146 | var hasDeafenedPeer = senderSettings.deafenedTags.Intersect(peerVoiceSettings.myTags).Any(); 147 | return !hasDeafenedPeer; 148 | } 149 | // If peer doesn't have voice settings, we can keep the peer in the list 150 | return true; 151 | }); 152 | } 153 | 154 | // We iterate through each recipient peer that the sender wants to send audio to, checking if 155 | // they have muted the sender, before forwarding the audio to them. 156 | foreach (var recipient in peersToForwardAudioTo) { 157 | // Get the settings of a potential recipient 158 | if (ClientVoiceSettings.TryGetValue(recipient, out var recipientSettings)) { 159 | // If a peer has muted everyone, don't send audio 160 | if (recipientSettings.muteAll) 161 | continue; 162 | 163 | // If the peers has muted the sender using ID, skip sending audio 164 | if (recipientSettings.mutedPeers.Contains(clientId)) 165 | continue; 166 | 167 | // If the peer has muted the sender using tag, skip sending audio 168 | if (recipientSettings.mutedTags.Intersect(senderSettings.myTags).Any()) 169 | continue; 170 | } 171 | SendToClient(recipient, message.data, Channel.Unreliable); 172 | } 173 | } 174 | else if (tag.Equals(FishNetBroadcastTags.VOICE_SETTINGS)) { 175 | //Debug.unityLogger.Log(LogType.Log, TAG, "FishNet server stopped"); 176 | // We create the VoiceSettings object by reading from the reader 177 | // and update the peer voice settings map 178 | var muteAll = reader.ReadInt() == 1; 179 | var mutedPeers = reader.ReadIntArray().ToList(); 180 | var deafenAll = reader.ReadInt() == 1; 181 | var deafenedPeers = reader.ReadIntArray().ToList(); 182 | var myTags = reader.ReadString().Split(",").ToList(); 183 | var mutedTags = reader.ReadString().Split(",").ToList(); 184 | var deafenedTags = reader.ReadString().Split(",").ToList(); 185 | 186 | var voiceSettings = new VoiceSettings { 187 | muteAll = muteAll, 188 | mutedPeers = mutedPeers, 189 | deafenAll = deafenAll, 190 | deafenedPeers = deafenedPeers, 191 | myTags = myTags, 192 | mutedTags = mutedTags, 193 | deafenedTags = deafenedTags 194 | }; 195 | ClientVoiceSettings[clientId] = voiceSettings; 196 | OnClientVoiceSettingsUpdated?.Invoke(); 197 | } 198 | } 199 | 200 | private void OnServerConnected(int connId) 201 | { 202 | Debug.unityLogger.Log(LogType.Log, TAG, $"Client {connId} connected"); 203 | ClientIDs.Add(connId); 204 | } 205 | 206 | private void OnServerDisconnected(int connId) 207 | { 208 | ClientIDs.Remove(connId); 209 | Debug.unityLogger.Log(LogType.Log, TAG, $"Client {connId} disconnected"); 210 | } 211 | 212 | private void SendToClient(int clientConnId, byte[] bytes, Channel channel) 213 | { 214 | if (!TryGetConnectionToClient(clientConnId, out var connection)) 215 | return; 216 | 217 | var message = new FishNetBroadcast {data = bytes}; 218 | _networkManager.ServerManager.Broadcast(connection, message, false, channel); 219 | } 220 | 221 | private bool TryGetConnectionToClient(int desiredClientID, out NetworkConnection resultConnection) 222 | { 223 | resultConnection = null; 224 | foreach (var (clientID, conn) in _networkManager.ServerManager.Clients) 225 | { 226 | if (clientID == desiredClientID) 227 | { 228 | resultConnection = conn; 229 | return true; 230 | } 231 | } 232 | return false; 233 | } 234 | } 235 | } 236 | #endif 237 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/FishNet/FishNetServer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7a700664d8ca951499b4d5a0fdd8a5f5 -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/Mirror.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4ddfae6dcdf2d1f4a8ebf8fba7da1fc9 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/Mirror/MirrorClient.cs: -------------------------------------------------------------------------------- 1 | #if MIRROR 2 | using System; 3 | using System.Linq; 4 | using System.Collections.Generic; 5 | 6 | using Mirror; 7 | using Adrenak.BRW; 8 | using UnityEngine; 9 | 10 | namespace Adrenak.UniVoice.Networks { 11 | /// 12 | /// Activate this class by including the UNIVOICE_MIRROR_NETWORK compilaton symbol 13 | /// in your project. 14 | /// This is the implementation of interface for Mirror. 15 | /// It uses the Mirror transport to send and receive UniVoice data to the server. 16 | /// 17 | public class MirrorClient : IAudioClient { 18 | const string TAG = "[MirrorClient]"; 19 | 20 | public int ID { get; private set; } = -1; 21 | 22 | public List PeerIDs { get; private set; } 23 | 24 | public VoiceSettings YourVoiceSettings { get; private set; } 25 | 26 | public event Action> OnJoined; 27 | public event Action OnLeft; 28 | public event Action OnPeerJoined; 29 | public event Action OnPeerLeft; 30 | public event Action OnReceivedPeerAudioFrame; 31 | 32 | readonly MirrorModeObserver mirrorEvents; 33 | 34 | public MirrorClient() { 35 | PeerIDs = new List(); 36 | YourVoiceSettings = new VoiceSettings(); 37 | 38 | mirrorEvents = MirrorModeObserver.New("for MirrorClient"); 39 | mirrorEvents.ModeChanged += OnModeChanged; 40 | 41 | NetworkClient.RegisterHandler(OnReceivedMessage, false); 42 | } 43 | 44 | public void Dispose() { 45 | PeerIDs.Clear(); 46 | } 47 | 48 | void OnModeChanged(NetworkManagerMode oldMode, NetworkManagerMode newMode) { 49 | // For some reason, handlers don't always work as expected when the connection mode changes 50 | NetworkClient.ReplaceHandler(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(); 60 | 61 | OnClientDisconnected(); 62 | } 63 | } 64 | 65 | void OnClientDisconnected() { 66 | YourVoiceSettings = new VoiceSettings(); 67 | var oldPeerIds = PeerIDs; 68 | PeerIDs.Clear(); 69 | ID = -1; 70 | foreach (var peerId in oldPeerIds) 71 | OnPeerLeft?.Invoke(peerId); 72 | OnLeft?.Invoke(); 73 | } 74 | 75 | void OnReceivedMessage(MirrorMessage msg) { 76 | var reader = new BytesReader(msg.data); 77 | var tag = reader.ReadString(); 78 | switch (tag) { 79 | // When the server sends the data to initial this client with. 80 | // This includes the ID of this client along with the IDs of all the 81 | // peers that are already connected to the server 82 | case MirrorMessageTags.PEER_INIT: 83 | ID = reader.ReadInt(); 84 | PeerIDs = reader.ReadIntArray().ToList(); 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); 94 | foreach (var peerId in PeerIDs) 95 | OnPeerJoined?.Invoke(peerId); 96 | break; 97 | 98 | // When the server notifies that a new peer has joined the network 99 | case MirrorMessageTags.PEER_JOINED: 100 | var newPeerID = reader.ReadInt(); 101 | if (!PeerIDs.Contains(newPeerID)) { 102 | PeerIDs.Add(newPeerID); 103 | Debug.unityLogger.Log(LogType.Log, TAG, 104 | $"Peer {newPeerID} joined. Peer list is now {string.Join(", ", PeerIDs)}"); 105 | OnPeerJoined?.Invoke(newPeerID); 106 | } 107 | break; 108 | 109 | // When the server notifies that a peer has left the network 110 | case MirrorMessageTags.PEER_LEFT: 111 | var leftPeerID = reader.ReadInt(); 112 | if (PeerIDs.Contains(leftPeerID)) { 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); 121 | OnPeerLeft?.Invoke(leftPeerID); 122 | } 123 | break; 124 | 125 | // When the server sends audio from a peer meant for this client 126 | case MirrorMessageTags.AUDIO_FRAME: 127 | var sender = reader.ReadInt(); 128 | if (sender == ID || !PeerIDs.Contains(sender)) 129 | return; 130 | var frame = new AudioFrame { 131 | timestamp = reader.ReadLong(), 132 | frequency = reader.ReadInt(), 133 | channelCount = reader.ReadInt(), 134 | samples = reader.ReadByteArray() 135 | }; 136 | OnReceivedPeerAudioFrame?.Invoke(sender, frame); 137 | break; 138 | } 139 | } 140 | 141 | /// 142 | /// Sends an audio frame captured on this client to the server 143 | /// 144 | /// 145 | public void SendAudioFrame(AudioFrame frame) { 146 | if (ID == -1) 147 | return; 148 | var writer = new BytesWriter(); 149 | writer.WriteString(MirrorMessageTags.AUDIO_FRAME); 150 | writer.WriteInt(ID); 151 | writer.WriteLong(frame.timestamp); 152 | writer.WriteInt(frame.frequency); 153 | writer.WriteInt(frame.channelCount); 154 | writer.WriteByteArray(frame.samples); 155 | 156 | var message = new MirrorMessage { 157 | data = writer.Bytes 158 | }; 159 | NetworkClient.Send(message, Channels.Unreliable); 160 | } 161 | 162 | /// 163 | /// Updates the server with the voice settings of this client 164 | /// 165 | public void SubmitVoiceSettings() { 166 | if (ID == -1) 167 | return; 168 | var writer = new BytesWriter(); 169 | writer.WriteString(MirrorMessageTags.VOICE_SETTINGS); 170 | writer.WriteInt(YourVoiceSettings.muteAll ? 1 : 0); 171 | writer.WriteIntArray(YourVoiceSettings.mutedPeers.ToArray()); 172 | writer.WriteInt(YourVoiceSettings.deafenAll ? 1 : 0); 173 | writer.WriteIntArray(YourVoiceSettings.deafenedPeers.ToArray()); 174 | writer.WriteString(string.Join(",", YourVoiceSettings.myTags)); 175 | writer.WriteString(string.Join(",", YourVoiceSettings.mutedTags)); 176 | writer.WriteString(string.Join(",", YourVoiceSettings.deafenedTags)); 177 | 178 | var message = new MirrorMessage { 179 | data = writer.Bytes 180 | }; 181 | NetworkClient.Send(message, Channels.Reliable); 182 | } 183 | } 184 | } 185 | #endif -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/Mirror/MirrorClient.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 504fe5edfdfe40245a3ad7149b63c1c0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/Mirror/MirrorMessage.cs: -------------------------------------------------------------------------------- 1 | #if MIRROR 2 | using System; 3 | 4 | using Mirror; 5 | 6 | namespace Adrenak.UniVoice.Networks { 7 | /// 8 | /// The messages exchanged between the server and client. 9 | /// To see how the Mirror implementation of UniVoice uses this struct 10 | /// find the references to the object in the project. 11 | /// The gist is, it uses BRW (https://www.github.com/adrenak/brw) to 12 | /// write and read data. The data always starts with a tag. All the tags 13 | /// used for this UniVoice Mirror implementation are available in 14 | /// 15 | /// 16 | [Serializable] 17 | public struct MirrorMessage : NetworkMessage { 18 | public byte[] data; 19 | } 20 | } 21 | #endif -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/Mirror/MirrorMessage.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 26fed4dcaa91f29418e64833d3c475f7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/Mirror/MirrorMessageTags.cs: -------------------------------------------------------------------------------- 1 | #if MIRROR 2 | namespace Adrenak.UniVoice.Networks { 3 | /// 4 | /// The different types of messages we send over Mirror 5 | /// to implement the and 6 | /// interfaces for Mirror 7 | /// 8 | public class MirrorMessageTags { 9 | public const string PEER_INIT = "PEER_INIT"; 10 | public const string PEER_JOINED = "PEER_JOINED"; 11 | public const string PEER_LEFT = "PEER_LEFT"; 12 | public const string AUDIO_FRAME = "AUDIO_FRAME"; 13 | public const string VOICE_SETTINGS = "VOICE_SETTINGS"; 14 | } 15 | } 16 | #endif -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/Mirror/MirrorMessageTags.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: addae80214286ff4bb4a0676c91ad4b0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/Mirror/MirrorModeObserver.cs: -------------------------------------------------------------------------------- 1 | #if MIRROR 2 | using Mirror; 3 | 4 | using System; 5 | 6 | using UnityEngine; 7 | 8 | namespace Adrenak.UniVoice.Networks { 9 | /// 10 | /// Observes the mode of the Mirror NetworkManager and fires an event 11 | /// when it changes 12 | /// 13 | public class MirrorModeObserver : MonoBehaviour { 14 | const string TAG = "[MirrorModeObserver]"; 15 | 16 | /// 17 | /// Event fired when the Mirror NetworkManager changes the mode 18 | /// 19 | public event Action ModeChanged; 20 | 21 | [Obsolete("Use .New instead.", true)] 22 | public MirrorModeObserver() { } 23 | 24 | NetworkManagerMode lastMode = NetworkManagerMode.Offline; 25 | 26 | /// 27 | /// Creates a new instance of this class on a GameObject 28 | /// 29 | /// 30 | public static MirrorModeObserver New(string name = "") { 31 | var go = new GameObject($"MirrorEventProvider {name}"); 32 | DontDestroyOnLoad(go); 33 | return go.AddComponent(); 34 | } 35 | 36 | void Update() { 37 | var newMode = NetworkManager.singleton.mode; 38 | if (lastMode != newMode) { 39 | try { 40 | ModeChanged?.Invoke(lastMode, newMode); 41 | } 42 | catch (Exception e) { 43 | Debug.unityLogger.Log(LogType.Error, TAG, "Exception while handling Mirror Mode change: " + e); 44 | } 45 | lastMode = newMode; 46 | } 47 | } 48 | } 49 | } 50 | #endif -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/Mirror/MirrorModeObserver.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d2ff1b74d93a3f740a02892fcda7e129 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Networks/Mirror/MirrorServer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2f3e6da2ea4c92140aefa5dee6abeec6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Outputs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ced8c50ab9df8284a8c637b091cc9aaf 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Outputs/StreamedAudioSourceOutput.cs: -------------------------------------------------------------------------------- 1 | using Adrenak.UniMic; 2 | 3 | using UnityEngine; 4 | 5 | namespace Adrenak.UniVoice.Outputs { 6 | /// 7 | /// An implementation of that plays 8 | /// peer audio using StreamedAUdioSource, which is included in UniMic 9 | /// 10 | [RequireComponent(typeof(StreamedAudioSource))] 11 | public class StreamedAudioSourceOutput : MonoBehaviour, IAudioOutput { 12 | const string TAG = "[StreamedAudioSourceOutput]"; 13 | 14 | public StreamedAudioSource Stream { get; private set; } 15 | 16 | [System.Obsolete("Cannot use new keyword to create an instance. Use the .New() method instead")] 17 | public StreamedAudioSourceOutput() { } 18 | 19 | /// 20 | /// Creates a new instance using the dependencies. 21 | /// 22 | public static StreamedAudioSourceOutput New() { 23 | var go = new GameObject("StreamedAudioSourceOutput"); 24 | DontDestroyOnLoad(go); 25 | var cted = go.AddComponent(); 26 | cted.Stream = go.GetComponent(); 27 | Debug.unityLogger.Log(LogType.Log, TAG, "StreamedAudioSource created"); 28 | return cted; 29 | } 30 | 31 | /// 32 | /// Feeds an incoming into the audio buffer. 33 | /// 34 | /// 35 | public void Feed(AudioFrame frame) { 36 | Stream.Feed(frame.frequency, frame.channelCount, Utils.Bytes.BytesToFloats(frame.samples)); 37 | } 38 | 39 | /// 40 | /// Disposes the instance by deleting the GameObject of the component. 41 | /// 42 | public void Dispose() { 43 | Debug.unityLogger.Log(LogType.Log, TAG, "Disposing StreamedAudioSource"); 44 | Destroy(gameObject); 45 | } 46 | 47 | /// 48 | /// Creates instances 49 | /// 50 | public class Factory : IAudioOutputFactory { 51 | public IAudioOutput Create() { 52 | return New(); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Impl/Outputs/StreamedAudioSourceOutput.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 17486dc6a1c2cc54585b7e53976d63b9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Interfaces.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a0b094f3fe5bc624fae2ffb1e18c19a1 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Interfaces/IAudioClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Adrenak.UniVoice { 5 | /// 6 | /// Audio client interface. 7 | /// The implementation of this class is generally based on some networking 8 | /// framework such as Mirror, FishNet, Unity Netcode, etc. 9 | /// 10 | /// 11 | /// The identifier data type used by the framework you're using in your implementation. 12 | /// For example, Mirror identifies players using int. So, a MirrorAudioClient class 13 | /// that implements this interface would be MirrorAudioClient : IAudioClient 14 | /// 15 | public interface IAudioClient : IDisposable { 16 | /// 17 | /// The clients peer ID in the voice chat 18 | /// 19 | T ID { get; } 20 | 21 | /// 22 | /// IDs of all the peers (except this client) in the voice chat 23 | /// 24 | List PeerIDs { get; } 25 | 26 | /// 27 | /// The voice settings of this client. Call 28 | /// after making changes to this object to submit the updates to the server. 29 | /// 30 | VoiceSettings YourVoiceSettings { get; } 31 | 32 | /// 33 | /// Fired when this client connects and joins the voice chat 34 | /// Includes the following parameters 35 | /// - own peer ID (int). This should also get assigned to 36 | /// - IDs of other peers. This should also get assigned to 37 | /// 38 | event Action> OnJoined; 39 | 40 | /// 41 | /// Fired when this client disconnects and leaves the voice chat 42 | /// 43 | event Action OnLeft; 44 | 45 | /// 46 | /// Fired when a new peer joins the voice chat. 47 | /// Provides the ID of the client as event data. 48 | /// 49 | event Action OnPeerJoined; 50 | 51 | /// 52 | /// Fired when a client leaves the chatroom. 53 | /// Provides the ID of the client as event data. 54 | /// 55 | event Action OnPeerLeft; 56 | 57 | /// 58 | /// Event fired when an audio frame is received from 59 | /// another peer via the server. Parameters: 60 | /// - peer ID: the ID of the peer that sent the audio frame 61 | /// - AudioFrame: the frame containing audio data 62 | /// 63 | event Action OnReceivedPeerAudioFrame; 64 | 65 | /// 66 | /// Sends an audio frame to the server for being 67 | /// broadcasted to the other peers 68 | /// 69 | /// The audio frame to be sent 70 | void SendAudioFrame(AudioFrame frame); 71 | 72 | /// 73 | /// Submits to the server 74 | /// 75 | void SubmitVoiceSettings(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Interfaces/IAudioClient.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fe301245402533549bcd0b1df29c1dd9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Interfaces/IAudioFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Adrenak.UniVoice { 2 | /// 3 | /// Offers ways to modify audio after being captured. 4 | /// To prevent the audio from being sent (for example when performing some pass or gating) 5 | /// return an empty byte array (new byte[0]) 6 | /// 7 | public interface IAudioFilter { 8 | AudioFrame Run(AudioFrame input); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Interfaces/IAudioFilter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7f0c17dee257ca046b3356a3ef107503 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Interfaces/IAudioInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Adrenak.UniVoice { 4 | /// 5 | /// Source of user voice input. This would usually be implemented 6 | /// over a microphone to get the users voice. But it can also be used 7 | /// in other ways such as streaming an mp4 file from disk. It's just 8 | /// an input and the source doesn't matter. 9 | /// 10 | public interface IAudioInput : IDisposable { 11 | event Action OnFrameReady; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Interfaces/IAudioInput.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d56c2c84daac5494b93669532bca0b98 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Interfaces/IAudioOutput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Adrenak.UniVoice { 4 | /// 5 | /// Responsible for handling incoming audio. 6 | /// You'd normally want a 7 | /// based implementation to play the audio in Unity. But this class can 8 | /// be used in other ways such as streaming the received audio to a server 9 | /// or writing it to a local file. It's just an audio output and the 10 | /// destination depends on your implementation. 11 | /// 12 | public interface IAudioOutput : IDisposable { 13 | /// 14 | /// Feeds a object to the audio output. 15 | /// 16 | /// The audio data to be sent. 17 | void Feed(AudioFrame frame); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Interfaces/IAudioOutput.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7d34118811133f44eb7ba1b5c3f6bb77 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Interfaces/IAudioOutputFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Adrenak.UniVoice { 2 | /// 3 | /// An abstract factory that creates a concrete 4 | /// 5 | public interface IAudioOutputFactory { 6 | /// 7 | /// Creates an instance of a concrete class 8 | /// 9 | IAudioOutput Create(); 10 | } 11 | } -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Interfaces/IAudioOutputFactory.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 095ec1523c98b7f4997c105a06856823 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Interfaces/IAudioServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Adrenak.UniVoice { 5 | /// 6 | /// Audio server interface. 7 | /// The implementation of this class is generally based on some networking 8 | /// framework such as Mirror, FishNet, Unity Netcode, etc. 9 | /// 10 | /// 11 | /// The identifier data type used by the framework you're using in your implementation. 12 | /// For example, Mirror identifies players using int. So, a MirrorAudioClient class 13 | /// that implements this interface would be MirrorAudioServer : IAudioServer 14 | /// 15 | public interface IAudioServer : IDisposable { 16 | /// 17 | /// Event fired when the server starts 18 | /// 19 | event Action OnServerStart; 20 | 21 | /// 22 | /// Event fired when the server stops 23 | /// 24 | event Action OnServerStop; 25 | 26 | /// 27 | /// Event fired when the peer voice settings are updated 28 | /// 29 | event Action OnClientVoiceSettingsUpdated; 30 | 31 | /// 32 | /// IDs of all the clients in the voice chat 33 | /// 34 | List ClientIDs { get; } 35 | 36 | /// 37 | /// of every client connected to the server 38 | /// 39 | Dictionary ClientVoiceSettings { get; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Interfaces/IAudioServer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 04f9cb1c5b36b564d9fcc45138c78705 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Types.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: af3826214be11544e9a38629b9c93566 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Types/AudioFrame.cs: -------------------------------------------------------------------------------- 1 | namespace Adrenak.UniVoice { 2 | [System.Serializable] 3 | /// 4 | /// A data structure representing the audio transmitted over the network. 5 | /// 6 | public struct AudioFrame { 7 | /// 8 | /// The UTC Unix timestamp (in ms) when the samples were captured. 9 | /// The timestamp is local to the client the audio was captured from. 10 | /// 11 | public long timestamp; 12 | 13 | /// 14 | /// The frequency (or sampling rate) of the audio 15 | /// 16 | public int frequency; 17 | 18 | /// 19 | /// The number of channels in the audio 20 | /// 21 | public int channelCount; 22 | 23 | /// 24 | /// A byte array representing the audio data 25 | /// 26 | public byte[] samples; 27 | } 28 | } -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Types/AudioFrame.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e3c9ef93e574acf4094f05ab7346bfd5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Types/VoiceSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Adrenak.UniVoice { 4 | public class VoiceSettings { 5 | /// 6 | /// If true, we don't want to listen to any peer 7 | /// 8 | public bool muteAll; 9 | 10 | /// 11 | /// The peers we don't want to listen to 12 | /// 13 | public List mutedPeers = new List(); 14 | 15 | /// 16 | /// If true, we don't want any peer to listen to us 17 | /// 18 | public bool deafenAll; 19 | 20 | /// 21 | /// The peers we don't want listening to us 22 | /// 23 | public List deafenedPeers = new List(); 24 | 25 | /// 26 | /// The tags associated with this client. 27 | /// DO NOT USE COMMAS (,) 28 | /// 29 | public List myTags = new List(); 30 | 31 | /// 32 | /// The tags, which if associated with a peer, would cause 33 | /// those the audio of that peer to not be send to this peer 34 | /// DO NOT USE COMMAS (,) 35 | /// 36 | public List mutedTags = new List(); 37 | 38 | /// 39 | /// The tags, which if associated with a peer, would cause 40 | /// those peers to not receive audio from this client 41 | /// DO NOT USE COMMAS (,) 42 | /// 43 | public List deafenedTags = new List(); 44 | 45 | /// 46 | /// Sets the deaf status of a peer 47 | /// 48 | public void SetDeaf(int peerId, bool state) { 49 | if(state) { 50 | if (!deafenedPeers.Contains(peerId)) 51 | deafenedPeers.Add(peerId); 52 | } 53 | else { 54 | if (deafenedPeers.Contains(peerId)) 55 | deafenedPeers.Remove(peerId); 56 | } 57 | } 58 | 59 | /// 60 | /// Sets the mute status of a peer 61 | /// 62 | public void SetMute(int peerId, bool state) { 63 | if (state) { 64 | if (!mutedPeers.Contains(peerId)) 65 | mutedPeers.Add(peerId); 66 | } 67 | else { 68 | if (mutedPeers.Contains(peerId)) 69 | mutedPeers.Remove(peerId); 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Runtime/Types/VoiceSettings.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0508f9903d5ba23429db06389f09b8b4 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dc0b6ac6ddea66b4b8aee68c2ee0e99a 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Basic Setup Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7974694d7a30f894db7efac8d8d46573 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Basic Setup Scripts/FishNet-SinglePrefabObjects.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 4489d77032a81ef42b0067acf2737d4d, type: 3} 13 | m_Name: FishNet-SinglePrefabObjects 14 | m_EditorClassIdentifier: 15 | _prefabs: [] 16 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Basic Setup Scripts/FishNet-SinglePrefabObjects.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b3f3638a0e223704fb042d01a269adcb 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Basic Setup Scripts/UniVoiceFishNetSetupSample.cs: -------------------------------------------------------------------------------- 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 | /// 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 | /// - , an implementation of 20 | /// - , an implementation of 21 | /// - , an implementation of that captures audio from a mic 22 | /// - , an implementation of that is basically 23 | /// an idle audio input used when there is no input device 24 | /// - , an implementation of that removes noise from 25 | /// captured audio. 26 | /// - , an implementation of that encodes captured audio 27 | /// using Concentus (C# Opus) to reduce the size of audio frames 28 | /// - , an implementation of that decodes incoming audio 29 | /// using Concentus to decode and make the audio frame playable. 30 | /// 31 | public class UniVoiceFishNetSetupSample : MonoBehaviour { 32 | const string TAG = "[UniVoiceFishNetSetupSample]"; 33 | 34 | /// 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 | /// 38 | public static bool HasSetUp { get; private set; } 39 | 40 | /// 41 | /// The server object. 42 | /// 43 | public static IAudioServer AudioServer { get; private set; } 44 | 45 | /// 46 | /// The client session. 47 | /// 48 | public static ClientSession ClientSession { get; private set; } 49 | 50 | [SerializeField] bool useRNNoise4UnityIfAvailable = true; 51 | 52 | [SerializeField] bool useConcentusEncodeAndDecode = true; 53 | 54 | [SerializeField] bool useVad = true; 55 | 56 | void Start() { 57 | if (HasSetUp) { 58 | Debug.unityLogger.Log(LogType.Log, TAG, "UniVoice is already set up. Ignoring..."); 59 | return; 60 | } 61 | HasSetUp = Setup(); 62 | } 63 | 64 | bool Setup() { 65 | Debug.unityLogger.Log(LogType.Log, TAG, "Trying to setup UniVoice"); 66 | 67 | bool failed = false; 68 | 69 | // Set setup the AudioServer and ClientSession on ALL builds. This means that you'd 70 | // have a ClientSession on a dedicated server, even though there's not much you can do with it. 71 | // Similarly, a client would also have an AudioServer object. But it would just be inactive. 72 | // This sample is for ease of use and to get something working quickly, so we don't bother 73 | // with these minor details. Note that doing so does not have any performance implications 74 | // so you can do this, so you could keep this approach without any tradeoffs. 75 | var createdAudioServer = SetupAudioServer(); 76 | if (!createdAudioServer) { 77 | Debug.unityLogger.Log(LogType.Error, TAG, "Could not setup UniVoice server."); 78 | failed = true; 79 | } 80 | 81 | var setupAudioClient = SetupClientSession(); 82 | if (!setupAudioClient) { 83 | Debug.unityLogger.Log(LogType.Error, TAG, "Could not setup UniVoice client."); 84 | failed = true; 85 | } 86 | 87 | if (!failed) 88 | Debug.unityLogger.Log(LogType.Log, TAG, "UniVoice successfully setup!"); 89 | else 90 | Debug.unityLogger.Log(LogType.Error, TAG, $"Refer to the notes on top of {typeof(UniVoiceMirrorSetupSample).Name}.cs for setup instructions."); 91 | return !failed; 92 | } 93 | 94 | bool SetupAudioServer() { 95 | #if FISHNET 96 | // ---- CREATE AUDIO SERVER AND SUBSCRIBE TO EVENTS TO PRINT LOGS ---- 97 | // We create a server. If this code runs in server mode, MirrorServer will take care 98 | // or automatically handling all incoming messages. On a device connecting as a client, 99 | // this code doesn't do anything. 100 | AudioServer = new FishNetServer(); 101 | Debug.unityLogger.Log(LogType.Log, TAG, "Created MirrorServer object"); 102 | 103 | AudioServer.OnServerStart += () => { 104 | Debug.unityLogger.Log(LogType.Log, TAG, "Server started"); 105 | }; 106 | 107 | AudioServer.OnServerStop += () => { 108 | Debug.unityLogger.Log(LogType.Log, TAG, "Server stopped"); 109 | }; 110 | return true; 111 | #else 112 | Debug.unityLogger.Log(LogType.Error, TAG, "MirrorServer implementation not found!"); 113 | return false; 114 | #endif 115 | } 116 | 117 | bool SetupClientSession() { 118 | #if FISHNET 119 | // ---- CREATE AUDIO CLIENT AND SUBSCRIBE TO EVENTS ---- 120 | IAudioClient client = new FishNetClient(); 121 | client.OnJoined += (id, peerIds) => { 122 | Debug.unityLogger.Log(LogType.Log, TAG, $"You are Peer ID {id}"); 123 | }; 124 | 125 | client.OnLeft += () => { 126 | Debug.unityLogger.Log(LogType.Log, TAG, "You left the chatroom"); 127 | }; 128 | 129 | // When a peer joins, we instantiate a new peer view 130 | client.OnPeerJoined += id => { 131 | Debug.unityLogger.Log(LogType.Log, TAG, $"Peer {id} joined"); 132 | }; 133 | 134 | // When a peer leaves, destroy the UI representing them 135 | client.OnPeerLeft += id => { 136 | Debug.unityLogger.Log(LogType.Log, TAG, $"Peer {id} left"); 137 | }; 138 | 139 | Debug.unityLogger.Log(LogType.Log, TAG, "Created MirrorClient object"); 140 | 141 | // ---- CREATE AUDIO INPUT ---- 142 | IAudioInput input; 143 | // Since in this sample we use microphone input via UniMic, we first check if there 144 | // are any mic devices available. 145 | Mic.Init(); // Must do this to use the Mic class 146 | if (Mic.AvailableDevices.Count == 0) { 147 | Debug.unityLogger.Log(LogType.Log, TAG, "Device has no microphones." + 148 | "Will only be able to hear other clients, cannot send any audio."); 149 | input = new EmptyAudioInput(); 150 | Debug.unityLogger.Log(LogType.Log, TAG, "Created EmptyAudioInput"); 151 | } 152 | else { 153 | // Get the first recording device that we have available and start it. 154 | // Then we create a UniMicInput instance that requires the mic object 155 | // For more info on UniMic refer to https://www.github.com/adrenak/unimic 156 | var mic = Mic.AvailableDevices[0]; 157 | mic.StartRecording(60); 158 | Debug.unityLogger.Log(LogType.Log, TAG, "Started recording with Mic device named." + 159 | mic.Name + $" at frequency {mic.SamplingFrequency} with frame duration {mic.FrameDurationMS} ms."); 160 | input = new UniMicInput(mic); 161 | Debug.unityLogger.Log(LogType.Log, TAG, "Created UniMicInput"); 162 | } 163 | 164 | // ---- CREATE AUDIO OUTPUT FACTORY ---- 165 | IAudioOutputFactory outputFactory; 166 | // We want the incoming audio from peers to be played via the StreamedAudioSourceOutput 167 | // implementation of IAudioSource interface. So we get the factory for it. 168 | outputFactory = new StreamedAudioSourceOutput.Factory(); 169 | Debug.unityLogger.Log(LogType.Log, TAG, "Using StreamedAudioSourceOutput.Factory as output factory"); 170 | 171 | // ---- CREATE CLIENT SESSION AND ADD FILTERS TO IT ---- 172 | // With the client, input and output factory ready, we create create the client session 173 | ClientSession = new ClientSession(client, input, outputFactory); 174 | Debug.unityLogger.Log(LogType.Log, TAG, "Created session"); 175 | 176 | #if UNIVOICE_FILTER_RNNOISE4UNITY 177 | if(useRNNoise4UnityIfAvailable) { 178 | // RNNoiseFilter to remove noise from captured audio 179 | session.InputFilters.Add(new RNNoiseFilter()); 180 | Debug.unityLogger.Log(LogType.Log, TAG, "Registered RNNoiseFilter as an input filter"); 181 | } 182 | #endif 183 | 184 | if (useVad) { 185 | // We add the VAD filter after RNNoise. 186 | // This way lot of the background noise has been removed, VAD is truly trying to detect voice 187 | ClientSession.InputFilters.Add(new SimpleVadFilter(new SimpleVad())); 188 | } 189 | 190 | if (useConcentusEncodeAndDecode) { 191 | // ConcentureEncoder filter to encode captured audio that reduces the audio frame size 192 | ClientSession.InputFilters.Add(new ConcentusEncodeFilter()); 193 | Debug.unityLogger.Log(LogType.Log, TAG, "Registered ConcentusEncodeFilter as an input filter"); 194 | 195 | // For incoming audio register the ConcentusDecodeFilter to decode the encoded audio received from other clients 196 | ClientSession.AddOutputFilter(() => new ConcentusDecodeFilter()); 197 | Debug.unityLogger.Log(LogType.Log, TAG, "Registered ConcentusDecodeFilter as an output filter"); 198 | } 199 | 200 | return true; 201 | #else 202 | Debug.unityLogger.Log(LogType.Error, TAG, "MirrorClient implementation not found!"); 203 | return false; 204 | #endif 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Basic Setup Scripts/UniVoiceFishNetSetupSample.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 812e71335abfc894ea5415f23adf5ea6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Basic Setup Scripts/UniVoiceFishNetSetupSample.unity.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fcc19499518b4204d8fdeb4ddd323bde 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Basic Setup Scripts/UniVoiceMirrorSetupSample.cs: -------------------------------------------------------------------------------- 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 | /// 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 | /// - , an implementation of 20 | /// - , an implementation of 21 | /// - , an implementation of that captures audio from a mic 22 | /// - , an implementation of that is basically 23 | /// an idle audio input used when there is no input device 24 | /// - , an implementation of that removes noise from 25 | /// captured audio. 26 | /// - , an implementation of that encodes captured audio 27 | /// using Concentus (C# Opus) to reduce the size of audio frames 28 | /// - , an implementation of that decodes incoming audio 29 | /// using Concentus to decode and make the audio frame playable. 30 | /// 31 | public class UniVoiceMirrorSetupSample : MonoBehaviour { 32 | const string TAG = "[BasicUniVoiceSetupSample]"; 33 | 34 | /// 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 | /// 38 | public static bool HasSetUp { get; private set; } 39 | 40 | /// 41 | /// The server object. 42 | /// 43 | public static IAudioServer AudioServer { get; private set; } 44 | 45 | /// 46 | /// The client session. 47 | /// 48 | public static ClientSession ClientSession { get; private set; } 49 | 50 | [SerializeField] bool useRNNoise4UnityIfAvailable = true; 51 | 52 | [SerializeField] bool useConcentusEncodeAndDecode = true; 53 | 54 | [SerializeField] bool useVad = true; 55 | 56 | void Start() { 57 | if (HasSetUp) { 58 | Debug.unityLogger.Log(LogType.Log, TAG, "UniVoice is already set up. Ignoring..."); 59 | return; 60 | } 61 | HasSetUp = Setup(); 62 | } 63 | 64 | bool Setup() { 65 | Debug.unityLogger.Log(LogType.Log, TAG, "Trying to setup UniVoice"); 66 | 67 | bool failed = false; 68 | 69 | // Set setup the AudioServer and ClientSession on ALL builds. This means that you'd 70 | // have a ClientSession on a dedicated server, even though there's not much you can do with it. 71 | // Similarly, a client would also have an AudioServer object. But it would just be inactive. 72 | // This sample is for ease of use and to get something working quickly, so we don't bother 73 | // with these minor details. Note that doing so does not have any performance implications 74 | // so you can do this, so you could keep this approach without any tradeoffs. 75 | var createdAudioServer = SetupAudioServer(); 76 | if (!createdAudioServer) { 77 | Debug.unityLogger.Log(LogType.Error, TAG, "Could not setup UniVoice server."); 78 | failed = true; 79 | } 80 | 81 | var setupAudioClient = SetupClientSession(); 82 | if (!setupAudioClient) { 83 | Debug.unityLogger.Log(LogType.Error, TAG, "Could not setup UniVoice client."); 84 | failed = true; 85 | } 86 | 87 | if (!failed) 88 | Debug.unityLogger.Log(LogType.Log, TAG, "UniVoice successfully setup!"); 89 | else 90 | Debug.unityLogger.Log(LogType.Error, TAG, $"Refer to the notes on top of {typeof(UniVoiceMirrorSetupSample).Name}.cs for setup instructions."); 91 | return !failed; 92 | } 93 | 94 | bool SetupAudioServer() { 95 | #if MIRROR 96 | // ---- CREATE AUDIO SERVER AND SUBSCRIBE TO EVENTS TO PRINT LOGS ---- 97 | // We create a server. If this code runs in server mode, MirrorServer will take care 98 | // or automatically handling all incoming messages. On a device connecting as a client, 99 | // this code doesn't do anything. 100 | AudioServer = new MirrorServer(); 101 | Debug.unityLogger.Log(LogType.Log, TAG, "Created MirrorServer object"); 102 | 103 | AudioServer.OnServerStart += () => { 104 | Debug.unityLogger.Log(LogType.Log, TAG, "Server started"); 105 | }; 106 | 107 | AudioServer.OnServerStop += () => { 108 | Debug.unityLogger.Log(LogType.Log, TAG, "Server stopped"); 109 | }; 110 | return true; 111 | #else 112 | Debug.unityLogger.Log(LogType.Error, TAG, "MirrorServer implementation not found!"); 113 | return false; 114 | #endif 115 | } 116 | 117 | bool SetupClientSession() { 118 | #if MIRROR 119 | // ---- CREATE AUDIO CLIENT AND SUBSCRIBE TO EVENTS ---- 120 | IAudioClient client = new MirrorClient(); 121 | client.OnJoined += (id, peerIds) => { 122 | Debug.unityLogger.Log(LogType.Log, TAG, $"You are Peer ID {id}"); 123 | }; 124 | 125 | client.OnLeft += () => { 126 | Debug.unityLogger.Log(LogType.Log, TAG, "You left the chatroom"); 127 | }; 128 | 129 | // When a peer joins, we instantiate a new peer view 130 | client.OnPeerJoined += id => { 131 | Debug.unityLogger.Log(LogType.Log, TAG, $"Peer {id} joined"); 132 | }; 133 | 134 | // When a peer leaves, destroy the UI representing them 135 | client.OnPeerLeft += id => { 136 | Debug.unityLogger.Log(LogType.Log, TAG, $"Peer {id} left"); 137 | }; 138 | 139 | Debug.unityLogger.Log(LogType.Log, TAG, "Created MirrorClient object"); 140 | 141 | // ---- CREATE AUDIO INPUT ---- 142 | IAudioInput input; 143 | // Since in this sample we use microphone input via UniMic, we first check if there 144 | // are any mic devices available. 145 | Mic.Init(); // Must do this to use the Mic class 146 | if (Mic.AvailableDevices.Count == 0) { 147 | Debug.unityLogger.Log(LogType.Log, TAG, "Device has no microphones." + 148 | "Will only be able to hear other clients, cannot send any audio."); 149 | input = new EmptyAudioInput(); 150 | Debug.unityLogger.Log(LogType.Log, TAG, "Created EmptyAudioInput"); 151 | } 152 | else { 153 | // Get the first recording device that we have available and start it. 154 | // Then we create a UniMicInput instance that requires the mic object 155 | // For more info on UniMic refer to https://www.github.com/adrenak/unimic 156 | var mic = Mic.AvailableDevices[0]; 157 | mic.StartRecording(60); 158 | Debug.unityLogger.Log(LogType.Log, TAG, "Started recording with Mic device named." + 159 | mic.Name + $" at frequency {mic.SamplingFrequency} with frame duration {mic.FrameDurationMS} ms."); 160 | input = new UniMicInput(mic); 161 | Debug.unityLogger.Log(LogType.Log, TAG, "Created UniMicInput"); 162 | } 163 | 164 | // ---- CREATE AUDIO OUTPUT FACTORY ---- 165 | IAudioOutputFactory outputFactory; 166 | // We want the incoming audio from peers to be played via the StreamedAudioSourceOutput 167 | // implementation of IAudioSource interface. So we get the factory for it. 168 | outputFactory = new StreamedAudioSourceOutput.Factory(); 169 | Debug.unityLogger.Log(LogType.Log, TAG, "Using StreamedAudioSourceOutput.Factory as output factory"); 170 | 171 | // ---- CREATE CLIENT SESSION AND ADD FILTERS TO IT ---- 172 | // With the client, input and output factory ready, we create create the client session 173 | ClientSession = new ClientSession(client, input, outputFactory); 174 | Debug.unityLogger.Log(LogType.Log, TAG, "Created session"); 175 | 176 | #if UNIVOICE_FILTER_RNNOISE4UNITY 177 | if(useRNNoise4UnityIfAvailable) { 178 | // RNNoiseFilter to remove noise from captured audio 179 | session.InputFilters.Add(new RNNoiseFilter()); 180 | Debug.unityLogger.Log(LogType.Log, TAG, "Registered RNNoiseFilter as an input filter"); 181 | } 182 | #endif 183 | 184 | if (useVad) { 185 | // We add the VAD filter after RNNoise. 186 | // This way lot of the background noise has been removed, VAD is truly trying to detect voice 187 | ClientSession.InputFilters.Add(new SimpleVadFilter(new SimpleVad())); 188 | } 189 | 190 | if (useConcentusEncodeAndDecode) { 191 | // ConcentureEncoder filter to encode captured audio that reduces the audio frame size 192 | ClientSession.InputFilters.Add(new ConcentusEncodeFilter()); 193 | Debug.unityLogger.Log(LogType.Log, TAG, "Registered ConcentusEncodeFilter as an input filter"); 194 | 195 | // For incoming audio register the ConcentusDecodeFilter to decode the encoded audio received from other clients 196 | ClientSession.AddOutputFilter(() => new ConcentusDecodeFilter()); 197 | Debug.unityLogger.Log(LogType.Log, TAG, "Registered ConcentusDecodeFilter as an output filter"); 198 | } 199 | 200 | return true; 201 | #else 202 | Debug.unityLogger.Log(LogType.Error, TAG, "MirrorClient implementation not found!"); 203 | return false; 204 | #endif 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Basic Setup Scripts/UniVoiceMirrorSetupSample.cs.meta: -------------------------------------------------------------------------------- 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: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Basic Setup Scripts/UniVoiceMirrorSetupSample.unity.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e2397deac1a1ddc489d2e846a22708fa 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 45c86a5896484134fa5fa36ff14a0d34 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Prefabs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 919b175f03c1b974f91e263e12b82e38 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Prefabs/Mic Toggle.prefab: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!1 &8149259125552772643 4 | GameObject: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | serializedVersion: 6 10 | m_Component: 11 | - component: {fileID: 8149259125552772640} 12 | - component: {fileID: 8149259125552772641} 13 | m_Layer: 5 14 | m_Name: Mic Toggle 15 | m_TagString: Untagged 16 | m_Icon: {fileID: 0} 17 | m_NavMeshLayer: 0 18 | m_StaticEditorFlags: 0 19 | m_IsActive: 1 20 | --- !u!224 &8149259125552772640 21 | RectTransform: 22 | m_ObjectHideFlags: 0 23 | m_CorrespondingSourceObject: {fileID: 0} 24 | m_PrefabInstance: {fileID: 0} 25 | m_PrefabAsset: {fileID: 0} 26 | m_GameObject: {fileID: 8149259125552772643} 27 | m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} 28 | m_LocalPosition: {x: 0, y: 0, z: 0} 29 | m_LocalScale: {x: 1, y: 1, z: 1} 30 | m_Children: 31 | - {fileID: 8149259127049677661} 32 | m_Father: {fileID: 0} 33 | m_RootOrder: 0 34 | m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} 35 | m_AnchorMin: {x: 1, y: 1} 36 | m_AnchorMax: {x: 1, y: 1} 37 | m_AnchoredPosition: {x: -97.72986, y: -207.73003} 38 | m_SizeDelta: {x: 153.73212, y: 63.91461} 39 | m_Pivot: {x: 0.5, y: 0.5} 40 | --- !u!114 &8149259125552772641 41 | MonoBehaviour: 42 | m_ObjectHideFlags: 0 43 | m_CorrespondingSourceObject: {fileID: 0} 44 | m_PrefabInstance: {fileID: 0} 45 | m_PrefabAsset: {fileID: 0} 46 | m_GameObject: {fileID: 8149259125552772643} 47 | m_Enabled: 1 48 | m_EditorHideFlags: 0 49 | m_Script: {fileID: 11500000, guid: 9085046f02f69544eb97fd06b6048fe2, type: 3} 50 | m_Name: 51 | m_EditorClassIdentifier: 52 | m_Navigation: 53 | m_Mode: 3 54 | m_SelectOnUp: {fileID: 0} 55 | m_SelectOnDown: {fileID: 0} 56 | m_SelectOnLeft: {fileID: 0} 57 | m_SelectOnRight: {fileID: 0} 58 | m_Transition: 1 59 | m_Colors: 60 | m_NormalColor: {r: 1, g: 1, b: 1, a: 1} 61 | m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} 62 | m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} 63 | m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} 64 | m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} 65 | m_ColorMultiplier: 1 66 | m_FadeDuration: 0.1 67 | m_SpriteState: 68 | m_HighlightedSprite: {fileID: 0} 69 | m_PressedSprite: {fileID: 0} 70 | m_SelectedSprite: {fileID: 0} 71 | m_DisabledSprite: {fileID: 0} 72 | m_AnimationTriggers: 73 | m_NormalTrigger: Normal 74 | m_HighlightedTrigger: Highlighted 75 | m_PressedTrigger: Pressed 76 | m_SelectedTrigger: Highlighted 77 | m_DisabledTrigger: Disabled 78 | m_Interactable: 1 79 | m_TargetGraphic: {fileID: 8149259127049677658} 80 | toggleTransition: 1 81 | graphic: {fileID: 8149259127216001340} 82 | m_Group: {fileID: 0} 83 | onValueChanged: 84 | m_PersistentCalls: 85 | m_Calls: [] 86 | m_IsOn: 1 87 | --- !u!1 &8149259127049677660 88 | GameObject: 89 | m_ObjectHideFlags: 0 90 | m_CorrespondingSourceObject: {fileID: 0} 91 | m_PrefabInstance: {fileID: 0} 92 | m_PrefabAsset: {fileID: 0} 93 | serializedVersion: 6 94 | m_Component: 95 | - component: {fileID: 8149259127049677661} 96 | - component: {fileID: 8149259127049677659} 97 | - component: {fileID: 8149259127049677658} 98 | m_Layer: 5 99 | m_Name: Off 100 | m_TagString: Untagged 101 | m_Icon: {fileID: 0} 102 | m_NavMeshLayer: 0 103 | m_StaticEditorFlags: 0 104 | m_IsActive: 1 105 | --- !u!224 &8149259127049677661 106 | RectTransform: 107 | m_ObjectHideFlags: 0 108 | m_CorrespondingSourceObject: {fileID: 0} 109 | m_PrefabInstance: {fileID: 0} 110 | m_PrefabAsset: {fileID: 0} 111 | m_GameObject: {fileID: 8149259127049677660} 112 | m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} 113 | m_LocalPosition: {x: 0, y: 0, z: 0} 114 | m_LocalScale: {x: 1, y: 1, z: 1} 115 | m_Children: 116 | - {fileID: 8149259127216001343} 117 | m_Father: {fileID: 8149259125552772640} 118 | m_RootOrder: 0 119 | m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} 120 | m_AnchorMin: {x: 0, y: 0} 121 | m_AnchorMax: {x: 1, y: 1} 122 | m_AnchoredPosition: {x: 0, y: 0} 123 | m_SizeDelta: {x: -0.0004272461, y: 0} 124 | m_Pivot: {x: 0.5, y: 0.5} 125 | --- !u!222 &8149259127049677659 126 | CanvasRenderer: 127 | m_ObjectHideFlags: 0 128 | m_CorrespondingSourceObject: {fileID: 0} 129 | m_PrefabInstance: {fileID: 0} 130 | m_PrefabAsset: {fileID: 0} 131 | m_GameObject: {fileID: 8149259127049677660} 132 | m_CullTransparentMesh: 0 133 | --- !u!114 &8149259127049677658 134 | MonoBehaviour: 135 | m_ObjectHideFlags: 0 136 | m_CorrespondingSourceObject: {fileID: 0} 137 | m_PrefabInstance: {fileID: 0} 138 | m_PrefabAsset: {fileID: 0} 139 | m_GameObject: {fileID: 8149259127049677660} 140 | m_Enabled: 1 141 | m_EditorHideFlags: 0 142 | m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} 143 | m_Name: 144 | m_EditorClassIdentifier: 145 | m_Material: {fileID: 0} 146 | m_Color: {r: 1, g: 1, b: 1, a: 1} 147 | m_RaycastTarget: 1 148 | m_Maskable: 1 149 | m_OnCullStateChanged: 150 | m_PersistentCalls: 151 | m_Calls: [] 152 | m_Sprite: {fileID: 21300000, guid: fef297624ed5b2043ac6c92214afbcf3, type: 3} 153 | m_Type: 0 154 | m_PreserveAspect: 1 155 | m_FillCenter: 1 156 | m_FillMethod: 4 157 | m_FillAmount: 1 158 | m_FillClockwise: 1 159 | m_FillOrigin: 0 160 | m_UseSpriteMesh: 0 161 | m_PixelsPerUnitMultiplier: 1 162 | --- !u!1 &8149259127216001342 163 | GameObject: 164 | m_ObjectHideFlags: 0 165 | m_CorrespondingSourceObject: {fileID: 0} 166 | m_PrefabInstance: {fileID: 0} 167 | m_PrefabAsset: {fileID: 0} 168 | serializedVersion: 6 169 | m_Component: 170 | - component: {fileID: 8149259127216001343} 171 | - component: {fileID: 8149259127216001341} 172 | - component: {fileID: 8149259127216001340} 173 | m_Layer: 5 174 | m_Name: On 175 | m_TagString: Untagged 176 | m_Icon: {fileID: 0} 177 | m_NavMeshLayer: 0 178 | m_StaticEditorFlags: 0 179 | m_IsActive: 1 180 | --- !u!224 &8149259127216001343 181 | RectTransform: 182 | m_ObjectHideFlags: 0 183 | m_CorrespondingSourceObject: {fileID: 0} 184 | m_PrefabInstance: {fileID: 0} 185 | m_PrefabAsset: {fileID: 0} 186 | m_GameObject: {fileID: 8149259127216001342} 187 | m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} 188 | m_LocalPosition: {x: 0, y: 0, z: 0} 189 | m_LocalScale: {x: 1, y: 1, z: 1} 190 | m_Children: [] 191 | m_Father: {fileID: 8149259127049677661} 192 | m_RootOrder: 0 193 | m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} 194 | m_AnchorMin: {x: 0, y: 0} 195 | m_AnchorMax: {x: 1, y: 1} 196 | m_AnchoredPosition: {x: -0.00011444092, y: 0} 197 | m_SizeDelta: {x: -0.0002746582, y: 0} 198 | m_Pivot: {x: 0.5, y: 0.5} 199 | --- !u!222 &8149259127216001341 200 | CanvasRenderer: 201 | m_ObjectHideFlags: 0 202 | m_CorrespondingSourceObject: {fileID: 0} 203 | m_PrefabInstance: {fileID: 0} 204 | m_PrefabAsset: {fileID: 0} 205 | m_GameObject: {fileID: 8149259127216001342} 206 | m_CullTransparentMesh: 0 207 | --- !u!114 &8149259127216001340 208 | MonoBehaviour: 209 | m_ObjectHideFlags: 0 210 | m_CorrespondingSourceObject: {fileID: 0} 211 | m_PrefabInstance: {fileID: 0} 212 | m_PrefabAsset: {fileID: 0} 213 | m_GameObject: {fileID: 8149259127216001342} 214 | m_Enabled: 1 215 | m_EditorHideFlags: 0 216 | m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} 217 | m_Name: 218 | m_EditorClassIdentifier: 219 | m_Material: {fileID: 0} 220 | m_Color: {r: 1, g: 1, b: 1, a: 1} 221 | m_RaycastTarget: 1 222 | m_Maskable: 1 223 | m_OnCullStateChanged: 224 | m_PersistentCalls: 225 | m_Calls: [] 226 | m_Sprite: {fileID: 21300000, guid: 091b808e85d9e3843ab27e808a68ad44, type: 3} 227 | m_Type: 0 228 | m_PreserveAspect: 1 229 | m_FillCenter: 1 230 | m_FillMethod: 4 231 | m_FillAmount: 1 232 | m_FillClockwise: 1 233 | m_FillOrigin: 0 234 | m_UseSpriteMesh: 0 235 | m_PixelsPerUnitMultiplier: 1 236 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Prefabs/Mic Toggle.prefab.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4d8ec9d3bffb8a34498598c2c5a4eb64 3 | PrefabImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Prefabs/Peer View.prefab.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4df848340ed9e8d4aa53174f49205cc6 3 | PrefabImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Scenes.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 08c719f363433af489cc02afdc250c3c 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Scenes/GroupVoiceCallSample-Mirror.unity.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 112506fba9c2d954d9eec18ab175ec08 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 37aed8b1bd69e794fad8f9688e8c68bb 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Scripts/GroupVoiceCallMirrorSample.cs: -------------------------------------------------------------------------------- 1 | #if UNIVOICE_MIRROR_NETWORK || UNIVOICE_NETWORK_MIRROR 2 | using System.Linq; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | 6 | using UnityEngine; 7 | using UnityEngine.UI; 8 | using UnityEngine.Android; 9 | 10 | using Adrenak.UniMic; 11 | using Adrenak.UniVoice.Networks; 12 | using Adrenak.UniVoice.Outputs; 13 | using Adrenak.UniVoice.Inputs; 14 | using Adrenak.UniVoice.Filters; 15 | 16 | namespace Adrenak.UniVoice.Samples { 17 | public class GroupVoiceCallMirrorSample : MonoBehaviour { 18 | public Transform peerViewContainer; 19 | public PeerView peerViewTemplate; 20 | public Text chatroomMessage; 21 | public Toggle muteSelfToggle; 22 | public Toggle muteOthersToggle; 23 | 24 | ClientSession session; 25 | Dictionary peerViews = new Dictionary(); 26 | 27 | IEnumerator Start() { 28 | Mic.Init(); 29 | Screen.sleepTimeout = SleepTimeout.NeverSleep; 30 | 31 | #if UNITY_ANDROID 32 | while(!Permission.HasUserAuthorizedPermission("android.permission.RECORD_AUDIO")) { 33 | Permission.RequestUserPermission("android.permission.RECORD_AUDIO"); 34 | yield return new WaitForSeconds(1); 35 | } 36 | #endif 37 | yield return null; 38 | 39 | InitializeSession(); 40 | 41 | // Listen to changes in the toggles to deafen or mute all peers 42 | muteSelfToggle.SetIsOnWithoutNotify(session.Client.YourVoiceSettings.deafenAll); 43 | muteSelfToggle.onValueChanged.AddListener(value => { 44 | session.Client.YourVoiceSettings.deafenAll = value; 45 | session.Client.SubmitVoiceSettings(); 46 | }); 47 | 48 | muteOthersToggle.SetIsOnWithoutNotify(session.Client.YourVoiceSettings.muteAll); 49 | muteOthersToggle.onValueChanged.AddListener(value => { 50 | session.Client.YourVoiceSettings.muteAll = value; 51 | session.Client.SubmitVoiceSettings(); 52 | }); 53 | } 54 | 55 | void InitializeSession() { 56 | // We create a server. If this code runs in server mode, MirrorServer will take care 57 | // or automatically handling all incoming messages. 58 | var server = new MirrorServer(); 59 | 60 | // Since in this sample we use microphone input via UniMic, we first check if there 61 | // are any mic devices available. 62 | if (Mic.AvailableDevices.Count == 0) 63 | return; 64 | 65 | // Create a client for this device 66 | var client = new MirrorClient(); 67 | 68 | // Get the first recording device that we have available and start it. 69 | // Then we create a UniMicInput instance that requires the mic object 70 | // For more info on UniMic refer to https://www.github.com/adrenak/unimic 71 | var mic = Mic.AvailableDevices[0]; 72 | mic.StartRecording(); 73 | var input = new UniMicInput(mic); 74 | 75 | // We want the incoming audio from peers to be played via the StreamedAudioSourceOutput 76 | // implementation of IAudioSource interface. So we get the factory for it. 77 | var outputFactory = new StreamedAudioSourceOutput.Factory(); 78 | 79 | // With the client, input and output factory ready, we create create the client session 80 | session = new ClientSession(client, input, outputFactory); 81 | 82 | // We add some filters to the input audio 83 | // - The first is audio blur, so that the audio that's been captured by this client 84 | // has lesser noise 85 | session.InputFilters.Add(new GaussianAudioBlur()); 86 | // - The next one is the Opus encoder filter. This is VERY important. Without this the 87 | // outgoing data would be very large, usually by a factor of 10 or more. 88 | session.InputFilters.Add(new ConcentusEncodeFilter()); 89 | 90 | // Next, for incoming audio we register the Concentus decode filter as the audio we'd 91 | // receive from other clients would be encoded and not readily playable 92 | session.AddOutputFilter(() => new ConcentusDecodeFilter()); 93 | 94 | // Subscribe to some server events 95 | server.OnServerStart += () => { 96 | ShowMessage("Server started"); 97 | }; 98 | 99 | server.OnServerStop += () => { 100 | ShowMessage("Server stopped"); 101 | }; 102 | 103 | // We subscribe to some client events to show updates on the UI when you join or leave 104 | client.OnJoined += (id, peerIds) => { 105 | ShowMessage($"You are Peer ID {id} your peers are {string.Join(", ", peerIds)}"); 106 | }; 107 | 108 | client.OnLeft += () => { 109 | ShowMessage("You left the chatroom"); 110 | foreach (var view in peerViews) 111 | Destroy(view.Value.gameObject); 112 | peerViews.Clear(); 113 | }; 114 | 115 | // When a peer joins, we instantiate a new peer view 116 | client.OnPeerJoined += id => { 117 | var view = Instantiate(peerViewTemplate, peerViewContainer); 118 | view.SetPeerID(id); 119 | 120 | // we listen to the changes in the individual toggle buttons 121 | // on this view to selectively mute or deafen the peer that 122 | // is represented by the view 123 | view.OnAllowIncomingAudioChange += x => { 124 | client.YourVoiceSettings.SetMute(id, !x); 125 | client.SubmitVoiceSettings(); 126 | }; 127 | view.OnAllowOutgoingAudioChange += x => { 128 | client.YourVoiceSettings.SetDeaf(id, !x); 129 | client.SubmitVoiceSettings(); 130 | }; 131 | peerViews.Add(id, view); 132 | }; 133 | 134 | // When a peer leaves, destroy the UI representing them 135 | client.OnPeerLeft += id => { 136 | if (peerViews.ContainsKey(id)) { 137 | var peerViewInstance = peerViews[id]; 138 | Destroy(peerViewInstance.gameObject); 139 | peerViews.Remove(id); 140 | } 141 | }; 142 | } 143 | 144 | // Here we just show some audio visualization of incoming peer audio. 145 | void Update() { 146 | if (session == null) return; 147 | 148 | foreach (var output in session.PeerOutputs) { 149 | if (peerViews.ContainsKey(output.Key)) { 150 | /* 151 | * This is an inefficient way of showing a part of the 152 | * audio source spectrum. AudioSource.GetSpectrumData returns 153 | * frequency values up to 24000 Hz in some cases. Most human 154 | * speech is no more than 5000 Hz. Showing the entire spectrum 155 | * will therefore lead to a spectrum where much of it doesn't 156 | * change. So we take only the spectrum frequencies between 157 | * the average human vocal range. 158 | * 159 | * Great source of information here: 160 | * http://answers.unity.com/answers/158800/view.html 161 | */ 162 | var size = 512; 163 | var minVocalFrequency = 50; 164 | var maxVocalFrequency = 8000; 165 | var sampleRate = AudioSettings.outputSampleRate; 166 | var frequencyResolution = sampleRate / 2 / size; 167 | 168 | var audioSource = (output.Value as StreamedAudioSourceOutput).Stream.UnityAudioSource; 169 | var spectrumData = new float[size]; 170 | audioSource.GetSpectrumData(spectrumData, 0, FFTWindow.BlackmanHarris); 171 | 172 | var indices = Enumerable.Range(0, size - 1).ToList(); 173 | var minVocalFrequencyIndex = indices.Min(x => (Mathf.Abs(x * frequencyResolution - minVocalFrequency), x)).x; 174 | var maxVocalFrequencyIndex = indices.Min(x => (Mathf.Abs(x * frequencyResolution - maxVocalFrequency), x)).x; 175 | var indexRange = maxVocalFrequencyIndex - minVocalFrequency; 176 | 177 | // Using LINQ here to keep it short. But this generates a lot of garbage. 178 | // If you're visualizing incoming audio data in your app/game, consider using some 179 | // caching and memory allocation saving techniques 180 | spectrumData = spectrumData.Select(x => 1000 * x) 181 | .ToList() 182 | .GetRange(minVocalFrequency, indexRange) 183 | .ToArray(); 184 | peerViews[output.Key].DisplaySpectrum(spectrumData); 185 | } 186 | } 187 | } 188 | 189 | void ShowMessage(object obj) { 190 | Debug.Log("GroupVoiceCall_MirrorSample:" + obj); 191 | chatroomMessage.text = obj.ToString(); 192 | } 193 | } 194 | } 195 | #endif -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Scripts/GroupVoiceCallMirrorSample.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f1fbe701d5771df469acad7064df7cc7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Scripts/PeerView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using UnityEngine; 5 | using UnityEngine.UI; 6 | 7 | namespace Adrenak.UniVoice.Samples { 8 | public static class ToggleExtension{ 9 | public static void SetIsOnWithoutNotify(this Toggle instance, bool value) { 10 | var originalEvent = instance.onValueChanged; 11 | instance.onValueChanged = new Toggle.ToggleEvent(); 12 | instance.isOn = value; 13 | instance.onValueChanged = originalEvent; 14 | } 15 | } 16 | 17 | public class PeerView : MonoBehaviour { 18 | public event Action OnAllowIncomingAudioChange; 19 | public event Action OnAllowOutgoingAudioChange; 20 | 21 | [SerializeField] Text idText; 22 | [SerializeField] Transform barContainer; 23 | [SerializeField] Transform barTemplate; 24 | [SerializeField] Toggle speakerToggle; 25 | [SerializeField] Toggle micToggle; 26 | 27 | public bool AllowIncomingAudio { 28 | get => speakerToggle.isOn; 29 | set => speakerToggle.SetIsOnWithoutNotify(value); 30 | } 31 | 32 | public bool AllowOutgoingAudio { 33 | get => micToggle.isOn; 34 | set => micToggle.SetIsOnWithoutNotify(value); 35 | } 36 | 37 | List bars = new List(); 38 | 39 | void Start() { 40 | speakerToggle.onValueChanged.AddListener(value => 41 | OnAllowIncomingAudioChange?.Invoke(value)); 42 | 43 | micToggle.onValueChanged.AddListener(value => 44 | OnAllowOutgoingAudioChange?.Invoke(value)); 45 | } 46 | 47 | public void SetPeerID(int id) { 48 | idText.text = id.ToString(); 49 | } 50 | 51 | public void DisplaySpectrum(float[] spectrum) { 52 | InitBars(spectrum.Length); 53 | 54 | if (spectrum.Length != bars.Count) return; 55 | 56 | for (int i = 0; i < bars.Count; i++) 57 | bars[i].localScale = new Vector3(1, Mathf.Clamp01(spectrum[i]), 1); 58 | } 59 | 60 | void InitBars(int count) { 61 | if (bars.Count == count) return; 62 | 63 | foreach (var bar in bars) 64 | Destroy(bar.gameObject); 65 | bars.Clear(); 66 | 67 | for (int i = 0; i < count; i++) { 68 | var instance = Instantiate(barTemplate, barContainer); 69 | instance.gameObject.SetActive(true); 70 | bars.Add(instance); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Scripts/PeerView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d7cbcf3d831071140b35bc0fd8205a16 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Sprites.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 918a405e0939fa54daf8eee8faaac909 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Sprites/mic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrenak/univoice/b4539c94336f8cda31b03b77b88d000f5b1bbf0a/Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Sprites/mic.png -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Sprites/mic.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 48991d65e071871499902c8cde27d649 3 | TextureImporter: 4 | fileIDToRecycleName: {} 5 | externalObjects: {} 6 | serializedVersion: 9 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: -1 36 | mipBias: -100 37 | wrapU: 1 38 | wrapV: 1 39 | wrapW: -1 40 | nPOTScale: 0 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 1 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 1 53 | spriteTessellationDetail: -1 54 | textureType: 8 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | platformSettings: 61 | - serializedVersion: 2 62 | buildTarget: DefaultTexturePlatform 63 | maxTextureSize: 256 64 | resizeAlgorithm: 0 65 | textureFormat: -1 66 | textureCompression: 1 67 | compressionQuality: 50 68 | crunchedCompression: 0 69 | allowsAlphaSplitting: 0 70 | overridden: 0 71 | androidETC2FallbackOverride: 0 72 | spriteSheet: 73 | serializedVersion: 2 74 | sprites: [] 75 | outline: [] 76 | physicsShape: [] 77 | bones: [] 78 | spriteID: 9ff3a2e24a6348a4498d20d7fc001556 79 | vertices: [] 80 | indices: 81 | edges: [] 82 | weights: [] 83 | spritePackingTag: 84 | pSDRemoveMatte: 0 85 | pSDShowRemoveMatteOption: 0 86 | userData: 87 | assetBundleName: 88 | assetBundleVariant: 89 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Sprites/off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrenak/univoice/b4539c94336f8cda31b03b77b88d000f5b1bbf0a/Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Sprites/off.png -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Sprites/off.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fef297624ed5b2043ac6c92214afbcf3 3 | TextureImporter: 4 | fileIDToRecycleName: {} 5 | externalObjects: {} 6 | serializedVersion: 9 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: -1 36 | mipBias: -100 37 | wrapU: 1 38 | wrapV: 1 39 | wrapW: -1 40 | nPOTScale: 0 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 1 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 1 53 | spriteTessellationDetail: -1 54 | textureType: 8 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | platformSettings: 61 | - serializedVersion: 2 62 | buildTarget: DefaultTexturePlatform 63 | maxTextureSize: 256 64 | resizeAlgorithm: 0 65 | textureFormat: -1 66 | textureCompression: 1 67 | compressionQuality: 50 68 | crunchedCompression: 0 69 | allowsAlphaSplitting: 0 70 | overridden: 0 71 | androidETC2FallbackOverride: 0 72 | spriteSheet: 73 | serializedVersion: 2 74 | sprites: [] 75 | outline: [] 76 | physicsShape: [] 77 | bones: [] 78 | spriteID: 9c153eda0403ca546828095fc0904b7a 79 | vertices: [] 80 | indices: 81 | edges: [] 82 | weights: [] 83 | spritePackingTag: 84 | pSDRemoveMatte: 0 85 | pSDShowRemoveMatteOption: 0 86 | userData: 87 | assetBundleName: 88 | assetBundleVariant: 89 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Sprites/on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrenak/univoice/b4539c94336f8cda31b03b77b88d000f5b1bbf0a/Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Sprites/on.png -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Sprites/on.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 091b808e85d9e3843ab27e808a68ad44 3 | TextureImporter: 4 | fileIDToRecycleName: {} 5 | externalObjects: {} 6 | serializedVersion: 9 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: -1 36 | mipBias: -100 37 | wrapU: 1 38 | wrapV: 1 39 | wrapW: -1 40 | nPOTScale: 0 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 1 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 1 53 | spriteTessellationDetail: -1 54 | textureType: 8 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | platformSettings: 61 | - serializedVersion: 2 62 | buildTarget: DefaultTexturePlatform 63 | maxTextureSize: 256 64 | resizeAlgorithm: 0 65 | textureFormat: -1 66 | textureCompression: 1 67 | compressionQuality: 50 68 | crunchedCompression: 0 69 | allowsAlphaSplitting: 0 70 | overridden: 0 71 | androidETC2FallbackOverride: 0 72 | spriteSheet: 73 | serializedVersion: 2 74 | sprites: [] 75 | outline: [] 76 | physicsShape: [] 77 | bones: [] 78 | spriteID: 1d85175d92546e14aa7525901d0b3fea 79 | vertices: [] 80 | indices: 81 | edges: [] 82 | weights: [] 83 | spritePackingTag: 84 | pSDRemoveMatte: 0 85 | pSDShowRemoveMatteOption: 0 86 | userData: 87 | assetBundleName: 88 | assetBundleVariant: 89 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Sprites/speaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrenak/univoice/b4539c94336f8cda31b03b77b88d000f5b1bbf0a/Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Sprites/speaker.png -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Sprites/speaker.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f614e31fdd0562a4ebb9b6c2126ebf8a 3 | TextureImporter: 4 | fileIDToRecycleName: {} 5 | externalObjects: {} 6 | serializedVersion: 9 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: -1 36 | mipBias: -100 37 | wrapU: 1 38 | wrapV: 1 39 | wrapW: -1 40 | nPOTScale: 0 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 1 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 1 53 | spriteTessellationDetail: -1 54 | textureType: 8 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | platformSettings: 61 | - serializedVersion: 2 62 | buildTarget: DefaultTexturePlatform 63 | maxTextureSize: 256 64 | resizeAlgorithm: 0 65 | textureFormat: -1 66 | textureCompression: 1 67 | compressionQuality: 50 68 | crunchedCompression: 0 69 | allowsAlphaSplitting: 0 70 | overridden: 0 71 | androidETC2FallbackOverride: 0 72 | spriteSheet: 73 | serializedVersion: 2 74 | sprites: [] 75 | outline: [] 76 | physicsShape: [] 77 | bones: [] 78 | spriteID: 3622c0ab7514b294e91e1e29cbfae826 79 | vertices: [] 80 | indices: 81 | edges: [] 82 | weights: [] 83 | spritePackingTag: 84 | pSDRemoveMatte: 0 85 | pSDShowRemoveMatteOption: 0 86 | userData: 87 | assetBundleName: 88 | assetBundleVariant: 89 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.adrenak.univoice", 3 | "version": "4.10.1", 4 | "displayName": "Adrenak.UniVoice", 5 | "description": "Voice chat/VoIP framework for Unity.", 6 | "unity": "2021.2", 7 | "author": { 8 | "name": "Vatsal Ambastha", 9 | "email": "ambastha.vatsal@gmail.com", 10 | "url": "http://www.vatsalambastha.com" 11 | }, 12 | "license": "MIT", 13 | "keywords": [ 14 | "Mirror", 15 | "Networking", 16 | "Voice Chat", 17 | "voip", 18 | "Unity", 19 | "Unity3d", 20 | "VR", 21 | "AR", 22 | "Metaverse", 23 | "mic", 24 | "opus", 25 | "concentus" 26 | ], 27 | "samples": [ 28 | { 29 | "displayName": "Samples", 30 | "description": "UniVoice Samples to get you started", 31 | "path": "Samples~" 32 | } 33 | ], 34 | "dependencies": { 35 | "com.adrenak.brw": "1.0.1", 36 | "com.adrenak.unimic": "3.4.0", 37 | "com.adrenak.concentus-unity": "1.0.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Assets/Adrenak.UniVoice/package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5677af848094d404f835a6468b36f003 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Documentation/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "src": "..", 7 | "files": [ 8 | "Assets/**/*.cs" 9 | ], 10 | "exclude": [ 11 | "Assets/Plugins/**" 12 | ] 13 | } 14 | ], 15 | "globalNamespaceId": "Global", 16 | "filter": "filterConfig.yml", 17 | "dest": "api" 18 | } 19 | ], 20 | "build": { 21 | "globalMetadata": { 22 | "_appTitle": "UniVoice documentation", 23 | "_appFooter": "UniVoice documentation", 24 | "_enableSearch": true 25 | }, 26 | "content": [ 27 | { 28 | "files": [ 29 | "toc.yml", 30 | "index.md" 31 | ] 32 | }, 33 | { 34 | "src": "api", 35 | "files": [ 36 | "*.yml" 37 | ], 38 | "dest": "api" 39 | }, 40 | { 41 | "src": "manual", 42 | "files": [ 43 | "toc.yml", 44 | "*.md" 45 | ], 46 | "dest": "manual" 47 | } 48 | ], 49 | "overwrite": [ 50 | { 51 | "src": "..", 52 | "files": [ 53 | "Assets/Scripts/**/*.md" 54 | ] 55 | } 56 | ], 57 | "resource": [ 58 | { 59 | "files": [ 60 | "resources/**/*" 61 | ] 62 | } 63 | ], 64 | "sitemap": { 65 | "baseUrl": "https://adrenak.github.io/univoice", 66 | "changefreq": "weekly", 67 | "fileOptions": { 68 | "api/*": { 69 | "changefreq": "daily" 70 | } 71 | } 72 | }, 73 | "xref": [ 74 | "https://normanderwan.github.io/UnityXrefMaps/xrefmap.yml" 75 | ], 76 | "xrefService": [ 77 | "https://xref.docs.microsoft.com/query?uid={uid}" 78 | ], 79 | "dest": "../_site" 80 | } 81 | } -------------------------------------------------------------------------------- /Documentation/filterConfig.yml: -------------------------------------------------------------------------------- 1 | apiRules: 2 | - include: 3 | uidRegex: ^Adrenak.UniVoice 4 | type: Namespace 5 | - exclude: 6 | uidRegex: .* 7 | type: Namespace -------------------------------------------------------------------------------- /Documentation/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Manual 2 | href: manual/ 3 | - name: Scripting API 4 | href: api/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Vatsal Ambastha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Logs/Packages-Update.log: -------------------------------------------------------------------------------- 1 | 2 | === Sun May 23 18:33:12 2021 3 | 4 | Packages were changed. 5 | Update Mode: resetToDefaultDependencies 6 | 7 | The following packages were added: 8 | com.unity.analytics@3.2.3 9 | com.unity.purchasing@2.2.1 10 | com.unity.ads@2.0.8 11 | com.unity.textmeshpro@1.4.1 12 | com.unity.package-manager-ui@2.0.13 13 | com.unity.collab-proxy@1.2.15 14 | com.unity.modules.ai@1.0.0 15 | com.unity.modules.animation@1.0.0 16 | com.unity.modules.assetbundle@1.0.0 17 | com.unity.modules.audio@1.0.0 18 | com.unity.modules.cloth@1.0.0 19 | com.unity.modules.director@1.0.0 20 | com.unity.modules.imageconversion@1.0.0 21 | com.unity.modules.imgui@1.0.0 22 | com.unity.modules.jsonserialize@1.0.0 23 | com.unity.modules.particlesystem@1.0.0 24 | com.unity.modules.physics@1.0.0 25 | com.unity.modules.physics2d@1.0.0 26 | com.unity.modules.screencapture@1.0.0 27 | com.unity.modules.terrain@1.0.0 28 | com.unity.modules.terrainphysics@1.0.0 29 | com.unity.modules.tilemap@1.0.0 30 | com.unity.modules.ui@1.0.0 31 | com.unity.modules.uielements@1.0.0 32 | com.unity.modules.umbra@1.0.0 33 | com.unity.modules.unityanalytics@1.0.0 34 | com.unity.modules.unitywebrequest@1.0.0 35 | com.unity.modules.unitywebrequestassetbundle@1.0.0 36 | com.unity.modules.unitywebrequestaudio@1.0.0 37 | com.unity.modules.unitywebrequesttexture@1.0.0 38 | com.unity.modules.unitywebrequestwww@1.0.0 39 | com.unity.modules.vehicles@1.0.0 40 | com.unity.modules.video@1.0.0 41 | com.unity.modules.vr@1.0.0 42 | com.unity.modules.wind@1.0.0 43 | com.unity.modules.xr@1.0.0 44 | 45 | === Fri Aug 19 15:12:08 2022 46 | 47 | Packages were changed. 48 | Update Mode: updateDependencies 49 | 50 | The following packages were added: 51 | com.unity.2d.sprite@1.0.0 52 | com.unity.2d.tilemap@1.0.0 53 | com.unity.ide.rider@1.2.1 54 | com.unity.ide.visualstudio@2.0.14 55 | com.unity.ide.vscode@1.2.4 56 | com.unity.modules.androidjni@1.0.0 57 | com.unity.multiplayer-hlapi@1.0.8 58 | com.unity.test-framework@1.1.30 59 | com.unity.timeline@1.2.18 60 | com.unity.ugui@1.0.0 61 | com.unity.xr.legacyinputhelpers@2.1.8 62 | The following packages were removed: 63 | com.unity.package-manager-ui@2.2.0 64 | 65 | === Sun Dec 29 20:04:06 2024 66 | 67 | Packages were changed. 68 | Update Mode: mergeDefaultDependencies 69 | 70 | The following packages were updated: 71 | com.unity.nuget.newtonsoft-json from version 3.0.2 to 3.2.1 72 | -------------------------------------------------------------------------------- /Packages/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.adrenak.brw": "1.0.1", 4 | "com.adrenak.concentus-unity": "1.0.1", 5 | "com.adrenak.unimic": "3.4.0", 6 | "com.unity.ide.visualstudio": "2.0.22", 7 | "com.unity.nuget.newtonsoft-json": "3.2.1", 8 | "com.unity.ugui": "1.0.0", 9 | "com.unity.modules.ai": "1.0.0", 10 | "com.unity.modules.androidjni": "1.0.0", 11 | "com.unity.modules.animation": "1.0.0", 12 | "com.unity.modules.assetbundle": "1.0.0", 13 | "com.unity.modules.audio": "1.0.0", 14 | "com.unity.modules.cloth": "1.0.0", 15 | "com.unity.modules.director": "1.0.0", 16 | "com.unity.modules.imageconversion": "1.0.0", 17 | "com.unity.modules.imgui": "1.0.0", 18 | "com.unity.modules.jsonserialize": "1.0.0", 19 | "com.unity.modules.particlesystem": "1.0.0", 20 | "com.unity.modules.physics": "1.0.0", 21 | "com.unity.modules.physics2d": "1.0.0", 22 | "com.unity.modules.screencapture": "1.0.0", 23 | "com.unity.modules.terrain": "1.0.0", 24 | "com.unity.modules.terrainphysics": "1.0.0", 25 | "com.unity.modules.tilemap": "1.0.0", 26 | "com.unity.modules.ui": "1.0.0", 27 | "com.unity.modules.uielements": "1.0.0", 28 | "com.unity.modules.umbra": "1.0.0", 29 | "com.unity.modules.unityanalytics": "1.0.0", 30 | "com.unity.modules.unitywebrequest": "1.0.0", 31 | "com.unity.modules.unitywebrequestassetbundle": "1.0.0", 32 | "com.unity.modules.unitywebrequestaudio": "1.0.0", 33 | "com.unity.modules.unitywebrequesttexture": "1.0.0", 34 | "com.unity.modules.unitywebrequestwww": "1.0.0", 35 | "com.unity.modules.vehicles": "1.0.0", 36 | "com.unity.modules.video": "1.0.0", 37 | "com.unity.modules.vr": "1.0.0", 38 | "com.unity.modules.wind": "1.0.0", 39 | "com.unity.modules.xr": "1.0.0" 40 | }, 41 | "scopedRegistries": [ 42 | { 43 | "name": "NPM", 44 | "url": "https://registry.npmjs.org", 45 | "scopes": [ 46 | "com.adrenak.brw", 47 | "com.adrenak.unimic", 48 | "com.adrenak.rnnoise4unity", 49 | "com.adrenak.concentus-unity" 50 | ] 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /Packages/packages-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.adrenak.brw": { 4 | "version": "1.0.1", 5 | "depth": 0, 6 | "source": "registry", 7 | "dependencies": {}, 8 | "url": "https://registry.npmjs.org" 9 | }, 10 | "com.adrenak.concentus-unity": { 11 | "version": "1.0.1", 12 | "depth": 0, 13 | "source": "registry", 14 | "dependencies": {}, 15 | "url": "https://registry.npmjs.org" 16 | }, 17 | "com.adrenak.unimic": { 18 | "version": "3.4.0", 19 | "depth": 0, 20 | "source": "registry", 21 | "dependencies": {}, 22 | "url": "https://registry.npmjs.org" 23 | }, 24 | "com.unity.ext.nunit": { 25 | "version": "1.0.6", 26 | "depth": 2, 27 | "source": "registry", 28 | "dependencies": {}, 29 | "url": "https://packages.unity.com" 30 | }, 31 | "com.unity.ide.visualstudio": { 32 | "version": "2.0.22", 33 | "depth": 0, 34 | "source": "registry", 35 | "dependencies": { 36 | "com.unity.test-framework": "1.1.9" 37 | }, 38 | "url": "https://packages.unity.com" 39 | }, 40 | "com.unity.nuget.newtonsoft-json": { 41 | "version": "3.2.1", 42 | "depth": 0, 43 | "source": "registry", 44 | "dependencies": {}, 45 | "url": "https://packages.unity.com" 46 | }, 47 | "com.unity.test-framework": { 48 | "version": "1.1.33", 49 | "depth": 1, 50 | "source": "registry", 51 | "dependencies": { 52 | "com.unity.ext.nunit": "1.0.6", 53 | "com.unity.modules.imgui": "1.0.0", 54 | "com.unity.modules.jsonserialize": "1.0.0" 55 | }, 56 | "url": "https://packages.unity.com" 57 | }, 58 | "com.unity.ugui": { 59 | "version": "1.0.0", 60 | "depth": 0, 61 | "source": "builtin", 62 | "dependencies": { 63 | "com.unity.modules.ui": "1.0.0", 64 | "com.unity.modules.imgui": "1.0.0" 65 | } 66 | }, 67 | "com.unity.modules.ai": { 68 | "version": "1.0.0", 69 | "depth": 0, 70 | "source": "builtin", 71 | "dependencies": {} 72 | }, 73 | "com.unity.modules.androidjni": { 74 | "version": "1.0.0", 75 | "depth": 0, 76 | "source": "builtin", 77 | "dependencies": {} 78 | }, 79 | "com.unity.modules.animation": { 80 | "version": "1.0.0", 81 | "depth": 0, 82 | "source": "builtin", 83 | "dependencies": {} 84 | }, 85 | "com.unity.modules.assetbundle": { 86 | "version": "1.0.0", 87 | "depth": 0, 88 | "source": "builtin", 89 | "dependencies": {} 90 | }, 91 | "com.unity.modules.audio": { 92 | "version": "1.0.0", 93 | "depth": 0, 94 | "source": "builtin", 95 | "dependencies": {} 96 | }, 97 | "com.unity.modules.cloth": { 98 | "version": "1.0.0", 99 | "depth": 0, 100 | "source": "builtin", 101 | "dependencies": { 102 | "com.unity.modules.physics": "1.0.0" 103 | } 104 | }, 105 | "com.unity.modules.director": { 106 | "version": "1.0.0", 107 | "depth": 0, 108 | "source": "builtin", 109 | "dependencies": { 110 | "com.unity.modules.audio": "1.0.0", 111 | "com.unity.modules.animation": "1.0.0" 112 | } 113 | }, 114 | "com.unity.modules.imageconversion": { 115 | "version": "1.0.0", 116 | "depth": 0, 117 | "source": "builtin", 118 | "dependencies": {} 119 | }, 120 | "com.unity.modules.imgui": { 121 | "version": "1.0.0", 122 | "depth": 0, 123 | "source": "builtin", 124 | "dependencies": {} 125 | }, 126 | "com.unity.modules.jsonserialize": { 127 | "version": "1.0.0", 128 | "depth": 0, 129 | "source": "builtin", 130 | "dependencies": {} 131 | }, 132 | "com.unity.modules.particlesystem": { 133 | "version": "1.0.0", 134 | "depth": 0, 135 | "source": "builtin", 136 | "dependencies": {} 137 | }, 138 | "com.unity.modules.physics": { 139 | "version": "1.0.0", 140 | "depth": 0, 141 | "source": "builtin", 142 | "dependencies": {} 143 | }, 144 | "com.unity.modules.physics2d": { 145 | "version": "1.0.0", 146 | "depth": 0, 147 | "source": "builtin", 148 | "dependencies": {} 149 | }, 150 | "com.unity.modules.screencapture": { 151 | "version": "1.0.0", 152 | "depth": 0, 153 | "source": "builtin", 154 | "dependencies": { 155 | "com.unity.modules.imageconversion": "1.0.0" 156 | } 157 | }, 158 | "com.unity.modules.subsystems": { 159 | "version": "1.0.0", 160 | "depth": 1, 161 | "source": "builtin", 162 | "dependencies": { 163 | "com.unity.modules.jsonserialize": "1.0.0" 164 | } 165 | }, 166 | "com.unity.modules.terrain": { 167 | "version": "1.0.0", 168 | "depth": 0, 169 | "source": "builtin", 170 | "dependencies": {} 171 | }, 172 | "com.unity.modules.terrainphysics": { 173 | "version": "1.0.0", 174 | "depth": 0, 175 | "source": "builtin", 176 | "dependencies": { 177 | "com.unity.modules.physics": "1.0.0", 178 | "com.unity.modules.terrain": "1.0.0" 179 | } 180 | }, 181 | "com.unity.modules.tilemap": { 182 | "version": "1.0.0", 183 | "depth": 0, 184 | "source": "builtin", 185 | "dependencies": { 186 | "com.unity.modules.physics2d": "1.0.0" 187 | } 188 | }, 189 | "com.unity.modules.ui": { 190 | "version": "1.0.0", 191 | "depth": 0, 192 | "source": "builtin", 193 | "dependencies": {} 194 | }, 195 | "com.unity.modules.uielements": { 196 | "version": "1.0.0", 197 | "depth": 0, 198 | "source": "builtin", 199 | "dependencies": { 200 | "com.unity.modules.ui": "1.0.0", 201 | "com.unity.modules.imgui": "1.0.0", 202 | "com.unity.modules.jsonserialize": "1.0.0", 203 | "com.unity.modules.uielementsnative": "1.0.0" 204 | } 205 | }, 206 | "com.unity.modules.uielementsnative": { 207 | "version": "1.0.0", 208 | "depth": 1, 209 | "source": "builtin", 210 | "dependencies": { 211 | "com.unity.modules.ui": "1.0.0", 212 | "com.unity.modules.imgui": "1.0.0", 213 | "com.unity.modules.jsonserialize": "1.0.0" 214 | } 215 | }, 216 | "com.unity.modules.umbra": { 217 | "version": "1.0.0", 218 | "depth": 0, 219 | "source": "builtin", 220 | "dependencies": {} 221 | }, 222 | "com.unity.modules.unityanalytics": { 223 | "version": "1.0.0", 224 | "depth": 0, 225 | "source": "builtin", 226 | "dependencies": { 227 | "com.unity.modules.unitywebrequest": "1.0.0", 228 | "com.unity.modules.jsonserialize": "1.0.0" 229 | } 230 | }, 231 | "com.unity.modules.unitywebrequest": { 232 | "version": "1.0.0", 233 | "depth": 0, 234 | "source": "builtin", 235 | "dependencies": {} 236 | }, 237 | "com.unity.modules.unitywebrequestassetbundle": { 238 | "version": "1.0.0", 239 | "depth": 0, 240 | "source": "builtin", 241 | "dependencies": { 242 | "com.unity.modules.assetbundle": "1.0.0", 243 | "com.unity.modules.unitywebrequest": "1.0.0" 244 | } 245 | }, 246 | "com.unity.modules.unitywebrequestaudio": { 247 | "version": "1.0.0", 248 | "depth": 0, 249 | "source": "builtin", 250 | "dependencies": { 251 | "com.unity.modules.unitywebrequest": "1.0.0", 252 | "com.unity.modules.audio": "1.0.0" 253 | } 254 | }, 255 | "com.unity.modules.unitywebrequesttexture": { 256 | "version": "1.0.0", 257 | "depth": 0, 258 | "source": "builtin", 259 | "dependencies": { 260 | "com.unity.modules.unitywebrequest": "1.0.0", 261 | "com.unity.modules.imageconversion": "1.0.0" 262 | } 263 | }, 264 | "com.unity.modules.unitywebrequestwww": { 265 | "version": "1.0.0", 266 | "depth": 0, 267 | "source": "builtin", 268 | "dependencies": { 269 | "com.unity.modules.unitywebrequest": "1.0.0", 270 | "com.unity.modules.unitywebrequestassetbundle": "1.0.0", 271 | "com.unity.modules.unitywebrequestaudio": "1.0.0", 272 | "com.unity.modules.audio": "1.0.0", 273 | "com.unity.modules.assetbundle": "1.0.0", 274 | "com.unity.modules.imageconversion": "1.0.0" 275 | } 276 | }, 277 | "com.unity.modules.vehicles": { 278 | "version": "1.0.0", 279 | "depth": 0, 280 | "source": "builtin", 281 | "dependencies": { 282 | "com.unity.modules.physics": "1.0.0" 283 | } 284 | }, 285 | "com.unity.modules.video": { 286 | "version": "1.0.0", 287 | "depth": 0, 288 | "source": "builtin", 289 | "dependencies": { 290 | "com.unity.modules.audio": "1.0.0", 291 | "com.unity.modules.ui": "1.0.0", 292 | "com.unity.modules.unitywebrequest": "1.0.0" 293 | } 294 | }, 295 | "com.unity.modules.vr": { 296 | "version": "1.0.0", 297 | "depth": 0, 298 | "source": "builtin", 299 | "dependencies": { 300 | "com.unity.modules.jsonserialize": "1.0.0", 301 | "com.unity.modules.physics": "1.0.0", 302 | "com.unity.modules.xr": "1.0.0" 303 | } 304 | }, 305 | "com.unity.modules.wind": { 306 | "version": "1.0.0", 307 | "depth": 0, 308 | "source": "builtin", 309 | "dependencies": {} 310 | }, 311 | "com.unity.modules.xr": { 312 | "version": "1.0.0", 313 | "depth": 0, 314 | "source": "builtin", 315 | "dependencies": { 316 | "com.unity.modules.physics": "1.0.0", 317 | "com.unity.modules.jsonserialize": "1.0.0", 318 | "com.unity.modules.subsystems": "1.0.0" 319 | } 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /ProjectSettings/AudioManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!11 &1 4 | AudioManager: 5 | m_ObjectHideFlags: 0 6 | m_Volume: 1 7 | Rolloff Scale: 1 8 | Doppler Factor: 1 9 | Default Speaker Mode: 2 10 | m_SampleRate: 0 11 | m_DSPBufferSize: 0 12 | m_VirtualVoiceCount: 512 13 | m_RealVoiceCount: 32 14 | m_SpatializerPlugin: 15 | m_AmbisonicDecoderPlugin: 16 | m_DisableAudio: 0 17 | m_VirtualizeEffects: 1 18 | -------------------------------------------------------------------------------- /ProjectSettings/ClusterInputManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!236 &1 4 | ClusterInputManager: 5 | m_ObjectHideFlags: 0 6 | m_Inputs: [] 7 | -------------------------------------------------------------------------------- /ProjectSettings/DynamicsManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!55 &1 4 | PhysicsManager: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 3 7 | m_Gravity: {x: 0, y: -9.81, z: 0} 8 | m_DefaultMaterial: {fileID: 0} 9 | m_BounceThreshold: 2 10 | m_SleepThreshold: 0.005 11 | m_DefaultContactOffset: 0.01 12 | m_DefaultSolverIterations: 6 13 | m_DefaultSolverVelocityIterations: 1 14 | m_QueriesHitBackfaces: 0 15 | m_QueriesHitTriggers: 1 16 | m_EnableAdaptiveForce: 0 17 | m_EnablePCM: 1 18 | m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 19 | m_AutoSimulation: 1 20 | -------------------------------------------------------------------------------- /ProjectSettings/EditorBuildSettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!1045 &1 4 | EditorBuildSettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 2 7 | m_Scenes: 8 | - enabled: 1 9 | path: Assets/Adrenak.UniVoice/Samples/Group Chat Sample/Scenes/GroupVoiceCallSample-Mirror.unity 10 | guid: 112506fba9c2d954d9eec18ab175ec08 11 | - enabled: 1 12 | path: Assets/Adrenak.UniVoice/Samples/Basic Setup Scripts/UniVoiceMirrorSetupSample.unity 13 | guid: e2397deac1a1ddc489d2e846a22708fa 14 | - enabled: 1 15 | path: Assets/Adrenak.UniVoice/Samples/Basic Setup Scripts/UniVoiceFishNetSetupSample.unity 16 | guid: fcc19499518b4204d8fdeb4ddd323bde 17 | m_configObjects: {} 18 | -------------------------------------------------------------------------------- /ProjectSettings/EditorSettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!159 &1 4 | EditorSettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 4 7 | m_ExternalVersionControlSupport: Visible Meta Files 8 | m_SerializationMode: 2 9 | m_DefaultBehaviorMode: 0 10 | m_SpritePackerMode: 0 11 | m_SpritePackerPaddingPower: 1 12 | m_ProjectGenerationIncludedExtensions: txt;xml;fnt;cd 13 | m_ProjectGenerationRootNamespace: 14 | m_UserGeneratedProjectSuffix: 15 | m_CollabEditorSettings: 16 | inProgressEnabled: 1 17 | -------------------------------------------------------------------------------- /ProjectSettings/GraphicsSettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!30 &1 4 | GraphicsSettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 13 7 | m_Deferred: 8 | m_Mode: 1 9 | m_Shader: {fileID: 69, guid: 0000000000000000f000000000000000, type: 0} 10 | m_DeferredReflections: 11 | m_Mode: 1 12 | m_Shader: {fileID: 74, guid: 0000000000000000f000000000000000, type: 0} 13 | m_ScreenSpaceShadows: 14 | m_Mode: 1 15 | m_Shader: {fileID: 64, guid: 0000000000000000f000000000000000, type: 0} 16 | m_LegacyDeferred: 17 | m_Mode: 1 18 | m_Shader: {fileID: 63, guid: 0000000000000000f000000000000000, type: 0} 19 | m_DepthNormals: 20 | m_Mode: 1 21 | m_Shader: {fileID: 62, guid: 0000000000000000f000000000000000, type: 0} 22 | m_MotionVectors: 23 | m_Mode: 1 24 | m_Shader: {fileID: 75, guid: 0000000000000000f000000000000000, type: 0} 25 | m_LightHalo: 26 | m_Mode: 1 27 | m_Shader: {fileID: 105, guid: 0000000000000000f000000000000000, type: 0} 28 | m_LensFlare: 29 | m_Mode: 1 30 | m_Shader: {fileID: 102, guid: 0000000000000000f000000000000000, type: 0} 31 | m_AlwaysIncludedShaders: 32 | - {fileID: 7, guid: 0000000000000000f000000000000000, type: 0} 33 | - {fileID: 15104, guid: 0000000000000000f000000000000000, type: 0} 34 | - {fileID: 15105, guid: 0000000000000000f000000000000000, type: 0} 35 | - {fileID: 15106, guid: 0000000000000000f000000000000000, type: 0} 36 | - {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0} 37 | - {fileID: 10770, guid: 0000000000000000f000000000000000, type: 0} 38 | - {fileID: 16000, guid: 0000000000000000f000000000000000, type: 0} 39 | - {fileID: 17000, guid: 0000000000000000f000000000000000, type: 0} 40 | - {fileID: 16001, guid: 0000000000000000f000000000000000, type: 0} 41 | - {fileID: 16003, guid: 0000000000000000f000000000000000, type: 0} 42 | m_PreloadedShaders: [] 43 | m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, 44 | type: 0} 45 | m_CustomRenderPipeline: {fileID: 0} 46 | m_TransparencySortMode: 0 47 | m_TransparencySortAxis: {x: 0, y: 0, z: 1} 48 | m_DefaultRenderingPath: 1 49 | m_DefaultMobileRenderingPath: 1 50 | m_TierSettings: [] 51 | m_LightmapStripping: 0 52 | m_FogStripping: 0 53 | m_InstancingStripping: 0 54 | m_LightmapKeepPlain: 1 55 | m_LightmapKeepDirCombined: 1 56 | m_LightmapKeepDynamicPlain: 1 57 | m_LightmapKeepDynamicDirCombined: 1 58 | m_LightmapKeepShadowMask: 1 59 | m_LightmapKeepSubtractive: 1 60 | m_FogKeepLinear: 1 61 | m_FogKeepExp: 1 62 | m_FogKeepExp2: 1 63 | m_AlbedoSwatchInfos: [] 64 | m_LightsUseLinearIntensity: 0 65 | m_LightsUseColorTemperature: 0 66 | m_LogWhenShaderIsCompiled: 0 67 | m_AllowEnlightenSupportForUpgradedProject: 1 68 | -------------------------------------------------------------------------------- /ProjectSettings/InputManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!13 &1 4 | InputManager: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 2 7 | m_Axes: 8 | - serializedVersion: 3 9 | m_Name: Horizontal 10 | descriptiveName: 11 | descriptiveNegativeName: 12 | negativeButton: left 13 | positiveButton: right 14 | altNegativeButton: a 15 | altPositiveButton: d 16 | gravity: 3 17 | dead: 0.001 18 | sensitivity: 3 19 | snap: 1 20 | invert: 0 21 | type: 0 22 | axis: 0 23 | joyNum: 0 24 | - serializedVersion: 3 25 | m_Name: Vertical 26 | descriptiveName: 27 | descriptiveNegativeName: 28 | negativeButton: down 29 | positiveButton: up 30 | altNegativeButton: s 31 | altPositiveButton: w 32 | gravity: 3 33 | dead: 0.001 34 | sensitivity: 3 35 | snap: 1 36 | invert: 0 37 | type: 0 38 | axis: 0 39 | joyNum: 0 40 | - serializedVersion: 3 41 | m_Name: Fire1 42 | descriptiveName: 43 | descriptiveNegativeName: 44 | negativeButton: 45 | positiveButton: left ctrl 46 | altNegativeButton: 47 | altPositiveButton: mouse 0 48 | gravity: 1000 49 | dead: 0.001 50 | sensitivity: 1000 51 | snap: 0 52 | invert: 0 53 | type: 0 54 | axis: 0 55 | joyNum: 0 56 | - serializedVersion: 3 57 | m_Name: Fire2 58 | descriptiveName: 59 | descriptiveNegativeName: 60 | negativeButton: 61 | positiveButton: left alt 62 | altNegativeButton: 63 | altPositiveButton: mouse 1 64 | gravity: 1000 65 | dead: 0.001 66 | sensitivity: 1000 67 | snap: 0 68 | invert: 0 69 | type: 0 70 | axis: 0 71 | joyNum: 0 72 | - serializedVersion: 3 73 | m_Name: Fire3 74 | descriptiveName: 75 | descriptiveNegativeName: 76 | negativeButton: 77 | positiveButton: left shift 78 | altNegativeButton: 79 | altPositiveButton: mouse 2 80 | gravity: 1000 81 | dead: 0.001 82 | sensitivity: 1000 83 | snap: 0 84 | invert: 0 85 | type: 0 86 | axis: 0 87 | joyNum: 0 88 | - serializedVersion: 3 89 | m_Name: Jump 90 | descriptiveName: 91 | descriptiveNegativeName: 92 | negativeButton: 93 | positiveButton: space 94 | altNegativeButton: 95 | altPositiveButton: 96 | gravity: 1000 97 | dead: 0.001 98 | sensitivity: 1000 99 | snap: 0 100 | invert: 0 101 | type: 0 102 | axis: 0 103 | joyNum: 0 104 | - serializedVersion: 3 105 | m_Name: Mouse X 106 | descriptiveName: 107 | descriptiveNegativeName: 108 | negativeButton: 109 | positiveButton: 110 | altNegativeButton: 111 | altPositiveButton: 112 | gravity: 0 113 | dead: 0 114 | sensitivity: 0.1 115 | snap: 0 116 | invert: 0 117 | type: 1 118 | axis: 0 119 | joyNum: 0 120 | - serializedVersion: 3 121 | m_Name: Mouse Y 122 | descriptiveName: 123 | descriptiveNegativeName: 124 | negativeButton: 125 | positiveButton: 126 | altNegativeButton: 127 | altPositiveButton: 128 | gravity: 0 129 | dead: 0 130 | sensitivity: 0.1 131 | snap: 0 132 | invert: 0 133 | type: 1 134 | axis: 1 135 | joyNum: 0 136 | - serializedVersion: 3 137 | m_Name: Mouse ScrollWheel 138 | descriptiveName: 139 | descriptiveNegativeName: 140 | negativeButton: 141 | positiveButton: 142 | altNegativeButton: 143 | altPositiveButton: 144 | gravity: 0 145 | dead: 0 146 | sensitivity: 0.1 147 | snap: 0 148 | invert: 0 149 | type: 1 150 | axis: 2 151 | joyNum: 0 152 | - serializedVersion: 3 153 | m_Name: Horizontal 154 | descriptiveName: 155 | descriptiveNegativeName: 156 | negativeButton: 157 | positiveButton: 158 | altNegativeButton: 159 | altPositiveButton: 160 | gravity: 0 161 | dead: 0.19 162 | sensitivity: 1 163 | snap: 0 164 | invert: 0 165 | type: 2 166 | axis: 0 167 | joyNum: 0 168 | - serializedVersion: 3 169 | m_Name: Vertical 170 | descriptiveName: 171 | descriptiveNegativeName: 172 | negativeButton: 173 | positiveButton: 174 | altNegativeButton: 175 | altPositiveButton: 176 | gravity: 0 177 | dead: 0.19 178 | sensitivity: 1 179 | snap: 0 180 | invert: 1 181 | type: 2 182 | axis: 1 183 | joyNum: 0 184 | - serializedVersion: 3 185 | m_Name: Fire1 186 | descriptiveName: 187 | descriptiveNegativeName: 188 | negativeButton: 189 | positiveButton: joystick button 0 190 | altNegativeButton: 191 | altPositiveButton: 192 | gravity: 1000 193 | dead: 0.001 194 | sensitivity: 1000 195 | snap: 0 196 | invert: 0 197 | type: 0 198 | axis: 0 199 | joyNum: 0 200 | - serializedVersion: 3 201 | m_Name: Fire2 202 | descriptiveName: 203 | descriptiveNegativeName: 204 | negativeButton: 205 | positiveButton: joystick button 1 206 | altNegativeButton: 207 | altPositiveButton: 208 | gravity: 1000 209 | dead: 0.001 210 | sensitivity: 1000 211 | snap: 0 212 | invert: 0 213 | type: 0 214 | axis: 0 215 | joyNum: 0 216 | - serializedVersion: 3 217 | m_Name: Fire3 218 | descriptiveName: 219 | descriptiveNegativeName: 220 | negativeButton: 221 | positiveButton: joystick button 2 222 | altNegativeButton: 223 | altPositiveButton: 224 | gravity: 1000 225 | dead: 0.001 226 | sensitivity: 1000 227 | snap: 0 228 | invert: 0 229 | type: 0 230 | axis: 0 231 | joyNum: 0 232 | - serializedVersion: 3 233 | m_Name: Jump 234 | descriptiveName: 235 | descriptiveNegativeName: 236 | negativeButton: 237 | positiveButton: joystick button 3 238 | altNegativeButton: 239 | altPositiveButton: 240 | gravity: 1000 241 | dead: 0.001 242 | sensitivity: 1000 243 | snap: 0 244 | invert: 0 245 | type: 0 246 | axis: 0 247 | joyNum: 0 248 | - serializedVersion: 3 249 | m_Name: Submit 250 | descriptiveName: 251 | descriptiveNegativeName: 252 | negativeButton: 253 | positiveButton: return 254 | altNegativeButton: 255 | altPositiveButton: joystick button 0 256 | gravity: 1000 257 | dead: 0.001 258 | sensitivity: 1000 259 | snap: 0 260 | invert: 0 261 | type: 0 262 | axis: 0 263 | joyNum: 0 264 | - serializedVersion: 3 265 | m_Name: Submit 266 | descriptiveName: 267 | descriptiveNegativeName: 268 | negativeButton: 269 | positiveButton: enter 270 | altNegativeButton: 271 | altPositiveButton: space 272 | gravity: 1000 273 | dead: 0.001 274 | sensitivity: 1000 275 | snap: 0 276 | invert: 0 277 | type: 0 278 | axis: 0 279 | joyNum: 0 280 | - serializedVersion: 3 281 | m_Name: Cancel 282 | descriptiveName: 283 | descriptiveNegativeName: 284 | negativeButton: 285 | positiveButton: escape 286 | altNegativeButton: 287 | altPositiveButton: joystick button 1 288 | gravity: 1000 289 | dead: 0.001 290 | sensitivity: 1000 291 | snap: 0 292 | invert: 0 293 | type: 0 294 | axis: 0 295 | joyNum: 0 296 | -------------------------------------------------------------------------------- /ProjectSettings/MemorySettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!387306366 &1 4 | MemorySettings: 5 | m_ObjectHideFlags: 0 6 | m_EditorMemorySettings: 7 | m_MainAllocatorBlockSize: -1 8 | m_ThreadAllocatorBlockSize: -1 9 | m_MainGfxBlockSize: -1 10 | m_ThreadGfxBlockSize: -1 11 | m_CacheBlockSize: -1 12 | m_TypetreeBlockSize: -1 13 | m_ProfilerBlockSize: -1 14 | m_ProfilerEditorBlockSize: -1 15 | m_BucketAllocatorGranularity: -1 16 | m_BucketAllocatorBucketsCount: -1 17 | m_BucketAllocatorBlockSize: -1 18 | m_BucketAllocatorBlockCount: -1 19 | m_ProfilerBucketAllocatorGranularity: -1 20 | m_ProfilerBucketAllocatorBucketsCount: -1 21 | m_ProfilerBucketAllocatorBlockSize: -1 22 | m_ProfilerBucketAllocatorBlockCount: -1 23 | m_TempAllocatorSizeMain: -1 24 | m_JobTempAllocatorBlockSize: -1 25 | m_BackgroundJobTempAllocatorBlockSize: -1 26 | m_JobTempAllocatorReducedBlockSize: -1 27 | m_TempAllocatorSizeGIBakingWorker: -1 28 | m_TempAllocatorSizeNavMeshWorker: -1 29 | m_TempAllocatorSizeAudioWorker: -1 30 | m_TempAllocatorSizeCloudWorker: -1 31 | m_TempAllocatorSizeGfx: -1 32 | m_TempAllocatorSizeJobWorker: -1 33 | m_TempAllocatorSizeBackgroundWorker: -1 34 | m_TempAllocatorSizePreloadManager: -1 35 | m_PlatformMemorySettings: {} 36 | -------------------------------------------------------------------------------- /ProjectSettings/NavMeshAreas.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!126 &1 4 | NavMeshProjectSettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 2 7 | areas: 8 | - name: Walkable 9 | cost: 1 10 | - name: Not Walkable 11 | cost: 1 12 | - name: Jump 13 | cost: 2 14 | - name: 15 | cost: 1 16 | - name: 17 | cost: 1 18 | - name: 19 | cost: 1 20 | - name: 21 | cost: 1 22 | - name: 23 | cost: 1 24 | - name: 25 | cost: 1 26 | - name: 27 | cost: 1 28 | - name: 29 | cost: 1 30 | - name: 31 | cost: 1 32 | - name: 33 | cost: 1 34 | - name: 35 | cost: 1 36 | - name: 37 | cost: 1 38 | - name: 39 | cost: 1 40 | - name: 41 | cost: 1 42 | - name: 43 | cost: 1 44 | - name: 45 | cost: 1 46 | - name: 47 | cost: 1 48 | - name: 49 | cost: 1 50 | - name: 51 | cost: 1 52 | - name: 53 | cost: 1 54 | - name: 55 | cost: 1 56 | - name: 57 | cost: 1 58 | - name: 59 | cost: 1 60 | - name: 61 | cost: 1 62 | - name: 63 | cost: 1 64 | - name: 65 | cost: 1 66 | - name: 67 | cost: 1 68 | - name: 69 | cost: 1 70 | - name: 71 | cost: 1 72 | m_LastAgentTypeID: -887442657 73 | m_Settings: 74 | - serializedVersion: 2 75 | agentTypeID: 0 76 | agentRadius: 0.5 77 | agentHeight: 2 78 | agentSlope: 45 79 | agentClimb: 0.75 80 | ledgeDropHeight: 0 81 | maxJumpAcrossDistance: 0 82 | minRegionArea: 2 83 | manualCellSize: 0 84 | cellSize: 0.16666667 85 | manualTileSize: 0 86 | tileSize: 256 87 | accuratePlacement: 0 88 | m_SettingNames: 89 | - Humanoid 90 | -------------------------------------------------------------------------------- /ProjectSettings/NetworkManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!149 &1 4 | NetworkManager: 5 | m_ObjectHideFlags: 0 6 | m_DebugLevel: 0 7 | m_Sendrate: 15 8 | m_AssetToPrefab: {} 9 | -------------------------------------------------------------------------------- /ProjectSettings/PackageManagerSettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &1 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 61 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0} 13 | m_Name: 14 | m_EditorClassIdentifier: 15 | m_EnablePreReleasePackages: 0 16 | m_EnablePackageDependencies: 0 17 | m_AdvancedSettingsExpanded: 1 18 | m_ScopedRegistriesSettingsExpanded: 1 19 | m_SeeAllPackageVersions: 0 20 | oneTimeWarningShown: 0 21 | m_Registries: 22 | - m_Id: main 23 | m_Name: 24 | m_Url: https://packages.unity.com 25 | m_Scopes: [] 26 | m_IsDefault: 1 27 | m_Capabilities: 7 28 | - m_Id: scoped:NPM 29 | m_Name: NPM 30 | m_Url: https://registry.npmjs.org 31 | m_Scopes: 32 | - com.adrenak.brw 33 | - com.adrenak.unimic 34 | - com.adrenak.rnnoise4unity 35 | - com.adrenak.concentus-unity 36 | m_IsDefault: 0 37 | m_Capabilities: 0 38 | m_UserSelectedRegistryName: NPM 39 | m_UserAddingNewScopedRegistry: 0 40 | m_RegistryInfoDraft: 41 | m_Modified: 0 42 | m_ErrorMessage: 43 | m_UserModificationsInstanceId: -848 44 | m_OriginalInstanceId: -852 45 | m_LoadAssets: 0 46 | -------------------------------------------------------------------------------- /ProjectSettings/Physics2DSettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!19 &1 4 | Physics2DSettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 3 7 | m_Gravity: {x: 0, y: -9.81} 8 | m_DefaultMaterial: {fileID: 0} 9 | m_VelocityIterations: 8 10 | m_PositionIterations: 3 11 | m_VelocityThreshold: 1 12 | m_MaxLinearCorrection: 0.2 13 | m_MaxAngularCorrection: 8 14 | m_MaxTranslationSpeed: 100 15 | m_MaxRotationSpeed: 360 16 | m_BaumgarteScale: 0.2 17 | m_BaumgarteTimeOfImpactScale: 0.75 18 | m_TimeToSleep: 0.5 19 | m_LinearSleepTolerance: 0.01 20 | m_AngularSleepTolerance: 2 21 | m_DefaultContactOffset: 0.01 22 | m_AutoSimulation: 1 23 | m_QueriesHitTriggers: 1 24 | m_QueriesStartInColliders: 1 25 | m_ChangeStopsCallbacks: 0 26 | m_CallbacksOnDisable: 1 27 | m_AlwaysShowColliders: 0 28 | m_ShowColliderSleep: 1 29 | m_ShowColliderContacts: 0 30 | m_ShowColliderAABB: 0 31 | m_ContactArrowScale: 0.2 32 | m_ColliderAwakeColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.7529412} 33 | m_ColliderAsleepColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.36078432} 34 | m_ColliderContactColor: {r: 1, g: 0, b: 1, a: 0.6862745} 35 | m_ColliderAABBColor: {r: 1, g: 1, b: 0, a: 0.2509804} 36 | m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 37 | -------------------------------------------------------------------------------- /ProjectSettings/PresetManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!1386491679 &1 4 | PresetManager: 5 | m_ObjectHideFlags: 0 6 | m_DefaultList: [] 7 | -------------------------------------------------------------------------------- /ProjectSettings/ProjectVersion.txt: -------------------------------------------------------------------------------- 1 | m_EditorVersion: 2021.3.45f2 2 | m_EditorVersionWithRevision: 2021.3.45f2 (88f88f591b2e) 3 | -------------------------------------------------------------------------------- /ProjectSettings/QualitySettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!47 &1 4 | QualitySettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 5 7 | m_CurrentQuality: 5 8 | m_QualitySettings: 9 | - serializedVersion: 2 10 | name: Very Low 11 | pixelLightCount: 0 12 | shadows: 0 13 | shadowResolution: 0 14 | shadowProjection: 1 15 | shadowCascades: 1 16 | shadowDistance: 15 17 | shadowNearPlaneOffset: 3 18 | shadowCascade2Split: 0.33333334 19 | shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} 20 | shadowmaskMode: 0 21 | blendWeights: 1 22 | textureQuality: 1 23 | anisotropicTextures: 0 24 | antiAliasing: 0 25 | softParticles: 0 26 | softVegetation: 0 27 | realtimeReflectionProbes: 0 28 | billboardsFaceCameraPosition: 0 29 | vSyncCount: 0 30 | lodBias: 0.3 31 | maximumLODLevel: 0 32 | particleRaycastBudget: 4 33 | asyncUploadTimeSlice: 2 34 | asyncUploadBufferSize: 4 35 | resolutionScalingFixedDPIFactor: 1 36 | excludedTargetPlatforms: [] 37 | - serializedVersion: 2 38 | name: Low 39 | pixelLightCount: 0 40 | shadows: 0 41 | shadowResolution: 0 42 | shadowProjection: 1 43 | shadowCascades: 1 44 | shadowDistance: 20 45 | shadowNearPlaneOffset: 3 46 | shadowCascade2Split: 0.33333334 47 | shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} 48 | shadowmaskMode: 0 49 | blendWeights: 2 50 | textureQuality: 0 51 | anisotropicTextures: 0 52 | antiAliasing: 0 53 | softParticles: 0 54 | softVegetation: 0 55 | realtimeReflectionProbes: 0 56 | billboardsFaceCameraPosition: 0 57 | vSyncCount: 0 58 | lodBias: 0.4 59 | maximumLODLevel: 0 60 | particleRaycastBudget: 16 61 | asyncUploadTimeSlice: 2 62 | asyncUploadBufferSize: 4 63 | resolutionScalingFixedDPIFactor: 1 64 | excludedTargetPlatforms: [] 65 | - serializedVersion: 2 66 | name: Medium 67 | pixelLightCount: 1 68 | shadows: 1 69 | shadowResolution: 0 70 | shadowProjection: 1 71 | shadowCascades: 1 72 | shadowDistance: 20 73 | shadowNearPlaneOffset: 3 74 | shadowCascade2Split: 0.33333334 75 | shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} 76 | shadowmaskMode: 0 77 | blendWeights: 2 78 | textureQuality: 0 79 | anisotropicTextures: 1 80 | antiAliasing: 0 81 | softParticles: 0 82 | softVegetation: 0 83 | realtimeReflectionProbes: 0 84 | billboardsFaceCameraPosition: 0 85 | vSyncCount: 1 86 | lodBias: 0.7 87 | maximumLODLevel: 0 88 | particleRaycastBudget: 64 89 | asyncUploadTimeSlice: 2 90 | asyncUploadBufferSize: 4 91 | resolutionScalingFixedDPIFactor: 1 92 | excludedTargetPlatforms: [] 93 | - serializedVersion: 2 94 | name: High 95 | pixelLightCount: 2 96 | shadows: 2 97 | shadowResolution: 1 98 | shadowProjection: 1 99 | shadowCascades: 2 100 | shadowDistance: 40 101 | shadowNearPlaneOffset: 3 102 | shadowCascade2Split: 0.33333334 103 | shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} 104 | shadowmaskMode: 1 105 | blendWeights: 2 106 | textureQuality: 0 107 | anisotropicTextures: 1 108 | antiAliasing: 0 109 | softParticles: 0 110 | softVegetation: 1 111 | realtimeReflectionProbes: 1 112 | billboardsFaceCameraPosition: 1 113 | vSyncCount: 1 114 | lodBias: 1 115 | maximumLODLevel: 0 116 | particleRaycastBudget: 256 117 | asyncUploadTimeSlice: 2 118 | asyncUploadBufferSize: 4 119 | resolutionScalingFixedDPIFactor: 1 120 | excludedTargetPlatforms: [] 121 | - serializedVersion: 2 122 | name: Very High 123 | pixelLightCount: 3 124 | shadows: 2 125 | shadowResolution: 2 126 | shadowProjection: 1 127 | shadowCascades: 2 128 | shadowDistance: 70 129 | shadowNearPlaneOffset: 3 130 | shadowCascade2Split: 0.33333334 131 | shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} 132 | shadowmaskMode: 1 133 | blendWeights: 4 134 | textureQuality: 0 135 | anisotropicTextures: 2 136 | antiAliasing: 2 137 | softParticles: 1 138 | softVegetation: 1 139 | realtimeReflectionProbes: 1 140 | billboardsFaceCameraPosition: 1 141 | vSyncCount: 1 142 | lodBias: 1.5 143 | maximumLODLevel: 0 144 | particleRaycastBudget: 1024 145 | asyncUploadTimeSlice: 2 146 | asyncUploadBufferSize: 4 147 | resolutionScalingFixedDPIFactor: 1 148 | excludedTargetPlatforms: [] 149 | - serializedVersion: 2 150 | name: Ultra 151 | pixelLightCount: 4 152 | shadows: 2 153 | shadowResolution: 2 154 | shadowProjection: 1 155 | shadowCascades: 4 156 | shadowDistance: 150 157 | shadowNearPlaneOffset: 3 158 | shadowCascade2Split: 0.33333334 159 | shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} 160 | shadowmaskMode: 1 161 | blendWeights: 4 162 | textureQuality: 0 163 | anisotropicTextures: 2 164 | antiAliasing: 2 165 | softParticles: 1 166 | softVegetation: 1 167 | realtimeReflectionProbes: 1 168 | billboardsFaceCameraPosition: 1 169 | vSyncCount: 1 170 | lodBias: 2 171 | maximumLODLevel: 0 172 | particleRaycastBudget: 4096 173 | asyncUploadTimeSlice: 2 174 | asyncUploadBufferSize: 4 175 | resolutionScalingFixedDPIFactor: 1 176 | excludedTargetPlatforms: [] 177 | m_PerPlatformDefaultQuality: 178 | Android: 2 179 | Nintendo 3DS: 5 180 | Nintendo Switch: 5 181 | PS4: 5 182 | PSM: 5 183 | PSP2: 2 184 | Samsung TV: 2 185 | Standalone: 5 186 | Tizen: 2 187 | Web: 5 188 | WebGL: 3 189 | WiiU: 5 190 | Windows Store Apps: 5 191 | XboxOne: 5 192 | iPhone: 2 193 | tvOS: 2 194 | -------------------------------------------------------------------------------- /ProjectSettings/TagManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!78 &1 4 | TagManager: 5 | serializedVersion: 2 6 | tags: [] 7 | layers: 8 | - Default 9 | - TransparentFX 10 | - Ignore Raycast 11 | - 12 | - Water 13 | - UI 14 | - 15 | - 16 | - 17 | - 18 | - 19 | - 20 | - 21 | - 22 | - 23 | - 24 | - 25 | - 26 | - 27 | - 28 | - 29 | - 30 | - 31 | - 32 | - 33 | - 34 | - 35 | - 36 | - 37 | - 38 | - 39 | - 40 | m_SortingLayers: 41 | - name: Default 42 | uniqueID: 0 43 | locked: 0 44 | -------------------------------------------------------------------------------- /ProjectSettings/TimeManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!5 &1 4 | TimeManager: 5 | m_ObjectHideFlags: 0 6 | Fixed Timestep: 0.02 7 | Maximum Allowed Timestep: 0.33333334 8 | m_TimeScale: 1 9 | Maximum Particle Timestep: 0.03 10 | -------------------------------------------------------------------------------- /ProjectSettings/UnityConnectSettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!310 &1 4 | UnityConnectSettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 1 7 | m_Enabled: 0 8 | m_TestMode: 0 9 | m_EventOldUrl: https://api.uca.cloud.unity3d.com/v1/events 10 | m_EventUrl: https://cdp.cloud.unity3d.com/v1/events 11 | m_ConfigUrl: https://config.uca.cloud.unity3d.com 12 | m_DashboardUrl: https://dashboard.unity3d.com 13 | m_TestInitMode: 0 14 | CrashReportingSettings: 15 | m_EventUrl: https://perf-events.cloud.unity3d.com 16 | m_Enabled: 0 17 | m_LogBufferSize: 10 18 | m_CaptureEditorExceptions: 1 19 | UnityPurchasingSettings: 20 | m_Enabled: 0 21 | m_TestMode: 0 22 | UnityAnalyticsSettings: 23 | m_Enabled: 0 24 | m_TestMode: 0 25 | m_InitializeOnStartup: 1 26 | m_PackageRequiringCoreStatsPresent: 0 27 | UnityAdsSettings: 28 | m_Enabled: 0 29 | m_InitializeOnStartup: 1 30 | m_TestMode: 0 31 | m_IosGameId: 32 | m_AndroidGameId: 33 | m_GameIds: {} 34 | m_GameId: 35 | PerformanceReportingSettings: 36 | m_Enabled: 0 37 | -------------------------------------------------------------------------------- /ProjectSettings/VFXManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!937362698 &1 4 | VFXManager: 5 | m_ObjectHideFlags: 0 6 | m_IndirectShader: {fileID: 0} 7 | m_CopyBufferShader: {fileID: 0} 8 | m_SortShader: {fileID: 0} 9 | m_RenderPipeSettingsPath: 10 | m_FixedTimeStep: 0.016666668 11 | m_MaxDeltaTime: 0.05 12 | -------------------------------------------------------------------------------- /ProjectSettings/VersionControlSettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!890905787 &1 4 | VersionControlSettings: 5 | m_ObjectHideFlags: 0 6 | m_Mode: Visible Meta Files 7 | m_CollabEditorSettings: 8 | inProgressEnabled: 1 9 | -------------------------------------------------------------------------------- /ProjectSettings/XRSettings.asset: -------------------------------------------------------------------------------- 1 | { 2 | "m_SettingKeys": [ 3 | "VR Device Disabled", 4 | "VR Device User Alert" 5 | ], 6 | "m_SettingValues": [ 7 | "False", 8 | "False" 9 | ] 10 | } -------------------------------------------------------------------------------- /ProjectSettings/boot.config: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrenak/univoice/b4539c94336f8cda31b03b77b88d000f5b1bbf0a/ProjectSettings/boot.config -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UniVoice 2 | UniVoice is a voice chat/VoIP solution for Unity. 3 | 4 | Some features of UniVoice: 5 | - 🎨 Customize your audio input, output and networking layers. 6 | * 🌐 __Configurable Network__: 7 | - 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. 8 | - Built-in support for: 9 | - [Mirror networking](https://mirror-networking.com/) 10 | - [Fish Networking](https://fish-networking.gitbook.io/docs) 11 | 12 | * 🎤 __Configurable Audio Input__: 13 | - UniVoice is audio input agnostic. You can change the source of outgoing audio by implementing the `IAudioInput` interface. 14 | - Built-in support for: 15 | - Capturing Mic audio as device input. 16 | 17 | * 🔊 __Configurable Audio Output__: 18 | - UniVoice is audio output agnostic. You can divert incoming audio to anywhere you want by implementing the `IAudioOutput` interface. 19 | - Built-in support for: 20 | - Playing incoming audio using Unity AudioSource. 21 | 22 | * ✏️ __Audio Filters__: 23 | - Modify outgoing and incoming audio by implementing the `IAudioFilter` interface. 24 | - Built-in support for: 25 | - Opus (Concentus) encoding & decoding. 26 | - RNNoise based noise removal. 27 | - Energy based VAD (Voice Activity Detection) 28 | - Gaussian blurring for minor denoising. 29 | 30 | - 👥 Easy integration with your existing networking solution 31 | - Whether you're using Mirror or FishNet, UniVoice runs in the background in sync with your networking lifecycle 32 | - A basic integration involves just initializing it on start. 33 | - For advanced usage like teams, chatrooms, lobbies, you can use the UniVoice API to create runtime behaviour. 34 | 35 | - ⚙ Fine control over audio data flow. 36 | * Don't want to listen to a peer? Mute them. Don't want someone listening to you? Deafen them. 37 | * Group players using tags and control audio flow between them. For example: 38 | - "red", "blue" and "spectator" tags for two teams playing against each other. 39 | - Red and Blue teams can only hear each other 40 | - Spectators can hear everyone 41 | - clients with "contestant", "judge" and "audience" tags for a virtual talent show. 42 | - Contestant can be heard by everyone, but don't hear anyone else (for focus) 43 | - Judges can talk to and hear each other for discussions. They can hear the contestant. But not the audience (for less noise) 44 | - Audience can hear and talk to each other. They can hear the performer. But they cannot hear the judges. 45 | 46 | ## Installation 47 | ⚠️ [OpenUPM](https://openupm.com/packages/com.adrenak.univoice/?subPage=versions) may not have up to date releases. Install using NPM registry instead 👇 48 | 49 | Ensure you have the NPM registry in the `manifest.json` file of your Unity project with the following scopes: 50 | ``` 51 | "scopedRegistries": [ 52 | { 53 | "name": "npmjs", 54 | "url": "https://registry.npmjs.org", 55 | "scopes": [ 56 | "com.npmjs", 57 | "com.adrenak.univoice", 58 | "com.adrenak.brw", 59 | "com.adrenak.unimic", 60 | "com.adrenak.concentus-unity" 61 | ] 62 | } 63 | ] 64 | ``` 65 | 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). 66 | 67 | ## Useful links 68 | * API reference is available here: http://www.vatsalambastha.com/univoice 69 | * UniVoice blog: https://blog.vatsalambastha.com/search/label/univoice 70 | * Discord server: https://discord.gg/NGvkEVbdjQ 71 | 72 | ## Integration 73 | 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. 74 | 75 | ## Samples 76 | This repository contains two samples: 77 | * `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. 78 | * `UniVoiceFishNetSetypSample.cs` is also very similar. Just drag and drop and it should work! 79 | * A sample scene that shows the other clients in a UI as well as allows you to mute yourself/them. This sample is Mirror based. 80 | 81 | > UniVoice currently only supports Mirror and FishNetworking out of the box. Follow the instructions in the "Activating non-packaged dependencies" section below before trying it out the samples. 82 | 83 | ## Dependencies 84 | [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. 85 | 86 | [com.adrenak.unimic](https://www.github.com/adrenak/unimic) for easily capturing audio from any connected mic devices. See `UniMicInput.cs` for usage. Also used for streaming audio playback. See `StreamedAudioSourceOutput.cs` for usage. 87 | 88 | [com.adrenak.concentus-unity](https://www.github.com/adrenak/concentus-unity) for Opus encoding and decoding. See `ConcentusEncodeFilter.cs` and `ConcentusDecodeFilter.cs` for usage 89 | 90 | ## Activating non-packaged dependencies 91 | UniVoice includes and installs the dependencies mentioned above along with itself. The following implementations are available out of the box when you install it: 92 | * Opus encoding/decoding filter (via Contentus-Unity) 93 | * GaussianAudioBlur filter (plain C#, no dependencies used) 94 | * Mic audio capture input (via UniMic) 95 | * AudioSource based playback output (via UniMic) 96 | 97 | UniVoice has code that uses dependencies that you have to install and sometimes enable via compilation symbols as they are _not_ UniVoice dependencies and _don't_ get installed along with UniVoice. This is because they are either third party modules or based on native libraries (not plain C#) that can pose build issues. 98 | * RNNoise Noise removal filter: 99 | * To enable, ensure the [RNNoise4Unity](https://github.com/adrenak/RNNoise4Unity) package is in your project and add `UNIVOICE_FILTER_RNNOISE4UNITY` to activate it 100 | * Mirror network: 101 | * Just add the Mirror package to your project. UniVoice will detect it. 102 | * Fish Networking: 103 | * Just install FishNet package in your project. UniVoice will detect it. 104 | 105 | ## License and Support 106 | This project is under the [MIT license](https://github.com/adrenak/univoice/blob/master/LICENSE). 107 | 108 | Community contributions are welcome. 109 | 110 | Commercial engagements with the author can be arranged, subject to schedule and availability. 111 | 112 | ## Acknowledgements and contributors 113 | * [@metater](https://github.com/Metater/) for helping make improvements to audio streaming quality. [A related blog post](https://blog.vatsalambastha.com/2025/07/unimic-330-many-streamedaudiosource.html) 114 | * [@FrantisekHolubec](https://github.com/FrantisekHolubec) for [FishNet support code](https://github.com/adrenak/univoice/commit/fdc3424180d8991c92b3e092b3edb50b6110c863). Here's a [related blog post](https://blog.vatsalambastha.com/2025/09/univoice-480-fishnet-support.html) 115 | * [Masaryk University](https://www.muni.cz/en) for using UniVoice in their projects and providing helpful feedback 116 | 117 | ## Contact 118 | The author can be reached at the following links: 119 | 120 | [Website](http://www.vatsalambastha.com) 121 | [LinkedIn](https://www.linkedin.com/in/vatsalAmbastha) 122 | [GitHub](https://www.github.com/adrenak) 123 | [Twitter](https://www.twitter.com/vatsalAmbastha) 124 | 125 | Discord: `adrenak#1934` 126 | --------------------------------------------------------------------------------