UnityでOSC通信 | ExtremeOsc

# ドキュメント

# はじめに

あるプロジェクトでUnityを使ってOSC通信をすることになり、すでにいくつか公開されているOSCのライブラリを使ってました。

しかし、アドレスの種類や引数の数が増えていくにしたがって、ソースコードが長くなってしまったり、ひたすらボイラープレート的なコードを書かなければならなかったりと大変な思いをしました…。

例えば、こんな感じのOSCがあるとしたら
```
/test/address ,iiifffiiffif
```

こんな感じのコードをひたすら書きます。（仮想のライブラリです）
```cs
OscServer server;

public void Initialize()
{
    server = new OscServer(12345);
    server.AddCallback("/test/address", OnMessageTestAddress);
    // アドレスが増えたら追加していく
}

private void OnMessageTestAddress(string address, OscArguments arguments)
{
    int a = arguments.GetInt(0);
    int b = arguments.GetInt(1);
    int c = arguments.GetInt(2);
    // これをひたすら続ける
}
```

OSCの仕様上、上記のようになってしまうのは仕方のないことです。一方で、C#を使っているのだから値をまとめたクラス/構造体で扱えないものか…と思ったので、自分でつくってみました！

# ExtremeOsc

https://umetaman.github.io/ExtremeOsc/

https://github.com/umetaman/ExtremeOsc

ExtremeOscは、Unity向けのOSC (Open Sound Control)のC#実装です。SourceGeneratorによって、これまで書かざるを得なかった大量のコードを自動生成します。これによって、次のような恩恵が受けられます。

- クラスまたは構造体に対して、自動的にOSC信号へ変換するメソッドを追加
- クラスの関数にOSCのアドレスを指定すると、自動的にOSC信号に対してコールバックを実行

## サポートしている型

[OpenSoundControl Specification 1.0](https://opensoundcontrol.stanford.edu/spec-1_0.html)にある基本的な型はサポートしています！

| Tag | C# Type |
| --- | --- |
| i | int |
| h | long |
| f | float |
| s | string |
| S | Symbol |
| b | byte[] |
| d | double |
| c | char |
| r | UnityEngine.Color32 |
| t | TimeTag (DateTime) |
| T | bool (true) |
| F | bool (false) |
| N | Nil |
| I | Infinitum |
| m | MIDI as int |

## 使い方

### インストール方法

丁寧にテストをしたつもりですが、まだ自信がないのでプレビュー版としています。ReleasesページからUnity Packageをダウンロードしてインポートするか、リポジトリをクローンしてください。

https://github.com/umetaman/ExtremeOsc/releases/tag/preview-0.0.1

### OSCの送信

1. OSCで送信したいデータのクラス（構造体）を作成して`[OscPackable]`アトリビュートをつけてください。クラスの条件は次の通りです。
    - Partialなクラス（構造体）である。
    - Root（クラスの入れ子ではない）である。

2. 次にOSCで送信したいプロパティまたはフィールドに`[OscElementAt(int)]`アトリビュートをつけてください。必ず連番にします。

    ```cs
    // 自動生成されるコードのために、必ずpartialにします。
    [OscPackable]
    public partial class ExampleData
    {
        // OscElementAtの引数は必ず連番にします。
        [OscElementAt(0)]
        public int IntValue { get; set; }
        [OscElementAt(1)]
        public float FloatValue { get; set; }
        [OscElementAt(2)]
        public string StringValue { get; set; }
    
        public ExampleData()
        {
            IntValue = 0;
            FloatValue = 0.0f;
            StringValue = string.Empty;
        }
    }
    ```
    
    上記のようなコードを書くと、次のようなコードが自動的に生成されます。
    ```cs
    partial class ExampleData : IOscPackable
    {
        // OSCのタグ -> ,ifs
        public static readonly byte[] TagTypes = new byte[] { 44, 105, 102, 115 };
    
        // 送信するbyte[]に書き込む
        public void Pack (byte[] buffer, ref int offset)
        {
            int offsetTagTypes = offset + 1;
            OscWriter.WriteString(buffer, TagTypes, ref offset);
            OscWriter.WriteInt32(buffer, IntValue, ref offset);
            offsetTagTypes++;
            OscWriter.WriteFloat(buffer, FloatValue, ref offset);
            offsetTagTypes++;
            OscWriter.WriteStringUtf8(buffer, StringValue, ref offset);
            offsetTagTypes++;
        }
    
        // 受信したbyte[]から読み出す
        public void Unpack (byte[] buffer, ref int offset)
        {
            int offsetTagTypes = offset + 1;
            OscReader.ReadString(buffer, ref offset);
            this.IntValue = OscReader.ReadInt32(buffer, ref offset);
            offsetTagTypes++;
            this.FloatValue = OscReader.ReadFloat(buffer, ref offset);
            offsetTagTypes++;
            this.StringValue = OscReader.ReadString(buffer, ref offset);
            offsetTagTypes++;
        }
    }
    ```

3. 最後に、信号を送信するOscClientでインスタンスを送信します。いくつか例を示します。

    ```cs
    public class ExampleClient : MonoBehaviour
    {
        private OscClient client = null;
    
        private void Awake()
        {
            client = new OscClient("127.0.0.1", 5555);
        }
    
        private void Update()
        {
            if(Input.GetKeyDown(KeyCode.Space))
            {
                var data = new ExampleData
                {
                    IntValue = Random.Range(0, 100),
                    FloatValue = Random.Range(0.0f, 1.0f),
                    StringValue = "Hello, World!"
                };
    
                // Packableなクラスの送信
                client.Send("/example", data);
            }
    
            if (Input.GetKeyDown(KeyCode.Return))
            {
                var data = new ExampleData
                {
                    IntValue = Random.Range(0, 100),
                    FloatValue = Random.Range(0.0f, 1.0f),
                    StringValue = "Hello, World!"
                };
    
                // 後述 : Packableなクラスと同じ順番で並んでいる引数に対して
                client.Send("/example/arguments", data);
            }
    
            if(Input.GetKeyDown(KeyCode.LeftShift))
            {
                // 引数なし
                client.Send("/example/noargument");
            }
    
            if(Input.GetKeyDown(KeyCode.RightShift))
            {
                var data = new object[]
                {
                    Random.Range(0, 100),
                    Random.Range(0.0f, 1.0f),
                    "Hello, World!"
                };
    
                // 型の順番が合っていればobject[]もOK。
                // ただし、無駄なアロケーションあり
                client.Send("/example/arguments", data);
            }
        }
    
        private void OnDestroy()
        {
            client?.Dispose();
            client = null;
        }
    }
    ```

### OSCの受信

1. OSCを受信してコールバックするクラス（構造体）に`[OscReceiver]`アトリビュートをつけてください。このクラスも必ずPartialにします。

2. OSCを受信するための`OscServer`も作成しましょう。コールバックを実行するクラスのインスタンスを登録しましょう。

    ```cs
    // 1. 必ずPartial
    [OscReceiver]
    public partial class ExampleServer : MonoBehaviour
    {
        private OscServer server = null;
    
        private void Awake()
        {
            server = new OscServer(5555);
            // 2. このクラスのインスタンスでコールバックしたいのでRegister
            server.Register(this);
            server.Open();
        }
    
        private void OnDestroy()
        {
            server.Unregister(this);
            server?.Dispose();
            server = null;
        }
    }
    ```

3. コールバックする関数を定義し、`[OscCallback(address)]`アトリビュートをつけてください。必ず最初の引数は`string address`にしてください。

    ```cs
    // 2.のコードの続き
    [OscReceiver]
    public partial class ExampleServer : MonoBehaviour
    {
        [OscCallback("/example/noargument")]
        private void OnExampleNoArgument(string address)
        {
            // 引数なし
        }
        
        [OscCallback("/example")]
        private void OnExample(string address, ExampleData data)
        {
            // Packableなクラス
        }
        
        [OscCallback("/example/arguments")]
        private void OnExampleArguments(string address, int intValue, float floatValue, string stringValue)
        {
            // 値ごとに引数に分けてもOK
        }
    
        [OscCallback("/example/argument/ref")]
        private void OnExampleArgumentRef(string address, ref ExampleData data)
        {
            // ref, inをつけると作成済みの値を使いまわします。
        }
    }
    ```

    上記のようなコードを書くと、次のようなコードが自動生成されます。アドレスを判別してUnpackする処理を出力しただけです。

    ```cs
    public void ReceiveOscPacket (byte[] buffer)
    {
        int offset = 0;
        string address = OscReader.ReadString(buffer, ref offset);
        int offsetTagTypes = offset + 1;
        switch (address)
        {
            case "/example/noargument":
            {
                OnExampleNoArgument(address);
                break;
            }
            case "/example":
            {
                var __value = new ExtremeOsc.Example.ExampleData();
                __value.Unpack(buffer, ref offset);
                OnExample(address, __value);
                break;
            }
            case "/example/arguments":
            {
                OscReader.ReadString(buffer, ref offset);
                int intValue = OscReader.ReadInt32(buffer, ref offset);
                offsetTagTypes++;
                float floatValue = OscReader.ReadFloat(buffer, ref offset);
                offsetTagTypes++;
                string stringValue = OscReader.ReadString(buffer, ref offset);
                offsetTagTypes++;
                OnExampleArguments(address, intValue, floatValue, stringValue);
                break;
            }
        }
    }
    ```

---
長々と書いてしまいましたが、要約しますと

1. OscPackableなクラスを作る！送る！
2. OscReceiverなクラスを作る！
3. OscCallbackな関数を作る！引数はPackableなクラスに合わせる！

となります。ヨシ！書き込み、読み出し部分は一切書かなくてヨシ！

もちろん、送受信先がUnityじゃなくても動作します。

![スクリーンショット 2025-02-23 182446.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279082/07ebf564-ac7f-44dd-885a-09eb92c7a537.png)

# 何が嬉しいのか

なんといっても送受信に関わるほとんどのコードが自動生成されるので、「何を送るか、どこで受信するか」を考えることに集中できます。

また、プロジェクトの要件・仕様は日々揺れ動くもので、「この引数を追加・削除したい！」ということは多いと思います。引数の順番や型をクラス（構造体）として扱うことで、「クラスを変更するだけ」でよいのです。これで仕様変更し放題！ヨシ！

一応、何も考えずに`object[]`として送るようにもできますが、配列の余計なアロケーションなどが走るのであまりおすすめできません。上記のような恩恵も受けられませんしね。

# 今後の課題

***複数のPackableなクラス（構造体）を引数に取りたい。***

多ければ多いほどよいわけでもないので悩ましいですが、値や機能ごとに分割するようなこともあるので一応サポートはしたいと思っています。

***UnityEngineの型を送受信したい。***

これ完全に忘れてました…。現状は自分で定義した型のみをサポートしているので、どうにかして`UnityEngine.Vector3`とかをサポートしたい…。たぶん、[hadashiA/VYaml](https://github.com/hadashiA/VYaml)のリポジトリが参考になるのではと思っております。

***Issueやプルリク大歓迎!***

せっかくここまで作ったので、使ってフィードバック・プルリクください！お待ちしております！

# 参考

OSCについて
- [OpenSoundControl Specification 1.0](https://opensoundcontrol.stanford.edu/spec-1_0.html)
- [keijiro/OscJack](https://github.com/keijiro/OscJack)
- [hecomi/uOSC](https://github.com/hecomi/uOSC)

SourceGeneratorについて
- [2022年(2024年)のC# Incremental Source Generator開発手法](https://neue.cc/2022/12/16_IncrementalSourceGenerator.html)
- [hadashiA/VYaml](https://github.com/hadashiA/VYaml)