Initial commit: Arduino Uno Q USB Audio Synth project
- USB gadget (UAC1 + MIDI) with boot persistence - USB host mode with auto-detection - Pure Data synth patch with ALSA + UDP netreceive MIDI input - MIDI rawmidi-to-UDP bridge (midi-bridge.py) - LED matrix visualization (led-matrix-viz.py via arduino-router RPC) - MCU sketch for LED matrix control (Arduino_RouterBridge + ArduinoGraphics) - Conftest/OPA Rego policy validation suite for .pd files - gen_synth_pd.py: programmatic .pd file generator - System services: midi-bridge, led-matrix-viz, pd-synth-auto, usb-role-detect - Justfile with deploy, enable, diagnostic recipes - Diagnostic script (diag.sh)
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
local M = {}
|
||||||
|
M.board = 'arduino:qualcomm:uno_q'
|
||||||
|
M.port = '/dev/ttyACM0'
|
||||||
|
M.baudrate = 115200
|
||||||
|
return M
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Arduino/C++ friendly format
|
||||||
|
BasedOnStyle: WebKit
|
||||||
|
|
||||||
|
IndentWidth: 4
|
||||||
|
TabWidth: 4
|
||||||
|
UseTab: Never
|
||||||
|
IndentCaseLabels: false
|
||||||
|
|
||||||
|
BreakBeforeBraces: Attach
|
||||||
|
|
||||||
|
SpaceBeforeParens: Never
|
||||||
|
SpaceAfterCStyleCast: false
|
||||||
|
PointerAlignment: Left
|
||||||
|
|
||||||
|
ColumnLimit: 100
|
||||||
|
|
||||||
|
SortIncludes: true
|
||||||
|
IncludeBlocks: Regroup
|
||||||
|
|
||||||
|
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
/tmp/
|
||||||
|
build/
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"MD013": false,
|
||||||
|
"MD060": false,
|
||||||
|
"MD025": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# Arduino Uno Q — USB Audio Synth
|
||||||
|
|
||||||
|
A 2-oscillator subtractive synthesizer running Pure Data on the Arduino Uno Q.
|
||||||
|
**Auto-detects** its USB role at boot and works in three modes — no config switching needed.
|
||||||
|
|
||||||
|
## Modes
|
||||||
|
|
||||||
|
| Mode | Connection | Audio Out | MIDI In | Use Case |
|
||||||
|
|------|-----------|-----------|---------|----------|
|
||||||
|
| **Gadget** | USB-C → Host computer | UAC1 gadget (virtual sound card) | gmidi gadget (over same cable) | Plug-and-play with DAWs |
|
||||||
|
| **Host** | USB hub → MIDI controller + USB audio interface | USB audio interface (your interface) | MIDI controller via hub | Standalone jamming |
|
||||||
|
| **I2S DAC** | I2S DAC on GPIO pins (e.g. PCM5102) | Analog line out via DAC | USB MIDI controller (gadget) | Embedded installs |
|
||||||
|
|
||||||
|
## Auto-Detection
|
||||||
|
|
||||||
|
At boot, `detect-usb-role.sh` probes the USB controller to determine the role:
|
||||||
|
|
||||||
|
```
|
||||||
|
USB cable plugged into Host? ─→ gadget mode (UAC1 + gmidi)
|
||||||
|
USB device plugged into Uno? ─→ host mode (drives USB audio interface + MIDI)
|
||||||
|
Neither? ─→ defaults to gadget (safe fallback)
|
||||||
|
```
|
||||||
|
|
||||||
|
A udev rule (`99-usb-role.rules`) re-runs detection on hotplug events and
|
||||||
|
restarts Pd with the new config — unplug from computer and plug into a hub,
|
||||||
|
it just works.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ GADGET MODE │
|
||||||
|
│ [Host DAW] ←USB-C→ ┌──────────┐ │
|
||||||
|
│ │ gmidi │←─ ALSA seq ──→ Pd │
|
||||||
|
│ │ UAC1 │── ALSA pcm ──→ Pd │
|
||||||
|
│ └──────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ HOST MODE │
|
||||||
|
│ [MIDI Ctrl] ─┐ │
|
||||||
|
│ [USB Audio] ─┼─USB hub─→ ┌──────────┐ │
|
||||||
|
│ │ │ USB host │ │
|
||||||
|
│ └──────────→ │ driver │──→ Pd │
|
||||||
|
│ └──────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ I2S DAC MODE │
|
||||||
|
│ [PCM5102] ←GPIO(I2S)→ ALSA I2S driver ──→ Pd │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
uno-q-audio-synth/
|
||||||
|
├── pd/
|
||||||
|
│ └── synth.pd # Pure Data synthesizer patch
|
||||||
|
├── system/
|
||||||
|
│ ├── configure-usb-audio.sh # UAC1 + gmidi gadget setup
|
||||||
|
│ ├── configure-usb-host.sh # Host-mode USB audio discovery
|
||||||
|
│ ├── detect-usb-role.sh # Boot-time USB role detection
|
||||||
|
│ ├── detect-i2s-device.sh # I2S device auto-detection
|
||||||
|
│ ├── on-usb-role-change.sh # udev hotplug handler
|
||||||
|
│ ├── asound.conf # ALSA config (gadget mode)
|
||||||
|
│ ├── asound-i2s.conf # ALSA config (I2S DAC mode)
|
||||||
|
│ ├── 99-usb-role.rules # udev rules for hotplug
|
||||||
|
│ ├── usb-role-detect.service # Early-boot role detection
|
||||||
|
│ ├── usb-gadget-audio.service # Gadget-only service
|
||||||
|
│ ├── pd-synth-auto.service # Unified auto-detect service
|
||||||
|
│ ├── pd-synth.service # Gadget-only Pd service
|
||||||
|
│ ├── pd-synth-i2s.service # I2S-only Pd service
|
||||||
|
│ └── detect-i2s-device.service # I2S probe service
|
||||||
|
├── scripts/
|
||||||
|
│ └── start-synth.sh # Manual dev startup (all modes)
|
||||||
|
├── install.sh # Board-side dependency install
|
||||||
|
├── justfile # Task runner
|
||||||
|
└── mise.toml # Tool version management
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Deploy to board
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export BOARD_HOST=uno-q.local # or IP if mDNS not working
|
||||||
|
|
||||||
|
just deploy # copy all files
|
||||||
|
just enable-auto # enable auto-detect mode
|
||||||
|
just reboot # apply
|
||||||
|
```
|
||||||
|
|
||||||
|
Once booted, plug into a computer — it appears as "Uno Q Audio Synth" with
|
||||||
|
audio sink + MIDI port. Or plug a USB hub with MIDI controller + audio
|
||||||
|
interface for standalone play.
|
||||||
|
|
||||||
|
### Development cycle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just cycle # deploy patch + restart Pd
|
||||||
|
just log # tail Pd logs
|
||||||
|
just status # role, ALSA cards, MIDI ports
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just test-audio # test gadget audio out
|
||||||
|
just test-audio-host # test host-mode USB audio interface
|
||||||
|
just run-remote # run Pd in foreground with auto-detect
|
||||||
|
```
|
||||||
|
|
||||||
|
## I2S DAC Wiring
|
||||||
|
|
||||||
|
Add a DAC later without reconfiguring. Common I2S DAC modules:
|
||||||
|
|
||||||
|
| DAC | Pins | Voltage |
|
||||||
|
|-----|------|---------|
|
||||||
|
| PCM5102 | BCK, DIN, LCK, 5V, GND | 3.3V–5V |
|
||||||
|
| MAX98357 (speaker amp) | BCLK, DIN, LRCLK, Vin, GND | 2.7V–5.5V |
|
||||||
|
| PCM5242 (balanced out) | I2S + I2C config | 3.3V |
|
||||||
|
|
||||||
|
Typical PCM5102 wiring to Uno Q GPIO:
|
||||||
|
|
||||||
|
```
|
||||||
|
PCM5102 → Uno Q GPIO
|
||||||
|
VIN → 5V
|
||||||
|
GND → GND
|
||||||
|
BCK → I2S_BCK (GPIO pin — check board docs)
|
||||||
|
DIN → I2S_DIN
|
||||||
|
LCK → I2S_LCK
|
||||||
|
```
|
||||||
|
|
||||||
|
Activate I2S mode: `just enable-i2s && just reboot`
|
||||||
|
|
||||||
|
## Controls (MIDI)
|
||||||
|
|
||||||
|
| Control | Mapping |
|
||||||
|
|---------|---------|
|
||||||
|
| MIDI Note On/Off | Play notes |
|
||||||
|
| Velocity | Volume |
|
||||||
|
| CC 74 | Filter cutoff |
|
||||||
|
| GUI sliders (Pd) | Detune, volume, ADSR |
|
||||||
|
|
||||||
|
## Requirements (on the Uno Q)
|
||||||
|
|
||||||
|
- Pure Data (`apt install puredata`)
|
||||||
|
- ALSA utilities (`apt install alsa-utils`)
|
||||||
|
- Linux kernel with UAC + gmidi gadget support (Qualcomm DragonWing)
|
||||||
|
|
||||||
|
## Pd Patch: synth.pd
|
||||||
|
|
||||||
|
- **OSC 1**: Saw wave, direct pitch
|
||||||
|
- **OSC 2**: Saw wave, detunable (±50 cents)
|
||||||
|
- **Mix**: Balanced stereo mix
|
||||||
|
- **Filter**: Low-pass (lop~) with adjustable cutoff
|
||||||
|
- **Envelope**: env~ (attack, decay, sustain, release)
|
||||||
|
- **Output**: Stereo 48kHz 16-bit via dac~
|
||||||
|
|
||||||
|
Open in Pd GUI to adjust slider positions and save.
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# install.sh — Install system dependencies on the Uno Q
|
||||||
|
# Run this on the board: ssh arduino@uno-q.local 'bash -s' < install.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "[INSTALL] Updating packages..."
|
||||||
|
sudo apt update -qq
|
||||||
|
|
||||||
|
echo "[INSTALL] Installing Pure Data..."
|
||||||
|
sudo apt install -y -qq puredata alsa-utils
|
||||||
|
|
||||||
|
echo "[INSTALL] Installing USB gadget config..."
|
||||||
|
sudo cp system/configure-usb-audio.sh /usr/local/bin/
|
||||||
|
sudo chmod +x /usr/local/bin/configure-usb-audio.sh
|
||||||
|
sudo cp system/asound.conf /etc/asound.conf
|
||||||
|
|
||||||
|
echo "[INSTALL] Installing systemd services..."
|
||||||
|
sudo cp system/usb-gadget-audio.service /etc/systemd/system/
|
||||||
|
sudo cp system/pd-synth.service /etc/systemd/system/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
echo "[INSTALL] Enabling services..."
|
||||||
|
sudo systemctl enable usb-gadget-audio
|
||||||
|
sudo systemctl enable pd-synth
|
||||||
|
|
||||||
|
echo "[INSTALL] Done. Reboot with: sudo reboot"
|
||||||
@@ -0,0 +1,448 @@
|
|||||||
|
# Arduino Uno Q — USB Audio Synth
|
||||||
|
# Run with: just <recipe>
|
||||||
|
# Override board host: just BOARD_HOST=192.168.1.100 deploy
|
||||||
|
|
||||||
|
BOARD_HOST := env_var_or_default('BOARD_HOST', 'uno-q.local')
|
||||||
|
BOARD_USER := env_var_or_default('BOARD_USER', 'arduino')
|
||||||
|
BOARD_PASS := env_var_or_default('BOARD_PASS', 'arduino')
|
||||||
|
SSH_KEY := env_var_or_default('SSH_KEY', '/home/david/.ssh/unoq_deploy_key')
|
||||||
|
PD_DIR := "/home/" + BOARD_USER + "/pd"
|
||||||
|
SYSTEM_DIR := "/usr/local"
|
||||||
|
SSH_OPTS := "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i " + SSH_KEY
|
||||||
|
|
||||||
|
default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────
|
||||||
|
# FACTORY SETUP — first-time board configuration
|
||||||
|
# ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Full factory setup: connect to board via USB (ADB), set password,
|
||||||
|
# connect WiFi, enable SSH, install Pd, deploy project
|
||||||
|
factory-setup:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
echo "[SETUP] Arduino Uno Q — Factory to Synth"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check ADB
|
||||||
|
if ! command -v adb &>/dev/null; then
|
||||||
|
echo "[ERROR] adb not found. Install: sudo apt install adb"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEVICE=$(adb devices | grep -v "List" | grep "device$" | head -1 | awk '{print $1}')
|
||||||
|
if [ -z "$DEVICE" ]; then
|
||||||
|
echo "[ERROR] No ADB device found. Connect Uno Q via USB."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[OK] Found ADB device: $DEVICE"
|
||||||
|
|
||||||
|
# Reset expired password
|
||||||
|
echo "[STEP] Setting board user password..."
|
||||||
|
adb -s "$DEVICE" shell 'printf "{{BOARD_PASS}}\n{{BOARD_PASS}}\n" | passwd' 2>/dev/null
|
||||||
|
|
||||||
|
# Connect WiFi
|
||||||
|
read -p "WiFi SSID: " WIFI_SSID
|
||||||
|
read -rsp "WiFi password: " WIFI_PASS
|
||||||
|
echo
|
||||||
|
adb -s "$DEVICE" shell "echo '{{BOARD_PASS}}' | sudo -S nmcli device wifi connect \"$WIFI_SSID\" password \"$WIFI_PASS\""
|
||||||
|
|
||||||
|
# Get IP
|
||||||
|
IP=$(adb -s "$DEVICE" shell "ip addr show wlan0 2>/dev/null | grep 'inet ' | awk '{print \$2}' | cut -d/ -f1")
|
||||||
|
if [ -z "$IP" ]; then
|
||||||
|
echo "[ERROR] Could not get IP address"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[OK] Board IP: $IP"
|
||||||
|
|
||||||
|
# Generate SSH host keys + enable SSH
|
||||||
|
adb -s "$DEVICE" shell 'echo "{{BOARD_PASS}}" | sudo -S ssh-keygen -A 2>/dev/null'
|
||||||
|
adb -s "$DEVICE" shell 'echo "{{BOARD_PASS}}" | sudo -S systemctl enable --now ssh 2>/dev/null'
|
||||||
|
|
||||||
|
# Install SSH key
|
||||||
|
KEY=$(cat "{{SSH_KEY}}.pub" 2>/dev/null || echo "")
|
||||||
|
if [ -n "$KEY" ]; then
|
||||||
|
adb -s "$DEVICE" shell "mkdir -p ~/.ssh && echo '$KEY' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && chmod 700 ~/.ssh"
|
||||||
|
echo "[OK] SSH key installed"
|
||||||
|
else
|
||||||
|
echo "[WARN] No SSH key at {{SSH_KEY}}.pub — generate with: ssh-keygen -t ed25519 -f {{SSH_KEY}}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install packages
|
||||||
|
echo "[STEP] Installing puredata (this may take a while)..."
|
||||||
|
ssh {{SSH_OPTS}} "{{BOARD_USER}}@$IP" 'echo "{{BOARD_PASS}}" | sudo -S apt-get install -y -qq puredata alsa-utils 2>&1 | tail -3'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[DONE] Board is ready at $IP"
|
||||||
|
echo " Run: just BOARD_HOST=$IP setup"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────
|
||||||
|
# DEPLOY
|
||||||
|
# ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Copy Pd patch to board
|
||||||
|
deploy-patch:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
SCP_OPTS="{{SSH_OPTS}}"
|
||||||
|
echo "[DEPLOY] Patching to {{BOARD_USER}}@{{BOARD_HOST}}:{{PD_DIR}}"
|
||||||
|
ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" "mkdir -p {{PD_DIR}}"
|
||||||
|
scp $SCP_OPTS pd/synth.pd "{{BOARD_USER}}@{{BOARD_HOST}}:{{PD_DIR}}/"
|
||||||
|
|
||||||
|
# Copy system config files + scripts to board
|
||||||
|
deploy-system:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
SCP_OPTS="{{SSH_OPTS}}"
|
||||||
|
echo "[DEPLOY] System config to {{BOARD_USER}}@{{BOARD_HOST}}"
|
||||||
|
for script in configure-usb-audio.sh configure-usb-host.sh detect-usb-role.sh detect-i2s-device.sh on-usb-role-change.sh; do
|
||||||
|
scp $SCP_OPTS "system/$script" "{{BOARD_USER}}@{{BOARD_HOST}}:{{SYSTEM_DIR}}/bin/"
|
||||||
|
done
|
||||||
|
ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" 'echo "{{BOARD_PASS}}" | sudo -S chmod +x {{SYSTEM_DIR}}/bin/configure-usb-*.sh {{SYSTEM_DIR}}/bin/detect-*.sh {{SYSTEM_DIR}}/bin/on-usb-role-change.sh'
|
||||||
|
scp $SCP_OPTS system/asound.conf "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/"
|
||||||
|
scp $SCP_OPTS system/asound-i2s.conf "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/"
|
||||||
|
ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" 'echo "{{BOARD_PASS}}" | sudo -S mv /tmp/asound.conf /etc/asound.conf && echo "{{BOARD_PASS}}" | sudo -S mv /tmp/asound-i2s.conf /etc/asound-i2s.conf'
|
||||||
|
scp $SCP_OPTS scripts/start-synth.sh "{{BOARD_USER}}@{{BOARD_HOST}}:{{SYSTEM_DIR}}/bin/"
|
||||||
|
ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" 'echo "{{BOARD_PASS}}" | sudo -S chmod +x {{SYSTEM_DIR}}/bin/start-synth.sh'
|
||||||
|
|
||||||
|
# Deploy everything
|
||||||
|
deploy: deploy-patch deploy-system
|
||||||
|
@echo "[OK] All files deployed"
|
||||||
|
|
||||||
|
# Deploy midi-bridge script and service to board
|
||||||
|
deploy-midi-bridge:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
SCP_OPTS="{{SSH_OPTS}}"
|
||||||
|
echo "[DEPLOY] midi-bridge to {{BOARD_USER}}@{{BOARD_HOST}}"
|
||||||
|
scp $SCP_OPTS scripts/midi-bridge.py "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/"
|
||||||
|
ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo mv /tmp/midi-bridge.py {{SYSTEM_DIR}}/bin/ && sudo chmod +x {{SYSTEM_DIR}}/bin/midi-bridge.py"
|
||||||
|
scp $SCP_OPTS system/midi-bridge.service "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/"
|
||||||
|
ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo mv /tmp/midi-bridge.service /etc/systemd/system/ && sudo systemctl daemon-reload"
|
||||||
|
echo "[OK] midi-bridge deployed"
|
||||||
|
|
||||||
|
# Deploy LED matrix visualization script and service
|
||||||
|
deploy-led-matrix-viz:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
SCP_OPTS="{{SSH_OPTS}}"
|
||||||
|
echo "[DEPLOY] led-matrix-viz to {{BOARD_USER}}@{{BOARD_HOST}}"
|
||||||
|
scp $SCP_OPTS scripts/led-matrix-viz.py "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/"
|
||||||
|
ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo mv /tmp/led-matrix-viz.py {{SYSTEM_DIR}}/bin/ && sudo chmod +x {{SYSTEM_DIR}}/bin/led-matrix-viz.py"
|
||||||
|
scp $SCP_OPTS system/led-matrix-viz.service "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/"
|
||||||
|
ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo mv /tmp/led-matrix-viz.service /etc/systemd/system/ && sudo systemctl daemon-reload"
|
||||||
|
echo "[OK] led-matrix-viz deployed"
|
||||||
|
|
||||||
|
# Deploy everything (patch + system + midi-bridge + led-matrix-viz)
|
||||||
|
deploy-all: deploy deploy-midi-bridge deploy-led-matrix-viz
|
||||||
|
@echo "[OK] All files and scripts deployed"
|
||||||
|
|
||||||
|
# Enable midi-bridge service
|
||||||
|
enable-midi-bridge:
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo systemctl enable midi-bridge && echo '[OK] midi-bridge enabled'"
|
||||||
|
|
||||||
|
# Enable led-matrix-viz service
|
||||||
|
enable-led-matrix-viz:
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo systemctl enable led-matrix-viz && echo '[OK] led-matrix-viz enabled'"
|
||||||
|
|
||||||
|
# Start & enable host mode services (midi-bridge + led-matrix-viz)
|
||||||
|
enable-host-mode: deploy-midi-bridge deploy-led-matrix-viz
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
|
||||||
|
sudo systemctl enable midi-bridge led-matrix-viz && \
|
||||||
|
sudo systemctl start midi-bridge led-matrix-viz && \
|
||||||
|
echo '[OK] Host mode services enabled and started'"
|
||||||
|
|
||||||
|
# Install all systemd services
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
SCP_OPTS="{{SSH_OPTS}}"
|
||||||
|
echo "[INSTALL] Services on {{BOARD_USER}}@{{BOARD_HOST}}"
|
||||||
|
for svc in usb-role-detect usb-gadget-audio pd-synth-auto pd-synth detect-i2s-device pd-synth-i2s; do
|
||||||
|
scp $SCP_OPTS "system/$svc.service" "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/"
|
||||||
|
done
|
||||||
|
scp $SCP_OPTS system/99-usb-role.rules "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/"
|
||||||
|
ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" "\
|
||||||
|
for svc in usb-role-detect usb-gadget-audio pd-synth-auto pd-synth detect-i2s-device pd-synth-i2s; do \
|
||||||
|
sudo mv /tmp/\$svc.service /etc/systemd/system/; \
|
||||||
|
done && \
|
||||||
|
sudo mv /tmp/99-usb-role.rules /etc/udev/rules.d/ && \
|
||||||
|
sudo udevadm control --reload-rules && \
|
||||||
|
sudo systemctl daemon-reload"
|
||||||
|
|
||||||
|
# Enable auto-detect mode (default — works for both gadget and host)
|
||||||
|
enable-auto: install-services
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
|
||||||
|
sudo systemctl disable usb-gadget-audio pd-synth detect-i2s-device pd-synth-i2s 2>/dev/null; \
|
||||||
|
sudo systemctl enable usb-role-detect pd-synth-auto && \
|
||||||
|
echo '[OK] Auto-detect mode enabled — reboot to apply'"
|
||||||
|
|
||||||
|
# Enable fixed USB gadget mode (always acts as peripheral)
|
||||||
|
enable-gadget: install-services
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
|
||||||
|
sudo systemctl disable usb-role-detect pd-synth-auto detect-i2s-device pd-synth-i2s 2>/dev/null; \
|
||||||
|
sudo systemctl enable usb-gadget-audio pd-synth && \
|
||||||
|
echo '[OK] USB gadget mode enabled — reboot to apply'"
|
||||||
|
|
||||||
|
# Enable fixed I2S DAC mode
|
||||||
|
enable-i2s: install-services
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
|
||||||
|
sudo systemctl disable usb-role-detect pd-synth-auto usb-gadget-audio pd-synth 2>/dev/null; \
|
||||||
|
sudo systemctl enable detect-i2s-device pd-synth-i2s && \
|
||||||
|
sudo cp /etc/asound-i2s.conf /etc/asound.conf && \
|
||||||
|
echo '[OK] I2S DAC mode enabled — reboot to apply'"
|
||||||
|
|
||||||
|
# Start Pd (auto-detect or whichever service is enabled)
|
||||||
|
start:
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
|
||||||
|
sudo systemctl start pd-synth-auto 2>/dev/null || \
|
||||||
|
sudo systemctl start pd-synth 2>/dev/null || \
|
||||||
|
sudo systemctl start pd-synth-i2s 2>/dev/null || \
|
||||||
|
echo '[ERROR] No Pd service found'"
|
||||||
|
|
||||||
|
# Stop Pd
|
||||||
|
stop:
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo killall pd 2>/dev/null; echo '[OK] Pd stopped'"
|
||||||
|
|
||||||
|
# Restart Pd
|
||||||
|
restart: stop start
|
||||||
|
|
||||||
|
# Reboot board
|
||||||
|
reboot:
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "echo '{{BOARD_PASS}}' | sudo -S reboot"
|
||||||
|
|
||||||
|
# Run Pd manually in auto-detect mode (for testing)
|
||||||
|
run-remote:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
SCP_OPTS="{{SSH_OPTS}}"
|
||||||
|
echo "[RUN] Starting Pd on {{BOARD_USER}}@{{BOARD_HOST}} (auto-detect)"
|
||||||
|
ssh $SCP_OPTS -t "{{BOARD_USER}}@{{BOARD_HOST}}" "killall pd 2>/dev/null; sudo {{SYSTEM_DIR}}/bin/start-synth.sh --auto --verbose"
|
||||||
|
|
||||||
|
# Test audio (gadget mode)
|
||||||
|
test-audio:
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "speaker-test -D hw:UAC1Gadget,0 -c 2 -t sine -f 440 -l 1"
|
||||||
|
|
||||||
|
# Test audio (host mode — first USB audio interface)
|
||||||
|
test-audio-host:
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
|
||||||
|
card=\$(aplay -l 2>/dev/null | grep -i usb | grep -vi gadget | head -1 | awk '{print \$2}' | tr -d ':'); \
|
||||||
|
if [ -n \"\$card\" ]; then \
|
||||||
|
speaker-test -D hw:\$card,0 -c 2 -t sine -f 440 -l 1; \
|
||||||
|
else \
|
||||||
|
echo '[ERROR] No USB audio interface found'; \
|
||||||
|
fi"
|
||||||
|
|
||||||
|
# Deploy diagnostic script to board
|
||||||
|
deploy-diag:
|
||||||
|
@scp {{SSH_OPTS}} scripts/diag.sh "{{BOARD_USER}}@{{BOARD_HOST}}:{{SYSTEM_DIR}}/bin/diag.sh"
|
||||||
|
|
||||||
|
# Show system status (quick — uses diag.sh on board)
|
||||||
|
status:
|
||||||
|
@scp {{SSH_OPTS}} -q scripts/diag.sh "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/diag.sh" && \
|
||||||
|
ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "echo '{{BOARD_PASS}}' | sudo -S bash /tmp/diag.sh 2>/dev/null" | head -60
|
||||||
|
|
||||||
|
# Test MIDI input (list ALSA sequencer ports)
|
||||||
|
midi-test:
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "aconnect -i -o -l"
|
||||||
|
|
||||||
|
# Quick deploy and restart cycle
|
||||||
|
cycle: deploy-patch
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo killall -s TERM pd 2>/dev/null; sleep 2; echo '[OK] Pd restarted with new patch'"
|
||||||
|
|
||||||
|
# Tail Pd logs (auto-detect mode)
|
||||||
|
log:
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
|
||||||
|
for svc in pd-synth-auto pd-synth pd-synth-i2s; do \
|
||||||
|
if systemctl is-active \$svc &>/dev/null; then \
|
||||||
|
sudo journalctl -u \$svc -f -n 50; \
|
||||||
|
exit 0; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
echo '[ERROR] No Pd service is running'"
|
||||||
|
|
||||||
|
# Configure USB gadget manually
|
||||||
|
init-usb-audio:
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo configure-usb-audio.sh start"
|
||||||
|
|
||||||
|
# Full setup: deploy + enable auto-detect + reboot
|
||||||
|
setup: deploy enable-auto
|
||||||
|
@echo "[SETUP] Deployed and auto-detect enabled. Run 'just reboot' to start."
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────
|
||||||
|
# LOCAL TESTING (on your computer)
|
||||||
|
# ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Open local plugdata controller + monitor for Uno Q synth
|
||||||
|
play-local:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
echo "[PLAY] Opening plugdata controller for Uno Q synth..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if ! command -v plugdata &>/dev/null; then
|
||||||
|
echo "[ERROR] plugdata not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! aconnect -l 2>/dev/null | grep -q "Uno Q Audio Synth"; then
|
||||||
|
echo "[ERROR] Uno Q not found in ALSA sequencer — is it connected?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
MIDI_PORT=$(aconnect -l 2>/dev/null | grep "Uno Q Audio Synth" | head -1 | awk '{print $2}' | tr -d ':')
|
||||||
|
|
||||||
|
echo " MIDI port: $MIDI_PORT (Uno Q Audio Synth)"
|
||||||
|
echo " Audio in: hw:Synth,0 (UAC1 gadget — synth return)"
|
||||||
|
echo " Patch: pd/controller.pd"
|
||||||
|
echo ""
|
||||||
|
echo " IN PLUGDATA, configure:"
|
||||||
|
echo " Settings → Audio → Device Type: ALSA"
|
||||||
|
echo " Settings → Audio → Input: hw:Synth,0"
|
||||||
|
echo " Settings → Audio → Output: Default"
|
||||||
|
echo " Settings → MIDI Input: all — MIDI Output: Uno Q"
|
||||||
|
echo " Enable DSP (Ctrl+D)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
plugdata pd/controller.pd
|
||||||
|
|
||||||
|
# Start interactive play session: on-screen keyboard + audio monitor
|
||||||
|
play:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
echo "[PLAY] Starting Uno Q synth test environment..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Install vmpk if missing
|
||||||
|
if ! command -v vmpk &>/dev/null; then
|
||||||
|
echo "[STEP] Installing vmpk..."
|
||||||
|
sudo apt-get install -y -qq vmpk
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify Uno Q is connected via ALSA (should be visible through PipeWire)
|
||||||
|
if ! aconnect -l 2>/dev/null | grep -q "Uno Q Audio Synth"; then
|
||||||
|
echo "[ERROR] Uno Q not found in ALSA sequencer"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! pw-cli list-objects 2>/dev/null | grep -q "Uno Q Audio Synth"; then
|
||||||
|
echo "[ERROR] Uno Q not found via PipeWire"
|
||||||
|
[ -z "$(lsusb -d 2341:006a 2>/dev/null)" ] && echo " Device not showing in lsusb either!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start vmpk in background
|
||||||
|
echo "[STEP] Starting on-screen keyboard..."
|
||||||
|
vmpk &
|
||||||
|
VMPK_PID=$!
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Route vmpk MIDI -> Uno Q via ALSA sequencer
|
||||||
|
VMPK_PORT=$(aconnect -l 2>/dev/null | grep -B1 "Virtual MIDI Piano" | head -1 | awk '{print $2}' | tr -d ':')
|
||||||
|
UNOQ_PORT=$(aconnect -l 2>/dev/null | grep "Uno Q Audio Synth" | head -1 | awk '{print $2}' | tr -d ':')
|
||||||
|
if [ -n "$VMPK_PORT" ] && [ -n "$UNOQ_PORT" ]; then
|
||||||
|
aconnect "$VMPK_PORT" "$UNOQ_PORT" 2>/dev/null && echo " MIDI: vmpk -> Uno Q Synth"
|
||||||
|
else
|
||||||
|
echo " [WARN] Auto-connect failed. Use: aconnect \$VMPK_PORT \$UNOQ_PORT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Route Uno Q audio -> host speakers via PipeWire
|
||||||
|
UNOQ_CAPTURE=$(pw-cli list-objects 2>/dev/null | grep -B1 "alsa_input.*Uno_Q" | grep "node.name" | head -1 | awk -F'"' '{print $2}')
|
||||||
|
HOST_OUTPUT="alsa_output.pci-0000_00_1f.3.analog-stereo"
|
||||||
|
if [ -n "$UNOQ_CAPTURE" ]; then
|
||||||
|
pw-link "$UNOQ_CAPTURE:capture_MONO" "$HOST_OUTPUT:playback_FL" 2>/dev/null && \
|
||||||
|
pw-link "$UNOQ_CAPTURE:capture_MONO" "$HOST_OUTPUT:playback_FR" 2>/dev/null && \
|
||||||
|
echo " Audio: Uno Q Synth -> host speakers"
|
||||||
|
else
|
||||||
|
echo " [WARN] Could not find Uno Q audio capture node"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=============================="
|
||||||
|
echo " Synth is LIVE!"
|
||||||
|
echo " Click keys in the vmpk window"
|
||||||
|
echo " Close vmpk or Ctrl+C to stop"
|
||||||
|
echo "=============================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
wait $VMPK_PID 2>/dev/null
|
||||||
|
echo "[STOP] Cleaning up..."
|
||||||
|
pw-disconnect "$UNOQ_CAPTURE:capture_MONO" "$HOST_OUTPUT:playback_FL" 2>/dev/null || true
|
||||||
|
pw-disconnect "$UNOQ_CAPTURE:capture_MONO" "$HOST_OUTPUT:playback_FR" 2>/dev/null || true
|
||||||
|
kill $VMPK_PID 2>/dev/null || true
|
||||||
|
aconnect -x 2>/dev/null || true
|
||||||
|
echo "[OK] Done"
|
||||||
|
|
||||||
|
# Stop test session
|
||||||
|
stop-play:
|
||||||
|
@pw-disconnect "$(pw-cli list-objects 2>/dev/null | grep -B1 "alsa_input.*Uno_Q" | grep "node.name" | head -1 | awk -F'"' '{print $2}'):capture_MONO" "alsa_output.pci-0000_00_1f.3.analog-stereo:playback_FL" 2>/dev/null; \
|
||||||
|
pw-disconnect "$(pw-cli list-objects 2>/dev/null | grep -B1 "alsa_input.*Uno_Q" | grep "node.name" | head -1 | awk -F'"' '{print $2}'):capture_MONO" "alsa_output.pci-0000_00_1f.3.analog-stereo:playback_FR" 2>/dev/null; \
|
||||||
|
killall vmpk 2>/dev/null; aconnect -x 2>/dev/null; echo "[OK] Stopped"
|
||||||
|
|
||||||
|
# Show current MIDI routing
|
||||||
|
midi-route:
|
||||||
|
@echo "=== ALSA MIDI Connections ===" && aconnect -l && echo "" && echo "=== Active connections ===" && aconnect -lio 2>/dev/null | grep -A1 "Connected From\|Connected To" || echo "(none)"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────
|
||||||
|
# DIAGNOSTICS
|
||||||
|
# ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Full diagnostic dump of the board
|
||||||
|
diag:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
SCP_OPTS="{{SSH_OPTS}}"
|
||||||
|
HOST="{{BOARD_USER}}@{{BOARD_HOST}}"
|
||||||
|
|
||||||
|
echo "=============================="
|
||||||
|
echo " Uno Q Audio Synth Diagnostics"
|
||||||
|
echo "=============================="
|
||||||
|
|
||||||
|
# Push and run the diagnostic script on the board
|
||||||
|
scp $SCP_OPTS scripts/diag.sh "$HOST:/tmp/diag.sh" >/dev/null
|
||||||
|
ssh $SCP_OPTS "$HOST" "echo '{{BOARD_PASS}}' | sudo -S bash /tmp/diag.sh"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- USB bus (host-side) ---"
|
||||||
|
if command -v lsusb &>/dev/null; then
|
||||||
|
lsusb -d 2341:006a 2>/dev/null | awk '{print " " $0}' || echo " (no Uno Q device on host bus)"
|
||||||
|
lsusb -d 2341:006a -t 2>/dev/null | awk '{print " " $0}'
|
||||||
|
else
|
||||||
|
echo " (lsusb not available on host)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "--- Host ALSA ---"
|
||||||
|
echo " Audio:"
|
||||||
|
aplay -l 2>/dev/null | grep -i "synth\|uno" | awk '{print " " $0}' || echo " (not found on host)"
|
||||||
|
echo " MIDI:"
|
||||||
|
aconnect -l 2>/dev/null | grep -i "synth\|uno\|midi" | awk '{print " " $0}' || echo " (not found on host)"
|
||||||
|
amidi -l 2>/dev/null | grep -i "synth\|uno" | awk '{print " " $0}' || echo " (not found on host)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=============================="
|
||||||
|
echo " Diagnostics Complete"
|
||||||
|
echo "=============================="
|
||||||
|
|
||||||
|
# Show kernel module info for custom modules
|
||||||
|
modinfo:
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
|
||||||
|
for m in usb_f_uac1 usb_f_midi; do \
|
||||||
|
echo \"=== \$m ===\"; \
|
||||||
|
modinfo \$m 2>/dev/null | grep -E 'filename|description|version|parm' | awk '{print \" \" \$0}'; \
|
||||||
|
echo ''; \
|
||||||
|
done"
|
||||||
|
|
||||||
|
# Check kernel config for USB gadget features
|
||||||
|
kconfig:
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
|
||||||
|
zgrep -E 'CONFIG_USB_(GADGET|CONFIGFS|F_UAC1|F_MIDI|DWC3|F_ACC|F_SERIAL)' /proc/config.gz 2>/dev/null | awk -F= '{printf \" %-45s %s\\n\", \$1, \$2}'"
|
||||||
|
|
||||||
|
# Validate all .pd files using Conftest/OPA Rego policies
|
||||||
|
validate:
|
||||||
|
python3 pd-validator/validate.py pd/*.pd
|
||||||
|
|
||||||
|
# Install udev rules for hotplug role changes
|
||||||
|
install-udev-rules:
|
||||||
|
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
|
||||||
|
sudo cp /etc/udev/rules.d/99-usb-role.rules /etc/udev/rules.d/99-usb-role.rules.bak 2>/dev/null; \
|
||||||
|
sudo cp /tmp/99-usb-role.rules /etc/udev/rules.d/ && \
|
||||||
|
sudo udevadm control --reload-rules && \
|
||||||
|
echo '[OK] udev rules installed'"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# Arduino Uno Q — USB Audio Synth
|
||||||
|
#
|
||||||
|
# Tasks are managed by just (see justfile), not mise.
|
||||||
|
# Run `just --list` for available commands.
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[tools]
|
||||||
|
just = "latest"
|
||||||
|
|
||||||
|
[env]
|
||||||
|
BOARD_HOST = "uno-q.local"
|
||||||
|
BOARD_USER = "arduino"
|
||||||
|
PD_PATCH = "synth.pd"
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
# ─── Pd .pd file format validator (OPA v1.x syntax) ───
|
||||||
|
# Based on puredata.info/docs/developer/PdFileFormat
|
||||||
|
# OPA version: 1.15.2 (bundled with conftest 0.68.2)
|
||||||
|
|
||||||
|
# Input structure (from pd2json parser):
|
||||||
|
# {
|
||||||
|
# "x_record_count": N,
|
||||||
|
# "records": [{ "type": "obj"|"connect"|..., "x_index": N, ... }],
|
||||||
|
# "parse_errors": [...]
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Get a record by its x_index
|
||||||
|
record_by_idx(idx) := r if {
|
||||||
|
some r_rec in input.records
|
||||||
|
r_rec.x_index == idx
|
||||||
|
r := r_rec
|
||||||
|
}
|
||||||
|
|
||||||
|
xcount := input.x_record_count
|
||||||
|
|
||||||
|
# Check if an object is a signal (~) type
|
||||||
|
is_signal(name) if {
|
||||||
|
endswith(name, "~")
|
||||||
|
}
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════
|
||||||
|
# VIOLATIONS
|
||||||
|
# ═══════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# Conftest evaluates rules named `violation`, `violations`, `deny`, `warn`
|
||||||
|
|
||||||
|
# Out-of-bounds source index
|
||||||
|
violation contains msg if {
|
||||||
|
some rec in input.records
|
||||||
|
rec.type == "connect"
|
||||||
|
rec.src >= xcount
|
||||||
|
msg := sprintf("connect[%d]: src index %d out of range (max %d)", [rec.x_index, rec.src, xcount - 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Out-of-bounds destination index
|
||||||
|
violation contains msg if {
|
||||||
|
some rec in input.records
|
||||||
|
rec.type == "connect"
|
||||||
|
rec.dst >= xcount
|
||||||
|
msg := sprintf("connect[%d]: dst index %d out of range (max %d)", [rec.x_index, rec.dst, xcount - 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Negative source
|
||||||
|
violation contains msg if {
|
||||||
|
some rec in input.records
|
||||||
|
rec.type == "connect"
|
||||||
|
rec.src < 0
|
||||||
|
msg := sprintf("connect[%d]: negative src index %d", [rec.x_index, rec.src])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Negative destination
|
||||||
|
violation contains msg if {
|
||||||
|
some rec in input.records
|
||||||
|
rec.type == "connect"
|
||||||
|
rec.dst < 0
|
||||||
|
msg := sprintf("connect[%d]: negative dst index %d", [rec.x_index, rec.dst])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Negative outlet
|
||||||
|
violation contains msg if {
|
||||||
|
some rec in input.records
|
||||||
|
rec.type == "connect"
|
||||||
|
rec.outlet < 0
|
||||||
|
msg := sprintf("connect[%d]: negative outlet %d", [rec.x_index, rec.outlet])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Negative inlet
|
||||||
|
violation contains msg if {
|
||||||
|
some rec in input.records
|
||||||
|
rec.type == "connect"
|
||||||
|
rec.inlet < 0
|
||||||
|
msg := sprintf("connect[%d]: negative inlet %d", [rec.x_index, rec.inlet])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Self-loop
|
||||||
|
violation contains msg if {
|
||||||
|
some rec in input.records
|
||||||
|
rec.type == "connect"
|
||||||
|
rec.src == rec.dst
|
||||||
|
msg := sprintf("connect[%d]: self-loop (src=%d dst=%d)", [rec.x_index, rec.src, rec.dst])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Duplicate connections
|
||||||
|
violation contains msg if {
|
||||||
|
some a, b in input.records
|
||||||
|
a.type == "connect"
|
||||||
|
b.type == "connect"
|
||||||
|
a.x_index < b.x_index
|
||||||
|
a.src == b.src
|
||||||
|
a.outlet == b.outlet
|
||||||
|
a.dst == b.dst
|
||||||
|
a.inlet == b.inlet
|
||||||
|
msg := sprintf("connect[%d]/[%d]: duplicate (%d,%d)->(%d,%d)", [a.x_index, b.x_index, a.src, a.outlet, a.dst, a.inlet])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Signal-to-control connection
|
||||||
|
violation contains msg if {
|
||||||
|
some rec in input.records
|
||||||
|
rec.type == "connect"
|
||||||
|
src_rec := record_by_idx(rec.src)
|
||||||
|
dst_rec := record_by_idx(rec.dst)
|
||||||
|
src_rec.type == "obj"
|
||||||
|
dst_rec.type == "obj"
|
||||||
|
is_signal(src_rec.name)
|
||||||
|
not is_signal(dst_rec.name)
|
||||||
|
msg := sprintf("connect[%d]: signal (~) outlet %d (%s) to control inlet %d (%s)", [rec.x_index, rec.src, src_rec.name, rec.dst, dst_rec.name])
|
||||||
|
}
|
||||||
|
|
||||||
|
# No canvas definition
|
||||||
|
violation contains msg if {
|
||||||
|
not has_canvas
|
||||||
|
msg := "No #N canvas definition found"
|
||||||
|
}
|
||||||
|
|
||||||
|
has_canvas if {
|
||||||
|
some rec in input.records
|
||||||
|
rec.type == "canvas"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Outlet exceeds known count (skip -1 = variable)
|
||||||
|
violation contains msg if {
|
||||||
|
some rec in input.records
|
||||||
|
rec.type == "connect"
|
||||||
|
src_rec := record_by_idx(rec.src)
|
||||||
|
src_rec.type == "obj"
|
||||||
|
known_outlets[src_rec.name]
|
||||||
|
known_outlets[src_rec.name] != -1
|
||||||
|
rec.outlet >= known_outlets[src_rec.name]
|
||||||
|
msg := sprintf("connect[%d]: obj %d (%s) has %d outlets, referenced outlet %d", [rec.x_index, rec.src, src_rec.name, known_outlets[src_rec.name], rec.outlet])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Inlet exceeds known count (skip -1 = variable)
|
||||||
|
violation contains msg if {
|
||||||
|
some rec in input.records
|
||||||
|
rec.type == "connect"
|
||||||
|
dst_rec := record_by_idx(rec.dst)
|
||||||
|
dst_rec.type == "obj"
|
||||||
|
known_inlets[dst_rec.name]
|
||||||
|
known_inlets[dst_rec.name] != -1
|
||||||
|
rec.inlet >= known_inlets[dst_rec.name]
|
||||||
|
msg := sprintf("connect[%d]: obj %d (%s) has %d inlets, referenced inlet %d", [rec.x_index, rec.dst, dst_rec.name, known_inlets[dst_rec.name], rec.inlet])
|
||||||
|
}
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════
|
||||||
|
# KNOWLEDGE BASE: inlet/outlet counts by object name
|
||||||
|
# ═══════════════════════════════════════════════════════
|
||||||
|
# -1 means variable/unknown
|
||||||
|
|
||||||
|
known_inlets := {
|
||||||
|
# Audio signal objects
|
||||||
|
"osc~": 1, "phasor~": 1, "saw~": 1, "noise~": 0,
|
||||||
|
"*~": 2, "+~": 2, "-~": 2, "/~": 2, "pow~": 2,
|
||||||
|
"lop~": 2, "hip~": 2, "bp~": 3, "vcf~": 3,
|
||||||
|
"cos~": 1, "wrap~": 1, "abs~": 1,
|
||||||
|
"clip~": 3, "env~": 4,
|
||||||
|
"dbtopower~": 1, "powertodb~": 1,
|
||||||
|
"mtof": 1, "ftom": 1,
|
||||||
|
"dac~": 2, "adc~": 0,
|
||||||
|
"sig~": 1, "snapshot~": 1,
|
||||||
|
"line~": 2, "vline~": 2,
|
||||||
|
"tabread4~": 2, "tabwrite~": 2, "tabread~": 2,
|
||||||
|
"send~": 1, "receive~": 1,
|
||||||
|
"delwrite~": 2, "delread~": 1, "vd~": 2,
|
||||||
|
"rfft~": 1, "rifft~": 1,
|
||||||
|
"inlet~": 0, "outlet~": 0,
|
||||||
|
"block~": 0, "switch~": 1,
|
||||||
|
# MIDI
|
||||||
|
"notein": 0, "noteout": 1,
|
||||||
|
"ctlin": 0, "ctlout": 1,
|
||||||
|
"midiin": 0, "midiout": 1,
|
||||||
|
# Data flow
|
||||||
|
"pack": -1, "unpack": 1,
|
||||||
|
"makenote": 2, "stripnote": 2,
|
||||||
|
"route": 1, "select": 1, "spigot": 2,
|
||||||
|
"trigger": 1, "t": 1,
|
||||||
|
"float": 1, "f": 1, "symbol": 1, "int": 1,
|
||||||
|
# Math
|
||||||
|
"+": 2, "-": 2, "*": 2, "/": 2, "pow": 2,
|
||||||
|
"div": 2, "mod": 2,
|
||||||
|
"max": 2, "min": 2,
|
||||||
|
"moses": 2, "until": 1,
|
||||||
|
"timer": 1, "random": 1,
|
||||||
|
# Data
|
||||||
|
"array": 1, "tabread": 1, "tabwrite": 2,
|
||||||
|
"text": 1, "textfile": 1,
|
||||||
|
"value": 0, "v": 0,
|
||||||
|
"soundfiler": 1,
|
||||||
|
# GUI
|
||||||
|
"openpanel": 0, "savepanel": 0,
|
||||||
|
"cnv": 0, "hsl": 1, "vsl": 1, "hradio": 1, "vradio": 1,
|
||||||
|
"bng": 0, "tgl": 0, "nbx": 1, "vu": 0,
|
||||||
|
"print": 1,
|
||||||
|
# Time
|
||||||
|
"pipe": 1, "delay": 1, "del": 1,
|
||||||
|
"metro": 1, "tempo": 1,
|
||||||
|
"line": 2, "poly": 2,
|
||||||
|
# Send/receive
|
||||||
|
"s": 1, "send": 1, "r": 0, "receive": 0,
|
||||||
|
"netsend": 2, "netreceive": 1,
|
||||||
|
# Misc
|
||||||
|
"loadbang": 0, "loadb": 0, "bang": 0,
|
||||||
|
"inlet": 0, "outlet": 0,
|
||||||
|
"expr": 1, "clone": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Conftest checks deny, warn, and violation (singular)
|
||||||
|
|
||||||
|
known_outlets := {
|
||||||
|
# Audio signal
|
||||||
|
"osc~": 1, "phasor~": 1, "saw~": 1, "noise~": 1,
|
||||||
|
"*~": 1, "+~": 1, "-~": 1, "/~": 1, "pow~": 1,
|
||||||
|
"lop~": 1, "hip~": 1, "bp~": 1, "vcf~": 1,
|
||||||
|
"cos~": 1, "wrap~": 1, "abs~": 1,
|
||||||
|
"clip~": 1, "env~": 1,
|
||||||
|
"dbtopower~": 1, "powertodb~": 1,
|
||||||
|
"mtof": 1, "ftom": 1,
|
||||||
|
"dac~": 0, "adc~": 2,
|
||||||
|
"sig~": 1, "snapshot~": 1,
|
||||||
|
"line~": 1, "vline~": 1,
|
||||||
|
"tabread4~": 1, "tabwrite~": 0, "tabread~": 1,
|
||||||
|
"send~": 0, "receive~": 1,
|
||||||
|
"delwrite~": 0, "delread~": 1, "vd~": 1,
|
||||||
|
"rfft~": 2, "rifft~": 1,
|
||||||
|
"inlet~": 1, "outlet~": 0,
|
||||||
|
"block~": 0, "switch~": 0,
|
||||||
|
# MIDI
|
||||||
|
"notein": 2, "noteout": 0,
|
||||||
|
"ctlin": 1, "ctlout": 0,
|
||||||
|
"midiin": 1, "midiout": 0,
|
||||||
|
# Data flow
|
||||||
|
"pack": 1, "unpack": -1,
|
||||||
|
"makenote": 0, "stripnote": 2,
|
||||||
|
"route": -1, "select": 1, "spigot": 2,
|
||||||
|
"trigger": -1, "t": -1,
|
||||||
|
"float": 1, "f": 1, "symbol": 1, "int": 1,
|
||||||
|
# Math
|
||||||
|
"+": 1, "-": 1, "*": 1, "/": 1,
|
||||||
|
"div": 1, "mod": 1, "pow": 1,
|
||||||
|
"max": 1, "min": 1,
|
||||||
|
"moses": 2, "until": 1,
|
||||||
|
"timer": 2, "random": 1,
|
||||||
|
# Data
|
||||||
|
"array": 2, "tabread": 1, "tabwrite": 0,
|
||||||
|
"text": 2, "textfile": 2,
|
||||||
|
"value": 1, "v": 1,
|
||||||
|
"soundfiler": 0,
|
||||||
|
# GUI
|
||||||
|
"openpanel": 1, "savepanel": 1,
|
||||||
|
"cnv": 1,
|
||||||
|
"hsl": 1, "vsl": 1, "hradio": 1, "vradio": 1,
|
||||||
|
"bng": 1, "tgl": 1, "nbx": 2, "vu": 0,
|
||||||
|
"print": 0,
|
||||||
|
# Time
|
||||||
|
"pipe": 1, "delay": 1, "del": 1,
|
||||||
|
"metro": 1, "tempo": 2,
|
||||||
|
"line": 1, "poly": 1,
|
||||||
|
# Send/receive
|
||||||
|
"s": 0, "send": 0, "r": 1, "receive": 1,
|
||||||
|
"netsend": 0, "netreceive": 1,
|
||||||
|
# Misc
|
||||||
|
"loadbang": 1, "loadb": 1, "bang": 1,
|
||||||
|
"inlet": 0, "outlet": 0,
|
||||||
|
"expr": 1, "clone": -1,
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Parse a .pd file into JSON for Conftest/Rego validation."""
|
||||||
|
import json, sys, re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def parse_pd(filepath):
|
||||||
|
with open(filepath) as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
|
lines = text.split("\n")
|
||||||
|
records = []
|
||||||
|
x_index = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for i, raw in enumerate(lines):
|
||||||
|
line = raw.strip()
|
||||||
|
if not line or line == ";":
|
||||||
|
continue
|
||||||
|
if not line.startswith("#"):
|
||||||
|
errors.append(f"line {i+1}: expected # prefix")
|
||||||
|
continue
|
||||||
|
|
||||||
|
raw = line[1:].strip()
|
||||||
|
if not raw.endswith(";"):
|
||||||
|
errors.append(f"line {i+1}: missing terminating semicolon")
|
||||||
|
continue
|
||||||
|
body = raw[:-1].strip()
|
||||||
|
|
||||||
|
if line.startswith("#N"):
|
||||||
|
nbody = body[1:].strip() # remove 'N'
|
||||||
|
parts = nbody.split(None, 1)
|
||||||
|
if parts[0] == "canvas":
|
||||||
|
tokens = parts[1].split()
|
||||||
|
rec = {
|
||||||
|
"type": "canvas",
|
||||||
|
"x": safe_int(tokens[0]),
|
||||||
|
"y": safe_int(tokens[1]),
|
||||||
|
"width": safe_int(tokens[2]),
|
||||||
|
"height": safe_int(tokens[3]),
|
||||||
|
"name": tokens[4] if len(tokens) > 4 else "",
|
||||||
|
}
|
||||||
|
records.append(rec)
|
||||||
|
else:
|
||||||
|
records.append({"type": "new", "data": rest})
|
||||||
|
|
||||||
|
elif line.startswith("#A"):
|
||||||
|
values = [safe_float(v) for v in rest.split()]
|
||||||
|
records.append({"type": "array_data", "values": values})
|
||||||
|
|
||||||
|
elif line.startswith("#X connect"):
|
||||||
|
xbody = body[1:].strip()
|
||||||
|
elems = xbody.split()
|
||||||
|
if len(elems) >= 5:
|
||||||
|
rec = {
|
||||||
|
"type": "connect",
|
||||||
|
"x_index": x_index,
|
||||||
|
"src": safe_int(elems[1]),
|
||||||
|
"outlet": safe_int(elems[2]),
|
||||||
|
"dst": safe_int(elems[3]),
|
||||||
|
"inlet": safe_int(elems[4]),
|
||||||
|
}
|
||||||
|
records.append(rec)
|
||||||
|
# Connect records do NOT increment Pd's object index
|
||||||
|
|
||||||
|
elif line.startswith("#X obj"):
|
||||||
|
xbody = body[1:].strip()
|
||||||
|
# xbody = "obj 20 20 notein"
|
||||||
|
elem, x_str, y_str, rest = xbody.split(None, 3)
|
||||||
|
obj_parts = rest.split()
|
||||||
|
rec = {
|
||||||
|
"type": "obj",
|
||||||
|
"x_index": x_index,
|
||||||
|
"x": safe_int(x_str),
|
||||||
|
"y": safe_int(y_str),
|
||||||
|
"name": obj_parts[0] if obj_parts else "",
|
||||||
|
"args": obj_parts[1:],
|
||||||
|
}
|
||||||
|
records.append(rec)
|
||||||
|
x_index += 1
|
||||||
|
|
||||||
|
elif line.startswith("#X msg"):
|
||||||
|
xbody = body[1:].strip()
|
||||||
|
elem, x_str, y_str, content = xbody.split(None, 3)
|
||||||
|
records.append({
|
||||||
|
"type": "msg",
|
||||||
|
"x_index": x_index,
|
||||||
|
"x": safe_int(x_str),
|
||||||
|
"y": safe_int(y_str),
|
||||||
|
"content": content,
|
||||||
|
})
|
||||||
|
x_index += 1
|
||||||
|
|
||||||
|
elif line.startswith("#X floatatom"):
|
||||||
|
xbody = body[1:].strip()
|
||||||
|
elems = xbody.split()
|
||||||
|
rec = {"type": "floatatom", "x_index": x_index, "x": safe_int(elems[1]), "y": safe_int(elems[2])}
|
||||||
|
if len(elems) > 3: rec["width"] = safe_int(elems[3])
|
||||||
|
if len(elems) > 4: rec["min"] = safe_float(elems[4])
|
||||||
|
if len(elems) > 5: rec["max"] = safe_float(elems[5])
|
||||||
|
records.append(rec)
|
||||||
|
x_index += 1
|
||||||
|
|
||||||
|
elif line.startswith("#X symbolatom"):
|
||||||
|
records.append({"type": "symbolatom", "x_index": x_index})
|
||||||
|
x_index += 1
|
||||||
|
|
||||||
|
elif line.startswith("#X text"):
|
||||||
|
xbody = body[1:].strip()
|
||||||
|
records.append({"type": "text", "x_index": x_index, "content": xbody})
|
||||||
|
x_index += 1
|
||||||
|
|
||||||
|
elif line.startswith("#X restore"):
|
||||||
|
xbody = body[1:].strip()
|
||||||
|
records.append({"type": "restore", "x_index": x_index, "data": xbody})
|
||||||
|
x_index += 1
|
||||||
|
|
||||||
|
elif line.startswith("#X coords"):
|
||||||
|
xbody = body[1:].strip()
|
||||||
|
records.append({"type": "coords", "x_index": x_index, "data": xbody})
|
||||||
|
x_index += 1
|
||||||
|
|
||||||
|
elif line.startswith("#X array"):
|
||||||
|
xbody = body[1:].strip()
|
||||||
|
records.append({"type": "array", "x_index": x_index, "data": xbody})
|
||||||
|
x_index += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
errors.append(f"line {i+1}: unknown record type: {line[:60]}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"file": str(filepath),
|
||||||
|
"x_record_count": x_index,
|
||||||
|
"records": records,
|
||||||
|
"parse_errors": errors,
|
||||||
|
}
|
||||||
|
|
||||||
|
def safe_int(s):
|
||||||
|
try:
|
||||||
|
# Handle Pd special values like "-" for empty
|
||||||
|
if s == "-":
|
||||||
|
return 0
|
||||||
|
return int(s)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def safe_float(s):
|
||||||
|
try:
|
||||||
|
if s == "-":
|
||||||
|
return 0.0
|
||||||
|
return float(s)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: pd2json.py <file.pd> [file2.pd ...]")
|
||||||
|
sys.exit(1)
|
||||||
|
results = []
|
||||||
|
for arg in sys.argv[1:]:
|
||||||
|
results.append(parse_pd(arg))
|
||||||
|
print(json.dumps(results if len(results) > 1 else results[0], indent=2))
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#N canvas 0 0 400 300 10;
|
||||||
|
#X obj 20 20 notein;
|
||||||
|
#X obj 80 20 osc~;
|
||||||
|
#X connect 0 0 1 0;
|
||||||
|
#X connect 0 1 1 1;
|
||||||
|
#X connect 0 0 1 0;
|
||||||
|
#X connect 99 0 1 0;
|
||||||
|
#X connect 0 0 99 0;
|
||||||
|
#X connect 0 0 0 0;
|
||||||
|
#X connect 0 -1 1 0;
|
||||||
|
#X obj 20 100 dac~;
|
||||||
|
#X connect 1 99 3 0;
|
||||||
|
#X connect 1 0 3 99;
|
||||||
|
#X connect 1 0 3 0;
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Validate .pd files using Conftest/OPA Rego policies."""
|
||||||
|
import json, sys, subprocess, tempfile, os, glob
|
||||||
|
from pathlib import Path
|
||||||
|
from pd2json import parse_pd
|
||||||
|
|
||||||
|
def main():
|
||||||
|
policy_dir = Path(__file__).parent
|
||||||
|
|
||||||
|
# Find conftest/opa (check mise install dirs first)
|
||||||
|
mise_dir = os.path.expanduser("~/.local/share/mise/installs")
|
||||||
|
for tool in ["conftest", "opa"]:
|
||||||
|
for ver_dir in sorted(glob.glob(os.path.join(mise_dir, tool, "*")), reverse=True):
|
||||||
|
for p in [os.path.join(ver_dir, tool), os.path.join(ver_dir, "bin", tool)]:
|
||||||
|
if os.path.isfile(p):
|
||||||
|
os.environ["PATH"] = os.path.dirname(p) + ":" + os.environ.get("PATH", "")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
|
||||||
|
runner = None
|
||||||
|
for cmd in ["conftest", "opa"]:
|
||||||
|
try:
|
||||||
|
subprocess.run([cmd, "--version"], capture_output=True, timeout=5)
|
||||||
|
runner = cmd
|
||||||
|
break
|
||||||
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not runner:
|
||||||
|
print("ERROR: neither 'conftest' nor 'opa' found in PATH.")
|
||||||
|
print("Install: pip install conftest or https://www.conftest.dev/")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
pd_files = sys.argv[1:] or list(Path(".").glob("*.pd"))
|
||||||
|
if not pd_files:
|
||||||
|
print("Usage: validate.py <file.pd> [file2.pd ...]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
all_passed = True
|
||||||
|
for pf in pd_files:
|
||||||
|
pf = Path(pf)
|
||||||
|
data = parse_pd(pf)
|
||||||
|
|
||||||
|
if runner == "conftest":
|
||||||
|
result = run_conftest(data, policy_dir)
|
||||||
|
else:
|
||||||
|
result = run_opa(data, policy_dir)
|
||||||
|
|
||||||
|
if result["passed"]:
|
||||||
|
print(f" ✓ {pf.name} — all checks passed")
|
||||||
|
else:
|
||||||
|
all_passed = False
|
||||||
|
print(f" ✗ {pf.name} — {len(result['violations'])} violation(s):")
|
||||||
|
for v in result["violations"]:
|
||||||
|
print(f" • {v}")
|
||||||
|
|
||||||
|
sys.exit(0 if all_passed else 1)
|
||||||
|
|
||||||
|
|
||||||
|
def run_conftest(data, policy_dir):
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False) as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
json_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["conftest", "test", "--policy", str(policy_dir), json_path],
|
||||||
|
capture_output=True, text=True, timeout=30
|
||||||
|
)
|
||||||
|
out = result.stdout + result.stderr
|
||||||
|
violations = []
|
||||||
|
for l in out.split("\n"):
|
||||||
|
if "FAIL" in l or "failed" in l.lower():
|
||||||
|
violations.append(l.strip())
|
||||||
|
# Also parse structured output if json
|
||||||
|
if result.stdout and not violations:
|
||||||
|
try:
|
||||||
|
import json as j
|
||||||
|
report = j.loads(result.stdout)
|
||||||
|
for fname, checks in report.get("failures", {}).items():
|
||||||
|
for c in checks:
|
||||||
|
violations.append(c)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {"passed": result.returncode == 0, "violations": violations}
|
||||||
|
finally:
|
||||||
|
os.unlink(json_path)
|
||||||
|
|
||||||
|
|
||||||
|
def run_opa(data, policy_dir):
|
||||||
|
pkg = "main"
|
||||||
|
query = "data.main.violations"
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False) as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
json_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["opa", "eval", "-d", str(policy_dir), "-d", json_path, "--format", "json", query],
|
||||||
|
capture_output=True, text=True, timeout=30
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return {"passed": False, "violations": [result.stderr.strip()]}
|
||||||
|
|
||||||
|
out = json.loads(result.stdout)
|
||||||
|
violations = []
|
||||||
|
for expr in out.get("result", []):
|
||||||
|
for binding in expr.get("expressions", []):
|
||||||
|
vals = binding.get("value", [])
|
||||||
|
if isinstance(vals, list):
|
||||||
|
violations.extend(vals)
|
||||||
|
return {"passed": len(violations) == 0, "violations": violations}
|
||||||
|
finally:
|
||||||
|
os.unlink(json_path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#N canvas 0 0 900 500 10;
|
||||||
|
#X obj 5 5 loadbang;
|
||||||
|
#X msg 5 25 pd dsp 1;
|
||||||
|
#X connect 0 0 1 0;
|
||||||
|
|
||||||
|
#X text 20 10 -- NOTE BUTTONS (click to play) --;
|
||||||
|
|
||||||
|
#X text 22 50 C3;
|
||||||
|
#X obj 20 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
|
||||||
|
#X msg 20 100 48 100;
|
||||||
|
#X obj 20 130 noteout;
|
||||||
|
#X connect 4 0 5 0;
|
||||||
|
#X connect 5 0 6 0;
|
||||||
|
|
||||||
|
#X text 62 50 D3;
|
||||||
|
#X obj 60 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
|
||||||
|
#X msg 60 100 50 100;
|
||||||
|
#X obj 60 130 noteout;
|
||||||
|
#X connect 8 0 9 0;
|
||||||
|
#X connect 9 0 10 0;
|
||||||
|
|
||||||
|
#X text 102 50 E3;
|
||||||
|
#X obj 100 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
|
||||||
|
#X msg 100 100 52 100;
|
||||||
|
#X obj 100 130 noteout;
|
||||||
|
#X connect 12 0 13 0;
|
||||||
|
#X connect 13 0 14 0;
|
||||||
|
|
||||||
|
#X text 142 50 F3;
|
||||||
|
#X obj 140 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
|
||||||
|
#X msg 140 100 53 100;
|
||||||
|
#X obj 140 130 noteout;
|
||||||
|
#X connect 16 0 17 0;
|
||||||
|
#X connect 17 0 18 0;
|
||||||
|
|
||||||
|
#X text 182 50 G3;
|
||||||
|
#X obj 180 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
|
||||||
|
#X msg 180 100 55 100;
|
||||||
|
#X obj 180 130 noteout;
|
||||||
|
#X connect 20 0 21 0;
|
||||||
|
#X connect 21 0 22 0;
|
||||||
|
|
||||||
|
#X text 222 50 A3;
|
||||||
|
#X obj 220 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
|
||||||
|
#X msg 220 100 57 100;
|
||||||
|
#X obj 220 130 noteout;
|
||||||
|
#X connect 24 0 25 0;
|
||||||
|
#X connect 25 0 26 0;
|
||||||
|
|
||||||
|
#X text 262 50 B3;
|
||||||
|
#X obj 260 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
|
||||||
|
#X msg 260 100 59 100;
|
||||||
|
#X obj 260 130 noteout;
|
||||||
|
#X connect 28 0 29 0;
|
||||||
|
#X connect 29 0 30 0;
|
||||||
|
|
||||||
|
#X text 302 50 C4;
|
||||||
|
#X obj 300 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
|
||||||
|
#X msg 300 100 60 100;
|
||||||
|
#X obj 300 130 noteout;
|
||||||
|
#X connect 32 0 33 0;
|
||||||
|
#X connect 33 0 34 0;
|
||||||
|
|
||||||
|
#X text 20 210 -- CONTROLS --;
|
||||||
|
#X obj 30 230 hsl 128 15 0 127 64 0 0 0 empty Filter-CC -2 -8 0 10 -262144 -1 -1 7200 1;
|
||||||
|
#X obj 30 270 ctlout 74;
|
||||||
|
#X connect 36 0 37 0;
|
||||||
|
|
||||||
|
#X text 20 310 -- AUDIO MONITOR --;
|
||||||
|
#X obj 30 330 adc~ 1;
|
||||||
|
#X obj 30 370 *~;
|
||||||
|
#X obj 30 410 dac~ 1 2;
|
||||||
|
#X obj 140 330 hsl 128 15 0 100 50 0 0 0 empty Monitor-Volume -2 -8 0 10 -262144 -1 -1 7200 1;
|
||||||
|
#X obj 140 370 / 100;
|
||||||
|
#X connect 39 0 40 0;
|
||||||
|
#X connect 40 0 41 0;
|
||||||
|
#X connect 40 0 41 1;
|
||||||
|
#X connect 42 0 43 0;
|
||||||
|
#X connect 43 0 40 1;
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#N canvas 0 0 900 550 10;
|
||||||
|
#X obj 20 20 notein;
|
||||||
|
#X obj 80 50 pack 0 0;
|
||||||
|
#X obj 80 80 unpack 0 0;
|
||||||
|
#X obj 20 110 s midi-pitch;
|
||||||
|
#X obj 80 110 s midi-vel;
|
||||||
|
#X obj 400 20 netreceive -u 8081;
|
||||||
|
#X obj 400 60 route midi;
|
||||||
|
#X obj 400 100 route 144 128;
|
||||||
|
#X obj 400 140 unpack 0 0;
|
||||||
|
#X obj 480 140 unpack 0 0;
|
||||||
|
#X obj 400 180 s midi-pitch;
|
||||||
|
#X obj 480 180 s midi-vel;
|
||||||
|
#X obj 20 160 r midi-vel;
|
||||||
|
#X obj 20 190 r midi-pitch;
|
||||||
|
#X obj 20 220 stripnote;
|
||||||
|
#X obj 20 260 mtof;
|
||||||
|
#X obj 20 290 env~ 50 200 0.7 400;
|
||||||
|
#X obj 140 260 osc~;
|
||||||
|
#X obj 140 290 *~ 0.3;
|
||||||
|
#X obj 220 260 osc~;
|
||||||
|
#X obj 220 290 *~ 0.3;
|
||||||
|
#X obj 180 330 +~;
|
||||||
|
#X obj 180 360 *~ 0.5;
|
||||||
|
#X obj 180 400 lop~;
|
||||||
|
#X obj 180 460 *~;
|
||||||
|
#X obj 320 460 *~;
|
||||||
|
#X obj 250 530 dac~;
|
||||||
|
#X obj 350 330 hsl 128 15 100 18000 10000 0 0 0 empty Cutoff -2 -8 0 10 -262144 -1 -1 7200 1;
|
||||||
|
#X obj 350 430 hsl 128 15 0 100 40 0 0 0 empty Volume -2 -8 0 10 -262144 -1 -1 7200 1;
|
||||||
|
#X obj 350 460 / 100;
|
||||||
|
#X connect 0 0 1 0;
|
||||||
|
#X connect 0 1 1 1;
|
||||||
|
#X connect 1 0 2 0;
|
||||||
|
#X connect 2 0 3 0;
|
||||||
|
#X connect 2 1 4 0;
|
||||||
|
#X connect 5 0 6 0;
|
||||||
|
#X connect 6 0 7 0;
|
||||||
|
#X connect 7 0 8 0;
|
||||||
|
#X connect 7 1 9 0;
|
||||||
|
#X connect 8 0 10 0;
|
||||||
|
#X connect 8 1 11 0;
|
||||||
|
#X connect 9 0 10 0;
|
||||||
|
#X connect 9 1 11 0;
|
||||||
|
#X connect 12 0 14 1;
|
||||||
|
#X connect 13 0 14 0;
|
||||||
|
#X connect 14 0 15 0;
|
||||||
|
#X connect 15 0 17 0;
|
||||||
|
#X connect 15 0 19 0;
|
||||||
|
#X connect 17 0 18 0;
|
||||||
|
#X connect 19 0 20 0;
|
||||||
|
#X connect 18 0 21 0;
|
||||||
|
#X connect 20 0 21 1;
|
||||||
|
#X connect 21 0 22 0;
|
||||||
|
#X connect 22 0 23 0;
|
||||||
|
#X connect 23 0 24 0;
|
||||||
|
#X connect 14 0 16 0;
|
||||||
|
#X connect 16 0 24 1;
|
||||||
|
#X connect 24 0 25 0;
|
||||||
|
#X connect 27 0 23 1;
|
||||||
|
#X connect 28 0 29 0;
|
||||||
|
#X connect 29 0 25 1;
|
||||||
|
#X connect 25 0 26 0;
|
||||||
|
#X connect 25 0 26 1;
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# diag.sh — run on the board for diagnostics
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "--- System ---"
|
||||||
|
echo " Kernel: $(uname -r)"
|
||||||
|
echo " Uptime: $(uptime -p | sed 's/up //')"
|
||||||
|
echo " Load: $(cat /proc/loadavg | cut -d' ' -f1-3)"
|
||||||
|
echo " Memory: $(free -h | awk '/^Mem:/{print $3 "/" $2}')"
|
||||||
|
echo " Disk: $(df -h / | awk 'NR==2{print $3 "/" $2 " (" $5 ")"}')"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- USB Gadget (configfs) ---"
|
||||||
|
UDC=$(cat /sys/kernel/config/usb_gadget/g1/UDC 2>/dev/null || echo '(not bound)')
|
||||||
|
echo " UDC bound: $UDC"
|
||||||
|
for f in /sys/kernel/config/usb_gadget/g1/functions/*/; do
|
||||||
|
echo " Function: $(basename "$f")"
|
||||||
|
done
|
||||||
|
for c in /sys/kernel/config/usb_gadget/g1/configs/*/; do
|
||||||
|
cfg=$(basename "$c")
|
||||||
|
for l in "$c"/*.*/; do
|
||||||
|
[ -L "$l" ] && echo " linked: $(basename "$l") -> config $cfg"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- UDC Hardware ---"
|
||||||
|
for u in /sys/class/udc/*/; do
|
||||||
|
name=$(basename "$u")
|
||||||
|
echo " Controller: $name"
|
||||||
|
echo " state: $(cat "$u/state" 2>/dev/null)"
|
||||||
|
echo " speed: $(cat "$u/current_speed" 2>/dev/null)"
|
||||||
|
echo " max_speed: $(cat "$u/maximum_speed" 2>/dev/null)"
|
||||||
|
done
|
||||||
|
role=$(cat /sys/class/usb_role/*/role 2>/dev/null || echo '(none)')
|
||||||
|
echo " USB role: $role"
|
||||||
|
echo " Role sw: $(ls /sys/class/usb_role/ 2>/dev/null | tr '\n' ' ')"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- USB Audio Params ---"
|
||||||
|
for p in /sys/kernel/config/usb_gadget/g1/functions/uac1.gs0/p_* /sys/kernel/config/usb_gadget/g1/functions/uac1.gs0/c_*; do
|
||||||
|
name=$(basename "$p")
|
||||||
|
echo " $name: $(cat "$p" 2>/dev/null)"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- USB MIDI Params ---"
|
||||||
|
for p in in_ports out_ports index; do
|
||||||
|
val=$(cat /sys/kernel/config/usb_gadget/g1/functions/midi.gs0/$p 2>/dev/null)
|
||||||
|
echo " $p: $val"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Modules ---"
|
||||||
|
lsmod | awk '$1 ~ /^(usb_f_|u_audio|libcomposite|snd_usb|snd_rawmidi|snd_soc|dwc3)/ {print " " $0}'
|
||||||
|
for m in usb_f_uac1 usb_f_midi dwc3-qcom-legacy; do
|
||||||
|
if lsmod 2>/dev/null | grep -q "^$m "; then
|
||||||
|
echo " $m: LOADED"
|
||||||
|
else
|
||||||
|
echo " $m: not loaded"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- ALSA Cards ---"
|
||||||
|
cat /proc/asound/cards
|
||||||
|
echo ""
|
||||||
|
aplay -l 2>/dev/null | awk '{print " " $0}'
|
||||||
|
echo ""
|
||||||
|
arecord -l 2>/dev/null | awk '{print " " $0}'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- ALSA MIDI ---"
|
||||||
|
echo " Raw MIDI devices:"
|
||||||
|
ls /dev/snd/midi* 2>/dev/null | awk '{print " " $0}' || echo " (none)"
|
||||||
|
echo " ALSA sequencer:"
|
||||||
|
aconnect -lio 2>/dev/null | awk '{print " " $0}' || echo " (none)"
|
||||||
|
echo " amidi ports:"
|
||||||
|
amidi -l 2>/dev/null | awk '{print " " $0}' || echo " (none)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Pd Status ---"
|
||||||
|
pd_pid=$(pgrep -x pd 2>/dev/null || echo "")
|
||||||
|
if [ -n "$pd_pid" ]; then
|
||||||
|
echo " PID: $pd_pid"
|
||||||
|
echo " Args: $(tr '\0' ' ' < /proc/$pd_pid/cmdline 2>/dev/null)"
|
||||||
|
echo " Uptime: $(ps -o etime= -p $pd_pid 2>/dev/null | xargs)"
|
||||||
|
echo " CPU: $(ps -o %cpu= -p $pd_pid 2>/dev/null | xargs)%"
|
||||||
|
echo " MEM: $(ps -o %mem= -p $pd_pid 2>/dev/null | xargs)%"
|
||||||
|
else
|
||||||
|
echo " Pd: not running"
|
||||||
|
fi
|
||||||
|
for svc in pd-synth-auto pd-synth pd-synth-i2s usb-role-detect usb-gadget-audio; do
|
||||||
|
state=$(systemctl is-active "$svc" 2>/dev/null || echo '(not found)')
|
||||||
|
enabled=$(systemctl is-enabled "$svc" 2>/dev/null || echo '(not found)')
|
||||||
|
echo " $svc: $state (enabled: $enabled)"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- dmesg (USB/gadget errors) ---"
|
||||||
|
dmesg -l err,warn 2>/dev/null | grep -iE 'usb|gadget|uac1|midi|pd|audio|dwc3' | tail -20 | awk '{print " " $0}'
|
||||||
|
echo ""
|
||||||
|
dmesg 2>/dev/null | grep -i 'failed to start\|incorrect sample\|couldn.t find.*UDC\|probe.*failed' | tail -5 | awk '{print " " $0}'
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate synth.pd programmatically."""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
OUTPUT = SCRIPT_DIR.parent / "pd" / "synth.pd"
|
||||||
|
|
||||||
|
def connect(c, src, sout, dst, din):
|
||||||
|
c.append((src, sout, dst, din))
|
||||||
|
|
||||||
|
def obj(o, x, y, *args):
|
||||||
|
idx = len(o)
|
||||||
|
o.append((x, y, "obj", *args))
|
||||||
|
return idx
|
||||||
|
|
||||||
|
def gen():
|
||||||
|
o = [] # objects
|
||||||
|
c = [] # connections
|
||||||
|
|
||||||
|
def Obj(x, y, *args):
|
||||||
|
return obj(o, x, y, *args)
|
||||||
|
|
||||||
|
# ── ALSA MIDI input (gadget mode) ──
|
||||||
|
notein = Obj(20, 20, "notein")
|
||||||
|
pack_note = Obj(80, 50, "pack 0 0")
|
||||||
|
unpack_note = Obj(80, 80, "unpack 0 0")
|
||||||
|
s_pitch_n = Obj(20, 110, "s midi-pitch")
|
||||||
|
s_vel_n = Obj(80, 110, "s midi-vel")
|
||||||
|
connect(c, notein, 0, pack_note, 0)
|
||||||
|
connect(c, notein, 1, pack_note, 1)
|
||||||
|
connect(c, pack_note, 0, unpack_note, 0)
|
||||||
|
connect(c, unpack_note, 0, s_pitch_n, 0)
|
||||||
|
connect(c, unpack_note, 1, s_vel_n, 0)
|
||||||
|
|
||||||
|
# ── UDP MIDI input (host mode via midi-bridge) ──
|
||||||
|
net = Obj(400, 20, "netreceive -u 8081")
|
||||||
|
route_midi = Obj(400, 60, "route midi")
|
||||||
|
route_note = Obj(400, 100, "route 144 128")
|
||||||
|
unpack2_on = Obj(400, 140, "unpack 0 0")
|
||||||
|
unpack2_off = Obj(480, 140, "unpack 0 0")
|
||||||
|
s_pitch_u = Obj(400, 180, "s midi-pitch")
|
||||||
|
s_vel_u = Obj(480, 180, "s midi-vel")
|
||||||
|
connect(c, net, 0, route_midi, 0)
|
||||||
|
connect(c, route_midi, 0, route_note, 0)
|
||||||
|
connect(c, route_note, 0, unpack2_on, 0)
|
||||||
|
connect(c, route_note, 1, unpack2_off, 0)
|
||||||
|
connect(c, unpack2_on, 0, s_pitch_u, 0)
|
||||||
|
connect(c, unpack2_on, 1, s_vel_u, 0)
|
||||||
|
connect(c, unpack2_off, 0, s_pitch_u, 0)
|
||||||
|
connect(c, unpack2_off, 1, s_vel_u, 0)
|
||||||
|
|
||||||
|
# ── Synth voice ──
|
||||||
|
r_vel = Obj(20, 160, "r midi-vel")
|
||||||
|
r_pitch = Obj(20, 190, "r midi-pitch")
|
||||||
|
strip = Obj(20, 220, "stripnote")
|
||||||
|
mtof = Obj(20, 260, "mtof")
|
||||||
|
env = Obj(20, 290, "env~ 50 200 0.7 400")
|
||||||
|
osc1 = Obj(140, 260, "osc~")
|
||||||
|
mul1 = Obj(140, 290, "*~ 0.3")
|
||||||
|
osc2 = Obj(220, 260, "osc~")
|
||||||
|
mul2 = Obj(220, 290, "*~ 0.3")
|
||||||
|
sum = Obj(180, 330, "+~")
|
||||||
|
scale = Obj(180, 360, "*~ 0.5")
|
||||||
|
filt = Obj(180, 400, "lop~")
|
||||||
|
amp = Obj(180, 460, "*~")
|
||||||
|
panscale = Obj(320, 460, "*~")
|
||||||
|
dac = Obj(250, 530, "dac~")
|
||||||
|
|
||||||
|
connect(c, r_vel, 0, strip, 1)
|
||||||
|
connect(c, r_pitch, 0, strip, 0)
|
||||||
|
connect(c, strip, 0, mtof, 0)
|
||||||
|
connect(c, mtof, 0, osc1, 0)
|
||||||
|
connect(c, mtof, 0, osc2, 0)
|
||||||
|
connect(c, osc1, 0, mul1, 0)
|
||||||
|
connect(c, osc2, 0, mul2, 0)
|
||||||
|
connect(c, mul1, 0, sum, 0)
|
||||||
|
connect(c, mul2, 0, sum, 1)
|
||||||
|
connect(c, sum, 0, scale, 0)
|
||||||
|
connect(c, scale, 0, filt, 0)
|
||||||
|
connect(c, filt, 0, amp, 0)
|
||||||
|
connect(c, strip, 0, env, 0)
|
||||||
|
connect(c, env, 0, amp, 1)
|
||||||
|
connect(c, amp, 0, panscale, 0)
|
||||||
|
|
||||||
|
# ── Controls ──
|
||||||
|
cutoff = Obj(350, 330, "hsl 128 15 100 18000 10000 0 0 0 empty Cutoff -2 -8 0 10 -262144 -1 -1 7200 1")
|
||||||
|
volume = Obj(350, 430, "hsl 128 15 0 100 40 0 0 0 empty Volume -2 -8 0 10 -262144 -1 -1 7200 1")
|
||||||
|
div = Obj(350, 460, "/ 100")
|
||||||
|
connect(c, cutoff, 0, filt, 1)
|
||||||
|
connect(c, volume, 0, div, 0)
|
||||||
|
connect(c, div, 0, panscale, 1)
|
||||||
|
|
||||||
|
# ── Output ──
|
||||||
|
connect(c, panscale, 0, dac, 0)
|
||||||
|
connect(c, panscale, 0, dac, 1)
|
||||||
|
|
||||||
|
# ── Write ──
|
||||||
|
lines = ["#N canvas 0 0 900 550 10;"]
|
||||||
|
for x, y, t, *args in o:
|
||||||
|
joined = " ".join(str(a) for a in args)
|
||||||
|
lines.append(f"#X obj {x} {y} {joined};")
|
||||||
|
for src, sout, dst, din in c:
|
||||||
|
lines.append(f"#X connect {src} {sout} {dst} {din};")
|
||||||
|
lines.append("")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
content = gen()
|
||||||
|
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
OUTPUT.write_text(content)
|
||||||
|
print(f"Wrote {OUTPUT}")
|
||||||
|
# Print connections for debugging
|
||||||
|
for line in content.splitlines():
|
||||||
|
if line.startswith("#X connect"):
|
||||||
|
print(f" {line}")
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Listen for MIDI events via UDP and display note activity on LED matrix.
|
||||||
|
|
||||||
|
Uses RPC requests (not notifications) for reliability, with rate limiting.
|
||||||
|
"""
|
||||||
|
import socket, msgpack, time, os, re
|
||||||
|
|
||||||
|
VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082"))
|
||||||
|
ROUTER_SOCK = os.environ.get("ROUTER_SOCK", "/var/run/arduino-router.sock")
|
||||||
|
DECAY_FRAMES = int(os.environ.get("DECAY_FRAMES", "8"))
|
||||||
|
MIN_INTERVAL = 1 / 15
|
||||||
|
|
||||||
|
COLS = 13
|
||||||
|
ROWS = 8
|
||||||
|
|
||||||
|
def pack_frame(pixels):
|
||||||
|
bits = 0
|
||||||
|
for row in range(ROWS):
|
||||||
|
for col in range(COLS):
|
||||||
|
if pixels[row][col]:
|
||||||
|
bits |= 1 << (row * COLS + col)
|
||||||
|
return (bits & 0xFFFFFFFF, (bits >> 32) & 0xFFFFFFFF,
|
||||||
|
(bits >> 64) & 0xFFFFFFFF, (bits >> 96) & 0xFFFFFFFF)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sock.bind(("0.0.0.0", VIZ_PORT))
|
||||||
|
sock.setblocking(False)
|
||||||
|
|
||||||
|
router = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
router.settimeout(2)
|
||||||
|
router.connect(ROUTER_SOCK)
|
||||||
|
packer = msgpack.Packer()
|
||||||
|
|
||||||
|
active_notes = {}
|
||||||
|
last_frame = None
|
||||||
|
last_send = 0
|
||||||
|
buf = b""
|
||||||
|
msg_id = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
now = time.monotonic()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data, _ = sock.recvfrom(1024)
|
||||||
|
buf += data
|
||||||
|
except BlockingIOError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
while b";" in buf:
|
||||||
|
msg, buf = buf.split(b";", 1)
|
||||||
|
msg = msg.decode().strip()
|
||||||
|
parts = msg.split()
|
||||||
|
if len(parts) >= 3 and parts[0] == "midi":
|
||||||
|
status = int(parts[1])
|
||||||
|
note = int(parts[2])
|
||||||
|
vel = int(parts[3]) if len(parts) >= 4 else 0
|
||||||
|
msg_type = status & 0xF0
|
||||||
|
if msg_type == 0x90 and vel > 0:
|
||||||
|
active_notes[note] = DECAY_FRAMES
|
||||||
|
elif msg_type == 0x80 or (msg_type == 0x90 and vel == 0):
|
||||||
|
active_notes.pop(note, None)
|
||||||
|
|
||||||
|
if now - last_send >= MIN_INTERVAL:
|
||||||
|
pixels = [[0] * COLS for _ in range(ROWS)]
|
||||||
|
for note, decay in list(active_notes.items()):
|
||||||
|
if decay <= 0:
|
||||||
|
del active_notes[note]
|
||||||
|
continue
|
||||||
|
active_notes[note] = decay - 1
|
||||||
|
note_min, note_max = 36, 96
|
||||||
|
row = 7 - int((note - note_min) / (note_max - note_min) * 7)
|
||||||
|
row = max(0, min(7, row))
|
||||||
|
col = note % 12
|
||||||
|
pixels[row][col] = 1
|
||||||
|
|
||||||
|
frame = pack_frame(pixels)
|
||||||
|
if frame != last_frame:
|
||||||
|
msg_id = (msg_id + 1) % 65535
|
||||||
|
params = [str(v) for v in frame]
|
||||||
|
req = packer.pack([0, msg_id, "draw_frame", params])
|
||||||
|
try:
|
||||||
|
router.sendall(req)
|
||||||
|
router.settimeout(0.3)
|
||||||
|
router.recv(128)
|
||||||
|
router.settimeout(2)
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
except OSError:
|
||||||
|
router.close()
|
||||||
|
router = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
router.settimeout(2)
|
||||||
|
router.connect(ROUTER_SOCK)
|
||||||
|
last_frame = frame
|
||||||
|
last_send = now
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Bridge raw MIDI device to Pd via UDP. For host mode with MPK Mini.
|
||||||
|
|
||||||
|
Sends parsed MIDI messages as Pd-style 'midi status data1 data2;' strings
|
||||||
|
to UDP ports for Pd (8081) and LED matrix visualization (8082).
|
||||||
|
"""
|
||||||
|
import os, socket, time, select, struct
|
||||||
|
|
||||||
|
MIDI_DEV = os.environ.get("MIDI_BRIDGE_DEV", "/dev/snd/midiC0D0")
|
||||||
|
PD_HOST = os.environ.get("MIDI_BRIDGE_HOST", "127.0.0.1")
|
||||||
|
PD_PORT = int(os.environ.get("MIDI_BRIDGE_PORT", "8081"))
|
||||||
|
VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082"))
|
||||||
|
|
||||||
|
def midi_msg(status, data1, data2=None):
|
||||||
|
if data2 is not None:
|
||||||
|
return f"midi {status} {data1} {data2};"
|
||||||
|
return f"midi {status} {data1};"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
while not os.path.exists(MIDI_DEV):
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
fd = os.open(MIDI_DEV, os.O_RDWR | os.O_NONBLOCK)
|
||||||
|
os.set_blocking(fd, False)
|
||||||
|
poll = select.poll()
|
||||||
|
poll.register(fd, select.POLLIN)
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
|
||||||
|
running = True
|
||||||
|
buf = bytearray()
|
||||||
|
while running:
|
||||||
|
try:
|
||||||
|
events = poll.poll(500)
|
||||||
|
for f, flags in events:
|
||||||
|
if flags & select.POLLIN:
|
||||||
|
data = os.read(fd, 1024)
|
||||||
|
if not data:
|
||||||
|
continue
|
||||||
|
for byte in data:
|
||||||
|
if byte & 0x80:
|
||||||
|
if len(buf) > 0 and buf[0] >= 0x80:
|
||||||
|
_flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT)
|
||||||
|
buf = bytearray([byte])
|
||||||
|
else:
|
||||||
|
buf.append(byte)
|
||||||
|
if len(buf) >= 3 and (buf[0] & 0xE0) in (0x80, 0x90, 0xA0, 0xB0, 0xE0):
|
||||||
|
_flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT)
|
||||||
|
buf = bytearray()
|
||||||
|
elif len(buf) >= 2 and (buf[0] & 0xE0) in (0xC0, 0xD0):
|
||||||
|
_flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT)
|
||||||
|
buf = bytearray()
|
||||||
|
except (BlockingIOError, OSError) as e:
|
||||||
|
if isinstance(e, OSError) and e.errno == 19:
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
if not isinstance(e, BlockingIOError):
|
||||||
|
raise
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
running = False
|
||||||
|
|
||||||
|
os.close(fd)
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
def _flush(sock, buf, pd_host, pd_port, viz_port):
|
||||||
|
status = buf[0]
|
||||||
|
if (buf[0] & 0xF0) == 0xF0:
|
||||||
|
return
|
||||||
|
data1 = buf[1]
|
||||||
|
data2 = buf[2] if len(buf) >= 3 else None
|
||||||
|
msg = midi_msg(status, data1, data2)
|
||||||
|
sock.sendto(msg.encode(), (pd_host, pd_port))
|
||||||
|
sock.sendto(msg.encode(), (pd_host, viz_port))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+152
@@ -0,0 +1,152 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PD_DIR="/home/arduino/pd"
|
||||||
|
PATCH="$PD_DIR/synth.pd"
|
||||||
|
MODE="usb"
|
||||||
|
VERBOSE=false
|
||||||
|
|
||||||
|
die() { echo "[ERROR] $*" >&2; exit 1; }
|
||||||
|
info() { echo "[INFO] $*"; }
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-a|--auto) MODE="auto"; shift ;;
|
||||||
|
-i|--i2s) MODE="i2s"; shift ;;
|
||||||
|
-l|--loopback) MODE="loopback"; shift ;;
|
||||||
|
-v|--verbose) VERBOSE=true; shift ;;
|
||||||
|
-k|--kill) pkill -x pd 2>/dev/null && info "Pd stopped" || info "Pd not running"; exit 0 ;;
|
||||||
|
*) die "Unknown option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! command -v pd &>/dev/null; then
|
||||||
|
die "Pure Data not found. Install: apt install puredata"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$PATCH" ]; then
|
||||||
|
die "Patch not found: $PATCH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Auto-detect mode ---
|
||||||
|
if [ "$MODE" = "auto" ]; then
|
||||||
|
if [ -f /run/usb-role ]; then
|
||||||
|
ROLE=$(cat /run/usb-role)
|
||||||
|
else
|
||||||
|
ROLE=$(/usr/local/bin/detect-usb-role.sh 2>/dev/null && cat /run/usb-role || echo "gadget")
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$ROLE" in
|
||||||
|
gadget)
|
||||||
|
MODE="usb"
|
||||||
|
if ! aplay -l 2>/dev/null | grep -q "UAC1Gadget"; then
|
||||||
|
info "Configuring USB gadget..."
|
||||||
|
/usr/local/bin/configure-usb-audio.sh start 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
host)
|
||||||
|
if [ -f /run/usb-audio-dev ] && [ -s /run/usb-audio-dev ]; then
|
||||||
|
ALSA_DEV=$(cat /run/usb-audio-dev)
|
||||||
|
else
|
||||||
|
info "Detecting USB audio interface..."
|
||||||
|
/usr/local/bin/configure-usb-host.sh 2>/dev/null || {
|
||||||
|
die "No USB audio interface found. Plug one in via hub."
|
||||||
|
}
|
||||||
|
ALSA_DEV=$(cat /run/usb-audio-dev)
|
||||||
|
fi
|
||||||
|
info "Mode: USB host (MIDI controller + USB audio interface)"
|
||||||
|
MIDI_OPTS=(-alsamidi)
|
||||||
|
midi_dev=$(amidi -l 2>/dev/null | grep -v "^Dir" | head -1 | awk '{print $2}' | sed 's/\(hw:[0-9]*,[0-9]*\).*/\1/')
|
||||||
|
if [ -n "$midi_dev" ]; then
|
||||||
|
MIDI_OPTS+=(-midiadddev "$midi_dev")
|
||||||
|
info " MIDI device: $midi_dev"
|
||||||
|
fi
|
||||||
|
AUDIO_OPTS=(-audiooutdev 4 -audioindev 4)
|
||||||
|
PD_ARGS=(
|
||||||
|
-nogui -alsa
|
||||||
|
-send "pd dsp 1"
|
||||||
|
"${MIDI_OPTS[@]}"
|
||||||
|
"${AUDIO_OPTS[@]}"
|
||||||
|
-audiobuf 20 -blocksize 64 -r 48000
|
||||||
|
-channels 2 -inchannels 0 -outchannels 2
|
||||||
|
-path "$PD_DIR" "$PATCH"
|
||||||
|
)
|
||||||
|
if [ "$VERBOSE" = true ]; then pd "${PD_ARGS[@]}"
|
||||||
|
else pd "${PD_ARGS[@]}" > /dev/null 2>&1 &
|
||||||
|
PID=$!; info "Pd started (PID: $PID)"; info " Stop: kill $PID"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
info "Auto-detect: unknown role '$ROLE', falling back to gadget"
|
||||||
|
MODE="usb"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Configure per-mode options ---
|
||||||
|
AUDIO_OPTS=()
|
||||||
|
MIDI_OPTS=(-alsamidi)
|
||||||
|
|
||||||
|
if [ "$MODE" = "usb" ]; then
|
||||||
|
info "Mode: USB gadget (UAC1 audio + gmidi MIDI)"
|
||||||
|
AUDIO_OPTS=(-audiooutdev 2 -audioindev 2)
|
||||||
|
if aplay -l 2>/dev/null | grep -q "UAC1Gadget"; then
|
||||||
|
info " USB gadget: OK"
|
||||||
|
midi_dev=$(amidi -l 2>/dev/null | grep "f_midi" | head -1 | awk '{print $2}')
|
||||||
|
if [ -n "$midi_dev" ]; then
|
||||||
|
MIDI_OPTS+=(-midiadddev "$midi_dev")
|
||||||
|
info " MIDI device: $midi_dev"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
info " USB gadget: NOT DETECTED (kernel modules needed)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$MODE" = "i2s" ]; then
|
||||||
|
if [ -f /run/i2s-audio-dev ] && [ -s /run/i2s-audio-dev ]; then
|
||||||
|
ALSA_DEV=$(cat /run/i2s-audio-dev)
|
||||||
|
else
|
||||||
|
for dev in "hw:I2S,0" "hw:MI2S,0" "hw:0,0"; do
|
||||||
|
if aplay -l 2>/dev/null | grep -qi "${dev%,*}"; then
|
||||||
|
ALSA_DEV="$dev"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
info "Mode: I2S DAC (standalone analog audio)"
|
||||||
|
AUDIO_OPTS=(-audioadddev "$ALSA_DEV")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$MODE" = "loopback" ]; then
|
||||||
|
info "Mode: ALSA loopback (testing)"
|
||||||
|
AUDIO_OPTS=(-audiodev 2)
|
||||||
|
fi
|
||||||
|
|
||||||
|
PD_ARGS=(
|
||||||
|
-nogui
|
||||||
|
-alsa
|
||||||
|
"${MIDI_OPTS[@]}"
|
||||||
|
"${AUDIO_OPTS[@]}"
|
||||||
|
-send "pd dsp 1"
|
||||||
|
-audiobuf 20
|
||||||
|
-blocksize 64
|
||||||
|
-r 48000
|
||||||
|
-channels 1
|
||||||
|
-inchannels 0
|
||||||
|
-outchannels 1
|
||||||
|
-path "$PD_DIR"
|
||||||
|
"$PATCH"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "$VERBOSE" = true ]; then
|
||||||
|
pd "${PD_ARGS[@]}" &
|
||||||
|
else
|
||||||
|
pd "${PD_ARGS[@]}" > /dev/null 2>&1 &
|
||||||
|
fi
|
||||||
|
PID=$!
|
||||||
|
info "Pd started (PID: $PID)"
|
||||||
|
info " Stop: kill $PID"
|
||||||
|
wait $PID
|
||||||
|
info "Pd exited (code: $?)"
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#include "ArduinoGraphics.h"
|
||||||
|
#include "Arduino_LED_Matrix.h"
|
||||||
|
#include "Arduino_RouterBridge.h"
|
||||||
|
|
||||||
|
Arduino_LED_Matrix matrix;
|
||||||
|
String last_displayed = "";
|
||||||
|
|
||||||
|
String clear_matrix(String arg) {
|
||||||
|
matrix.clear();
|
||||||
|
last_displayed = "";
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
String draw_frame(String a_str, String b_str, String c_str, String d_str) {
|
||||||
|
uint32_t f[4];
|
||||||
|
f[0] = (uint32_t)a_str.toInt();
|
||||||
|
f[1] = (uint32_t)b_str.toInt();
|
||||||
|
f[2] = (uint32_t)c_str.toInt();
|
||||||
|
f[3] = (uint32_t)d_str.toInt();
|
||||||
|
matrix.loadFrame(f);
|
||||||
|
last_displayed = "frame";
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
String show_text(String text) {
|
||||||
|
matrix.clear();
|
||||||
|
matrix.beginText(0, 0, 0xFFFFFF);
|
||||||
|
matrix.text(text);
|
||||||
|
matrix.endText(SCROLL_LEFT);
|
||||||
|
last_displayed = "text:" + text;
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
String show_char(String c) {
|
||||||
|
if (c.length() == 0) return "ERROR: no char";
|
||||||
|
matrix.clear();
|
||||||
|
matrix.beginText(0, 0, 0xFFFFFF);
|
||||||
|
matrix.text(c);
|
||||||
|
matrix.endText(NO_SCROLL);
|
||||||
|
last_displayed = "char:" + c;
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
String show_two(String a, String b) {
|
||||||
|
matrix.clear();
|
||||||
|
matrix.beginText(0, 0, 0xFFFFFF);
|
||||||
|
matrix.text(a);
|
||||||
|
matrix.beginText(4, 0, 0xFFFFFF);
|
||||||
|
matrix.text(b);
|
||||||
|
matrix.endText(NO_SCROLL);
|
||||||
|
last_displayed = "two:" + a + b;
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
String set_brightness(String level) {
|
||||||
|
int l = level.toInt();
|
||||||
|
if (l < 1) l = 1;
|
||||||
|
if (l > 8) l = 8;
|
||||||
|
matrix.setGrayscaleBits(l);
|
||||||
|
matrix.clear();
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
String get_status(String arg) {
|
||||||
|
if (last_displayed.length() == 0) return "Matrix: cleared";
|
||||||
|
return "Matrix shows: " + last_displayed;
|
||||||
|
}
|
||||||
|
|
||||||
|
String play_heart(String arg) {
|
||||||
|
matrix.clear();
|
||||||
|
uint32_t hframes[][5] = {
|
||||||
|
{0x38022020, 0x810408a0, 0x2200e800, 0x20000000, 66},
|
||||||
|
{0x1c011010, 0x40820450, 0x11007400, 0x10000000, 66},
|
||||||
|
{0x0e008808, 0x20410228, 0x08803a00, 0x08000000, 66},
|
||||||
|
{0x07004404, 0x10208114, 0x04401d00, 0x04000000, 66},
|
||||||
|
{0x03802202, 0x0810408a, 0x02200e80, 0x02000000, 66},
|
||||||
|
{0x01c01101, 0x04082045, 0x01100740, 0x01000000, 66},
|
||||||
|
{0x00e00880, 0x82041022, 0x808803a0, 0x00000000, 66},
|
||||||
|
{0x00700440, 0x40020011, 0x004401c0, 0x00000000, 66},
|
||||||
|
{0x00380200, 0x20010008, 0x802000e0, 0x00000000, 66},
|
||||||
|
{0x00180100, 0x10008004, 0x00100060, 0x00000000, 66},
|
||||||
|
{0x00080080, 0x08004002, 0x00080020, 0x00000000, 66},
|
||||||
|
};
|
||||||
|
int n = sizeof(hframes) / sizeof(hframes[0]);
|
||||||
|
for (int i = 0; i < n * 3; i++) {
|
||||||
|
matrix.loadFrame(hframes[i % n]);
|
||||||
|
delay(66);
|
||||||
|
}
|
||||||
|
matrix.clear();
|
||||||
|
last_displayed = "heart animation";
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
matrix.begin();
|
||||||
|
matrix.textFont(Font_5x7);
|
||||||
|
matrix.textScrollSpeed(100);
|
||||||
|
|
||||||
|
matrix.clear();
|
||||||
|
matrix.beginText(0, 0, 0xFFFFFF);
|
||||||
|
matrix.text(".");
|
||||||
|
matrix.endText(NO_SCROLL);
|
||||||
|
|
||||||
|
delay(15000);
|
||||||
|
|
||||||
|
Bridge.begin();
|
||||||
|
Monitor.begin();
|
||||||
|
|
||||||
|
Bridge.provide_safe("draw_frame", draw_frame);
|
||||||
|
Bridge.provide_safe("show_text", show_text);
|
||||||
|
Bridge.provide_safe("show_char", show_char);
|
||||||
|
Bridge.provide_safe("show_two", show_two);
|
||||||
|
Bridge.provide_safe("clear", clear_matrix);
|
||||||
|
Bridge.provide_safe("set_brightness", set_brightness);
|
||||||
|
Bridge.provide("get_status", get_status);
|
||||||
|
Bridge.provide("play_heart", play_heart);
|
||||||
|
|
||||||
|
matrix.clear();
|
||||||
|
play_heart("");
|
||||||
|
Monitor.println("LED Matrix Bridge ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
profiles:
|
||||||
|
default:
|
||||||
|
fqbn: arduino:zephyr:unoq
|
||||||
|
platforms:
|
||||||
|
- platform: arduino:zephyr
|
||||||
|
libraries:
|
||||||
|
- Arduino_RouterBridge
|
||||||
|
- ArduinoGraphics
|
||||||
|
default_profile: default
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# USB role change udev rule
|
||||||
|
# Triggers role re-detection when USB-C cable is plugged/unplugged
|
||||||
|
# or when USB devices are connected/disconnected in host mode.
|
||||||
|
#
|
||||||
|
# Install: sudo cp 99-usb-role.rules /etc/udev/rules.d/
|
||||||
|
# Reload: sudo udevadm control --reload-rules
|
||||||
|
|
||||||
|
# USB gadget/UDC events (dwc3 role changes)
|
||||||
|
ACTION=="change", SUBSYSTEM=="udc", RUN+="/usr/local/bin/on-usb-role-change.sh"
|
||||||
|
|
||||||
|
# USB device connect/disconnect (host mode)
|
||||||
|
ACTION=="add|remove", SUBSYSTEM=="usb", RUN+="/usr/local/bin/on-usb-role-change.sh"
|
||||||
|
|
||||||
|
# USB audio card (host mode audio interface)
|
||||||
|
ACTION=="add|remove", SUBSYSTEM=="sound", RUN+="/usr/local/bin/on-usb-role-change.sh"
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# ALSA configuration for I2S DAC (e.g. PCM5102, PCM5242, MAX98357)
|
||||||
|
# Installed alongside asound.conf — activate via:
|
||||||
|
# sudo mv /etc/asound.conf /etc/asound.conf.bak
|
||||||
|
# sudo cp /etc/asound-i2s.conf /etc/asound.conf
|
||||||
|
#
|
||||||
|
# The I2S interface on the Qualcomm DragonWing typically appears as:
|
||||||
|
# hw:0,0 (Primary MI2S — default audio path)
|
||||||
|
# hw:MI2S,0 (Named MI2S device)
|
||||||
|
#
|
||||||
|
# Find available devices: aplay -l
|
||||||
|
|
||||||
|
# Default PCM — try common I2S device names
|
||||||
|
pcm.!default {
|
||||||
|
type plug
|
||||||
|
slave {
|
||||||
|
pcm "hw:I2S,0"
|
||||||
|
format S16_LE
|
||||||
|
channels 2
|
||||||
|
rate 48000
|
||||||
|
}
|
||||||
|
hint.description "I2S DAC (48kHz stereo)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback: try hw:0,0 if named device not found
|
||||||
|
pcm.i2s-default {
|
||||||
|
type plug
|
||||||
|
slave {
|
||||||
|
pcm "hw:0,0"
|
||||||
|
format S16_LE
|
||||||
|
channels 2
|
||||||
|
rate 48000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Software volume control (I2S DACs typically have no hardware mixer)
|
||||||
|
pcm.i2s-softvol {
|
||||||
|
type softvol
|
||||||
|
slave.pcm "hw:I2S,0"
|
||||||
|
control {
|
||||||
|
name "I2S Playback Volume"
|
||||||
|
card I2S
|
||||||
|
}
|
||||||
|
min_dB -51.0
|
||||||
|
max_dB 0.0
|
||||||
|
resolution 256
|
||||||
|
}
|
||||||
|
|
||||||
|
# Direct hardware access (bypass plug/softvol for low latency)
|
||||||
|
pcm.i2s-direct {
|
||||||
|
type plug
|
||||||
|
slave {
|
||||||
|
pcm "hw:I2S,0"
|
||||||
|
format S16_LE
|
||||||
|
channels 2
|
||||||
|
rate 48000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Loopback test (no I2S hardware connected)
|
||||||
|
pcm.loopback {
|
||||||
|
type plug
|
||||||
|
slave {
|
||||||
|
pcm "hw:Loopback,0,0"
|
||||||
|
format S16_LE
|
||||||
|
channels 2
|
||||||
|
rate 48000
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# ALSA configuration for USB gadget audio
|
||||||
|
# The UAC1 gadget appears as a ALSA card (typically card 1 or 2).
|
||||||
|
# This config routes Pd audio output to the USB gadget.
|
||||||
|
|
||||||
|
# Default PCM device for USB gadget audio
|
||||||
|
pcm.!default {
|
||||||
|
type plug
|
||||||
|
slave {
|
||||||
|
pcm "hw:UAC1Gadget,0"
|
||||||
|
format S16_LE
|
||||||
|
channels 2
|
||||||
|
rate 48000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Control device
|
||||||
|
ctl.!default {
|
||||||
|
type hw
|
||||||
|
card UAC1Gadget
|
||||||
|
}
|
||||||
|
|
||||||
|
# Software volume control via ALSA (hardware may not have mixer)
|
||||||
|
pcm.softvol {
|
||||||
|
type softvol
|
||||||
|
slave.pcm "hw:UAC1Gadget,0"
|
||||||
|
control {
|
||||||
|
name "Master Playback Volume"
|
||||||
|
card UAC1Gadget
|
||||||
|
}
|
||||||
|
min_dB -51.0
|
||||||
|
max_dB 0.0
|
||||||
|
resolution 256
|
||||||
|
}
|
||||||
|
|
||||||
|
# Null sink for testing when no USB gadget is connected
|
||||||
|
pcm.loopback {
|
||||||
|
type plug
|
||||||
|
slave {
|
||||||
|
pcm "hw:Loopback,0,0"
|
||||||
|
format S16_LE
|
||||||
|
channels 2
|
||||||
|
rate 48000
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+132
@@ -0,0 +1,132 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# configure-usb-audio.sh
|
||||||
|
# Configure the Arduino Uno Q as a composite USB gadget:
|
||||||
|
# UAC1 (USB Audio Class) + gmidi (USB MIDI)
|
||||||
|
#
|
||||||
|
# The host sees one device with both an audio sink and a MIDI port,
|
||||||
|
# allowing MIDI input over the same USB connection as audio output.
|
||||||
|
#
|
||||||
|
# Based on Linux USB gadget configfs setup.
|
||||||
|
# The Uno Q's USB peripheral controller is exposed via configfs.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GADGET_DIR="/sys/kernel/config/usb_gadget"
|
||||||
|
GADGET_NAME="g1"
|
||||||
|
UDC_DIR="/sys/class/udc"
|
||||||
|
|
||||||
|
# Gadget config
|
||||||
|
VID="0x2341" # Arduino VID
|
||||||
|
PID="0x006A" # Custom PID (Uno Q audio + MIDI)
|
||||||
|
SERIAL="$(cat /sys/firmware/devicetree/base/serial-number 2>/dev/null | tr -d '\0' || cat /proc/cpuinfo 2>/dev/null | grep Serial | head -1 | awk '{print $3}' || echo 'UnoQ')"
|
||||||
|
MANUFACTURER="Arduino"
|
||||||
|
PRODUCT="Uno Q Audio Synth"
|
||||||
|
|
||||||
|
# USB audio params
|
||||||
|
CHANNELS=2 # stereo out
|
||||||
|
SAMPLE_RATE=48000
|
||||||
|
SAMPLE_SIZE=2 # bytes (2 = 16-bit; kernel UAC1 gadget expects bytes, not bits)
|
||||||
|
|
||||||
|
# Find UDC (USB Device Controller)
|
||||||
|
find_udc() {
|
||||||
|
for controller in "$UDC_DIR"/*; do
|
||||||
|
if [ -d "$controller" ]; then
|
||||||
|
basename "$controller"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "ERROR: No UDC found" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
create_gadget() {
|
||||||
|
local udc="$1"
|
||||||
|
|
||||||
|
# Remove existing gadget if present
|
||||||
|
if [ -d "$GADGET_DIR/$GADGET_NAME" ]; then
|
||||||
|
echo "" > "$GADGET_DIR/$GADGET_NAME/UDC" 2>/dev/null || true
|
||||||
|
for _ in $(seq 1 50); do
|
||||||
|
if [ ! -f "$GADGET_DIR/$GADGET_NAME/UDC" ] || [ -z "$(cat "$GADGET_DIR/$GADGET_NAME/UDC" 2>/dev/null)" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
for config in "$GADGET_DIR/$GADGET_NAME/configs"/*/; do
|
||||||
|
config=$(basename "$config")
|
||||||
|
for func in "$GADGET_DIR/$GADGET_NAME/configs/$config/"*.*/; do
|
||||||
|
func=$(basename "$func")
|
||||||
|
[ -L "$GADGET_DIR/$GADGET_NAME/configs/$config/$func" ] && rm -f "$GADGET_DIR/$GADGET_NAME/configs/$config/$func"
|
||||||
|
done
|
||||||
|
rmdir "$GADGET_DIR/$GADGET_NAME/configs/$config" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
for func in "$GADGET_DIR/$GADGET_NAME/functions"/*/; do
|
||||||
|
func=$(basename "$func")
|
||||||
|
rmdir "$GADGET_DIR/$GADGET_NAME/functions/$func" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
rmdir "$GADGET_DIR/$GADGET_NAME" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$GADGET_DIR/$GADGET_NAME"
|
||||||
|
echo "$VID" > "$GADGET_DIR/$GADGET_NAME/idVendor"
|
||||||
|
echo "$PID" > "$GADGET_DIR/$GADGET_NAME/idProduct"
|
||||||
|
echo "0x0100" > "$GADGET_DIR/$GADGET_NAME/bcdDevice"
|
||||||
|
echo "0x0200" > "$GADGET_DIR/$GADGET_NAME/bcdUSB"
|
||||||
|
|
||||||
|
mkdir -p "$GADGET_DIR/$GADGET_NAME/strings/0x409"
|
||||||
|
echo "$SERIAL" > "$GADGET_DIR/$GADGET_NAME/strings/0x409/serialnumber"
|
||||||
|
echo "$MANUFACTURER" > "$GADGET_DIR/$GADGET_NAME/strings/0x409/manufacturer"
|
||||||
|
echo "$PRODUCT" > "$GADGET_DIR/$GADGET_NAME/strings/0x409/product"
|
||||||
|
|
||||||
|
# Create UAC1 function (audio output, wide host compatibility)
|
||||||
|
mkdir -p "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0"
|
||||||
|
echo "$CHANNELS" > "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0/p_chmask"
|
||||||
|
echo "$SAMPLE_RATE" > "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0/p_srate"
|
||||||
|
echo "$SAMPLE_SIZE" > "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0/p_ssize"
|
||||||
|
echo "$CHANNELS" > "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0/c_chmask"
|
||||||
|
echo "$SAMPLE_RATE" > "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0/c_srate"
|
||||||
|
echo "$SAMPLE_SIZE" > "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0/c_ssize"
|
||||||
|
|
||||||
|
# Create MIDI function (MIDI input from host)
|
||||||
|
mkdir -p "$GADGET_DIR/$GADGET_NAME/functions/midi.gs0"
|
||||||
|
echo 1 > "$GADGET_DIR/$GADGET_NAME/functions/midi.gs0/in_ports"
|
||||||
|
echo 1 > "$GADGET_DIR/$GADGET_NAME/functions/midi.gs0/out_ports"
|
||||||
|
|
||||||
|
# Create configuration
|
||||||
|
mkdir -p "$GADGET_DIR/$GADGET_NAME/configs/c.1"
|
||||||
|
mkdir -p "$GADGET_DIR/$GADGET_NAME/configs/c.1/strings/0x409"
|
||||||
|
echo "Audio + MIDI" > "$GADGET_DIR/$GADGET_NAME/configs/c.1/strings/0x409/configuration"
|
||||||
|
echo 250 > "$GADGET_DIR/$GADGET_NAME/configs/c.1/MaxPower"
|
||||||
|
|
||||||
|
# Link both functions to the same config (composite device)
|
||||||
|
ln -s "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0" "$GADGET_DIR/$GADGET_NAME/configs/c.1/"
|
||||||
|
ln -s "$GADGET_DIR/$GADGET_NAME/functions/midi.gs0" "$GADGET_DIR/$GADGET_NAME/configs/c.1/"
|
||||||
|
|
||||||
|
# Bind to UDC
|
||||||
|
echo "$udc" > "$GADGET_DIR/$GADGET_NAME/UDC"
|
||||||
|
echo "USB gadget '$GADGET_NAME' bound to $udc (UAC1 audio + gmidi)"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-start}" in
|
||||||
|
start)
|
||||||
|
udc=$(find_udc)
|
||||||
|
create_gadget "$udc"
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
if [ -d "$GADGET_DIR/$GADGET_NAME" ]; then
|
||||||
|
udc=$(cat "$GADGET_DIR/$GADGET_NAME/UDC" 2>/dev/null || true)
|
||||||
|
[ -n "$udc" ] && echo "" > "$GADGET_DIR/$GADGET_NAME/UDC" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
echo "USB gadget '$GADGET_NAME' stopped"
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
if [ -d "$GADGET_DIR/$GADGET_NAME/UDC" ]; then
|
||||||
|
echo "Gadget: $(cat "$GADGET_DIR/$GADGET_NAME/UDC" 2>/dev/null || echo 'not bound')"
|
||||||
|
else
|
||||||
|
echo "Gadget: not configured"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {start|stop|status}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# configure-usb-host.sh — Wait for USB audio interface in host mode.
|
||||||
|
# Finds the first USB audio capture/playback device and writes device
|
||||||
|
# info to /run/usb-audio-dev for Pd to use.
|
||||||
|
#
|
||||||
|
# When the Uno Q acts as USB host (MIDI controller + USB audio interface
|
||||||
|
# connected via hub), the USB audio interface appears as a ALSA card
|
||||||
|
# driven by snd-usb-audio.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DEV_FILE="/run/usb-audio-dev"
|
||||||
|
POLL_SECONDS=10
|
||||||
|
|
||||||
|
die() { echo "[USB-HOST] $*" >&2; exit 1; }
|
||||||
|
info() { echo "[USB-HOST] $*"; }
|
||||||
|
|
||||||
|
info "Waiting for USB audio interface..."
|
||||||
|
|
||||||
|
for ((i = 0; i < POLL_SECONDS; i++)); do
|
||||||
|
# Look for USB audio cards (not UAC1Gadget, not internal)
|
||||||
|
cards=$(aplay -l 2>/dev/null | grep -i "usb" | grep -vi "gadget" || true)
|
||||||
|
if [ -n "$cards" ]; then
|
||||||
|
# Pick the first USB audio card
|
||||||
|
card=$(echo "$cards" | head -1 | awk '{print $2}' | tr -d ':')
|
||||||
|
info "Found USB audio card: $card"
|
||||||
|
echo "hw:$card,0" > "$DEV_FILE"
|
||||||
|
cat "$DEV_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Also check if any MIDI devices appeared
|
||||||
|
if command -v aseqdump &>/dev/null; then
|
||||||
|
midi_ports=$(aconnect -i -o -l 2>/dev/null | grep -i "usb" | head -1 || true)
|
||||||
|
if [ -n "$midi_ports" ]; then
|
||||||
|
info "Found USB MIDI device: $midi_ports"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
die "No USB audio interface found after ${POLL_SECONDS}s (connect one and restart)"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Determine and set I2S audio ALSA device
|
||||||
|
Before=pd-synth-i2s.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/detect-i2s-device.sh
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# detect-i2s-device.sh — Probe for available I2S audio devices
|
||||||
|
# and create /run/i2s-audio-dev containing the best candidate.
|
||||||
|
# Called by detect-i2s-device.service before pd-synth-i2s starts.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DEVICE_FILE="/run/i2s-audio-dev"
|
||||||
|
write_dev() { echo "$1" > "$DEVICE_FILE"; echo "[DETECT] I2S device: $1"; exit 0; }
|
||||||
|
|
||||||
|
# Try named I2S devices first (most reliable)
|
||||||
|
for dev in "hw:I2S,0" "hw:MI2S,0" "hw:i2s,0" "hw:mi2s,0"; do
|
||||||
|
if aplay -l 2>/dev/null | grep -qi "${dev%,*}"; then
|
||||||
|
write_dev "$dev"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Try probing ALSA device list for anything that looks like I2S
|
||||||
|
while read -r card; do
|
||||||
|
id=$(echo "$card" | awk '{print $2}' | tr -d ':')
|
||||||
|
name=$(echo "$card" | cut -d: -f2- | xargs)
|
||||||
|
if echo "$name" | grep -qi -E "(i2s|mi2s|quat|tert|dac|audio)"; then
|
||||||
|
write_dev "hw:$id,0"
|
||||||
|
fi
|
||||||
|
done < <(cat /proc/asound/cards 2>/dev/null || true)
|
||||||
|
|
||||||
|
# Fallback to hw:0,0 (first audio device, may be HDMI or analog)
|
||||||
|
if aplay -l 2>/dev/null | grep -q "^card 0"; then
|
||||||
|
write_dev "hw:0,0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Nothing found
|
||||||
|
echo "[DETECT] No I2S audio device found" >&2
|
||||||
|
echo "" > "$DEVICE_FILE"
|
||||||
|
exit 1
|
||||||
Executable
+94
@@ -0,0 +1,94 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# detect-usb-role.sh — Detect whether USB-C should be gadget or host.
|
||||||
|
# Writes result to /run/usb-role for consumption by pd-synth-auto.service.
|
||||||
|
#
|
||||||
|
# Detection methods (tried in order):
|
||||||
|
# 1. extcon — USB cable detection (most reliable on Qualcomm)
|
||||||
|
# 2. dwc3 debug — USB controller mode
|
||||||
|
# 3. UDC state — if a UDC is bound as configured, we're in gadget mode
|
||||||
|
# 4. lsusb — if USB devices appear (after host controller init), we're host
|
||||||
|
#
|
||||||
|
# Exit code: 0 if role detected, 1 if unknown
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROLE_FILE="/run/usb-role"
|
||||||
|
unset ROLE
|
||||||
|
|
||||||
|
die() { echo "[USB-ROLE] $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# --- Method 1: extcon ---
|
||||||
|
check_extcon() {
|
||||||
|
for path in /sys/class/extcon/*/state; do
|
||||||
|
[ -f "$path" ] || continue
|
||||||
|
local state
|
||||||
|
state=$(cat "$path" 2>/dev/null || true)
|
||||||
|
if echo "$state" | grep -q "USB=1"; then
|
||||||
|
echo "gadget"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Method 2: dwc3 debugfs ---
|
||||||
|
check_dwc3() {
|
||||||
|
local mode_file
|
||||||
|
for mode_file in /sys/kernel/debug/usb/dwc3*/mode; do
|
||||||
|
[ -f "$mode_file" ] || continue
|
||||||
|
local mode
|
||||||
|
mode=$(cat "$mode_file" 2>/dev/null || true)
|
||||||
|
case "$mode" in
|
||||||
|
*peripheral*) echo "gadget"; return 0 ;;
|
||||||
|
*host*) echo "host"; return 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Method 3: UDC state ---
|
||||||
|
check_udc() {
|
||||||
|
for path in /sys/class/udc/*/state; do
|
||||||
|
[ -f "$path" ] || continue
|
||||||
|
local state
|
||||||
|
state=$(cat "$path" 2>/dev/null || true)
|
||||||
|
case "$state" in
|
||||||
|
configured) echo "gadget"; return 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Method 4: USB host devices ---
|
||||||
|
check_lsusb() {
|
||||||
|
if command -v lsusb &>/dev/null; then
|
||||||
|
local count
|
||||||
|
count=$(lsusb 2>/dev/null | grep -v "Linux Foundation" | wc -l)
|
||||||
|
# If we see non-root-hub devices, likely we're the host
|
||||||
|
if [ "$count" -gt 1 ]; then
|
||||||
|
echo "host"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try methods in order
|
||||||
|
ROLE=$(check_extcon || check_dwc3 || check_udc || echo "")
|
||||||
|
|
||||||
|
# If still unknown, check lsusb after brief delay (host controller needs time)
|
||||||
|
if [ -z "$ROLE" ]; then
|
||||||
|
sleep 2
|
||||||
|
ROLE=$(check_lsusb || echo "")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write result
|
||||||
|
if [ -n "$ROLE" ]; then
|
||||||
|
echo "$ROLE" > "$ROLE_FILE"
|
||||||
|
echo "[USB-ROLE] Detected: $ROLE"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "[USB-ROLE] Unknown — defaulting to gadget"
|
||||||
|
echo "gadget" > "$ROLE_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=LED Matrix MIDI Visualization
|
||||||
|
After=arduino-router.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=arduino
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/led-matrix-viz.py
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=MIDI rawmidi to Pd UDP bridge (MPK Mini → Pd)
|
||||||
|
After=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/midi-bridge.py
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# on-usb-role-change.sh — Called by udev on USB role/connect events.
|
||||||
|
# Re-detects role, stops/restarts Pd with appropriate config.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROLE_FILE="/run/usb-role"
|
||||||
|
LOG_TAG="[USB-HOTPLUG]"
|
||||||
|
|
||||||
|
# Debounce: skip if we just ran (within 3s)
|
||||||
|
LOCK="/tmp/usb-role-change.lock"
|
||||||
|
if [ -f "$LOCK" ]; then
|
||||||
|
age=$(($(date +%s) - $(stat -c %Y "$LOCK" 2>/dev/null || echo 0)))
|
||||||
|
[ "$age" -lt 3 ] && exit 0
|
||||||
|
fi
|
||||||
|
touch "$LOCK"
|
||||||
|
|
||||||
|
logger -t "$LOG_TAG" "USB role change event — re-detecting..."
|
||||||
|
|
||||||
|
/usr/local/bin/detect-usb-role.sh
|
||||||
|
|
||||||
|
# Restart Pd if it's running
|
||||||
|
if systemctl is-active pd-synth-auto &>/dev/null; then
|
||||||
|
logger -t "$LOG_TAG" "Restarting Pd with new role"
|
||||||
|
systemctl restart pd-synth-auto
|
||||||
|
fi
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Pure Data Synth (auto-detect: gadget or host)
|
||||||
|
Documentation=https://github.com/arduino/uno-q-audio-synth
|
||||||
|
After=usb-role-detect.service
|
||||||
|
Wants=usb-role-detect.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/start-synth.sh --auto --verbose
|
||||||
|
ExecStop=/usr/bin/killall -s TERM pd
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Pure Data I2S DAC Synth (standalone)
|
||||||
|
After=network.target sound.target
|
||||||
|
Wants=configure-usb-audio.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=arduino
|
||||||
|
ExecStart=/usr/local/bin/start-synth.sh --i2s
|
||||||
|
ExecStop=/usr/bin/killall pd
|
||||||
|
WorkingDirectory=/home/arduino/pd
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Pure Data USB Audio Synth
|
||||||
|
After=network.target sound.target
|
||||||
|
Wants=usb-gadget-audio.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=arduino
|
||||||
|
ExecStart=/usr/local/bin/start-synth.sh
|
||||||
|
ExecStop=/usr/bin/killall pd
|
||||||
|
WorkingDirectory=/home/arduino/pd
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Reset USB controller and re-bind gadget
|
||||||
|
UDC="4e00000.usb"
|
||||||
|
GADGET="/sys/kernel/config/usb_gadget/g1"
|
||||||
|
|
||||||
|
# Find the driver binding
|
||||||
|
DRIVER=""
|
||||||
|
for d in /sys/bus/platform/drivers/*/; do
|
||||||
|
dname=$(basename "$d")
|
||||||
|
if [ -L "$d/$UDC" ] 2>/dev/null; then
|
||||||
|
DRIVER="$dname"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$DRIVER" ]; then
|
||||||
|
# Try looking at the UDC device itself
|
||||||
|
UDC_PATH=$(readlink -f /sys/class/udc/$UDC/device 2>/dev/null || echo "")
|
||||||
|
if [ -n "$UDC_PATH" ]; then
|
||||||
|
DRIVER=$(basename "$(dirname "$UDC_PATH")" 2>/dev/null || echo "")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Driver: ${DRIVER:-unknown}"
|
||||||
|
|
||||||
|
# Unbind
|
||||||
|
if [ -n "$DRIVER" ]; then
|
||||||
|
echo "$UDC" > "/sys/bus/platform/drivers/$DRIVER/unbind" 2>/dev/null && echo "Unbound" || echo "Unbind failed"
|
||||||
|
sleep 2
|
||||||
|
echo "$UDC" > "/sys/bus/platform/drivers/$DRIVER/bind" 2>/dev/null && echo "Rebound" || echo "Rebind failed"
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bind gadget
|
||||||
|
echo "$UDC" > "$GADGET/UDC" 2>&1 && echo "Gadget bound" || echo "Gadget bind failed"
|
||||||
|
cat /sys/class/udc/$UDC/state
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Configure USB Audio Gadget
|
||||||
|
Before=pd-synth.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/configure-usb-audio.sh start
|
||||||
|
ExecStop=/usr/local/bin/configure-usb-audio.sh stop
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Detect USB role (gadget vs host)
|
||||||
|
DefaultDependencies=no
|
||||||
|
After=sysinit.target
|
||||||
|
Before=pd-synth-auto.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/detect-usb-role.sh
|
||||||
|
RemainAfterExit=yes
|
||||||
|
StandardOutput=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=sysinit.target
|
||||||
Reference in New Issue
Block a user