├── .gitignore ├── LICENSE ├── MumbleClient ├── ConsoleMumbleProtocol.cs ├── MicrophoneRecorder.cs ├── MumbleClient.csproj ├── Program.cs └── server.txt ├── MumbleGuiClient ├── ChannelsTreeView.cs ├── Extensions │ ├── BasicVoiceDetector.cs │ ├── ConnectionProtocol.cs │ ├── IVoiceDetector.cs │ ├── MicrophoneRecorder.cs │ └── SpeakerPlayback.cs ├── Form1.Designer.cs ├── Form1.cs ├── Form1.resx ├── MumbleGuiClient.csproj ├── Program.cs └── Properties │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── MumbleSharp.sln ├── MumbleSharp ├── Audio │ ├── AudioDecodingBuffer.cs │ ├── AudioEncodingBuffer.cs │ ├── CodecSet.cs │ ├── Codecs │ │ ├── CeltAlpha │ │ │ └── CeltAlphaCodec.cs │ │ ├── CeltBeta │ │ │ └── CeltBetaCodec.cs │ │ ├── IVoiceCodec.cs │ │ ├── Opus │ │ │ ├── Application.cs │ │ │ ├── Libs │ │ │ │ ├── 32bit │ │ │ │ │ └── opus.dll │ │ │ │ └── 64bit │ │ │ │ │ └── opus.dll │ │ │ ├── NativeMethods.cs │ │ │ ├── OpusCodec.cs │ │ │ ├── OpusDecoder.cs │ │ │ └── OpusEncoder.cs │ │ ├── SpeechCodecs.cs │ │ └── Speex │ │ │ └── SpeexCodec.cs │ ├── DynamicCircularBuffer.cs │ └── SpeechTarget.cs ├── BasicMumbleProtocol.cs ├── ConnectionStates.cs ├── Constants.cs ├── CryptState.cs ├── Extensions │ ├── IEnumerableOfChannelExtensions.cs │ └── Log.cs ├── IMumbleProtocol.cs ├── InternalsVisibleTo.cs ├── LibraryLoader.cs ├── Model │ ├── Channel.cs │ ├── Message.cs │ ├── Permissions.cs │ └── User.cs ├── MumbleConnection.cs ├── MumbleSharp.csproj ├── OcbAes.cs ├── Packets │ ├── Implementation Note.md │ ├── Mumble.cs │ ├── PacketType.cs │ └── mumble.proto ├── PlatformDetails.cs ├── TcpSocket.cs ├── UdpPacketReader.cs ├── UdpSocket.cs ├── Var64.cs └── mumblesharp.ico ├── MumbleSharpTest ├── DynamicCircularBufferTest.cs ├── MumbleSharpTest.csproj ├── OcbAesTest.cs ├── ReadMe.md └── UdpPacketReaderTest.cs ├── Readme.md ├── mumble-protocol-1.2.5-alpha.pdf ├── mumble-protocol.url ├── mumblesharp.ico ├── mumblesharp.png ├── mumblesharp.xcf ├── nuget.pack.bat └── nuget.publish.bat /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.user 3 | *.cachefile 4 | *.xnb 5 | *.sln.docstates 6 | *.cache 7 | 8 | .vs 9 | packages 10 | TestResults 11 | _Resharper.* 12 | NuGetRelease 13 | 14 | MumbleClient/bin 15 | MumbleClient/obj 16 | MumbleGuiClient/bin 17 | MumbleGuiClient/obj 18 | MumbleSharp/bin 19 | MumbleSharp/obj 20 | MumbleSharpTest/bin 21 | MumbleSharpTest/obj 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Martin Evans 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MumbleClient/ConsoleMumbleProtocol.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using MumbleProto; 5 | using MumbleSharp; 6 | using MumbleSharp.Audio; 7 | using MumbleSharp.Audio.Codecs; 8 | using MumbleSharp.Model; 9 | using MumbleSharp.Packets; 10 | using NAudio.Wave; 11 | 12 | namespace MumbleClient 13 | { 14 | /// 15 | /// A test mumble protocol. Currently just prints the name of whoever is speaking, as well as printing messages it receives 16 | /// 17 | public class ConsoleMumbleProtocol 18 | : BasicMumbleProtocol 19 | { 20 | readonly Dictionary _players = new Dictionary(); 21 | 22 | public override void EncodedVoice(byte[] data, uint userId, long sequence, IVoiceCodec codec, SpeechTarget target) 23 | { 24 | User user = Users.FirstOrDefault(u => u.Id == userId); 25 | if (user != null) 26 | Console.WriteLine(user.Name + " is speaking. Seq" + sequence); 27 | 28 | base.EncodedVoice(data, userId, sequence, codec, target); 29 | } 30 | 31 | protected override void UserJoined(User user) 32 | { 33 | base.UserJoined(user); 34 | 35 | _players.Add(user, new AudioPlayer(user.Voice)); 36 | } 37 | 38 | protected override void UserLeft(User user) 39 | { 40 | base.UserLeft(user); 41 | 42 | _players.Remove(user); 43 | } 44 | 45 | public override void ServerConfig(ServerConfig serverConfig) 46 | { 47 | base.ServerConfig(serverConfig); 48 | 49 | Console.WriteLine(serverConfig.WelcomeText); 50 | } 51 | 52 | protected override void ChannelMessageReceived(ChannelMessage message) 53 | { 54 | if (message.Channel.Equals(LocalUser.Channel)) 55 | Console.WriteLine(string.Format("{0} (channel message): {1}", message.Sender.Name, message.Text)); 56 | 57 | base.ChannelMessageReceived(message); 58 | } 59 | 60 | protected override void PersonalMessageReceived(PersonalMessage message) 61 | { 62 | Console.WriteLine(string.Format("{0} (personal message): {1}", message.Sender.Name, message.Text)); 63 | 64 | base.PersonalMessageReceived(message); 65 | } 66 | 67 | private class AudioPlayer 68 | { 69 | private readonly WaveOutEvent _playbackDevice = new WaveOutEvent(); 70 | 71 | public AudioPlayer(IWaveProvider provider) 72 | { 73 | _playbackDevice.Init(provider); 74 | _playbackDevice.Play(); 75 | 76 | _playbackDevice.PlaybackStopped += (sender, args) => Console.WriteLine("Playback stopped: " + args.Exception); 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /MumbleClient/MicrophoneRecorder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MumbleSharp; 3 | using NAudio.Wave; 4 | 5 | namespace MumbleClient 6 | { 7 | public class MicrophoneRecorder 8 | { 9 | private readonly IMumbleProtocol _protocol; 10 | 11 | private bool _recording = true; 12 | 13 | public MicrophoneRecorder(IMumbleProtocol protocol) 14 | { 15 | _protocol = protocol; 16 | var sourceStream = new WaveInEvent 17 | { 18 | WaveFormat = new WaveFormat(Constants.DEFAULT_AUDIO_SAMPLE_RATE, Constants.DEFAULT_AUDIO_SAMPLE_BITS, Constants.DEFAULT_AUDIO_SAMPLE_CHANNELS) 19 | }; 20 | sourceStream.DataAvailable += VoiceDataAvailable; 21 | 22 | sourceStream.StartRecording(); 23 | } 24 | 25 | private void VoiceDataAvailable(object sender, WaveInEventArgs e) 26 | { 27 | if (!_recording) 28 | return; 29 | 30 | //At the moment we're sending *from* the local user, this is kinda stupid. 31 | //What we really want is to send *to* other users, or to channels. Something like: 32 | // 33 | // _connection.Users.First().SendVoiceWhisper(e.Buffer); 34 | // 35 | // _connection.Channels.First().SendVoice(e.Buffer, shout: true); 36 | 37 | //if (_protocol.LocalUser != null) 38 | // _protocol.LocalUser.SendVoice(new ArraySegment(e.Buffer, 0, e.BytesRecorded)); 39 | 40 | //Send to the channel LocalUser is currently in 41 | if (_protocol.LocalUser != null && _protocol.LocalUser.Channel != null) 42 | _protocol.LocalUser.Channel.SendVoice(new ArraySegment(e.Buffer, 0, e.BytesRecorded)); 43 | } 44 | 45 | public void Record() 46 | { 47 | _recording = true; 48 | } 49 | 50 | public void Stop() 51 | { 52 | _recording = false; 53 | _protocol.LocalUser.Channel.SendVoiceStop(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /MumbleClient/MumbleClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | MumbleSharp 7 | martindevans, Meetsch 8 | Copyright © 2022 9 | Console demo application for the use of the MumbleSharp library. 10 | For more info on MumbleSharp please visit https://github.com/martindevans/MumbleSharp 11 | mumblesharp.png 12 | 13 | https://github.com/martindevans/MumbleSharp 14 | Git 15 | MumbleSharp Mumble voip voice chat console demo 16 | LICENSE 17 | https://github.com/martindevans/MumbleSharp 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | True 30 | 31 | 32 | 33 | True 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /MumbleClient/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using System.Threading; 8 | using MumbleSharp; 9 | using MumbleSharp.Model; 10 | 11 | namespace MumbleClient 12 | { 13 | internal class Program 14 | { 15 | private static Exception _updateLoopThreadException = null; 16 | 17 | private static void Main(string[] args) 18 | { 19 | string addr, name, pass; 20 | int port; 21 | FileInfo serverConfigFile = new FileInfo(Environment.CurrentDirectory + "\\server.txt"); 22 | if (serverConfigFile.Exists) 23 | { 24 | using (StreamReader reader = new StreamReader(serverConfigFile.OpenRead())) 25 | { 26 | addr = reader.ReadLine(); 27 | port = int.Parse(reader.ReadLine()); 28 | name = reader.ReadLine(); 29 | pass = reader.ReadLine(); 30 | } 31 | } 32 | else 33 | { 34 | Console.WriteLine("Enter server address:"); 35 | addr = Console.ReadLine(); 36 | Console.WriteLine("Enter server port (leave blank for default (64738)):"); 37 | string line = Console.ReadLine(); 38 | if (line == "") 39 | { 40 | port = 64738; 41 | } 42 | else 43 | { 44 | port = int.Parse(line); 45 | } 46 | Console.WriteLine("Enter name:"); 47 | name = Console.ReadLine(); 48 | Console.WriteLine("Enter password:"); 49 | pass = Console.ReadLine(); 50 | 51 | using (StreamWriter writer = new StreamWriter(serverConfigFile.OpenWrite())) 52 | { 53 | writer.WriteLine(addr); 54 | writer.WriteLine(port); 55 | writer.WriteLine(name); 56 | writer.WriteLine(pass); 57 | } 58 | } 59 | 60 | ConsoleMumbleProtocol protocol = new ConsoleMumbleProtocol(); 61 | MumbleConnection connection = new MumbleConnection(new IPEndPoint(Dns.GetHostAddresses(addr).First(a => a.AddressFamily == AddressFamily.InterNetwork), port), protocol); 62 | connection.Connect(name, pass, new string[0], addr); 63 | 64 | //Start the UpdateLoop thread, and collect a possible exception at termination 65 | ThreadStart updateLoopThreadStart = new ThreadStart(() => UpdateLoop(connection, out _updateLoopThreadException)); 66 | updateLoopThreadStart += () => { 67 | if(_updateLoopThreadException != null) 68 | throw new Exception($"{nameof(UpdateLoop)} was terminated unexpectedly because of a {_updateLoopThreadException.GetType().ToString()}", _updateLoopThreadException); 69 | }; 70 | Thread updateLoopThread = new Thread(updateLoopThreadStart) {IsBackground = true}; 71 | updateLoopThread.Start(); 72 | 73 | var r = new MicrophoneRecorder(protocol); 74 | 75 | //When localuser is set it means we're really connected 76 | while (!protocol.ReceivedServerSync) 77 | { 78 | } 79 | 80 | Console.WriteLine("Connected as " + protocol.LocalUser.Id); 81 | 82 | DrawChannel("", protocol.Channels.ToArray(), protocol.Users.ToArray(), protocol.RootChannel); 83 | 84 | Console.ReadLine(); 85 | } 86 | 87 | private static void DrawChannel(string indent, IEnumerable channels, IEnumerable users, Channel c) 88 | { 89 | Console.WriteLine(indent + c.Name + (c.Temporary ? "(temp)" : "")); 90 | 91 | foreach (var user in users.Where(u => u.Channel.Equals(c))) 92 | { 93 | if (string.IsNullOrWhiteSpace(user.Comment)) 94 | Console.WriteLine(indent + "-> " + user.Name); 95 | else 96 | Console.WriteLine(indent + "-> " + user.Name + " (" + user.Comment + ")"); 97 | } 98 | 99 | foreach (var channel in channels.Where(ch => ch.Parent == c.Id && ch.Parent != ch.Id)) 100 | DrawChannel(indent + "\t", channels, users, channel); 101 | } 102 | 103 | private static void UpdateLoop(MumbleConnection connection, out Exception exception) 104 | { 105 | exception = null; 106 | try 107 | { 108 | while (connection.State != ConnectionStates.Disconnected) 109 | { 110 | if (connection.Process()) 111 | Thread.Yield(); 112 | else 113 | Thread.Sleep(1); 114 | } 115 | } catch (Exception ex) 116 | { 117 | exception = ex; 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /MumbleClient/server.txt: -------------------------------------------------------------------------------- 1 | mumble.placeholder-software.co.uk 2 | 64738 3 | .AI 4 | -------------------------------------------------------------------------------- /MumbleGuiClient/ChannelsTreeView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows.Forms; 7 | 8 | namespace MumbleGuiClient 9 | { 10 | internal class ChannelsTreeView : TreeView 11 | { 12 | private Timer updateTimer = new Timer(); 13 | private List blinkingNodes = new List(); 14 | private Dictionary> notifyingNodes = new Dictionary>(); 15 | public ChannelsTreeView() 16 | { 17 | updateTimer.Interval = 200; 18 | updateTimer.Tick += new EventHandler(t_Tick); 19 | } 20 | bool isNodeBlinked = false; 21 | void t_Tick(object sender, EventArgs e) 22 | { 23 | foreach (TreeNode tn in blinkingNodes) 24 | { 25 | if (isNodeBlinked) 26 | { 27 | //update Icon 28 | tn.Text = tn.Text.Substring(0, tn.Text.Length - 1);//to test 29 | isNodeBlinked = false; 30 | } 31 | else 32 | { 33 | //update Icon 34 | tn.Text = tn.Text + "*";//to test 35 | isNodeBlinked = true; 36 | } 37 | } 38 | 39 | List endNotifyingNodes = new List(); 40 | foreach (KeyValuePair> kvp in notifyingNodes) 41 | { 42 | if (DateTime.Now > kvp.Value.Item1 + kvp.Value.Item3) 43 | endNotifyingNodes.Add(kvp.Key); 44 | } 45 | foreach (TreeNode endNotifyingNode in endNotifyingNodes) 46 | { 47 | RemoveNotifyingNode(endNotifyingNode); 48 | } 49 | } 50 | 51 | public void AddBlinkNode(TreeNode node) 52 | { 53 | blinkingNodes.Add(node); 54 | } 55 | public void RemoveBlinkNode(TreeNode node) 56 | { 57 | blinkingNodes.Remove(node); 58 | } 59 | public void ClearBlinkNodes() 60 | { 61 | blinkingNodes.Clear(); 62 | } 63 | public List BlinkingNodes 64 | { 65 | get { return blinkingNodes; } 66 | } 67 | 68 | public void AddNotifyingNode(TreeNode node, string notificationMessage, TimeSpan duration) 69 | { 70 | if (notifyingNodes.ContainsKey(node)) 71 | { 72 | notifyingNodes[node] = new Tuple(DateTime.Now, notifyingNodes[node].Item2, duration); 73 | } 74 | else 75 | { 76 | notifyingNodes.Add(node, new Tuple(DateTime.Now, node.Text, duration)); 77 | node.Text = node.Text + " " + notificationMessage; 78 | } 79 | } 80 | public void RemoveNotifyingNode(TreeNode node) 81 | { 82 | if (notifyingNodes.ContainsKey(node)) 83 | { 84 | node.Text = notifyingNodes[node].Item2; 85 | notifyingNodes.Remove(node); 86 | } 87 | } 88 | public void ClearNotifyingNodes() 89 | { 90 | notifyingNodes.Clear(); 91 | } 92 | public List NotifyingNodes 93 | { 94 | get { return notifyingNodes.Keys.ToList(); } 95 | } 96 | 97 | public int UpdateInterval 98 | { 99 | get { return updateTimer.Interval; } 100 | set { updateTimer.Interval = value; } 101 | } 102 | public void StartUpdating() 103 | { 104 | isNodeBlinked = false; 105 | updateTimer.Enabled = true; 106 | } 107 | public void StopUpdating() 108 | { 109 | updateTimer.Enabled = false; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /MumbleGuiClient/Extensions/BasicVoiceDetector.cs: -------------------------------------------------------------------------------- 1 | using NAudio.Wave; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace MumbleGuiClient 9 | { 10 | public class BasicVoiceDetector : IVoiceDetector 11 | { 12 | public short VoiceDetectionSampleVolume { get; set; } 13 | public short NoiseDetectionSampleVolume { get; set; } 14 | 15 | public BasicVoiceDetector() 16 | { 17 | VoiceDetectionSampleVolume = Convert.ToInt16(short.MaxValue * 0.5f); 18 | NoiseDetectionSampleVolume = Convert.ToInt16(short.MaxValue * 0.25f); 19 | } 20 | 21 | private enum SoundType 22 | { 23 | NOTHING, 24 | NOISE, 25 | VOICE 26 | } 27 | 28 | private DateTime _lastDetectedSoundTime = DateTime.MinValue; 29 | private TimeSpan _minVoiceHoldTime = TimeSpan.FromMilliseconds(1000); 30 | private TimeSpan _minNoiseHoldTime = TimeSpan.FromMilliseconds(250); 31 | private SoundType _lastDetectedSound = SoundType.NOTHING; 32 | 33 | public bool VoiceDetected(WaveBuffer waveBuffer, int bytesRecorded) 34 | { 35 | var now = DateTime.Now; 36 | 37 | SoundType detectedSound = DetectSound(waveBuffer, bytesRecorded, VoiceDetectionSampleVolume, NoiseDetectionSampleVolume); 38 | if (detectedSound != SoundType.NOTHING) 39 | _lastDetectedSoundTime = now; 40 | 41 | //adjust the detectedSound to take into account to hold times. 42 | if (_lastDetectedSound == SoundType.NOISE && (_lastDetectedSoundTime + _minNoiseHoldTime > now)) 43 | { 44 | switch (detectedSound) 45 | { 46 | case SoundType.NOTHING: 47 | case SoundType.NOISE: 48 | detectedSound = SoundType.NOISE; 49 | break; 50 | case SoundType.VOICE: 51 | detectedSound = SoundType.VOICE; 52 | break; 53 | } 54 | } 55 | else if (_lastDetectedSound == SoundType.VOICE && (_lastDetectedSoundTime + _minVoiceHoldTime > now)) 56 | { 57 | detectedSound = SoundType.VOICE; 58 | } 59 | 60 | _lastDetectedSound = detectedSound; 61 | 62 | if (detectedSound == SoundType.NOTHING) 63 | return false; 64 | else 65 | return true; 66 | } 67 | 68 | private static SoundType DetectSound(WaveBuffer buffer, int bytesRecorded, short minVoiceRecordSampleVolume, short minNoiseRecordSampleVolume) 69 | { 70 | if (minVoiceRecordSampleVolume == 0) 71 | return SoundType.VOICE; 72 | if (minNoiseRecordSampleVolume == 0) 73 | return SoundType.NOISE; 74 | 75 | SoundType result = SoundType.NOTHING; 76 | 77 | //check if the volume peaks above the MinRecordVolume 78 | // interpret as 32 bit floating point audio 79 | for (int index = 0; index < bytesRecorded / 4; index++) 80 | { 81 | var sample = buffer.ShortBuffer[index]; 82 | 83 | //Check voice volume threshold 84 | if (sample > minVoiceRecordSampleVolume || sample < -minVoiceRecordSampleVolume) 85 | { 86 | result = SoundType.VOICE; 87 | //skip testing the rest of the sample data as soon as voice volume threshold has been reached 88 | break; 89 | } 90 | //Check noise volume threshold 91 | else if (sample > minNoiseRecordSampleVolume || sample < -minNoiseRecordSampleVolume) 92 | { 93 | result = SoundType.NOISE; 94 | } 95 | } 96 | 97 | return result; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /MumbleGuiClient/Extensions/ConnectionProtocol.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | using MumbleProto; 8 | using MumbleSharp; 9 | using MumbleSharp.Audio; 10 | using MumbleSharp.Audio.Codecs; 11 | using MumbleSharp.Model; 12 | using MumbleSharp.Packets; 13 | using NAudio.Wave; 14 | 15 | namespace MumbleGuiClient 16 | { 17 | public class ConnectionMumbleProtocol : BasicMumbleProtocol 18 | { 19 | public delegate void EncodedVoiceDelegate(BasicMumbleProtocol proto, byte[] data, uint userId, long sequence, IVoiceCodec codec, SpeechTarget target); 20 | public delegate void UserJoinedDelegate(BasicMumbleProtocol proto, User user); 21 | public delegate void UserStateChangedDelegate(BasicMumbleProtocol proto, User user); 22 | public delegate void UserStateChannelChangedDelegate(BasicMumbleProtocol proto, User user, uint oldChannelId); 23 | public delegate void UserLeftDelegate(BasicMumbleProtocol proto, User user); 24 | public delegate void ChannelJoinedDelegate(BasicMumbleProtocol proto, Channel channel); 25 | public delegate void ChannelLeftDelegate(BasicMumbleProtocol proto, Channel channel); 26 | public delegate void ServerConfigDelegate(BasicMumbleProtocol proto, ServerConfig serverConfig); 27 | public delegate void ChannelMessageReceivedDelegate(BasicMumbleProtocol proto, ChannelMessage message); 28 | public delegate void PersonalMessageReceivedDelegate(BasicMumbleProtocol proto, PersonalMessage message); 29 | 30 | public EncodedVoiceDelegate encodedVoice; 31 | public UserJoinedDelegate userJoinedDelegate; 32 | public UserStateChangedDelegate userStateChangedDelegate; 33 | public UserStateChannelChangedDelegate userStateChannelChangedDelegate; 34 | public UserLeftDelegate userLeftDelegate; 35 | public ChannelJoinedDelegate channelJoinedDelegate; 36 | public ChannelLeftDelegate channelLeftDelegate; 37 | public ServerConfigDelegate serverConfigDelegate; 38 | public ChannelMessageReceivedDelegate channelMessageReceivedDelegate; 39 | public PersonalMessageReceivedDelegate personalMessageReceivedDelegate; 40 | 41 | public override void EncodedVoice(byte[] data, uint userId, long sequence, IVoiceCodec codec, SpeechTarget target) 42 | { 43 | if (encodedVoice != null) encodedVoice(this, data, userId, sequence, codec, target); 44 | //User user = Users.FirstOrDefault(u => u.Id == userId); 45 | //if (user != null) 46 | // Console.WriteLine(user.Name + " is speaking. Seq" + sequence); 47 | 48 | base.EncodedVoice(data, userId, sequence, codec, target); 49 | } 50 | 51 | protected override void UserJoined(User user) 52 | { 53 | base.UserJoined(user); 54 | 55 | if (userJoinedDelegate != null) userJoinedDelegate(this, user); 56 | } 57 | 58 | protected override void UserStateChanged(User user) 59 | { 60 | base.UserStateChanged(user); 61 | 62 | if (userStateChangedDelegate != null) userStateChangedDelegate(this, user); 63 | } 64 | 65 | protected override void UserStateChannelChanged(User user, uint oldChannelId) 66 | { 67 | base.UserStateChannelChanged(user, oldChannelId); 68 | 69 | if (userStateChannelChangedDelegate != null) userStateChannelChangedDelegate(this, user, oldChannelId); 70 | } 71 | 72 | protected override void UserLeft(User user) 73 | { 74 | base.UserLeft(user); 75 | 76 | if (userLeftDelegate != null) userLeftDelegate(this, user); 77 | } 78 | 79 | protected override void ChannelJoined(Channel channel) 80 | { 81 | base.ChannelJoined(channel); 82 | 83 | if (channelJoinedDelegate != null) channelJoinedDelegate(this, channel); 84 | } 85 | 86 | protected override void ChannelLeft(Channel channel) 87 | { 88 | base.ChannelLeft(channel); 89 | 90 | if (channelLeftDelegate != null) channelLeftDelegate(this, channel); 91 | } 92 | 93 | public override void ServerConfig(ServerConfig serverConfig) 94 | { 95 | base.ServerConfig(serverConfig); 96 | 97 | if (serverConfigDelegate != null) serverConfigDelegate(this, serverConfig); 98 | } 99 | 100 | protected override void ChannelMessageReceived(ChannelMessage message) 101 | { 102 | if (channelMessageReceivedDelegate != null) channelMessageReceivedDelegate(this, message); 103 | 104 | base.ChannelMessageReceived(message); 105 | } 106 | 107 | protected override void PersonalMessageReceived(PersonalMessage message) 108 | { 109 | if (personalMessageReceivedDelegate != null) personalMessageReceivedDelegate(this, message); 110 | 111 | base.PersonalMessageReceived(message); 112 | } 113 | 114 | public new void SendVoice(ArraySegment pcm, SpeechTarget target, uint targetId) 115 | { 116 | base.SendVoice(pcm,target,targetId); 117 | } 118 | 119 | public new void SendVoiceStop() 120 | { 121 | base.SendVoiceStop(); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /MumbleGuiClient/Extensions/IVoiceDetector.cs: -------------------------------------------------------------------------------- 1 | using NAudio.Wave; 2 | 3 | namespace MumbleGuiClient 4 | { 5 | public interface IVoiceDetector 6 | { 7 | bool VoiceDetected(WaveBuffer waveBuffer, int bytesRecorded); 8 | } 9 | } -------------------------------------------------------------------------------- /MumbleGuiClient/Extensions/MicrophoneRecorder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MumbleSharp; 3 | using NAudio.Wave; 4 | 5 | namespace MumbleGuiClient 6 | { 7 | public class MicrophoneRecorder 8 | { 9 | private readonly IMumbleProtocol _protocol; 10 | 11 | public bool _recording = false; 12 | public double lastPingSendTime; 13 | WaveInEvent sourceStream; 14 | public static int SelectedDevice; 15 | private IVoiceDetector voiceDetector = new BasicVoiceDetector(); 16 | private float _voiceDetectionThresholdy; 17 | public float VoiceDetectionThreshold 18 | { 19 | get 20 | { 21 | return _voiceDetectionThresholdy; 22 | } 23 | set 24 | { 25 | _voiceDetectionThresholdy = value; 26 | ((BasicVoiceDetector)voiceDetector).VoiceDetectionSampleVolume = Convert.ToInt16(short.MaxValue * value); 27 | ((BasicVoiceDetector)voiceDetector).NoiseDetectionSampleVolume = Convert.ToInt16(short.MaxValue * value * 0.8); 28 | } 29 | } 30 | 31 | 32 | public MicrophoneRecorder(IMumbleProtocol protocol) 33 | { 34 | VoiceDetectionThreshold = 0.5f; 35 | _protocol = protocol; 36 | } 37 | 38 | private void VoiceDataAvailable(object sender, WaveInEventArgs e) 39 | { 40 | if (!_recording) 41 | return; 42 | 43 | if (voiceDetector.VoiceDetected(new WaveBuffer(e.Buffer), e.BytesRecorded)) 44 | { 45 | //At the moment we're sending *from* the local user, this is kinda stupid. 46 | //What we really want is to send *to* other users, or to channels. Something like: 47 | // 48 | // _connection.Users.First().SendVoiceWhisper(e.Buffer); 49 | // 50 | // _connection.Channels.First().SendVoice(e.Buffer, shout: true); 51 | 52 | //if (_protocol.LocalUser != null) 53 | // _protocol.LocalUser.SendVoice(new ArraySegment(e.Buffer, 0, e.BytesRecorded)); 54 | 55 | //Send to the channel LocalUser is currently in 56 | if (_protocol.LocalUser != null && _protocol.LocalUser.Channel != null) 57 | { 58 | //_protocol.Connection.SendControl<> 59 | _protocol.LocalUser.Channel.SendVoice(new ArraySegment(e.Buffer, 0, e.BytesRecorded)); 60 | } 61 | 62 | //if (DateTime.Now.TimeOfDay.TotalMilliseconds - lastPingSendTime > 1000 || DateTime.Now.TimeOfDay.TotalMilliseconds < lastPingSendTime) 63 | //{ 64 | // _protocol.Connection.SendVoice 65 | //} 66 | } 67 | } 68 | 69 | 70 | 71 | public void Record() 72 | { 73 | _recording = true; 74 | 75 | if (sourceStream != null) 76 | sourceStream.Dispose(); 77 | sourceStream = new WaveInEvent 78 | { 79 | WaveFormat = new WaveFormat(Constants.DEFAULT_AUDIO_SAMPLE_RATE, Constants.DEFAULT_AUDIO_SAMPLE_BITS, Constants.DEFAULT_AUDIO_SAMPLE_CHANNELS) 80 | }; 81 | sourceStream.BufferMilliseconds = 10; 82 | sourceStream.DeviceNumber = SelectedDevice; 83 | sourceStream.NumberOfBuffers = 3; 84 | sourceStream.DataAvailable += VoiceDataAvailable; 85 | 86 | sourceStream.StartRecording(); 87 | } 88 | 89 | public void Stop() 90 | { 91 | _recording = false; 92 | _protocol.LocalUser?.Channel.SendVoiceStop(); 93 | 94 | sourceStream.StopRecording(); 95 | sourceStream.Dispose(); 96 | sourceStream = null; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /MumbleGuiClient/Extensions/SpeakerPlayback.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using NAudio.Wave; 7 | 8 | namespace MumbleGuiClient 9 | { 10 | public class SpeakerPlayback 11 | { 12 | private static int _selectedDevice = -1; 13 | public static int SelectedDevice { 14 | get { return _selectedDevice; } 15 | set { 16 | _selectedDevice = value; 17 | foreach(var player in _players) 18 | { 19 | player.Value.ChangeDevice(); 20 | } 21 | } 22 | } 23 | private static readonly Dictionary _players = new Dictionary(); 24 | 25 | public static void AddPlayer(uint id, NAudio.Wave.IWaveProvider provider) 26 | { 27 | if (!_players.ContainsKey(id)) 28 | _players.Add(id, new Player(provider)); 29 | } 30 | 31 | public static void RemovePlayer(uint id) 32 | { 33 | if(_players.ContainsKey(id)) 34 | _players[id].Dispose(); 35 | _players.Remove(id); 36 | } 37 | 38 | public static void Play(uint id) 39 | { 40 | if (_players.ContainsKey(id)) 41 | _players[id].Play(); 42 | } 43 | 44 | public static void Stop(uint id) 45 | { 46 | if (_players.ContainsKey(id)) 47 | _players[id].Stop(); 48 | } 49 | 50 | public static void Pause(uint id) 51 | { 52 | if (_players.ContainsKey(id)) 53 | _players[id].Pause(); 54 | } 55 | 56 | private class Player : IDisposable 57 | { 58 | private NAudio.Wave.WaveOut _waveOut; 59 | private readonly NAudio.Wave.IWaveProvider _provider; 60 | 61 | public Player(NAudio.Wave.IWaveProvider provider) 62 | { 63 | _provider = provider; 64 | _waveOut = new NAudio.Wave.WaveOut(); 65 | _waveOut.DeviceNumber = SelectedDevice; 66 | _waveOut.Init(_provider); 67 | Play(); 68 | } 69 | 70 | public void Dispose() 71 | { 72 | Stop(); 73 | _waveOut.Dispose(); 74 | } 75 | 76 | public void Stop() 77 | { 78 | if (_waveOut.PlaybackState != PlaybackState.Stopped) 79 | _waveOut.Stop(); 80 | } 81 | 82 | public void Play() 83 | { 84 | if (_waveOut.PlaybackState != PlaybackState.Playing) 85 | _waveOut.Play(); 86 | } 87 | 88 | public void Pause() 89 | { 90 | if(_waveOut.PlaybackState == PlaybackState.Playing) 91 | _waveOut.Pause(); 92 | } 93 | 94 | public void ChangeDevice() 95 | { 96 | var state = _waveOut.PlaybackState; 97 | var latency = _waveOut.DesiredLatency; 98 | var numberOfBuffers = _waveOut.NumberOfBuffers; 99 | var waveFormat = _waveOut.OutputWaveFormat; 100 | var volume = _waveOut.Volume; 101 | 102 | _waveOut.Dispose(); 103 | _waveOut = new WaveOut(); 104 | _waveOut.DeviceNumber = SelectedDevice; 105 | _waveOut.Init(_provider); 106 | 107 | switch (state) 108 | { 109 | case PlaybackState.Paused: Pause(); break; 110 | case PlaybackState.Playing: Play(); break; 111 | } 112 | } 113 | 114 | } 115 | 116 | 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /MumbleGuiClient/Form1.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 17, 17 122 | 123 | -------------------------------------------------------------------------------- /MumbleGuiClient/MumbleGuiClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net6.0-windows 6 | True 7 | martindevans, Meetsch 8 | MumbleSharp 9 | Windows desktop demo application for the use of the MumbleSharp library. 10 | For more info on MumbleSharp please visit https://github.com/martindevans/MumbleSharp 11 | Copyright © 2022 12 | LICENSE 13 | mumblesharp.png 14 | 15 | https://github.com/martindevans/MumbleSharp 16 | https://github.com/martindevans/MumbleSharp 17 | Git 18 | MumbleSharp Mumble voip voice chat windows desktop demo 19 | 20 | 21 | 22 | 23 | True 24 | 25 | 26 | 27 | True 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | True 43 | True 44 | Resources.resx 45 | 46 | 47 | True 48 | True 49 | Settings.settings 50 | 51 | 52 | 53 | 54 | 55 | ResXFileCodeGenerator 56 | Resources.Designer.cs 57 | 58 | 59 | 60 | 61 | 62 | SettingsSingleFileGenerator 63 | Settings.Designer.cs 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /MumbleGuiClient/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.Windows.Forms; 6 | 7 | namespace MumbleGuiClient 8 | { 9 | static class Program 10 | { 11 | /// 12 | /// The main entry point for the application. 13 | /// 14 | [STAThread] 15 | static void Main() 16 | { 17 | Application.EnableVisualStyles(); 18 | Application.SetCompatibleTextRenderingDefault(false); 19 | Application.Run(new Form1()); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MumbleGuiClient/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.34209 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace MumbleGuiClient.Properties 12 | { 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources 26 | { 27 | 28 | private static global::System.Resources.ResourceManager resourceMan; 29 | 30 | private static global::System.Globalization.CultureInfo resourceCulture; 31 | 32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 33 | internal Resources() 34 | { 35 | } 36 | 37 | /// 38 | /// Returns the cached ResourceManager instance used by this class. 39 | /// 40 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 41 | internal static global::System.Resources.ResourceManager ResourceManager 42 | { 43 | get 44 | { 45 | if ((resourceMan == null)) 46 | { 47 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MumbleGuiClient.Properties.Resources", typeof(Resources).Assembly); 48 | resourceMan = temp; 49 | } 50 | return resourceMan; 51 | } 52 | } 53 | 54 | /// 55 | /// Overrides the current thread's CurrentUICulture property for all 56 | /// resource lookups using this strongly typed resource class. 57 | /// 58 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 59 | internal static global::System.Globalization.CultureInfo Culture 60 | { 61 | get 62 | { 63 | return resourceCulture; 64 | } 65 | set 66 | { 67 | resourceCulture = value; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /MumbleGuiClient/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /MumbleGuiClient/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.34209 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace MumbleGuiClient.Properties 12 | { 13 | 14 | 15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] 17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase 18 | { 19 | 20 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 21 | 22 | public static Settings Default 23 | { 24 | get 25 | { 26 | return defaultInstance; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MumbleGuiClient/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MumbleSharp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29728.190 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MumbleSharp", "MumbleSharp\MumbleSharp.csproj", "{CCF6DB4F-D16F-45F3-920D-E265927BB430}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6F41C145-F1AC-4BD0-B4BA-59B7D389452A}" 9 | ProjectSection(SolutionItems) = preProject 10 | LICENSE = LICENSE 11 | mumble-protocol-1.2.5-alpha.pdf = mumble-protocol-1.2.5-alpha.pdf 12 | mumble-protocol.url = mumble-protocol.url 13 | mumblesharp.ico = mumblesharp.ico 14 | mumblesharp.png = mumblesharp.png 15 | mumblesharp.xcf = mumblesharp.xcf 16 | nuget.pack.bat = nuget.pack.bat 17 | nuget.publish.bat = nuget.publish.bat 18 | Readme.md = Readme.md 19 | EndProjectSection 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{22EE2999-2A63-4C22-9187-0A435E9763D7}" 22 | ProjectSection(SolutionItems) = preProject 23 | .nuget\NuGet.Config = .nuget\NuGet.Config 24 | .nuget\NuGet.exe = .nuget\NuGet.exe 25 | .nuget\NuGet.targets = .nuget\NuGet.targets 26 | EndProjectSection 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MumbleSharpTest", "MumbleSharpTest\MumbleSharpTest.csproj", "{1B8BCBE5-4C2E-4295-ABE8-3F63EB68033A}" 29 | EndProject 30 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MumbleClient", "MumbleClient\MumbleClient.csproj", "{CE9C5569-39A8-498E-A03E-CA4C06D406B8}" 31 | EndProject 32 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MumbleGuiClient", "MumbleGuiClient\MumbleGuiClient.csproj", "{8A9BA968-D7E3-4E2A-8D9E-488486BBE843}" 33 | EndProject 34 | Global 35 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 36 | Debug|Any CPU = Debug|Any CPU 37 | Release|Any CPU = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 40 | {CCF6DB4F-D16F-45F3-920D-E265927BB430}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {CCF6DB4F-D16F-45F3-920D-E265927BB430}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {CCF6DB4F-D16F-45F3-920D-E265927BB430}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {CCF6DB4F-D16F-45F3-920D-E265927BB430}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {1B8BCBE5-4C2E-4295-ABE8-3F63EB68033A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {1B8BCBE5-4C2E-4295-ABE8-3F63EB68033A}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {1B8BCBE5-4C2E-4295-ABE8-3F63EB68033A}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {1B8BCBE5-4C2E-4295-ABE8-3F63EB68033A}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {CE9C5569-39A8-498E-A03E-CA4C06D406B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {CE9C5569-39A8-498E-A03E-CA4C06D406B8}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {CE9C5569-39A8-498E-A03E-CA4C06D406B8}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {CE9C5569-39A8-498E-A03E-CA4C06D406B8}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {8A9BA968-D7E3-4E2A-8D9E-488486BBE843}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {8A9BA968-D7E3-4E2A-8D9E-488486BBE843}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {8A9BA968-D7E3-4E2A-8D9E-488486BBE843}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {8A9BA968-D7E3-4E2A-8D9E-488486BBE843}.Release|Any CPU.Build.0 = Release|Any CPU 56 | EndGlobalSection 57 | GlobalSection(SolutionProperties) = preSolution 58 | HideSolutionNode = FALSE 59 | EndGlobalSection 60 | GlobalSection(ExtensibilityGlobals) = postSolution 61 | SolutionGuid = {24294624-16F2-4049-B905-762FF87FF0A3} 62 | EndGlobalSection 63 | GlobalSection(TestCaseManagementSettings) = postSolution 64 | CategoryFile = MumbleClient.vsmdi 65 | EndGlobalSection 66 | EndGlobal 67 | -------------------------------------------------------------------------------- /MumbleSharp/Audio/AudioDecodingBuffer.cs: -------------------------------------------------------------------------------- 1 | using MumbleSharp.Audio.Codecs; 2 | using NAudio.Wave; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace MumbleSharp.Audio 7 | { 8 | /// 9 | /// Buffers up encoded audio packets and provides a constant stream of sound (silence if there is no more audio to decode) 10 | /// 11 | public class AudioDecodingBuffer 12 | : IWaveProvider 13 | { 14 | private readonly int _sampleRate; 15 | private readonly ushort _frameSize; 16 | public WaveFormat WaveFormat { get; private set; } 17 | private int _decodedOffset; 18 | private int _decodedCount; 19 | private readonly byte[] _decodedBuffer; 20 | 21 | 22 | /// 23 | /// Initializes a new instance of the class which Buffers up encoded audio packets and provides a constant stream of sound (silence if there is no more audio to decode). 24 | /// 25 | /// The sample rate in Hertz (samples per second). 26 | /// The sample bit depth. 27 | /// The sample channels (1 for mono, 2 for stereo). 28 | /// Size of the frame in samples. 29 | public AudioDecodingBuffer(int sampleRate = Constants.DEFAULT_AUDIO_SAMPLE_RATE, byte sampleBits = Constants.DEFAULT_AUDIO_SAMPLE_BITS, byte sampleChannels = Constants.DEFAULT_AUDIO_SAMPLE_CHANNELS, ushort frameSize = Constants.DEFAULT_AUDIO_FRAME_SIZE) 30 | { 31 | WaveFormat = new WaveFormat(sampleRate, sampleBits, sampleChannels); 32 | _sampleRate = sampleRate; 33 | _frameSize = frameSize; 34 | _decodedBuffer = new byte[sampleRate * (sampleBits / 8) * sampleChannels]; 35 | } 36 | 37 | private long _nextSequenceToDecode; 38 | private readonly List _encodedBuffer = new List(); 39 | 40 | private IVoiceCodec _codec; 41 | 42 | /// 43 | /// The time, in milliseconds, for the jitter buffer to delay when network data is exhausted. Only updates internally when jitter is detected. 44 | /// 45 | public TimeSpan JitterDelay { get; set; } = TimeSpan.FromMilliseconds(350f); 46 | 47 | private bool isJitterDetected; 48 | private bool isJitterTimerRunning; 49 | private DateTime jitterTimer = DateTime.UtcNow; 50 | private double jitterMillis = 350f; 51 | 52 | public int Read(byte[] buffer, int offset, int count) 53 | { 54 | int readCount = 0; 55 | 56 | if (isJitterTimerRunning && ((DateTime.UtcNow - jitterTimer).TotalMilliseconds > jitterMillis)) 57 | { 58 | isJitterDetected = false; 59 | isJitterTimerRunning = false; 60 | } 61 | 62 | if (!isJitterDetected) 63 | { 64 | while (readCount < count) 65 | { 66 | readCount += ReadFromBuffer(buffer, offset + readCount, count - readCount); 67 | 68 | if (readCount == 0) 69 | { 70 | isJitterDetected = true; 71 | } 72 | 73 | //Try to decode some more data into the buffer 74 | if (!FillBuffer()) 75 | break; 76 | } 77 | } 78 | 79 | if (readCount == 0) 80 | { 81 | //Return silence 82 | Array.Clear(buffer, 0, count); 83 | return count; 84 | } 85 | 86 | return readCount; 87 | } 88 | 89 | /// 90 | /// Add a new packet of encoded data 91 | /// 92 | /// Sequence number of this packet 93 | /// The encoded audio packet 94 | /// The codec to use to decode this packet 95 | public void AddEncodedPacket(long sequence, byte[] data, IVoiceCodec codec) 96 | { 97 | if (isJitterDetected && !isJitterTimerRunning) 98 | { 99 | jitterTimer = DateTime.UtcNow; 100 | jitterMillis = JitterDelay.TotalMilliseconds; 101 | isJitterTimerRunning = true; 102 | } 103 | 104 | if (sequence == 0) 105 | _nextSequenceToDecode = 0; 106 | 107 | if (_codec == null) 108 | _codec = codec; 109 | else if (_codec != null && _codec != codec) 110 | ChangeCodec(codec); 111 | 112 | //If the next seq we expect to decode comes after this packet we've already missed our opportunity! 113 | if (_nextSequenceToDecode > sequence) 114 | return; 115 | 116 | _encodedBuffer.Add(new BufferPacket 117 | { 118 | Data = data, 119 | Sequence = sequence 120 | }); 121 | } 122 | 123 | private void ChangeCodec(IVoiceCodec codec) 124 | { 125 | //Decode all buffered packets using current codec 126 | while (_encodedBuffer.Count > 0) 127 | FillBuffer(); 128 | 129 | _codec = codec; 130 | } 131 | 132 | private BufferPacket? GetNextEncodedData() 133 | { 134 | if (_encodedBuffer.Count == 0) 135 | return null; 136 | 137 | int minIndex = 0; 138 | for (int i = 1; i < _encodedBuffer.Count; i++) 139 | minIndex = _encodedBuffer[minIndex].Sequence < _encodedBuffer[i].Sequence ? minIndex : i; 140 | 141 | var packet = _encodedBuffer[minIndex]; 142 | _encodedBuffer.RemoveAt(minIndex); 143 | 144 | return packet; 145 | } 146 | 147 | /// 148 | /// Read data that has already been decoded 149 | /// 150 | /// 151 | /// 152 | /// 153 | /// 154 | private int ReadFromBuffer(byte[] dst, int offset, int count) 155 | { 156 | //Copy as much data as we can from the buffer up to the limit 157 | int readCount = Math.Min(count, _decodedCount); 158 | Array.Copy(_decodedBuffer, _decodedOffset, dst, offset, readCount); 159 | _decodedCount -= readCount; 160 | _decodedOffset += readCount; 161 | 162 | //When the buffer is emptied, put the start offset back to index 0 163 | if (_decodedCount == 0) 164 | _decodedOffset = 0; 165 | 166 | //If the offset is nearing the end of the buffer then copy the data back to offset 0 167 | if ((_decodedOffset > _decodedCount) && (_decodedOffset + _decodedCount) > _decodedBuffer.Length * 0.9) 168 | Buffer.BlockCopy(_decodedBuffer, _decodedOffset, _decodedBuffer, 0, _decodedCount); 169 | 170 | return readCount; 171 | } 172 | 173 | /// 174 | /// Decoded data into the buffer 175 | /// 176 | /// 177 | private bool FillBuffer() 178 | { 179 | var packet = GetNextEncodedData(); 180 | if (!packet.HasValue) 181 | return false; 182 | 183 | ////todo: _nextSequenceToDecode calculation is wrong, which causes this to happen for almost every packet! 184 | ////Decode a null to indicate a dropped packet 185 | //if (packet.Value.Sequence != _nextSequenceToDecode) 186 | // _codec.Decode(null); 187 | 188 | var d = _codec.Decode(packet.Value.Data); 189 | _nextSequenceToDecode = packet.Value.Sequence + d.Length / (_sampleRate / _frameSize); 190 | 191 | Array.Copy(d, 0, _decodedBuffer, _decodedOffset, d.Length); 192 | _decodedCount += d.Length; 193 | return true; 194 | } 195 | 196 | private struct BufferPacket 197 | { 198 | public byte[] Data; 199 | public long Sequence; 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /MumbleSharp/Audio/AudioEncodingBuffer.cs: -------------------------------------------------------------------------------- 1 | using MumbleSharp.Audio.Codecs; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.Linq; 5 | 6 | namespace MumbleSharp.Audio 7 | { 8 | public class AudioEncodingBuffer 9 | { 10 | private readonly BlockingCollection _unencodedBuffer = new BlockingCollection(new ConcurrentQueue()); 11 | 12 | private readonly CodecSet _codecs; 13 | 14 | private SpeechTarget _target; 15 | private uint _targetId; 16 | private readonly DynamicCircularBuffer _pcmBuffer = new DynamicCircularBuffer(); 17 | 18 | private TargettedSpeech? _unencodedItem; 19 | 20 | /// 21 | /// Initializes a new instance of the class. 22 | /// 23 | /// The sample rate in Hertz (samples per second). 24 | /// The sample bit depth. 25 | /// The sample channels (1 for mono, 2 for stereo). 26 | /// Size of the frame in samples. 27 | public AudioEncodingBuffer(int sampleRate = Constants.DEFAULT_AUDIO_SAMPLE_RATE, byte sampleBits = Constants.DEFAULT_AUDIO_SAMPLE_BITS, byte sampleChannels = Constants.DEFAULT_AUDIO_SAMPLE_CHANNELS, ushort frameSize = Constants.DEFAULT_AUDIO_FRAME_SIZE) 28 | { 29 | _codecs = new CodecSet(sampleRate, sampleBits, sampleChannels, frameSize); 30 | } 31 | 32 | /// 33 | /// Add some raw PCM data to the buffer to send 34 | /// 35 | /// 36 | /// 37 | /// 38 | public void Add(ArraySegment pcm, SpeechTarget target, uint targetId) 39 | { 40 | _unencodedBuffer.Add(new TargettedSpeech(pcm, target, targetId)); 41 | } 42 | 43 | public void Stop() 44 | { 45 | _unencodedBuffer.Add(new TargettedSpeech(stop: true)); 46 | } 47 | 48 | public EncodedTargettedSpeech? Encode(SpeechCodecs codec) 49 | { 50 | //Get the codec 51 | var codecInstance = _codecs.GetCodec(codec); 52 | 53 | //How many bytes can we fit into the larget frame? 54 | var maxBytes = codecInstance.PermittedEncodingFrameSizes.Max() * sizeof(ushort); 55 | 56 | bool stopped = false; 57 | 58 | //If we have an unencoded item stored here it's because a previous iteration pulled from the queue and discovered it could not process this packet (different target) 59 | if (_unencodedItem.HasValue && TryAddToEncodingBuffer(_unencodedItem.Value, out stopped)) 60 | { 61 | _unencodedItem = null; 62 | } 63 | 64 | if (stopped) 65 | { 66 | //remove stop packet 67 | TargettedSpeech item; 68 | _unencodedBuffer.TryTake(out item, TimeSpan.FromMilliseconds(1)); 69 | _unencodedItem = null; 70 | } 71 | 72 | //Accumulate as many bytes as we can stuff into a single frame 73 | while (_pcmBuffer.Count < maxBytes && !stopped) 74 | { 75 | TargettedSpeech item; 76 | if (!_unencodedBuffer.TryTake(out item, TimeSpan.FromMilliseconds(1))) 77 | break; 78 | 79 | //Add this packet to the encoding buffer, or stop accumulating bytes 80 | if (!TryAddToEncodingBuffer(item, out stopped)) 81 | { 82 | _unencodedItem = item; 83 | break; 84 | } 85 | } 86 | 87 | //Nothing to encode, early exit 88 | if (_pcmBuffer.Count == 0) 89 | return null; 90 | 91 | if (stopped) 92 | { 93 | //User has stopped talking, pad buffer up to next buffer size with silence 94 | var frameBytes = codecInstance.PermittedEncodingFrameSizes.Select(f => f * sizeof(ushort)).Where(f => f >= _pcmBuffer.Count).Min(); 95 | byte[] b = new byte[frameBytes]; 96 | int read = _pcmBuffer.Read(new ArraySegment(b)); 97 | 98 | return new EncodedTargettedSpeech( 99 | codecInstance.Encode(new ArraySegment(b, 0, read)), 100 | _target, 101 | _targetId); 102 | } 103 | else 104 | { 105 | //We have a load of bytes of PCM data, let's encode them 106 | var frameBytesList = codecInstance.PermittedEncodingFrameSizes.Select(f => f * sizeof(ushort)).Where(f => f <= _pcmBuffer.Count); 107 | if (frameBytesList.Count() > 0) 108 | { 109 | var frameBytes = frameBytesList.Max(); 110 | byte[] b = new byte[frameBytes]; 111 | int read = _pcmBuffer.Read(new ArraySegment(b)); 112 | 113 | return new EncodedTargettedSpeech( 114 | codecInstance.Encode(new ArraySegment(b, 0, read)), 115 | _target, 116 | _targetId); 117 | } 118 | else return null; 119 | } 120 | } 121 | 122 | private bool TryAddToEncodingBuffer(TargettedSpeech t, out bool stopped) 123 | { 124 | if (t.IsStop) 125 | { 126 | stopped = true; 127 | return false; 128 | } 129 | stopped = false; 130 | 131 | if (!(_pcmBuffer.Count == 0 || (_target == t.Target && _targetId == t.TargetId))) 132 | return false; 133 | 134 | _pcmBuffer.Write(t.Pcm); 135 | 136 | _target = t.Target; 137 | _targetId = t.TargetId; 138 | 139 | return true; 140 | } 141 | 142 | public struct EncodedTargettedSpeech 143 | { 144 | public readonly byte[] EncodedPcm; 145 | public readonly SpeechTarget Target; 146 | public readonly uint TargetId; 147 | 148 | public EncodedTargettedSpeech(byte[] encodedPcm, SpeechTarget target, uint targetId) 149 | { 150 | TargetId = targetId; 151 | Target = target; 152 | EncodedPcm = encodedPcm; 153 | } 154 | } 155 | 156 | /// 157 | /// PCM data targetted at a specific person 158 | /// 159 | private struct TargettedSpeech 160 | { 161 | public readonly ArraySegment Pcm; 162 | public readonly SpeechTarget Target; 163 | public readonly uint TargetId; 164 | 165 | public readonly bool IsStop; 166 | 167 | public TargettedSpeech(ArraySegment pcm, SpeechTarget target, uint targetId) 168 | { 169 | TargetId = targetId; 170 | Target = target; 171 | Pcm = pcm; 172 | 173 | IsStop = false; 174 | } 175 | 176 | public TargettedSpeech(bool stop = true) 177 | { 178 | IsStop = stop; 179 | 180 | Pcm = default(ArraySegment); 181 | Target = SpeechTarget.Normal; 182 | TargetId = 0; 183 | } 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /MumbleSharp/Audio/CodecSet.cs: -------------------------------------------------------------------------------- 1 | using MumbleSharp.Audio.Codecs; 2 | using MumbleSharp.Audio.Codecs.CeltAlpha; 3 | using MumbleSharp.Audio.Codecs.CeltBeta; 4 | using MumbleSharp.Audio.Codecs.Opus; 5 | using MumbleSharp.Audio.Codecs.Speex; 6 | using System; 7 | 8 | namespace MumbleSharp.Audio 9 | { 10 | public class CodecSet 11 | { 12 | private readonly Lazy _alpha; 13 | private readonly Lazy _beta; 14 | private readonly Lazy _speex; 15 | private readonly Lazy _opus; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The sample rate in Hertz (samples per second). 21 | /// The sample bit depth. 22 | /// The sample channels (1 for mono, 2 for stereo). 23 | /// Size of the frame in samples. 24 | public CodecSet(int sampleRate = Constants.DEFAULT_AUDIO_SAMPLE_RATE, byte sampleBits = Constants.DEFAULT_AUDIO_SAMPLE_BITS, byte sampleChannels = Constants.DEFAULT_AUDIO_SAMPLE_CHANNELS, ushort frameSize = Constants.DEFAULT_AUDIO_FRAME_SIZE) 25 | { 26 | _alpha = new Lazy(); 27 | _beta = new Lazy(); 28 | _speex = new Lazy(); 29 | _opus = new Lazy(() => new OpusCodec(sampleRate, sampleBits, sampleChannels, frameSize)); 30 | } 31 | 32 | protected internal IVoiceCodec GetCodec(SpeechCodecs codec) 33 | { 34 | switch (codec) 35 | { 36 | case SpeechCodecs.CeltAlpha: 37 | return _alpha.Value; 38 | case SpeechCodecs.Speex: 39 | return _speex.Value; 40 | case SpeechCodecs.CeltBeta: 41 | return _beta.Value; 42 | case SpeechCodecs.Opus: 43 | return _opus.Value; 44 | default: 45 | throw new ArgumentOutOfRangeException("codec"); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /MumbleSharp/Audio/Codecs/CeltAlpha/CeltAlphaCodec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MumbleSharp.Audio.Codecs.CeltAlpha 4 | { 5 | public class CeltBetaCodec 6 | : IVoiceCodec 7 | { 8 | public byte[] Decode(byte[] encodedData) 9 | { 10 | throw new NotImplementedException(); 11 | } 12 | 13 | public System.Collections.Generic.IEnumerable PermittedEncodingFrameSizes 14 | { 15 | get { throw new NotImplementedException(); } 16 | } 17 | 18 | public byte[] Encode(ArraySegment pcm) 19 | { 20 | throw new NotImplementedException(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MumbleSharp/Audio/Codecs/CeltBeta/CeltBetaCodec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MumbleSharp.Audio.Codecs.CeltBeta 4 | { 5 | public class CeltAlphaCodec 6 | : IVoiceCodec 7 | { 8 | public byte[] Decode(byte[] encodedData) 9 | { 10 | throw new NotImplementedException(); 11 | } 12 | 13 | public System.Collections.Generic.IEnumerable PermittedEncodingFrameSizes 14 | { 15 | get { throw new NotImplementedException(); } 16 | } 17 | 18 | public byte[] Encode(ArraySegment pcm) 19 | { 20 | throw new NotImplementedException(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MumbleSharp/Audio/Codecs/IVoiceCodec.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace MumbleSharp.Audio.Codecs 6 | { 7 | public interface IVoiceCodec 8 | { 9 | /// 10 | /// decode the given frame of encoded data into 16 bit PCM 11 | /// 12 | /// 13 | /// 14 | byte[] Decode(byte[] encodedData); 15 | 16 | /// 17 | /// The set of allowed frame sizes for encoding 18 | /// 19 | IEnumerable PermittedEncodingFrameSizes { get; } 20 | 21 | /// 22 | /// Encode a frame of data (must be a permitted size) 23 | /// 24 | /// 25 | /// 26 | byte[] Encode(ArraySegment pcm); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MumbleSharp/Audio/Codecs/Opus/Application.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Author: John Carruthers (johnc@frag-labs.com) 3 | // 4 | // Copyright (C) 2013 John Carruthers 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | namespace MumbleSharp.Audio.Codecs.Opus 26 | { 27 | /// 28 | /// Supported coding modes. 29 | /// 30 | public enum Application 31 | { 32 | /// 33 | /// Best for most VoIP/videoconference applications where listening quality and intelligibility matter most. 34 | /// 35 | Voip = 2048, 36 | /// 37 | /// Best for broadcast/high-fidelity application where the decoded audio should be as close as possible to input. 38 | /// 39 | Audio = 2049, 40 | /// 41 | /// Only use when lowest-achievable latency is what matters most. Voice-optimized modes cannot be used. 42 | /// 43 | RestrictedLowLatency = 2051 44 | } 45 | } -------------------------------------------------------------------------------- /MumbleSharp/Audio/Codecs/Opus/Libs/32bit/opus.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martindevans/MumbleSharp/739568c812852c5a5652dc71b77e848ace7ae7f9/MumbleSharp/Audio/Codecs/Opus/Libs/32bit/opus.dll -------------------------------------------------------------------------------- /MumbleSharp/Audio/Codecs/Opus/Libs/64bit/opus.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martindevans/MumbleSharp/739568c812852c5a5652dc71b77e848ace7ae7f9/MumbleSharp/Audio/Codecs/Opus/Libs/64bit/opus.dll -------------------------------------------------------------------------------- /MumbleSharp/Audio/Codecs/Opus/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Author: John Carruthers (johnc@frag-labs.com) 3 | // 4 | // Copyright (C) 2013 John Carruthers 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | using System; 27 | using System.IO; 28 | using System.Reflection; 29 | using System.Runtime.InteropServices; 30 | 31 | namespace MumbleSharp.Audio.Codecs.Opus 32 | { 33 | /// 34 | /// Wraps the Opus API. 35 | /// 36 | internal class NativeMethods 37 | { 38 | static NativeMethods() 39 | { 40 | IntPtr image; 41 | if (PlatformDetails.IsMac) 42 | { 43 | image = LibraryLoader.Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Audio", "Codecs", "Opus", "Libs", "32bit", "libopus.dylib")); 44 | } 45 | else if (PlatformDetails.IsWindows) 46 | { 47 | if (!Environment.Is64BitProcess) 48 | image = LibraryLoader.Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Audio", "Codecs", "Opus", "Libs", "32bit", "opus.dll")); 49 | else 50 | image = LibraryLoader.Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Audio", "Codecs", "Opus", "Libs", "64bit", "opus.dll")); 51 | } 52 | else 53 | { 54 | image = LibraryLoader.Load("libopus.so.0"); 55 | if (image.Equals(IntPtr.Zero)) 56 | image = LibraryLoader.Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Audio", "Codecs", "Opus", "Libs", "libopus.so")); 57 | } 58 | 59 | if (image != IntPtr.Zero) 60 | { 61 | var type = typeof(NativeMethods); 62 | foreach (var member in type.GetFields(BindingFlags.Static | BindingFlags.NonPublic)) 63 | { 64 | var methodName = member.Name; 65 | if (methodName == "opus_encoder_ctl_out") methodName = "opus_encoder_ctl"; 66 | var fieldType = member.FieldType; 67 | var ptr = LibraryLoader.ResolveSymbol(image, methodName); 68 | if (ptr == IntPtr.Zero) 69 | throw new Exception(string.Format("Could not resolve symbol \"{0}\"", methodName)); 70 | member.SetValue(null, Marshal.GetDelegateForFunctionPointer(ptr, fieldType)); 71 | } 72 | } 73 | } 74 | 75 | // ReSharper disable InconsistentNaming 76 | // ReSharper disable UnassignedField.Compiler 77 | 78 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 79 | internal delegate IntPtr opus_encoder_create_delegate(int sampleRate, int channelCount, int application, out IntPtr error); 80 | internal static opus_encoder_create_delegate opus_encoder_create; 81 | 82 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 83 | internal delegate void opus_encoder_destroy_delegate(IntPtr encoder); 84 | internal static opus_encoder_destroy_delegate opus_encoder_destroy; 85 | 86 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 87 | internal delegate int opus_encode_delegate(IntPtr encoder, IntPtr pcm, int frameSize, IntPtr data, int maxDataBytes); 88 | internal static opus_encode_delegate opus_encode; 89 | 90 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 91 | internal delegate IntPtr opus_decoder_create_delegate(int sampleRate, int channelCount, out IntPtr error); 92 | internal static opus_decoder_create_delegate opus_decoder_create; 93 | 94 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 95 | internal delegate void opus_decoder_destroy_delegate(IntPtr decoder); 96 | internal static opus_decoder_destroy_delegate opus_decoder_destroy; 97 | 98 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 99 | internal delegate int opus_decode_delegate(IntPtr decoder, IntPtr data, int len, IntPtr pcm, int frameSize, int decodeFec); 100 | internal static opus_decode_delegate opus_decode; 101 | 102 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 103 | internal delegate int opus_packet_get_nb_channels_delegate(IntPtr data); 104 | internal static opus_packet_get_nb_channels_delegate opus_packet_get_nb_channels; 105 | 106 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 107 | internal delegate int opus_packet_get_nb_samples_delegate(IntPtr data, int len, int sampleRate); 108 | internal static opus_packet_get_nb_samples_delegate opus_packet_get_nb_samples; 109 | 110 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 111 | internal delegate int opus_encoder_ctl_delegate(IntPtr encoder, Ctl request, int value); 112 | internal static opus_encoder_ctl_delegate opus_encoder_ctl; 113 | 114 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 115 | internal delegate int opus_encoder_ctl_out_delegate(IntPtr encoder, Ctl request, out int value); 116 | internal static opus_encoder_ctl_out_delegate opus_encoder_ctl_out; 117 | 118 | // ReSharper restore UnassignedField.Compiler 119 | // ReSharper restore InconsistentNaming 120 | 121 | public enum Ctl 122 | { 123 | SetBitrateRequest = 4002, 124 | GetBitrateRequest = 4003, 125 | SetInbandFecRequest = 4012, 126 | GetInbandFecRequest = 4013 127 | } 128 | 129 | public enum OpusErrors 130 | { 131 | Ok = 0, 132 | BadArgument = -1, 133 | BufferToSmall = -2, 134 | InternalError = -3, 135 | InvalidPacket = -4, 136 | NotImplemented = -5, 137 | InvalidState = -6, 138 | AllocFail = -7 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /MumbleSharp/Audio/Codecs/Opus/OpusCodec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace MumbleSharp.Audio.Codecs.Opus 5 | { 6 | public class OpusCodec 7 | : IVoiceCodec 8 | { 9 | private readonly OpusDecoder _decoder; 10 | private readonly OpusEncoder _encoder; 11 | private readonly int _sampleRate; 12 | private readonly ushort _frameSize; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The sample rate in Hertz (samples per second). 18 | /// The sample bit depth. 19 | /// The sample channels (1 for mono, 2 for stereo). 20 | /// Size of the frame in samples. 21 | public OpusCodec(int sampleRate = Constants.DEFAULT_AUDIO_SAMPLE_RATE, byte sampleBits = Constants.DEFAULT_AUDIO_SAMPLE_BITS, byte channels = Constants.DEFAULT_AUDIO_SAMPLE_CHANNELS, ushort frameSize = Constants.DEFAULT_AUDIO_FRAME_SIZE) 22 | { 23 | _sampleRate = sampleRate; 24 | _frameSize = frameSize; 25 | _decoder = new OpusDecoder(sampleRate, channels) { EnableForwardErrorCorrection = true }; 26 | _encoder = new OpusEncoder(sampleRate, channels) { EnableForwardErrorCorrection = true }; 27 | } 28 | 29 | public byte[] Decode(byte[] encodedData) 30 | { 31 | if (encodedData == null) 32 | { 33 | _decoder.Decode(null, 0, 0, new byte[_sampleRate / _frameSize], 0); 34 | return null; 35 | } 36 | 37 | int samples = OpusDecoder.GetSamples(encodedData, 0, encodedData.Length, _sampleRate); 38 | if (samples < 1) 39 | return null; 40 | 41 | byte[] dst = new byte[samples * sizeof(ushort)]; 42 | int length = _decoder.Decode(encodedData, 0, encodedData.Length, dst, 0); 43 | if (dst.Length != length) 44 | Array.Resize(ref dst, length); 45 | return dst; 46 | } 47 | 48 | public IEnumerable PermittedEncodingFrameSizes 49 | { 50 | get 51 | { 52 | return _encoder.PermittedFrameSizes; 53 | } 54 | } 55 | 56 | public byte[] Encode(ArraySegment pcm) 57 | { 58 | var samples = pcm.Count / sizeof(ushort); 59 | var numberOfBytes = _encoder.FrameSizeInBytes(samples); 60 | 61 | byte[] dst = new byte[numberOfBytes]; 62 | int encodedBytes = _encoder.Encode(pcm.Array, pcm.Offset, dst, 0, samples); 63 | 64 | //without it packet will have huge zero-value-tale 65 | Array.Resize(ref dst, encodedBytes); 66 | 67 | return dst; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /MumbleSharp/Audio/Codecs/Opus/OpusDecoder.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Author: John Carruthers (johnc@frag-labs.com) 3 | // 4 | // Copyright (C) 2013 John Carruthers 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | using System; 27 | 28 | namespace MumbleSharp.Audio.Codecs.Opus 29 | { 30 | /// 31 | /// Opus decoder. 32 | /// 33 | public class OpusDecoder 34 | : IDisposable 35 | { 36 | /// 37 | /// Opus decoder. 38 | /// 39 | private IntPtr _decoder; 40 | 41 | /// 42 | /// Size of a sample, in bytes. 43 | /// 44 | private readonly int _sampleSize; 45 | 46 | /// 47 | /// Gets or sets if Forward Error Correction decoding is enabled. 48 | /// 49 | public bool EnableForwardErrorCorrection { get; set; } 50 | 51 | public OpusDecoder(int outputSampleRate, int outputChannelCount) 52 | { 53 | if (outputSampleRate != 8000 && 54 | outputSampleRate != 12000 && 55 | outputSampleRate != 16000 && 56 | outputSampleRate != 24000 && 57 | outputSampleRate != 48000) 58 | throw new ArgumentOutOfRangeException("outputSampleRate"); 59 | if (outputChannelCount != 1 && outputChannelCount != 2) 60 | throw new ArgumentOutOfRangeException("outputChannelCount"); 61 | 62 | IntPtr error; 63 | _decoder = NativeMethods.opus_decoder_create(outputSampleRate, outputChannelCount, out error); 64 | if ((NativeMethods.OpusErrors)error != NativeMethods.OpusErrors.Ok) 65 | throw new Exception(string.Format("Exception occured while creating decoder, {0}", ((NativeMethods.OpusErrors)error))); 66 | _sampleSize = sizeof(ushort) * outputChannelCount; 67 | } 68 | 69 | ~OpusDecoder() 70 | { 71 | Dispose(); 72 | } 73 | 74 | /// 75 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 76 | /// 77 | /// 2 78 | public void Dispose() 79 | { 80 | if (_decoder != IntPtr.Zero) 81 | { 82 | NativeMethods.opus_decoder_destroy(_decoder); 83 | _decoder = IntPtr.Zero; 84 | } 85 | } 86 | 87 | /// 88 | /// Decodes audio samples. 89 | /// 90 | /// Encoded data. 91 | /// The zero-based byte offset in srcEncodedBuffer at which to begin reading encoded data. 92 | /// The number of bytes to read from srcEncodedBuffer. 93 | /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values starting at offset replaced with audio samples. 94 | /// The zero-based byte offset in dstBuffer at which to begin writing decoded audio samples. 95 | /// The number of bytes decoded and written to dstBuffer. 96 | /// Set srcEncodedBuffer to null to instruct the decoder that a packet was dropped. 97 | public unsafe int Decode(byte[] srcEncodedBuffer, int srcOffset, int srcLength, byte[] dstBuffer, int dstOffset) 98 | { 99 | var availableBytes = dstBuffer.Length - dstOffset; 100 | var frameCount = availableBytes / _sampleSize; 101 | int length; 102 | fixed (byte* bdec = dstBuffer) 103 | { 104 | var decodedPtr = IntPtr.Add(new IntPtr(bdec), dstOffset); 105 | if (srcEncodedBuffer != null) 106 | { 107 | fixed (byte* bsrc = srcEncodedBuffer) 108 | { 109 | var srcPtr = IntPtr.Add(new IntPtr(bsrc), srcOffset); 110 | length = NativeMethods.opus_decode(_decoder, srcPtr, srcLength, decodedPtr, frameCount, 0); 111 | } 112 | } 113 | else 114 | { 115 | length = NativeMethods.opus_decode(_decoder, IntPtr.Zero, 0, decodedPtr, frameCount, Convert.ToInt32(EnableForwardErrorCorrection)); 116 | } 117 | } 118 | if (length < 0) 119 | throw new Exception("Decoding failed - " + ((NativeMethods.OpusErrors)length)); 120 | return length * _sampleSize; 121 | } 122 | 123 | public static unsafe int GetSamples(byte[] srcEncodedBuffer, int srcOffset, int srcLength, int sampleRate) 124 | { 125 | fixed (byte* bsrc = srcEncodedBuffer) 126 | { 127 | var srcPtr = IntPtr.Add(new IntPtr(bsrc), srcOffset); 128 | return NativeMethods.opus_packet_get_nb_samples(srcPtr, srcLength, sampleRate); 129 | } 130 | } 131 | 132 | public static unsafe int GetChannels(byte[] srcEncodedBuffer, int srcOffset) 133 | { 134 | fixed (byte* bsrc = srcEncodedBuffer) 135 | { 136 | var srcPtr = IntPtr.Add(new IntPtr(bsrc), srcOffset); 137 | return NativeMethods.opus_packet_get_nb_channels(srcPtr); 138 | } 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /MumbleSharp/Audio/Codecs/Opus/OpusEncoder.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Author: John Carruthers (johnc@frag-labs.com) 3 | // 4 | // Copyright (C) 2013 John Carruthers 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | using System; 27 | using System.Linq; 28 | 29 | namespace MumbleSharp.Audio.Codecs.Opus 30 | { 31 | /// 32 | /// Opus encoder. 33 | /// 34 | public class OpusEncoder 35 | : IDisposable 36 | { 37 | /// 38 | /// Opus encoder. 39 | /// 40 | private IntPtr _encoder; 41 | 42 | /// 43 | /// Size of each sample in bytes. 44 | /// 45 | private readonly int _sampleSize; 46 | 47 | /// 48 | /// Permitted frame sizes in ms. 49 | /// 50 | private readonly float[] _permittedFrameSizesInMillisec = { 51 | 2.5f, 5, 10, 52 | 20, 40, 60 53 | }; 54 | 55 | /// 56 | /// Creates a new Opus encoder. 57 | /// 58 | /// The sampling rate of the input stream. 59 | /// The number of channels in the input stream. 60 | public OpusEncoder(int srcSamplingRate, int srcChannelCount) 61 | { 62 | if (srcSamplingRate != 8000 && 63 | srcSamplingRate != 12000 && 64 | srcSamplingRate != 16000 && 65 | srcSamplingRate != 24000 && 66 | srcSamplingRate != 48000) 67 | throw new ArgumentOutOfRangeException("srcSamplingRate"); 68 | if (srcChannelCount != 1 && srcChannelCount != 2) 69 | throw new ArgumentOutOfRangeException("srcChannelCount"); 70 | 71 | IntPtr error; 72 | var encoder = NativeMethods.opus_encoder_create(srcSamplingRate, srcChannelCount, (int)Application.Voip, out error); 73 | if ((NativeMethods.OpusErrors)error != NativeMethods.OpusErrors.Ok) 74 | { 75 | throw new Exception("Exception occured while creating encoder"); 76 | } 77 | _encoder = encoder; 78 | 79 | const int BIT_DEPTH = 16; 80 | _sampleSize = SampleSize(BIT_DEPTH, srcChannelCount); 81 | 82 | PermittedFrameSizes = new int[_permittedFrameSizesInMillisec.Length]; 83 | for (var i = 0; i < _permittedFrameSizesInMillisec.Length; i++) 84 | PermittedFrameSizes[i] = (int)(srcSamplingRate / 1000f * _permittedFrameSizesInMillisec[i]); 85 | } 86 | 87 | private static int SampleSize(int bitDepth, int channelCount) 88 | { 89 | return (bitDepth / 8) * channelCount; 90 | } 91 | 92 | ~OpusEncoder() 93 | { 94 | Dispose(); 95 | } 96 | 97 | /// 98 | /// Encode audio samples. 99 | /// 100 | /// PCM samples to be encoded. 101 | /// The zero-based byte offset in srcPcmSamples at which to begin reading PCM samples. 102 | /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values starting at offset replaced with encoded audio data. 103 | /// The zero-based byte offset in dstOutputBuffer at which to begin writing encoded audio. 104 | /// The number of samples, per channel, to encode. 105 | /// The total number of bytes written to dstOutputBuffer. 106 | public unsafe int Encode(byte[] srcPcmSamples, int srcOffset, byte[] dstOutputBuffer, int dstOffset, int sampleCount) 107 | { 108 | if (srcPcmSamples == null) throw new ArgumentNullException("srcPcmSamples"); 109 | if (dstOutputBuffer == null) throw new ArgumentNullException("dstOutputBuffer"); 110 | if (!PermittedFrameSizes.Contains(sampleCount)) 111 | throw new Exception("Frame size is not permitted"); 112 | var readSize = _sampleSize*sampleCount; 113 | if (srcOffset + readSize > srcPcmSamples.Length) 114 | throw new Exception("Not enough samples in source"); 115 | var maxSizeBytes = dstOutputBuffer.Length - dstOffset; 116 | int encodedLen; 117 | fixed (byte* benc = dstOutputBuffer) 118 | { 119 | fixed (byte* bsrc = srcPcmSamples) 120 | { 121 | var encodedPtr = IntPtr.Add(new IntPtr(benc), dstOffset); 122 | var pcmPtr = IntPtr.Add(new IntPtr(bsrc), srcOffset); 123 | encodedLen = NativeMethods.opus_encode(_encoder, pcmPtr, sampleCount, encodedPtr, maxSizeBytes); 124 | } 125 | } 126 | if (encodedLen < 0) 127 | throw new Exception("Encoding failed - " + ((NativeMethods.OpusErrors)encodedLen)); 128 | return encodedLen; 129 | } 130 | 131 | /// 132 | /// Calculates the size of a frame in bytes. 133 | /// 134 | /// Size of the frame in samples per channel. 135 | /// The size of a frame in bytes. 136 | public int FrameSizeInBytes(int frameSizeInSamples) 137 | { 138 | return frameSizeInSamples*_sampleSize; 139 | } 140 | 141 | /// 142 | /// Permitted frame sizes in samples per channel. 143 | /// 144 | public int[] PermittedFrameSizes { get; private set; } 145 | 146 | /// 147 | /// Gets or sets the bitrate setting of the encoding. 148 | /// 149 | public int Bitrate 150 | { 151 | get 152 | { 153 | if (_encoder == IntPtr.Zero) 154 | throw new ObjectDisposedException("OpusEncoder"); 155 | int bitrate; 156 | var ret = NativeMethods.opus_encoder_ctl_out(_encoder, NativeMethods.Ctl.GetBitrateRequest, out bitrate); 157 | if (ret < 0) 158 | throw new Exception("Encoder error - " + ((NativeMethods.OpusErrors)ret)); 159 | return bitrate; 160 | } 161 | set 162 | { 163 | if (_encoder == IntPtr.Zero) 164 | throw new ObjectDisposedException("OpusEncoder"); 165 | var ret = NativeMethods.opus_encoder_ctl(_encoder, NativeMethods.Ctl.SetBitrateRequest, value); 166 | if (ret < 0) 167 | throw new Exception("Encoder error - " + ((NativeMethods.OpusErrors)ret)); 168 | } 169 | } 170 | 171 | /// 172 | /// Gets or sets if Forward Error Correction encoding is enabled. 173 | /// 174 | public bool EnableForwardErrorCorrection 175 | { 176 | get 177 | { 178 | if (_encoder == IntPtr.Zero) 179 | throw new ObjectDisposedException("OpusEncoder"); 180 | int fec; 181 | var ret = NativeMethods.opus_encoder_ctl_out(_encoder, NativeMethods.Ctl.GetInbandFecRequest, out fec); 182 | if (ret < 0) 183 | throw new Exception("Encoder error - " + ((NativeMethods.OpusErrors)ret)); 184 | return fec > 0; 185 | } 186 | set 187 | { 188 | if (_encoder == IntPtr.Zero) 189 | throw new ObjectDisposedException("OpusEncoder"); 190 | var ret = NativeMethods.opus_encoder_ctl(_encoder, NativeMethods.Ctl.SetInbandFecRequest, Convert.ToInt32(value)); 191 | if (ret < 0) 192 | throw new Exception("Encoder error - " + ((NativeMethods.OpusErrors)ret)); 193 | } 194 | } 195 | 196 | /// 197 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 198 | /// 199 | /// 2 200 | public void Dispose() 201 | { 202 | if (_encoder != IntPtr.Zero) 203 | { 204 | NativeMethods.opus_encoder_destroy(_encoder); 205 | _encoder = IntPtr.Zero; 206 | } 207 | } 208 | } 209 | } -------------------------------------------------------------------------------- /MumbleSharp/Audio/Codecs/SpeechCodecs.cs: -------------------------------------------------------------------------------- 1 | namespace MumbleSharp.Audio.Codecs 2 | { 3 | /// 4 | /// 5 | /// 6 | /// See part way down https://github.com/mumble-voip/mumble/blob/master/src/Message.h for the equivalent declaration in the official mumble repo 7 | public enum SpeechCodecs 8 | { 9 | CeltAlpha = 0, 10 | Speex = 2, 11 | CeltBeta = 3, 12 | Opus = 4 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MumbleSharp/Audio/Codecs/Speex/SpeexCodec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MumbleSharp.Audio.Codecs.Speex 4 | { 5 | public class SpeexCodec 6 | : IVoiceCodec 7 | { 8 | public byte[] Decode(byte[] encodedData) 9 | { 10 | throw new NotImplementedException(); 11 | } 12 | 13 | public System.Collections.Generic.IEnumerable PermittedEncodingFrameSizes 14 | { 15 | get { throw new NotImplementedException(); } 16 | } 17 | 18 | public byte[] Encode(ArraySegment pcm) 19 | { 20 | throw new NotImplementedException(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MumbleSharp/Audio/DynamicCircularBuffer.cs: -------------------------------------------------------------------------------- 1 | #region Copyright & License 2 | /************************************************************************* 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2014 Roman Atachiants (kelindar@gmail.com) 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | *************************************************************************/ 25 | #endregion 26 | 27 | using System; 28 | 29 | //https://github.com/Kelindar/circular-buffer/blob/master/Source/ByteQueue.cs 30 | 31 | namespace MumbleSharp.Audio 32 | { 33 | /// 34 | /// Defines a class that represents a resizable circular byte queue. 35 | /// 36 | public sealed class DynamicCircularBuffer 37 | { 38 | // Private fields 39 | private int _head; 40 | private int _tail; 41 | private int _size; 42 | private byte[] _buffer; 43 | 44 | /// 45 | /// Gets the length of the byte queue 46 | /// 47 | public int Count 48 | { 49 | get { return _size; } 50 | } 51 | 52 | public int Capacity 53 | { 54 | get { return _buffer.Length; } 55 | } 56 | 57 | /// 58 | /// Constructs a new instance of a byte queue. 59 | /// 60 | public DynamicCircularBuffer(int capacity = 2048) 61 | { 62 | _buffer = new byte[capacity]; 63 | } 64 | 65 | /// 66 | /// Extends the capacity of the bytequeue 67 | /// 68 | private void SetCapacity(int capacity) 69 | { 70 | byte[] newBuffer = new byte[capacity]; 71 | 72 | if (_size > 0) 73 | { 74 | if (_head < _tail) 75 | { 76 | Buffer.BlockCopy(_buffer, _head, newBuffer, 0, _size); 77 | } 78 | else 79 | { 80 | Buffer.BlockCopy(_buffer, _head, newBuffer, 0, _buffer.Length - _head); 81 | Buffer.BlockCopy(_buffer, 0, newBuffer, _buffer.Length - _head, _tail); 82 | } 83 | } 84 | 85 | _head = 0; 86 | _tail = _size; 87 | _buffer = newBuffer; 88 | } 89 | 90 | 91 | /// 92 | /// Enqueues a buffer to the queue and inserts it to a correct position 93 | /// 94 | internal void Write(ArraySegment write) 95 | { 96 | var size = write.Count; 97 | var buffer = write.Array; 98 | var offset = write.Offset; 99 | 100 | if (size == 0) 101 | return; 102 | 103 | lock (this) 104 | { 105 | if ((_size + size) > _buffer.Length) 106 | SetCapacity((_size + size + 2047) & ~2047); 107 | 108 | if (_head < _tail) 109 | { 110 | int rightLength = (_buffer.Length - _tail); 111 | 112 | if (rightLength >= size) 113 | { 114 | Buffer.BlockCopy(buffer, offset, _buffer, _tail, size); 115 | } 116 | else 117 | { 118 | Buffer.BlockCopy(buffer, offset, _buffer, _tail, rightLength); 119 | Buffer.BlockCopy(buffer, offset + rightLength, _buffer, 0, size - rightLength); 120 | } 121 | } 122 | else 123 | { 124 | Buffer.BlockCopy(buffer, offset, _buffer, _tail, size); 125 | } 126 | 127 | _tail = (_tail + size) % _buffer.Length; 128 | _size += size; 129 | } 130 | } 131 | 132 | /// 133 | /// read from the queue 134 | /// 135 | /// 136 | /// Number of bytes dequeued 137 | internal int Read(ArraySegment read) 138 | { 139 | var size = read.Count; 140 | var buffer = read.Array; 141 | var offset = read.Offset; 142 | 143 | lock (this) 144 | { 145 | if (size > _size) 146 | size = _size; 147 | 148 | if (size == 0) 149 | return 0; 150 | 151 | if (_head < _tail) 152 | { 153 | Buffer.BlockCopy(_buffer, _head, buffer, offset, size); 154 | } 155 | else 156 | { 157 | int rightLength = (_buffer.Length - _head); 158 | 159 | if (rightLength >= size) 160 | { 161 | Buffer.BlockCopy(_buffer, _head, buffer, offset, size); 162 | } 163 | else 164 | { 165 | Buffer.BlockCopy(_buffer, _head, buffer, offset, rightLength); 166 | Buffer.BlockCopy(_buffer, 0, buffer, offset + rightLength, size - rightLength); 167 | } 168 | } 169 | 170 | _head = (_head + size) % _buffer.Length; 171 | _size -= size; 172 | 173 | if (_size == 0) 174 | { 175 | _head = 0; 176 | _tail = 0; 177 | } 178 | 179 | return size; 180 | } 181 | } 182 | } 183 | 184 | } -------------------------------------------------------------------------------- /MumbleSharp/Audio/SpeechTarget.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace MumbleSharp.Audio 3 | { 4 | public enum SpeechTarget 5 | { 6 | Normal = 0, 7 | WhisperToChannel = 1, 8 | IncomingDirectWhisper = 2, 9 | DirectWhisper1 = 3, 10 | DirectWhisper2 = 4, 11 | DirectWhisper3 = 5, 12 | DirectWhisper4 = 6, 13 | DirectWhisper5 = 7, 14 | DirectWhisper6 = 8, 15 | DirectWhisper7 = 9, 16 | DirectWhisper8 = 10, 17 | DirectWhisper9 = 11, 18 | DirectWhisper10 = 12, 19 | DirectWhisper11 = 13, 20 | DirectWhisper12 = 14, 21 | DirectWhisper13 = 15, 22 | DirectWhisper14 = 16, 23 | DirectWhisper15 = 17, 24 | DirectWhisper16 = 18, 25 | DirectWhisper17 = 19, 26 | DirectWhisper18 = 20, 27 | DirectWhisper19 = 21, 28 | DirectWhisper20 = 22, 29 | DirectWhisper21 = 23, 30 | DirectWhisper22 = 24, 31 | DirectWhisper23 = 25, 32 | DirectWhisper24 = 26, 33 | DirectWhisper25 = 27, 34 | DirectWhisper26 = 28, 35 | DirectWhisper27 = 29, 36 | DirectWhisper28 = 30, 37 | ServerLoopback = 31, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MumbleSharp/ConnectionStates.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace MumbleSharp 3 | { 4 | /// 5 | /// Indicates the state of a conection to a mumble server 6 | /// 7 | public enum ConnectionStates 8 | { 9 | Disconnecting, 10 | Disconnected, 11 | 12 | Connecting, 13 | Connected 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MumbleSharp/Constants.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace MumbleSharp 3 | { 4 | public static class Constants 5 | { 6 | public const int DEFAULT_AUDIO_SAMPLE_RATE = 48000; 7 | public const byte DEFAULT_AUDIO_SAMPLE_BITS = 16; 8 | public const byte DEFAULT_AUDIO_SAMPLE_CHANNELS = 1; 9 | public const ushort DEFAULT_AUDIO_FRAME_SIZE = 960; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MumbleSharp/CryptState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace MumbleSharp 5 | { 6 | class CryptState 7 | { 8 | readonly ReaderWriterLockSlim _aesLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); 9 | OcbAes _aes; 10 | 11 | byte[] _serverNonce; 12 | 13 | public byte[] ServerNonce 14 | { 15 | get 16 | { 17 | try 18 | { 19 | _aesLock.EnterReadLock(); 20 | return (byte[])_serverNonce.Clone(); 21 | } 22 | finally 23 | { 24 | _aesLock.ExitReadLock(); 25 | } 26 | } 27 | set 28 | { 29 | try 30 | { 31 | _aesLock.EnterWriteLock(); 32 | _serverNonce = value; 33 | } 34 | finally 35 | { 36 | _aesLock.ExitWriteLock(); 37 | } 38 | } 39 | } 40 | 41 | byte[] _clientNonce; 42 | public byte[] ClientNonce 43 | { 44 | get 45 | { 46 | try 47 | { 48 | _aesLock.EnterReadLock(); 49 | return _clientNonce; 50 | } 51 | finally 52 | { 53 | _aesLock.ExitReadLock(); 54 | } 55 | } 56 | private set 57 | { 58 | try 59 | { 60 | _aesLock.EnterWriteLock(); 61 | _clientNonce = value; 62 | } 63 | finally 64 | { 65 | _aesLock.ExitWriteLock(); 66 | } 67 | } 68 | } 69 | 70 | readonly byte[] _decryptHistory = new byte[256]; 71 | 72 | public int Good { get; private set; } 73 | public int Late { get; private set; } 74 | public int Lost { get; private set; } 75 | 76 | public void SetKeys(byte[] key, byte[] clientNonce, byte[] serverNonce) 77 | { 78 | try 79 | { 80 | _aesLock.EnterWriteLock(); 81 | 82 | _aes = new OcbAes(); 83 | _aes.Initialise(key); 84 | 85 | ServerNonce = serverNonce; 86 | ClientNonce = clientNonce; 87 | } 88 | finally 89 | { 90 | _aesLock.ExitWriteLock(); 91 | } 92 | } 93 | 94 | public byte[] Decrypt(byte[] source, int length) 95 | { 96 | try 97 | { 98 | _aesLock.EnterReadLock(); 99 | 100 | if (length < 4) 101 | return null; 102 | 103 | int plainLength = length - 4; 104 | 105 | byte[] saveiv = new byte[OcbAes.BLOCK_SIZE]; 106 | short ivbyte = (short)(source[0] & 0xFF); 107 | bool restore = false; 108 | byte[] tag = new byte[OcbAes.BLOCK_SIZE]; 109 | 110 | int lost = 0; 111 | int late = 0; 112 | 113 | Array.ConstrainedCopy(ServerNonce, 0, saveiv, 0, OcbAes.BLOCK_SIZE); 114 | 115 | if (((ServerNonce[0] + 1) & 0xFF) == ivbyte) 116 | { 117 | // In order as expected. 118 | if (ivbyte > ServerNonce[0]) 119 | { 120 | ServerNonce[0] = (byte)ivbyte; 121 | } 122 | else if (ivbyte < ServerNonce[0]) 123 | { 124 | ServerNonce[0] = (byte)ivbyte; 125 | for (int i = 1; i < OcbAes.BLOCK_SIZE; i++) 126 | { 127 | if ((++ServerNonce[i]) != 0) 128 | { 129 | break; 130 | } 131 | } 132 | } 133 | else 134 | { 135 | return null; 136 | } 137 | } 138 | else 139 | { 140 | // This is either out of order or a repeat. 141 | int diff = ivbyte - ServerNonce[0]; 142 | if (diff > 128) 143 | { 144 | diff = diff - 256; 145 | } 146 | else if (diff < -128) 147 | { 148 | diff = diff + 256; 149 | } 150 | 151 | if ((ivbyte < ServerNonce[0]) && (diff > -30) && (diff < 0)) 152 | { 153 | // Late packet, but no wraparound. 154 | late = 1; 155 | lost = -1; 156 | ServerNonce[0] = (byte)ivbyte; 157 | restore = true; 158 | } 159 | else if ((ivbyte > ServerNonce[0]) && (diff > -30) && 160 | (diff < 0)) 161 | { 162 | // Last was 0x02, here comes 0xff from last round 163 | late = 1; 164 | lost = -1; 165 | ServerNonce[0] = (byte)ivbyte; 166 | for (int i = 1; i < OcbAes.BLOCK_SIZE; i++) 167 | { 168 | if ((ServerNonce[i]--) != 0) 169 | { 170 | break; 171 | } 172 | } 173 | restore = true; 174 | } 175 | else if ((ivbyte > ServerNonce[0]) && (diff > 0)) 176 | { 177 | // Lost a few packets, but beyond that we're good. 178 | lost = ivbyte - ServerNonce[0] - 1; 179 | ServerNonce[0] = (byte)ivbyte; 180 | } 181 | else if ((ivbyte < ServerNonce[0]) && (diff > 0)) 182 | { 183 | // Lost a few packets, and wrapped around 184 | lost = 256 - ServerNonce[0] + ivbyte - 1; 185 | ServerNonce[0] = (byte)ivbyte; 186 | for (int i = 1; i < OcbAes.BLOCK_SIZE; i++) 187 | { 188 | if ((++ServerNonce[i]) != 0) 189 | { 190 | break; 191 | } 192 | } 193 | } 194 | else 195 | { 196 | return null; 197 | } 198 | 199 | if (_decryptHistory[ServerNonce[0]] == ClientNonce[0]) 200 | { 201 | Array.ConstrainedCopy(saveiv, 0, ServerNonce, 0, OcbAes.BLOCK_SIZE); 202 | return null; 203 | } 204 | } 205 | 206 | byte[] dst = _aes.Decrypt(source, 4, plainLength, ServerNonce, 0, source, 0); 207 | 208 | if (tag[0] != source[1] || tag[1] != source[2] || tag[2] != source[3]) 209 | { 210 | Array.ConstrainedCopy(saveiv, 0, ServerNonce, 0, OcbAes.BLOCK_SIZE); 211 | return null; 212 | } 213 | _decryptHistory[ServerNonce[0]] = ServerNonce[1]; 214 | 215 | if (restore) 216 | { 217 | Array.ConstrainedCopy(saveiv, 0, ServerNonce, 0, OcbAes.BLOCK_SIZE); 218 | } 219 | 220 | Good++; 221 | Late += late; 222 | Lost += lost; 223 | 224 | return dst; 225 | } 226 | finally 227 | { 228 | _aesLock.ExitReadLock(); 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /MumbleSharp/Extensions/IEnumerableOfChannelExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using MumbleProto; 5 | using MumbleSharp.Model; 6 | using MumbleSharp.Packets; 7 | 8 | namespace MumbleSharp.Extensions 9 | { 10 | public static class IEnumerableOfChannelExtensions 11 | { 12 | public static void SendMessage(this IEnumerable channels, string[] message, bool recursive) 13 | { 14 | // It's conceivable that this group could include channels from multiple different server connections 15 | // group by server 16 | foreach (var group in channels.GroupBy(a => a.Owner)) 17 | { 18 | var owner = group.First().Owner; 19 | 20 | var msg = new TextMessage 21 | { 22 | Actor = owner.LocalUser.Id, 23 | Message = string.Join(Environment.NewLine, message), 24 | }; 25 | 26 | if (recursive) 27 | { 28 | if (msg.TreeIds == null) 29 | msg.TreeIds = group.Select(c => c.Id).ToArray(); 30 | else 31 | msg.TreeIds = msg.TreeIds.Concat(group.Select(c => c.Id)).ToArray(); 32 | } 33 | else 34 | { 35 | if (msg.ChannelIds == null) 36 | msg.ChannelIds = group.Select(c => c.Id).ToArray(); 37 | else 38 | msg.ChannelIds = msg.ChannelIds.Concat(group.Select(c => c.Id)).ToArray(); 39 | } 40 | 41 | owner.Connection.SendControl(PacketType.TextMessage, msg); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /MumbleSharp/Extensions/Log.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace MumbleSharp.Extensions 8 | { 9 | class Log 10 | { 11 | const bool enabled = true; 12 | 13 | public static void Write(string message) 14 | { 15 | if (enabled) 16 | Console.WriteLine(message); 17 | } 18 | 19 | public static void Info(string message) 20 | { 21 | if (enabled) 22 | Log.Write("Info: " + message); 23 | } 24 | 25 | public static void Info(string message,object value) 26 | { 27 | if (enabled) 28 | Log.Write("Info: " + message + " - " + ObjectToString(value)); 29 | } 30 | 31 | private static string ObjectToString(object value) 32 | { 33 | var stringPropertyNamesAndValues = value.GetType() 34 | .GetProperties() 35 | .Where(pi => pi.GetGetMethod() != null) 36 | .Select(pi => new 37 | { 38 | Name = pi.Name, 39 | Value = pi.GetGetMethod().Invoke(value, null) 40 | }); 41 | 42 | StringBuilder builder = new StringBuilder(); 43 | bool first = true; 44 | 45 | foreach (var pair in stringPropertyNamesAndValues) 46 | { 47 | if (first) first = false; 48 | else builder.Append(", "); 49 | if (pair.Value is string) builder.AppendFormat("{0}: \"{1}\"", pair.Name, pair.Value); 50 | else builder.AppendFormat("{0}: {1}", pair.Name, pair.Value); 51 | } 52 | 53 | return builder.ToString(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /MumbleSharp/IMumbleProtocol.cs: -------------------------------------------------------------------------------- 1 | using MumbleProto; 2 | using MumbleSharp.Audio; 3 | using MumbleSharp.Audio.Codecs; 4 | using MumbleSharp.Model; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Net.Security; 8 | using System.Security.Cryptography.X509Certificates; 9 | using Version = MumbleProto.Version; 10 | 11 | namespace MumbleSharp 12 | { 13 | /// 14 | /// An object which handles the higher level logic of a connection to a mumble server to support reception of all mumble packet types. 15 | /// 16 | public interface IMumbleProtocol 17 | { 18 | MumbleConnection Connection { get; } 19 | 20 | /// 21 | /// The user of the local client 22 | /// 23 | User LocalUser { get; } 24 | 25 | /// 26 | /// The root channel of the server 27 | /// 28 | Channel RootChannel { get; } 29 | 30 | /// 31 | /// All channels on the server 32 | /// 33 | IEnumerable Channels { get; } 34 | 35 | /// 36 | /// All users on the server 37 | /// 38 | IEnumerable Users { get; } 39 | 40 | /// 41 | /// If true, this indicates that the connection was setup and the server accept this client 42 | /// 43 | bool ReceivedServerSync { get; } 44 | 45 | SpeechCodecs TransmissionCodec { get; } 46 | 47 | /// 48 | /// Associates this protocol with an opening mumble connection 49 | /// 50 | /// 51 | void Initialise(MumbleConnection connection); 52 | 53 | /// 54 | /// Validate the certificate the server sends for itself. By default this will acept *all* certificates. 55 | /// 56 | /// 57 | /// 58 | /// 59 | /// 60 | /// 61 | bool ValidateCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors); 62 | 63 | X509Certificate SelectCertificate(object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers); 64 | 65 | /// 66 | /// Server has sent a version update. 67 | /// 68 | /// 69 | void Version(Version version); 70 | 71 | /// 72 | /// Used to communicate channel properties between the client and the server. 73 | /// Sent by the server during the login process or when channel properties are updated. 74 | /// 75 | /// 76 | void ChannelState(ChannelState channelState); 77 | 78 | /// 79 | /// Sent by the server when it communicates new and changed users to client. 80 | /// First seen during login procedure. 81 | /// 82 | /// 83 | void UserState(UserState userState); 84 | 85 | // Authenticate is only sent from client to server (see https://github.com/mumble-voip/mumble/blob/master/src/Mumble.proto) 86 | //void Authenticate(Authenticate authenticate); 87 | 88 | void CodecVersion(CodecVersion codecVersion); 89 | 90 | void ContextAction(ContextAction contextAction); 91 | 92 | // ContextActionModify is only sent from client to server (see https://github.com/mumble-voip/mumble/blob/master/src/Mumble.proto) 93 | //void ContextActionModify(ContextActionModify contextActionModify); 94 | 95 | /// 96 | /// Sent by the server when it replies to the query or wants the user to resync all channel permissions. 97 | /// 98 | /// 99 | void PermissionQuery(PermissionQuery permissionQuery); 100 | 101 | /// 102 | /// ServerSync message is sent by the server when it has authenticated the user and finished synchronizing the server state. 103 | /// 104 | /// 105 | void ServerSync(ServerSync serverSync); 106 | 107 | /// 108 | /// Sent by the server when it informs the clients on server configuration details. 109 | /// 110 | /// 111 | void ServerConfig(ServerConfig serverConfig); 112 | 113 | /// 114 | /// Received a voice packet from the server 115 | /// 116 | /// 117 | /// 118 | /// 119 | /// 120 | /// 121 | void EncodedVoice(byte[] packet, uint userSession, long sequence, IVoiceCodec codec, SpeechTarget target); 122 | 123 | /// 124 | /// Received a UDP ping from the server 125 | /// 126 | /// 127 | void UdpPing(byte[] packet); 128 | 129 | /// 130 | /// Received a ping over the TCP connection. 131 | /// Server must reply to the client Ping packet with the same timestamp and its own good/late/lost/resync numbers. None of the fields is strictly required. 132 | /// 133 | /// 134 | void Ping(Ping ping); 135 | 136 | /// 137 | /// Used to communicate user leaving or being kicked. 138 | /// Sent by the server when it informs the clients that a user is not present anymore. 139 | /// 140 | /// 141 | void UserRemove(UserRemove userRemove); 142 | 143 | /// 144 | /// Sent by the server when a channel has been removed and clients should be notified. 145 | /// 146 | /// 147 | void ChannelRemove(ChannelRemove channelRemove); 148 | 149 | /// 150 | /// Received a text message from the server. 151 | /// 152 | /// 153 | void TextMessage(TextMessage textMessage); 154 | 155 | /// 156 | /// Lists the registered users. 157 | /// 158 | /// 159 | void UserList(UserList userList); 160 | 161 | /// 162 | /// Sent by the server to inform the clients of suggested client configuration specified by the server administrator. 163 | /// 164 | /// 165 | void SuggestConfig(SuggestConfig suggestedConfiguration); 166 | 167 | /// 168 | /// Get a voice decoder for the specified user/codec combination 169 | /// 170 | /// 171 | /// 172 | /// 173 | IVoiceCodec GetCodec(uint user, SpeechCodecs codec); 174 | 175 | void SendVoice(ArraySegment pcm, SpeechTarget target, uint targetId); 176 | 177 | void SendVoiceStop(); 178 | 179 | /// 180 | /// Sent by the server when it rejects the user connection. 181 | /// 182 | /// 183 | void Reject(Reject reject); 184 | 185 | void PermissionDenied(PermissionDenied permissionDenied); 186 | 187 | void Acl(Acl acl); 188 | 189 | /// 190 | /// Sent by the server to inform the client to refresh its registered user information. 191 | /// 192 | /// 193 | void QueryUsers(QueryUsers queryUsers); 194 | 195 | // VoiceTarget is only sent from client to server (see https://github.com/mumble-voip/mumble/blob/master/src/Mumble.proto) 196 | //void VoiceTarget(VoiceTarget voiceTarget); 197 | 198 | /// 199 | /// Used to communicate user stats between the server and clients. 200 | /// 201 | void UserStats(UserStats userStats); 202 | 203 | // RequestBlob is only sent from client to server (see https://github.com/mumble-voip/mumble/blob/master/src/Mumble.proto) 204 | //void RequestBlob(RequestBlob requestBlob); 205 | 206 | /// 207 | /// Relays information on the bans. 208 | /// The server sends this list only after a client queries for it. 209 | /// 210 | void BanList(BanList banList); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /MumbleSharp/InternalsVisibleTo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | [assembly: InternalsVisibleTo("MumbleSharpTest")] -------------------------------------------------------------------------------- /MumbleSharp/LibraryLoader.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Author: John Carruthers (johnc@frag-labs.com) 3 | // 4 | // Copyright (C) 2013 John Carruthers 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | using System; 27 | using System.Runtime.InteropServices; 28 | 29 | namespace MumbleSharp 30 | { 31 | /// 32 | /// Library loader. 33 | /// 34 | //internal class LibraryLoader 35 | public class LibraryLoader 36 | { 37 | static System.Collections.Generic.List libraries = new System.Collections.Generic.List(); 38 | 39 | [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi)] 40 | private static extern IntPtr LoadLibrary(string lpFileName); 41 | 42 | [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi)] 43 | private static extern bool FreeLibrary(IntPtr module); 44 | 45 | [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] 46 | private static extern IntPtr GetProcAddress(IntPtr hModule, string procName); 47 | 48 | [DllImport("dl", CharSet = CharSet.Ansi)] 49 | private static extern IntPtr dlopen(string filename, int flags); 50 | 51 | [DllImport("dl", CharSet = CharSet.Ansi)] 52 | private static extern void dlclose(IntPtr module); 53 | 54 | [DllImport("dl", CharSet = CharSet.Ansi)] 55 | static extern IntPtr dlsym(IntPtr handle, string symbol); 56 | 57 | public static void UnloadAll() 58 | { 59 | foreach (IntPtr ptr in libraries) 60 | Free(ptr); 61 | } 62 | 63 | /// 64 | /// Load a library. 65 | /// 66 | /// 67 | /// 68 | internal static IntPtr Load(string fileName) 69 | { 70 | IntPtr lib = PlatformDetails.IsWindows ? LoadLibrary(fileName) : dlopen(fileName, 1); 71 | libraries.Add(lib); 72 | 73 | return lib; 74 | } 75 | 76 | internal static bool Free(IntPtr module) 77 | { 78 | if(PlatformDetails.IsWindows) 79 | return FreeLibrary(module); 80 | 81 | dlclose(module); 82 | return true; 83 | } 84 | 85 | /// 86 | /// Resolves library function pointer. 87 | /// 88 | /// 89 | /// 90 | /// 91 | internal static IntPtr ResolveSymbol(IntPtr image, string symbol) 92 | { 93 | return PlatformDetails.IsWindows ? GetProcAddress(image, symbol) : dlsym(image, symbol); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /MumbleSharp/Model/Channel.cs: -------------------------------------------------------------------------------- 1 |  2 | using MumbleProto; 3 | using MumbleSharp.Audio; 4 | using MumbleSharp.Packets; 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | 10 | namespace MumbleSharp.Model 11 | { 12 | public class Channel 13 | : IEquatable 14 | { 15 | internal readonly IMumbleProtocol Owner; 16 | 17 | public bool Temporary { get; set; } 18 | public string Name { get; set; } 19 | public string Description { get; set; } 20 | public int Position { get; set; } 21 | public uint Id { get; private set; } 22 | public uint Parent { get; internal set; } 23 | public Permission Permissions { get; internal set; } 24 | 25 | // Using a concurrent dictionary as a concurrent hashset (why doesn't .net provide a concurrent hashset?!) - http://stackoverflow.com/a/18923091/108234 26 | private readonly ConcurrentDictionary _users = new ConcurrentDictionary(); 27 | public IEnumerable Users 28 | { 29 | get { return _users.Keys; } 30 | } 31 | 32 | public Channel(IMumbleProtocol owner, uint id, string name, uint parent) 33 | { 34 | Owner = owner; 35 | Id = id; 36 | Name = name; 37 | Parent = parent; 38 | } 39 | 40 | /// 41 | /// Send a text message 42 | /// 43 | /// Individual lines of a text message 44 | public void SendMessage(string[] message, bool recursive) 45 | { 46 | var msg = new TextMessage 47 | { 48 | Actor = Owner.LocalUser.Id, 49 | Message = string.Join(Environment.NewLine, message), 50 | }; 51 | 52 | if (recursive) 53 | { 54 | if (msg.TreeIds == null) 55 | msg.TreeIds = new uint[] { Id }; 56 | else 57 | msg.TreeIds = msg.TreeIds.Concat(new uint[] { Id }).ToArray(); 58 | } 59 | else 60 | { 61 | if (msg.ChannelIds == null) 62 | msg.ChannelIds = new uint[] { Id }; 63 | else 64 | msg.ChannelIds = msg.ChannelIds.Concat(new uint[] { Id }).ToArray(); 65 | } 66 | 67 | Owner.Connection.SendControl(PacketType.TextMessage, msg); 68 | } 69 | 70 | private static readonly string[] _split = { "\r\n", "\n" }; 71 | 72 | /// 73 | /// Send a text message 74 | /// 75 | /// A text message (which will be split on newline characters) 76 | public void SendMessage(string message, bool recursive) 77 | { 78 | var messages = message.Split(_split, StringSplitOptions.None); 79 | SendMessage(messages, recursive); 80 | } 81 | 82 | public void SendVoice(ArraySegment buffer, SpeechTarget target = SpeechTarget.Normal) 83 | { 84 | Owner.SendVoice( 85 | buffer, 86 | target: target, 87 | targetId: Id 88 | ); 89 | } 90 | 91 | public void SendVoiceStop() 92 | { 93 | Owner.SendVoiceStop(); 94 | } 95 | 96 | public override string ToString() 97 | { 98 | return Name; 99 | } 100 | 101 | public void Join() 102 | { 103 | var state = new UserState 104 | { 105 | Session = Owner.LocalUser.Id, 106 | Actor = Owner.LocalUser.Id, 107 | ChannelId = Id 108 | }; 109 | 110 | Owner.Connection.SendControl(PacketType.UserState, state); 111 | } 112 | 113 | internal void AddUser(User user) 114 | { 115 | _users.GetOrAdd(user, true); 116 | } 117 | 118 | internal void RemoveUser(User user) 119 | { 120 | bool _; 121 | _users.TryRemove(user, out _); 122 | } 123 | 124 | public override int GetHashCode() 125 | { 126 | return Id.GetHashCode(); 127 | } 128 | 129 | public override bool Equals(object obj) 130 | { 131 | var c = obj as Channel; 132 | if (c != null) 133 | return Equals(c); 134 | 135 | return ReferenceEquals(this, obj); 136 | } 137 | 138 | public bool Equals(Channel other) 139 | { 140 | return other.Id == Id; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /MumbleSharp/Model/Message.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace MumbleSharp.Model 3 | { 4 | public abstract class Message 5 | { 6 | public User Sender { get; protected set; } 7 | public string Text { get; protected set; } 8 | 9 | protected Message(User sender, string text) 10 | { 11 | Sender = sender; 12 | Text = text; 13 | } 14 | } 15 | 16 | public class PersonalMessage : Message 17 | { 18 | public PersonalMessage(User sender, string text) 19 | :base(sender, text) 20 | { 21 | } 22 | } 23 | 24 | public class ChannelMessage : Message 25 | { 26 | public Channel Channel { get; protected set; } 27 | public bool IsRecursive { get; protected set; } 28 | 29 | public ChannelMessage(User sender, string text, Channel channel, bool isRecursive = false) 30 | :base(sender, text) 31 | { 32 | Channel = channel; 33 | IsRecursive = isRecursive; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /MumbleSharp/Model/Permissions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace MumbleSharp.Model 8 | { 9 | public class Permissions 10 | { 11 | public const Permission DEFAULT_PERMISSIONS = Permission.Traverse | Permission.Enter | Permission.Speak | Permission.Whisper | Permission.TextMessage; 12 | } 13 | 14 | [Flags] 15 | public enum Permission : uint 16 | { 17 | //https://github.com/mumble-voip/mumble/blob/master/src/ACL.h 18 | //https://github.com/mumble-voip/mumble/blob/master/src/ACL.cpp 19 | 20 | /// 21 | /// This represents no privileges. 22 | /// 23 | None = 0x0, 24 | 25 | /// 26 | /// This represents total access to the channel, including the ability to change group and ACL information. 27 | /// This privilege implies all other privileges. 28 | /// 29 | Write = 0x1, 30 | 31 | /// 32 | /// This represents the permission to traverse the channel. 33 | /// If a user is denied this privilege, he will be unable to access this channel and any sub-channels in any way, regardless of other permissions in the sub-channels. 34 | /// 35 | Traverse = 0x2, 36 | 37 | /// 38 | /// This represents the permission to join the channel. 39 | /// If you have a hierarchical channel structure, you might want to give everyone Traverse, but restrict Enter in the root of your hierarchy. 40 | /// 41 | Enter = 0x4, 42 | 43 | /// 44 | /// This represents the permission to speak in a channel. 45 | /// Users without this privilege will be suppressed by the server (seen as muted), and will be unable to speak until they are unmuted by someone with the appropriate privileges. 46 | /// 47 | Speak = 0x8, 48 | 49 | /// 50 | /// This represents the permission to mute and deafen other users. 51 | /// Once muted, a user will stay muted until he is unmuted by another privileged user or reconnects to the server. 52 | /// 53 | MuteDeafen = 0x10, 54 | 55 | /// 56 | /// This represents the permission to move a user to another channel or kick him from the server. 57 | /// To actually move the user, either the moving user must have Move privileges in the destination channel, or the user must normally be allowed to enter the channel. 58 | /// Users with this privilege can move users into channels the target user normally wouldn't have permission to enter. 59 | /// 60 | Move = 0x20, 61 | 62 | /// 63 | /// This represents the permission to make sub-channels. 64 | /// The user making the sub-channel will be added to the admin group of the sub-channel. 65 | /// 66 | MakeChannel = 0x40, 67 | 68 | /// 69 | /// This represents the permission to link channels. 70 | /// Users in linked channels hear each other, as long as the speaking user has the speak privilege in the channel of the listener. 71 | /// You need the link privilege in both channels to create a link, but just in either channel to remove it. 72 | /// 73 | LinkChannel = 0x80, 74 | 75 | /// 76 | /// This represents the permission to whisper to this channel from the outside. 77 | /// This works exactly like the speak privilege, but applies to packets spoken with the Whisper key held down. 78 | /// This may be used to broadcast to a hierarchy of channels without linking. 79 | /// 80 | Whisper = 0x100, 81 | 82 | /// 83 | /// This represents the permission to write text messages to other users in this channel. 84 | /// 85 | TextMessage = 0x200, 86 | 87 | /// 88 | /// This represents the permission to make a temporary subchannel. 89 | /// The user making the sub-channel will be added to the admin group of the sub-channel. 90 | /// Temporary channels are not stored and disappear when the last user leaves. 91 | /// 92 | MakeTempChannel = 0x400, 93 | 94 | // --- Root channel only --- 95 | 96 | /// 97 | /// This represents the permission to forcibly remove users from the server. 98 | /// Root channel only. 99 | /// 100 | Kick = 0x10000, 101 | 102 | /// 103 | /// This represents the permission to permanently remove users from the server. 104 | /// Root channel only. 105 | /// 106 | Ban = 0x20000, 107 | 108 | /// 109 | /// This represents the permission to register and unregister users on the server. 110 | /// Root channel only. 111 | /// 112 | Register = 0x40000, 113 | 114 | /// 115 | /// This represents the permission to register oneself on the server. 116 | /// Root channel only. 117 | /// 118 | SelfRegister = 0x80000, 119 | 120 | Cached = 0x8000000, 121 | All = 0xf07ff 122 | }; 123 | } 124 | -------------------------------------------------------------------------------- /MumbleSharp/Model/User.cs: -------------------------------------------------------------------------------- 1 | using MumbleProto; 2 | using MumbleSharp.Audio; 3 | using MumbleSharp.Audio.Codecs; 4 | using MumbleSharp.Packets; 5 | using System; 6 | using NAudio.Wave; 7 | 8 | namespace MumbleSharp.Model 9 | { 10 | public class User 11 | : IEquatable 12 | { 13 | private readonly IMumbleProtocol _owner; 14 | 15 | public UInt32 Id { get; private set; } 16 | public string Name { get; set; } 17 | public string Comment { get; set; } 18 | public bool Deaf { get; set; } 19 | public bool Muted { get; set; } 20 | public bool SelfDeaf { get; set; } 21 | public bool SelfMuted { get; set; } 22 | public bool Suppress { get; set; } 23 | 24 | private Channel _channel; 25 | public Channel Channel 26 | { 27 | get { return _channel; } 28 | set 29 | { 30 | if (_channel != null) 31 | _channel.RemoveUser(this); 32 | 33 | _channel = value; 34 | 35 | if (value != null) 36 | value.AddUser(this); 37 | } 38 | } 39 | 40 | private readonly CodecSet _codecs; 41 | 42 | /// Initializes a new instance of the class. 43 | /// The mumble protocol used by the User to communicate. 44 | /// The id of the user. 45 | /// The sample rate in Hertz (samples per second). 46 | /// The sample bit depth. 47 | /// The sample channels (1 for mono, 2 for stereo). 48 | /// Size of the frame in samples. 49 | public User(IMumbleProtocol owner, uint id, int audioSampleRate = Constants.DEFAULT_AUDIO_SAMPLE_RATE, byte audioSampleBits = Constants.DEFAULT_AUDIO_SAMPLE_BITS, byte audioSampleChannels = Constants.DEFAULT_AUDIO_SAMPLE_CHANNELS, ushort audioFrameSize = Constants.DEFAULT_AUDIO_FRAME_SIZE) 50 | { 51 | if (owner.Connection.VoiceSupportEnabled) 52 | { 53 | _codecs = new CodecSet(audioSampleRate, audioSampleBits, audioSampleChannels, audioFrameSize); 54 | _buffer = new AudioDecodingBuffer(audioSampleRate, audioSampleBits, audioSampleChannels, audioFrameSize); 55 | } 56 | _owner = owner; 57 | Id = id; 58 | } 59 | 60 | private static readonly string[] _split = {"\r\n", "\n"}; 61 | 62 | /// 63 | /// Send a text message 64 | /// 65 | /// A text message (which will be split on newline characters) 66 | public void SendMessage(string message) 67 | { 68 | var messages = message.Split(_split, StringSplitOptions.None); 69 | SendMessage(messages); 70 | } 71 | 72 | /// 73 | /// Send a text message 74 | /// 75 | /// Individual lines of a text message 76 | public void SendMessage(string[] message) 77 | { 78 | _owner.Connection.SendControl(PacketType.TextMessage, new TextMessage 79 | { 80 | Actor = _owner.LocalUser.Id, 81 | Message = string.Join(Environment.NewLine, message), 82 | }); 83 | } 84 | 85 | /// 86 | /// Move user to a channel 87 | /// 88 | /// Channel to move to 89 | public void Move(Channel channel) 90 | { 91 | if (_channel == channel) 92 | return; 93 | 94 | UserState userstate = new UserState() 95 | { 96 | Actor = _owner.LocalUser.Id, 97 | ChannelId = channel.Id 98 | }; 99 | 100 | if (this.Id != _owner.LocalUser.Id) 101 | { 102 | userstate.UserId = this.Id; 103 | } 104 | 105 | _owner.Connection.SendControl(PacketType.UserState, userstate); 106 | } 107 | 108 | /// 109 | /// Send user mute and deaf states 110 | /// 111 | public void SendMuteDeaf() 112 | { 113 | UserState userstate = new UserState() 114 | { 115 | Actor = _owner.LocalUser.Id 116 | }; 117 | 118 | if(this.Id == _owner.LocalUser.Id) 119 | { 120 | userstate.SelfMute = this.SelfMuted || this.SelfDeaf; //mumble disallows being deaf without being muted 121 | userstate.SelfDeaf = this.SelfDeaf; 122 | } else 123 | { 124 | userstate.UserId = this.Id; 125 | userstate.Mute = this.Muted || this.Deaf; //mumble disallows being deaf without being muted 126 | userstate.Deaf = this.Deaf; 127 | } 128 | 129 | _owner.Connection.SendControl(PacketType.UserState, userstate); 130 | } 131 | 132 | protected internal IVoiceCodec GetCodec(SpeechCodecs codec) 133 | { 134 | if (!_owner.Connection.VoiceSupportEnabled) 135 | throw new InvalidOperationException("Voice Support is disabled with this connection"); 136 | 137 | return _codecs.GetCodec(codec); 138 | } 139 | 140 | public override string ToString() 141 | { 142 | return Name; 143 | } 144 | 145 | public override int GetHashCode() 146 | { 147 | return Id.GetHashCode(); 148 | } 149 | 150 | public override bool Equals(object obj) 151 | { 152 | var u = obj as User; 153 | if (u != null) 154 | return (Equals(u)); 155 | 156 | return ReferenceEquals(this, obj); 157 | } 158 | 159 | public bool Equals(User other) 160 | { 161 | return other.Id == Id; 162 | } 163 | 164 | private readonly AudioDecodingBuffer _buffer; 165 | public IWaveProvider Voice 166 | { 167 | get 168 | { 169 | if (!_owner.Connection.VoiceSupportEnabled) 170 | throw new InvalidOperationException("Voice Support is disabled with this connection"); 171 | 172 | return _buffer; 173 | } 174 | } 175 | 176 | public void ReceiveEncodedVoice(byte[] data, long sequence, IVoiceCodec codec) 177 | { 178 | if (!_owner.Connection.VoiceSupportEnabled) 179 | throw new InvalidOperationException("Voice Support is disabled with this connection"); 180 | 181 | _buffer.AddEncodedPacket(sequence, data, codec); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /MumbleSharp/MumbleConnection.cs: -------------------------------------------------------------------------------- 1 | using MumbleProto; 2 | using MumbleSharp.Audio; 3 | using MumbleSharp.Audio.Codecs; 4 | using MumbleSharp.Packets; 5 | using ProtoBuf; 6 | using System; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Net; 10 | using System.Net.Security; 11 | using System.Net.Sockets; 12 | using System.Security.Cryptography.X509Certificates; 13 | 14 | namespace MumbleSharp 15 | { 16 | /// 17 | /// Handles the low level details of connecting to a mumble server. Once connection is established decoded packets are passed off to the MumbleProtocol for processing 18 | /// 19 | public class MumbleConnection 20 | { 21 | private static double PING_DELAY_MILLISECONDS = 5000; 22 | 23 | public float? TcpPingAverage { get; set; } 24 | public float? TcpPingVariance { get; set; } 25 | public uint? TcpPingPackets { get; set; } 26 | 27 | public ConnectionStates State { get; private set; } 28 | 29 | TcpSocket _tcp; 30 | UdpSocket _udp; 31 | 32 | DateTime _lastSentPing = DateTime.MinValue; 33 | 34 | public IMumbleProtocol Protocol { get; private set; } 35 | 36 | /// 37 | /// Whether or not voice support is unabled with this connection 38 | /// 39 | public bool VoiceSupportEnabled { get; private set; } 40 | 41 | public IPEndPoint Host 42 | { 43 | get; 44 | private set; 45 | } 46 | 47 | readonly CryptState _cryptState = new CryptState(); 48 | 49 | /// 50 | /// Creates a connection to the server using the given address and port. 51 | /// 52 | /// The server adress or IP. 53 | /// The port the server listens to. 54 | /// An object which will handle messages from the server. 55 | /// Whether or not voice support is unabled with this connection. 56 | public MumbleConnection(string server, int port, IMumbleProtocol protocol, bool voiceSupport = true) 57 | : this(new IPEndPoint(Dns.GetHostAddresses(server).First(a => a.AddressFamily == AddressFamily.InterNetwork), port), protocol, voiceSupport) 58 | { 59 | } 60 | 61 | /// 62 | /// Creates a connection to the server 63 | /// 64 | /// 65 | /// 66 | /// Whether or not voice support is unabled with this connection. 67 | public MumbleConnection(IPEndPoint host, IMumbleProtocol protocol, bool voiceSupport = true) 68 | { 69 | Host = host; 70 | State = ConnectionStates.Disconnected; 71 | Protocol = protocol; 72 | VoiceSupportEnabled = voiceSupport; 73 | } 74 | 75 | public void Connect(string username, string password, string[] tokens, string serverName) 76 | { 77 | if (State != ConnectionStates.Disconnected) 78 | throw new InvalidOperationException(string.Format("Cannot start connecting MumbleConnection when connection state is {0}", State)); 79 | 80 | State = ConnectionStates.Connecting; 81 | Protocol.Initialise(this); 82 | 83 | _tcp = new TcpSocket(Host, Protocol, this); 84 | _tcp.Connect(username, password, tokens, serverName); 85 | 86 | // UDP Connection is disabled while decryption is broken 87 | // See: https://github.com/martindevans/MumbleSharp/issues/4 88 | // UDP being disabled does not reduce functionality, it forces packets to be sent over TCP instead 89 | _udp = new UdpSocket(Host, Protocol, this); 90 | //_udp.Connect(); 91 | 92 | State = ConnectionStates.Connected; 93 | } 94 | 95 | public void Close() 96 | { 97 | State = ConnectionStates.Disconnecting; 98 | 99 | _udp?.Close(); 100 | _tcp?.Close(); 101 | 102 | State = ConnectionStates.Disconnected; 103 | } 104 | 105 | /// 106 | /// Processes a received network packet. 107 | /// This method should be called periodically. 108 | /// 109 | /// true, if a packet was processed. When this returns true you may want to recall the Process() method as soon as possible as their might be a queue on the network stack (like after a simple Thread.Yield() instead of a more relaxed Thread.Sleep(1) if it returned false). 110 | public bool Process() 111 | { 112 | if ((DateTime.UtcNow - _lastSentPing).TotalMilliseconds > PING_DELAY_MILLISECONDS) 113 | { 114 | _tcp.SendPing(); 115 | 116 | if (_udp.IsConnected) 117 | _udp.SendPing(); 118 | 119 | _lastSentPing = DateTime.UtcNow; 120 | } 121 | 122 | _tcpProcessed = _tcp.Process(); 123 | _udpProcessed = _udp.IsConnected ? _udp.Process() : false; 124 | return _tcpProcessed || _udpProcessed; 125 | } 126 | //declared outside method for alloc optimization 127 | private bool _tcpProcessed; 128 | private bool _udpProcessed; 129 | 130 | public void SendControl(PacketType type, T packet) 131 | { 132 | _tcp.Send(type, packet); 133 | } 134 | 135 | public void SendVoice(ArraySegment packet) 136 | { 137 | //The packet must be a well formed Mumble packet as described in https://mumble-protocol.readthedocs.org/en/latest/voice_data.html#packet-format 138 | //The packet is created in BasicMumbleProtocol's EncodingThread 139 | 140 | if (VoiceSupportEnabled) 141 | _tcp.SendVoice(PacketType.UDPTunnel, packet); 142 | else 143 | throw new InvalidOperationException("Voice Support is disabled with this connection"); 144 | } 145 | 146 | internal void ReceivedEncryptedUdp(byte[] packet) 147 | { 148 | byte[] plaintext = _cryptState.Decrypt(packet, packet.Length); 149 | 150 | if (plaintext == null) 151 | { 152 | Console.WriteLine("Decryption failed"); 153 | return; 154 | } 155 | 156 | ReceiveDecryptedUdp(plaintext); 157 | } 158 | 159 | internal void ReceiveDecryptedUdp(byte[] packet) 160 | { 161 | var type = packet[0] >> 5 & 0x7; 162 | 163 | if (type == 1) 164 | Protocol.UdpPing(packet); 165 | else if(VoiceSupportEnabled) 166 | UnpackVoicePacket(packet, type); 167 | } 168 | 169 | private void PackVoicePacket(ArraySegment packet) 170 | { 171 | } 172 | 173 | private void UnpackVoicePacket(byte[] packet, int type) 174 | { 175 | var vType = (SpeechCodecs)type; 176 | var target = (SpeechTarget)(packet[0] & 0x1F); 177 | 178 | using (var reader = new UdpPacketReader(new MemoryStream(packet, 1, packet.Length - 1))) 179 | { 180 | UInt32 session = (uint)reader.ReadVarInt64(); 181 | Int64 sequence = reader.ReadVarInt64(); 182 | 183 | //Null codec means the user was not found. This can happen if a user leaves while voice packets are still in flight 184 | IVoiceCodec codec = Protocol.GetCodec(session, vType); 185 | if (codec == null) 186 | return; 187 | 188 | if (vType == SpeechCodecs.Opus) 189 | { 190 | int size = (int)reader.ReadVarInt64(); 191 | size &= 0x1fff; 192 | 193 | if (size == 0) 194 | return; 195 | 196 | byte[] data = reader.ReadBytes(size); 197 | if (data == null) 198 | return; 199 | 200 | Protocol.EncodedVoice(data, session, sequence, codec, target); 201 | } 202 | else 203 | { 204 | throw new NotImplementedException("Codec is not opus"); 205 | 206 | //byte header; 207 | //do 208 | //{ 209 | // header = reader.ReadByte(); 210 | // int length = header & 0x7F; 211 | // if (length > 0) 212 | // { 213 | // byte[] data = reader.ReadBytes(length); 214 | // if (data == null) 215 | // break; 216 | 217 | // //TODO: Put *encoded* packets into a queue, then decode the head of the queue 218 | // //TODO: This allows packets to come into late and be inserted into the correct place in the queue (if they arrive before decoding handles a later packet) 219 | // byte[] decodedPcmData = codec.Decode(data); 220 | // if (decodedPcmData != null) 221 | // Protocol.Voice(decodedPcmData, session, sequence); 222 | // } 223 | 224 | //} while ((header & 0x80) > 0); 225 | } 226 | } 227 | } 228 | 229 | internal void ProcessCryptState(CryptSetup cryptSetup) 230 | { 231 | if (cryptSetup.ShouldSerializeKey() && cryptSetup.ShouldSerializeClientNonce() && cryptSetup.ShouldSerializeServerNonce()) // Full key setup 232 | { 233 | _cryptState.SetKeys(cryptSetup.Key, cryptSetup.ClientNonce, cryptSetup.ServerNonce); 234 | } 235 | else if (cryptSetup.ServerNonce != null) // Server syncing its nonce to us. 236 | { 237 | _cryptState.ServerNonce = cryptSetup.ServerNonce; 238 | } 239 | else // Server wants our nonce. 240 | { 241 | SendControl(PacketType.CryptSetup, new CryptSetup { ClientNonce = _cryptState.ClientNonce }); 242 | } 243 | } 244 | 245 | #region pings 246 | //using the approch described here to do running calculations of ping values. 247 | // http://dsp.stackexchange.com/questions/811/determining-the-mean-and-standard-deviation-in-real-time 248 | private float _meanOfPings; 249 | private float _varianceTimesCountOfPings; 250 | private int _countOfPings; 251 | 252 | /// 253 | /// Gets a value indicating whether ping stats should set timestamp when pinging. 254 | /// Only set the timestamp if we're currently connected. This prevents the ping stats from being built. 255 | /// otherwise the stats will be throw off by the time it takes to connect. 256 | /// 257 | /// 258 | /// true if ping stats should set timestamp when pinging; otherwise, false. 259 | /// 260 | internal bool ShouldSetTimestampWhenPinging { get; private set; } 261 | 262 | internal void ReceivePing(Ping ping) 263 | { 264 | ShouldSetTimestampWhenPinging = true; 265 | if (ping.ShouldSerializeTimestamp() && ping.Timestamp != 0) 266 | { 267 | var mostRecentPingtime = 268 | (float)TimeSpan.FromTicks(DateTime.UtcNow.Ticks - (long)ping.Timestamp).TotalMilliseconds; 269 | 270 | //The ping time is the one-way transit time. 271 | mostRecentPingtime /= 2; 272 | 273 | var previousMean = _meanOfPings; 274 | _countOfPings++; 275 | _meanOfPings = _meanOfPings + ((mostRecentPingtime - _meanOfPings) / _countOfPings); 276 | _varianceTimesCountOfPings = _varianceTimesCountOfPings + 277 | ((mostRecentPingtime - _meanOfPings) * (mostRecentPingtime - previousMean)); 278 | 279 | TcpPingPackets = (uint)_countOfPings; 280 | TcpPingAverage = _meanOfPings; 281 | TcpPingVariance = _varianceTimesCountOfPings / _countOfPings; 282 | } 283 | 284 | 285 | } 286 | #endregion 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /MumbleSharp/MumbleSharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1;netstandard2.0 5 | 2.0.1 6 | false 7 | MumbleSharp is a mumble protocol implementation in C#. 8 | For more info on Mumble please visit https://www.mumble.info/ 9 | Copyright © 2023 10 | https://github.com/martindevans/MumbleSharp 11 | mumblesharp.ico 12 | https://github.com/martindevans/MumbleSharp 13 | Git 14 | Mumble voip voice chat 15 | martindevans, Meetsch 16 | 17 | LICENSE 18 | mumblesharp.png 19 | 20 | Readme.md 21 | fix inclusion of opus.dll in projects referencing this package. 22 | 23 | 24 | 25 | true 26 | false 27 | 28 | 29 | 30 | true 31 | true 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | True 42 | 43 | Never 44 | 45 | 46 | True 47 | 48 | Never 49 | 50 | 51 | 52 | 53 | 54 | 55 | True 56 | \ 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Always 68 | true 69 | 70 | 71 | Always 72 | true 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /MumbleSharp/Packets/Implementation Note.md: -------------------------------------------------------------------------------- 1 | # Generation of mumble.cs # 2 | 3 | The mumble.cs file is to be generated using the protobuf-net protogen tool, this from the Mumble's [mumble.proto](https://github.com/mumble-voip/mumble/blob/master/src/Mumble.proto) file. 4 | 5 | ## Protobuf-Net ## 6 | 7 | You may install protobuf-net protogen tool following the instructions here: 8 | 9 | https://www.nuget.org/packages/protobuf-net.Protogen/ 10 | 11 | `dotnet tool install --global protobuf-net.Protogen --version 2.3.17` 12 | 13 | Then launch the protogen executable from the command-line, within the MumbleSharp\Packets folder: 14 | 15 | C:\SomePath\MumbleSharp\Packets> ./protogen --csharp_out=. mumble.proto 16 | -------------------------------------------------------------------------------- /MumbleSharp/Packets/PacketType.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace MumbleSharp.Packets 3 | { 4 | public enum PacketType 5 | :short 6 | { 7 | Version = 0, 8 | 9 | /// 10 | /// Not used. Not even for tunneling UDP through TCP. 11 | /// 12 | UDPTunnel = 1, 13 | 14 | /// 15 | /// Used by the client to send the authentication credentials to the server. 16 | /// 17 | Authenticate = 2, 18 | 19 | /// 20 | /// Sent by the client to notify the server that the client is still alive. 21 | /// Server must reply to the packet with the same timestamp and its own good/late/lost/resync numbers. None of the fields is strictly required. 22 | /// 23 | Ping = 3, 24 | 25 | /// 26 | /// Sent by the server when it rejects the user connection. 27 | /// 28 | Reject = 4, 29 | 30 | /// 31 | /// ServerSync message is sent by the server when it has authenticated the user and finished synchronizing the server state. 32 | /// 33 | ServerSync = 5, 34 | 35 | /// 36 | /// Sent by the client when it wants a channel removed. Sent by the server when a channel has been removed and clients should be notified. 37 | /// 38 | ChannelRemove = 6, 39 | 40 | /// 41 | /// Used to communicate channel properties between the client and the server. 42 | /// Sent by the server during the login process or when channel properties are updated. 43 | /// Client may use this message to update said channel properties. 44 | /// 45 | ChannelState = 7, 46 | 47 | /// 48 | /// Used to communicate user leaving or being kicked. May be sent by the client 49 | /// when it attempts to kick a user. Sent by the server when it informs the clients that a user is not present anymore. 50 | /// 51 | UserRemove = 8, 52 | 53 | /// 54 | /// Sent by the server when it communicates new and changed users to client. 55 | /// First seen during login procedure. May be sent by the client when it wishes to alter its state. 56 | /// 57 | UserState = 9, 58 | 59 | /// 60 | /// Relays information on the bans. 61 | /// The client may send the BanList message to either modify the list of bans or query them from the server. 62 | /// The server sends this list only after a client queries for it. 63 | /// 64 | BanList = 10, 65 | 66 | /// 67 | /// Used to send and broadcast text messages. 68 | /// 69 | TextMessage = 11, 70 | 71 | 72 | PermissionDenied = 12, 73 | 74 | 75 | ACL = 13, 76 | 77 | /// 78 | /// Client may use this message to refresh its registered user information. 79 | /// The client should fill the IDs or Names of the users it wants to refresh. 80 | /// The server fills the missing parts and sends the message back. 81 | /// 82 | QueryUsers = 14, 83 | 84 | /// 85 | /// Used to initialize and resync the UDP encryption. Either side may request a resync by sending the message without any values filled. 86 | /// The resync is performed by sending the message with only the client or server nonce filled. 87 | /// 88 | CryptSetup = 15, 89 | 90 | 91 | ContextActionModify= 16, 92 | 93 | /// 94 | /// Sent by the client when it wants to initiate a Context action. 95 | /// 96 | ContextAction = 17, 97 | 98 | /// 99 | /// Lists the registered users. 100 | /// 101 | UserList = 18, 102 | 103 | /// 104 | /// Sent by the client when it wants to register or clear whisper targets. 105 | /// Note: The first available target ID is 1 as 0 is reserved for normal talking. Maximum target ID is 30. 106 | /// 107 | VoiceTarget = 19, 108 | 109 | /// 110 | /// Sent by the client when it wants permissions for a certain channel. 111 | /// Sent by the server when it replies to the query or wants the user to resync all channel permissions. 112 | /// 113 | PermissionQuery = 20, 114 | 115 | /// 116 | /// Sent by the server to notify the users of the version of the CELT codec they should use. 117 | /// This may change during the connection when new users join. 118 | /// 119 | CodecVersion = 21, 120 | 121 | /// 122 | /// Used to communicate user stats between the server and clients. 123 | /// 124 | UserStats = 22, 125 | 126 | /// 127 | /// Used by the client to request binary data from the server. 128 | /// By default large comments or textures are not sent within standard messages but instead the hash is. 129 | /// If the client does not recognize the hash it may request the resource when it needs it. 130 | /// The client does so by sending a RequestBlob message with the correct fields filled with the user sessions or channel_ids it wants to receive. 131 | /// The server replies to this by sending a new UserState/ChannelState message with the resources filled even if they would normally be transmitted as hashes. 132 | /// 133 | RequestBlob = 23, 134 | 135 | /// 136 | /// Sent by the server when it informs the clients on server configuration details. 137 | /// 138 | ServerConfig = 24, 139 | 140 | /// 141 | /// Sent by the server to inform the clients of suggested client configuration specified by the server administrator. 142 | /// 143 | SuggestConfig = 25, 144 | 145 | 146 | Empty = 32767 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /MumbleSharp/PlatformDetails.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Author: John Carruthers (johnc@frag-labs.com) 3 | // 4 | // Copyright (C) 2013 John Carruthers 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | using System; 27 | using System.IO; 28 | 29 | namespace MumbleSharp 30 | { 31 | /// 32 | /// Provides access to platform details. 33 | /// 34 | public class PlatformDetails 35 | { 36 | static PlatformDetails() 37 | { 38 | if (Directory.Exists("/Applications") 39 | && Directory.Exists("/System") 40 | && Directory.Exists("/Users") 41 | && Directory.Exists("/Volumes")) 42 | IsMac = true; 43 | if (Environment.OSVersion.Platform == PlatformID.Win32NT || 44 | Environment.OSVersion.Platform == PlatformID.Win32Windows) 45 | IsWindows = true; 46 | } 47 | 48 | /// 49 | /// Gets if the current system is a Mac OSX. 50 | /// 51 | public static bool IsMac { get; private set; } 52 | 53 | /// 54 | /// Gets if the current system is windows. 55 | /// 56 | /// true if is windows; otherwise, false. 57 | public static bool IsWindows { get; private set; } 58 | } 59 | } -------------------------------------------------------------------------------- /MumbleSharp/UdpPacketReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace MumbleSharp 5 | { 6 | public class UdpPacketReader 7 | :IDisposable 8 | { 9 | readonly Stream _inner; 10 | 11 | public UdpPacketReader(Stream innerStream) 12 | { 13 | _inner = innerStream; 14 | } 15 | 16 | public void Dispose() 17 | { 18 | _inner.Dispose(); 19 | } 20 | 21 | public byte ReadByte() 22 | { 23 | if (_inner.Position >= _inner.Length) 24 | throw new EndOfStreamException("Cannot read any more bytes from Udp Packet"); 25 | 26 | return (byte)_inner.ReadByte(); 27 | } 28 | 29 | public byte[] ReadBytes(int length) 30 | { 31 | byte[] buffer = new byte[length]; 32 | var read = _inner.Read(buffer, 0, length); 33 | if (length != read) 34 | return null; 35 | 36 | return buffer; 37 | } 38 | 39 | public long ReadVarInt64() 40 | { 41 | //My implementation, neater (imo) but broken 42 | byte b = ReadByte(); 43 | int leadingOnes = LeadingOnes(b); 44 | switch (leadingOnes) 45 | { 46 | case 0: 47 | return b & 127; 48 | case 1: 49 | //10xxxxxx + 1 byte 50 | return ((b & 63) << 8) | ReadByte(); 51 | case 2: 52 | //110xxxxx + 2 bytes 53 | return ((b & 31) << 16) | ReadByte() << 8 | ReadByte(); 54 | case 3: 55 | //1110xxxx + 3 bytes 56 | return ((b & 15) << 24) | ReadByte() << 16 | ReadByte() << 8 | ReadByte(); 57 | case 4: 58 | // Either: 59 | // > 111100__ + int (4 bytes) 60 | // > 111101__ + long (8 bytes) 61 | if ((b & 4) == 4) 62 | { 63 | //111101__ + long (8 bytes) 64 | return ReadByte() << 56 | ReadByte() << 48 | ReadByte() << 40 | ReadByte() << 32 | ReadByte() << 24 | ReadByte() << 16 | ReadByte() << 8 | ReadByte(); 65 | } 66 | else 67 | { 68 | //111100__ + int (4 bytes) 69 | return ReadByte() << 24 | ReadByte() << 16 | ReadByte() << 8 | ReadByte(); 70 | } 71 | case 5: 72 | //111110 + varint (negative) 73 | return ~ReadVarInt64(); 74 | case 6: 75 | case 7: 76 | case 8: 77 | //111111xx Byte-inverted negative two bit number (~xx) 78 | 79 | //We need three cases here because all the other leading parts are capped off by a zero, e.g. 11110xxx 80 | //However in this case it's just 6 ones, and then the data (111111xx). Depending on the data, the leading count changes 81 | return ~(b & 3); 82 | default: 83 | throw new InvalidDataException("Invalid varint encoding"); 84 | } 85 | } 86 | 87 | private static readonly byte[] _leadingOnesLookup = { 88 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 89 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 91 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 92 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 94 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 95 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 97 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 98 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 99 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 100 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 101 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 102 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 103 | 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7, 8, 104 | }; 105 | 106 | public static int LeadingOnes(byte value) 107 | { 108 | return _leadingOnesLookup[value]; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /MumbleSharp/UdpSocket.cs: -------------------------------------------------------------------------------- 1 | using MumbleProto; 2 | using MumbleSharp.Audio; 3 | using MumbleSharp.Audio.Codecs; 4 | using MumbleSharp.Packets; 5 | using ProtoBuf; 6 | using System; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Net; 10 | using System.Net.Security; 11 | using System.Net.Sockets; 12 | using System.Security.Cryptography.X509Certificates; 13 | 14 | namespace MumbleSharp 15 | { 16 | internal class UdpSocket 17 | { 18 | readonly UdpClient _client; 19 | readonly IPEndPoint _host; 20 | readonly IMumbleProtocol _protocol; 21 | readonly MumbleConnection _connection; 22 | 23 | public bool IsConnected { get; private set; } 24 | 25 | public UdpSocket(IPEndPoint host, IMumbleProtocol protocol, MumbleConnection connection) 26 | { 27 | _host = host; 28 | _protocol = protocol; 29 | _connection = connection; 30 | _client = new UdpClient(); 31 | } 32 | 33 | public void Connect() 34 | { 35 | _client.Connect(_host); 36 | IsConnected = true; 37 | } 38 | 39 | public void Close() 40 | { 41 | IsConnected = false; 42 | _client.Close(); 43 | } 44 | 45 | public void SendPing() 46 | { 47 | long timestamp = DateTime.UtcNow.Ticks; 48 | 49 | byte[] buffer = new byte[9]; 50 | buffer[0] = 1 << 5; 51 | buffer[1] = (byte)((timestamp >> 56) & 0xFF); 52 | buffer[2] = (byte)((timestamp >> 48) & 0xFF); 53 | buffer[3] = (byte)((timestamp >> 40) & 0xFF); 54 | buffer[4] = (byte)((timestamp >> 32) & 0xFF); 55 | buffer[5] = (byte)((timestamp >> 24) & 0xFF); 56 | buffer[6] = (byte)((timestamp >> 16) & 0xFF); 57 | buffer[7] = (byte)((timestamp >> 8) & 0xFF); 58 | buffer[8] = (byte)((timestamp) & 0xFF); 59 | 60 | _client.Send(buffer, buffer.Length); 61 | } 62 | 63 | public bool Process() 64 | { 65 | if (_client.Client == null 66 | || _client.Available == 0) 67 | return false; 68 | 69 | IPEndPoint sender = _host; 70 | byte[] data = _client.Receive(ref sender); 71 | 72 | _connection.ReceivedEncryptedUdp(data); 73 | 74 | return true; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /MumbleSharp/Var64.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace MumbleSharp 5 | { 6 | static class Var64 7 | { 8 | //This stuff is a partial duplicate of the varint64 stuff in UdpPacketReader! 9 | //Should write a UdpPacketWriter to mirror it 10 | 11 | public static int calculateVarint64(UInt64 value) 12 | { 13 | UInt64 part0 = value; 14 | UInt64 part1 = value >> 28; 15 | UInt64 part2 = value >> 56; 16 | if (part2 == 0) { 17 | if (part1 == 0) { 18 | if (part0 < (1 << 14)) 19 | return part0 < (1 << 7) ? 1 : 2; 20 | else 21 | return part0 < (1 << 21) ? 3 : 4; 22 | } else { 23 | if (part1 < (1 << 14)) 24 | return part1 < (1 << 7) ? 5 : 6; 25 | else 26 | return part1 < (1 << 21) ? 7 : 8; 27 | } 28 | } else 29 | return part2 < (1 << 7) ? 9 : 10; 30 | } 31 | 32 | public static byte[] writeVarint64(UInt64 value) 33 | { 34 | UInt64 part0 = value; 35 | UInt64 part1 = value >> 28; 36 | UInt64 part2 = value >> 56; 37 | int size = calculateVarint64(value); 38 | byte[] array = new byte[size]; 39 | 40 | //var dst = new Uint8Array(this.array); 41 | switch (size) 42 | { 43 | case 10: array[9] = (byte)((part2 >> 7) | 0x80); goto case 9; 44 | case 9: array[8] = (byte)((part2) | 0x80); goto case 8; 45 | case 8: array[7] = (byte)((part1 >> 21) | 0x80); goto case 7; 46 | case 7: array[6] = (byte)((part1 >> 14) | 0x80); goto case 6; 47 | case 6: array[5] = (byte)((part1 >> 7) | 0x80); goto case 5; 48 | case 5: array[4] = (byte)((part1) | 0x80); goto case 4; 49 | case 4: array[3] = (byte)((part0 >> 21) | 0x80); goto case 3; 50 | case 3: array[2] = (byte)((part0 >> 14) | 0x80); goto case 2; 51 | case 2: array[1] = (byte)((part0 >> 7) | 0x80); goto case 1; 52 | case 1: array[0] = (byte)((part0) | 0x80); break; 53 | } 54 | array[size - 1] &= 0x7F; 55 | 56 | return array; 57 | } 58 | 59 | public static byte[] writeVarint64_alternative(UInt64 value) 60 | { 61 | UInt64 i = value; 62 | List byteList = new List(); 63 | 64 | if ( 65 | ((i & 0x8000000000000000L) != 0) && 66 | (~i < 0x100000000L) 67 | ) 68 | { 69 | // Signed number. 70 | i = ~i; 71 | if (i <= 0x3) 72 | { 73 | // Shortcase for -1 to -4 74 | byteList.Add((byte)(0xFC | i)); 75 | return byteList.ToArray(); 76 | } 77 | else 78 | { 79 | byteList.Add(0xF8); 80 | } 81 | } 82 | if (i < 0x80) 83 | { 84 | // Need top bit clear 85 | byteList.Add((byte)i); 86 | } 87 | else if (i < 0x4000) 88 | { 89 | // Need top two bits clear 90 | byteList.Add((byte)((i >> 8) | 0x80)); 91 | byteList.Add((byte)(i & 0xFF)); 92 | } 93 | else if (i < 0x200000) 94 | { 95 | // Need top three bits clear 96 | byteList.Add((byte)((i >> 16) | 0xC0)); 97 | byteList.Add((byte)((i >> 8) & 0xFF)); 98 | byteList.Add((byte)(i & 0xFF)); 99 | } 100 | else if (i < 0x10000000) 101 | { 102 | // Need top four bits clear 103 | byteList.Add((byte)((i >> 24) | 0xE0)); 104 | byteList.Add((byte)((i >> 16) & 0xFF)); 105 | byteList.Add((byte)((i >> 8) & 0xFF)); 106 | byteList.Add((byte)(i & 0xFF)); 107 | } 108 | else if (i < 0x100000000L) 109 | { 110 | // It's a full 32-bit integer. 111 | byteList.Add(0xF0); 112 | byteList.Add((byte)((i >> 24) & 0xFF)); 113 | byteList.Add((byte)((i >> 16) & 0xFF)); 114 | byteList.Add((byte)((i >> 8) & 0xFF)); 115 | byteList.Add((byte)(i & 0xFF)); 116 | } 117 | else 118 | { 119 | // It's a 64-bit value. 120 | byteList.Add(0xF4); 121 | byteList.Add((byte)((i >> 56) & 0xFF)); 122 | byteList.Add((byte)((i >> 48) & 0xFF)); 123 | byteList.Add((byte)((i >> 40) & 0xFF)); 124 | byteList.Add((byte)((i >> 32) & 0xFF)); 125 | byteList.Add((byte)((i >> 24) & 0xFF)); 126 | byteList.Add((byte)((i >> 16) & 0xFF)); 127 | byteList.Add((byte)((i >> 8) & 0xFF)); 128 | byteList.Add((byte)(i & 0xFF)); 129 | } 130 | return byteList.ToArray(); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /MumbleSharp/mumblesharp.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martindevans/MumbleSharp/739568c812852c5a5652dc71b77e848ace7ae7f9/MumbleSharp/mumblesharp.ico -------------------------------------------------------------------------------- /MumbleSharpTest/DynamicCircularBufferTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using MumbleSharp.Audio; 4 | 5 | namespace MumbleSharpTest 6 | { 7 | [TestClass] 8 | public class DynamicCircularBufferTest 9 | { 10 | private readonly Random _random = new Random(12352); 11 | 12 | private readonly DynamicCircularBuffer _buffer = new DynamicCircularBuffer(1024); 13 | 14 | [TestMethod] 15 | public void ConstructingBufferCreatesBufferWithGivenCapacity() 16 | { 17 | Assert.AreEqual(1024, _buffer.Capacity); 18 | } 19 | 20 | [TestMethod] 21 | public void NewBufferIsEmpty() 22 | { 23 | Assert.AreEqual(0, _buffer.Count); 24 | } 25 | 26 | [TestMethod] 27 | public void WritingIntoBufferIncreasesCount() 28 | { 29 | _buffer.Write(new ArraySegment(new byte[100])); 30 | 31 | Assert.AreEqual(100, _buffer.Count); 32 | } 33 | 34 | [TestMethod] 35 | public void WritingMoreDataIntoBufferGrowsBuffer() 36 | { 37 | byte[] b = new byte[1500]; 38 | _random.NextBytes(b); 39 | 40 | _buffer.Write(new ArraySegment(b)); 41 | 42 | Assert.AreEqual(1500, _buffer.Count); 43 | } 44 | 45 | [TestMethod] 46 | public void ReadingSmallNumberOfBytesFromBufferIsCorrect() 47 | { 48 | byte[] b = new byte[100]; 49 | _random.NextBytes(b); 50 | 51 | _buffer.Write(new ArraySegment(b)); 52 | 53 | byte[] r = new byte[100]; 54 | Assert.AreEqual(100, _buffer.Read(new ArraySegment(r))); 55 | 56 | for (int i = 0; i < b.Length; i++) 57 | { 58 | Assert.AreEqual(b[i], r[i]); 59 | } 60 | } 61 | 62 | [TestMethod] 63 | public void ReadingLargeNumberOfBytesFromBufferIsCorrect() 64 | { 65 | byte[] b = new byte[1500]; 66 | _random.NextBytes(b); 67 | 68 | _buffer.Write(new ArraySegment(b)); 69 | 70 | byte[] r = new byte[1500]; 71 | Assert.AreEqual(1500, _buffer.Read(new ArraySegment(r))); 72 | 73 | for (int i = 0; i < b.Length; i++) 74 | { 75 | Assert.AreEqual(b[i], r[i]); 76 | } 77 | } 78 | 79 | [TestMethod] 80 | public void ReadingBytesFromTwistedArrayIsCorrect() 81 | { 82 | //Write enough bytes to nearly fill the buffer 83 | _buffer.Write(new ArraySegment(new byte[900])); 84 | 85 | //Read most of the bytes back 86 | Assert.AreEqual(800, _buffer.Read(new ArraySegment(new byte[800]))); 87 | 88 | //Write bytes to wraparound 89 | byte[] b = new byte[400]; 90 | _random.NextBytes(b); 91 | _buffer.Write(new ArraySegment(b)); 92 | 93 | //Read them back 94 | byte[] r = new byte[500]; 95 | Assert.AreEqual(500, _buffer.Read(new ArraySegment(r))); 96 | 97 | for (int i = 0; i < 100; i++) 98 | Assert.AreEqual(0, r[i]); 99 | 100 | for (int i = 0; i < b.Length; i++) 101 | Assert.AreEqual(b[i], r[i + 100]); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /MumbleSharpTest/MumbleSharpTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | Library 11 | 12 | 13 | 14 | 15 | 16 | AnyCPU 17 | 18 | 19 | 20 | AnyCPU 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /MumbleSharpTest/OcbAesTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using MumbleSharp; 3 | 4 | namespace MumbleSharpTest 5 | { 6 | [TestClass] 7 | public class OcbAesTest 8 | { 9 | private void AssertArraysEqual(T[] expected, T[] actual) 10 | { 11 | Assert.AreEqual(expected.Length, actual.Length); 12 | 13 | for (int i = 0; i < expected.Length; i++) 14 | Assert.AreEqual(expected[i], actual[i]); 15 | } 16 | 17 | private void Test(byte[] key, byte[] nonce, byte[] plaintext, byte[] expectedCipherText, byte[] expectedTag) 18 | { 19 | OcbAes a = new OcbAes(); 20 | a.Initialise(key); 21 | 22 | byte[] tag = new byte[16]; 23 | var cipher = a.Encrypt(plaintext, 0, plaintext.Length, nonce, 0, tag, 0); 24 | 25 | AssertArraysEqual(expectedCipherText, cipher); 26 | AssertArraysEqual(expectedTag, tag); 27 | 28 | var plain2 = a.Decrypt(cipher, 0, cipher.Length, nonce, 0, tag, 0); 29 | AssertArraysEqual(plaintext, plain2); 30 | } 31 | 32 | [TestMethod] 33 | public void OCB_AES_128_0B() 34 | { 35 | Test( 36 | new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }, 37 | new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }, 38 | new byte[0], 39 | new byte[0], 40 | new byte[] { 0x15, 0xd3, 0x7d, 0xd7, 0xc8, 0x90, 0xd5, 0xd6, 0xac, 0xab, 0x92, 0x7b, 0xc0, 0xdc, 0x60, 0xee } 41 | ); 42 | } 43 | 44 | [TestMethod] 45 | public void OCB_AES_128_3B() 46 | { 47 | Test( 48 | new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }, 49 | new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }, 50 | new byte[] { 0x00, 0x01, 0x02 }, 51 | new byte[] { 0xfc, 0xd3, 0x7d }, 52 | new byte[] { 0x02, 0x25, 0x47, 0x39, 0xa5, 0xe3, 0x56, 0x5a, 0xe2, 0xdc, 0xd6, 0x2c, 0x65, 0x97, 0x46, 0xba } 53 | ); 54 | } 55 | 56 | [TestMethod] 57 | public void OCB_AES_128_16B() 58 | { 59 | Test( 60 | new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }, 61 | new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }, 62 | new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }, 63 | new byte[] { 0x37, 0xdf, 0x8c, 0xe1, 0x5b, 0x48, 0x9b, 0xf3, 0x1d, 0x0f, 0xc4, 0x4d, 0xa1, 0xfa, 0xf6, 0xd6 }, 64 | new byte[] { 0xdf, 0xb7, 0x63, 0xeb, 0xdb, 0x5f, 0x0e, 0x71, 0x9c, 0x7b, 0x41, 0x61, 0x80, 0x80, 0x04, 0xdf } 65 | ); 66 | } 67 | 68 | [TestMethod] 69 | public void OCB_AES_128_20B() 70 | { 71 | Test( 72 | new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }, 73 | new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }, 74 | new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13 }, 75 | new byte[] { 0x01, 0xa0, 0x75, 0xf0, 0xd8, 0x15, 0xb1, 0xa4, 0xe9, 0xc8, 0x81, 0xa1, 0xbc, 0xff, 0xc3, 0xeb, 0x70, 0x03, 0xeb, 0x55 }, 76 | new byte[] { 0x75, 0x30, 0x84, 0x14, 0x4e, 0xb6, 0x3b, 0x77, 0x0b, 0x06, 0x3c, 0x2e, 0x23, 0xcd, 0xa0, 0xbb } 77 | ); 78 | } 79 | 80 | [TestMethod] 81 | public void OCB_AES_128_32B() 82 | { 83 | Test( 84 | new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }, 85 | new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }, 86 | new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f }, 87 | new byte[] { 0x01, 0xa0, 0x75, 0xf0, 0xd8, 0x15, 0xb1, 0xa4, 0xe9, 0xc8, 0x81, 0xa1, 0xbc, 0xff, 0xc3, 0xeb, 0x4a, 0xfc, 0xbb, 0x7f, 0xed, 0xc0, 0x8c, 0xa8, 0x65, 0x4c, 0x6d, 0x30, 0x4d, 0x16, 0x12, 0xfa }, 88 | new byte[] { 0xc1, 0x4c, 0xbf, 0x2c, 0x1a, 0x1f, 0x1c, 0x3c, 0x13, 0x7e, 0xad, 0xea, 0x1f, 0x2f, 0x2f, 0xcf } 89 | ); 90 | } 91 | 92 | [TestMethod] 93 | public void OCB_AES_128_34B() 94 | { 95 | Test( 96 | new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }, 97 | new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }, 98 | new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21 }, 99 | new byte[] { 0x01, 0xa0, 0x75, 0xf0, 0xd8, 0x15, 0xb1, 0xa4, 0xe9, 0xc8, 0x81, 0xa1, 0xbc, 0xff, 0xc3, 0xeb, 0xd4, 0x90, 0x3d, 0xd0, 0x02, 0x5b, 0xa4, 0xaa, 0x83, 0x7c, 0x74, 0xf1, 0x21, 0xb0, 0x26, 0x0f, 0xa9, 0x5d }, 100 | new byte[] { 0xcf, 0x83, 0x41, 0xbb, 0x10, 0x82, 0x0c, 0xcf, 0x14, 0xbd, 0xec, 0x56, 0xb8, 0xd7, 0xd6, 0xab } 101 | ); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /MumbleSharpTest/ReadMe.md: -------------------------------------------------------------------------------- 1 | To run the unit tests, run the following command line from withing the MubleSharpTest folder: 2 | 3 | `dotnet test` 4 | 5 | Otherwise to run the test from within Visual Studio's test explorer you need at least VS2019 v16.4 -------------------------------------------------------------------------------- /MumbleSharpTest/UdpPacketReaderTest.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using MumbleSharp; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System; 5 | using System.IO; 6 | 7 | namespace MumbleSharpTest 8 | { 9 | 10 | 11 | /// 12 | ///This is a test class for UdpPacketReaderTest and is intended 13 | ///to contain all UdpPacketReaderTest Unit Tests 14 | /// 15 | [TestClass] 16 | public class UdpPacketReaderTest 17 | { 18 | [TestMethod] 19 | public void LeadingOnesInAllBytes() 20 | { 21 | for (int i = 0; i <= byte.MaxValue; i++) 22 | { 23 | var digits = Convert.ToString(i, 2); 24 | if (digits.Length < 8) 25 | digits = Enumerable.Repeat("0", 8 - digits.Length).Aggregate((a, b) => a + b) + digits; 26 | 27 | var expected = digits.TakeWhile(a => a == '1').Count(); 28 | var actual = UdpPacketReader.LeadingOnes((byte)i); 29 | Assert.AreEqual(expected, actual); 30 | } 31 | 32 | 33 | } 34 | 35 | private static byte B(string s) 36 | { 37 | return Convert.ToByte(s, 2); 38 | } 39 | 40 | private static UdpPacketReader R(params byte[] bytes) 41 | { 42 | return new UdpPacketReader(new MemoryStream(bytes)); 43 | } 44 | 45 | [TestMethod] 46 | public void VariableLength_ZeroLeadingOnes() 47 | { 48 | var expected = B("01001001"); 49 | 50 | UdpPacketReader r = R(expected, B("11111111")); 51 | 52 | var actual = r.ReadVarInt64(); 53 | Assert.AreEqual(expected, actual); 54 | } 55 | 56 | [TestMethod] 57 | public void VariableLength_OneLeadingOne() 58 | { 59 | var expected = B("00101100") << 8 | B("11100101"); 60 | 61 | UdpPacketReader r = R(B("10101100"), B("11100101"), B("11111111")); 62 | 63 | var actual = r.ReadVarInt64(); 64 | Assert.AreEqual(expected, actual); 65 | } 66 | 67 | [TestMethod] 68 | public void VariableLength_TwoLeadingOnes() 69 | { 70 | var expected = B("00010010") << 16 | B("11100101") << 8 | B("11111111"); 71 | 72 | UdpPacketReader r = R(B("11010010"), B("11100101"), B("11111111")); 73 | 74 | var actual = r.ReadVarInt64(); 75 | Assert.AreEqual(expected, actual); 76 | } 77 | 78 | [TestMethod] 79 | public void VariableLength_ThreeLeadingOnes() 80 | { 81 | var expected = B("00000010") << 24 | B("11100101") << 16 | B("11111111") << 8 | B("00001111"); 82 | 83 | UdpPacketReader r = R(B("11100010"), B("11100101"), B("11111111"), B("00001111")); 84 | 85 | var actual = r.ReadVarInt64(); 86 | Assert.AreEqual(expected, actual); 87 | } 88 | 89 | [TestMethod] 90 | public void VariableLength_FourLeadingOnes_FourBytes() 91 | { 92 | var expected = B("11100101") << 24 | B("11111111") << 16 | B("00001111") << 8 | B("10101010"); 93 | 94 | UdpPacketReader r = R(B("11110010"), B("11100101"), B("11111111"), B("00001111"), B("10101010")); 95 | 96 | var actual = r.ReadVarInt64(); 97 | Assert.AreEqual(expected, actual); 98 | } 99 | 100 | [TestMethod] 101 | public void VariableLength_FourLeadingOnes_EightBytes() 102 | { 103 | var expected = B("11100101") << 56 | B("11111111") << 48 | B("00001111") << 40 | B("10101010") << 32 | B("11100101") << 24 | B("11111111") << 16 | B("00001111") << 8 | B("10101010"); 104 | 105 | UdpPacketReader r = R(B("11110110"), B("11100101"), B("11111111"), B("00001111"), B("10101010"), B("11100101"), B("11111111"), B("00001111"), B("10101010")); 106 | 107 | var actual = r.ReadVarInt64(); 108 | Assert.AreEqual(expected, actual); 109 | } 110 | 111 | [TestMethod] 112 | public void VariableLength_Negative() 113 | { 114 | var expected = ~(B("11100101") << 24 | B("10000001") << 16 | B("00001111") << 8 | B("10101010")); 115 | 116 | UdpPacketReader r = R(B("11111000"), B("11110010"), B("11100101"), B("10000001"), B("00001111"), B("10101010")); 117 | 118 | var actual = r.ReadVarInt64(); 119 | Assert.AreEqual(expected, actual); 120 | } 121 | 122 | [TestMethod] 123 | public void VariableLength_InvertedTwoBitNumber() 124 | { 125 | //These are 2 bit numbers, test all 4 possibilities 126 | 127 | var expected = ~B("00000000"); 128 | var r = R(B("11111100")); 129 | var actual = r.ReadVarInt64(); 130 | Assert.AreEqual(expected, actual); 131 | 132 | expected = ~B("00000001"); 133 | r = R(B("11111101")); 134 | actual = r.ReadVarInt64(); 135 | Assert.AreEqual(expected, actual); 136 | 137 | expected = ~B("00000010"); 138 | r = R(B("11111110")); 139 | actual = r.ReadVarInt64(); 140 | Assert.AreEqual(expected, actual); 141 | 142 | expected = ~B("00000011"); 143 | r = R(B("11111111")); 144 | actual = r.ReadVarInt64(); 145 | Assert.AreEqual(expected, actual); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ## MumbleSharp 2 | 3 | MumbleSharp is Mumble client library and sample client implementations in C#. 4 | 5 | ![MumbleSharp Logo](https://raw.githubusercontent.com/martindevans/MumbleSharp/master/mumblesharp.png) 6 | 7 | The library targets .NET Standard 2.1 and 2.0 8 | 9 | The sample client implementations targets .NET 6.0 10 | 11 | [Mumble](https://www.mumble.info/) is a free, open source, low latency, high quality voice chat application. 12 | 13 | The solution comes in two parts: 14 | 15 | 1. The MumbleSharp Library 16 | - MumbleSharp is the actual MumbleSharp library which is a class library for building mumble clients. 17 | 2. Demo implementations available on [GitHub](https://github.com/martindevans/MumbleSharp) 18 | - MumbleClient is a console mumble client, a console application to use for testing and learning. 19 | - MumbleGuiClient is a winform mumble client, a minimalistic client but fully functional: channels display and switching, message chat, voice support, playback/recording device selection and a very basic voice detection. 20 | 21 | ## NuGet Package 22 | 23 | You may find the MumbleSharp library as a NuGet package: 24 | 25 | | Package Name | Version | 26 | |--------------|---------| 27 | | [MumbleSharp][MumbleSharpNuget] | [![MumbleSharpShield]][MumbleSharpNuget] | 28 | 29 | [MumbleSharpNuget]: https://www.nuget.org/packages/MumbleSharp/ 30 | [MumbleSharpShield]: https://img.shields.io/nuget/vpre/MumbleSharp.svg 31 | 32 | ## Quick Start 33 | 34 | As you can see from the MumbleClient's Program.cs creating a new client is very simple: 35 | 36 | 1. Implement IMumbleProtocol and implement methods to respond to messages of different types however you wish. 37 | 2. Use a MumbleConnection to connect to a server. 38 | 39 | ## Work In Progress 40 | 41 | The library nearly supports all non-voice things that Mumble can do. For voice it only supports [Opus](http://www.opus-codec.org/) encoded packets (Opus is Mumble's primary codec). 42 | 43 | ## Contributing 44 | 45 | We're working on MumbleSharp in our spare time but are very happy to receive contributions. If you're thinking of contributing create an issue and assign-it to yourself and we'll try to give you any advice we can to achieve whatever you want, and that's a good way to make sure no one else is duplicating your work as well as being a good place to have discussions. 46 | 47 | When contributing it's often useful to reference the [Mumble source code](https://github.com/mumble-voip/mumble). 48 | 49 | ### Things To Do 50 | 51 | If you want to contribute here's some ideas: 52 | 53 | #### Jitter Buffer 54 | There is no jitter buffering at the moment. Port the jitter buffering from mumble or implement your own. AudioBuffer.cs is probably the correct place to start doing this. 55 | 56 | #### Other Codecs 57 | Supporting other codecs should be relatively simple. For CELT you'll need to find the correct version of the DLL (check out the Mumble-Protocol.pdf for the version) and then write a wrapper with P/Invoke. For Speex you should be able to use NSpeex. 58 | 59 | ## Looking For VoIP In Unity? 60 | 61 | Martin Evans developed a VoIP asset for Unity: [Dissonance Voice Chat](https://placeholder-software.co.uk/dissonance/) 62 | -------------------------------------------------------------------------------- /mumble-protocol-1.2.5-alpha.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martindevans/MumbleSharp/739568c812852c5a5652dc71b77e848ace7ae7f9/mumble-protocol-1.2.5-alpha.pdf -------------------------------------------------------------------------------- /mumble-protocol.url: -------------------------------------------------------------------------------- 1 | [{000214A0-0000-0000-C000-000000000046}] 2 | Prop3=19,11 3 | [InternetShortcut] 4 | IDList= 5 | URL=https://mumble-protocol.readthedocs.io/en/latest/ 6 | -------------------------------------------------------------------------------- /mumblesharp.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martindevans/MumbleSharp/739568c812852c5a5652dc71b77e848ace7ae7f9/mumblesharp.ico -------------------------------------------------------------------------------- /mumblesharp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martindevans/MumbleSharp/739568c812852c5a5652dc71b77e848ace7ae7f9/mumblesharp.png -------------------------------------------------------------------------------- /mumblesharp.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martindevans/MumbleSharp/739568c812852c5a5652dc71b77e848ace7ae7f9/mumblesharp.xcf -------------------------------------------------------------------------------- /nuget.pack.bat: -------------------------------------------------------------------------------- 1 | dotnet clean MumbleSharp\MumbleSharp.csproj -c Release 2 | dotnet build MumbleSharp\MumbleSharp.csproj -c Release 3 | dotnet pack MumbleSharp\MumbleSharp.csproj -c Release -o NuGetRelease -------------------------------------------------------------------------------- /nuget.publish.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo This script will publish MumbleSharp NuGet package to NuGet.org 3 | set /p NuGetOrgApiKey="Nuget.org API Key? " 4 | 5 | :version 6 | echo List of NuGet package found: 7 | dir NuGetRelease\MumbleSharp.*.nupkg /b /a-d 8 | set /p Version="NuGet package version to publish? (enter only the version number, e.g: '1.0.0') " 9 | set Package=MumbleSharp.%Version%.nupkg 10 | 11 | :confirm 12 | echo Ok to publish %Package% to NuGet.org? 13 | set Confirm=n 14 | set /p Confirm="y/n? " 15 | If "%Confirm%"=="Y" goto publish 16 | If "%Confirm%"=="y" goto publish 17 | If "%Confirm%"=="N" goto abort 18 | If "%Confirm%"=="n" goto abort 19 | echo Error: Input not recognized, answer by 'y' or 'n' 20 | goto :confirm 21 | 22 | :publish 23 | echo Publishing %Package% to NuGet.org 24 | dotnet nuget push NuGetRelease\MumbleSharp.%Version%.nupkg -k %NuGetOrgApiKey% -s https://api.nuget.org/v3/index.json 25 | goto end 26 | 27 | :abort 28 | echo Aborted, nothing published 29 | 30 | :end 31 | pause --------------------------------------------------------------------------------