P7-R1: Build Script / SD Card Image — automated build system
Lint & Validate / lint (push) Has been cancelled
Lint & Validate / lint (push) Has been cancelled
Adds complete SD card image builder for the RPi Audio Mixer: - build/build.sh: Main builder (RPiOS Lite base + chroot config + optional RT kernel cross-compile) - build/configure-system.sh: Chroot configuration (packages, services, audio config) - build/first-boot/setup-wizard.sh: Interactive first-boot setup (audio, WiFi, hostname, API key, JACK) - build/README.md: Flashing instructions and build documentation - Updated .gitignore to track build scripts but ignore artifacts - Updated README.md with current project status 735/735 tests pass. Build runs on x86_64 without a Raspberry Pi.
This commit is contained in:
+9
-1
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+247
@@ -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`
|
||||
Executable
+607
@@ -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-<version>.img SD card image
|
||||
# build/out/rpi-audio-mixer-<version>.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 <<EOF
|
||||
${BOLD}Usage:${NC} $0 [OPTIONS]
|
||||
|
||||
Build a ready-to-flash Raspberry Pi 4B SD card image for the RPi Audio Mixer.
|
||||
|
||||
${BOLD}Options:${NC}
|
||||
--skip-kernel Skip PREEMPT_RT kernel build (use stock RPiOS kernel)
|
||||
--kernel-only Only build the RT kernel, skip image creation
|
||||
--output PATH Output image path (default: build/out/rpi-audio-mixer-<version>.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-<version>.img Raw SD card image
|
||||
build/out/rpi-audio-mixer-<version>.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 <<FDISK | fdisk "$img_file" >/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 <<CMDLINE
|
||||
${existing_cmdline} quiet loglevel=3 threadirqs nohz_full=1-3 rcu_nocbs=1-3 isolcpus=1-3 usbcore.autosuspend=-1 fsck.mode=skip
|
||||
CMDLINE
|
||||
}
|
||||
|
||||
# ─── Step 7: Finalize and compress ───────────────────────────────────────────
|
||||
|
||||
finalize_image() {
|
||||
local mount_point="$1"
|
||||
local loopdev="$2"
|
||||
local img_file="$3"
|
||||
|
||||
step "Step 7/7: Finalizing image"
|
||||
|
||||
# Unmount
|
||||
unmount_image "$mount_point" "$loopdev"
|
||||
|
||||
# Compress
|
||||
log "Compressing image..."
|
||||
xz -3 -f -T0 -v "$img_file"
|
||||
log "Compressed image: ${img_file}.xz"
|
||||
|
||||
# Report sizes
|
||||
local raw_size compressed_size
|
||||
raw_size=$(du -h "$img_file" | cut -f1)
|
||||
compressed_size=$(du -h "${img_file}.xz" | cut -f1)
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}${BOLD}║ Build Complete ║${NC}"
|
||||
echo -e "${GREEN}${BOLD}╚══════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo " Version: $MIXER_VERSION"
|
||||
echo " Image: $img_file ($raw_size)"
|
||||
echo " Compressed: ${img_file}.xz ($compressed_size)"
|
||||
echo ""
|
||||
if $BUILD_KERNEL; then
|
||||
echo " Kernel: PREEMPT_RT (cross-compiled)"
|
||||
else
|
||||
echo " Kernel: Stock RPiOS (PREEMPT_RT not included)"
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${BOLD}To flash to SD card:${NC}"
|
||||
echo " sudo dd if=$img_file of=/dev/sdX bs=4M status=progress conv=fsync"
|
||||
echo ""
|
||||
echo -e "${BOLD}First boot:${NC}"
|
||||
echo " - The setup wizard will guide you through configuration"
|
||||
echo " - Default hostname: $MIXER_HOSTNAME"
|
||||
echo " - Default API key: $MIXER_API_KEY"
|
||||
echo " - Web UI: http://<pi-ip>:8080"
|
||||
echo " - SSH: pi@<pi-ip> (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 "$@"
|
||||
Executable
+416
@@ -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 <<WIFIEOF
|
||||
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||
update_config=1
|
||||
country=US
|
||||
|
||||
network={
|
||||
ssid="$MIXER_WIFI_SSID"
|
||||
psk="$MIXER_WIFI_PASS"
|
||||
key_mgmt=WPA-PSK
|
||||
}
|
||||
WIFIEOF
|
||||
chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf
|
||||
fi
|
||||
|
||||
# ─── Done ────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Write version marker
|
||||
echo "rpi-audio-mixer $MIXER_VERSION" > /etc/mixer-version
|
||||
echo "Installed: $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> /etc/mixer-version
|
||||
|
||||
log "System configuration complete."
|
||||
Executable
+344
@@ -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" <<WIFIEOF
|
||||
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||
update_config=1
|
||||
country=US
|
||||
|
||||
network={
|
||||
ssid="$WIFI_SSID"
|
||||
psk="$WIFI_PASS"
|
||||
key_mgmt=WPA-PSK
|
||||
}
|
||||
WIFIEOF
|
||||
chmod 600 "$WPA_CONF"
|
||||
echo -e "${GREEN}WiFi configured.${NC}"
|
||||
else
|
||||
echo "Skipping WiFi configuration."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Network status:"
|
||||
ip -4 addr show | grep -E "inet " | grep -v "127.0.0.1" | sed 's/^/ /' || echo " No network interfaces active"
|
||||
|
||||
press_enter
|
||||
|
||||
# ─── 4. Hostname ─────────────────────────────────────────────────────────────
|
||||
|
||||
header "Step 3/5: Hostname"
|
||||
|
||||
CURRENT_HOSTNAME=$(hostname 2>/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
|
||||
Reference in New Issue
Block a user