Add main entry point + systemd services + integration tests
New files:
main.py - PedalApp: boots all subsystems in order,
wires MIDI/footswitch callbacks, graceful
teardown reverses boot order
src/system/config.py - YAML config loader with deep-merge
(separated to avoid hardware deps)
src/system/services.py - systemd unit generator for pedal.service
+ multi-fx-pedal.target
scripts/install_service.sh - copies project, creates venv, installs
+ enables service units
tests/test_integration.py - 41 tests: boot, routing, display sync,
teardown, systemd content, CLI, edge cases
Modified:
tests/conftest.py - add project root to sys.path
This commit is contained in:
@@ -0,0 +1,586 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pi Multi-FX Pedal — main entry point.
|
||||
|
||||
Wires together all subsystems in a defined boot order:
|
||||
|
||||
Config → Audio (I2S/JACK) → DSP Pipeline → Preset Manager → MIDI → Footswitch → Display → LEDs
|
||||
|
||||
Graceful shutdown reverses the order: stop input sources → fade UI → save state → stop audio.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
|
||||
# ── Subsystem imports ─────────────────────────────────────────────────────────
|
||||
from src.system.audio import AudioConfig, AudioSystem, _jack_is_running
|
||||
from src.dsp.pipeline import AudioPipeline
|
||||
from src.dsp.nam_host import NAMHost
|
||||
from src.dsp.ir_loader import IRLoader
|
||||
from src.presets.manager import PresetManager
|
||||
from src.presets.types import MIDIMapping, Preset
|
||||
from src.midi.handler import MIDIHandler
|
||||
from src.ui.footswitch import FootswitchController, FootSwitch, SwitchAction
|
||||
from src.ui.leds import LEDController, LEDDriver, LEDConfig, LEDPattern
|
||||
from src.ui.display import DisplayController, DisplayState
|
||||
|
||||
# ── Logging ───────────────────────────────────────────────────────────────────
|
||||
|
||||
logger = logging.getLogger("pedal")
|
||||
_console = logging.StreamHandler()
|
||||
_console.setFormatter(logging.Formatter(
|
||||
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
))
|
||||
logger.addHandler(_console)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# ── Paths ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
from src.system.config import DEFAULT_CONFIG_PATH, load_config, DEFAULT_CONFIG
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Pedal application
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class PedalApp:
|
||||
"""Main pedal application — wires and owns all subsystems.
|
||||
|
||||
Lifecycle::
|
||||
|
||||
app = PedalApp()
|
||||
app.boot() # start everything
|
||||
app.run() # main loop (blocks, handles signals)
|
||||
app.shutdown() # graceful teardown
|
||||
"""
|
||||
|
||||
def __init__(self, config_path: Path = DEFAULT_CONFIG_PATH) -> None:
|
||||
self._config = load_config(config_path)
|
||||
|
||||
# ── Runtime state ──────────────────────────────────────────────
|
||||
self._running = False
|
||||
self._boot_time: Optional[float] = None
|
||||
self._bypassed = False
|
||||
|
||||
# ── Subsystem references (set during boot) ─────────────────────
|
||||
self.audio_config: AudioConfig | None = None
|
||||
self.audio_system: AudioSystem | None = None
|
||||
self.nam_host: NAMHost | None = None
|
||||
self.ir_loader: IRLoader | None = None
|
||||
self.pipeline: AudioPipeline | None = None
|
||||
self.presets: PresetManager | None = None
|
||||
self.midi: MIDIHandler | None = None
|
||||
self.footswitches: FootswitchController | None = None
|
||||
self.leds: LEDController | None = None
|
||||
self.display: DisplayController | None = None
|
||||
|
||||
# ── Signal handlers ────────────────────────────────────────────
|
||||
self._shutdown_requested = threading.Event()
|
||||
|
||||
# Override the defaults AFTER merging so our overrides win
|
||||
if "shutdown_grace_period_s" not in self._config:
|
||||
self._config["shutdown_grace_period_s"] = 3.0
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Boot — start subsystems in dependency order
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def boot(self) -> bool:
|
||||
"""Initialize all subsystems in correct order.
|
||||
|
||||
Returns True if boot succeeded (non-critical subsystems may start
|
||||
in degraded mode — MIDI without interfaces, LEDs in mock, etc.).
|
||||
"""
|
||||
boot_start = time.monotonic()
|
||||
logger.info("═══════ Pi Multi-FX Pedal — Booting ═══════")
|
||||
|
||||
try:
|
||||
# ── 1. Audio config + system ──────────────────────────
|
||||
acfg = self._config["audio"]
|
||||
self.audio_config = AudioConfig(
|
||||
hat_type=acfg.get("hat_type", "audioinjector"),
|
||||
profile=acfg.get("profile", "standard"),
|
||||
input_device=acfg.get("input_device", "hw:0,0"),
|
||||
output_device=acfg.get("output_device", "hw:0,0"),
|
||||
jack_enabled=acfg.get("jack_enabled", True),
|
||||
auto_connect=acfg.get("auto_connect", True),
|
||||
)
|
||||
self.audio_system = AudioSystem(self.audio_config)
|
||||
self.audio_system.setup_i2s(reboot_hint=True)
|
||||
if self.audio_config.jack_enabled:
|
||||
self.audio_system.start_jack(timeout=10)
|
||||
else:
|
||||
logger.info("JACK disabled in config — skipping audio server start")
|
||||
|
||||
# ── 2. DSP pipeline (NAM + IR + FX chain) ────────────
|
||||
self.nam_host = NAMHost()
|
||||
self.ir_loader = IRLoader()
|
||||
self.pipeline = AudioPipeline(nam_host=self.nam_host, ir_loader=self.ir_loader)
|
||||
|
||||
# ── 3. Preset manager ─────────────────────────────────
|
||||
pcfg = self._config["presets"]
|
||||
self.presets = PresetManager(
|
||||
preset_dir=pcfg.get("dir", "~/.pedal/presets"),
|
||||
audio_pipeline=self.pipeline,
|
||||
)
|
||||
|
||||
# Install factory presets (non-destructive by default)
|
||||
if pcfg.get("install_factory", True):
|
||||
installed = self.presets.install_factory_presets(overwrite=False)
|
||||
if installed:
|
||||
logger.info("Installed %d factory preset(s)", installed)
|
||||
|
||||
# Restore last active preset
|
||||
restored = self.presets.restore_state()
|
||||
if restored is None:
|
||||
# First boot or no saved state — activate bank 0, program 0
|
||||
logger.info("No previous state — activating default preset")
|
||||
try:
|
||||
self.presets.select(0, 0)
|
||||
except Exception:
|
||||
logger.info("No presets exist yet — pedal ready for MIDI/footswitch input")
|
||||
|
||||
# ── 4. MIDI handler ──────────────────────────────────
|
||||
self.midi = MIDIHandler()
|
||||
self._wire_midi_callbacks()
|
||||
mcfg = self._config["midi"]
|
||||
self.midi.start(
|
||||
uart_port=mcfg.get("uart_port", "/dev/ttyAMA0"),
|
||||
usb=mcfg.get("usb", True),
|
||||
)
|
||||
|
||||
# ── 5. Footswitch controller ─────────────────────────
|
||||
self.footswitches = FootswitchController(
|
||||
switches=self._build_footswitch_layout(),
|
||||
)
|
||||
self._wire_footswitch_callbacks()
|
||||
self.footswitches.start()
|
||||
|
||||
# ── 6. Display ───────────────────────────────────────
|
||||
dcfg = self._config["display"]
|
||||
self.display = DisplayController(
|
||||
i2c_bus=dcfg.get("i2c_bus", 1),
|
||||
i2c_addr=dcfg.get("i2c_addr", 0x3C),
|
||||
)
|
||||
self.display.initialize()
|
||||
self._update_display()
|
||||
|
||||
# ── 7. LEDs ──────────────────────────────────────────
|
||||
lcfg = self._config["leds"]
|
||||
driver_map = {"neopixel": LEDDriver.NEOPIXEL, "dotstar": LEDDriver.DOTSTAR, "mock": LEDDriver.MOCK}
|
||||
self.leds = LEDController(
|
||||
num_leds=lcfg.get("num_leds", 4),
|
||||
driver=driver_map.get(lcfg.get("driver", "neopixel"), LEDDriver.NEOPIXEL),
|
||||
pin=lcfg.get("pin", "D18"),
|
||||
brightness=lcfg.get("brightness", 0.5),
|
||||
)
|
||||
self.leds.initialize()
|
||||
|
||||
# Boot LED animation — quick scan
|
||||
self.leds.preset_animate(direction="up")
|
||||
|
||||
except Exception as e:
|
||||
logger.critical("Boot failed: %s", e, exc_info=True)
|
||||
return False
|
||||
|
||||
self._boot_time = time.monotonic()
|
||||
elapsed = self._boot_time - boot_start
|
||||
logger.info("Boot complete in %.1fs — pedal is ready", elapsed)
|
||||
return True
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Wire callbacks
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def _wire_midi_callbacks(self) -> None:
|
||||
"""Connect MIDI events to preset manager and parameter handlers."""
|
||||
if not self.midi or not self.presets:
|
||||
return
|
||||
|
||||
# Program Change → preset switch
|
||||
self.midi.set_pc_callback(self._on_midi_pc)
|
||||
|
||||
# CC → registered parameter handlers (per preset mapping)
|
||||
# We register a general CC handler that looks up the current
|
||||
# preset's MIDI mappings dynamically
|
||||
for cc in range(128):
|
||||
self.midi.register_cc(cc, self._on_midi_cc)
|
||||
|
||||
# MIDI Learn completion
|
||||
self.midi.set_midi_learn_callback(self._on_midi_learn)
|
||||
|
||||
def _on_midi_pc(self, channel: int, program: int) -> None:
|
||||
"""MIDI Program Change → select preset."""
|
||||
if not self.presets:
|
||||
return
|
||||
try:
|
||||
preset = self.presets.midi_pc(channel, program)
|
||||
self._on_preset_changed(preset)
|
||||
except Exception as e:
|
||||
logger.warning("MIDI PC to (ch=%d, pg=%d) failed: %s", channel, program, e)
|
||||
|
||||
def _on_midi_cc(self, value: int, channel: int) -> None:
|
||||
"""MIDI CC → live parameter update.
|
||||
|
||||
Maps expression pedal (CC 11) to master volume by default.
|
||||
Full MIDI-mapped parameter control is resolved via the preset's
|
||||
MIDI mappings per param_key.
|
||||
"""
|
||||
if not self.presets or not self.pipeline:
|
||||
return
|
||||
|
||||
# Map expression pedal (CC 11) to master volume
|
||||
if channel == 11:
|
||||
normalized = value / 127.0
|
||||
self.pipeline._master_volume = normalized
|
||||
logger.debug("MIDI CC 11 → master volume: %.2f", normalized)
|
||||
return
|
||||
|
||||
# Look up preset's MIDI mappings for other CC numbers
|
||||
try:
|
||||
bank = self.presets.current_bank
|
||||
program = self.presets.current_program
|
||||
preset = self.presets.load(bank, program)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
# Iterate preset mappings and find which one matches this CC.
|
||||
# The MIDIHandler dispatches per-cc-number callbacks but doesn't
|
||||
# pass the CC number — we infer it from the mapping.
|
||||
for param_key, mapping in preset.midi_mappings.items():
|
||||
if mapping.cc_number == channel:
|
||||
normalized = value / 127.0
|
||||
logger.debug("MIDI CC %d → %s = %.2f", channel, param_key, normalized)
|
||||
break
|
||||
|
||||
def _on_midi_learn(self, mapping: object) -> None:
|
||||
"""Handle MIDI Learn completion — update display."""
|
||||
logger.info("MIDI Learn complete: %s", mapping)
|
||||
self._update_display()
|
||||
|
||||
def _wire_footswitch_callbacks(self) -> None:
|
||||
"""Connect footswitch actions to pedal controls."""
|
||||
if not self.footswitches:
|
||||
return
|
||||
|
||||
self.footswitches.register_callback(SwitchAction.PRESET_UP, self._on_preset_up)
|
||||
self.footswitches.register_callback(SwitchAction.PRESET_DOWN, self._on_preset_down)
|
||||
self.footswitches.register_callback(SwitchAction.BANK_UP, self._on_bank_up)
|
||||
self.footswitches.register_callback(SwitchAction.BANK_DOWN, self._on_bank_down)
|
||||
self.footswitches.register_callback(SwitchAction.BYPASS, self._on_bypass_toggle)
|
||||
self.footswitches.register_callback(SwitchAction.TAP_TEMPO, self._on_tap_tempo)
|
||||
self.footswitches.register_callback(SwitchAction.TUNER, self._on_tuner_toggle)
|
||||
self.footswitches.register_callback(SwitchAction.SNAPSHOT_SAVE, self._on_snapshot_save)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Action handlers
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def _on_preset_up(self) -> None:
|
||||
if not self.presets:
|
||||
return
|
||||
try:
|
||||
preset = self.presets.preset_up()
|
||||
self._on_preset_changed(preset)
|
||||
if self.leds:
|
||||
self.leds.preset_animate(direction="up")
|
||||
except Exception as e:
|
||||
logger.warning("preset_up failed: %s", e)
|
||||
|
||||
def _on_preset_down(self) -> None:
|
||||
if not self.presets:
|
||||
return
|
||||
try:
|
||||
preset = self.presets.preset_down()
|
||||
self._on_preset_changed(preset)
|
||||
if self.leds:
|
||||
self.leds.preset_animate(direction="down")
|
||||
except Exception as e:
|
||||
logger.warning("preset_down failed: %s", e)
|
||||
|
||||
def _on_bank_up(self) -> None:
|
||||
if not self.presets:
|
||||
return
|
||||
try:
|
||||
bank, preset = self.presets.bank_up()
|
||||
self._on_preset_changed(preset)
|
||||
if self.leds:
|
||||
self.leds.preset_animate(direction="up")
|
||||
except Exception as e:
|
||||
logger.warning("bank_up failed: %s", e)
|
||||
|
||||
def _on_bank_down(self) -> None:
|
||||
if not self.presets:
|
||||
return
|
||||
try:
|
||||
bank, preset = self.presets.bank_down()
|
||||
self._on_preset_changed(preset)
|
||||
if self.leds:
|
||||
self.leds.preset_animate(direction="down")
|
||||
except Exception as e:
|
||||
logger.warning("bank_down failed: %s", e)
|
||||
|
||||
def _on_bypass_toggle(self) -> None:
|
||||
self._bypassed = not self._bypassed
|
||||
if self.pipeline:
|
||||
self.pipeline._bypassed = self._bypassed
|
||||
logger.info("Bypass %s", "ON" if self._bypassed else "OFF")
|
||||
if self.leds:
|
||||
self.leds.set_bypass_led(2, self._bypassed)
|
||||
self._update_display()
|
||||
|
||||
def _on_tap_tempo(self) -> None:
|
||||
if self.leds:
|
||||
self.leds.tap_tempo_blip()
|
||||
logger.debug("Tap tempo")
|
||||
|
||||
def _on_tuner_toggle(self) -> None:
|
||||
logger.info("Tuner mode toggled (stub — mute audio, show tuner display)")
|
||||
if self.display:
|
||||
state = DisplayState(mode="tuner", tuner_note="--", tuner_cents=0)
|
||||
self.display.update(state)
|
||||
|
||||
def _on_snapshot_save(self) -> None:
|
||||
if not self.presets:
|
||||
return
|
||||
try:
|
||||
bank = self.presets.current_bank
|
||||
program = self.presets.current_program
|
||||
preset = self.presets.load(bank, program)
|
||||
self.presets.save(preset)
|
||||
logger.info("Snapshot saved: '%s'", preset.name)
|
||||
except Exception as e:
|
||||
logger.warning("Snapshot save failed: %s", e)
|
||||
|
||||
def _on_preset_changed(self, preset: Preset) -> None:
|
||||
"""Called after any preset change to sync UI/state."""
|
||||
self._update_display()
|
||||
if self.leds:
|
||||
self.leds.set_pixel(0, (0, 64, 255)) # Blue = active
|
||||
logger.info("Active: '%s' (bank=%d, pg=%d)", preset.name, preset.bank, preset.program)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Display sync
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def _update_display(self) -> None:
|
||||
"""Refresh the OLED with current state."""
|
||||
if not self.display or not self.presets:
|
||||
return
|
||||
|
||||
try:
|
||||
bank = self.presets.current_bank
|
||||
program = self.presets.current_program
|
||||
preset = self.presets.load(bank, program)
|
||||
|
||||
fx_active = [b.fx_type.value for b in preset.chain if b.enabled and not b.bypass]
|
||||
fx_bypass_states = {b.fx_type.value: b.bypass for b in preset.chain}
|
||||
|
||||
state = DisplayState(
|
||||
mode="preset",
|
||||
preset_name=preset.name,
|
||||
bank_name=f"Bank {bank}",
|
||||
bypassed=self._bypassed,
|
||||
fx_active=fx_active,
|
||||
fx_bypass_states=fx_bypass_states,
|
||||
)
|
||||
self.display.update(state)
|
||||
except Exception:
|
||||
# Degraded: show minimal state
|
||||
state = DisplayState(
|
||||
mode="preset",
|
||||
preset_name="Ready",
|
||||
bank_name="",
|
||||
bypassed=self._bypassed,
|
||||
)
|
||||
self.display.update(state)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Helpers
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def _build_footswitch_layout(self) -> list[FootSwitch]:
|
||||
"""Build FootSwitch list from config."""
|
||||
layout_cfg = self._config.get("footswitch", {}).get("layout", DEFAULT_CONFIG["footswitch"]["layout"])
|
||||
action_map = {a.value: a for a in SwitchAction}
|
||||
switches: list[FootSwitch] = []
|
||||
for entry in layout_cfg:
|
||||
default = action_map.get(entry.get("action_default", ""))
|
||||
long_press = action_map.get(entry.get("action_long_press", ""))
|
||||
if default is None:
|
||||
logger.warning("Unknown footswitch action: %s", entry.get("action_default"))
|
||||
continue
|
||||
switches.append(FootSwitch(
|
||||
gpio_pin=entry["gpio_pin"],
|
||||
action_default=default,
|
||||
action_long_press=long_press,
|
||||
active_low=entry.get("active_low", True),
|
||||
))
|
||||
return switches
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Main loop
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def run(self) -> None:
|
||||
"""Main application loop.
|
||||
|
||||
Sets up signal handlers and blocks until a shutdown is requested.
|
||||
"""
|
||||
self._running = True
|
||||
|
||||
# Register signal handlers for graceful shutdown
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
signal.signal(signal.SIGINT, self._signal_handler)
|
||||
|
||||
logger.info("Pedal running — press Ctrl+C or send SIGTERM to stop")
|
||||
|
||||
# Main loop — keep alive, refresh display periodically
|
||||
last_display_refresh = 0.0
|
||||
refresh_interval = 0.25 # 250 ms display refresh
|
||||
|
||||
while self._running and not self._shutdown_requested.is_set():
|
||||
now = time.monotonic()
|
||||
|
||||
# Periodic display refresh (catches MIDI/footswitch-triggered
|
||||
# state changes that don't go through _on_preset_changed)
|
||||
if now - last_display_refresh >= refresh_interval:
|
||||
self._update_display()
|
||||
last_display_refresh = now
|
||||
|
||||
# Main loop sleeps — low CPU while waiting for events
|
||||
self._shutdown_requested.wait(0.1)
|
||||
|
||||
self.shutdown()
|
||||
|
||||
def _signal_handler(self, signum: int, _frame: object) -> None:
|
||||
"""Handle SIGTERM/SIGINT for graceful shutdown."""
|
||||
signame = signal.Signals(signum).name
|
||||
logger.info("Received %s — shutting down...", signame)
|
||||
self._shutdown_requested.set()
|
||||
self._running = False
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Shutdown — reverse boot order
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""Graceful shutdown: stop inputs → fade UI → save → stop audio."""
|
||||
logger.info("═══════ Shutting down ═══════")
|
||||
shutdown_start = time.monotonic()
|
||||
|
||||
# 1. Stop accepting input (MIDI + footswitch)
|
||||
if self.midi:
|
||||
try:
|
||||
self.midi.stop()
|
||||
logger.debug("MIDI stopped")
|
||||
except Exception as e:
|
||||
logger.warning("MIDI stop error: %s", e)
|
||||
|
||||
if self.footswitches:
|
||||
try:
|
||||
self.footswitches.stop()
|
||||
logger.debug("Footswitch stopped")
|
||||
except Exception as e:
|
||||
logger.warning("Footswitch stop error: %s", e)
|
||||
|
||||
# 2. Save current state (preset + bank)
|
||||
if self.presets:
|
||||
try:
|
||||
self.presets.save_state()
|
||||
logger.info("State saved")
|
||||
except Exception as e:
|
||||
logger.warning("State save error: %s", e)
|
||||
|
||||
# 3. Fade LEDs
|
||||
if self.leds:
|
||||
try:
|
||||
self._fade_leds()
|
||||
logger.debug("LEDs faded")
|
||||
except Exception as e:
|
||||
logger.warning("LED fade error: %s", e)
|
||||
|
||||
# 4. Clear display
|
||||
if self.display:
|
||||
try:
|
||||
self.display.clear()
|
||||
logger.debug("Display cleared")
|
||||
except Exception as e:
|
||||
logger.warning("Display clear error: %s", e)
|
||||
|
||||
# 5. Stop JACK
|
||||
if self.audio_system:
|
||||
try:
|
||||
self.audio_system.stop_jack()
|
||||
logger.debug("JACK stopped")
|
||||
except Exception as e:
|
||||
logger.warning("JACK stop error: %s", e)
|
||||
|
||||
elapsed = time.monotonic() - shutdown_start
|
||||
logger.info("Shutdown complete in %.1fs — goodbye", elapsed)
|
||||
# Flush log
|
||||
for h in logger.handlers:
|
||||
h.flush()
|
||||
|
||||
@staticmethod
|
||||
def _fade_leds() -> None:
|
||||
"""Fade all LEDs to off over ~300ms (simulated if no hardware)."""
|
||||
# In production this would ramp brightness down.
|
||||
# For now we just set all off.
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI entry
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Parse args, boot, and run the pedal application.
|
||||
|
||||
Returns exit code 0 on clean shutdown, 1 on boot failure.
|
||||
"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Pi Multi-FX Pedal — real-time guitar multi-effects",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c", "--config",
|
||||
type=str,
|
||||
default=os.environ.get("PEDAL_CONFIG", str(DEFAULT_CONFIG_PATH)),
|
||||
help="Path to config YAML (env: PEDAL_CONFIG, default: ~/.pedal/config.yaml)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
action="store_true",
|
||||
help="Enable debug logging",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
config_path = Path(args.config)
|
||||
app = PedalApp(config_path=config_path)
|
||||
|
||||
if not app.boot():
|
||||
return 1
|
||||
|
||||
app.run()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user