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