{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://docs.switchboard.audio/schemas/switchboard-graph-config.json",
  "title": "Switchboard Graph Configuration",
  "description": "A Switchboard JSON graph configuration document. Two shapes are accepted: an engine configuration that wraps a graph, or a bare audio graph.",
  "oneOf": [
    { "$ref": "#/$defs/engineConfig" },
    { "$ref": "#/$defs/graph" }
  ],
  "$defs": {
    "engineConfig": {
      "title": "Engine configuration",
      "description": "An engine and the graph it renders.",
      "type": "object",
      "required": ["type"],
      "oneOf": [
        { "$ref": "#/$defs/realtimeEngine" },
        { "$ref": "#/$defs/offlineEngine" },
        { "$ref": "#/$defs/manualEngine" },
        { "$ref": "#/$defs/webSocketEngine" }
      ]
    },

    "realtimeEngine": {
      "title": "Realtime engine",
      "description": "Renders a graph against the platform's live audio I/O.",
      "type": "object",
      "additionalProperties": false,
      "required": ["type"],
      "properties": {
        "id": { "$ref": "#/$defs/objectID" },
        "objectID": { "$ref": "#/$defs/objectID" },
        "type": {
          "description": "Realtime engine type. `Switchboard.Realtime` is the canonical spelling.",
          "enum": [
            "Switchboard.Realtime",
            "Realtime",
            "Switchboard.RealTimeGraphRenderer",
            "RealTimeGraphRenderer"
          ]
        },
        "config": { "$ref": "#/$defs/realtimeEngineConfig" },
        "configuration": { "$ref": "#/$defs/realtimeEngineConfig" },
        "properties": { "$ref": "#/$defs/enginePropertiesBlock" }
      }
    },

    "realtimeEngineConfig": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "graph": { "$ref": "#/$defs/graph" },
        "defaultStreamParameters": { "$ref": "#/$defs/streamParameters" },
        "microphoneEnabled": {
          "description": "Android only. Opens the audio input stream. Ignored on every other platform.",
          "type": "boolean",
          "default": false
        }
      }
    },

    "offlineEngine": {
      "title": "Offline engine",
      "description": "Renders a graph faster than real time, reading from and writing to audio files.",
      "type": "object",
      "additionalProperties": false,
      "required": ["type"],
      "properties": {
        "id": { "$ref": "#/$defs/objectID" },
        "objectID": { "$ref": "#/$defs/objectID" },
        "type": {
          "description": "Offline engine type. `Switchboard.Offline` is the canonical spelling.",
          "enum": [
            "Switchboard.Offline",
            "Offline",
            "Switchboard.OfflineGraphRenderer",
            "OfflineGraphRenderer"
          ]
        },
        "config": { "$ref": "#/$defs/offlineEngineConfig" },
        "configuration": { "$ref": "#/$defs/offlineEngineConfig" },
        "properties": { "$ref": "#/$defs/enginePropertiesBlock" }
      }
    },

    "offlineEngineConfig": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "graph": { "$ref": "#/$defs/graph" },
        "inputFiles": {
          "description": "Audio files fed into the graph's input node, in bus order.",
          "type": "array",
          "items": { "$ref": "#/$defs/audioFile" }
        },
        "outputFiles": {
          "description": "Audio files written from the graph's output node, in bus order.",
          "type": "array",
          "items": { "$ref": "#/$defs/audioFile" }
        },
        "sampleRate": {
          "description": "Sample rate of the render, in Hz. 0 leaves it undefined, deriving it from the input files.",
          "type": "integer",
          "minimum": 0,
          "default": 0
        },
        "bufferDurationMs": {
          "description": "Duration of one render block, in milliseconds.",
          "type": "integer",
          "minimum": 1,
          "default": 10
        },
        "maxNumberOfSecondsToRender": {
          "description": "Upper bound on render length, in seconds. 0 renders until the inputs are exhausted.",
          "type": "number",
          "minimum": 0,
          "default": 0
        }
      }
    },

    "manualEngine": {
      "title": "Manual engine",
      "description": "Renders a graph only when the host explicitly asks for audio. The host owns the clock.",
      "type": "object",
      "additionalProperties": false,
      "required": ["type"],
      "properties": {
        "id": { "$ref": "#/$defs/objectID" },
        "objectID": { "$ref": "#/$defs/objectID" },
        "type": {
          "description": "Manual engine type. `Switchboard.Manual` is the canonical spelling.",
          "enum": ["Switchboard.Manual", "Manual"]
        },
        "config": { "$ref": "#/$defs/manualEngineConfig" },
        "configuration": { "$ref": "#/$defs/manualEngineConfig" },
        "properties": { "$ref": "#/$defs/enginePropertiesBlock" }
      }
    },

    "manualEngineConfig": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "graph": { "$ref": "#/$defs/graph" }
      }
    },

    "webSocketEngine": {
      "title": "WebSocket engine",
      "description": "Renders a graph over a WebSocket transport. Available only in builds with the WebSocket engine enabled.",
      "type": "object",
      "additionalProperties": false,
      "required": ["type"],
      "properties": {
        "id": { "$ref": "#/$defs/objectID" },
        "objectID": { "$ref": "#/$defs/objectID" },
        "type": {
          "description": "WebSocket engine type. `Switchboard.WebSocket` is the canonical spelling.",
          "enum": ["Switchboard.WebSocket", "WebSocket"]
        },
        "config": { "$ref": "#/$defs/webSocketEngineConfig" },
        "configuration": { "$ref": "#/$defs/webSocketEngineConfig" },
        "properties": { "$ref": "#/$defs/enginePropertiesBlock" }
      }
    },

    "webSocketEngineConfig": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "graph": { "$ref": "#/$defs/graph" },
        "port": {
          "description": "TCP port the engine listens on.",
          "type": "integer",
          "minimum": 1,
          "maximum": 65535
        },
        "sampleRate": {
          "description": "Sample rate of the transported audio, in Hz.",
          "type": "integer",
          "minimum": 1
        }
      }
    },

    "enginePropertiesBlock": {
      "description": "Runtime properties applied after the engine is constructed. `graph` may be supplied here instead of under `config`.",
      "type": "object",
      "properties": {
        "graph": { "$ref": "#/$defs/graph" }
      }
    },

    "graph": {
      "title": "Audio graph",
      "description": "A directed graph of audio nodes connected by buses. Graph parameters may sit directly on the graph object or inside a nested `config` block; when both are present the outer value wins.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "id": { "$ref": "#/$defs/objectID" },
        "objectID": { "$ref": "#/$defs/objectID" },
        "config": { "$ref": "#/$defs/graphParameters" },
        "configuration": { "$ref": "#/$defs/graphParameters" },
        "nodes": {
          "description": "Nodes belonging to the graph. The input and output nodes exist implicitly and must not be listed here.",
          "type": "array",
          "items": { "$ref": "#/$defs/node" }
        },
        "connections": {
          "description": "Audio bus connections between nodes.",
          "type": "array",
          "items": { "$ref": "#/$defs/connection" }
        },
        "dataConnections": {
          "description": "Event/data connections between node outputs and inputs.",
          "type": "array",
          "items": { "$ref": "#/$defs/dataConnection" }
        },
        "maxNumberOfChannels": {
          "description": "Maximum channels per internal audio bus. Sizes the pre-allocated buffer pool; must be at least as wide as the widest internal bus the graph produces.",
          "type": "integer",
          "minimum": 1,
          "default": 2
        },
        "maxNumberOfFrames": {
          "description": "Maximum frames per internal audio bus. Sizes the pre-allocated buffer pool; must be at least the largest per-call frame count the graph processes.",
          "type": "integer",
          "minimum": 1,
          "default": 48000
        },
        "sampleRate": {
          "description": "Fixed processing sample rate of the graph, in Hz. 0 runs the graph at the caller's sample rate and creates no resampler.",
          "type": "integer",
          "minimum": 0,
          "default": 0
        },
        "bufferSize": {
          "description": "Fixed processing buffer size. Only applies when `sampleRate` is non-zero.",
          "type": "integer",
          "minimum": 1,
          "default": 512
        },
        "numberOfInputs": {
          "description": "Number of buses on the graph's implicit input node.",
          "type": "integer",
          "minimum": 0,
          "default": 1
        },
        "numberOfOutputs": {
          "description": "Number of buses on the graph's implicit output node.",
          "type": "integer",
          "minimum": 0,
          "default": 1
        },
        "properties": {
          "description": "Runtime properties applied to the graph after construction.",
          "type": "object"
        }
      }
    },

    "graphParameters": {
      "title": "Graph parameters",
      "description": "Graph parameters supplied through a nested `config` block. Identical to the parameters accepted directly on the graph object.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "maxNumberOfChannels": {
          "description": "Maximum channels per internal audio bus.",
          "type": "integer",
          "minimum": 1,
          "default": 2
        },
        "maxNumberOfFrames": {
          "description": "Maximum frames per internal audio bus.",
          "type": "integer",
          "minimum": 1,
          "default": 48000
        },
        "sampleRate": {
          "description": "Fixed processing sample rate of the graph, in Hz. 0 runs the graph at the caller's sample rate.",
          "type": "integer",
          "minimum": 0,
          "default": 0
        },
        "bufferSize": {
          "description": "Fixed processing buffer size. Only applies when `sampleRate` is non-zero.",
          "type": "integer",
          "minimum": 1,
          "default": 512
        },
        "numberOfInputs": {
          "description": "Number of buses on the graph's implicit input node.",
          "type": "integer",
          "minimum": 0,
          "default": 1
        },
        "numberOfOutputs": {
          "description": "Number of buses on the graph's implicit output node.",
          "type": "integer",
          "minimum": 0,
          "default": 1
        },
        "nodes": {
          "description": "Nodes belonging to the graph.",
          "type": "array",
          "items": { "$ref": "#/$defs/node" }
        },
        "connections": {
          "description": "Audio bus connections between nodes.",
          "type": "array",
          "items": { "$ref": "#/$defs/connection" }
        },
        "dataConnections": {
          "description": "Event/data connections between node ports.",
          "type": "array",
          "items": { "$ref": "#/$defs/dataConnection" }
        }
      }
    },

    "node": {
      "title": "Node",
      "description": "One audio graph node.",
      "type": "object",
      "additionalProperties": false,
      "required": ["type"],
      "properties": {
        "id": {
          "description": "Identifier of the node within the graph. Referenced by connections. Auto-generated when omitted, which makes the node unreachable from `connections`.",
          "type": "string",
          "minLength": 1,
          "not": { "enum": ["inputNode", "outputNode"] }
        },
        "objectID": { "$ref": "#/$defs/objectID" },
        "type": {
          "description": "Fully-qualified node type, e.g. `Switchboard.AudioPlayer` or `Superpowered.Reverb`. An unqualified name is matched against every registered node factory in turn.",
          "type": "string",
          "minLength": 1,
          "not": { "enum": ["Switchboard.InputNode", "Switchboard.OutputNode"] }
        },
        "config": { "$ref": "#/$defs/nodeParameterBlock" },
        "configuration": { "$ref": "#/$defs/nodeParameterBlock" },
        "properties": { "$ref": "#/$defs/nodeParameterBlock" }
      }
    },

    "nodeParameterBlock": {
      "description": "Parameters for one node. Keys are defined by the node type's descriptor, so they are not constrained here.",
      "type": "object"
    },

    "connection": {
      "title": "Audio connection",
      "description": "An audio bus connection from one node to another.",
      "type": "object",
      "additionalProperties": false,
      "required": ["sourceNode", "destinationNode"],
      "properties": {
        "sourceNode": {
          "description": "ID of the source node, or `nodeID.outputName` to declare a data connection inline.",
          "type": "string",
          "minLength": 1
        },
        "destinationNode": {
          "description": "ID of the destination node, or `nodeID.inputName` to declare a data connection inline.",
          "type": "string",
          "minLength": 1
        },
        "sourceBusIndex": {
          "description": "Zero-based output bus on the source node. Defaults to the first unused output bus.",
          "type": "integer",
          "minimum": 0
        },
        "destinationBusIndex": {
          "description": "Zero-based input bus on the destination node. Defaults to the first unused input bus.",
          "type": "integer",
          "minimum": 0
        }
      }
    },

    "dataConnection": {
      "title": "Data connection",
      "description": "An event/data connection between two node ports.",
      "type": "object",
      "additionalProperties": false,
      "required": ["source", "destination"],
      "properties": {
        "source": {
          "description": "Source port, as `nodeID.outputName`.",
          "type": "string",
          "minLength": 1
        },
        "destination": {
          "description": "Destination port, as `nodeID.inputName`.",
          "type": "string",
          "minLength": 1
        }
      }
    },

    "streamParameters": {
      "title": "Stream parameters",
      "description": "Requested characteristics of the system audio stream. Every field is a request; the platform may return a stream that differs.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "selectedInputDevice": { "$ref": "#/$defs/audioDevice" },
        "selectedOutputDevice": { "$ref": "#/$defs/audioDevice" },
        "numberOfInputChannels": {
          "description": "Requested input channel count. 0 accepts the device's own channel count.",
          "type": "integer",
          "minimum": 0,
          "default": 0
        },
        "numberOfOutputChannels": {
          "description": "Requested output channel count. 0 accepts the device's own channel count. Set this to drive more than two output channels.",
          "type": "integer",
          "minimum": 0,
          "default": 0
        },
        "preferredBufferSize": {
          "description": "Requested I/O buffer size in frames. 0 accepts the platform default.",
          "type": "integer",
          "minimum": 0,
          "default": 0
        },
        "preferredSampleRate": {
          "description": "Requested sample rate in Hz. 0 accepts the platform default.",
          "type": "integer",
          "minimum": 0,
          "default": 0
        }
      }
    },

    "audioDevice": {
      "title": "System audio device",
      "description": "Identifies a system audio device. In a written configuration only `id` is normally supplied; the remaining fields are populated by the SDK when it reports devices back.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "id": {
          "description": "Platform device identifier, as reported by the SDK's device enumeration. Not stable across reboots or device hot-plugs.",
          "type": "integer",
          "minimum": 0
        },
        "name": { "description": "Human-readable device name.", "type": "string" },
        "inputChannels": { "description": "Input channels the device provides.", "type": "integer", "minimum": 0 },
        "outputChannels": { "description": "Output channels the device provides.", "type": "integer", "minimum": 0 },
        "isDefaultInput": { "description": "Whether this is the system default input.", "type": "boolean" },
        "isDefaultOutput": { "description": "Whether this is the system default output.", "type": "boolean" },
        "sampleRates": {
          "description": "Sample rates the device supports.",
          "type": "array",
          "items": { "type": "integer", "minimum": 1 }
        },
        "currentSampleRate": { "description": "Sample rate the device is running at.", "type": "integer", "minimum": 0 },
        "preferredSampleRate": { "description": "Sample rate the device prefers.", "type": "integer", "minimum": 0 }
      }
    },

    "audioFile": {
      "title": "Audio file",
      "description": "One input or output file of an offline render.",
      "type": "object",
      "additionalProperties": false,
      "required": ["filePath"],
      "properties": {
        "filePath": {
          "description": "Path to the file. Supports the SDK's path prefixes, e.g. `@system/`.",
          "type": "string",
          "minLength": 1
        },
        "codec": {
          "description": "Container/codec of the file.",
          "type": "string",
          "default": "wav"
        },
        "numberOfChannels": {
          "description": "Channel count of the file.",
          "type": "integer",
          "minimum": 1,
          "default": 2
        },
        "sampleRate": {
          "description": "Sample rate of the file in Hz. 0 leaves it undefined.",
          "type": "integer",
          "minimum": 0,
          "default": 0
        }
      }
    },

    "objectID": {
      "description": "Identifier of the object within its parent scope.",
      "type": "string",
      "minLength": 1
    }
  }
}
