Lightningbeam/scripts/add_output_chain.py

166 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""
Transform all instrument preset JSON files to add a Compressor → Pan → Gain
output chain, plus Volume and Pan AutomationInput nodes wired via CV.
Only modifies presets that have a MidiInput node (i.e., MIDI instrument presets).
"""
import json
import sys
from pathlib import Path
def transform_preset(data: dict) -> dict | None:
"""Transform a preset. Returns modified dict or None if no change needed."""
nodes = data.get("nodes", [])
connections = data.get("connections", [])
# Only modify presets with a MidiInput node
if not any(n["node_type"] == "MidiInput" for n in nodes):
return None
# Skip if already transformed (has a Compressor node)
if any(n["node_type"] == "Compressor" for n in nodes):
print(" Already transformed, skipping.")
return None
output_node_id = data.get("output_node")
if output_node_id is None:
print(" No output_node, skipping.")
return None
# Find the connection going into the output node
incoming = [c for c in connections if c["to_node"] == output_node_id]
if len(incoming) != 1:
print(f" Expected 1 incoming connection to output_node, found {len(incoming)}, skipping.")
return None
conn = incoming[0]
source_node = conn["from_node"]
source_port = conn["from_port"]
# Get AudioOutput node position — new chain starts where it was
output_node_data = next((n for n in nodes if n["id"] == output_node_id), None)
out_pos = output_node_data.get("position", [700.0, 150.0]) if output_node_data else [700.0, 150.0]
if isinstance(out_pos, list):
ox, oy = float(out_pos[0]), float(out_pos[1])
else:
ox, oy = 700.0, 150.0
step = 230.0 # horizontal spacing between nodes
# Compute new node IDs
max_id = max(n["id"] for n in nodes)
comp_id = max_id + 1 # Compressor
pan_id = max_id + 2 # Pan
gain_id = max_id + 3 # Gain (volume)
vol_id = max_id + 4 # Volume AutomationInput
pan_auto_id = max_id + 5 # Pan AutomationInput
# Move the AudioOutput node to the right of the new chain
if output_node_data is not None:
output_node_data["position"] = [ox + step * 3, oy]
# Remove the existing connection to output
connections = [c for c in connections if not (c["to_node"] == output_node_id and c["from_node"] == source_node)]
# New nodes — Compressor starts where AudioOutput was
new_nodes = [
{
"id": comp_id,
"node_type": "Compressor",
"parameters": {"0": -18.0, "1": 4.0, "2": 5.0, "3": 50.0, "4": 3.0, "5": 3.0},
"position": [ox, oy]
},
{
"id": pan_id,
"node_type": "Pan",
"parameters": {"0": 0.0},
"position": [ox + step, oy]
},
{
"id": gain_id,
"node_type": "Gain",
"parameters": {"0": 1.0},
"position": [ox + step * 2, oy]
},
{
"id": vol_id,
"node_type": "AutomationInput",
"parameters": {"0": 0.0, "1": 2.0},
"automation_display_name": "Volume",
"automation_keyframes": [
{
"time": 0.0,
"value": 1.0,
"interpolation": "linear",
"ease_out": [0.58, 1.0],
"ease_in": [0.42, 0.0]
}
],
"position": [ox + step, oy + 230.0]
},
{
"id": pan_auto_id,
"node_type": "AutomationInput",
"parameters": {"0": -1.0, "1": 1.0},
"automation_display_name": "Pan",
"automation_keyframes": [
{
"time": 0.0,
"value": 0.0,
"interpolation": "linear",
"ease_out": [0.58, 1.0],
"ease_in": [0.42, 0.0]
}
],
"position": [ox, oy + 230.0]
},
]
# New connections
new_connections = [
{"from_node": source_node, "from_port": source_port, "to_node": comp_id, "to_port": 0},
{"from_node": comp_id, "from_port": 0, "to_node": pan_id, "to_port": 0},
{"from_node": pan_id, "from_port": 0, "to_node": gain_id, "to_port": 0},
{"from_node": gain_id, "from_port": 0, "to_node": output_node_id, "to_port": 0},
{"from_node": vol_id, "from_port": 0, "to_node": gain_id, "to_port": 1},
{"from_node": pan_auto_id, "from_port": 0, "to_node": pan_id, "to_port": 1},
]
data["nodes"] = nodes + new_nodes
data["connections"] = connections + new_connections
return data
def main():
instruments_dir = Path(__file__).parent.parent / "src" / "assets" / "instruments"
if not instruments_dir.exists():
print(f"Instruments directory not found: {instruments_dir}", file=sys.stderr)
sys.exit(1)
json_files = sorted(instruments_dir.rglob("*.json"))
print(f"Found {len(json_files)} preset files")
modified = 0
for path in json_files:
print(f"Processing: {path.relative_to(instruments_dir)}")
with open(path) as f:
data = json.load(f)
result = transform_preset(data)
if result is not None:
with open(path, "w") as f:
json.dump(result, f, indent=2)
print(f" -> Modified")
modified += 1
else:
print(f" -> Skipped")
print(f"\nDone. Modified {modified}/{len(json_files)} presets.")
if __name__ == "__main__":
main()