#!/bin/bash # lv2lint-check.sh — Validate LV2 plugins for compatibility with Carla/RPi4B # Uses lv2lint to check plugin spec compliance, plus runtime checks. # # Usage: # chmod +x lv2lint-check.sh # ./lv2lint-check.sh # Check all installed LV2 plugins # ./lv2lint-check.sh /path/to/plugin.lv2 # Check a specific plugin # ./lv2lint-check.sh --nam-only # Only check NAM plugin # ./lv2lint-check.sh --list # List all installed LV2 plugins # # lv2lint checks (from https://git.open-music-kontrollers.ch/lv2/lv2lint): # - URI validity, manifest parsing # - Port definitions (in/out, control/audio/CV) # - TTL consistency # - Required features # - Instantiation test # - Parameter ranges set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' log() { echo -e "${GREEN}[lv2lint]${NC} $*"; } warn() { echo -e "${YELLOW}[lv2lint]${NC} $*"; } fail() { echo -e "${RED}[lv2lint] FAIL:${NC} $*"; } # ── Install lv2lint if not present ──────────────────── install_lv2lint() { if ! command -v lv2lint >/dev/null 2>&1; then log "lv2lint not found. Building from source..." sudo apt update -qq sudo apt install -y build-essential cmake git \ liblilv-dev libserd-dev libsord-dev libsratom-dev \ lv2-dev libexempi-dev libsndfile1-dev LV2LINT_DIR="/tmp/lv2lint-build" git clone --depth 1 https://git.open-music-kontrollers.ch/lv2/lv2lint "$LV2LINT_DIR" 2>/dev/null || \ git clone --depth 1 https://github.com/brummer10/lv2lint.git "$LV2LINT_DIR" cd "$LV2LINT_DIR" meson setup build 2>/dev/null || \ (mkdir -p build && cd build && cmake .. && make -j"$(nproc)") if [ -d "build" ]; then cd build if [ -f "meson.build" ] || [ -f "../meson.build" ]; then meson compile 2>/dev/null && sudo meson install 2>/dev/null || true elif [ -f "Makefile" ]; then make -j"$(nproc)" && sudo make install 2>/dev/null || true fi fi if command -v lv2lint >/dev/null 2>&1; then log "lv2lint installed successfully." else warn "Could not install lv2lint. Skipping spec checks." fi fi } # ── List installed LV2 plugins ──────────────────────── list_plugins() { log "Installed LV2 plugins:" echo "" for dir in /usr/lib/lv2 /usr/local/lib/lv2 /usr/lib/aarch64-linux-gnu/lv2 ~/.lv2; do if [ -d "$dir" ]; then find "$dir" -maxdepth 2 -name "*.ttl" | while read -r ttl; do uri=$(grep -oP 'a\s+lv2:Plugin' "$(dirname "$ttl")"/*.ttl 2>/dev/null | head -1 || true) if [ -n "$uri" ]; then echo " $(basename "$(dirname "$ttl")")" fi done | sort -u fi done } # ── Run lv2lint checks on a plugin ───────────────────── check_plugin() { local plugin_uri="$1" local plugin_path="$2" local issues=0 log "Checking: $(basename "$plugin_path")" echo " URI: $plugin_uri" # Manifest check if [ ! -f "$plugin_path/manifest.ttl" ]; then fail " Missing manifest.ttl" issues=$((issues + 1)) fi # Binary check local so_file=$(find "$plugin_path" -name "*.so" 2>/dev/null | head -1) if [ -z "$so_file" ]; then warn " No .so file found (might be an extension-only bundle)" else echo " Binary: $so_file" file "$so_file" 2>/dev/null | sed 's/^/ /' # ARM-specific: check architecture if file "$so_file" 2>/dev/null | grep -q "x86"; then fail " WRONG ARCH: x86 binary on ARM. Rebuild!" issues=$((issues + 1)) fi # Check shared library deps echo " Shared libs needed:" ldd "$so_file" 2>/dev/null | grep "not found" | while read -r missing; do fail " MISSING: $missing" issues=$((issues + 1)) done || true fi # Run lv2lint if available if command -v lv2lint >/dev/null 2>&1; then echo " lv2lint output:" local lint_output lint_output=$(lv2lint -M pack -q -s skip "$plugin_uri" 2>&1) || true if [ -n "$lint_output" ]; then echo "$lint_output" | sed 's/^/ /' # Count warnings/errors local lint_issues=$(echo "$lint_output" | grep -ciE "(FAIL|ERROR|WARN)" || true) issues=$((issues + lint_issues)) else echo " ✓ No issues found" fi fi # Check for Carla-specific features if grep -qi "carla" "$plugin_path"/*.ttl 2>/dev/null; then echo " ✓ Contains Carla-specific extensions" fi echo "" return $issues } # ── Check NAM specifically ───────────────────────────── check_nam() { local nam_dir="" for dir in /usr/lib/lv2/neural_amp_modeler.lv2 \ /usr/local/lib/lv2/neural_amp_modeler.lv2 \ ~/.lv2/neural_amp_modeler.lv2 \ /tmp/nam-lv2-build/build/neural_amp_modeler.lv2; do if [ -d "$dir" ]; then nam_dir="$dir" break fi done if [ -z "$nam_dir" ]; then warn "NAM LV2 plugin not found. Build it first: ./scripts/nam-build.sh --install" return 1 fi check_plugin "http://github.com/mikeoliphant/neural-amp-modeler-lv2" "$nam_dir" # NAM-specific: check if atom:Path is supported (critical for model loading) if grep -q "atom:Path" "$nam_dir"/*.ttl 2>/dev/null; then log "✓ NAM supports atom:Path (model selection in Carla works)" else warn "NAM does not declare atom:Path support. Model loading may not work in Carla." fi # Performance note for RPi4B echo "" warn "NAM on RPi4B — performance note:" echo " Standard models may cause xruns at 128f buffer." echo " Use 'nano' or 'feather' models from https://www.tone3000.com/" echo " Increase JACK buffer to 256 samples if needed." } # ── Check all installed plugins ──────────────────────── check_all() { local total_issues=0 for dir in /usr/lib/lv2 /usr/local/lib/lv2 /usr/lib/aarch64-linux-gnu/lv2 ~/.lv2; do if [ ! -d "$dir" ]; then continue; fi for bundle in "$dir"/*/; do if [ -f "$bundle/manifest.ttl" ]; then # Extract URI from manifest local uri="" uri=$(grep -oP 'a\s+lv2:Plugin' "$bundle"/*.ttl 2>/dev/null | head -1 | sed 's/:.*//' || true) if [ -z "$uri" ]; then # Just use bundle name uri="urn:lv2:$(basename "$bundle")" fi check_plugin "$uri" "${bundle%/}" || total_issues=$((total_issues + $?)) fi done done echo "" log "Total issues: $total_issues" } # ── Main ─────────────────────────────────────────────── case "${1:-}" in --list|-l) list_plugins exit 0 ;; --nam-only|-n) install_lv2lint check_nam exit 0 ;; "") install_lv2lint check_all check_nam ;; *) if [ -d "$1" ]; then install_lv2lint check_plugin "urn:lv2:$(basename "$1")" "$1" else echo "Usage: $0 [--list|--nam-only|]" exit 1 fi ;; esac