Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 3 additions & 47 deletions analyze_layout.sh
Original file line number Diff line number Diff line change
@@ -1,50 +1,6 @@
#!/usr/bin/env sh
# Run the layout analyser (build_layout_compress), wiring in lilv the same way
# run_emulator.sh does.
# Requires MOD Desktop running locally at http://127.0.0.1:18181 so plugin
# audio-port ordering can be resolved.

# Locate the lilv Python binding and shared library.
# Priority: pkg-config (Linux/macOS system install), then Homebrew.
_lilv_pypath=""
_lilv_libpath=""

if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists lilv-0 2>/dev/null; then
_libdir=$(pkg-config --variable=libdir lilv-0)
if [ -n "$_libdir" ]; then
_lilv_libpath="$_libdir"
_prefix=$(pkg-config --variable=prefix lilv-0)
for _pydir in "$_prefix"/lib/python*/site-packages; do
if [ -f "$_pydir/lilv.py" ]; then
_lilv_pypath="$_pydir"
break
fi
done
fi
fi

# Homebrew fallback (macOS)
if [ -z "$_lilv_pypath" ] && command -v brew >/dev/null 2>&1; then
_brew_prefix=$(brew --prefix lilv 2>/dev/null)
if [ -n "$_brew_prefix" ]; then
_lilv_libpath="$_brew_prefix/lib"
for _pydir in "$_brew_prefix"/lib/python*/site-packages; do
if [ -f "$_pydir/lilv.py" ]; then
_lilv_pypath="$_pydir"
break
fi
done
fi
fi

if [ -n "$_lilv_pypath" ]; then
export PYTHONPATH="${_lilv_pypath}${PYTHONPATH:+:$PYTHONPATH}"
fi
if [ -n "$_lilv_libpath" ]; then
case "$(uname -s)" in
Darwin) export DYLD_LIBRARY_PATH="${_lilv_libpath}${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" ;;
*) export LD_LIBRARY_PATH="${_lilv_libpath}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" ;;
esac
fi
# Run the layout analyser (build_layout_compress).
# Requires MOD Desktop running locally at http://127.0.0.1:18181: bundles are
# parsed by its /pedalboard/info, and plugin audio-port ordering resolved there.

exec uv run python3 tools/analyze_layout.py "$@"
2 changes: 1 addition & 1 deletion blend/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import json
import logging
import requests as req
import pistomp.httpclient as req
from pathlib import Path

from blend.types import (
Expand Down
15 changes: 8 additions & 7 deletions modalapi/connections.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Connection domain model for parsed pedalboard graphs.

Pure logic — no lilv dependency — so it stays unit-testable. The lilv arc
walk that feeds into `build_connection` lives in `modalapi.pedalboard`.
Pure logic, so it stays unit-testable. What feeds `build_connection` — mod-ui's
pedalboard/info arc list — lives in `modalapi.pedalboard`.
"""

from __future__ import annotations
Expand Down Expand Up @@ -106,7 +106,7 @@ def build_connection(
bundlepath: str,
instance_to_info: dict[str, Optional[dict]],
) -> Connection:
"""Pure builder used by both the lilv arc walk and unit tests."""
"""Pure builder used by both the pedalboard/info arc list and unit tests."""
src_id, src_sym = split_port_uri(tail_uri, bundlepath)
dst_id, dst_sym = split_port_uri(head_uri, bundlepath)
src_kind = classify_endpoint(src_id)
Expand All @@ -129,10 +129,11 @@ def build_connection(
def audio_connections(connections: Iterable[Connection]) -> list[Connection]:
"""Audio-only connections (drop midi/HW ports), in a canonical order.

lilv enumerates ingen:arc objects in a hash-randomised order, so the raw
connection list varies run-to-run. The layout pipeline (dummy numbering,
barycentric row tie-breaks) is order-sensitive, so we sort to a stable key
here — the single chokepoint every layout consumer funnels through.
Arc order is not guaranteed (mod-ui walks ingen:arc objects in a
hash-randomised order), so the raw connection list varies run-to-run. The
layout pipeline (dummy numbering, barycentric row tie-breaks) is
order-sensitive, so we sort to a stable key here — the single chokepoint
every layout consumer funnels through.
"""
audio = {EndpointKind.PLUGIN, EndpointKind.SOURCE, EndpointKind.SINK}
return sorted(
Expand Down
8 changes: 4 additions & 4 deletions modalapi/mod.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import logging
import os
import time
import requests as req
import pistomp.httpclient as req
import subprocess
import sys
import yaml
Expand Down Expand Up @@ -662,14 +662,12 @@ def load_pedalboards(self):

pbs = json.loads(resp.text)
for pb in pbs:
logging.info("Loading pedalboard info: %s" % pb[Token.TITLE])
bundle = pb[Token.BUNDLE]
title = pb[Token.TITLE]
# Hydrated on selection; see modhandler.load_pedalboards.
pedalboard = Pedalboard.Pedalboard(title, bundle, root_uri=self.root_uri)
pedalboard.load_bundle(bundle, self.plugin_dict)
self.pedalboards[bundle] = pedalboard
self.pedalboard_list.append(pedalboard)
#logging.debug("dump: %s" % pedalboard.to_json())

# TODO - example of querying host
#bund = self.get_current_pedalboard()
Expand All @@ -681,6 +679,8 @@ def get_current_pedalboard_bundle_path(self):
return read_pedalboard_bundle(self.last_json_monitor.path)

def set_current_pedalboard(self, pedalboard):
pedalboard.hydrate(self.plugin_dict)

# Cleanup all previous blend modes if active
for blend_mode in self.blend_modes.values():
blend_mode.cleanup()
Expand Down
13 changes: 7 additions & 6 deletions modalapi/modhandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
import os
import shutil
import time
import requests as req
from requests import Response
import pistomp.httpclient as req
from pistomp.httpclient import Response
import subprocess
import sys
import yaml
Expand Down Expand Up @@ -753,14 +753,13 @@ def load_pedalboards(self):

pbs = json.loads(resp.text)
for pb in pbs:
logging.info("Loading pedalboard info: %s" % pb[Token.TITLE])
bundle = pb[Token.BUNDLE]
title = pb[Token.TITLE]
# Left unhydrated: only the current board's graph is ever read, and
# hydrating all of them here cost ~10s of startup.
pedalboard = Pedalboard.Pedalboard(title, bundle, root_uri=self.root_uri, customizer=plugin_lookup)
pedalboard.load_bundle(bundle, self.plugin_dict)
self.pedalboards[bundle] = pedalboard
self.pedalboard_list.append(pedalboard)
# logging.debug("dump: %s" % pedalboard.to_json())

def reload_pedalboard(self, bundle):
# find the current pedalboard object associated with that bundle
Expand All @@ -769,7 +768,7 @@ def reload_pedalboard(self, bundle):

# create a new one
pedalboard = Pedalboard.Pedalboard(title, bundle, root_uri=self.root_uri, customizer=plugin_lookup)
pedalboard.load_bundle(bundle, self.plugin_dict)
pedalboard.hydrate(self.plugin_dict)
self.pedalboards[bundle] = pedalboard

# replace the pedalboard in pedalboard_list with the new one
Expand All @@ -787,6 +786,8 @@ def get_current_pedalboard_bundle_path(self):
return read_pedalboard_bundle(self.last_json_monitor.path)

def set_current_pedalboard(self, pedalboard):
pedalboard.hydrate(self.plugin_dict)

# Pop non-persisting panels above the first persister (e.g. a parameter
# dialog or plugin panel is dismissed; the tuner survives).
pstack = self.lcd.pstack
Expand Down
Loading
Loading