Examples

Single-File Project

A minimal project does not need hierarchy. One .patch file, no project.json:

# Simple worship venue — single file
template Rio3224 {
  meta { manufacturer: "Yamaha", model: "Rio3224", category: "Stagebox" }
  ports {
    Dante_Pri_In[1..32]: in(etherCON) [Dante, primary]
    Dante_Pri_Out[1..32]: out(etherCON) [Dante, primary]
    Mic_In[1..32]: in(XLR)
    Line_Out[1..16]: out(XLR)
  }
  bridge Mic_In -> Dante_Pri_Out
}

template CL5 {
  meta { manufacturer: "Yamaha", model: "CL5", category: "Console" }
  ports {
    Dante_Pri_In[1..72]: in(etherCON) [Dante, primary]
    Dante_Pri_Out[1..24]: out(etherCON) [Dante, primary]
    Mix_Bus[1..24]: out
  }
}

instance Stage_Left is Rio3224 { location: "Stage Left Wing" }
instance FOH_Console is CL5 { location: "Front of House" }

connect Stage_Left.Dante_Pri_Out -> FOH_Console.Dante_Pri_In {
  cable: "Cat6a_SL_Pri"
  length: "30m"
}

bridge Stage_Left.Mic_In[1..32] -> FOH_Console.Dante_Pri_In[1..32]

signal Lead_Vocal {
  origin: Stage_Left.Mic_In[1]
  description: "Worship leader vocal"
}

config FOH_Console {
  label Dante_Pri_In[1]: "Lead Vocal" { phantom: "true" }
  label Dante_Pri_In[2]: "Kick Drum"
}

That is a complete, valid project. The compiler parses it, validates it, and the frontend renders it on a canvas. The layout sidecar (simple-venue.layout.json) stores block positions — if it does not exist, auto-layout generates one.


Multi-File Project

A real-world project uses use statements to import templates from other files. The compiler discovers files by walking use statements from the root.

On disk

hillsong-mtg/
  project.json
  campus.patch
  campus.layout.json
  buildings/
    foh.patch
    monitors.patch
    stage.patch
  lib/
    yamaha.patch

project.json

{
  "name": "Hillsong MTG",
  "author": "A. Engineer",
  "root": "campus.patch"
}

campus.patch

# Hillsong MTG — Campus Level
use buildings.foh { FOH_System }
use buildings.monitors { Monitor_System }
use buildings.stage { Stage_System }

instance FOH is FOH_System { location: "Front of House" }
instance Monitors is Monitor_System { location: "Monitor World" }
instance Stage is Stage_System { location: "Main Stage" }

connect FOH.Dante_Out -> Monitors.Dante_In {
  cable: "Cat6a_FOH_MON"
  length: "45m"
}
connect FOH.Dante_Out -> Stage.Dante_In {
  cable: "Cat6a_FOH_STG"
  length: "60m"
}

buildings/foh.patch

# Front of House — consoles, stageboxes, network
use lib.yamaha { CL5, Rio3224 }

template FOH_System {
  meta { category: "Building" }
  ports {
    Dante_Out: out(etherCON) [Dante]
    Dante_In: in(etherCON) [Dante]
  }
}

instance Console is CL5 { location: "FOH Mix Position" }
instance SL_Rack is Rio3224 { location: "Stage Left" }

connect SL_Rack.Dante_Pri_Out -> Console.Dante_Pri_In {
  cable: "Cat6a_SL"
  length: "30m"
}

bridge SL_Rack.Mic_In[1..32] -> Console.Dante_Pri_In[1..32]

The hierarchy is not a special feature. It is just templates importing other templates via use. A .patch file IS a template at every scale — whether it describes a single device, a room, a building, or an entire campus.


Bus Display Names (label:)

Broadcast console software uses > as a routing convention in bus names — SPOTIFY>FOH, PQ>MM, Shouts > MD Conf. These characters are invalid in PatchLang identifiers. The label: property on a bus body carries the human-readable display name while the identifier remains the stable cross-reference key.

instance FOH_Engine is CL5 {
  bus PQ_MM {
    label: "PQ>MM"
    output "Mix L": Mix_Out[1]
    output "Mix R": Mix_Out[2]
  }
  bus SpotifyFOH {
    label: "SPOTIFY>FOH"
    input: Dante_Pri_In[41]
    input: Dante_Pri_In[42]
    output "Main L": Mix_Out[25]
    output "Main R": Mix_Out[26]
  }
}

The identifier (SpotifyFOH) is used everywhere buses are cross-referenced. The label is display-only — it does not affect routing, DRC, or signal tracing. The JSON output omits label when not set, so existing files are unaffected.

This is the same identifier-vs-display-name pattern used by config port labels (label Dante_Pri_In[1]: "Lead Vocal").


Ring Network (OptoCore)

A DiGiCo system with primary and redundant OptoCore rings. The ring keyword declares a fiber loop where member order reflects the physical ring topology.

template SD12 {
  meta { manufacturer: "DiGiCo", model: "SD12" }
  ports {
    OptoCore_A: io [OptoCore]
    OptoCore_B: io [OptoCore]
  }
}

template SD_Rack {
  meta { manufacturer: "DiGiCo", model: "SD-Rack" }
  ports {
    OptoCore_A: io [OptoCore]
    OptoCore_B: io [OptoCore]
  }
}

instance Console is SD12 { location: "FOH" }
instance StageRack_1 is SD_Rack { location: "Stage Left" }
instance StageRack_2 is SD_Rack { location: "Stage Right" }
instance MonitorRack is SD_Rack { location: "Monitor World" }

# Primary ring — implicit members (compiler resolves to the single OptoCore port)
ring OptoCore_Primary {
  protocol: "OptoCore"
  member Console
  member StageRack_1
  member StageRack_2
  member MonitorRack
}

# Redundant ring — explicit port references targeting the B ports
ring OptoCore_Redundant {
  protocol: "OptoCore"
  label: "Redundant ring via B ports"
  member Console.OptoCore_B
  member StageRack_1.OptoCore_B
  member StageRack_2.OptoCore_B
  member MonitorRack.OptoCore_B
}

Implicit members (member Console) work when the device has a single port matching the ring’s protocol. Explicit members (member Console.OptoCore_B) are needed when you want to specify which port participates — typically for redundant rings using the secondary fiber ports.


Channel Auto-Assignment with [auto]

When connecting devices over Dante, you need to specify which channels you’re using on each port. For six IEM packs on a monitor console, that means manually counting channel pairs:

connect MON.Dante_Pri_Out[1..2]   -> IEM_WL.Dante_In[1..2]
connect MON.Dante_Pri_Out[3..4]   -> IEM_MD.Dante_In[1..2]
connect MON.Dante_Pri_Out[5..6]   -> IEM_Keys.Dante_In[1..2]
# ...tedious and fragile

Replace the console’s output channels with [auto] and let the compiler assign them sequentially:

connect MON.Dante_Pri_Out[auto] -> IEM_WL.Dante_In[1..2]
connect MON.Dante_Pri_Out[auto] -> IEM_MD.Dante_In[1..2]
connect MON.Dante_Pri_Out[auto] -> IEM_Keys.Dante_In[1..2]
connect MON.Dante_Pri_Out[auto] -> IEM_Drums.Dante_In[1..2]
connect MON.Dante_Pri_Out[auto] -> IEM_Bass.Dante_In[1..2]
connect MON.Dante_Pri_Out[auto] -> IEM_BV.Dante_In[1..2]

The compiler resolves top-to-bottom: first [auto] gets [1..2], second gets [3..4], and so on. You can mix explicit and auto — explicit indices are pre-scanned and skipped during auto-allocation:

# Recorder always gets channels 33-34 (locked to a Dante preset)
connect MON.Dante_Pri_Out[33..34] -> Recorder.Dante_In[1..2]

# IEMs get whatever's available, starting from channel 1
connect MON.Dante_Pri_Out[auto] -> IEM_WL.Dante_In[1..2]     # resolves to [1..2]
connect MON.Dante_Pri_Out[auto] -> IEM_MD.Dante_In[1..2]     # resolves to [3..4]

Use [auto] when the specific channel numbers do not matter — the design intent is “each device gets its own pair, no overlaps.” Use explicit indices when the channel assignment is locked to hardware (a Dante preset, a saved show file) or when the diagram IS the configuration document.


DRC Suppression with @suppress

The compiler runs design rule checks (DRC) across several layers: direction, mechanical, electrical, logical, temporal, and convention. When a connection intentionally violates a rule, suppress the specific layer rather than ignoring the warning:

# A loopback cable from a console's record output back to its playback input.
# The DRC flags this as a direction concern (output feeding the same device's input).
connect Console.Dante_Pri_Out[1..2] -> Console.Dante_Pri_In[65..66] {
  @suppress(direction)
  cable: "Cat6a_Loopback"
  length: "1m"
}

You can suppress multiple layers on one connection:

# Test rig: SDI video output looped back into a Dante audio embedder.
# Intentionally crosses media domains and reverses normal signal flow.
connect Recorder.SDI_Out -> Embedder.SDI_In {
  @suppress(mechanical, logical)
  cable: "BNC_Test"
}

Use @suppress(all) as a last resort to silence every DRC layer on a single connection. Always prefer naming the specific layer so that other checks still run.


Split Directional Ports (Dante Template)

Network protocols like Dante carry audio in both directions over a single cable. In PatchLang, model this with separate in() and out() port vectors and bridge declarations that map the signal flow:

template Rio1608_D2 {
  meta { manufacturer: "Yamaha", model: "Rio1608-D2", category: "Stagebox" }
  ports {
    Dante_Pri_In[1..16]: in(etherCON) [Dante, primary]
    Dante_Pri_Out[1..16]: out(etherCON) [Dante, primary]
    Mic_In[1..16]: in(XLR)
    Line_Out[1..8]: out(XLR)
  }
  bridge Mic_In -> Dante_Pri_Out
  bridge Dante_Pri_In -> Line_Out
}

The first bridge says “mic inputs are forwarded out over Dante.” The second says “Dante receive channels drive the line outputs.” This gives the DRC enough information to trace a signal from a microphone on stage through the network to a console, and back out to an amplifier — without confusing which direction each channel flows.


Slot / Card Installation

Hardware with expansion slots (consoles, stage racks, routers) is modeled with slot declarations in the template and slot assignments on the instance:

template MADI_Card {
  meta { manufacturer: "AVID", model: "MADI" }
  ports {
    MADI_In[1..48]: in(BNC_75) [MADI]
    MADI_Out[1..48]: out(BNC_75) [MADI]
  }
}

template HDX_Card {
  meta { manufacturer: "AVID", model: "HDX Card" }
  ports {
    USB[1..64]: io(USB)
  }
}

template Venue_FOH_Rack {
  meta { manufacturer: "AVID", model: "Venue FOH Rack" }
  ports {
    AES_In[1..2]: in(XLR) [AES3]
    AES_Out[1..2]: out(XLR) [AES3]
    LINE[1..8]: io(TRS_14)
  }
  slot Expansion[1..3]: HDX_Card
  slot Snake[1..2]: MADI_Card
}

instance FOH_Engine is Venue_FOH_Rack {
  slot Expansion[1]: "HDX_Card"
  slot Expansion[2]: "HDX_Card"
  slot Snake[1]: "MADI_Card"
  slot Snake[2]: "MADI_Card"
}

The template declares what card types each slot accepts. The instance declares what is actually installed. Unassigned slots remain empty — the compiler does not require every slot to be populated.


Internal Routes

Routes describe signal paths inside a single device — how an input gets patched to an output within the hardware’s internal matrix:

template Venue_FOH_Rack {
  meta { manufacturer: "AVID", model: "Venue FOH Rack" }
  ports {
    MADI_In[1..48]: in(BNC_75) [MADI]
    MADI_Out[1..48]: out(BNC_75) [MADI]
    LINE[1..8]: io(TRS_14)
  }
}

instance FOH_Engine is Venue_FOH_Rack {
  route MADI_In[41] -> LINE[1]
  route MADI_In[42] -> LINE[2]
  route MADI_In[43] -> LINE[3]
  route MADI_In[44] -> LINE[4]
  route MADI_In[45] -> LINE[5]
  route MADI_In[46] -> LINE[6]
  route MADI_In[47] -> LINE[7]
  route MADI_In[48] -> LINE[8]
}

Routes are distinct from connect (which describes a cable between two devices) and bridge (which declares a logical signal path for DRC tracing). A route says “inside this specific device instance, input X is patched to output Y.” This is how you document a console’s internal routing matrix, a DSP’s patch bay, or a router’s crosspoints.


Config Block (Channel Labels)

A config block attaches metadata to individual channels on an instance — typically channel names and per-channel settings like phantom power:

template StageBox_16x0 {
  meta { model: "16x0 XLR" }
  ports {
    Stage_Box_Inputs[1..16]: in(XLR) [Analogue]
    Inputs_to_Patch[1..16]: out(XLR) [Analogue]
  }
}

instance Drums is StageBox_16x0 { location: "Stage Left" }

config Drums {
  label Stage_Box_Inputs[1]: "Kick In" { phantom: "true", stand: "Short Boom" }
  label Stage_Box_Inputs[2]: "Kick Out" { stand: "Short Boom" }
  label Stage_Box_Inputs[3]: "Snare Top" { stand: "Short Boom" }
  label Stage_Box_Inputs[4]: "Snare Bottom" { stand: "LP Claw" }
  label Stage_Box_Inputs[5]: "Hats" { phantom: "true" }
  label Stage_Box_Inputs[6]: "Tom 1"
  label Stage_Box_Inputs[7]: "Tom 2"
  label Stage_Box_Inputs[8]: "Tom 3"
  label Stage_Box_Inputs[10]: "OH SR" { phantom: "true", stand: "Tall Boom" }
  label Stage_Box_Inputs[11]: "OH SL" { phantom: "true", stand: "Tall Boom" }
}

Labels serve two purposes: they document the input list (what mic goes where) and they carry operational metadata (phantom power, stand type) that the frontend can display. The key-value pairs inside the braces after the label string are free-form — use whatever fields your workflow needs.


Template Composition

A template can contain instances of other templates and internal connections between them. This is how you build reusable subsystems — a “Monitor Rig” template that bundles a console with its IEM transmitters:

template PSM1000 {
  meta { manufacturer: "Shure", model: "PSM1000", category: "IEM" }
  ports {
    Dante_In[1..2]: in(etherCON) [Dante, primary]
    RF_Out[1..2]: out(BNC_50) [RF]
  }
}

template PM5D {
  meta { manufacturer: "Yamaha", model: "PM5D", category: "Console" }
  ports {
    Dante_Pri_In[1..48]: in(etherCON) [Dante, primary]
    Dante_Pri_Out[1..24]: out(etherCON) [Dante, primary]
  }
}

template Monitor_Rig {
  meta { category: "Subsystem" }
  ports {
    Dante_In[1..48]: in(etherCON) [Dante, primary]
  }

  instance Console is PM5D
  instance IEM_WL is PSM1000
  instance IEM_MD is PSM1000
  instance IEM_Keys is PSM1000

  connect Console.Dante_Pri_Out[1..2] -> IEM_WL.Dante_In[1..2]
  connect Console.Dante_Pri_Out[3..4] -> IEM_MD.Dante_In[1..2]
  connect Console.Dante_Pri_Out[5..6] -> IEM_Keys.Dante_In[1..2]
}

When you instantiate Monitor_Rig, you get the console and all three IEM transmitters as a single block on the canvas. Double-clicking it opens the internal view showing the wiring between them. This is the same mechanism that makes multi-file projects work — a building template containing room instances is just a larger-scale composition.