From 830e905300ed70b53ce7fb3da66ffe7e5ee9c947 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 8 Jun 2020 14:11:54 -0500 Subject: [PATCH] Initial Commit --- .gitignore | 199 +++++++++++++ QSharp.sln | 23 ++ QSharp/QBrowser.cs | 77 ++++++ QSharp/QClient.cs | 70 +++++ QSharp/QCue.cs | 19 ++ QSharp/QResponse.cs | 22 ++ QSharp/QServer.cs | 53 ++++ QSharp/QSharp.csproj | 11 + QSharp/QWorkspace.cs | 115 ++++++++ QSharp/QWorkspaceInfo.cs | 11 + QSharp/SharpOSC/Extensions.cs | 53 ++++ QSharp/SharpOSC/Midi.cs | 64 +++++ QSharp/SharpOSC/OscBundle.cs | 68 +++++ QSharp/SharpOSC/OscMessage.cs | 160 +++++++++++ QSharp/SharpOSC/OscPacket.cs | 492 +++++++++++++++++++++++++++++++++ QSharp/SharpOSC/RGBA.cs | 64 +++++ QSharp/SharpOSC/Symbol.cs | 69 +++++ QSharp/SharpOSC/TCPClient.cs | 156 +++++++++++ QSharp/SharpOSC/Timetag.cs | 92 ++++++ QSharp/SharpOSC/UDPListener.cs | 178 ++++++++++++ QSharp/SharpOSC/UDPSender.cs | 53 ++++ QSharp/SharpOSC/Utils.cs | 50 ++++ QSharpDemo/Program.cs | 19 ++ QSharpDemo/QSharpDemo.csproj | 11 + 24 files changed, 2129 insertions(+) create mode 100644 .gitignore create mode 100644 QSharp.sln create mode 100644 QSharp/QBrowser.cs create mode 100644 QSharp/QClient.cs create mode 100644 QSharp/QCue.cs create mode 100644 QSharp/QResponse.cs create mode 100644 QSharp/QServer.cs create mode 100644 QSharp/QSharp.csproj create mode 100644 QSharp/QWorkspace.cs create mode 100644 QSharp/QWorkspaceInfo.cs create mode 100755 QSharp/SharpOSC/Extensions.cs create mode 100755 QSharp/SharpOSC/Midi.cs create mode 100755 QSharp/SharpOSC/OscBundle.cs create mode 100755 QSharp/SharpOSC/OscMessage.cs create mode 100644 QSharp/SharpOSC/OscPacket.cs create mode 100755 QSharp/SharpOSC/RGBA.cs create mode 100644 QSharp/SharpOSC/Symbol.cs create mode 100644 QSharp/SharpOSC/TCPClient.cs create mode 100755 QSharp/SharpOSC/Timetag.cs create mode 100644 QSharp/SharpOSC/UDPListener.cs create mode 100644 QSharp/SharpOSC/UDPSender.cs create mode 100755 QSharp/SharpOSC/Utils.cs create mode 100644 QSharpDemo/Program.cs create mode 100644 QSharpDemo/QSharpDemo.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b24d23 --- /dev/null +++ b/.gitignore @@ -0,0 +1,199 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +Components +[Pp]ackages +*.userprefs +Resource.designer.cs + +# Visual Studio (>=2015) project-specific, machine local files +.vs/ + +# User-specific files +*.suo +*.user +*.sln.docstates +*.userprefs + +# ignore Xamarin.Android Resource.Designer.cs files +**/*.Droid/**/[Rr]esource.[Dd]esigner.cs +**/*.Android/**/[Rr]esource.[Dd]esigner.cs +**/Android/**/[Rr]esource.[Dd]esigner.cs +**/Droid/**/[Rr]esource.[Dd]esigner.cs + +# Xamarin Components +Components/ + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +#NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml + +# NuGet Packages Directory +packages/ +*.nuget.targets +*.lock.json +*.nuget.props + +## TODO: If the tool you use requires repositories.config uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets +# This line needs to be after the ignore of the build folder (and the packages folder if the line above has been uncommented) +!packages/build/ + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +.DS_Store +*.bak + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +.vscode/settings.json diff --git a/QSharp.sln b/QSharp.sln new file mode 100644 index 0000000..448ac76 --- /dev/null +++ b/QSharp.sln @@ -0,0 +1,23 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QSharp", "QSharp\QSharp.csproj", "{51CD2579-79C9-49A9-B68D-93CBE878BAA1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QSharpDemo", "QSharpDemo\QSharpDemo.csproj", "{8518D042-D511-4DBB-8406-5B11B2777ED2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {51CD2579-79C9-49A9-B68D-93CBE878BAA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51CD2579-79C9-49A9-B68D-93CBE878BAA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51CD2579-79C9-49A9-B68D-93CBE878BAA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51CD2579-79C9-49A9-B68D-93CBE878BAA1}.Release|Any CPU.Build.0 = Release|Any CPU + {8518D042-D511-4DBB-8406-5B11B2777ED2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8518D042-D511-4DBB-8406-5B11B2777ED2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8518D042-D511-4DBB-8406-5B11B2777ED2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8518D042-D511-4DBB-8406-5B11B2777ED2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/QSharp/QBrowser.cs b/QSharp/QBrowser.cs new file mode 100644 index 0000000..0239017 --- /dev/null +++ b/QSharp/QBrowser.cs @@ -0,0 +1,77 @@ +using Zeroconf; +using System; +using System.Collections.ObjectModel; + +namespace QSharp +{ + public class QBrowser + { + const string UDPServiceType = "_qlab._udp.local."; + const string TCPServiceType = "_qlab._tcp.local."; + + const int udpPort = 53000; + + IObservable netServices; + ZeroconfResolver.ResolverListener netServiceTCPBrowser; + + ObservableCollection servers = new ObservableCollection(); + + public QBrowser() + { + + Console.WriteLine("QBrowser Init"); + netServiceTCPBrowser = ZeroconfResolver.CreateListener(TCPServiceType); + netServiceTCPBrowser.ServiceFound += ServerFound; + netServiceTCPBrowser.ServiceLost += ServerLost; + } + + private void ServerLost(object sender, IZeroconfHost e) + { + Console.WriteLine($"Lost {e.DisplayName} : {e.IPAddress}"); + + } + + private void ServerFound(object sender, IZeroconfHost e) + { + + + foreach(var service in e.Services) + { + if (service.Key.Equals(TCPServiceType)) + { + Console.WriteLine($"Found {e.DisplayName} : {e.IPAddress} : {service.Value.Port}"); + + QServer server = getServerFromAddress(e.IPAddress); + + if(server == null) + { + Console.WriteLine("New Server Found so adding"); + QServer serverToAdd = new QServer(e.IPAddress, service.Value.Port, e.DisplayName); + servers.Add(serverToAdd); + serverToAdd.refreshWorkspaces(); + } + else + { + server.name = e.DisplayName; + Console.WriteLine("Server already exists in list so just updating name"); + } + } + } + + } + + public QServer getServerFromAddress(string address) + { + QServer foundServer = null; + foreach(var server in servers) + { + if (server.host.Equals(address)) + { + return server; + } + } + + return foundServer; + } + } +} \ No newline at end of file diff --git a/QSharp/QClient.cs b/QSharp/QClient.cs new file mode 100644 index 0000000..42eecd6 --- /dev/null +++ b/QSharp/QClient.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using SharpOSC; + +namespace QSharp +{ + public class QClient + { + TCPClient tcpClient; + + public class WorkspacesUpdatedArgs : EventArgs + { + public List Workspaces { get; set; } + } + + + public delegate void WorkspacesUpdatedHandler(object source, WorkspacesUpdatedArgs args); + public event WorkspacesUpdatedHandler WorkspacesUpdated; + + public QClient(string host, int port) + { + tcpClient = new TCPClient(host, port); + tcpClient.Connect(); + Console.WriteLine($"QClient setup {host}:{port}"); + tcpClient.MessageReceived += ResponseReceived; + } + + private void ResponseReceived(object source, MessageEventArgs args) + { + OscMessage msg = args.Message; + foreach (var obj in msg.Arguments) + { + QResponse response = JsonConvert.DeserializeObject(obj.ToString()); + + switch (response.getReplyType()) + { + case "WORKSPACES": + Console.WriteLine("WORKSPACES RECEIVED"); + OnWorkspacesUpdated(response); + break; + default: + break; + } + + } + } + + public void sendMessage(string address, params object[] args) + { + tcpClient.Send(new OscMessage(address, args)); + Console.WriteLine($"QClient send message {address}"); + } + + protected virtual void OnWorkspacesUpdated(QResponse response) + { + if(WorkspacesUpdated != null) + { + List workspaces = new List(); + foreach (var item in response.data) + { + QWorkspaceInfo workspacefound = JsonConvert.DeserializeObject(item.ToString()); + workspaces.Add(workspacefound); + Console.WriteLine(workspacefound.displayName); + } + WorkspacesUpdated(this, new WorkspacesUpdatedArgs { Workspaces = workspaces }); + } + } + } +} diff --git a/QSharp/QCue.cs b/QSharp/QCue.cs new file mode 100644 index 0000000..5007b05 --- /dev/null +++ b/QSharp/QCue.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace QSharp +{ + public class QCue + { + Dictionary cueData = new Dictionary(); + public QCue() + { + } + + public object propertyForKey(string key) + { + if (cueData.ContainsKey(key)) + return cueData[key]; + return null; + } + } +} diff --git a/QSharp/QResponse.cs b/QSharp/QResponse.cs new file mode 100644 index 0000000..0b3fdeb --- /dev/null +++ b/QSharp/QResponse.cs @@ -0,0 +1,22 @@ +using System; +using System.Text.RegularExpressions; + +namespace QSharp +{ + public class QResponse + { + private const string WORKSPACES_REGEX = @"/workspaces"; + public string status { get; set; } + public string address { get; set; } + public object[] data { get; set; } + + public string getReplyType() + { + Console.WriteLine($"QRESPONSE - Address to match: '{address}'"); + Match m = Regex.Match(address, WORKSPACES_REGEX); + if (m.Success) + return "WORKSPACES"; + return ""; + } + } +} diff --git a/QSharp/QServer.cs b/QSharp/QServer.cs new file mode 100644 index 0000000..7c68bab --- /dev/null +++ b/QSharp/QServer.cs @@ -0,0 +1,53 @@ +//General information about a QLab Workspace +using System.Collections.Generic; + +namespace QSharp +{ + public class QServer + { + private QClient qClient; + + public string host; + public int port; + public string name; + List workspaces = new List(); + + public QServer(string host, int port, string name) + { + this.host = host; + this.port = port; + this.name = name; + + qClient = new QClient(host, port); + qClient.WorkspacesUpdated += OnWorkspacesUpdated; + } + + private void OnWorkspacesUpdated(object source, QClient.WorkspacesUpdatedArgs args) + { + foreach(var workspace in args.Workspaces) + { + QWorkspace existingWorkspace = workspaceWithID(workspace.uniqueID); + if(existingWorkspace == null) + { + QWorkspace workspaceToAdd = new QWorkspace(workspace, this); + workspaces.Add(workspaceToAdd); + } + } + } + + public void refreshWorkspaces() + { + qClient.sendMessage("/workspaces"); + } + + public QWorkspace workspaceWithID(string uniqueID) + { + foreach(var workspace in workspaces) + { + if (workspace.getUniqueID().Equals(uniqueID)) + return workspace; + } + return null; + } + } +} diff --git a/QSharp/QSharp.csproj b/QSharp/QSharp.csproj new file mode 100644 index 0000000..50d81f1 --- /dev/null +++ b/QSharp/QSharp.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.1 + + + + + + + diff --git a/QSharp/QWorkspace.cs b/QSharp/QWorkspace.cs new file mode 100644 index 0000000..ef19599 --- /dev/null +++ b/QSharp/QWorkspace.cs @@ -0,0 +1,115 @@ +//TODO: connect methods, cue property fetch methods, everything else +namespace QSharp +{ + public class QWorkspace + { + + private QServer server; + private QClient client; + + private string name; + private string uniqueID; + private bool connected; + private bool hasPasscode; + private string oscPrefix; + + private void Init() + { + name = ""; + uniqueID = ""; + connected = false; + + hasPasscode = false; + + //TODO: add version + } + + public QWorkspace(QWorkspaceInfo workspaceInfo, QServer server) + { + if(workspaceInfo.uniqueID.Length > 0) + { + uniqueID = workspaceInfo.uniqueID; + oscPrefix = workspacePrefix(); + } + + //TODO add version if it has one if no version then it is QLab 3? + updateWithWorkspaceInfo(workspaceInfo); + + client = new QClient(server.host, server.port); + this.server = server; + + } + + public bool updateWithWorkspaceInfo(QWorkspaceInfo workspaceInfo) { + bool didUpdate = false; + if(workspaceInfo.displayName.Length > 0 && !workspaceInfo.displayName.Equals(this.name)) + { + this.name = workspaceInfo.displayName; + didUpdate = true; + } + if (!workspaceInfo.hasPasscode.Equals(this.hasPasscode)) + { + this.hasPasscode = workspaceInfo.hasPasscode; + didUpdate = true; + } + return didUpdate; + } + + + + + + public string getUniqueID() + { + return uniqueID; + } + + #region OSC address helpers + private string addressForCue(QCue cue, string action) + { + return $"{oscPrefix}/cue_id/{cue.propertyForKey("uniqueId")}/{action}"; + } + + private string addressForWildcardNumber(string number, string action) { + return $"{oscPrefix}/cue/{number}/{action}"; + } + + private string workspacePrefix() + { + return $"/workspace/{uniqueID}"; + } + #endregion + + #region Workspace Methods + public void disconnectFromWorkspace() { client.sendMessage($"{oscPrefix}/disconnect"); } + public void startReceivingUpdates() { client.sendMessage($"{oscPrefix}/updates", 1); } + public void stopReceivingUpdates() { client.sendMessage($"{oscPrefix}/updates", 0); } + public void enableAlwaysReply() { client.sendMessage($"{oscPrefix}/alwaysReply", 1); } + public void disableAlwaysReply() { client.sendMessage($"{oscPrefix}/alwaysReply", 0); } + + public void fetchQLabVersion() { + //TODO + } + + public void fetchCueLists() + { + //TODO + } + + public void fetchPlaybackPositionForCue() + { + //TODO + } + + public void go() { client.sendMessage($"{oscPrefix}/go"); } + public void save() { client.sendMessage($"{oscPrefix}/save"); } + public void undo() { client.sendMessage($"{oscPrefix}/undo"); } + public void redo() { client.sendMessage($"{oscPrefix}/redo"); } + public void resetAll() { client.sendMessage($"{oscPrefix}/reset"); } + public void pauseAll() { client.sendMessage($"{oscPrefix}/pause"); } + public void resumeAll() { client.sendMessage($"{oscPrefix}/resume"); } + public void stopAll() { client.sendMessage($"{oscPrefix}/stop"); } + public void panicAll() { client.sendMessage($"{oscPrefix}/panic"); } + #endregion + } +} diff --git a/QSharp/QWorkspaceInfo.cs b/QSharp/QWorkspaceInfo.cs new file mode 100644 index 0000000..969af11 --- /dev/null +++ b/QSharp/QWorkspaceInfo.cs @@ -0,0 +1,11 @@ +//General information about a QLab Workspace +namespace QSharp +{ + public class QWorkspaceInfo + { + public string version { get; set; } + public string displayName { get; set; } + public string uniqueID { get; set; } + public bool hasPasscode { get; set; } + } +} diff --git a/QSharp/SharpOSC/Extensions.cs b/QSharp/SharpOSC/Extensions.cs new file mode 100755 index 0000000..b469fe7 --- /dev/null +++ b/QSharp/SharpOSC/Extensions.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SharpOSC +{ + internal static class Extensions + { + public static int FirstIndexAfter(this IEnumerable items, int start, Func predicate) + { + if (items == null) throw new ArgumentNullException("items"); + if (predicate == null) throw new ArgumentNullException("predicate"); + if (start >= items.Count()) throw new ArgumentOutOfRangeException("start"); + + int retVal = 0; + foreach (var item in items) + { + if (retVal >= start && predicate(item)) return retVal; + retVal++; + } + return -1; + } + + public static List> Split(this IEnumerable data, Func predicate) + { + var output = new List>(); + var curr = new List(); + output.Add(curr); + foreach (var x in data) + { + if (predicate(x)) + { + curr = new List(); + output.Add(curr); + } + else + curr.Add(x); + } + + return output; + } + + + + public static T[] SubArray(this T[] data, int index, int length) + { + T[] result = new T[length]; + Array.Copy(data, index, result, 0, length); + return result; + } + } +} diff --git a/QSharp/SharpOSC/Midi.cs b/QSharp/SharpOSC/Midi.cs new file mode 100755 index 0000000..d494112 --- /dev/null +++ b/QSharp/SharpOSC/Midi.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SharpOSC +{ + public struct Midi + { + public byte Port; + public byte Status; + public byte Data1; + public byte Data2; + + public Midi(byte port, byte status, byte data1, byte data2) + { + this.Port = port; + this.Status = status; + this.Data1 = data1; + this.Data2 = data2; + } + + public override bool Equals(System.Object obj) + { + if (obj.GetType() == typeof(Midi)) + { + if (this.Port == ((Midi)obj).Port && this.Status == ((Midi)obj).Status && this.Data1 == ((Midi)obj).Data1 && this.Data2 == ((Midi)obj).Data2) + return true; + else + return false; + } + else if (obj.GetType() == typeof(byte[])) + { + if (this.Port == ((byte[])obj)[0] && this.Status == ((byte[])obj)[1] && this.Data1 == ((byte[])obj)[2] && this.Data2 == ((byte[])obj)[3]) + return true; + else + return false; + } + else + return false; + } + + public static bool operator ==(Midi a, Midi b) + { + if (a.Equals(b)) + return true; + else + return false; + } + + public static bool operator !=(Midi a, Midi b) + { + if (!a.Equals(b)) + return true; + else + return false; + } + + public override int GetHashCode() + { + return (Port << 24) + (Status << 16) + (Data1 << 8) + (Data2); + } + } +} diff --git a/QSharp/SharpOSC/OscBundle.cs b/QSharp/SharpOSC/OscBundle.cs new file mode 100755 index 0000000..1f1fe40 --- /dev/null +++ b/QSharp/SharpOSC/OscBundle.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SharpOSC +{ + public class OscBundle : OscPacket + { + Timetag _timetag; + + public UInt64 Timetag + { + get { return _timetag.Tag; } + set { _timetag.Tag = value; } + } + + public DateTime Timestamp + { + get { return _timetag.Timestamp; } + set { _timetag.Timestamp = value; } + } + + public List Messages; + + public OscBundle(UInt64 timetag, params OscMessage[] args) + { + _timetag = new Timetag(timetag); + Messages = new List(); + Messages.AddRange(args); + } + + public override byte[] GetBytes() + { + string bundle = "#bundle"; + int bundleTagLen = Utils.AlignedStringLength(bundle); + byte[] tag = setULong(_timetag.Tag); + + List outMessages = new List(); + foreach (OscMessage msg in Messages) + { + outMessages.Add(msg.GetBytes()); + } + + int len = bundleTagLen + tag.Length + outMessages.Sum(x => x.Length + 4); + + int i = 0; + byte[] output = new byte[len]; + Encoding.UTF8.GetBytes(bundle).CopyTo(output, i); + i += bundleTagLen; + tag.CopyTo(output, i); + i += tag.Length; + + foreach (byte[] msg in outMessages) + { + var size = setInt(msg.Length); + size.CopyTo(output, i); + i += size.Length; + + msg.CopyTo(output, i); + i += msg.Length; // msg size is always a multiple of 4 + } + + return output; + } + + } +} diff --git a/QSharp/SharpOSC/OscMessage.cs b/QSharp/SharpOSC/OscMessage.cs new file mode 100755 index 0000000..f34eced --- /dev/null +++ b/QSharp/SharpOSC/OscMessage.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SharpOSC +{ + public class OscMessage : OscPacket + { + public string Address; + public List Arguments; + + public OscMessage(string address, params object[] args) + { + this.Address = address; + Arguments = new List(); + Arguments.AddRange(args); + } + + public override byte[] GetBytes() + { + List parts = new List(); + + List currentList = Arguments; + int ArgumentsIndex = 0; + + string typeString = ","; + int i = 0; + while (i < currentList.Count) + { + var arg = currentList[i]; + + string type = (arg != null) ? arg.GetType().ToString() : "null"; + switch (type) + { + case "System.Int32": + typeString += "i"; + parts.Add(setInt((int)arg)); + break; + case "System.Single": + if (float.IsPositiveInfinity((float)arg)) + { + typeString += "I"; + } + else + { + typeString += "f"; + parts.Add(setFloat((float)arg)); + } + break; + case "System.String": + typeString += "s"; + parts.Add(setString((string)arg)); + break; + case "System.Byte[]": + typeString += "b"; + parts.Add(setBlob((byte[])arg)); + break; + case "System.Int64": + typeString += "h"; + parts.Add(setLong((Int64)arg)); + break; + case "System.UInt64": + typeString += "t"; + parts.Add(setULong((UInt64)arg)); + break; + case "SharpOSC.Timetag": + typeString += "t"; + parts.Add(setULong(((Timetag)arg).Tag)); + break; + case "System.Double": + if (Double.IsPositiveInfinity((double)arg)) + { + typeString += "I"; + } + else + { + typeString += "d"; + parts.Add(setDouble((double)arg)); + } + break; + + case "SharpOSC.Symbol": + typeString += "S"; + parts.Add(setString(((Symbol)arg).Value)); + break; + + case "System.Char": + typeString += "c"; + parts.Add(setChar((char)arg)); + break; + case "SharpOSC.RGBA": + typeString += "r"; + parts.Add(setRGBA((RGBA)arg)); + break; + case "SharpOSC.Midi": + typeString += "m"; + parts.Add(setMidi((Midi)arg)); + break; + case "System.Boolean": + typeString += ((bool)arg) ? "T" : "F"; + break; + case "null": + typeString += "N"; + break; + + // This part handles arrays. It points currentList to the array and resets i + // The array is processed like normal and when it is finished we replace + // currentList back with Arguments and continue from where we left off + case "System.Object[]": + case "System.Collections.Generic.List`1[System.Object]": + if(arg.GetType() == typeof(object[])) + arg = ((object[])arg).ToList(); + + if (Arguments != currentList) + throw new Exception("Nested Arrays are not supported"); + typeString += "["; + currentList = (List)arg; + ArgumentsIndex = i; + i = 0; + continue; + + default: + throw new Exception("Unable to transmit values of type " + type); + } + + i++; + if (currentList != Arguments && i == currentList.Count) + { + // End of array, go back to main Argument list + typeString += "]"; + currentList = Arguments; + i = ArgumentsIndex+1; + } + } + + int addressLen = (Address.Length == 0 || Address == null ) ? 0 : Utils.AlignedStringLength(Address); + int typeLen = Utils.AlignedStringLength(typeString); + + int total = addressLen + typeLen + parts.Sum(x => x.Length); + + byte[] output = new byte[total]; + i = 0; + + Encoding.UTF8.GetBytes(Address).CopyTo(output, i); + i += addressLen; + + Encoding.UTF8.GetBytes(typeString).CopyTo(output, i); + i += typeLen; + + foreach (byte[] part in parts) + { + part.CopyTo(output, i); + i += part.Length; + } + + return output; + } + } +} diff --git a/QSharp/SharpOSC/OscPacket.cs b/QSharp/SharpOSC/OscPacket.cs new file mode 100644 index 0000000..47d775c --- /dev/null +++ b/QSharp/SharpOSC/OscPacket.cs @@ -0,0 +1,492 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SharpOSC +{ + public abstract class OscPacket + { + public static OscPacket GetPacket(byte[] OscData) + { + if (OscData[0] == '#') + return parseBundle(OscData); + else + return parseMessage(OscData); + } + + public abstract byte[] GetBytes(); + + #region Parse OSC packages + + /// + /// Takes in an OSC bundle package in byte form and parses it into a more usable OscBundle object + /// + /// + /// Message containing various arguments and an address + private static OscMessage parseMessage(byte[] msg) + { + int index = 0; + //Log.Debug("Raw ASCII DATA: " + System.Text.Encoding.ASCII.GetString(msg)); + //Log.Debug("Raw UTF-8 DATA: " + System.Text.Encoding.UTF8.GetString(msg)); + string address = null; + char[] types = new char[0]; + List arguments = new List(); + List mainArray = arguments; // used as a reference when we are parsing arrays to get the main array back + // Get address + address = getAddress(msg, index); + index += msg.FirstIndexAfter(address.Length, x => x == ','); + + if (index % 4 != 0) + throw new Exception("Misaligned OSC Packet data. Address string is not padded correctly and does not align to 4 byte interval"); + + // Get type tags + types = getTypes(msg, index); + index += types.Length; + + while (index % 4 != 0) + index++; + + bool commaParsed = false; + + foreach (char type in types) + { + // skip leading comma + if (type == ',' && !commaParsed) + { + commaParsed = true; + continue; + } + + switch(type) + { + case ('\0'): + break; + + case('i'): + int intVal = getInt(msg, index); + arguments.Add(intVal); + index += 4; + break; + + case('f'): + float floatVal = getFloat(msg, index); + arguments.Add(floatVal); + index += 4; + break; + + case('s'): + string stringVal = getString(msg, index); + arguments.Add(stringVal); + index += stringVal.Length; + break; + + case('b'): + byte[] blob = getBlob(msg, index); + arguments.Add(blob); + index += 4 + blob.Length; + break; + + case ('h'): + Int64 hval = getLong(msg, index); + arguments.Add(hval); + index += 8; + break; + + case ('t'): + UInt64 sval = getULong(msg, index); + arguments.Add(new Timetag(sval)); + index += 8; + break; + + case ('d'): + double dval = getDouble(msg, index); + arguments.Add(dval); + index += 8; + break; + + case ('S'): + string SymbolVal = getString(msg, index); + arguments.Add(new Symbol(SymbolVal)); + index += SymbolVal.Length; + break; + + case ('c'): + char cval = getChar(msg, index); + arguments.Add(cval); + index += 4; + break; + + case ('r'): + RGBA rgbaval = getRGBA(msg, index); + arguments.Add(rgbaval); + index += 4; + break; + + case ('m'): + Midi midival = getMidi(msg, index); + arguments.Add(midival); + index += 4; + break; + + case ('T'): + arguments.Add(true); + break; + + case ('F'): + arguments.Add(false); + break; + + case ('N'): + arguments.Add(null); + break; + + case ('I'): + arguments.Add(double.PositiveInfinity); + break; + + case ('['): + if (arguments != mainArray) + throw new Exception("SharpOSC does not support nested arrays"); + arguments = new List(); // make arguments point to a new object array + break; + + case (']'): + mainArray.Add(arguments); // add the array to the main array + arguments = mainArray; // make arguments point back to the main array + break; + + default: + throw new Exception("OSC type tag '" + type + "' is unknown."); + } + + while (index % 4 != 0) + index++; + } + + return new OscMessage(address, arguments.ToArray()); + } + + /// + /// Takes in an OSC bundle package in byte form and parses it into a more usable OscBundle object + /// + /// + /// Bundle containing elements and a timetag + private static OscBundle parseBundle(byte[] bundle) + { + UInt64 timetag; + List messages = new List(); + + int index = 0; + + var bundleTag = Encoding.UTF8.GetString(bundle.SubArray(0, 8)); + index += 8; + + timetag = getULong(bundle, index); + index += 8; + + if (bundleTag != "#bundle\0") + throw new Exception("Not a bundle"); + + while (index < bundle.Length) + { + int size = getInt(bundle, index); + index += 4; + + byte[] messageBytes = bundle.SubArray(index, size); + var message = parseMessage(messageBytes); + + messages.Add(message); + + index += size; + while (index % 4 != 0) + index++; + } + + OscBundle output = new OscBundle(timetag, messages.ToArray()); + return output; + } + + #endregion + + #region Get arguments from byte array + + private static string getAddress(byte[] msg, int index) + { + int i = index; + string address = ""; + for (; i < msg.Length; i += 4) + { + if (msg[i] == ',') + { + if (i == 0) + return ""; + + address = Encoding.UTF8.GetString(msg.SubArray(index, i - 1)); + break; + } + } + + if (i >= msg.Length && address == null) + throw new Exception("no comma found"); + + return address.Replace("\0", ""); + } + + private static char[] getTypes(byte[] msg, int index) + { + int i = index + 4; + char[] types = null; + + for (; i < msg.Length; i += 4) + { + if (msg[i - 1] == 0) + { + types = Encoding.UTF8.GetChars(msg.SubArray(index, i - index)); + break; + } + } + + if (types == null) + { + byte[] term = { 0 }; + types = Encoding.UTF8.GetChars(term); + } + + if (i >= msg.Length && types == null) + throw new Exception("No null terminator after type string"); + + return types; + } + + private static int getInt(byte[] msg, int index) + { + int val = (msg[index] << 24) + (msg[index + 1] << 16) + (msg[index + 2] << 8) + (msg[index + 3] << 0); + return val; + } + + private static float getFloat(byte[] msg, int index) + { + byte[] reversed = new byte[4]; + reversed[3] = msg[index]; + reversed[2] = msg[index+1]; + reversed[1] = msg[index+2]; + reversed[0] = msg[index + 3]; + float val = System.BitConverter.ToSingle(reversed, 0); + return val; + } + + private static string getString(byte[] msg, int index) + { + string output = null; + int i = index + 4; + for (; (i-1) < msg.Length; i += 4) + { + if (msg[i - 1] == 0) + { + output = Encoding.UTF8.GetString(msg.SubArray(index, i - index)); + break; + } + } + if (output == null) + { + byte[] term = { 0 }; + output = Encoding.UTF8.GetString(term); + } + if (i >= msg.Length && output == null) + throw new Exception("No null terminator after type string"); + + return output.Replace("\0", ""); + } + + private static byte[] getBlob(byte[] msg, int index) + { + int size = getInt(msg, index); + return msg.SubArray(index + 4, size); + } + + private static UInt64 getULong(byte[] msg, int index) + { + UInt64 val = ((UInt64)msg[index] << 56) + ((UInt64)msg[index + 1] << 48) + ((UInt64)msg[index + 2] << 40) + ((UInt64)msg[index + 3] << 32) + + ((UInt64)msg[index + 4] << 24) + ((UInt64)msg[index + 5] << 16) + ((UInt64)msg[index + 6] << 8) + ((UInt64)msg[index + 7] << 0); + return val; + } + + private static Int64 getLong(byte[] msg, int index) + { + byte[] var = new byte[8]; + var[7] = msg[index]; + var[6] = msg[index+1]; + var[5] = msg[index+2]; + var[4] = msg[index+3]; + var[3] = msg[index+4]; + var[2] = msg[index+5]; + var[1] = msg[index+6]; + var[0] = msg[index+7]; + + Int64 val = BitConverter.ToInt64(var, 0); + return val; + } + + private static double getDouble(byte[] msg, int index) + { + byte[] var = new byte[8]; + var[7] = msg[index]; + var[6] = msg[index + 1]; + var[5] = msg[index + 2]; + var[4] = msg[index + 3]; + var[3] = msg[index + 4]; + var[2] = msg[index + 5]; + var[1] = msg[index + 6]; + var[0] = msg[index + 7]; + + double val = BitConverter.ToDouble(var, 0); + return val; + } + + private static char getChar(byte[] msg, int index) + { + return (char)msg[index + 3]; + } + + private static RGBA getRGBA(byte[] msg, int index) + { + return new RGBA(msg[index], msg[index + 1], msg[index + 2], msg[index + 3]); + } + + private static Midi getMidi(byte[] msg, int index) + { + return new Midi(msg[index], msg[index + 1], msg[index + 2], msg[index + 3]); + } + + #endregion + + #region Create byte arrays for arguments + + protected static byte[] setInt(int value) + { + byte[] msg = new byte[4]; + + var bytes = BitConverter.GetBytes(value); + msg[0] = bytes[3]; + msg[1] = bytes[2]; + msg[2] = bytes[1]; + msg[3] = bytes[0]; + + return msg; + } + + protected static byte[] setFloat(float value) + { + byte[] msg = new byte[4]; + + var bytes = BitConverter.GetBytes(value); + msg[0] = bytes[3]; + msg[1] = bytes[2]; + msg[2] = bytes[1]; + msg[3] = bytes[0]; + + return msg; + } + + protected static byte[] setString(string value) + { + var bytes = Encoding.UTF8.GetBytes(value); + int len = bytes.Length + (4 - bytes.Length % 4); + if (len <= bytes.Length) len = len + 4; + + byte[] msg = new byte[len]; + bytes.CopyTo(msg, 0); + + return msg; + } + + protected static byte[] setBlob(byte[] value) + { + int len = value.Length + 4; + len = len + (4 - len % 4); + + byte[] msg = new byte[len]; + byte[] size = setInt(value.Length); + size.CopyTo(msg, 0); + value.CopyTo(msg, 4); + return msg; + } + + protected static byte[] setLong(Int64 value) + { + byte[] rev = BitConverter.GetBytes(value); + byte[] output = new byte[8]; + output[0] = rev[7]; + output[1] = rev[6]; + output[2] = rev[5]; + output[3] = rev[4]; + output[4] = rev[3]; + output[5] = rev[2]; + output[6] = rev[1]; + output[7] = rev[0]; + return output; + } + + protected static byte[] setULong(UInt64 value) + { + byte[] rev = BitConverter.GetBytes(value); + byte[] output = new byte[8]; + output[0] = rev[7]; + output[1] = rev[6]; + output[2] = rev[5]; + output[3] = rev[4]; + output[4] = rev[3]; + output[5] = rev[2]; + output[6] = rev[1]; + output[7] = rev[0]; + return output; + } + + protected static byte[] setDouble(double value) + { + byte[] rev = BitConverter.GetBytes(value); + byte[] output = new byte[8]; + output[0] = rev[7]; + output[1] = rev[6]; + output[2] = rev[5]; + output[3] = rev[4]; + output[4] = rev[3]; + output[5] = rev[2]; + output[6] = rev[1]; + output[7] = rev[0]; + return output; + } + + protected static byte[] setChar(char value) + { + byte[] output = new byte[4]; + output[0] = 0; + output[1] = 0; + output[2] = 0; + output[3] = (byte)value; + return output; + } + + protected static byte[] setRGBA(RGBA value) + { + byte[] output = new byte[4]; + output[0] = value.R; + output[1] = value.G; + output[2] = value.B; + output[3] = value.A; + return output; + } + + protected static byte[] setMidi(Midi value) + { + byte[] output = new byte[4]; + output[0] = value.Port; + output[1] = value.Status; + output[2] = value.Data1; + output[3] = value.Data2; + return output; + } + + #endregion + } +} diff --git a/QSharp/SharpOSC/RGBA.cs b/QSharp/SharpOSC/RGBA.cs new file mode 100755 index 0000000..b17ef95 --- /dev/null +++ b/QSharp/SharpOSC/RGBA.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SharpOSC +{ + public struct RGBA + { + public byte R; + public byte G; + public byte B; + public byte A; + + public RGBA(byte red, byte green, byte blue, byte alpha) + { + this.R = red; + this.G = green; + this.B = blue; + this.A = alpha; + } + + public override bool Equals(System.Object obj) + { + if (obj.GetType() == typeof(RGBA)) + { + if (this.R == ((RGBA)obj).R && this.G == ((RGBA)obj).G && this.B == ((RGBA)obj).B && this.A == ((RGBA)obj).A) + return true; + else + return false; + } + else if (obj.GetType() == typeof(byte[])) + { + if (this.R == ((byte[])obj)[0] && this.G == ((byte[])obj)[1] && this.B == ((byte[])obj)[2] && this.A == ((byte[])obj)[3]) + return true; + else + return false; + } + else + return false; + } + + public static bool operator ==(RGBA a, RGBA b) + { + if (a.Equals(b)) + return true; + else + return false; + } + + public static bool operator !=(RGBA a, RGBA b) + { + if (!a.Equals(b)) + return true; + else + return false; + } + + public override int GetHashCode() + { + return (R << 24) + (G << 16) + (B << 8) + (A); + } + } +} diff --git a/QSharp/SharpOSC/Symbol.cs b/QSharp/SharpOSC/Symbol.cs new file mode 100644 index 0000000..7e4e289 --- /dev/null +++ b/QSharp/SharpOSC/Symbol.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SharpOSC +{ + public class Symbol + { + public string Value; + + public Symbol() + { + Value = ""; + } + + public Symbol(string value) + { + this.Value = value; + } + + override + public string ToString() + { + return Value; + } + + public override bool Equals(System.Object obj) + { + if (obj.GetType() == typeof(Symbol)) + { + if (this.Value == ((Symbol)obj).Value) + return true; + else + return false; + } + else if (obj.GetType() == typeof(string)) + { + if (this.Value == ((string)obj)) + return true; + else + return false; + } + else + return false; + } + + public static bool operator ==(Symbol a, Symbol b) + { + if (a.Equals(b)) + return true; + else + return false; + } + + public static bool operator !=(Symbol a, Symbol b) + { + if (!a.Equals(b)) + return true; + else + return false; + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + } +} diff --git a/QSharp/SharpOSC/TCPClient.cs b/QSharp/SharpOSC/TCPClient.cs new file mode 100644 index 0000000..e16df28 --- /dev/null +++ b/QSharp/SharpOSC/TCPClient.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Net.Sockets; +using System.Threading; + +namespace SharpOSC +{ + public class MessageEventArgs : EventArgs + { + public OscMessage Message + { + get; + set; + } + } + + public class TCPClient + { + public int Port + { + get { return _port; } + } + int _port; + + public string Address + { + get { return _address; } + } + public delegate void MessageReceivedHandler(object source, MessageEventArgs args); + public event MessageReceivedHandler MessageReceived; + + string _address; + TcpClient client; + + byte END = 0xc0; + byte ESC = 0xdb; + byte ESC_END = 0xDC; + byte ESC_ESC = 0xDD; + + + public TCPClient(string address, int port) + { + _port = port; + _address = address; + } + + public void Connect() + { + client = new TcpClient(Address, Port); + Console.WriteLine($"TCPClient - connect called for <{Address}:{Port}>"); + Thread receivingThread = new Thread(ReceiveLoop); + receivingThread.Start(); + } + + public void Send(byte[] message) + { + byte[] slipData = SlipEncode(message); + NetworkStream netStream = client.GetStream(); + netStream.Write(slipData.ToArray(), 0, slipData.ToArray().Length); + } + + public void Send(OscPacket packet) + { + byte[] data = packet.GetBytes(); + Send(data); + } + + public void ReceiveLoop() + { + while (client.Connected) + { + Receive(); + } + Console.WriteLine("TCPClient - Receive Loop has exited for some reason"); + } + + public void Receive() + { + Random random = new Random(); + int num = random.Next(1000); + NetworkStream netStream = client.GetStream(); + try + { + netStream.ReadTimeout = 250; + List responseData = new List(); + if (netStream.CanRead) + { + //var watch = System.Diagnostics.Stopwatch.StartNew(); + byte[] buffer = new byte[256]; + + int bytesRead = 0; + int reads = 0; + do + { + bytesRead = netStream.Read(buffer, 0, buffer.Length); + responseData.AddRange(buffer); + reads += 1; + Thread.Sleep(1); + //Log.Debug("Thread " + num + ": Bytes read: " + bytesRead + " - " + Encoding.UTF8.GetString(buffer)); + } while (netStream.DataAvailable); + + //Console.WriteLine("Raw TCP In: " + System.Text.Encoding.UTF8.GetString(responseData.ToArray())); + OscMessage response = (OscMessage)OscPacket.GetPacket(responseData.Skip(1).ToArray()); + //watch.Stop(); + //Console.WriteLine($"TCPCLient - message receive took {watch.ElapsedMilliseconds}ms and {reads} reads"); + OnMessageReceived(response); + } + } catch(Exception e) + { + //Log.Debug("TCPSENDER - Receive Exception: " + e.ToString()); + } + } + + public byte[] SlipEncode(byte[] data) + { + List slipData = new List(); + + byte[] esc_end = { ESC, ESC_END }; + byte[] esc_esc = { ESC, ESC_ESC }; + byte[] end = { END }; + + int length = data.Length; + for (int i = 0; i < length; i++) + { + if (data[i] == END) + { + slipData.AddRange(esc_end); + } + else if (data[i] == ESC) + { + slipData.AddRange(esc_esc); + } + else + { + slipData.Add(data[i]); + } + } + slipData.AddRange(end); + return slipData.ToArray(); + } + + public void Close() + { + client.GetStream().Close(); + client.Close(); + } + + protected virtual void OnMessageReceived(OscMessage msg) + { + if (MessageReceived != null) + MessageReceived(this, new MessageEventArgs() { Message = msg }); + } + } +} diff --git a/QSharp/SharpOSC/Timetag.cs b/QSharp/SharpOSC/Timetag.cs new file mode 100755 index 0000000..6a6c369 --- /dev/null +++ b/QSharp/SharpOSC/Timetag.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SharpOSC +{ + public struct Timetag + { + public UInt64 Tag; + + public DateTime Timestamp + { + get + { + return Utils.TimetagToDateTime(Tag); + } + set + { + Tag = Utils.DateTimeToTimetag(value); + } + } + + /// + /// Gets or sets the fraction of a second in the timestamp. the double precision number is multiplied by 2^32 + /// giving an accuracy down to about 230 picoseconds ( 1/(2^32) of a second) + /// + public double Fraction + { + get + { + return Utils.TimetagToFraction(Tag); + } + set + { + Tag = (Tag & 0xFFFFFFFF00000000) + (UInt32)(value * 0xFFFFFFFF); + } + } + + public Timetag(UInt64 value) + { + this.Tag = value; + } + + public Timetag(DateTime value) + { + Tag = 0; + this.Timestamp = value; + } + + public override bool Equals(System.Object obj) + { + if (obj.GetType() == typeof(Timetag)) + { + if (this.Tag == ((Timetag)obj).Tag) + return true; + else + return false; + } + else if (obj.GetType() == typeof(UInt64)) + { + if (this.Tag == ((UInt64)obj)) + return true; + else + return false; + } + else + return false; + } + + public static bool operator ==(Timetag a, Timetag b) + { + if (a.Equals(b)) + return true; + else + return false; + } + + public static bool operator !=(Timetag a, Timetag b) + { + if (a.Equals(b)) + return true; + else + return false; + } + + public override int GetHashCode() + { + return (int)( ((uint)(Tag >> 32) + (uint)(Tag & 0x00000000FFFFFFFF)) / 2); + } + } +} diff --git a/QSharp/SharpOSC/UDPListener.cs b/QSharp/SharpOSC/UDPListener.cs new file mode 100644 index 0000000..cee6e35 --- /dev/null +++ b/QSharp/SharpOSC/UDPListener.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Net; +using System.Threading; + +namespace SharpOSC +{ + public delegate void HandleOscPacket(OscPacket packet); + public delegate void HandleBytePacket(byte[] packet); + + public class UDPListener : IDisposable + { + public int Port { get; private set; } + + object callbackLock; + + UdpClient receivingUdpClient; + IPEndPoint RemoteIpEndPoint; + + HandleBytePacket BytePacketCallback = null; + HandleOscPacket OscPacketCallback = null; + + Queue queue; + ManualResetEvent ClosingEvent; + + public UDPListener(int port) + { + Port = port; + queue = new Queue(); + ClosingEvent = new ManualResetEvent(false); + callbackLock = new object(); + + // try to open the port 10 times, else fail + for (int i = 0; i < 10; i++) + { + try + { + receivingUdpClient = new UdpClient(port); + break; + } + catch (Exception) + { + // Failed in ten tries, throw the exception and give up + if (i >= 9) + throw; + + Thread.Sleep(5); + } + } + RemoteIpEndPoint = new IPEndPoint(IPAddress.Any, 0); + + // setup first async event + AsyncCallback callBack = new AsyncCallback(ReceiveCallback); + receivingUdpClient.BeginReceive(callBack, null); + } + + public UDPListener(int port, HandleOscPacket callback) : this(port) + { + this.OscPacketCallback = callback; + } + + public UDPListener(int port, HandleBytePacket callback) : this(port) + { + this.BytePacketCallback = callback; + } + + void ReceiveCallback(IAsyncResult result) + { + Monitor.Enter(callbackLock); + Byte[] bytes = null; + + try + { + bytes = receivingUdpClient.EndReceive(result, ref RemoteIpEndPoint); + } + catch (ObjectDisposedException e) + { + // Ignore if disposed. This happens when closing the listener + } + + // Process bytes + if (bytes != null && bytes.Length > 0) + { + if (BytePacketCallback != null) + { + BytePacketCallback(bytes); + } + else if (OscPacketCallback != null) + { + OscPacket packet = null; + try + { + packet = OscPacket.GetPacket(bytes); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + // If there is an error reading the packet, null is sent to the callback + } + //Log.Debug("Raw UDP In: " + System.Text.Encoding.ASCII.GetString(bytes)); + OscPacketCallback(packet); + } + else + { + lock (queue) + { + queue.Enqueue(bytes); + } + } + } + + if (closing) + ClosingEvent.Set(); + else + { + // Setup next async event + AsyncCallback callBack = new AsyncCallback(ReceiveCallback); + receivingUdpClient.BeginReceive(callBack, null); + } + Monitor.Exit(callbackLock); + } + + bool closing = false; + public void Close() + { + lock (callbackLock) + { + ClosingEvent.Reset(); + closing = true; + receivingUdpClient.Close(); + } + ClosingEvent.WaitOne(); + + } + + public void Dispose() + { + this.Close(); + } + + public OscPacket Receive() + { + if (closing) throw new Exception("UDPListener has been closed."); + + lock (queue) + { + if (queue.Count() > 0) + { + byte[] bytes = queue.Dequeue(); + var packet = OscPacket.GetPacket(bytes); + //Log.Debug("Raw UDP In: " + System.Text.Encoding.ASCII.GetString(bytes)); + return packet; + } + else + return null; + } + } + + public byte[] ReceiveBytes() + { + if (closing) throw new Exception("UDPListener has been closed."); + + lock (queue) + { + if (queue.Count() > 0) + { + byte[] bytes = queue.Dequeue(); + return bytes; + } + else + return null; + } + } + + } +} diff --git a/QSharp/SharpOSC/UDPSender.cs b/QSharp/SharpOSC/UDPSender.cs new file mode 100644 index 0000000..57e0ea7 --- /dev/null +++ b/QSharp/SharpOSC/UDPSender.cs @@ -0,0 +1,53 @@ +using System; +using System.Net.Sockets; +using System.Net; + +namespace SharpOSC +{ + public class UDPSender + { + public int Port + { + get { return _port; } + } + int _port; + + public string Address + { + get { return _address; } + } + string _address; + + IPEndPoint RemoteIpEndPoint; + Socket sock; + + public UDPSender(string address, int port) + { + _port = port; + _address = address; + + sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + + var addresses = System.Net.Dns.GetHostAddresses(address); + if (addresses.Length == 0) throw new Exception("Unable to find IP address for " + address); + + RemoteIpEndPoint = new IPEndPoint(addresses[0], port); + } + + public void Send(byte[] message) + { + sock.SendTo(message, RemoteIpEndPoint); + } + + public void Send(OscPacket packet) + { + byte[] data = packet.GetBytes(); + Send(data); + } + + public void Close() + { + sock.Close(); + } + } +} diff --git a/QSharp/SharpOSC/Utils.cs b/QSharp/SharpOSC/Utils.cs new file mode 100755 index 0000000..165c0f7 --- /dev/null +++ b/QSharp/SharpOSC/Utils.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SharpOSC +{ + public class Utils + { + public static DateTime TimetagToDateTime(UInt64 val) + { + if (val == 1) + return DateTime.Now; + + UInt32 seconds = (UInt32)(val >> 32); + var time = DateTime.Parse("1900-01-01 00:00:00"); + time = time.AddSeconds(seconds); + var fraction = TimetagToFraction(val); + time = time.AddSeconds(fraction); + return time; + } + + public static double TimetagToFraction(UInt64 val) + { + if (val == 1) + return 0.0; + + UInt32 seconds = (UInt32)(val & 0x00000000FFFFFFFF); + double fraction = (double)seconds / (UInt32)(0xFFFFFFFF); + return fraction; + } + + public static UInt64 DateTimeToTimetag(DateTime value) + { + UInt64 seconds = (UInt32)(value - DateTime.Parse("1900-01-01 00:00:00.000")).TotalSeconds; + UInt64 fraction = (UInt32)(0xFFFFFFFF * ((double)value.Millisecond / 1000)); + + UInt64 output = (seconds << 32) + fraction; + return output; + } + + public static int AlignedStringLength(string val) + { + int len = val.Length + (4 - val.Length % 4); + if (len <= val.Length) len += 4; + + return len; + } + } +} diff --git a/QSharpDemo/Program.cs b/QSharpDemo/Program.cs new file mode 100644 index 0000000..797012d --- /dev/null +++ b/QSharpDemo/Program.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading; +using QSharp; + +namespace qControlDemo +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("qControlDemo Started"); + QBrowser browser = new QBrowser(); + + + + Thread.Sleep(100000000); + } + } +} diff --git a/QSharpDemo/QSharpDemo.csproj b/QSharpDemo/QSharpDemo.csproj new file mode 100644 index 0000000..283fc7c --- /dev/null +++ b/QSharpDemo/QSharpDemo.csproj @@ -0,0 +1,11 @@ + + + + Exe + netcoreapp3.1 + + + + + +