commit 628562f762378cd07523655c1ad806db67bba5f9 Author: Cassandra de la Cruz-Munoz Date: Tue Aug 15 15:29:53 2023 -0400 initial commit diff --git a/OscAddress.cs b/OscAddress.cs new file mode 100644 index 0000000..375242f --- /dev/null +++ b/OscAddress.cs @@ -0,0 +1,67 @@ +using System; +using System.Text.RegularExpressions; + +namespace godotOscSharp +{ + public class Address + { + public string Pattern { get; } + + public Address(string pattern) + { + if (string.IsNullOrEmpty(pattern)) + { + throw new ArgumentException("Pattern cannot be null or empty"); + } + if (!pattern.StartsWith("/")) + { + throw new ArgumentException("Pattern must start with a slash (/)"); + } + if (pattern.Contains("//")) + { + throw new ArgumentException("Pattern cannot contain two consecutive slashes (//)"); + } + if (pattern.Contains(" ")) + { + throw new ArgumentException("Pattern cannot contain spaces"); + } + if (!Regex.IsMatch(pattern, @"^/([\w\-\.\*]+/)*([\w\-\.\*]+|\*)$")) + { + throw new ArgumentException("Pattern contains invalid characters"); + } + Pattern = pattern; + } + + public byte[] ToBytes() + { + var result = new System.Collections.Generic.List(); + result.AddRange(System.Text.Encoding.ASCII.GetBytes(Pattern)); + result.Add(0); + var padding = 4 - (result.Count % 4); + for (int i = 0; i < padding; i++) + { + result.Add(0); + } + return result.ToArray(); + } + + public static Address Parse(byte[] data, ref int index) + { + var start = index; + while (data[index] != 0) + { + index++; + } + var pattern = System.Text.Encoding.ASCII.GetString(data, start, index - start); + index++; + var padding = 4 - ((index - start) % 4); + index += padding; + return new Address(pattern); + } + + public override string ToString() + { + return Pattern; + } + } +} diff --git a/OscArgument.cs b/OscArgument.cs new file mode 100644 index 0000000..41f2db2 --- /dev/null +++ b/OscArgument.cs @@ -0,0 +1,100 @@ +using System; + +namespace godotOscSharp +{ + // A class that represents a DWord + public class OscArgument + { + // The value of the DWord as an unsigned integer + public char Type { get; } + public object Value { get; } + + // The constructor that takes an unsigned integer as the value + public OscArgument(object value, char type) + { + Value = value; + Type = type; + } + + // A method that parses a byte array to a DWord + public static OscArgument Parse(byte[] data, ref int index, char type) + { + // Use BitConverter to get the unsigned integer from the bytes at the given index in little-endian order + object value = null; + var start = index; + switch (type) { + case 'i': + value = BitConverter.ToInt32(data, index); + index += 4; + break; + case 'f': + value = BitConverter.ToSingle(data, index); + index += 4; + break; + case 's': + while (data[index] != 0) // Find the null terminator + { + index++; + } + value = System.Text.Encoding.ASCII.GetString(data, start, index - start); + while (data[index] == 0 && index < data.Length) + { + index++; + } + break; + case 'h': + value = BitConverter.ToInt64(data, index); + index += 8; + break; + case 'd': + value = BitConverter.ToDouble(data, index); + index += 8; + break; + case 'T': + value = true; + break; + case 'F': + value = false; + break; + case 'N': + value = null; + break; + } + + // Increment the index by 4 bytes + + // Return a new DWord instance with the value + return new OscArgument(value, type); + } + + public byte[] ToBytes() + { + switch (Type) { + case 'i': + return BitConverter.GetBytes((int)Value); + case 'f': + return BitConverter.GetBytes((float)Value); + case 's': + var result = new System.Collections.Generic.List(); + result.AddRange(System.Text.Encoding.ASCII.GetBytes((string)Value)); + result.Add(0); + var padding = 4 - (result.Count % 4); + for (int i = 0; i < padding; i++) + { + result.Add(0); + } + return result.ToArray(); + case 'h': + return BitConverter.GetBytes((long)Value); + case 'd': + return BitConverter.GetBytes((double)Value); + } + return new byte[0]; + } + + public override string ToString() + { + return Value.ToString(); + } + } +} diff --git a/OscBundle.cs b/OscBundle.cs new file mode 100644 index 0000000..c6964d4 --- /dev/null +++ b/OscBundle.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Godot; + +namespace godotOscSharp +{ + public class OscBundle + { + public long TimeTag { get; } + public List Messages { get; } + + public OscBundle(long timeTag, List messages) + { + TimeTag = timeTag; + Messages = messages; + } + + public byte[] ToBytes() + { + var result = new List(); + result.AddRange(Encoding.ASCII.GetBytes("#bundle")); + result.Add(0); + result.AddRange(BitConverter.GetBytes(TimeTag)); + foreach (var message in Messages) + { + var messageBytes = message.ToBytes(); + result.AddRange(BitConverter.GetBytes(messageBytes.Length)); + result.AddRange(messageBytes); + } + return result.ToArray(); + } + + public static OscBundle Parse(byte[] data) + { + var index = 0; + var identifier = Encoding.ASCII.GetString(data, index, 7); + if (identifier != "#bundle") + { + throw new ArgumentException("Invalid bundle identifier"); + } + index += 8; + var timeTag = BitConverter.ToInt64(data, index); + index += 8; + var messages = new List(); + while (index < data.Length) + { + var size = BitConverter.ToInt32(data, index); + index += 4; + var messageData = data.Skip(index).Take(size).ToArray(); + var message = OscMessage.Parse(messageData); + messages.Add(message); + index += size; + } + return new OscBundle(timeTag, messages); + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append("OscBundle: "); + sb.Append("TimeTag: "); + sb.Append(TimeTag); + sb.Append(", "); + sb.Append("Messages: "); + sb.Append("["); + foreach (var message in Messages) + { + sb.Append(message.ToString()); + sb.Append(", "); + } + sb.Remove(sb.Length - 2, 2); + sb.Append("]"); + return sb.ToString(); + } + } +} diff --git a/OscMessage.cs b/OscMessage.cs new file mode 100644 index 0000000..1c90c1e --- /dev/null +++ b/OscMessage.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Godot; + +namespace godotOscSharp +{ + public class OscMessage + { + public Address Address { get; } + + public List Data { get; } + + public OscMessage(Address address, List data) + { + Address = address; + Data = data; + } + + public static OscMessage Parse(byte[] data) + { + var index = 0; + var address = Address.Parse(data, ref index); + var start = index; + while (data[index] != 0) + { + index++; + } + var pattern = System.Text.Encoding.ASCII.GetString(data, start, index - start); + while (data[index] == 0) + { + index++; + } + var dataList = new List(); + for (var items = 1; items < pattern.Length; items++) + { + dataList.Add(OscArgument.Parse(data, ref index, pattern[items])); + } + return new OscMessage(address, dataList); + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(Address.ToString()); + foreach (var d in Data) + { + sb.Append(" "); + sb.Append(d.ToString()); + } + return sb.ToString(); + } + + public byte[] ToBytes() + { + var result = new System.Collections.Generic.List(); + result.AddRange(Address.ToBytes().ToList()); + result.Add(0x2c); + for (var i = 0; i < Data.Count(); i++) { + result.Add(BitConverter.GetBytes(Data[i].Type).ToList()[0]); + } + var padding = 4 - (result.Count % 4); + for (int i = 0; i < padding; i++) + { + result.Add(0); + } + for (var i = 0; i < Data.Count(); i++) { + result.AddRange(Data[i].ToBytes().ToList()); + } + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/OscReceiver.cs b/OscReceiver.cs new file mode 100644 index 0000000..b10e2a8 --- /dev/null +++ b/OscReceiver.cs @@ -0,0 +1,125 @@ +using Godot; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace godotOscSharp +{ + // A class that represents an OSC receiver + public class OscReceiver : IDisposable + { + // The UDP client for receiving data + private UdpClient udpClient; + + // The thread for listening to incoming messages + private Thread listenThread; + + // The flag to indicate if the receiver is running + private bool running; + + // The constructor that takes a port number + public OscReceiver(int port) + { + // Create a UDP client with the given port + udpClient = new UdpClient(port); + + // Create a thread for listening to incoming messages + listenThread = new Thread(new ThreadStart(Listen)); + + // Set the running flag to true + running = true; + + // Start the thread + listenThread.Start(); + } + + // A method that listens to incoming messages + private void Listen() + { + // While the receiver is running + while (running) + { + try + { + // Receive data from any source + IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); + byte[] data = udpClient.Receive(ref remoteEndPoint); + // Parse the data to an OSC message + if (data[0] == 0x2f) + { + OscMessage message = OscMessage.Parse(data); + MessageReceived?.Invoke(this, new OscMessageReceivedEventArgs(message, remoteEndPoint.Address.ToString(), remoteEndPoint.Port)); + } + else + { + // GD.Print(string.Join(", ", data)); + OscBundle bundle = OscBundle.Parse(data); + foreach (var message in bundle.Messages) + { + MessageReceived?.Invoke(this, new OscMessageReceivedEventArgs(message, remoteEndPoint.Address.ToString(), remoteEndPoint.Port)); + } + } + } + catch (Exception e) + { + // If an exception occurs, invoke the error received event with the exception message + ErrorReceived?.Invoke(this, new OscErrorReceivedEventArgs(e.Message)); + } + } + } + + // A method that disposes the receiver and releases resources + public void Dispose() + { + // Set the running flag to false + running = false; + + // Close the UDP client + udpClient.Close(); + + // Join the thread + listenThread.Join(); + } + + // An event that occurs when a message is received + public event EventHandler MessageReceived; + + // An event that occurs when an error is received + public event EventHandler ErrorReceived; + } + + // A class that contains the data for the message received event + public class OscMessageReceivedEventArgs : EventArgs + { + // The OSC message that was received + public OscMessage Message { get; } + + // The sender's IP address + public string IPAddress { get; } + + // The sender's port number + public int Port { get; } + + // The constructor that takes a message, an IP address and a port number + public OscMessageReceivedEventArgs(OscMessage message, string ipAddress, int port) + { + Message = message; + IPAddress = ipAddress; + Port = port; + } + } + + // A class that contains the data for the error received event + public class OscErrorReceivedEventArgs : EventArgs + { + // The error message that was received + public string ErrorMessage { get; } + + // The constructor that takes an error message + public OscErrorReceivedEventArgs(string errorMessage) + { + ErrorMessage = errorMessage; + } + } +} diff --git a/OscSender.cs b/OscSender.cs new file mode 100644 index 0000000..1f0bcf9 --- /dev/null +++ b/OscSender.cs @@ -0,0 +1,38 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace godotOscSharp +{ + public class OscSender : IDisposable + { + private UdpClient udpClient; + + private IPAddress host; + + private int port; + + public OscSender(IPAddress host, int port) + { + udpClient = new UdpClient(0); + this.host = host; + this.port = port; + } + + public void Connect() + { + udpClient.Connect(host, port); + } + + public void Send(OscMessage message) + { + byte[] data = message.ToBytes(); + udpClient.Send(data, data.Length); + } + + public void Dispose() + { + udpClient.Close(); + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..419cab7 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# godotOscSharp + +A simple library for OpenSoundControl sending and receiving over a network, for use in Godot. + +It's been lightly tested in Godot 4.1, invoked from a scene's script written in C#. + +### How to Use + +The easiest way to download this is to use one of the following commands from inside your project: `git submodule add https://github.com/cass-dlcm/godotOscSharp.git` or `git submodule add git@github.com:cass-dlcm/godotOscSharp.git`, depending on whether you connect to GitHub using HTTPS or SSH. + +Then use: + +```Bash +cd godotOscSharp +git checkout tags/v0.1.0 -b v0.1.0-branch +``` + +It'll put the files inside a directory in your project named `godotOscSharp`, and checkout the version listed in the tag. + +The constructor for the OscReceiver takes in an `int` port as a mandatory argument. + +The constructor for the OscSender takes in an `IPAddress` and a `int` port as mandatory arguments. + +Here's an example of how to create and use a receiver. +```C# +var receiver = new godotOscSharp.OscReceiver(9000); +receiver.MessageReceived += (sender, e) => +{ + GD.Print($"Received a message from {e.IPAddress}:{e.Port}"); + GD.Print(e.Message.ToString()); + }; +receiver.ErrorReceived += (sender, e) => +{ + GD.Print($"Error: {e.ErrorMessage}"); +}; +``` \ No newline at end of file