├── .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 | 
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
--------------------------------------------------------------------------------