From fa9b7830a330da3c55bc355a6967f99aa1d0ffa1 Mon Sep 17 00:00:00 2001 From: John Hurliman Date: Fri, 3 Aug 2007 07:07:49 +0000 Subject: [PATCH] * Added the beginnings of VoiceManager to libsecondlife.Utilities * Added VoiceTest to test VoiceManager. For now you will have to manually run the voice daemon (shipped with Second Life) each time git-svn-id: http://libopenmetaverse.googlecode.com/svn/trunk@1345 52acb1d6-8a22-11de-b505-999d5b087335 --- VoiceTest/VoiceTest.cs | 43 +++ VoiceTest/VoiceTest.csproj | 59 ++++ libsecondlife/MainAvatar.cs | 2 + libsecondlife/NetworkManager.cs | 2 +- libsecondlife/Simulator.cs | 2 +- .../libsecondlife.Utilities/TCPPipe.cs | 156 ++++++++ .../libsecondlife.Utilities/Utilities.cs | 30 ++ .../libsecondlife.Utilities/VoiceManager.cs | 332 ++++++++++++++++++ .../libsecondlife.Utilities.csproj | 2 + libsecondlife/libsecondlife.sln | 6 + 10 files changed, 632 insertions(+), 2 deletions(-) create mode 100644 VoiceTest/VoiceTest.cs create mode 100644 VoiceTest/VoiceTest.csproj create mode 100644 libsecondlife/libsecondlife.Utilities/TCPPipe.cs create mode 100644 libsecondlife/libsecondlife.Utilities/VoiceManager.cs diff --git a/VoiceTest/VoiceTest.cs b/VoiceTest/VoiceTest.cs new file mode 100644 index 00000000..7f733e25 --- /dev/null +++ b/VoiceTest/VoiceTest.cs @@ -0,0 +1,43 @@ +using System; +using libsecondlife; +using libsecondlife.Utilities; + +namespace VoiceTest +{ + class VoiceTest + { + static void Main(string[] args) + { + SecondLife client = new SecondLife(); + VoiceManager voice = new VoiceManager(client); + + if (voice.ConnectToDaemon()) + { + voice.RequestCaptureDevices(); + voice.RequestRenderDevices(); + + voice.RequestSetRenderDevice("Speakers (Realtek High Definiti"); + + voice.RequestSetSpeakerVolume(75); + voice.RequestSetCaptureVolume(75); + + voice.RequestStartTuningMode(10000); + + //voice.RequestCreateConnector(); + + + + voice.RequestRenderAudioStart("bugsbunny1.wav", true); + voice.RequestRenderAudioStart("bugsbunny1.wav", false); + + } + + Console.ReadKey(); + + voice.RequestRenderAudioStop(); + voice.RequestStopTuningMode(); + + Console.ReadKey(); + } + } +} diff --git a/VoiceTest/VoiceTest.csproj b/VoiceTest/VoiceTest.csproj new file mode 100644 index 00000000..50e4515b --- /dev/null +++ b/VoiceTest/VoiceTest.csproj @@ -0,0 +1,59 @@ + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {B69597A7-5DC5-4564-9089-727D0348EB4E} + Exe + Properties + VoiceTest + VoiceTest + + + true + full + false + ..\bin\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + ..\bin\ + TRACE + prompt + 4 + + + + + + + + + + + + {D9CDEDFB-8169-4B03-B57F-0DF638F044EC} + libsecondlife + + + {CE5E06C2-2428-416B-ADC1-F1FE16A0FB27} + libsecondlife.Utilities + + + + + + + + \ No newline at end of file diff --git a/libsecondlife/MainAvatar.cs b/libsecondlife/MainAvatar.cs index 08ae5c8c..85be013e 100644 --- a/libsecondlife/MainAvatar.cs +++ b/libsecondlife/MainAvatar.cs @@ -2034,6 +2034,8 @@ namespace libsecondlife { Client.Log("Got EstablishAgentCommunication for unknown sim " + ipAndPort, Helpers.LogLevel.Error); + + // FIXME: Should we use this opportunity to connect to the simulator? } else { diff --git a/libsecondlife/NetworkManager.cs b/libsecondlife/NetworkManager.cs index d9422f30..3994e27a 100644 --- a/libsecondlife/NetworkManager.cs +++ b/libsecondlife/NetworkManager.cs @@ -664,7 +664,7 @@ namespace libsecondlife Simulator oldSim = CurrentSim; lock (Simulators) CurrentSim = simulator; // CurrentSim is synchronized against Simulators - simulator.SetSeedCaps(seedcaps); + simulator.SetSeedCaps(seedcaps); // If the current simulator changed fire the callback if (OnCurrentSimChanged != null && simulator != oldSim) diff --git a/libsecondlife/Simulator.cs b/libsecondlife/Simulator.cs index b1b315f7..109564a4 100644 --- a/libsecondlife/Simulator.cs +++ b/libsecondlife/Simulator.cs @@ -423,7 +423,7 @@ namespace libsecondlife SimCaps = null; } - if (Client.Settings.ENABLE_CAPS) // [TODO] Implement caps + if (Client.Settings.ENABLE_CAPS) { // Connect to the new CAPS system if (!String.IsNullOrEmpty(seedcaps)) diff --git a/libsecondlife/libsecondlife.Utilities/TCPPipe.cs b/libsecondlife/libsecondlife.Utilities/TCPPipe.cs new file mode 100644 index 00000000..665f37d9 --- /dev/null +++ b/libsecondlife/libsecondlife.Utilities/TCPPipe.cs @@ -0,0 +1,156 @@ +using System; +using System.Net; +using System.Net.Sockets; + +public class TCPPipe +{ + protected class SocketPacket + { + public System.Net.Sockets.Socket TCPSocket; + public byte[] DataBuffer = new byte[1]; + } + + public delegate void OnReceiveLineCallback(string line); + public delegate void OnDisconnectedCallback(SocketException se); + + public event OnReceiveLineCallback OnReceiveLine; + public event OnDisconnectedCallback OnDisconnected; + + protected Socket _TCPSocket; + protected IAsyncResult _Result; + protected AsyncCallback _Callback; + protected string _Buffer = String.Empty; + + public bool Connected + { + get + { + if (_TCPSocket != null && _TCPSocket.Connected) + return true; + else + return false; + } + } + + public TCPPipe() + { + } + + public SocketException Connect(string address, int port) + { + if (_TCPSocket != null && _TCPSocket.Connected) + Disconnect(); + + try + { + IPAddress ip; + if (!IPAddress.TryParse(address, out ip)) + { + IPAddress[] ips = Dns.GetHostAddresses(address); + ip = ips[0]; + } + _TCPSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + IPEndPoint ipEndPoint = new IPEndPoint(ip, port); + _TCPSocket.Connect(ipEndPoint); + if (_TCPSocket.Connected) + { + WaitForData(); + return null; + } + else + { + return new SocketException(10000); + } + } + catch (SocketException se) + { + return se; + } + } + + public void Disconnect() + { + _TCPSocket.Disconnect(true); + } + + public void SendData(byte[] data) + { + if (Connected) + _TCPSocket.Send(data); + else + throw new InvalidOperationException("socket is not connected"); + } + + public void SendLine(string message) + { + if (Connected) + { + byte[] byData = System.Text.Encoding.ASCII.GetBytes(message + "\n"); + _TCPSocket.Send(byData); + } + else + { + throw new InvalidOperationException("socket is not connected"); + } + } + + void WaitForData() + { + try + { + if (_Callback == null) _Callback = new AsyncCallback(OnDataReceived); + SocketPacket packet = new SocketPacket(); + packet.TCPSocket = _TCPSocket; + _Result = _TCPSocket.BeginReceive(packet.DataBuffer, 0, packet.DataBuffer.Length, SocketFlags.None, _Callback, packet); + } + catch (SocketException se) + { + Console.WriteLine(se.Message); + } + } + + void ReceiveData(string data) + { + if (OnReceiveLine == null) return; + + string[] splitNull = { "\0" }; + string[] line = data.Split(splitNull, StringSplitOptions.None); + _Buffer += line[0]; + string[] splitLines = { "\r\n", "\r", "\n" }; + string[] lines = _Buffer.Split(splitLines, StringSplitOptions.None); + + if (lines.Length > 1) + { + int i; + for (i = 0; i < lines.Length - 1; i++) + { + if (lines[i].Trim().Length > 0) OnReceiveLine(lines[i]); + } + _Buffer = lines[i]; + } + } + + void OnDataReceived(IAsyncResult asyn) + { + try + { + SocketPacket packet = (SocketPacket)asyn.AsyncState; + int end = packet.TCPSocket.EndReceive(asyn); + char[] chars = new char[end + 1]; + System.Text.Decoder d = System.Text.Encoding.UTF8.GetDecoder(); + int charLen = d.GetChars(packet.DataBuffer, 0, end, chars, 0); + System.String data = new System.String(chars); + ReceiveData(data); + WaitForData(); + } + catch (ObjectDisposedException) + { + Console.WriteLine("WARNING: Socket closed unexpectedly"); + } + catch (SocketException se) + { + if (!_TCPSocket.Connected) OnDisconnected(se); + } + } + +} diff --git a/libsecondlife/libsecondlife.Utilities/Utilities.cs b/libsecondlife/libsecondlife.Utilities/Utilities.cs index 3c358ce8..ff847886 100644 --- a/libsecondlife/libsecondlife.Utilities/Utilities.cs +++ b/libsecondlife/libsecondlife.Utilities/Utilities.cs @@ -22,6 +22,36 @@ namespace libsecondlife.Utilities Underwater } + public enum Platform + { + Unknown, + Windows, + Linux, + OSX + } + + public static class SLPlatformTools + { + public const string OSX_CHECK_FILE = "/Library/Extensions.kextcache"; + + public static Platform GetRunningPlatform() + { + int plat = (int)Environment.OSVersion.Platform; + + if ((plat != 4) && (plat != 128)) + { + return Platform.Windows; + } + else + { + if (System.IO.File.Exists(OSX_CHECK_FILE)) + return Platform.OSX; + else + return Platform.Linux; + } + } + } + public static class Realism { /// diff --git a/libsecondlife/libsecondlife.Utilities/VoiceManager.cs b/libsecondlife/libsecondlife.Utilities/VoiceManager.cs new file mode 100644 index 00000000..9c4b5d84 --- /dev/null +++ b/libsecondlife/libsecondlife.Utilities/VoiceManager.cs @@ -0,0 +1,332 @@ +using System; +using System.Net.Sockets; +using System.Text; +using libsecondlife; + +namespace libsecondlife.Utilities +{ + public enum VoiceStatus + { + StatusLoginRetry, + StatusLoggedIn, + StatusJoining, + StatusJoined, + StatusLeftChannel, + BeginErrorStatus, + ErrorChannelFull, + ErrorChannelLocked, + ErrorNotAvailable, + ErrorUnknown + } + + public enum VoiceServiceType + { + /// Unknown voice service level + Unknown, + /// Spatialized local chat + TypeA, + /// Remote multi-party chat + TypeB, + /// One-to-one and small group chat + TypeC + } + + public class VoiceManager + { + public const string DAEMON_ARGS = " -p tcp -h -c -ll "; + public const int DAEMON_LOG_LEVEL = 1; + public const int DAEMON_PORT = 44124; + public const string VOICE_RELEASE_SERVER = "bhr.vivox.com"; + public const string VOICE_DEBUG_SERVER = "bhd.vivox.com"; + public const string REQUEST_TERMINATOR = "\n\n\n"; + + public SecondLife Client; + + protected TCPPipe _DaemonPipe; + protected VoiceStatus _Status; + protected int _CommandCookie = 0; + protected string _TuningSoundFile = String.Empty; + protected string _VoiceServer = VOICE_RELEASE_SERVER; + + public VoiceManager(SecondLife client) + { + Client = client; + } + + public bool IsDaemonRunning() + { + return false; + } + + public bool StartDaemon() + { + return false; + } + + public void StopDaemon() + { + } + + public bool ConnectToDaemon() + { + return ConnectToDaemon("127.0.0.1", DAEMON_PORT); + } + + public bool ConnectToDaemon(string address, int port) + { + _DaemonPipe = new TCPPipe(); + _DaemonPipe.OnDisconnected += new TCPPipe.OnDisconnectedCallback(_DaemonPipe_OnDisconnected); + _DaemonPipe.OnReceiveLine += new TCPPipe.OnReceiveLineCallback(_DaemonPipe_OnReceiveLine); + + SocketException se = _DaemonPipe.Connect(address, port); + + if (se == null) + { + return true; + } + else + { + Console.WriteLine("Connection failed: " + se.Message); + return false; + } + } + + public string VoiceAccountFromUUID(LLUUID id) + { + string result = "x" + Convert.ToBase64String(id.GetBytes()); + return result.Replace('+', '-').Replace('/', '_'); + } + + public LLUUID UUIDFromVoiceAccount(string accountName) + { + if (accountName.Length == 25 && accountName[0] == 'x' && accountName[23] == '=' && accountName[24] == '=') + { + accountName = accountName.Replace('/', '_').Replace('+', '-'); + byte[] idBytes = Convert.FromBase64String(accountName); + + if (idBytes.Length == 16) + return new LLUUID(idBytes, 0); + else + return LLUUID.Zero; + } + else + { + return LLUUID.Zero; + } + } + + public string SIPURIFromVoiceAccount(string account) + { + return String.Format("sip:{0}@{1}", account, _VoiceServer); + } + + public void RequestCaptureDevices() + { + if (_DaemonPipe.Connected) + { + _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( + "{1}", + _CommandCookie++, + REQUEST_TERMINATOR))); + } + else + { + Client.Log("VoiceManager.RequestCaptureDevices() called when the daemon pipe is disconnected", Helpers.LogLevel.Error); + } + } + + public void RequestRenderDevices() + { + if (_DaemonPipe.Connected) + { + _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( + "{1}", + _CommandCookie++, + REQUEST_TERMINATOR))); + } + else + { + Client.Log("VoiceManager.RequestRenderDevices() called when the daemon pipe is disconnected", Helpers.LogLevel.Error); + } + } + + public void RequestCreateConnector() + { + RequestCreateConnector(VOICE_RELEASE_SERVER); + } + + public void RequestCreateConnector(string voiceServer) + { + if (_DaemonPipe.Connected) + { + _VoiceServer = voiceServer; + + string accountServer = String.Format("https://www.{0}/api2/", _VoiceServer); + string logPath = "."; + + StringBuilder request = new StringBuilder(); + request.Append(String.Format("", _CommandCookie++)); + request.Append("V2 SDK"); + request.Append(String.Format("{0}", accountServer)); + request.Append(""); + request.Append("false"); + request.Append(String.Format("{0}", logPath)); + request.Append("vivox-gateway"); + request.Append(".log"); + request.Append("0"); + request.Append(""); + request.Append(""); + request.Append(REQUEST_TERMINATOR); + + _DaemonPipe.SendData(Encoding.ASCII.GetBytes(request.ToString())); + } + else + { + Client.Log("VoiceManager.CreateConnector() called when the daemon pipe is disconnected", Helpers.LogLevel.Error); + } + } + + public void RequestLogin(string accountName, string password, string connectorHandle) + { + if (_DaemonPipe.Connected) + { + StringBuilder request = new StringBuilder(); + request.Append(String.Format("", _CommandCookie++)); + request.Append(String.Format("{0}", connectorHandle)); + request.Append(String.Format("{0}", accountName)); + request.Append(String.Format("{0}", password)); + request.Append("VerifyAnswer"); + request.Append(""); + request.Append(REQUEST_TERMINATOR); + + _DaemonPipe.SendData(Encoding.ASCII.GetBytes(request.ToString())); + } + else + { + Client.Log("VoiceManager.Login() called when the daemon pipe is disconnected", Helpers.LogLevel.Error); + } + } + + public void RequestSetRenderDevice(string deviceName) + { + if (_DaemonPipe.Connected) + { + _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( + "{1}{2}", + _CommandCookie, deviceName, REQUEST_TERMINATOR))); + } + else + { + Client.Log("VoiceManager.RequestSetRenderDevice() called when the daemon pipe is disconnected", Helpers.LogLevel.Error); + } + } + + public void RequestStartTuningMode(int duration) + { + if (_DaemonPipe.Connected) + { + _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( + "{1}{2}", + _CommandCookie, duration, REQUEST_TERMINATOR))); + } + else + { + Client.Log("VoiceManager.RequestStartTuningMode() called when the daemon pipe is disconnected", Helpers.LogLevel.Error); + } + } + + public void RequestStopTuningMode() + { + if (_DaemonPipe.Connected) + { + _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( + "{1}", + _CommandCookie, REQUEST_TERMINATOR))); + } + else + { + Client.Log("VoiceManager.RequestStopTuningMode() called when the daemon pipe is disconnected", Helpers.LogLevel.Error); + } + } + + public void RequestSetSpeakerVolume(int volume) + { + if (volume < 0 || volume > 100) + throw new ArgumentException("volume must be between 0 and 100", "volume"); + + if (_DaemonPipe.Connected) + { + _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( + "{1}{2}", + _CommandCookie, volume, REQUEST_TERMINATOR))); + } + else + { + Client.Log("VoiceManager.RequestSetSpeakerVolume() called when the daemon pipe is disconnected", Helpers.LogLevel.Error); + } + } + + public void RequestSetCaptureVolume(int volume) + { + if (volume < 0 || volume > 100) + throw new ArgumentException("volume must be between 0 and 100", "volume"); + + if (_DaemonPipe.Connected) + { + _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( + "{1}{2}", + _CommandCookie, volume, REQUEST_TERMINATOR))); + } + else + { + Client.Log("VoiceManager.RequestSetCaptureVolume() called when the daemon pipe is disconnected", Helpers.LogLevel.Error); + } + } + + /// + /// Does not appear to be working + /// + /// + /// + public void RequestRenderAudioStart(string fileName, bool loop) + { + if (_DaemonPipe.Connected) + { + _TuningSoundFile = fileName; + + _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( + "{1}{2}{3}", + _CommandCookie++, _TuningSoundFile, (loop ? "1" : "0"), REQUEST_TERMINATOR))); + } + else + { + Client.Log("VoiceManager.RequestRenderAudioStart() called when the daemon pipe is disconnected", Helpers.LogLevel.Error); + } + } + + public void RequestRenderAudioStop() + { + if (_DaemonPipe.Connected) + { + _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( + "{1}{2}", + _CommandCookie++, _TuningSoundFile, REQUEST_TERMINATOR))); + } + else + { + Client.Log("VoiceManager.RequestRenderAudioStop() called when the daemon pipe is disconnected", Helpers.LogLevel.Error); + } + } + + private void _DaemonPipe_OnDisconnected(SocketException se) + { + if (se != null) Console.WriteLine("Disconnected! " + se.Message); + else Console.WriteLine("Disconnected!"); + } + + private void _DaemonPipe_OnReceiveLine(string line) + { + Client.DebugLog("VOICE: " + line); + } + } +} diff --git a/libsecondlife/libsecondlife.Utilities/libsecondlife.Utilities.csproj b/libsecondlife/libsecondlife.Utilities/libsecondlife.Utilities.csproj index 2ad096ff..a52fa31d 100644 --- a/libsecondlife/libsecondlife.Utilities/libsecondlife.Utilities.csproj +++ b/libsecondlife/libsecondlife.Utilities/libsecondlife.Utilities.csproj @@ -37,8 +37,10 @@ Code + + diff --git a/libsecondlife/libsecondlife.sln b/libsecondlife/libsecondlife.sln index 716032a1..a91922e9 100644 --- a/libsecondlife/libsecondlife.sln +++ b/libsecondlife/libsecondlife.sln @@ -42,6 +42,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "importprimscript", "..\impo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SLImageUpload", "..\SLImageUpload\SLImageUpload.csproj", "{F3BDC0BC-74EB-451E-8FFE-AA162474F2F2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VoiceTest", "..\VoiceTest\VoiceTest.csproj", "{B69597A7-5DC5-4564-9089-727D0348EB4E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -131,6 +133,10 @@ Global {F3BDC0BC-74EB-451E-8FFE-AA162474F2F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {F3BDC0BC-74EB-451E-8FFE-AA162474F2F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3BDC0BC-74EB-451E-8FFE-AA162474F2F2}.Release|Any CPU.Build.0 = Release|Any CPU + {B69597A7-5DC5-4564-9089-727D0348EB4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B69597A7-5DC5-4564-9089-727D0348EB4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B69597A7-5DC5-4564-9089-727D0348EB4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B69597A7-5DC5-4564-9089-727D0348EB4E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE