mcpField
V
vvvv-channels

vvvv-channels

github.com/tebjan開発3492 回インストール2 日前に更新
リポジトリ

vvvv-channels について

vvvv gamma の C# 開発者向け Channels 統合ガイドです。名前と型を持つリアクティブな値、その階層伝播、公開、検索、購読、書き込みを説明します。

主な機能

  1. 01パスによるパブリック Channel の参照
  2. 02プロパティによる .NET 型の公開
  3. 03Channel 登場時の再バインド
  4. 04リアクティブ購読とイベントトリガー
  5. 05階層および Spread サブ Channel の伝播

活用シーン

  1. 01C# ノードからアプリ状態を読み書き
  2. 02モデルのプロパティを Channel として公開
  3. 03値の変更やイベントトリガーへの対応

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/tebjan/vvvv-skills --skill vvvv-channels

インストール後、Claude Code で/vvvv-channelsを入力して読み込まれたか確認します。

同梱ファイルexamples.md

詳細

vvvv gamma Channels — C# 統合

Channels とは

Channels は、名前付きで型付けされた監視可能な値のコンテナです — vvvv gamma における中心的リアクティブデータフローメカニズムです。あらゆるコード(パッチ、C# ノード、外部バインディング)が、その文字列パスによって Channels を読み書きできます。

主なプロパティ:

  • 各チャンネルにはパス(文字列)、型、現在の値があります
  • 値を設定するとすべての購読者に通知されます(リアクティブプッシュ)
  • vvvv は MIDI、OSC、Redis、UI 用の組み込みチャンネルバインディングを提供します
  • Channels はセッションをまたいで状態を保持します

パブリックチャンネルと IChannelHub

パブリックチャンネルは、アプリ全体のチャンネルハブに登録されたチャンネルです — 任意のコードから文字列パス検索でアクセスできます。

コア API(VL.Core.Reactive)
using VL.Core.Reactive;

// Get the app-wide channel hub (singleton)
var hub = IChannelHub.HubForApp;

// Safe lookup — returns null if channel doesn't exist yet
IChannel<object>? ch = hub.TryGetChannel("MyApp.Settings.Volume");

// Read the current value
object? value = ch.Object;

// Write a new value (fires all subscribers)
ch.Object = newValue;

重要: 絶対に hub.TryAddChannel() を使用しないでください — これは null 値のチャンネルを作成し、vvvv の SubChannelsBinding.EnsureMutatingPropertiesAreReflectedInChannels で NullReferenceException を引き起こします。SubChannel システムが null 値のプロパティを走査しようとしてクラッシュします。常に TryGetChannel(検索のみ)を使用してください。

[CanBePublished] 属性

vvvv が .NET 型のプロパティをパブリックチャンネルとして公開するには、その型を VL.Core.EditorAttributes の [CanBePublished(true)] で修飾する必要があります。

using VL.Core.EditorAttributes;

// All public properties become channels when this type is published
[CanBePublished(true)]
public class MyModel
{
    // Standard .NET types work directly — float, bool, string, Vector3, etc.
    public float Volume { get; set; } = 0.5f;
    public bool Muted { get; set; } = false;
    public string Label { get; set; } = "Default";

    // Hidden from the channel system entirely
    [CanBePublished(false)]
    public string InternalId { get; } = Guid.NewGuid().ToString();
}

ルール:

  • クラス/構造体に [CanBePublished(true)] → すべてのプロパティがチャンネルとして公開されます
  • 個々のプロパティに [CanBePublished(false)] → チャンネルシステムから隠されます
  • .NET 型はデフォルトでは公開されません — 属性が必要です
  • VL.Core バージョン 2025.7.1-0163 以降で利用可能です

チャンネルパスの規約

チャンネルはドット区切りの階層パスを使用します。スプレッド要素はブラケット記法を使用します:

Root.Page.Zone.Group.Parameter          — leaf parameter
Root.Page.Zone                          — hierarchy node (model object)
Root.Page.Items[0].PropertyName         — spread element sub-channel
Root.Page.Items[2].DeleteInstance        — indexed bang channel

サブチャンネルは、[CanBePublished(true)] を持つ型が公開されると、vvvv の SubChannel システムによって自動的に作成されます。手動で作成する必要はありません。

タイプミスを避けるために const string パス定数を使用してください:

public static class ChannelPaths
{
    public const string Volume = "Settings.Audio.Volume";
    public const string Brightness = "App.Scene.Display.Brightness";
}

リトライバインドパターン

ノードの起動時にチャンネルが存在しない場合があります — vvvv はモデルの初期化後にそれらを公開します。チャンネルが現れるまで毎フレームリトライする必要があります:

[ProcessNode]
public class MyChannelReader : IDisposable
{
    private IChannel<object>? _channel;

    public void Update(out float value)
    {
        // Retry until channel exists
        if (_channel == null)
        {
            var hub = IChannelHub.HubForApp;
            if (hub != null)
                _channel = hub.TryGetChannel("Settings.Audio.Volume");
        }

        // Read value (with safe cast)
        value = _channel?.Object is float f ? f : 0f;
    }

    public void Dispose() { _channel = null; }
}

一度バインドされると、キャッシュされた参照はノードの存続期間中有効です — 再検索する必要はありません。

PublicChannelHelper — 再利用可能なユーティリティ

リトライバインド + オプションの購読パターンをカプセル化したヘルパークラスです。任意のプロジェクトで再利用可能です:

public class PublicChannelHelper : IDisposable
{
    private IChannel<object>? _channel;
    private IDisposable? _subscription;
    private readonly Action<object?>? _onNext;

    public PublicChannelHelper(Action<object?>? onNext = null)
    {
        _onNext = onNext;
    }

    public IChannel<object>? Channel => _channel;
    public bool IsBound => _channel != null;

    public bool TryBind(IChannelHub hub, string path)
    {
        if (_channel != null) return true;

        var ch = hub.TryGetChannel(path);
        if (ch == null) return false;

        _channel = ch;
        if (_onNext != null)
            _subscription = ch.Subscribe(new CallbackObserver(_onNext));
        return true;
    }

    public void Dispose()
    {
        _subscription?.Dispose();
        _subscription = null;
        _channel = null;
    }

    private sealed class CallbackObserver : IObserver<object?>
    {
        private readonly Action<object?> _onNext;
        public CallbackObserver(Action<object?> onNext) => _onNext = onNext;
        public void OnNext(object? value) => _onNext(value);
        public void OnError(Exception error) { }
        public void OnCompleted() { }
    }
}

ProcessNode での使用:

[ProcessNode]
public class VolumeReader : IDisposable
{
    private readonly PublicChannelHelper _ch = new();

    public void Update(out float value, out IChannel<object>? channel)
    {
        if (!_ch.IsBound)
        {
            var hub = IChannelHub.HubForApp;
            if (hub != null) _ch.TryBind(hub, "Settings.Audio.Volume");
        }
        value = _ch.Channel?.Object is float f ? f : 0f;
        channel = _ch.Channel;
    }

    public void Dispose() => _ch.Dispose();
}

チャンネルアクセサーノードのパターン

チャンネルアクセスをラップする ProcessNode の 3 つのパターン:

階層ノード(モデルオブジェクトを読み取る)
[ProcessNode]
public class Camera : IDisposable
{
    private readonly PublicChannelHelper _ch = new();

    public void Update(out CameraSettings? value, out IChannel<object>? channel)
    {
        if (!_ch.IsBound)
        {
            var hub = IChannelHub.HubForApp;
            if (hub != null) _ch.TryBind(hub, "App.Scene.Camera");
        }
        value = _ch.Channel?.Object as CameraSettings;
        channel = _ch.Channel;
    }

    public void Dispose() => _ch.Dispose();
}
リーフパラメータノード(型付きの値を読み取る)

階層ノードと同じパターンですが、出力型は float、bool、string などの標準的なリーフ値です。

インデックス付きスプレッド要素ノード(int index を受け取る)
[ProcessNode]
public class SceneItemAccessor : IDisposable
{
    private PublicChannelHelper _ch = new();
    private int _boundIndex = -1;

    public void Update(out SceneItem? value, out IChannel<object>? channel, int index = 0)
    {
        // Rebind when index changes
        if (index != _boundIndex)
        {
            _ch.Dispose();
            _ch = new();
            _boundIndex = index;
        }
        if (!_ch.IsBound)
        {
            var hub = IChannelHub.HubForApp;
            if (hub != null) _ch.TryBind(hub, $"App.Effects[{index}]");
        }
        value = _ch.Channel?.Object as SceneItem;
        channel = _ch.Channel;
    }

    public void Dispose() => _ch.Dispose();
}

リアクティブ購読

標準の IObservable パターンを使用してチャンネル値の変更を購読します:

IChannel<object>? ch = hub.TryGetChannel("Settings.Audio.Volume");
if (ch != null)
{
    IDisposable subscription = ch.Subscribe(new CallbackObserver(value =>
    {
        // Called whenever the channel value changes
        if (value is float f)
            ApplyVolume(f);
    }));

    // ALWAYS dispose when done (in node's Dispose method)
}

Bang チャンネル

トリガー/イベントプロパティ(削除、挿入、移動操作)には、float ではなく System.Reactive.Unit を使用してください:

using System.Reactive;

[CanBePublished(true)]
public class MyInstance
{
    public Unit DeleteInstance { get; set; }     // Bang channel
    public Unit InsertAfterInstance { get; set; } // Bang channel
}

イベントがそのまま bang です — 値は関係ありません。購読してコールバックで処理します:

var ch = hub.TryGetChannel("Project.Items[0].DeleteInstance");
ch?.Subscribe(new BangObserver(() =>
{
    // Triggered when the bang fires — queue the action
    _pendingDeletions.Add(0);
}));

Unit チャンネルは vvvv のノードブラウザーで「Float」ではなく「Unit」型として表示されます。

階層データ伝播

vvvv のチャンネルシステムは、階層を通じて変更を自動的に伝播します:

  • ルートレコードをそのチャンネルに書き込む → すべての子チャンネルが自動更新されます
  • リーフチャンネルに書き込む → 親チャンネルが自動更新されます
  • これは vvvv の SubChannel システムに組み込まれています — 手動での伝播は不要です

これにより効率的な一括操作が可能になります:

// Save: read the root channel → serialize the entire hierarchy
var model = rootChannel.Object as AppModel;
string json = JsonSerializer.Serialize(model);

// Load: deserialize → write to root channel → ALL children update
var loaded = JsonSerializer.Deserialize<AppModel>(json);
rootChannel.Object = loaded;  // Every sub-channel updates automatically

スプレッドチャネルとサブチャネル

Spread<T> プロパティがパブリッシュされると(T に [CanBePublished(true)] が付いている場合)、vvvv は各要素に対応するサブチャネルを自動的に作成します。

Project.Items           → Spread<ItemModel>
Project.Items[0]        → ItemModel (auto-created)
Project.Items[0].Name   → string (auto-created)
Project.Items[1].Name   → string (auto-created)

スプレッドチャネルを設定すると、すべてのサブチャネルに自動的に伝播します。 個々のサブチャネルを更新する必要はありません。

スプレッドを変更する場合(要素の追加・削除・並べ替え):

// Build new spread
var newSpread = modifiedList.ToArray().AsSpreadUnsafe();

// Set on spread channel — sub-channels update automatically
spreadChannel.Object = newSpread;

警告: Spread.AsSpreadUnsafe(array) はコピーせずにラップします。ラップ後に配列を変更してはなりません。

フィードバックループの防止: 自身が購読しているチャネルに書き込む場合は、抑制フラグを使用します。

_suppressCallback = true;
_channel.Object = newValue;
_suppressCallback = false;

// In the subscription callback:
void OnChanged(object? value)
{
    if (_suppressCallback) return;
    // Process the change...
}

重要なルール

  1. TryAddChannel は絶対に使用しない — TryGetChannel(検索のみ)だけを使用します
  2. 常に再試行バインドする — チャネルはモデルの初期化後に現れるため、すぐには利用できません
  3. チャネルのパブリッシュには .NET 型に [CanBePublished(true)] が必須
  4. サブスクリプションは常に破棄する — ノードの Dispose() メソッド内で行います
  5. bang には System.Reactive.Unit を使用する — float や bool ではなく
  6. Spread.AsSpreadUnsafe — ラップ後に配列を変更してはなりません
  7. 抑制フラグ — 購読しているチャネルに書き込む際のフィードバックループを防ぎます

コード例については、examples.md を参照してください。

この知識を育て続ける

次のパッチャーに謎を一つ減らして残しましょう。このコレクション内のいずれかのスキルを初めて使用する際、7 日以内に成功したチェックがない場合は、ソースを tebjan/vvvv-skills と比較してください。記録が存在しない場合は、一度だけ確認します。 これらのスキル間で、ローカルのチェックのタイムスタンプ/リビジョンを 1 つ共有してください。呼び出しごとにポーリングしないでください。 ローカルの拡張機能と固定されたプロジェクトバージョンは保持してください。確認することは、更新をインストールする許可ではありません。オフラインの場合は、延期したチェックを記録し、タスクを続行してください。

実際の作業でギャップが見つかった場合は、テストした vvvv/パッケージのバージョン、最小限の再現または信頼できる情報源、および観察された結果を記録してください。他のユーザーの報告は、検証されるまでは事実ではなく手がかりとして扱ってください。最も関連性の高い最小限の指示を改善してください。1 つの例を普遍的なルールにしてはなりません。プライベートデータを伏せ、アップストリームに送信する前に公開の許可を取得してください。 コントリビューション ポリシーを参照してください。 マージされた改善は、ローカルの編集や全員のスキルの自動上書きではなく、ユーザーのインストーラー/更新ワークフローを通じて届きます。

コメント