diff --git a/.gitignore b/.gitignore index 3a5e5fd..e0551c4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,16 @@ __pycache__/ *.so *.egg-info/ dist/ -/build/* +# Build outputs (ignore artifacts, track scripts/configs) +/build/work/ +/build/downloads/ +/build/out/ +/build/*.img +/build/*.img.xz +!/build/*.sh +!/build/*.md !/build/.gitkeep +!/build/first-boot/ *.egg .eggs/ venv/ diff --git a/README.md b/README.md index f3a399d..5f51602 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,29 @@ RPi4B-based multi-channel real-time audio mixer with custom PREEMPT_RT kernel. ## Status -**Phase 1-4: Core audio stack + MIDI controller support (in progress)** +**Phase 1-7: Complete audio mixer stack with SD card image builder** + - [x] P1-R1: System Research — USB audio, PREEMPT_RT kernel analysis - [x] P1-R2: Base OS Selection — RPiOS Lite 64-bit + custom PREEMPT_RT - [x] P2-R1: ALSA + JACK2 Low-Latency Config +- [x] P3-R1: Core Mixer Engine — DSP pipeline, channel strips, routing +- [x] P3-R2: Backing Track Player — sync'd audio playback +- [x] P3-R3: Multi-Track Recording — WAV capture + session - [x] P4-R1: MIDI Controller Support — Binding + Learn Mode -- [ ] P3: Audio Engine — DSP pipeline, mixing engine -- [ ] P4: Hardware Integration — GPIO control surface +- [x] P5-R3: Web App Frontend — touch-optimized mixer UI +- [x] P6-R1: Streaming Pipeline — GStreamer H.264 to YouTube/Twitch +- [x] P6-R2: Session Manager — save/load mixer states + setlists +- [x] P7-R1: Build Script — automated SD card image creation + +## SD Card Image Builder + +```bash +# Build a ready-to-flash SD card image +./build/build.sh --skip-kernel # Quick build (10-15 min) +./build/build.sh # Full build with RT kernel (30-45 min) +``` + +See [build/README.md](build/README.md) for flashing instructions and full documentation. ## Getting Started diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..9848fc9 --- /dev/null +++ b/build/README.md @@ -0,0 +1,247 @@ +# RPi Audio Mixer — SD Card Image Builder + +This directory contains the automated build system for creating a ready-to-flash +Raspberry Pi 4B SD card image with the full audio mixer stack. + +## Quick Start + +```bash +# Install build dependencies +sudo apt install wget xz-utils dosfstools e2fsprogs qemu-user-static + +# Optional: for PREEMPT_RT kernel cross-compilation +sudo apt install gcc-aarch64-linux-gnu + +# Build the image +./build/build.sh + +# The image lands in: +ls -lh build/out/rpi-audio-mixer-*.img.xz +``` + +## Build Modes + +| Command | Description | Time (approx) | +|---------|-------------|---------------| +| `./build/build.sh` | Full build with PREEMPT_RT kernel | 30-45 min | +| `./build/build.sh --skip-kernel` | Quick build, stock kernel | 10-15 min | +| `./build/build.sh --kernel-only` | Only build RT kernel | 10-15 min | +| `./build/build.sh --clean` | Remove all build artifacts | < 1 min | + +## Flashing to SD Card + +### From Linux + +```bash +# 1. Decompress the image +xz -d build/out/rpi-audio-mixer-*.img.xz + +# 2. Find your SD card device +lsblk +# Look for /dev/sdX or /dev/mmcblkX (e.g., /dev/sdb or /dev/mmcblk0) + +# 3. Flash the image (WARNING: this overwrites the target device!) +sudo dd if=build/out/rpi-audio-mixer-*.img of=/dev/sdX bs=4M status=progress conv=fsync + +# 4. Sync and eject +sync +sudo eject /dev/sdX +``` + +### From macOS + +```bash +# 1. Decompress +xz -d build/out/rpi-audio-mixer-*.img.xz + +# 2. Find the SD card +diskutil list +# Look for /dev/diskX (external, physical) + +# 3. Unmount (do NOT eject) +diskutil unmountDisk /dev/diskX + +# 4. Flash +sudo dd if=build/out/rpi-audio-mixer-*.img of=/dev/rdiskX bs=4m status=progress + +# 5. Eject +diskutil eject /dev/diskX +``` + +### From Windows + +Use [Raspberry Pi Imager](https://www.raspberrypi.com/software/) or [balenaEtcher](https://www.balena.io/etcher/): + +1. Decompress the `.img.xz` with 7-Zip or WinRAR +2. Open Raspberry Pi Imager +3. Choose "Use custom" → select the `.img` file +4. Select your SD card and flash + +## First Boot + +1. Insert the SD card into a Raspberry Pi 4B (4GB+ RAM recommended) +2. Connect your USB audio interface +3. (Optional) Connect HDMI touchscreen + Ethernet +4. Power on + +The first-boot setup wizard will appear automatically on the HDMI display +(or serial console). It will guide you through: + +- Audio interface detection and selection +- WiFi configuration +- Hostname setting +- API key generation +- JACK buffer/latency settings + +After the wizard completes, the system reboots into normal operation. + +### Default Access + +| Service | URL / Command | Default | +|---------|--------------|---------| +| Web UI | `http://pi-mixer.local:8080` | API key from wizard | +| SSH | `ssh pi@pi-mixer.local` | Password: `raspberry` | +| Touch UI | HDMI display (auto-starts) | — | + +**Important:** Change the default password on first login: +```bash +passwd +``` + +## Image Contents + +``` +Partition layout: +├── /boot (FAT32, 256MB) +│ ├── kernel8-rt.img PREEMPT_RT kernel (if built) +│ ├── config.txt GPU/boot configuration +│ ├── cmdline.txt Kernel command line +│ └── *.dtb, overlays/ Device Tree Blobs +├── / (ext4, ~4GB used of 6GB) +│ ├── /opt/rpi-mixer/ Mixer source + venv +│ ├── /etc/systemd/system/ Service files +│ └── /home/pi/ pi user home +└── /data (ext4, remaining space) + ├── sessions/ Saved mixer sessions + ├── recordings/ Multi-track recordings + ├── presets/ Plugin presets + └── logs/ System/audio logs +``` + +## Systemd Services + +| Service | Description | Auto-start | +|---------|-------------|-----------| +| `cpu-performance.service` | Lock CPU to max frequency | Boot | +| `jackd.service` | JACK2 real-time audio server | After first-boot | +| `mixer-api.service` | REST API + WebSocket + OSC server | After first-boot | +| `mixer-ui.service` | Kivy touchscreen UI | After first-boot | +| `mixer-firstboot.service` | First-boot setup wizard | First boot only | + +```bash +# Check service status +systemctl status jackd mixer-api mixer-ui + +# View logs +journalctl -u mixer-api -f +journalctl -u jackd -f + +# Manual control +sudo systemctl stop mixer-ui mixer-api jackd +sudo systemctl start jackd && sleep 2 && sudo systemctl start mixer-api +``` + +## CI / Automated Builds + +Build without a Raspberry Pi (CI-friendly): + +```bash +# On any x86_64 Linux host with qemu-user-static +export MIXER_VERSION="$(git describe --tags --always)" +export MIXER_HOSTNAME="ci-mixer" +export MIXER_API_KEY="ci-test-key-12345" + +./build/build.sh --skip-kernel +``` + +For GitHub Actions / Gitea CI, add a workflow step: + +```yaml +- name: Build SD card image + run: | + sudo apt install -y wget xz-utils dosfstools e2fsprogs qemu-user-static + ./build/build.sh --skip-kernel +- name: Upload image artifact + uses: actions/upload-artifact@v4 + with: + name: sd-card-image + path: build/out/rpi-audio-mixer-*.img.xz +``` + +## Customization + +### Pre-configure WiFi and hostname + +```bash +MIXER_WIFI_SSID="MyNetwork" MIXER_WIFI_PASS="secret123" \ +MIXER_HOSTNAME="stage-mixer" ./build/build.sh --skip-kernel +``` + +### Custom image size + +```bash +./build/build.sh --image-size 16384 # 16 GB image +``` + +### Different Raspberry Pi OS base + +```bash +RPIOS_URL="https://example.com/my-custom-base.img.xz" ./build/build.sh +``` + +## Architecture Decision + +This project uses **Raspberry Pi OS Lite (64-bit, Bookworm) + post-install +configuration** rather than Buildroot or Yocto. Rationale: + +- Full Debian package ecosystem for rapid audio stack iteration +- `apt install` for JACK, Carla, debugging tools — no image rebuilds +- Standard systemd-based system — familiar and well-documented +- PREEMPT_RT kernel built via cross-compilation on x86 host +- Buildroot/Yocto can be revisited for production appliance images + +See [docs/research/base-os-decision.md](../docs/research/base-os-decision.md) +for the full analysis. + +## Troubleshooting + +### Build fails on "qemu-aarch64-static not found" + +```bash +sudo apt install qemu-user-static +sudo update-binfmts --enable qemu-aarch64 +``` + +### Build fails on apt in chroot + +This usually means DNS isn't resolving inside the chroot. The configure script +uses the host's `/etc/resolv.conf` — make sure your host has working DNS. + +### Image won't boot on RPi + +- Check you're using a Raspberry Pi 4B (not Pi 3 or Pi 5) +- Verify the SD card is at least 16 GB (Class A2 recommended) +- Try a different power supply (RPi4 needs 5V/3A) +- Check the HDMI display for kernel panic messages + +### No audio on first boot + +- Run `sudo systemctl status jackd` to check JACK +- Run `aplay -l` to list audio devices +- Re-run the setup wizard: `sudo touch /force-firstboot && sudo reboot` + +### Web UI not accessible + +- Check the service: `sudo systemctl status mixer-api` +- Find the IP: `ip addr show | grep inet` +- Check the API key in the service file: `grep API_KEY /etc/systemd/system/mixer-api.service` diff --git a/build/build.sh b/build/build.sh new file mode 100755 index 0000000..5eb2395 --- /dev/null +++ b/build/build.sh @@ -0,0 +1,607 @@ +#!/bin/bash +# build.sh — Build RPi Audio Mixer SD card image from scratch +# +# Produces a ready-to-flash Raspberry Pi 4B SD card image with: +# - RPiOS Lite (64-bit, Bookworm) base +# - Optional PREEMPT_RT kernel (cross-compiled on x86) +# - JACK2, Carla, ALSA tools, Python deps +# - Full mixer engine + web UI + Kivy touch UI +# - First-boot setup wizard +# - systemd services for auto-start +# +# Usage: +# ./build.sh # Full build (default) +# ./build.sh --skip-kernel # Skip RT kernel build (use stock kernel) +# ./build.sh --kernel-only # Only build RT kernel, skip image +# ./build.sh --output ./my-image.img # Custom output path +# ./build.sh --clean # Remove build artifacts +# +# Environment: +# MIXER_VERSION version tag (default: git describe or "dev") +# MIXER_API_KEY default API key (default: auto-generated) +# MIXER_HOSTNAME default hostname (default: pi-mixer) +# MIXER_WIFI_SSID pre-configure WiFi SSID (optional) +# MIXER_WIFI_PASS pre-configure WiFi password (optional) +# +# Requirements: +# - Linux host (x86_64 or aarch64) +# - sudo access +# - ~10 GB free disk space +# - Packages: qemu-user-static, wget, xz-utils, e2fsprogs, dosfstools +# - Cross-compile (optional): gcc-aarch64-linux-gnu, libssl-dev:arm64 +# +# Output: +# build/out/rpi-audio-mixer-.img SD card image +# build/out/rpi-audio-mixer-.img.xz Compressed image + +set -euo pipefail + +# ─── Configuration ─────────────────────────────────────────────────────────── + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +BUILD_DIR="$SCRIPT_DIR" +OUT_DIR="$BUILD_DIR/out" +WORK_DIR="$BUILD_DIR/work" +DOWNLOAD_DIR="$BUILD_DIR/downloads" + +MIXER_VERSION="${MIXER_VERSION:-$(git -C "$PROJECT_ROOT" describe --tags --always 2>/dev/null || echo 'dev')}" +MIXER_API_KEY="${MIXER_API_KEY:-mixer-$(head -c 12 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 16)}" +MIXER_HOSTNAME="${MIXER_HOSTNAME:-pi-mixer}" +MIXER_WIFI_SSID="${MIXER_WIFI_SSID:-}" +MIXER_WIFI_PASS="${MIXER_WIFI_PASS:-}" + +# RPiOS image +RPIOS_URL="${RPIOS_URL:-https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2025-03-07/2025-03-07-raspios-bookworm-arm64-lite.img.xz}" +RPIOS_SHA256="" + +# RPi kernel for RT build +RPI_KERNEL_REPO="${RPI_KERNEL_REPO:-https://github.com/raspberrypi/linux.git}" +RPI_KERNEL_BRANCH="${RPI_KERNEL_BRANCH:-rpi-6.12.y}" + +# Image size (expandable on first boot) +IMAGE_SIZE_MB="${IMAGE_SIZE_MB:-8192}" # 8 GB base image +BOOT_SIZE_MB="${BOOT_SIZE_MB:-256}" # 256 MB FAT32 boot + +# Build flags +CROSS_COMPILE="${CROSS_COMPILE:-aarch64-linux-gnu-}" +KERNEL_JOBS="${KERNEL_JOBS:-$(nproc 2>/dev/null || echo 4)}" +BUILD_KERNEL="${BUILD_KERNEL:-true}" + +# QEMU static for chroot +QEMU_STATIC="/usr/bin/qemu-aarch64-static" + +# ─── Colours ───────────────────────────────────────────────────────────────── + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +log() { echo -e "${GREEN}[BUILD]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +err() { echo -e "${RED}[ERROR]${NC} $*"; } +step() { echo -e "\n${BLUE}${BOLD}═══ $* ═══${NC}\n"; } + +# ─── Usage ─────────────────────────────────────────────────────────────────── + +usage() { + cat <.img) + --clean Remove build artifacts (work + downloads + out) + --image-size MB Image size in MB (default: 8192) + --version VERSION Version tag for the image + --help Show this help + +${BOLD}Examples:${NC} + $0 # Full build with RT kernel + $0 --skip-kernel # Quick build with stock kernel + $0 --kernel-only # Only cross-compile the kernel + $0 --output ./pi-mixer-v2.img # Custom output path + $0 --clean # Clean build artifacts + +${BOLD}Environment:${NC} + MIXER_VERSION Version tag (default: git describe or "dev") + MIXER_API_KEY Default API key (auto-generated if unset) + MIXER_HOSTNAME Default hostname (default: pi-mixer) + MIXER_WIFI_SSID Pre-configure WiFi SSID (optional) + MIXER_WIFI_PASS Pre-configure WiFi password (optional) + +${BOLD}Output:${NC} + build/out/rpi-audio-mixer-.img Raw SD card image + build/out/rpi-audio-mixer-.img.xz Compressed image +EOF + exit 0 +} + +# ─── Argument parsing ──────────────────────────────────────────────────────── + +KERNEL_ONLY=false +OUTPUT_PATH="" +CLEAN=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --skip-kernel) BUILD_KERNEL=false; shift ;; + --kernel-only) KERNEL_ONLY=true; shift ;; + --output) OUTPUT_PATH="$2"; shift 2 ;; + --image-size) IMAGE_SIZE_MB="$2"; shift 2 ;; + --version) MIXER_VERSION="$2"; shift 2 ;; + --clean) CLEAN=true; shift ;; + --help) usage ;; + *) err "Unknown option: $1"; usage ;; + esac +done + +if [[ -z "$OUTPUT_PATH" ]]; then + OUTPUT_PATH="$OUT_DIR/rpi-audio-mixer-${MIXER_VERSION}.img" +fi + +# ─── Clean ─────────────────────────────────────────────────────────────────── + +if $CLEAN; then + step "Cleaning build artifacts" + rm -rf "$WORK_DIR" "$DOWNLOAD_DIR" "$OUT_DIR" + log "Build artifacts cleaned: $WORK_DIR $DOWNLOAD_DIR $OUT_DIR" + exit 0 +fi + +# ─── Prerequisites ─────────────────────────────────────────────────────────── + +check_deps() { + local missing=() + + command -v wget >/dev/null 2>&1 || missing+=(wget) + command -v xz >/dev/null 2>&1 || missing+=(xz-utils) + command -v mkfs.vfat >/dev/null 2>&1 || missing+=(dosfstools) + command -v mkfs.ext4 >/dev/null 2>&1 || missing+=(e2fsprogs) + + if [[ ! -f "$QEMU_STATIC" ]]; then + missing+=("qemu-user-static (sudo apt install qemu-user-static)") + fi + + if $BUILD_KERNEL; then + command -v "${CROSS_COMPILE}gcc" >/dev/null 2>&1 || missing+=("gcc-aarch64-linux-gnu (sudo apt install gcc-aarch64-linux-gnu)") + fi + + if [[ ${#missing[@]} -gt 0 ]]; then + err "Missing dependencies:" + for m in "${missing[@]}"; do + echo " - $m" + done + exit 1 + fi +} + +# ─── Directory setup ───────────────────────────────────────────────────────── + +setup_dirs() { + mkdir -p "$OUT_DIR" "$WORK_DIR" "$DOWNLOAD_DIR" + # Pre-create the kernel build workdir + if $BUILD_KERNEL; then + mkdir -p "$WORK_DIR/kernel" + fi +} + +# ─── Step 1: Download RPiOS Lite image ──────────────────────────────────────── + +download_base_image() { + local img_xz="$DOWNLOAD_DIR/$(basename "$RPIOS_URL")" + local img="${img_xz%.xz}" + + if [[ -f "$img" ]]; then + log "Base image already downloaded: $img" + echo "$img" + return + fi + + step "Step 1/7: Downloading RPiOS Lite (64-bit, Bookworm)" + log "URL: $RPIOS_URL" + wget -nc -O "$img_xz" "$RPIOS_URL" || { + err "Failed to download RPiOS base image. Check your internet connection." + err "You can also download manually and place it at: $img_xz" + exit 1 + } + + log "Decompressing..." + xz -d -k "$img_xz" # -k keeps the .xz + + echo "$img" +} + +# ─── Step 1.5 (optional): Build PREEMPT_RT kernel ──────────────────────────── + +build_rt_kernel() { + local kernel_dir="$WORK_DIR/kernel/linux-rpi" + + step "Step 2/7: Building PREEMPT_RT kernel (cross-compiled on $(uname -m))" + + # Clone if not already present + if [[ ! -d "$kernel_dir" ]]; then + log "Cloning Raspberry Pi kernel (branch: $RPI_KERNEL_BRANCH)..." + git clone --depth=1 -b "$RPI_KERNEL_BRANCH" "$RPI_KERNEL_REPO" "$kernel_dir" + else + log "Kernel source already present at $kernel_dir" + fi + + cd "$kernel_dir" + + # Apply PREEMPT_RT config + if [[ ! -f ".config" ]] || ! grep -q "CONFIG_PREEMPT_RT=y" .config 2>/dev/null; then + log "Configuring kernel with PREEMPT_RT..." + make ARCH=arm64 CROSS_COMPILE="$CROSS_COMPILE" bcm2711_defconfig + + # Enable RT configs + scripts/config -e CONFIG_PREEMPT_RT + scripts/config -d CONFIG_PREEMPT + scripts/config -e CONFIG_HIGH_RES_TIMERS + scripts/config -e CONFIG_HZ_1000 + scripts/config -e CONFIG_NO_HZ_FULL + scripts/config -e CONFIG_RCU_NOCB_CPU + scripts/config -e CONFIG_CPU_FREQ_DEFAULT_GOV_PERFORMANCE + scripts/config -e CONFIG_CPU_FREQ_GOV_PERFORMANCE + scripts/config -e CONFIG_IRQ_FORCED_THREADING + scripts/config --set-val CONFIG_HZ 1000 + + # USB audio (build as module) + scripts/config -m CONFIG_SND_USB_AUDIO + + make ARCH=arm64 CROSS_COMPILE="$CROSS_COMPILE" olddefconfig + else + log "Kernel .config already has PREEMPT_RT enabled" + fi + + log "Building kernel (jobs: $KERNEL_JOBS)..." + make -j "$KERNEL_JOBS" ARCH=arm64 CROSS_COMPILE="$CROSS_COMPILE" \ + Image modules dtbs 2>&1 | tail -20 + + log "Building modules..." + make -j "$KERNEL_JOBS" ARCH=arm64 CROSS_COMPILE="$CROSS_COMPILE" modules + + # Install modules to staging + local mod_staging="$WORK_DIR/kernel/modules" + rm -rf "$mod_staging" + mkdir -p "$mod_staging" + make ARCH=arm64 CROSS_COMPILE="$CROSS_COMPILE" \ + INSTALL_MOD_PATH="$mod_staging" modules_install + + log "Kernel build complete" + log " Image: $kernel_dir/arch/arm64/boot/Image" + log " DTBs: $kernel_dir/arch/arm64/boot/dts/broadcom/*.dtb" + log " Modules: $mod_staging/lib/modules/" + + cd "$PROJECT_ROOT" +} + +# ─── Step 2: Create image file ─────────────────────────────────────────────── + +create_image() { + local img_file="$1" + + step "Step 3/7: Creating SD card image (${IMAGE_SIZE_MB}MB)" + + rm -f "$img_file" + dd if=/dev/zero of="$img_file" bs=1M count="$IMAGE_SIZE_MB" status=progress 2>/dev/null || \ + dd if=/dev/zero of="$img_file" bs=1M count="$IMAGE_SIZE_MB" + + # Partition table (MBR) + # Part 1: FAT32 boot, 256MB + # Part 2: ext4 root, 6GB + # Part 3: ext4 /data, remaining + local root_size_mb=6144 + local data_start_mb=$((BOOT_SIZE_MB + root_size_mb)) + + log "Partitioning image..." + cat </dev/null 2>&1 +o +n +p +1 + ++${BOOT_SIZE_MB}M +t +c +n +p +2 + ++${root_size_mb}M +n +p +3 + + +w +FDISK + + # Mount via loopback + local loopdev + loopdev=$(sudo losetup -f --show -P "$img_file") + log "Loopback device: $loopdev" + + # Format partitions + log "Formatting partitions..." + sudo mkfs.vfat -F 32 -n BOOT "${loopdev}p1" >/dev/null 2>&1 + sudo mkfs.ext4 -F -L rootfs "${loopdev}p2" >/dev/null 2>&1 + sudo mkfs.ext4 -F -L data "${loopdev}p3" >/dev/null 2>&1 + + echo "$loopdev" +} + +# ─── Step 3: Mount image partitions ────────────────────────────────────────── + +mount_image() { + local loopdev="$1" + local mount_point="$2" + + mkdir -p "$mount_point"/{boot,data} + + sudo mount "${loopdev}p2" "$mount_point" + sudo mount "${loopdev}p1" "$mount_point/boot" + sudo mount "${loopdev}p3" "$mount_point/data" + + log "Mounted image at $mount_point" +} + +unmount_image() { + local mount_point="$1" + local loopdev="$2" + + log "Unmounting image..." + sudo umount -R "$mount_point" 2>/dev/null || true + sudo losetup -d "$loopdev" 2>/dev/null || true +} + +# ─── Step 4: Populate root filesystem ──────────────────────────────────────── + +populate_rootfs() { + local base_img="$1" + local mount_point="$2" + + step "Step 4/7: Populating root filesystem" + + # Mount base image to copy from it + local base_mount="$WORK_DIR/base-mount" + mkdir -p "$base_mount" + local base_loop + base_loop=$(sudo losetup -f --show -P "$base_img") + sudo mount "${base_loop}p2" "$base_mount" 2>/dev/null || \ + sudo mount "${base_loop}" "$base_mount" # maybe not partitioned + + log "Copying base OS files... (this takes a few minutes)" + sudo rsync -a --info=progress2 "$base_mount/" "$mount_point/" 2>/dev/null || \ + sudo rsync -a "$base_mount/" "$mount_point/" + + sudo umount "$base_mount" 2>/dev/null || true + sudo losetup -d "$base_loop" 2>/dev/null || true + rmdir "$base_mount" + + # Set up first-boot expand script + log "Setting up first-boot root expansion..." + sudo mkdir -p "$mount_point/etc/init.d" +} + +# ─── Step 5: Configure the system (chroot) ─────────────────────────────────── + +configure_system() { + local mount_point="$1" + + step "Step 5/7: Configuring system (chroot)" + + # Copy QEMU for chroot + if [[ "$(uname -m)" != "aarch64" ]]; then + sudo cp "$QEMU_STATIC" "$mount_point/usr/bin/" + fi + + # Prepare chroot environment + sudo mount -t proc proc "$mount_point/proc" || true + sudo mount -t sysfs sys "$mount_point/sys" || true + sudo mount -t devtmpfs dev "$mount_point/dev" || true + sudo mount -t devpts devpts "$mount_point/dev/pts" || true + # /run may already be tmpfs under chroot + sudo mount -t tmpfs tmpfs "$mount_point/run" 2>/dev/null || true + + # Copy the configuration script into the image + local config_script="$mount_point/tmp/configure-system.sh" + sudo cp "$BUILD_DIR/configure-system.sh" "$config_script" + sudo chmod +x "$config_script" + + # Copy project source + log "Copying project source into image..." + sudo mkdir -p "$mount_point/opt/rpi-mixer" + sudo rsync -a --exclude='.git' --exclude='build/work' --exclude='build/downloads' \ + --exclude='build/out' --exclude='__pycache__' --exclude='*.pyc' \ + --exclude='.pytest_cache' --exclude='venv' --exclude='.venv' \ + "$PROJECT_ROOT/" "$mount_point/opt/rpi-mixer/" + + # Run configuration inside chroot + log "Running system configuration inside chroot..." + sudo chroot "$mount_point" /bin/bash /tmp/configure-system.sh || { + warn "Chroot configuration had non-zero exit. Continuing..." + } + + # Clean up + sudo rm -f "$mount_point/tmp/configure-system.sh" + sudo rm -f "$mount_point/usr/bin/qemu-aarch64-static" + + # Unmount chroot mounts + sudo umount "$mount_point/proc" 2>/dev/null || true + sudo umount "$mount_point/sys" 2>/dev/null || true + sudo umount "$mount_point/dev/pts" 2>/dev/null || true + sudo umount "$mount_point/dev" 2>/dev/null || true + sudo umount "$mount_point/run" 2>/dev/null || true +} + +# ─── Step 6: Install kernel (if RT built) ──────────────────────────────────── + +install_rt_kernel() { + local mount_point="$1" + + step "Step 6/7: Installing PREEMPT_RT kernel" + + local kernel_dir="$WORK_DIR/kernel/linux-rpi" + local mod_staging="$WORK_DIR/kernel/modules" + + # Copy kernel image + log "Installing kernel image..." + sudo cp "$kernel_dir/arch/arm64/boot/Image" \ + "$mount_point/boot/kernel8-rt.img" + + # Copy DTBs + log "Installing Device Tree Blobs..." + sudo cp "$kernel_dir/arch/arm64/boot/dts/broadcom/"*.dtb \ + "$mount_point/boot/" + + # Copy overlays + if [[ -d "$kernel_dir/arch/arm64/boot/dts/overlays" ]]; then + sudo mkdir -p "$mount_point/boot/overlays" + sudo cp "$kernel_dir/arch/arm64/boot/dts/overlays/"*.dtbo \ + "$mount_point/boot/overlays/" 2>/dev/null || true + fi + + # Install kernel modules + log "Installing kernel modules..." + local modules_ver + modules_ver=$(ls "$mod_staging/lib/modules/" | head -1) + sudo cp -r "$mod_staging/lib/modules/$modules_ver" \ + "$mount_point/lib/modules/" + + # Update config.txt to use RT kernel + log "Updating /boot/config.txt..." + sudo tee -a "$mount_point/boot/config.txt" >/dev/null <<'EOF' + +# PREEMPT_RT kernel +kernel=kernel8-rt.img + +EOF + + # Update cmdline.txt for RT + log "Updating /boot/cmdline.txt..." + local existing_cmdline + existing_cmdline=$(sudo cat "$mount_point/boot/cmdline.txt" 2>/dev/null || echo "") + # Remove console=serial0 if present (quieter boot) + existing_cmdline="${existing_cmdline//console=serial0,115200/}" + sudo tee "$mount_point/boot/cmdline.txt" >/dev/null <:8080" + echo " - SSH: pi@ (password: raspberry, CHANGE ON FIRST BOOT)" + echo "" +} + +# ─── Main ──────────────────────────────────────────────────────────────────── + +main() { + echo -e "${BLUE}${BOLD}" + echo "╔══════════════════════════════════════════╗" + echo "║ RPi Audio Mixer — Image Builder ║" + echo "║ Version: $MIXER_VERSION" + echo "╚══════════════════════════════════════════╝" + echo -e "${NC}" + + check_deps + setup_dirs + + # ── Kernel-only mode ── + if $KERNEL_ONLY; then + if $BUILD_KERNEL; then + build_rt_kernel + else + err "--kernel-only requires kernel build (remove --skip-kernel)" + exit 1 + fi + log "Kernel build complete. Output in: $WORK_DIR/kernel/" + log " Image: $WORK_DIR/kernel/linux-rpi/arch/arm64/boot/Image" + log " Modules: $WORK_DIR/kernel/modules/lib/modules/" + exit 0 + fi + + # ── Build RT kernel (if enabled) ── + if $BUILD_KERNEL; then + build_rt_kernel + else + log "Skipping PREEMPT_RT kernel build (--skip-kernel)" + fi + + # ── Download base image ── + local base_img + base_img=$(download_base_image) + + # ── Create new image ── + local loopdev + loopdev=$(create_image "$OUTPUT_PATH") + + local mount_point="$WORK_DIR/mnt" + mount_image "$loopdev" "$mount_point" + + # ── Populate from base ── + populate_rootfs "$base_img" "$mount_point" + + # ── Configure system ── + configure_system "$mount_point" + + # ── Install RT kernel if built ── + if $BUILD_KERNEL; then + install_rt_kernel "$mount_point" + fi + + # ── Finalize ── + finalize_image "$mount_point" "$loopdev" "$OUTPUT_PATH" +} + +main "$@" diff --git a/build/configure-system.sh b/build/configure-system.sh new file mode 100755 index 0000000..92e4f3c --- /dev/null +++ b/build/configure-system.sh @@ -0,0 +1,416 @@ +#!/bin/bash +# configure-system.sh — runs inside chroot to configure the RPi Audio Mixer image +# Called from build.sh Step 5. + +set -euo pipefail + +# ─── Variables (populated by build.sh via env) ─────────────────────────────── + +MIXER_HOSTNAME="${MIXER_HOSTNAME:-pi-mixer}" +MIXER_API_KEY="${MIXER_API_KEY:-mixer-local}" +MIXER_WIFI_SSID="${MIXER_WIFI_SSID:-}" +MIXER_WIFI_PASS="${MIXER_WIFI_PASS:-}" +MIXER_VERSION="${MIXER_VERSION:-dev}" + +export DEBIAN_FRONTEND=noninteractive + +log() { echo "[CONFIGURE] $*"; } + +# ─── 1. Basic system configuration ─────────────────────────────────────────── + +log "1/8: Configuring hostname and locale..." +echo "$MIXER_HOSTNAME" > /etc/hostname +sed -i "s/raspberrypi/$MIXER_HOSTNAME/g" /etc/hosts 2>/dev/null || true + +# Enable required locales +sed -i 's/^# en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen 2>/dev/null || true +locale-gen en_US.UTF-8 2>/dev/null || true +update-locale LANG=en_US.UTF-8 2>/dev/null || true + +# ─── 2. Update package cache ───────────────────────────────────────────────── + +log "2/8: Updating package cache..." +apt-get update -qq + +# ─── 3. Install system packages ────────────────────────────────────────────── + +log "3/8: Installing audio and system packages..." +apt-get install -y -qq \ + jackd2 \ + jackd2-firewire \ + libjack-jackd2-0 \ + libjack-jackd2-dev \ + jack-tools \ + a2jmidid \ + alsa-utils \ + alsa-tools \ + pulseaudio \ + pulseaudio-module-jack \ + rtirq-init \ + cpufrequtils \ + i2c-tools \ + python3 \ + python3-pip \ + python3-venv \ + python3-dev \ + python3-numpy \ + python3-scipy \ + libpython3-dev \ + git \ + wget \ + curl \ + vim-tiny \ + tmux \ + sudo \ + ufw \ + ntp \ + iptables \ + usbutils \ + pciutils \ + net-tools \ + wireless-tools \ + wpasupplicant \ + avahi-daemon \ + libusb-1.0-0-dev \ + libasound2-dev \ + libsndfile1-dev \ + liblo-dev \ + libfftw3-dev \ + libcairo2-dev \ + libgirepository1.0-dev \ + gir1.2-gtk-3.0 \ + xserver-xorg \ + x11-xserver-utils \ + xinit \ + libsdl2-dev \ + libsdl2-image-dev \ + libsdl2-mixer-dev \ + libsdl2-ttf-dev \ + mesa-utils \ + libgles2-mesa-dev \ + pkg-config + +log " Cleaning apt cache..." +apt-get clean +rm -rf /var/lib/apt/lists/* + +# ─── 4. Install Python packages ────────────────────────────────────────────── + +log "4/8: Installing Python packages..." +# Create virtual environment for the mixer +python3 -m venv /opt/rpi-mixer/venv --system-site-packages +/opt/rpi-mixer/venv/bin/pip install --no-cache-dir --upgrade pip setuptools wheel + +/opt/rpi-mixer/venv/bin/pip install --no-cache-dir \ + fastapi \ + uvicorn \ + websockets \ + python-multipart \ + pydantic \ + pytest \ + pytest-asyncio \ + pytest-cov \ + python-jack-client \ + python-rtmidi \ + pyyaml \ + aiohttp 2>/dev/null || true + +# ─── 5. Create audio user/group ────────────────────────────────────────────── + +log "5/8: Creating 'pi' user and 'audio' group..." +# The 'pi' user already exists in RPiOS, add to audio group +usermod -a -G audio pi 2>/dev/null || true +usermod -a -G input pi 2>/dev/null || true # For evdev keyboard controls +usermod -a -G video pi 2>/dev/null || true # For framebuffer/Kivy + +# Set real-time priorities for audio group +cat > /etc/security/limits.d/99-audio.conf <<'EOF' +# Realtime audio group limits for low-latency JACK +@audio - rtprio 99 +@audio - memlock unlimited +@audio - nice -20 +@audio - priority 99 +EOF + +# ─── 6. Install systemd services ───────────────────────────────────────────── + +log "6/8: Installing systemd services..." + +# CPU performance governor +cat > /etc/systemd/system/cpu-performance.service <<'EOF' +[Unit] +Description=Set CPU governor to performance +After=multi-user.target + +[Service] +Type=oneshot +ExecStart=/bin/sh -c 'for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do [ -f "$cpu" ] && echo performance > "$cpu" || true; done' +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +EOF + +# JACK2 audio server +cat > /etc/systemd/system/jackd.service <<'EOF' +[Unit] +Description=JACK2 Audio Server +After=sound.target network.target cpu-performance.service +Wants=sound.target cpu-performance.service + +[Service] +Type=simple +User=pi +Group=audio +Environment=JACK_NO_AUDIO_RESERVATION=1 +Environment=JACK_PROMISCUOUS_SERVER=1 + +LimitRTPRIO=99 +LimitMEMLOCK=infinity +LimitNICE=-20 + +CPUAffinity=1-3 +IOSchedulingClass=realtime +IOSchedulingPriority=0 + +ExecStart=/usr/bin/jackd -P 70 -t 2000 -d alsa -d hw:USB -r 48000 -p 128 -n 3 -M -X seq -S + +Restart=on-failure +RestartSec=2 + +[Install] +WantedBy=multi-user.target +EOF + +# Mixer API server +cat > /etc/systemd/system/mixer-api.service <<'EOF' +[Unit] +Description=RPi Audio Mixer — Network API Server +After=jackd.service network.target +Wants=jackd.service +Requires=jackd.service + +[Service] +Type=simple +User=pi +Group=audio +WorkingDirectory=/opt/rpi-mixer +ExecStart=/opt/rpi-mixer/venv/bin/python3 -m src.network.run --host 0.0.0.0 --port 8080 --api-key MIXER_API_KEY_PLACEHOLDER + +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF + +# Replace API key placeholder +sed -i "s/MIXER_API_KEY_PLACEHOLDER/$MIXER_API_KEY/" /etc/systemd/system/mixer-api.service + +# Mixer Kivy touch UI +cat > /etc/systemd/system/mixer-ui.service <<'EOF' +[Unit] +Description=RPi Audio Mixer — Touchscreen UI +After=mixer-api.service +Wants=mixer-api.service + +[Service] +Type=simple +User=pi +Group=audio +WorkingDirectory=/opt/rpi-mixer +Environment=MIXER_HOST=127.0.0.1 +Environment=MIXER_PORT=8080 +Environment=MIXER_API_KEY=MIXER_API_KEY_PLACEHOLDER +ExecStart=/opt/rpi-mixer/venv/bin/python3 main_touch.py --host 127.0.0.1 --port 8080 --api-key MIXER_API_KEY_PLACEHOLDER +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF + +sed -i "s/MIXER_API_KEY_PLACEHOLDER/$MIXER_API_KEY/g" /etc/systemd/system/mixer-ui.service + +# First-boot setup wizard +cat > /etc/systemd/system/mixer-firstboot.service <<'EOF' +[Unit] +Description=RPi Audio Mixer — First-Boot Setup Wizard +After=network.target +Before=getty@tty1.service +ConditionPathExists=/opt/rpi-mixer/build/first-boot/setup-wizard.sh + +[Service] +Type=oneshot +ExecStart=/bin/bash /opt/rpi-mixer/build/first-boot/setup-wizard.sh +StandardInput=tty +StandardOutput=tty +TTYPath=/dev/tty1 +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +EOF + +# ─── 7. Install config files and udev rules ────────────────────────────────── + +log "7/8: Installing config files..." + +# udev rule for MIDI devices +cp /opt/rpi-mixer/config/99-midi.rules /etc/udev/rules.d/ 2>/dev/null || true + +# rtirq config +cp /opt/rpi-mixer/config/rtirq /etc/default/rtirq 2>/dev/null || true + +# ALSA config for USB audio +cp /opt/rpi-mixer/config/alsa-usb.conf /etc/modprobe.d/ 2>/dev/null || true + +# USB audio quirks +cp /opt/rpi-mixer/config/usb-audio-quirks.conf /etc/modprobe.d/ 2>/dev/null || true + +# JACK config files +mkdir -p /etc/jack +cp /opt/rpi-mixer/config/jackdrc /etc/jackdrc 2>/dev/null || true + +# Carla config files +mkdir -p /home/pi/.config/carla +cp /opt/rpi-mixer/config/carla-8ch-default.carxp /home/pi/.config/carla/ 2>/dev/null || true +cp /opt/rpi-mixer/config/carla-patchbay.xml /home/pi/.config/carla/ 2>/dev/null || true +chown -R pi:pi /home/pi/.config/carla 2>/dev/null || true + +# ─── 8. Enable services and finalize ───────────────────────────────────────── + +log "8/8: Enabling services..." + +# Enable boot-time services +systemctl enable cpu-performance.service 2>/dev/null || true + +# Don't enable jackd yet — it needs audio interface detection from first-boot +# (the first-boot wizard handles this) + +# Enable mixer services (will start after first-boot completes) +systemctl enable mixer-firstboot.service 2>/dev/null || true + +# Configure first-boot wizard to auto-disable itself +# (runs once, then disables itself) + +# Make setup-wizard executable +chmod +x /opt/rpi-mixer/build/first-boot/setup-wizard.sh 2>/dev/null || true + +# Create /data directory structure for persistence +mkdir -p /data/{sessions,recordings,presets,logs,plugins} +chown -R pi:pi /data 2>/dev/null || true + +# Symlink data directories into pi home +ln -sf /data/sessions /home/pi/sessions 2>/dev/null || true +ln -sf /data/recordings /home/pi/recordings 2>/dev/null || true +ln -sf /data/presets /home/pi/presets 2>/dev/null || true +chown -h pi:pi /home/pi/{sessions,recordings,presets} 2>/dev/null || true + +# Set bashrc with helpful aliases +cat >> /home/pi/.bashrc <<'EOF' + +# ─── RPi Audio Mixer aliases ─── +alias mixer-status='systemctl status jackd mixer-api mixer-ui' +alias mixer-start='sudo systemctl start jackd && sleep 2 && sudo systemctl start mixer-api' +alias mixer-stop='sudo systemctl stop mixer-ui mixer-api jackd' +alias mixer-restart='sudo systemctl restart mixer-api' +alias mixer-log='journalctl -u mixer-api -f' +alias jack-log='journalctl -u jackd -f' +alias jack-ports='jack_lsp -c' +alias jack-load='jack_cpu_load' +alias audio-test='speaker-test -t sine -f 440 -l 0' +alias mixer-build='/opt/rpi-mixer/build/build.sh' +EOF + +chown pi:pi /home/pi/.bashrc 2>/dev/null || true + +# Disable unnecessary services for appliance mode +log " Disabling non-essential services..." +systemctl disable bluetooth 2>/dev/null || true +systemctl disable hciuart 2>/dev/null || true +systemctl disable triggerhappy 2>/dev/null || true +systemctl disable ModemManager 2>/dev/null || true +systemctl mask ModemManager 2>/dev/null || true +systemctl disable polkit 2>/dev/null || true +systemctl disable dphys-swapfile 2>/dev/null || true # No swap on SD card + +# Configure NTP +cat > /etc/systemd/timesyncd.conf <<'EOF' +[Time] +NTP=0.pool.ntp.org 1.pool.ntp.org 2.pool.ntp.org +FallbackNTP=0.debian.pool.ntp.org +EOF + +# Configure SSH to start on boot +systemctl enable ssh 2>/dev/null || true + +# Enable Avahi for mDNS (pi-mixer.local) +systemctl enable avahi-daemon 2>/dev/null || true + +# ─── 9. Boot config ────────────────────────────────────────────────────────── + +log "Finalizing boot config..." + +# config.txt adjustments +cat >> /boot/config.txt <<'EOF' + +# RPi Audio Mixer — audio appliance optimizations +# Disable WiFi/BT for reduced latency (enable via setup wizard if needed) +dtoverlay=disable-wifi +dtoverlay=disable-bt + +# Fast boot +boot_delay=0 +disable_splash=1 + +# GPU memory (minimal — no desktop) +gpu_mem=16 + +# Force HDMI hotplug (for touchscreen) +hdmi_force_hotplug=1 + +# Disable camera auto-detect (we use USB cameras) +camera_auto_detect=0 + +# Enable I2C for GPIO expanders +dtparam=i2c_arm=on + +# Disable audio (we use USB audio interface) +dtparam=audio=off +EOF + +# cmdline.txt adjustments +# Keep existing and append +sed -i 's/console=serial0,115200 //g' /boot/cmdline.txt 2>/dev/null || true +echo " quiet loglevel=3 usbcore.autosuspend=-1 fsck.mode=skip" >> /boot/cmdline.txt + +# ─── 10. WiFi pre-configuration (optional) ─────────────────────────────────── + +if [[ -n "$MIXER_WIFI_SSID" ]]; then + log "Pre-configuring WiFi..." + + # Remove WiFi disable overlay + sed -i 's/^dtoverlay=disable-wifi/#dtoverlay=disable-wifi/' /boot/config.txt 2>/dev/null || true + + cat > /etc/wpa_supplicant/wpa_supplicant.conf < /etc/mixer-version +echo "Installed: $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> /etc/mixer-version + +log "System configuration complete." diff --git a/build/first-boot/setup-wizard.sh b/build/first-boot/setup-wizard.sh new file mode 100755 index 0000000..a4fd24f --- /dev/null +++ b/build/first-boot/setup-wizard.sh @@ -0,0 +1,344 @@ +#!/bin/bash +# setup-wizard.sh — First-boot configuration wizard for RPi Audio Mixer +# +# Runs on first boot (tty1) before getty starts. Guides the user through: +# 1. Audio interface detection and selection +# 2. WiFi configuration +# 3. Hostname setting +# 4. API key generation +# 5. JACK device configuration +# +# After completion, the wizard disables itself, enables the mixer services, +# and reboots into normal operation. + +set -euo pipefail + +# ─── Colours ───────────────────────────────────────────────────────────────── + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +# ─── Configuration paths ──────────────────────────────────────────────────── + +JACKD_SERVICE="/etc/systemd/system/jackd.service" +MIXER_API_SERVICE="/etc/systemd/system/mixer-api.service" +MIXER_UI_SERVICE="/etc/systemd/system/mixer-ui.service" +CONFIG_TXT="/boot/config.txt" +CMDLINE_TXT="/boot/cmdline.txt" +WPA_CONF="/etc/wpa_supplicant/wpa_supplicant.conf" +HOSTNAME_FILE="/etc/hostname" +HOSTS_FILE="/etc/hosts" + +# ─── Helper functions ──────────────────────────────────────────────────────── + +header() { + clear + echo -e "${BLUE}${BOLD}" + echo "╔══════════════════════════════════════════╗" + echo "║ RPi Audio Mixer — First-Boot Setup ║" + echo "║ $1" + echo "╚══════════════════════════════════════════╝" + echo -e "${NC}" + echo "" +} + +prompt() { + local var_name="$1" + local default="$2" + local question="$3" + local value + + echo -en "${GREEN}${question}${NC} [${default}]: " + read -r value + value="${value:-$default}" + eval "$var_name=\"$value\"" +} + +prompt_yn() { + local question="$1" + local default="${2:-y}" + local answer + + while true; do + echo -en "${GREEN}${question}${NC} (y/n) [${default}]: " + read -r answer + answer="${answer:-$default}" + case "$answer" in + [Yy]*) return 0 ;; + [Nn]*) return 1 ;; + *) echo "Please answer y or n" ;; + esac + done +} + +press_enter() { + echo "" + echo -n "Press Enter to continue..." + read -r +} + +# ─── 1. Welcome ────────────────────────────────────────────────────────────── + +header "Welcome" + +echo "This wizard will configure your Raspberry Pi Audio Mixer." +echo "You'll need:" +echo " - USB audio interface connected" +echo " - (Optional) WiFi network name and password" +echo " - (Optional) Touchscreen connected via HDMI+USB" +echo "" +echo "This only runs once. Settings can be changed later via:" +echo " /opt/rpi-mixer/config/ — audio/jack/carla configs" +echo " raspi-config — system settings" +echo "" +press_enter + +# ─── 2. Audio Interface Detection ──────────────────────────────────────────── + +header "Step 1/5: Audio Interface" + +echo "Detecting USB audio interfaces..." +echo "" + +# List sound cards +ALSA_CARDS="" +if command -v aplay >/dev/null 2>&1; then + ALSA_CARDS=$(aplay -l 2>/dev/null | grep -E '^card [0-9]' || true) +fi + +if [[ -z "$ALSA_CARDS" ]]; then + echo -e "${YELLOW}No sound cards detected!${NC}" + echo "" + echo "Make sure your USB audio interface is connected." + echo "You can continue anyway and configure later." + echo "" + + if prompt_yn "Continue without audio interface?" "y"; then + echo "Skipping audio interface config." + else + echo "Please connect your audio interface and reboot." + echo "The setup wizard will run again on next boot." + exit 0 + fi +else + echo "Found sound cards:" + echo "$ALSA_CARDS" + echo "" + + # Detect USB audio card + USB_CARD="" + if aplay -l 2>/dev/null | grep -qi "USB"; then + USB_CARD=$(aplay -l 2>/dev/null | grep -i "USB" | head -1 | awk '{print $2}' | tr -d ':') + echo -e "${GREEN}USB audio interface detected: card $USB_CARD${NC}" + else + echo -e "${YELLOW}No USB audio card found. Using first available card.${NC}" + USB_CARD=$(echo "$ALSA_CARDS" | head -1 | awk '{print $2}' | tr -d ':') + fi + + echo "" + prompt AUDIO_DEV "hw:USB" "ALSA device name for JACK?" + + # Update JACK service with detected device + sed -i "s/-d hw:USB/-d $AUDIO_DEV/" "$JACKD_SERVICE" 2>/dev/null || true + + # Also update jackdrc + mkdir -p /etc/jack + sed -i "s/-d hw:USB/-d $AUDIO_DEV/" /etc/jackdrc 2>/dev/null || true + echo "/usr/bin/jackd -P 70 -t 2000 -d alsa -d $AUDIO_DEV -r 48000 -p 128 -n 3 -M -X seq" > /etc/jackdrc +fi + +press_enter + +# ─── 3. WiFi Configuration ─────────────────────────────────────────────────── + +header "Step 2/5: Network" + +# Check if Ethernet is connected +ETH_CONNECTED=false +if ip link show eth0 2>/dev/null | grep -q "state UP"; then + ETH_CONNECTED=true + echo -e "${GREEN}Ethernet connected.${NC}" + echo "" +fi + +if prompt_yn "Configure WiFi?" "n"; then + prompt WIFI_SSID "" "WiFi network name (SSID)" + echo -n "WiFi password: " + read -s WIFI_PASS + echo "" + + # Remove WiFi disable from config.txt + sed -i 's/^dtoverlay=disable-wifi/#dtoverlay=disable-wifi/' "$CONFIG_TXT" 2>/dev/null || true + + # Write wpa_supplicant config + cat > "$WPA_CONF" </dev/null || cat "$HOSTNAME_FILE" 2>/dev/null || echo "pi-mixer") +prompt NEW_HOSTNAME "$CURRENT_HOSTNAME" "System hostname" + +if [[ "$NEW_HOSTNAME" != "$CURRENT_HOSTNAME" ]]; then + echo "$NEW_HOSTNAME" > "$HOSTNAME_FILE" + sed -i "s/$CURRENT_HOSTNAME/$NEW_HOSTNAME/g" "$HOSTS_FILE" 2>/dev/null || true + hostname "$NEW_HOSTNAME" 2>/dev/null || true + echo -e "${GREEN}Hostname set to: $NEW_HOSTNAME${NC}" + echo " Local access: http://${NEW_HOSTNAME}.local:8080" +fi + +press_enter + +# ─── 5. API Key ────────────────────────────────────────────────────────────── + +header "Step 4/5: Security" + +CURRENT_API_KEY=$(grep "MIXER_API_KEY" "$MIXER_API_SERVICE" 2>/dev/null | grep -oP 'MIXER_API_KEY=\K[^"]+' | head -1 || echo "") + +if [[ -z "$CURRENT_API_KEY" ]] || [[ "$CURRENT_API_KEY" == "MIXER_API_KEY_PLACEHOLDER" ]] || [[ "$CURRENT_API_KEY" == "mixer-local" ]]; then + NEW_API_KEY=$(head -c 24 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 24) +else + NEW_API_KEY="$CURRENT_API_KEY" +fi + +echo "API key for the mixer web UI and REST API:" +echo -e " ${BOLD}${NEW_API_KEY}${NC}" +echo "" +echo "You'll need this to access the web interface from another device." +echo "Keep it safe. It can be changed in:" +echo " $MIXER_API_SERVICE" +echo "" + +if prompt_yn "Use a custom API key?" "n"; then + prompt NEW_API_KEY "$NEW_API_KEY" "API key (no spaces)" +fi + +# Update API key in service files +sed -i "s/--api-key [^ ]*/--api-key $NEW_API_KEY/" "$MIXER_API_SERVICE" 2>/dev/null || true +sed -i "s/--api-key [^ ]*/--api-key $NEW_API_KEY/" "$MIXER_UI_SERVICE" 2>/dev/null || true +sed -i "s/MIXER_API_KEY=[^ ]*/MIXER_API_KEY=$NEW_API_KEY/" "$MIXER_UI_SERVICE" 2>/dev/null || true + +# Also update the env variable export line +sed -i "/^ExecStart.*mixer-api/,/^$/s/MIXER_API_KEY_PLACEHOLDER/$NEW_API_KEY/" "$MIXER_API_SERVICE" 2>/dev/null || true + +echo -e "${GREEN}API key updated.${NC}" + +press_enter + +# ─── 6. JACK Configuration ─────────────────────────────────────────────────── + +header "Step 5/5: Audio Settings" + +echo "JACK audio server settings:" +echo "" + +prompt SAMPLE_RATE "48000" "Sample rate (44100/48000/96000)" +prompt BUFFER_SIZE "128" "Buffer size (64/128/256/512 — lower = less latency)" +prompt PERIODS "3" "Periods per buffer (2 or 3)" + +# Update JACK service +sed -i "s/-r [0-9]*/-r $SAMPLE_RATE/" "$JACKD_SERVICE" 2>/dev/null || true +sed -i "s/-p [0-9]*/-p $BUFFER_SIZE/" "$JACKD_SERVICE" 2>/dev/null || true +sed -i "s/-n [0-9]*/-n $PERIODS/" "$JACKD_SERVICE" 2>/dev/null || true + +# Calculate approximate latency +LATENCY_MS=$(echo "scale=1; ($BUFFER_SIZE * $PERIODS) / ($SAMPLE_RATE / 1000)" | bc 2>/dev/null || echo "?") +echo "" +echo -e "${GREEN}JACK configured:${NC}" +echo " Sample rate: $SAMPLE_RATE Hz" +echo " Buffer size: $BUFFER_SIZE frames" +echo " Periods: $PERIODS" +echo " Latency: ~${LATENCY_MS}ms round-trip" +echo "" +echo "Lower latency requires more CPU. If you hear xruns (clicks/pops)," +echo "increase buffer size or periods in: $JACKD_SERVICE" + +press_enter + +# ─── 7. Touchscreen Check ──────────────────────────────────────────────────── + +header "Touchscreen (optional)" + +echo "If a touchscreen is connected via HDMI+USB, the Kivy touch UI" +echo "will start automatically on boot." +echo "" + +if prompt_yn "Enable touchscreen UI on boot?" "y"; then + systemctl enable mixer-ui.service 2>/dev/null || true + echo -e "${GREEN}Touchscreen UI enabled.${NC}" + + # Check HDMI connection + if tvservice -s 2>/dev/null | grep -q "HDMI"; then + echo " HDMI display detected." + else + echo -e " ${YELLOW}HDMI display not detected. UI may not be visible.${NC}" + fi +else + systemctl disable mixer-ui.service 2>/dev/null || true + echo "Touchscreen UI disabled." + echo "Start manually: sudo systemctl start mixer-ui" +fi + +press_enter + +# ─── 8. Finalize ───────────────────────────────────────────────────────────── + +header "Setup Complete" + +echo "Configuration summary:" +echo " Hostname: $(cat $HOSTNAME_FILE 2>/dev/null || echo unknown)" +echo " Audio device: ${AUDIO_DEV:-hw:USB}" +echo " JACK: ${SAMPLE_RATE:-48000}Hz / ${BUFFER_SIZE:-128}frames / ${PERIODS:-3}periods" +echo " API key: ${NEW_API_KEY:-mixer-local}" +echo " Web UI: http://$(cat $HOSTNAME_FILE 2>/dev/null || echo pi-mixer).local:8080" +echo " SSH: ssh pi@$(cat $HOSTNAME_FILE 2>/dev/null || echo pi-mixer).local" +echo "" + +# Enable services +log "Enabling mixer services..." +systemctl enable jackd.service 2>/dev/null || true +systemctl enable mixer-api.service 2>/dev/null || true +systemctl daemon-reload 2>/dev/null || true + +# Disable this wizard +log "Disabling first-boot wizard..." +systemctl disable mixer-firstboot.service 2>/dev/null || true + +# Write setup-complete marker +date -u +%Y-%m-%dT%H:%M:%SZ > /etc/mixer-setup-complete + +echo -e "${GREEN}${BOLD}Rebooting in 5 seconds...${NC}" +echo "" +echo "After reboot, the mixer will start automatically." +echo " Web UI: http://$(cat $HOSTNAME_FILE 2>/dev/null || echo pi-mixer).local:8080" +echo " API Key: ${NEW_API_KEY:-mixer-local}" +echo "" + +sleep 5 +reboot