initial commit

This commit is contained in:
Cassandra de la Cruz-Munoz 2023-08-15 15:29:53 -04:00
commit 628562f762
7 changed files with 518 additions and 0 deletions

67
OscAddress.cs Normal file
View File

@ -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<byte>();
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;
}
}
}

100
OscArgument.cs Normal file
View File

@ -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<byte>();
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();
}
}
}

78
OscBundle.cs Normal file
View File

@ -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<OscMessage> Messages { get; }
public OscBundle(long timeTag, List<OscMessage> messages)
{
TimeTag = timeTag;
Messages = messages;
}
public byte[] ToBytes()
{
var result = new List<byte>();
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<OscMessage>();
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();
}
}
}

74
OscMessage.cs Normal file
View File

@ -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<OscArgument> Data { get; }
public OscMessage(Address address, List<OscArgument> 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<OscArgument>();
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<byte>();
result.AddRange(Address.ToBytes().ToList<byte>());
result.Add(0x2c);
for (var i = 0; i < Data.Count(); i++) {
result.Add(BitConverter.GetBytes(Data[i].Type).ToList<byte>()[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<byte>());
}
return result.ToArray();
}
}
}

125
OscReceiver.cs Normal file
View File

@ -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<OscMessageReceivedEventArgs> MessageReceived;
// An event that occurs when an error is received
public event EventHandler<OscErrorReceivedEventArgs> 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;
}
}
}

38
OscSender.cs Normal file
View File

@ -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();
}
}
}

36
README.md Normal file
View File

@ -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}");
};
```