R1: Dashboard 500 fix, Phase 2/7 uncommitted changes

- Fix dashboard 500 when deps disconnected — _gather_state() now returns
  full defaults instead of bare {'connected': False}
- Phase 2: FX param schemas aligned with DSP implementations
- Phase 7: Pipeline routing, preset manager improvements
- Factory presets: cleanup and normalization

Fixes dashboard crash on disconnected state (dogfood issue #1)
This commit is contained in:
2026-06-12 14:02:13 -04:00
parent 1301a8a128
commit 77a757cee6
62 changed files with 24166 additions and 20 deletions
+145
View File
@@ -0,0 +1,145 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 320" font-family="Inter, -apple-system, sans-serif">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1a1a2e"/>
<stop offset="100%" stop-color="#16213e"/>
</linearGradient>
<linearGradient id="led-off" x1="0" y1="1" x2="0" y2="0">
<stop offset="0%" stop-color="#2a2a3a"/>
<stop offset="100%" stop-color="#3a3a4a"/>
</linearGradient>
<linearGradient id="led-on" x1="0" y1="1" x2="0" y2="0">
<stop offset="0%" stop-color="#00b894"/>
<stop offset="100%" stop-color="#55efc4"/>
</linearGradient>
<linearGradient id="knob" x1="0" y1="1" x2="0" y2="0">
<stop offset="0%" stop-color="#2d3436"/>
<stop offset="100%" stop-color="#636e72"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="480" height="320" rx="12" fill="url(#bg)" stroke="#2d3436" stroke-width="2"/>
<!-- ═══ Status Bar ═══ -->
<rect x="0" y="0" width="480" height="36" rx="12" fill="#0a0a1a" opacity="0.6"/>
<rect x="0" y="24" width="480" height="12" fill="#0a0a1a" opacity="0.6"/>
<!-- Status dots (FX chain indicator) -->
<circle cx="28" cy="18" r="4" fill="#55efc4"/>
<circle cx="44" cy="18" r="4" fill="#55efc4"/>
<circle cx="60" cy="18" r="4" fill="#55efc4"/>
<circle cx="76" cy="18" r="4" fill="#e17055"/>
<circle cx="92" cy="18" r="4" fill="#636e72"/>
<circle cx="108" cy="18" r="4" fill="#636e72"/>
<!-- Status labels -->
<text x="140" y="23" fill="#636e72" font-size="10">GN Ch Dr ◇ Dly Rvb</text>
<!-- Tuner / Tempo / CPU -->
<text x="320" y="23" fill="#55efc4" font-size="11" font-weight="bold">♩ 120</text>
<text x="380" y="23" fill="#636e72" font-size="10">CPU 23%</text>
<text x="440" y="23" fill="#636e72" font-size="10">▣ 89%</text>
<!-- ═══ Main Content Area ═══ -->
<!-- Preset name badge -->
<rect x="12" y="44" width="456" height="32" rx="6" fill="#0a0a1a" opacity="0.5"/>
<text x="24" y="65" fill="#dfe6e9" font-size="14" font-weight="bold">Bank 1 / 03 — Edge of Breakup</text>
<text x="380" y="65" fill="#636e72" font-size="11">4CM ◇ BYPASS</text>
<!-- ═══ FX Block Cards ═══ -->
<!-- Row 1 -->
<g transform="translate(12, 84)">
<rect width="108" height="56" rx="6" fill="#2d3436" opacity="0.9"/>
<text x="54" y="22" fill="#dfe6e9" font-size="11" font-weight="bold" text-anchor="middle">NOISE GATE</text>
<text x="54" y="38" fill="#636e72" font-size="9" text-anchor="middle">Thresh: -56dB</text>
<circle cx="90" cy="12" r="4" fill="#55efc4"/>
</g>
<g transform="translate(128, 84)">
<rect width="108" height="56" rx="6" fill="#2d3436" opacity="0.9"/>
<text x="54" y="22" fill="#dfe6e9" font-size="11" font-weight="bold" text-anchor="middle">COMPRESSOR</text>
<text x="54" y="38" fill="#636e72" font-size="9" text-anchor="middle">Ratio: 3:1</text>
<circle cx="90" cy="12" r="4" fill="#55efc4"/>
</g>
<g transform="translate(244, 84)">
<rect width="108" height="56" rx="6" fill="#2d3436" opacity="0.9"/>
<text x="54" y="22" fill="#dfe6e9" font-size="11" font-weight="bold" text-anchor="middle">OVERDRIVE</text>
<text x="54" y="38" fill="#636e72" font-size="9" text-anchor="middle">Drive: 68%</text>
<circle cx="90" cy="12" r="4" fill="#55efc4"/>
</g>
<g transform="translate(360, 84)">
<rect width="108" height="56" rx="6" fill="#2d3436" opacity="0.9"/>
<text x="54" y="22" fill="#dfe6e9" font-size="11" font-weight="bold" text-anchor="middle">EQ</text>
<text x="54" y="38" fill="#636e72" font-size="9" text-anchor="middle">Bass: +2 Mid: 0</text>
<circle cx="90" cy="12" r="4" fill="#55efc4"/>
</g>
<!-- Row 2 -->
<g transform="translate(12, 148)">
<rect width="108" height="56" rx="6" fill="#2d3436" opacity="0.9"/>
<text x="54" y="22" fill="#dfe6e9" font-size="11" font-weight="bold" text-anchor="middle">NAM AMP</text>
<text x="54" y="38" fill="#e17055" font-size="9" text-anchor="middle">Matchless Chime</text>
<circle cx="90" cy="12" r="4" fill="#55efc4"/>
</g>
<g transform="translate(128, 148)">
<rect width="108" height="56" rx="6" fill="#2d3436" opacity="0.9"/>
<text x="54" y="22" fill="#dfe6e9" font-size="11" font-weight="bold" text-anchor="middle">CAB IR</text>
<text x="54" y="38" fill="#e17055" font-size="9" text-anchor="middle">Vox AC30 Blue</text>
<circle cx="90" cy="12" r="4" fill="#55efc4"/>
</g>
<g transform="translate(244, 148)">
<rect width="108" height="56" rx="6" fill="#2d3436" opacity="0.9"/>
<text x="54" y="22" fill="#dfe6e9" font-size="11" font-weight="bold" text-anchor="middle">CHORUS</text>
<text x="54" y="38" fill="#636e72" font-size="9" text-anchor="middle">Rate: 0.4 Depth: 55</text>
<circle cx="90" cy="12" r="4" fill="#55efc4"/>
</g>
<g transform="translate(360, 148)">
<rect width="108" height="56" rx="6" fill="#2d3436" opacity="0.8"/>
<text x="54" y="22" fill="#636e72" font-size="11" font-weight="bold" text-anchor="middle">DELAY</text>
<text x="54" y="38" fill="#636e72" font-size="9" text-anchor="middle">Time: 420ms</text>
<circle cx="90" cy="12" r="4" fill="#636e72"/>
</g>
<!-- ═══ Footer ═══ -->
<!-- VU Meters -->
<rect x="12" y="212" width="456" height="36" rx="6" fill="#0a0a1a" opacity="0.5"/>
<text x="24" y="234" fill="#636e72" font-size="9">L</text>
<rect x="36" y="220" width="200" height="20" rx="3" fill="#1a1a2e"/>
<rect x="36" y="220" width="120" height="20" rx="3" fill="#55efc4"/>
<rect x="36" y="220" width="160" height="20" rx="3" fill="#00b894" opacity="0.3"/>
<text x="256" y="234" fill="#636e72" font-size="9">R</text>
<rect x="268" y="220" width="200" height="20" rx="3" fill="#1a1a2e"/>
<rect x="268" y="220" width="80" height="20" rx="3" fill="#55efc4"/>
<rect x="268" y="220" width="140" height="20" rx="3" fill="#00b894" opacity="0.3"/>
<!-- Knobs row -->
<g transform="translate(40, 260)">
<circle cx="0" cy="0" r="18" fill="url(#knob)" stroke="#636e72" stroke-width="1"/>
<line x1="0" y1="-14" x2="0" y2="-6" stroke="#55efc4" stroke-width="2" stroke-linecap="round"/>
<text x="0" y="28" fill="#636e72" font-size="9" text-anchor="middle">MASTER</text>
</g>
<g transform="translate(140, 260)">
<circle cx="0" cy="0" r="18" fill="url(#knob)" stroke="#636e72" stroke-width="1"/>
<line x1="0" y1="-14" x2="8" y2="-8" stroke="#55efc4" stroke-width="2" stroke-linecap="round"/>
<text x="0" y="28" fill="#636e72" font-size="9" text-anchor="middle">GAIN</text>
</g>
<g transform="translate(240, 260)">
<circle cx="0" cy="0" r="18" fill="url(#knob)" stroke="#636e72" stroke-width="1"/>
<line x1="0" y1="-14" x2="-8" y2="-8" stroke="#55efc4" stroke-width="2" stroke-linecap="round"/>
<text x="0" y="28" fill="#636e72" font-size="9" text-anchor="middle">TONE</text>
</g>
<g transform="translate(340, 260)">
<circle cx="0" cy="0" r="18" fill="url(#knob)" stroke="#636e72" stroke-width="1"/>
<line x1="0" y1="-14" x2="4" y2="-12" stroke="#55efc4" stroke-width="2" stroke-linecap="round"/>
<text x="0" y="28" fill="#636e72" font-size="9" text-anchor="middle">VOLUME</text>
</g>
<!-- Bottom nav dots -->
<circle cx="190" cy="305" r="3" fill="#55efc4"/>
<circle cx="210" cy="305" r="3" fill="#636e72"/>
<circle cx="230" cy="305" r="3" fill="#636e72"/>
<circle cx="250" cy="305" r="3" fill="#636e72"/>
<circle cx="270" cy="305" r="3" fill="#636e72"/>
<circle cx="290" cy="305" r="3" fill="#636e72"/>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

+424
View File
@@ -0,0 +1,424 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pi Multi-FX Pedal — UI Template</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #1a1a2e;
color: #e0e0e0;
padding: 40px;
display: flex;
flex-direction: column;
align-items: center;
}
h1 { font-size: 18px; font-weight: 600; color: #888; margin-bottom: 24px; letter-spacing: 2px; text-transform: uppercase; }
.pedal-frame {
width: 800px;
background: #16213e;
border: 2px solid #0f3460;
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
position: relative;
}
.pedal-frame::before {
content: "480 × 320 (touchscreen)";
position: absolute;
top: -28px;
right: 0;
font-size: 11px;
color: #555;
letter-spacing: 1px;
}
/* ── Top bar ── */
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 0 16px 0;
border-bottom: 1px solid #0f3460;
margin-bottom: 16px;
}
.topbar .preset-name {
font-size: 28px;
font-weight: 700;
color: #fff;
}
.topbar .preset-bank {
font-size: 13px;
color: #888;
margin-top: 2px;
}
.topbar .status-area {
display: flex;
align-items: center;
gap: 12px;
}
.topbar .cpu-badge {
background: #0f3460;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
color: #53d769;
}
.topbar .bypass-btn {
background: #e94560;
color: #fff;
padding: 6px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
border: none;
}
.topbar .bypass-btn.off {
background: #2d4059;
color: #888;
}
/* ── VU Meters ── */
.vu-section {
display: flex;
gap: 16px;
margin-bottom: 20px;
padding: 12px 16px;
background: #0a1628;
border-radius: 10px;
}
.vu-meter {
flex: 1;
}
.vu-meter label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
color: #666;
display: block;
margin-bottom: 4px;
}
.vu-bar {
height: 8px;
background: #1a1a2e;
border-radius: 4px;
overflow: hidden;
position: relative;
}
.vu-fill {
height: 100%;
border-radius: 4px;
width: 65%;
}
.vu-fill.left { background: linear-gradient(90deg, #53d769, #e9d34b, #e94560); width: 72%; }
.vu-fill.right { background: linear-gradient(90deg, #53d769, #e9d34b, #e94560); width: 58%; }
.vu-clip { color: #e94560; font-size: 11px; margin-left: 4px; }
/* ── FX Chain ── */
.fx-chain {
display: flex;
gap: 6px;
margin-bottom: 20px;
flex-wrap: wrap;
padding: 8px 0;
}
.fx-block {
background: #0f3460;
border: 1px solid #1a4a7a;
border-radius: 8px;
padding: 8px 12px;
font-size: 11px;
text-align: center;
min-width: 60px;
cursor: pointer;
transition: all 0.15s;
}
.fx-block:hover { background: #1a4a7a; border-color: #53d769; }
.fx-block .icon { font-size: 18px; display: block; margin-bottom: 2px; }
.fx-block .name { color: #aaa; font-size: 10px; }
.fx-block.active { border-color: #53d769; }
.fx-block.bypassed { opacity: 0.4; }
.fx-chain .arrow { color: #333; font-size: 18px; align-self: center; padding: 0 2px; }
/* ── Main content grid ── */
.main-grid {
display: grid;
grid-template-columns: 1fr 220px;
gap: 16px;
}
/* ── Knob panel ── */
.knob-panel {
background: #0a1628;
border-radius: 10px;
padding: 16px;
}
.knob-panel .header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.knob-panel .header h3 { font-size: 14px; font-weight: 600; color: #ccc; }
.knob-panel .header .type { font-size: 11px; color: #53d769; }
.knob-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.knob {
text-align: center;
}
.knob .circle {
width: 48px;
height: 48px;
border: 3px solid #0f3460;
border-radius: 50%;
margin: 0 auto 6px;
position: relative;
background: #16213e;
cursor: grab;
}
.knob .circle::after {
content: "";
position: absolute;
top: 4px;
left: 50%;
width: 2px;
height: 14px;
background: #e94560;
transform: translateX(-50%) rotate(45deg);
border-radius: 1px;
}
.knob label { font-size: 10px; color: #888; display: block; }
.knob .value { font-size: 12px; color: #fff; font-weight: 600; }
/* ── Side panel (presets / browser) ── */
.side-panel {
background: #0a1628;
border-radius: 10px;
padding: 12px;
}
.side-panel .tabs {
display: flex;
gap: 4px;
margin-bottom: 10px;
}
.side-panel .tabs button {
flex: 1;
background: #0f3460;
border: none;
color: #888;
padding: 6px;
border-radius: 6px;
font-size: 11px;
cursor: pointer;
}
.side-panel .tabs button.active { background: #1a4a7a; color: #fff; }
.side-panel .preset-list { }
.preset-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 6px;
border-bottom: 1px solid #0f3460;
cursor: pointer;
}
.preset-item:hover { background: #0f3460; border-radius: 4px; }
.preset-item .p-name { font-size: 12px; }
.preset-item .p-num { color: #555; font-size: 10px; }
.preset-item.active { color: #53d769; }
.preset-item.bank-header { color: #555; font-size: 10px; letter-spacing: 1px; text-transform: uppercase; }
/* ── Bottom nav ── */
.bottom-nav {
display: flex;
gap: 4px;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #0f3460;
}
.bottom-nav button {
flex: 1;
background: none;
border: 1px solid #0f3460;
color: #888;
padding: 8px;
border-radius: 8px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
text-align: center;
}
.bottom-nav button:hover { border-color: #53d769; color: #fff; }
.bottom-nav button.active { background: #0f3460; color: #53d769; border-color: #53d769; }
/* ── Tag overlay ── */
.tag {
position: absolute;
font-size: 10px;
color: #53d76955;
letter-spacing: 1px;
font-weight: 600;
}
</style>
</head>
<body>
<h1>🎛️ Figma Template — Pi Multi-FX Pedal UI</h1>
<div class="pedal-frame">
<!-- TOP BAR -->
<div class="topbar">
<div>
<div class="preset-name">Crunch Rhythm</div>
<div class="preset-bank">Bank 1 · Preset 2 of 4</div>
</div>
<div class="status-area">
<div class="cpu-badge">CPU 34%</div>
<button class="bypass-btn off">⏸ BYPASS</button>
</div>
</div>
<!-- VU METERS -->
<div class="vu-section">
<div class="vu-meter">
<label>Input</label>
<div class="vu-bar"><div class="vu-fill left"></div></div>
</div>
<div class="vu-meter">
<label>Output</label>
<div class="vu-bar"><div class="vu-fill right"></div></div>
</div>
<div class="vu-meter" style="flex: 0 0 40px;">
<label>dB</label>
<div style="font-size:22px;font-weight:700;color:#e0e0e0;">-12</div>
</div>
</div>
<!-- FX CHAIN -->
<div class="fx-chain">
<div class="fx-block active"><span class="icon">🚪</span><span class="name">Gate</span></div>
<div class="arrow"></div>
<div class="fx-block active"><span class="icon">📦</span><span class="name">Comp</span></div>
<div class="arrow"></div>
<div class="fx-block active"><span class="icon">🎸</span><span class="name">OD</span></div>
<div class="arrow"></div>
<div class="fx-block active" style="border-color:#53d769;"><span class="icon">🧠</span><span class="name">NAM</span></div>
<div class="arrow"></div>
<div class="fx-block active"><span class="icon">📡</span><span class="name">IR</span></div>
<div class="arrow"></div>
<div class="fx-block bypassed"><span class="icon">〰️</span><span class="name">Chorus</span></div>
<div class="arrow"></div>
<div class="fx-block active"><span class="icon"></span><span class="name">Delay</span></div>
<div class="arrow"></div>
<div class="fx-block active"><span class="icon">🌊</span><span class="name">Reverb</span></div>
</div>
<!-- MAIN GRID -->
<div class="main-grid">
<!-- KNOB PANEL -->
<div class="knob-panel">
<div class="header">
<h3>NAM Amp</h3>
<span class="type">feather-lstm · 1.2ms</span>
</div>
<div class="knob-grid">
<div class="knob">
<div class="circle"></div>
<label>Gain</label>
<div class="value">68</div>
</div>
<div class="knob">
<div class="circle"></div>
<label>Tone</label>
<div class="value">52</div>
</div>
<div class="knob">
<div class="circle"></div>
<label>Level</label>
<div class="value">75</div>
</div>
<div class="knob">
<div class="circle"></div>
<label>Gate</label>
<div class="value">30</div>
</div>
<div class="knob">
<div class="circle"></div>
<label>Bass</label>
<div class="value">45</div>
</div>
<div class="knob">
<div class="circle"></div>
<label>Mid</label>
<div class="value">60</div>
</div>
<div class="knob">
<div class="circle"></div>
<label>Treble</label>
<div class="value">55</div>
</div>
<div class="knob">
<div class="circle"></div>
<label>Presence</label>
<div class="value">40</div>
</div>
</div>
</div>
<!-- SIDE PANEL -->
<div class="side-panel">
<div class="tabs">
<button class="active">Presets</button>
<button>Models</button>
<button>IRs</button>
</div>
<div class="preset-list">
<div class="preset-item bank-header">Bank 1 — Classics</div>
<div class="preset-item active">
<span class="p-name">✦ Crunch Rhythm</span>
<span class="p-num">1-2</span>
</div>
<div class="preset-item">
<span class="p-name">Lead Solo</span>
<span class="p-num">1-3</span>
</div>
<div class="preset-item">
<span class="p-name">Clean Jazz</span>
<span class="p-num">1-4</span>
</div>
<div class="preset-item bank-header">Bank 2 — FX Bench</div>
<div class="preset-item">
<span class="p-name">Overdrive</span>
<span class="p-num">2-1</span>
</div>
<div class="preset-item">
<span class="p-name">Ambient Wash</span>
<span class="p-num">2-2</span>
</div>
</div>
</div>
</div>
<!-- BOTTOM NAV -->
<div class="bottom-nav">
<button class="active">🎛️ Rig</button>
<button>🔧 FX</button>
<button>📋 Presets</button>
<button>🔍 Browse</button>
<button>⚙️ Settings</button>
</div>
</div>
<p style="margin-top:32px;color:#555;font-size:12px;text-align:center;max-width:600px;">
Dark theme · 800×500 mockup (scales to 480×320 touchscreen).<br>
Sections: Top bar, VU meters, FX chain, knob panel, presets sidebar, bottom nav.<br>
Trace over the labeled blocks in Figma — swap icons, fonts, spacing to taste.
</p>
</body>
</html>
+176
View File
@@ -0,0 +1,176 @@
# Dogfood QA Report — Pi Multi-FX Pedal
**Target:** http://pedal.local:8080 (RPi live hardware)
**Date:** 2026-06-12
**Scope:** REST API endpoints, HTML pages, Tone3000 search, edge cases vs live Pi
**Tester:** Hermes Agent (automated exploratory QA against real deployment)
---
## Executive Summary
| Severity | Count |
|----------|-------|
| 🔴 Critical | 1 |
| 🟠 High | 2 |
| 🟡 Medium | 1 |
| 🔵 Low | 1 |
| **Total** | **5** |
**Overall Assessment:** The pedal is working! The Pi is online, connected, and serving real data. Dashboard shows live state (volume at 0.33, mono routing, preset system working). Three key issues identified: one critical (dashboard crash when disconnected), one version-sync gap (tonehub aliases not pushed to Pi), and one missing endpoint (`/api/captures`).
---
## Live vs Local — Key Difference
| Check | Local (code test) | Live Pi (pedal.local) |
|-------|-------------------|----------------------|
| Dashboard | 200 | **200 with real data** |
| Connected state | `connected: False` | `connected: True` |
| master_volume | 0.8 (default) | **0.33 (live)** |
| routing_mode | mono | mono, breakpoint 7 |
| Tone3000 search | ✅ Works | ✅ Works |
| Tonehub alias routes | ✅ Work | **404 — not on Pi yet** |
---
## Issues
### Issue #1: Dashboard crashes with 500 when pedal deps not connected 🔴
*(Same as previously reported and fixed locally — needs push to Pi)*
| Field | Value |
|-------|-------|
| **Severity** | 🔴 Critical |
| **Category** | Functional |
| **URL** | `GET /` |
**Description:**
The dashboard template renders fine when pedal deps are connected (tested live — returns 200 with real data), but crashes with HTTP 500 when the pedal is disconnected. Root cause: `_gather_state()` returns only `{"connected": False}` when deps are None, but the template expects full default keys.
**Fix applied locally in `_gather_state()`** — returns full defaults (`master_volume: 0.8`, `routing_mode: "mono"`, etc.) when disconnected.
**Needs:** Commit & push to Pi.
---
### Issue #2: React SPA calls `/api/captures` but backend has no such route 🟠
| Field | Value |
|-------|-------|
| **Severity** | 🟠 High |
| **Category** | Functional |
| **URL** | `GET /api/captures` |
**Description:**
The React SPA (`pi-multifx-pedal-ui/src/App.jsx`) has an API layer that calls `GET /api/captures` to list NAM captures and `POST /api/captures/upload` to upload new ones. However, the backend (`src/web/server.py`) has no `/api/captures` route — returns 404 on both local test and live Pi.
Introduced during "Wire React Knobs to Live API" task — frontend calls were added without building the corresponding backend endpoints. The captures screen in the React UI will silently fail.
---
### Issue #3: Tonehub alias routes not pushed to Pi 🟠
| Field | Value |
|-------|-------|
| **Severity** | 🟠 High |
| **Category** | Deployment |
| **URL** | `GET /api/tonehub/search` |
**Description:**
The alias routes `GET /api/tonehub/search` and `GET /api/tonehub/search/irs` work locally (return Tone3000 search results) but return 404 on the live Pi. These routes were added in an uncommitted change on the local dev machine and were never pushed to Gitea, so the Pi doesn't have them.
The direct routes (`/api/irs/tonedownload/search`, `/api/models/tonedownload/search`) work on the Pi. Only the shorthand aliases are missing.
**Affected routes:**
- `GET /api/tonehub/search` → 404 on Pi (alias for `/api/models/tonedownload/search`)
- `GET /api/tonehub/search/irs` → 404 on Pi (alias for `/api/irs/tonedownload/search`)
**Needs:** Commit tonehub alias routes + `_gather_state` fix, push to Gitea, pull on Pi, restart server.
---
### Issue #4: `/api/block-params/{fx_type}` returns 200 for unknown types 🟡
| Field | Value |
|-------|-------|
| **Severity** | 🟡 Medium |
| **Category** | Functional |
| **URL** | `GET /api/block-params/99` |
**Description:**
`GET /api/block-params/99` returns 200 with `{"fx_type":"99","params":[]}` instead of 404. The route accepts any string without validating against known FX types. Confirmed on live Pi (same behavior).
---
### Issue #5: Unclosed aiohttp client session warnings 🔵
| Field | Value |
|-------|-------|
| **Severity** | 🔵 Low |
| **Category** | Console |
| **URL** | All Tone3000 routes |
**Description:**
Unclosed aiohttp ClientSession on Tone3000 API calls — resource leak that accumulates warning noise. Same on both local and Pi.
---
## Issues Summary Table
| # | Title | Severity | Type | Status |
|---|-------|----------|------|--------|
| 1 | Dashboard 500 when disconnected | 🔴 Critical | Functional | **Fixed locally** — needs push |
| 2 | React calls `/api/captures`, backend missing | 🟠 High | Functional | Needs new endpoint |
| 3 | Tonehub alias routes 404 on Pi | 🟠 High | Deployment | **Fixed locally** — needs push |
| 4 | block-params 200 for unknown types | 🟡 Medium | Functional | Needs validation |
| 5 | Unclosed aiohttp session | 🔵 Low | Console | Needs cleanup |
## Testing Coverage
### Pages (all 5 on Pi) ✅
| Page | Status | Notes |
|------|--------|-------|
| `GET /` (Dashboard) | 200 ✅ | Live data: master_volume=0.33 |
| `GET /presets` | 200 ✅ | Returns bank 0 / Clean Jazz preset |
| `GET /models` | 200 ✅ | Empty (no models installed) |
| `GET /irs` | 200 ✅ | Empty (no IRs installed) |
| `GET /settings` | 200 ✅ | 15KB, full settings UI |
### API Endpoints on Pi
| Endpoint | Result | Notes |
|----------|--------|-------|
| `GET /api/state` | 200 ✅ | `connected: true` |
| `GET /api/presets` | 200 ✅ | Real presets |
| `GET /api/routing` | 200 ✅ | mono, breakpoint 7 |
| `POST /api/routing` | 200 ✅ | Returns updated routing |
| `GET /api/irs` | 200 ✅ | No IRs loaded |
| `GET /api/models` | 200 ✅ | No models loaded |
| `GET /api/captures` | 404 ❌ | Missing endpoint |
| `GET /api/irs/tonedownload/search` | 200 ✅ | 36 results |
| `GET /api/tonehub/search` | 404 ❌ | Not pushed to Pi |
| `GET /api/tonehub/search/irs` | 404 ❌ | Not pushed to Pi |
| `GET /api/block-params/0` | 200 ✅ | |
| `GET /api/block-params/99` | 200 ⚠️ | Should be 404 |
| `POST /api/bypass/toggle` | 404 ❌ | Check if route exists |
| `POST /api/tuner/toggle` | 404 ❌ | Check if route exists |
| `POST /api/models/tonedownload/install` | 422/500 ⚠️ | Works locally, needs download target |
### Edge Cases
| Test | Result |
|------|--------|
| `GET /nonexistent` | 404 ✅ |
| `GET /api/nonexistent` | 404 ✅ |
| `GET /api/presets/99` | 404 ✅ |
---
## Next Steps
1. **Commit & push** the `_gather_state` fix and tonehub alias routes to Gitea
2. **Pull & restart** on the Pi
3. **Build `/api/captures`** endpoint (list captures + upload)
4. **Add FX type validation** to `GET /api/block-params/{fx_type}`
5. Run retest after fixes are deployed
+1
View File
@@ -0,0 +1 @@
This is a direct write_file test at $(date). If this persists, we know it's the tool, not the file.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
PERSISTENCE TEST: 1815770b8e48d35f
+1
View File
@@ -0,0 +1 @@
SAME_CHECK_1781065543
+2 -2
View File
@@ -7,9 +7,9 @@
"chain": [
{"fx_type": "noise_gate", "enabled": true, "bypass": false, "params": {"threshold": 0.01}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "compressor", "enabled": true, "bypass": false, "params": {"threshold": 0.3, "ratio": 4.0, "gain": 0.8}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "nam_amp", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "factory/clean_fender.nam", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "", "ir_file_path": "factory/twin_reverb.wav"},
{"fx_type": "eq", "enabled": true, "bypass": false, "params": {"bass": 0.5, "mid": 0.5, "treble": 0.5}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "reverb", "enabled": true, "bypass": false, "params": {"decay": 0.3, "mix": 0.2}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {"wet": 1.0, "dry": 0.0}, "nam_model_path": "", "ir_file_path": "~/.pedal/irs/vintage-1x12.wav"},
{"fx_type": "volume", "enabled": true, "bypass": false, "params": {"level": 0.85}, "nam_model_path": "", "ir_file_path": ""}
],
"midi_mappings": {
+2 -2
View File
@@ -7,9 +7,9 @@
"chain": [
{"fx_type": "noise_gate", "enabled": true, "bypass": false, "params": {"threshold": 0.008}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "overdrive", "enabled": true, "bypass": false, "params": {"drive": 0.3, "gain": 0.9}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "nam_amp", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "factory/vox_ac30.nam", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "", "ir_file_path": "factory/alnico_blue.wav"},
{"fx_type": "eq", "enabled": true, "bypass": false, "params": {"bass": 0.4, "mid": 0.6, "treble": 0.7}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "delay", "enabled": true, "bypass": false, "params": {"time": 350.0, "feedback": 0.25, "mix": 0.2}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {"wet": 1.0, "dry": 0.0}, "nam_model_path": "", "ir_file_path": "~/.pedal/irs/boutique-1x12.wav"},
{"fx_type": "volume", "enabled": true, "bypass": false, "params": {"level": 0.8}, "nam_model_path": "", "ir_file_path": ""}
],
"midi_mappings": {}
+2 -2
View File
@@ -6,10 +6,10 @@
"tuner_enabled": false,
"chain": [
{"fx_type": "noise_gate", "enabled": true, "bypass": false, "params": {"threshold": 0.01}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "nam_amp", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "factory/roland_jc120.nam", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "", "ir_file_path": "factory/jc120_ir.wav"},
{"fx_type": "eq", "enabled": true, "bypass": false, "params": {"bass": 0.5, "mid": 0.5, "treble": 0.6}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "chorus", "enabled": true, "bypass": false, "params": {"rate": 0.35, "depth": 0.5, "mix": 0.4}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "reverb", "enabled": true, "bypass": false, "params": {"decay": 0.4, "mix": 0.25}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {"wet": 1.0, "dry": 0.0}, "nam_model_path": "", "ir_file_path": "~/.pedal/irs/american-2x12.wav"},
{"fx_type": "volume", "enabled": true, "bypass": false, "params": {"level": 0.82}, "nam_model_path": "", "ir_file_path": ""}
],
"midi_mappings": {}
+2 -2
View File
@@ -7,9 +7,9 @@
"chain": [
{"fx_type": "compressor", "enabled": true, "bypass": false, "params": {"threshold": 0.25, "ratio": 6.0, "gain": 0.7}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "boost", "enabled": true, "bypass": false, "params": {"gain_db": 3.0}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "nam_amp", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "factory/fender_twin.nam", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "", "ir_file_path": "factory/american_2x12.wav"},
{"fx_type": "eq", "enabled": true, "bypass": false, "params": {"bass": 0.4, "mid": 0.7, "treble": 0.5}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "reverb", "enabled": true, "bypass": false, "params": {"decay": 0.5, "mix": 0.3}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {"wet": 1.0, "dry": 0.0}, "nam_model_path": "", "ir_file_path": "~/.pedal/irs/jazz-1x15.wav"},
{"fx_type": "volume", "enabled": true, "bypass": false, "params": {"level": 0.78}, "nam_model_path": "", "ir_file_path": ""}
],
"midi_mappings": {}
+2 -2
View File
@@ -7,9 +7,9 @@
"chain": [
{"fx_type": "noise_gate", "enabled": true, "bypass": false, "params": {"threshold": 0.015}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "overdrive", "enabled": true, "bypass": false, "params": {"drive": 0.55, "gain": 0.8}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "nam_amp", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "factory/marshall_plexi.nam", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "", "ir_file_path": "factory/greenback_4x12.wav"},
{"fx_type": "eq", "enabled": true, "bypass": false, "params": {"bass": 0.6, "mid": 0.4, "treble": 0.7}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "delay", "enabled": true, "bypass": false, "params": {"time": 380.0, "feedback": 0.3, "mix": 0.25}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {"wet": 1.0, "dry": 0.0}, "nam_model_path": "", "ir_file_path": "~/.pedal/irs/british-4x12.wav"},
{"fx_type": "volume", "enabled": true, "bypass": false, "params": {"level": 0.75}, "nam_model_path": "", "ir_file_path": ""}
],
"midi_mappings": {
+3 -3
View File
@@ -9,11 +9,11 @@
{"fx_type": "compressor", "enabled": true, "bypass": false, "params": {"threshold": 0.4, "ratio": 3.0, "gain": 1.2}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "overdrive", "enabled": true, "bypass": false, "params": {"drive": 0.7, "gain": 0.7}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "distortion", "enabled": true, "bypass": false, "params": {"drive": 0.6, "gain": 0.6}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "nam_amp", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "factory/5150_stealth.nam", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "", "ir_file_path": "factory/v30_4x12.wav"},
{"fx_type": "eq", "enabled": true, "bypass": false, "params": {"bass": 0.5, "mid": 0.3, "treble": 0.8}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "delay", "enabled": true, "bypass": false, "params": {"time": 450.0, "feedback": 0.35, "mix": 0.35}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "reverb", "enabled": true, "bypass": false, "params": {"decay": 0.6, "mix": 0.3}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "volume", "enabled": true, "bypass": false, "params": {"level": 0.9}, "nam_model_path": "", "ir_file_path": ""}
{"fx_type": "volume", "enabled": true, "bypass": false, "params": {"level": 0.9}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {"wet": 1.0, "dry": 0.0}, "nam_model_path": "", "ir_file_path": "~/.pedal/irs/british-4x12.wav"}
],
"midi_mappings": {
"delay_feedback": {"cc_number": 18, "channel": 1, "min_val": 0.0, "max_val": 1.0}
+2 -3
View File
@@ -8,10 +8,9 @@
{"fx_type": "noise_gate", "enabled": true, "bypass": false, "params": {"threshold": 0.03}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "boost", "enabled": true, "bypass": false, "params": {"gain_db": 8.0}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "distortion", "enabled": true, "bypass": false, "params": {"drive": 0.8, "gain": 0.5}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "nam_amp", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "factory/peavey_6505.nam", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "", "ir_file_path": "factory/v30_4x12.wav"},
{"fx_type": "eq", "enabled": true, "bypass": false, "params": {"bass": 0.6, "mid": 0.3, "treble": 0.7}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "volume", "enabled": true, "bypass": false, "params": {"level": 0.7}, "nam_model_path": "", "ir_file_path": ""}
{"fx_type": "volume", "enabled": true, "bypass": false, "params": {"level": 0.7}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {"wet": 1.0, "dry": 0.0}, "nam_model_path": "", "ir_file_path": "~/.pedal/irs/modern-4x12.wav"}
],
"midi_mappings": {}
}
+1 -2
View File
@@ -6,11 +6,10 @@
"tuner_enabled": false,
"chain": [
{"fx_type": "compressor", "enabled": true, "bypass": false, "params": {"threshold": 0.3, "ratio": 4.0, "gain": 0.9}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "nam_amp", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "factory/fender_twin.nam", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {}, "nam_model_path": "", "ir_file_path": "factory/american_2x12.wav"},
{"fx_type": "phaser", "enabled": true, "bypass": false, "params": {"rate": 0.2, "depth": 0.6, "feedback": 0.4}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "chorus", "enabled": true, "bypass": false, "params": {"rate": 0.3, "depth": 0.7, "mix": 0.5}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "delay", "enabled": true, "bypass": false, "params": {"time": 500.0, "feedback": 0.4, "mix": 0.35}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "ir_cab", "enabled": true, "bypass": false, "params": {"wet": 1.0, "dry": 0.0}, "nam_model_path": "", "ir_file_path": "~/.pedal/irs/american-2x12.wav"},
{"fx_type": "reverb", "enabled": true, "bypass": false, "params": {"decay": 0.8, "mix": 0.4}, "nam_model_path": "", "ir_file_path": ""},
{"fx_type": "volume", "enabled": true, "bypass": false, "params": {"level": 0.78}, "nam_model_path": "", "ir_file_path": ""}
],
+38
View File
@@ -0,0 +1,38 @@
# Pi Multi-FX Pedal — Default Cabinet IR Files
Synthetic impulse responses (.wav) for guitar cabinet simulation.
Generated using DSP filter cascades (Butterworth LP/HP + parametric
peak/notch filters + exponential decay envelope).
**All IRs are 48kHz, 16-bit mono WAV files.**
| File | Cabinet Model | Taps | Length | Description |
|------|--------------|------|--------|-------------|
| `vintage-1x12.wav` | Vintage 1x12 — Fender-style Open Back | 2048 | 43ms | Warm, scooped cleans with bell-like top end. Models a Fender-style 1x12 open-back combo. |
| `british-4x12.wav` | British 4x12 — Marshall-style Closed Back | 4096 | 85ms | Mid-forward, aggressive rock tones with tight low end. Models a Marshall 1960A-style 4x12 closed-back cabinet. |
| `american-2x12.wav` | American 2x12 — Vox-style Open Back | 2048 | 43ms | Chimey, complex midrange with sparkling highs. Models a Vox AC30-style 2x12 open-back cabinet. |
| `modern-4x12.wav` | Modern 4x12 — Mesa/Boogie-style Closed Back | 4096 | 85ms | Tight low-end, aggressive mids, smooth highs. Models a Mesa Rectifier-style 4x12 closed-back cab. |
| `jazz-1x15.wav` | Jazz 1x15 — Deep Open Back | 2048 | 43ms | Deep, warm, scooped tone for jazz cleans. Models a 15-inch speaker in a large open-back cab. |
| `boutique-1x12.wav` | Boutique 1x12 — Dumble-style Open Back | 2048 | 43ms | Smooth, rounded cleans with enhanced mid complexity. Models a Dumble-style 1x12 open-back combo. |
| `mini-1x8.wav` | Mini 1x8 — Small Practice Amp | 1024 | 21ms | Lo-fi, boxy tone for vintage radio-style sounds. Models a small 8-inch practice amp speaker. |
## Usage
These IRs are loaded by the `IRLoader` class through the pedal's cab simulation.
Place them in `~/.pedal/irs/` or reference them by absolute path in preset chain blocks.
## Replacement
Replace any `.wav` file with a real captured IR (48kHz, 16-bit mono) to
upgrade from synthetic to authentic cabinet tone. The pedal treats all
`.wav` files in the IR directory identically.
## Cabinet Type Guide
- **Vintage 1x12** — Fender Deluxe / Princeton-style cleans. Scooped mids, warm lows.
- **British 4x12** — Marshall 1960 / JCM-style crunch and rock. Mid-forward, aggressive.
- **American 2x12** — Vox AC30-style chime. Complex mids, sparkling treble.
- **Modern 4x12** — Mesa Rectifier-style high gain. Tight lows, smooth highs.
- **Jazz 1x15** — Polytone / Henriksen-style jazz. Deep lows, scooped mids.
- **Boutique 1x12** — Dumble / Two-Rock-style. Smooth, complex midrange.
- **Mini 1x8** — Small practice amp. Boxy, lo-fi, vintage radio tone.
+474
View File
@@ -0,0 +1,474 @@
#!/usr/bin/env python3
"""Generate seed cabinet IR (Impulse Response) .wav files for the Pi Multi-FX Pedal.
Each IR models a different speaker cabinet type using DSP techniques:
- Speaker low-pass roll-off (cone diameter determines cutoff)
- Cabinet resonance peaks (Helmholtz resonance)
- Cone breakup modes (notch filters for mechanical resonances)
- Exponential decay envelope (room-dependent)
- Microphone proximity effect (slight high-end roll-off)
Output: ~/.pedal/irs/ directory or a bundled factory-irs path.
Usage:
python3 generate_seed_irs.py [--dest DIR] [--mono]
"""
from __future__ import annotations
import argparse
import logging
import sys
from pathlib import Path
import numpy as np
from scipy.io import wavfile
from scipy.signal import butter, lfilter, freqz, sosfilt
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
SAMPLE_RATE = 48000 # Fixed 48kHz for the pedal
# ── Filter helpers ───────────────────────────────────────────────────
def _lowpass_sos(cutoff_hz: float, order: int = 2) -> np.ndarray:
"""2nd-order Butterworth low-pass filter as SOS array."""
sos = butter(order, cutoff_hz, btype="low", fs=SAMPLE_RATE, output="sos")
return sos
def _highpass_sos(cutoff_hz: float, order: int = 2) -> np.ndarray:
"""2nd-order Butterworth high-pass filter as SOS array."""
sos = butter(order, cutoff_hz, btype="high", fs=SAMPLE_RATE, output="sos")
return sos
def _peak_sos(freq_hz: float, q: float, gain_db: float) -> np.ndarray:
"""Parametric peak EQ as SOS array (biquad)."""
A = 10 ** (gain_db / 40.0)
omega = 2.0 * np.pi * freq_hz / SAMPLE_RATE
alpha = np.sin(omega) / (2.0 * q)
b0 = 1.0 + alpha * A
b1 = -2.0 * np.cos(omega)
b2 = 1.0 - alpha * A
a0 = 1.0 + alpha / A
a1 = -2.0 * np.cos(omega)
a2 = 1.0 - alpha / A
# Normalize
b = np.array([b0, b1, b2]) / a0
a = np.array([1.0, a1 / a0, a2 / a0])
# Convert to SOS
sos = np.array([[b[0], b[1], b[2], a[0], a[1], a[2]]])
return sos
def _notch_sos(freq_hz: float, q: float) -> np.ndarray:
"""Notch filter as SOS array."""
omega = 2.0 * np.pi * freq_hz / SAMPLE_RATE
alpha = np.sin(omega) / (2.0 * q)
b = np.array([1.0, -2.0 * np.cos(omega), 1.0])
a = np.array([1.0 + alpha, -2.0 * np.cos(omega), 1.0 - alpha])
sos = np.array([[b[0] / a[0], b[1] / a[0], b[2] / a[0], 1.0, a[1] / a[0], a[2] / a[0]]])
return sos
# ── Cabinet IR model ─────────────────────────────────────────────────
def _generate_cabinet_ir(
num_taps: int,
lowpass_cutoff: float,
lowpass_order: int,
highpass_cutoff: float,
resonance_peaks: list[tuple[float, float, float]], # (freq_hz, q, gain_db)
notch_freqs: list[tuple[float, float]], # (freq_hz, q)
decay_time_ms: float,
) -> np.ndarray:
"""Generate a synthetic cabinet IR using cascaded filters + decay envelope.
Args:
num_taps: Length of the IR in samples.
lowpass_cutoff: Low-pass filter cutoff (speaker roll-off) in Hz.
lowpass_order: Order of the low-pass filter (2 or 4).
highpass_cutoff: High-pass filter cutoff (cabinet resonance) in Hz.
resonance_peaks: List of (freq_Hz, Q, gain_dB) peak filters.
notch_freqs: List of (freq_Hz, Q) notch filters for cone breakup.
decay_time_ms: T60-like decay time in milliseconds.
Returns:
float32 numpy array of the IR, shape (num_taps,).
"""
# Start with a Dirac impulse
ir = np.zeros(num_taps, dtype=np.float64)
ir[0] = 1.0
# Apply filters in series
# 1. Low-pass (speaker roll-off)
sos = _lowpass_sos(lowpass_cutoff, lowpass_order)
ir = sosfilt(sos, ir)
# 2. High-pass (cabinet resonance - prevents subsonic rumble)
sos = _highpass_sos(highpass_cutoff, 1)
ir = sosfilt(sos, ir)
# 3. Resonance peaks (cabinet Helmholtz + mic position)
for freq, q, gain_db in resonance_peaks:
sos = _peak_sos(freq, q, gain_db)
ir = sosfilt(sos, ir)
# 4. Notch filters (cone breakup / standing waves)
for freq, q in notch_freqs:
sos = _notch_sos(freq, q)
ir = sosfilt(sos, ir)
# 5. Decay envelope (exponential, with slight room tail)
decay_samples = int(decay_time_ms * SAMPLE_RATE / 1000.0)
envelope = np.exp(-np.arange(num_taps) / max(1, decay_samples))
ir *= envelope
# Normalize to peak = 0.95 (headroom)
peak = np.max(np.abs(ir))
if peak > 0:
ir /= peak * 1.0526 # normalize to ~0.95
return ir.astype(np.float32)
# ── Cabinet definitions ─────────────────────────────────────────────
# Each definition creates one IR file.
# Parameters derived from published measurements of real cabinets
# (frequency responses, not IRs themselves, so no copyright issue).
CABINET_SPECS: list[dict] = [
{
"name": "vintage-1x12",
"display": "Vintage 1x12 — Fender-style Open Back",
"description": "Warm, scooped cleans with bell-like top end. "
"Models a Fender-style 1x12 open-back combo.",
"num_taps": 2048,
"lowpass_cutoff": 4800,
"lowpass_order": 2,
"highpass_cutoff": 75,
"resonance_peaks": [
(100, 2.0, 4.0), # Cab resonance hump
(800, 1.5, -3.0), # Scooped mids
(2800, 3.0, 2.0), # Presence peak
],
"notch_freqs": [
(1200, 8.0), # Minor cone mode
(3500, 10.0), # Cone edge resonance
],
"decay_time_ms": 60,
},
{
"name": "british-4x12",
"display": "British 4x12 — Marshall-style Closed Back",
"description": "Mid-forward, aggressive rock tones with tight low end. "
"Models a Marshall 1960A-style 4x12 closed-back cabinet.",
"num_taps": 4096,
"lowpass_cutoff": 5200,
"lowpass_order": 4,
"highpass_cutoff": 80,
"resonance_peaks": [
(110, 1.8, 5.0), # Big cab resonance
(700, 1.2, 4.0), # Mid-forward punch
(1500, 2.0, 3.0), # Upper mid grind
(3200, 2.5, 2.5), # Presence
],
"notch_freqs": [
(2500, 12.0), # Cross-over null
(4000, 8.0), # Speaker breakup
],
"decay_time_ms": 85,
},
{
"name": "american-2x12",
"display": "American 2x12 — Vox-style Open Back",
"description": "Chimey, complex midrange with sparkling highs. "
"Models a Vox AC30-style 2x12 open-back cabinet.",
"num_taps": 2048,
"lowpass_cutoff": 5800,
"lowpass_order": 2,
"highpass_cutoff": 70,
"resonance_peaks": [
(120, 1.5, 3.0), # Cab resonance
(600, 1.0, 2.0), # Low-mid body
(1300, 2.0, 5.0), # Chime / complex mids
(3500, 3.0, 3.0), # Top-end sparkle
],
"notch_freqs": [
(1800, 10.0), # Minor comb filter
(4200, 12.0), # Cone resonance
],
"decay_time_ms": 55,
},
{
"name": "modern-4x12",
"display": "Modern 4x12 — Mesa/Boogie-style Closed Back",
"description": "Tight low-end, aggressive mids, smooth highs. "
"Models a Mesa Rectifier-style 4x12 closed-back cab.",
"num_taps": 4096,
"lowpass_cutoff": 4800,
"lowpass_order": 4,
"highpass_cutoff": 85,
"resonance_peaks": [
(95, 2.5, 6.0), # Tight low-end thump
(600, 1.5, 2.0), # Low-mid body
(1000, 1.0, 5.0), # Aggressive mid bark
(2200, 3.0, 1.0), # Upper mid cut
(3200, 2.0, -1.0), # Slight high-end roll for smoothness
],
"notch_freqs": [
(450, 15.0), # Subsonic resonance cleanup
(2800, 10.0), # Cone breakup suppression
],
"decay_time_ms": 90,
},
{
"name": "jazz-1x15",
"display": "Jazz 1x15 — Deep Open Back",
"description": "Deep, warm, scooped tone for jazz cleans. "
"Models a 15-inch speaker in a large open-back cab.",
"num_taps": 2048,
"lowpass_cutoff": 4500,
"lowpass_order": 2,
"highpass_cutoff": 55,
"resonance_peaks": [
(80, 2.0, 8.0), # Deep low-end warmth
(500, 2.0, -4.0), # Scooped mids
(2500, 2.0, 3.0), # Presence for articulation
],
"notch_freqs": [
(200, 8.0), # Cab resonance smoothing
(3800, 8.0), # Cone edge roll-off
],
"decay_time_ms": 70,
},
{
"name": "boutique-1x12",
"display": "Boutique 1x12 — Dumble-style Open Back",
"description": "Smooth, rounded cleans with enhanced mid complexity. "
"Models a Dumble-style 1x12 open-back combo.",
"num_taps": 2048,
"lowpass_cutoff": 5200,
"lowpass_order": 2,
"highpass_cutoff": 78,
"resonance_peaks": [
(105, 2.0, 4.0), # Low-end body
(800, 1.8, 5.0), # Complex mid character
(1800, 2.5, 2.0), # Mid sparkle
(3000, 2.0, 1.5), # Top presence
],
"notch_freqs": [
(1500, 12.0), # Smoothing
(3500, 10.0),
],
"decay_time_ms": 65,
},
{
"name": "mini-1x8",
"display": "Mini 1x8 — Small Practice Amp",
"description": "Lo-fi, boxy tone for vintage radio-style sounds. "
"Models a small 8-inch practice amp speaker.",
"num_taps": 1024,
"lowpass_cutoff": 3800,
"lowpass_order": 2,
"highpass_cutoff": 90,
"resonance_peaks": [
(150, 1.5, 6.0), # Boxy resonance
(1000, 1.0, 4.0), # Nasally mids
(2500, 1.5, -2.0), # Rolled-off highs
],
"notch_freqs": [
(3000, 6.0),
],
"decay_time_ms": 40,
},
]
def generate_all_irs(dest_dir: Path, verify: bool = True) -> list[Path]:
"""Generate all seed IR files into dest_dir.
Returns:
List of created file paths (sorted by name).
"""
dest_dir.mkdir(parents=True, exist_ok=True)
created: list[Path] = []
for spec in CABINET_SPECS:
name = spec["name"]
path = dest_dir / f"{name}.wav"
logger.info(f" Generating {name}.wav ...")
ir = _generate_cabinet_ir(
num_taps=spec["num_taps"],
lowpass_cutoff=spec["lowpass_cutoff"],
lowpass_order=spec["lowpass_order"],
highpass_cutoff=spec["highpass_cutoff"],
resonance_peaks=spec["resonance_peaks"],
notch_freqs=spec["notch_freqs"],
decay_time_ms=spec["decay_time_ms"],
)
# Write as 16-bit WAV (smaller, compatible with scipy)
int16_data = (ir * 32767).astype(np.int16)
wavfile.write(str(path), SAMPLE_RATE, int16_data)
# Quick verification
file_size = path.stat().st_size
duration_ms = (spec["num_taps"] / SAMPLE_RATE) * 1000
logger.info(
f"{path.name} ({spec['num_taps']} taps, "
f"{duration_ms:.1f}ms, {file_size // 1024}KB)"
)
if verify:
# Read back and check
sr, data = wavfile.read(str(path))
assert sr == SAMPLE_RATE, f"Sample rate mismatch: {sr}"
assert len(data) == spec["num_taps"], f"Length mismatch: {len(data)}"
peak = np.max(np.abs(data.astype(np.float64))) / 32767.0
assert peak > 0.1, f"IR {name} peak too low: {peak:.3f}"
created.append(path)
return created
def verify_quality(ir_dir: Path) -> None:
"""Verify that generated IRs have reasonable frequency response."""
import matplotlib
matplotlib.use("Agg") # headless
import matplotlib.pyplot as plt
fig, axes = plt.subplots(len(CABINET_SPECS), 1, figsize=(10, 2 * len(CABINET_SPECS)))
if len(CABINET_SPECS) == 1:
axes = [axes]
for ax, spec in zip(axes, CABINET_SPECS):
path = ir_dir / f"{spec['name']}.wav"
if not path.exists():
continue
sr, data = wavfile.read(str(path))
# Normalize
data_f = data.astype(np.float64) / 32767.0
# FFT for frequency response
fft = np.abs(np.fft.rfft(data_f, n=4096))
freqs = np.fft.rfftfreq(4096, d=1.0 / sr)
# dB scale, normalized
fft_db = 20 * np.log10(fft / np.max(fft) + 1e-10)
ax.semilogx(freqs[1:], fft_db[1:], linewidth=0.8)
ax.axhline(-3, color="gray", linestyle=":", linewidth=0.5)
ax.axhline(-12, color="gray", linestyle=":", linewidth=0.5)
ax.set_xlim(20, sr / 2)
ax.set_ylim(-48, 3)
ax.set_title(f"{spec['display']} — Freq Response")
ax.set_xlabel("Frequency (Hz)")
ax.set_ylabel("dB")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plot_path = ir_dir / "frequency_response.png"
fig.savefig(plot_path, dpi=150)
plt.close(fig)
logger.info(f"Frequency response plot: {plot_path}")
def create_manifest(ir_dir: Path, created: list[Path]) -> Path:
"""Create a README manifest for the IR files."""
manifest = ir_dir / "README.md"
lines = [
"# Pi Multi-FX Pedal — Default Cabinet IR Files",
"",
"Synthetic impulse responses (.wav) for guitar cabinet simulation.",
"Generated using DSP filter cascades (Butterworth LP/HP + parametric",
"peak/notch filters + exponential decay envelope).",
"",
"**All IRs are 48kHz, 16-bit mono WAV files.**",
"",
"| File | Cabinet Model | Taps | Length | Description |",
"|------|--------------|------|--------|-------------|",
]
for spec in CABINET_SPECS:
path = ir_dir / f"{spec['name']}.wav"
if path.exists():
num_taps = spec["num_taps"]
length_ms = (num_taps / SAMPLE_RATE) * 1000
lines.append(
f"| `{spec['name']}.wav` | {spec['display']} | "
f"{num_taps} | {length_ms:.0f}ms | {spec['description']} |"
)
lines.extend([
"",
"## Usage",
"",
"These IRs are loaded by the `IRLoader` class through the pedal's cab simulation.",
"Place them in `~/.pedal/irs/` or reference them by absolute path in preset chain blocks.",
"",
"## Replacement",
"",
"Replace any `.wav` file with a real captured IR (48kHz, 16-bit mono) to",
"upgrade from synthetic to authentic cabinet tone. The pedal treats all",
"`.wav` files in the IR directory identically.",
"",
"## Cabinet Type Guide",
"",
"- **Vintage 1x12** — Fender Deluxe / Princeton-style cleans. Scooped mids, warm lows.",
"- **British 4x12** — Marshall 1960 / JCM-style crunch and rock. Mid-forward, aggressive.",
"- **American 2x12** — Vox AC30-style chime. Complex mids, sparkling treble.",
"- **Modern 4x12** — Mesa Rectifier-style high gain. Tight lows, smooth highs.",
"- **Jazz 1x15** — Polytone / Henriksen-style jazz. Deep lows, scooped mids.",
"- **Boutique 1x12** — Dumble / Two-Rock-style. Smooth, complex midrange.",
"- **Mini 1x8** — Small practice amp. Boxy, lo-fi, vintage radio tone.",
])
manifest.write_text("\n".join(lines) + "\n")
logger.info(f"Manifest written: {manifest}")
return manifest
# ── Main ─────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(description="Generate seed cabinet IR files")
parser.add_argument(
"--dest",
default=str(Path.home() / ".pedal" / "irs"),
help="Output directory (default: ~/.pedal/irs)",
)
parser.add_argument(
"--plot",
action="store_true",
help="Generate frequency response verification plot",
)
args = parser.parse_args()
dest = Path(args.dest)
logger.info(f"Generating {len(CABINET_SPECS)} seed cabinet IRs → {dest}")
logger.info("")
created = generate_all_irs(dest, verify=True)
logger.info("")
logger.info(f"Generated {len(created)} IR files successfully.")
if args.plot:
verify_quality(dest)
create_manifest(dest, created)
logger.info(f"\nAll IRs in: {dest.resolve()}")
if __name__ == "__main__":
main()
+468
View File
@@ -0,0 +1,468 @@
#!/usr/bin/env python3
"""Seed Default NAM Captures — lightweight synthetic captures for development.
Creates 3-5 default Neural Amp Modeler captures that cover the essential
guitar tones: clean, crunch, lead, rhythm, and hi-gain. Each capture uses
the Feather LSTM architecture (~350 parameters, runs in 0.5-3ms on RPi 4).
Usage
-----
# Default: seed into ~/.pedal/nam/default/
python3 scripts/seed_default_captures.py
# Custom target directory
python3 scripts/seed_default_captures.py --output /path/to/models
# Dry run: print what would be created without writing
python3 scripts/seed_default_captures.py --dry-run
# As a module:
from scripts.seed_default_captures import seed_captures
seed_captures(output_dir="/home/pedal/models")
Output
------
For each capture type, a ``.nam`` file is written containing JSON with
architecture, config, and weights keys.
A ``capture_index.json`` catalog is also written for UI discovery,
conforming to the NAMModel metadata shape expected by
``NAMModel`` in ``src/dsp/nam_host.py``.
Dependencies
- ``nam`` Python package (v0.13+) — for model creation
- Only requires stdlib + nam at runtime
"""
import argparse
import json
import os
import random
import sys
from pathlib import Path
try:
import numpy as np
import torch
from nam.models import init_from_nam
except ImportError as exc:
print(f"ERROR: {exc}. Install with: pip install nam", file=sys.stderr)
sys.exit(1)
# ── Constants ────────────────────────────────────────────────────────────
DEFAULT_OUTPUT = Path.home() / ".pedal" / "nam" / "default"
CAPTURES = [
{
"id": "default_clean",
"name": "Fender Twin Clean",
"type": "clean",
"category": "amp",
"description": "Sparkling clean, slight headroom — edge of breakup when pushed",
"gain_staging": 0.3,
"seed": 42,
"receptive_field": 33,
"latent_size": 8,
"sample_rate": 48000,
"tags": ["clean", "fender", "twin", "default"],
},
{
"id": "default_crunch",
"name": "Marshall Plexi Crunch",
"type": "crunch",
"category": "amp",
"description": "Classic rock crunch with smooth compression — Plexi on 7",
"gain_staging": 0.8,
"seed": 137,
"receptive_field": 33,
"latent_size": 8,
"sample_rate": 48000,
"tags": ["crunch", "marshall", "plexi", "rock", "default"],
},
{
"id": "default_lead",
"name": "Soldano Lead",
"type": "lead",
"category": "amp",
"description": "Sustaining lead tone with fluid mids — singing harmonics",
"gain_staging": 1.4,
"seed": 256,
"receptive_field": 65,
"latent_size": 8,
"sample_rate": 48000,
"tags": ["lead", "soldano", "high-gain", "sustain", "default"],
},
{
"id": "default_rhythm",
"name": "Vox AC30 Rhythm",
"type": "rhythm",
"category": "amp",
"description": "Chimey mid-gain rhythm — jangle with punch",
"gain_staging": 0.6,
"seed": 73,
"receptive_field": 33,
"latent_size": 8,
"sample_rate": 48000,
"tags": ["rhythm", "vox", "ac30", "jangle", "default"],
},
{
"id": "default_high_gain",
"name": "5150 Hi-Gain",
"type": "hi-gain",
"category": "amp",
"description": "Modern high-gain with tight low-end — for metal and hard rock",
"gain_staging": 2.0,
"seed": 512,
"receptive_field": 65,
"latent_size": 12,
"sample_rate": 48000,
"tags": ["hi-gain", "5150", "metal", "modern", "default"],
},
]
# ── Helpers ──────────────────────────────────────────────────────────────
def _lorenz(x0, y0, z0, sigma=10.0, rho=28.0, beta=8.0 / 3.0, steps=256):
"""Generate a chaotic attractor sequence (non-linear dynamics → amp-like)."""
xs, ys, zs = [], [], []
x, y, z = x0, y0, z0
dt = 0.01
for _ in range(steps):
dx = sigma * (y - x)
dy = x * (rho - z) - y
dz = x * y - beta * z
x += dx * dt
y += dy * dt
z += dz * dt
xs.append(x)
ys.append(y)
zs.append(z)
return np.array(ys)
def _generate_feather_weights(
gain_staging: float,
receptive_field: int,
latent_size: int,
seed: int,
sample_rate: int = 48000,
) -> dict:
"""Generate synthetic Feather-style LSTM weights for a NAM capture.
Uses deterministic chaotic dynamics (Lorenz attractor) seeded by the
capture type to produce structured, non-random weight vectors. The
result is a set of weights that a Feather LSTM model can load —
different captures get different chaotic signatures → different
tonal responses.
Returns a dict matching the ``init_from_nam()`` format:
{architecture, config, weights, sample_rate}
"""
rng = random.Random(seed)
torch.manual_seed(seed)
# Feather LSTM: 1 LSTM layer, latent_size hidden, input_width=1 (audio)
input_width = 1
lstm_hidden = latent_size
# output net: linear layer from latent_size -> 1
# --- Compute exact parameter count for LSTM with this config ---
# LSTM import_weights layout (per layer):
# Combined gate matrix: w_ih + w_hh = (4*hidden) * (input + hidden) elements
# bias_ih: 4 * hidden elements
# initial_hidden: hidden elements
# initial_cell: hidden elements
# head (Linear(hidden, 1)) weight: hidden elements + 1 bias = hidden + 1
per_layer_gate = 4 * latent_size * (input_width + latent_size)
per_layer_bias = 4 * latent_size
per_layer_states = 2 * latent_size # initial_hidden + initial_cell
head_params = latent_size + 1 # Linear weight + bias
total_params = per_layer_gate + per_layer_bias + per_layer_states + head_params
# Generate weights using Lorenz attractor for structured non-linearity
sig = _lorenz(
x0=float(seed),
y0=float(seed * 1.1),
z0=float(seed * 0.9),
steps=total_params + 500,
)
# Take the last `total_params` values and shape into weights
sig = sig[-total_params:]
# Apply gain staging + deterministic scaling
sig = sig * gain_staging * 0.1
# Normalize to prevent exploding activations
sig = sig / (np.std(sig) + 1e-8)
sig = np.clip(sig, -3.0, 3.0)
weights = sig.tolist()
# Build the config dict
# Build the LSTM config dict — only keys that LSTM.__init__ accepts.
# LSTM.__init__(self, hidden_size, input_size=1, sample_rate=None, **lstm_kwargs)
# Extra keys go into **lstm_kwargs → nn.LSTM(...) and would crash.
# Metadata (receptive_field, latent_size, type etc.) sit at TOP level,
# outside "config", so init_from_nam never sees them.
config = {
"architecture": "LSTM",
"config": {
"hidden_size": latent_size,
"input_size": input_width,
},
"weights": weights,
"sample_rate": sample_rate,
}
return config
def _build_nam_file(config: dict) -> dict:
"""Test that ``init_from_nam()`` can load this config.
Returns the config itself (pass-through). Raises if the model can't
be instantiated.
"""
# Validate that the model initializes
try:
model = init_from_nam(config)
# Run dry inference to check shapes
dummy = torch.randn(1, 256) # one block of audio
with torch.no_grad():
out = model(dummy)
# Note: actual model param count may exceed the flat weight array
# because some params (bias_hh) are zeroed internally by import_weights
# rather than loaded from the file. Only verify inference works.
assert out.shape == dummy.shape, (
f"Output shape {out.shape} != input shape {dummy.shape}"
)
except Exception as exc:
raise RuntimeError(f"Failed to validate model: {exc}") from exc
return config
def _write_capture(
output_dir: Path,
capture_def: dict,
dry_run: bool = False,
) -> Path:
"""Generate and write one NAM capture file.
Returns the path to the written file.
"""
nam_config = _generate_feather_weights(
gain_staging=capture_def["gain_staging"],
receptive_field=capture_def["receptive_field"],
latent_size=capture_def["latent_size"],
seed=capture_def["seed"],
sample_rate=capture_def["sample_rate"],
)
# Add metadata header for scan_models compatibility
# The nam_host.py list_available_models() reads: architecture, config.receptive_field
# We also embed UI-facing metadata
nam_config["name"] = capture_def["name"]
nam_config["type"] = capture_def["type"]
nam_config["tags"] = capture_def["tags"]
nam_config["description"] = capture_def["description"]
nam_config["id"] = capture_def["id"]
nam_config["category"] = capture_def["category"]
# Validate the model can be built
_build_nam_file(nam_config)
output_path = output_dir / f"{capture_def['id']}.nam"
if dry_run:
param_count = len(nam_config["weights"])
size_kb = param_count * 4 / 1024 # float32
print(f" [DRY-RUN] Would write: {output_path}")
print(f" Name: {capture_def['name']}")
print(f" Type: {capture_def['type']}")
print(f" Params: {param_count} ({size_kb:.1f} KB)")
return output_path
with open(output_path, "w") as f:
json.dump(nam_config, f, indent=2)
size_kb = output_path.stat().st_size / 1024
print(f" Wrote: {output_path.name} ({capture_def['name']}, {size_kb:.1f} KB)")
return output_path
def _write_catalog(
output_dir: Path,
capture_paths: list[Path],
capture_defs: list[dict],
dry_run: bool = False,
) -> Path:
"""Write a ``capture_index.json`` catalog for UI consumption.
This index mirrors the structure returned by
``NAMHost.list_available_models()`` in ``src/dsp/nam_host.py``
plus additional UI metadata (type, tags, description).
"""
entries = []
for path, cdef in zip(capture_paths, capture_defs):
size_bytes = 0 if dry_run else path.stat().st_size
entries.append({
"id": cdef["id"],
"name": cdef["name"],
"type": cdef["type"],
"category": cdef["category"],
"description": cdef["description"],
"tags": cdef["tags"],
"path": str(path),
"size_bytes": size_bytes,
"size_kb": round(size_bytes / 1024, 1),
"architecture": "LSTM",
"receptive_field": cdef["receptive_field"],
"latent_size": cdef["latent_size"],
"sample_rate": cdef["sample_rate"],
"is_default": True,
})
catalog_path = output_dir / "capture_index.json"
catalog = {
"version": 1,
"captures": entries,
}
if dry_run:
print(f" [DRY-RUN] Would write catalog: {catalog_path} ({len(entries)} entries)")
return catalog_path
with open(catalog_path, "w") as f:
json.dump(catalog, f, indent=2)
print(f" Catalog: {catalog_path.name} ({len(entries)} entries, "
f"{catalog_path.stat().st_size / 1024:.1f} KB)")
return catalog_path
# ── Public API ───────────────────────────────────────────────────────────
def seed_captures(
output_dir: str | Path = DEFAULT_OUTPUT,
capture_types: list[str] | None = None,
dry_run: bool = False,
) -> list[Path]:
"""Seed default NAM captures into ``output_dir``.
Parameters
----------
output_dir : str or Path
Directory to write captures into. Created if it doesn't exist.
capture_types : list of str or None
Filter which captures to generate by type (e.g. ``["clean", "lead"]``).
``None`` generates all defined captures.
dry_run : bool
If True, print what would be done without writing files.
Returns
-------
list[Path]
Paths to the written ``.nam`` files.
"""
output_dir = Path(output_dir)
capture_defs = CAPTURES
if capture_types is not None:
capture_defs = [c for c in CAPTURES if c["type"] in capture_types]
if not capture_defs:
print(f"No captures match the requested types: {capture_types}")
return []
if dry_run:
print(f"Dry-run: would seed {len(capture_defs)} captures to {output_dir}")
else:
output_dir.mkdir(parents=True, exist_ok=True)
print(f"Seeding {len(capture_defs)} captures to {output_dir}")
capture_paths = []
for cdef in capture_defs:
path = _write_capture(output_dir, cdef, dry_run=dry_run)
capture_paths.append(path)
_write_catalog(output_dir, capture_paths, capture_defs, dry_run=dry_run)
total_params_approx = sum(
(4 * c["latent_size"] * 1) # input-hidden gates
+ (4 * c["latent_size"] * c["latent_size"]) # hidden-hidden gates
+ c["latent_size"] # output projection
for c in capture_defs
)
approx_size_kb = total_params_approx * 4 / 1024
print(f"\nSummary: {len(capture_paths)} captures, ~{total_params_approx} total params, "
f"~{approx_size_kb:.0f} KB total (uncompressed)")
return capture_paths
# ── CLI ──────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Seed default NAM captures for development and testing",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--output", "-o",
type=Path,
default=DEFAULT_OUTPUT,
help=f"Output directory (default: {DEFAULT_OUTPUT})",
)
parser.add_argument(
"--types", "-t",
nargs="+",
choices=["clean", "crunch", "lead", "rhythm", "hi-gain"],
help="Specific capture types to generate (default: all)",
)
parser.add_argument(
"--dry-run", "-n",
action="store_true",
help="Print what would be created without writing files",
)
parser.add_argument(
"--force", "-f",
action="store_true",
help="Overwrite existing .nam files",
)
args = parser.parse_args()
# Check for existing captures
if not args.dry_run and args.output.exists() and not args.force:
existing = list(args.output.glob("default_*.nam"))
if existing:
print(
f"Warning: {args.output} already contains {len(existing)} default "
f"capture(s). Use --force to overwrite.",
file=sys.stderr,
)
if not input("Continue anyway? [y/N] ").lower().startswith("y"):
print("Aborted.")
sys.exit(1)
paths = seed_captures(
output_dir=args.output,
capture_types=args.types,
dry_run=args.dry_run,
)
if not args.dry_run:
print(f"\nDone. {len(paths)} captures written to {args.output}")
print("To use: add a preset that references one of these .nam files, or")
print("point NAMHost(models_dir=...) at this directory for scan_models().")
if __name__ == "__main__":
main()
+565
View File
@@ -0,0 +1,565 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pi Multi-FX Pedal — UI Mockup Template</title>
<style>
/* Figma-like canvas */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #1e1e1e;
display: flex;
justify-content: center;
padding: 40px 20px;
-webkit-font-smoothing: antialiased;
}
.canvas {
background: #2c2c2c;
border-radius: 12px;
padding: 40px;
width: 1080px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.canvas-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
color: #888;
font-size: 12px;
letter-spacing: 0.5px;
}
.canvas-header h1 {
font-size: 13px;
font-weight: 600;
color: #aaa;
}
.canvas-header span {
color: #666;
}
/* Frame styles — like Figma frames */
.frame {
background: #3a3a3a;
border: 1px solid #4a4a4a;
border-radius: 8px;
padding: 16px;
margin-bottom: 24px;
position: relative;
}
.frame-label {
position: absolute;
top: -8px;
left: 12px;
background: #3a3a3a;
padding: 0 6px;
font-size: 10px;
color: #888;
letter-spacing: 0.3px;
font-weight: 500;
}
.frame-title {
font-size: 11px;
color: #999;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Device screen mockup */
.device {
display: flex;
gap: 24px;
margin-bottom: 24px;
}
.touchscreen {
background: #1a1a2e;
border: 2px solid #555;
border-radius: 12px;
padding: 16px;
width: 480px;
height: 320px;
position: relative;
overflow: hidden;
}
.touchscreen .status-bar {
display: flex;
justify-content: space-between;
font-size: 10px;
color: #666;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #2a2a3e;
}
/* UI elements on the screen */
.ui-rig {
display: flex;
gap: 6px;
margin-bottom: 10px;
}
.amp-block {
background: #2a2a4a;
border-radius: 6px;
padding: 8px;
flex: 1;
text-align: center;
}
.amp-block .knob {
width: 24px;
height: 24px;
border-radius: 50%;
background: #444;
margin: 0 auto 4px;
border: 2px solid #666;
cursor: grab;
}
.amp-block .knob-label {
font-size: 7px;
color: #888;
}
.amp-block .value {
font-size: 8px;
color: #aaa;
margin-top: 2px;
}
/* FX chain row */
.fx-chain {
display: flex;
gap: 4px;
margin: 10px 0;
}
.fx-block {
background: #2a2a3a;
border-radius: 4px;
padding: 6px 8px;
font-size: 8px;
color: #ccc;
text-align: center;
flex: 1;
border: 1px solid #3a3a4a;
min-width: 60px;
}
.fx-block.active {
border-color: #4a9;
background: #1a3a2a;
}
.fx-block.bypassed {
opacity: 0.4;
}
/* Tuner overlay */
.tuner-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.85);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tuner-note {
font-size: 48px;
color: #4a9;
font-weight: 700;
letter-spacing: 4px;
}
.tuner-meter {
width: 200px;
height: 4px;
background: #333;
border-radius: 2px;
margin-top: 12px;
position: relative;
}
.tuner-meter .indicator {
position: absolute;
left: 48%;
width: 4px;
height: 4px;
background: #4a9;
border-radius: 2px;
}
/* Footswitch row */
.footswitch-row {
display: flex;
gap: 12px;
margin-top: 12px;
justify-content: center;
}
.footswitch {
width: 44px;
height: 44px;
border-radius: 50%;
background: #333;
border: 2px solid #555;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
color: #aaa;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.4), inset 0 -2px 4px rgba(0,0,0,0.3);
}
.footswitch.active {
border-color: #4a9;
color: #4a9;
box-shadow: 0 0 12px rgba(68, 153, 153, 0.3);
}
/* Label / shape palette (Figa element toolbox) */
.palette {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px;
background: #333;
border-radius: 6px;
margin-bottom: 16px;
}
.palette-label {
font-size: 10px;
color: #888;
width: 100%;
margin-bottom: 4px;
}
.palette-item {
background: #444;
border-radius: 4px;
padding: 6px 10px;
font-size: 10px;
color: #ccc;
cursor: grab;
border: 1px solid transparent;
}
.palette-item.shape {
background: #3a3a5a;
border-color: #5a5a8a;
}
.palette-item.text {
background: #3a4a3a;
border-color: #5a8a5a;
}
.palette-item.knob {
background: #4a3a3a;
border-color: #8a5a5a;
}
/* Schematic layout area */
.schematic {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-top: 8px;
}
.schematic .column {
background: #333;
border-radius: 6px;
padding: 12px;
min-height: 160px;
}
.schematic .column-title {
font-size: 10px;
color: #777;
margin-bottom: 8px;
}
/* Annotation arrows */
.arrow-annotation {
display: flex;
align-items: center;
gap: 8px;
color: #666;
font-size: 10px;
margin: 4px 0;
}
.arrow-line {
flex: 1;
height: 1px;
background: #555;
position: relative;
}
.arrow-line::after {
content: '▶';
position: absolute;
right: -12px;
top: -6px;
font-size: 8px;
color: #666;
}
/* Four-cable method diagram */
.signal-flow {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px;
background: #2a2a2a;
border-radius: 6px;
margin-bottom: 16px;
}
.signal-flow .node {
background: #444;
border-radius: 4px;
padding: 6px 10px;
font-size: 9px;
color: #aaa;
text-align: center;
min-width: 60px;
}
.signal-flow .arrow {
color: #666;
font-size: 14px;
}
.signal-flow .split {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
/* Colors */
.color-swatches {
display: flex;
gap: 8px;
margin: 8px 0;
}
.swatch {
width: 24px;
height: 24px;
border-radius: 4px;
border: 1px solid #555;
}
</style>
</head>
<body>
<div class="canvas">
<div class="canvas-header">
<h1>🎛️ Pi Multi-FX Pedal UI — Figma Template</h1>
<span>v1 · 480×320 touchscreen · 4.3" DSI</span>
</div>
<!-- Color palette -->
<div class="frame" style="margin-bottom: 16px;">
<div class="frame-label">COLORS</div>
<div class="color-swatches">
<div class="swatch" style="background:#1a1a2e;" title="Screen BG"></div>
<div class="swatch" style="background:#2a2a4a;" title="Card bg"></div>
<div class="swatch" style="background:#4a9;" title="Accent green"></div>
<div class="swatch" style="background:#e85;" title="Warning orange"></div>
<div class="swatch" style="background:#f44;" title="Clip red"></div>
<div class="swatch" style="background:#48f;" title="Blue (MIDI)"></div>
<div class="swatch" style="background:#888;" title="Text muted"></div>
<div class="swatch" style="background:#ccc;" title="Text primary"></div>
</div>
<div style="font-size:9px; color:#666; margin-top:4px;">bg=#1a1a2e · card=#2a2a4a · accent=#44aa99 · warn=#ee8855 · clip=#ff4444 · midi=#4488ff</div>
</div>
<!-- Element palette -->
<div class="palette">
<div class="palette-label">🧩 ELEMENTS</div>
<div class="palette-item shape">▭ Knob (48×48)</div>
<div class="palette-item shape">▭ FX Block (72×48)</div>
<div class="palette-item shape">▭ Footswitch ⏺</div>
<div class="palette-item text">T Title</div>
<div class="palette-item text">T Label 10px</div>
<div class="palette-item text">T Value 8px</div>
<div class="palette-item knob">📊 VU Meter</div>
<div class="palette-item knob">📡 Tuner</div>
<div class="palette-item shape">▭ Preset Slot</div>
<div class="palette-item shape">▭ Divider</div>
</div>
<!-- Device preview -->
<div class="frame">
<div class="frame-label">MAIN SCREEN — RIG VIEW</div>
<div class="device">
<div class="touchscreen">
<div class="status-bar">
<span>🔊 Preset 03 — Crunch</span>
<span>⚡ 34% | 🎛️ BNK 1</span>
</div>
<!-- Amp section -->
<div class="frame-title">AMP</div>
<div class="ui-rig">
<div class="amp-block"><div class="knob"></div><div class="knob-label">GAIN</div><div class="value">68</div></div>
<div class="amp-block"><div class="knob"></div><div class="knob-label">BASS</div><div class="value">52</div></div>
<div class="amp-block"><div class="knob"></div><div class="knob-label">MID</div><div class="value">45</div></div>
<div class="amp-block"><div class="knob"></div><div class="knob-label">TREBLE</div><div class="value">61</div></div>
<div class="amp-block"><div class="knob"></div><div class="knob-label">VOL</div><div class="value">72</div></div>
</div>
<!-- FX chain -->
<div class="frame-title">FX CHAIN</div>
<div class="fx-chain">
<div class="fx-block active">🎸 NS Gate</div>
<div class="fx-block active">🔊 Comp</div>
<div class="fx-block active">🔥 OD</div>
<div class="fx-block bypassed">🌀 Chorus</div>
<div class="fx-block active">⏳ Delay</div>
<div class="fx-block active">🌊 Reverb</div>
</div>
<!-- Footswitch indicators -->
<div class="footswitch-row">
<div class="footswitch active">A</div>
<div class="footswitch">B</div>
<div class="footswitch">C</div>
<div class="footswitch">D</div>
<div class="footswitch"></div>
<div class="footswitch"></div>
</div>
</div>
<!-- Side annotation -->
<div style="flex:1; padding: 8px;">
<div class="frame-title" style="color:#777;">SCREEN LAYOUT</div>
<div class="arrow-annotation"><span>Status bar</span><div class="arrow-line"></div><span style="color:#aaa;">preset name + CPU</span></div>
<div class="arrow-annotation"><span>AMP section</span><div class="arrow-line"></div><span style="color:#aaa;">5 knobs, values inline</span></div>
<div class="arrow-annotation"><span>FX chain row</span><div class="arrow-line"></div><span style="color:#aaa;">scrollable, active/bypassed</span></div>
<div class="arrow-annotation"><span>Footswitch bar</span><div class="arrow-line"></div><span style="color:#aaa;">A-D + bank nav</span></div>
<div style="margin-top:16px; border-top:1px solid #3a3a3a; padding-top:12px;">
<div class="frame-title" style="color:#777;">ALTERNATE SCREENS</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<div class="palette-item">🎛️ FX Edit</div>
<div class="palette-item">📋 Presets</div>
<div class="palette-item">🎸 Captures</div>
<div class="palette-item">🏚️ IRs</div>
<div class="palette-item">⚙️ Settings</div>
<div class="palette-item">📡 Tuner</div>
</div>
</div>
</div>
</div>
</div>
<!-- Signal flow diagram -->
<div class="frame">
<div class="frame-label">SIGNAL FLOW — 4CM</div>
<div class="signal-flow">
<div class="node">🎸 Guitar</div>
<div class="arrow"></div>
<div class="node">Noise Gate</div>
<div class="arrow"></div>
<div class="node">Compressor</div>
<div class="arrow"></div>
<div class="node">OD / Dist</div>
<div class="arrow"></div>
<div class="split">
<div style="display:flex; gap:4px;">
<div class="node" style="background:#3a3a5a;">PRE blocks</div>
<div class="arrow"></div>
<div class="node" style="border-color:#4a9;">SEND → Amp</div>
</div>
<div style="display:flex; gap:4px; margin-top:4px;">
<div class="node" style="border-color:#4a9;">RETURN ← FX</div>
<div class="arrow"></div>
<div class="node" style="background:#3a3a5a;">POST blocks</div>
</div>
</div>
<div class="arrow"></div>
<div class="node">Delay</div>
<div class="arrow"></div>
<div class="node">Reverb</div>
<div class="arrow"></div>
<div class="node">🔊 Output</div>
</div>
</div>
<!-- FX Edit screen -->
<div class="schematic">
<div class="column">
<div class="column-title">🎛️ FX EDIT PANEL (tap a block)</div>
<div style="display:flex; gap:6px; flex-wrap:wrap;">
<div class="amp-block" style="width:56px;"><div class="knob" style="width:28px;height:28px;"></div><div class="knob-label">MIX</div></div>
<div class="amp-block" style="width:56px;"><div class="knob" style="width:28px;height:28px;"></div><div class="knob-label">TIME</div></div>
<div class="amp-block" style="width:56px;"><div class="knob" style="width:28px;height:28px;"></div><div class="knob-label">FDBK</div></div>
<div class="amp-block" style="width:56px;"><div class="knob" style="width:28px;height:28px;"></div><div class="knob-label">TONE</div></div>
</div>
<div style="font-size:9px;color:#666;margin-top:8px;">Per-FX params from block-params schema</div>
</div>
<div class="column">
<div class="column-title">📋 PRESET BROWSER</div>
<div style="display:flex; flex-direction:column; gap:4px;">
<div style="background:#2a2a4a; border-radius:4px; padding:6px 8px; display:flex; justify-content:space-between;">
<span style="color:#4a9; font-size:10px;">01 Clean</span>
<span style="color:#666; font-size:9px;">Fender Twin</span>
</div>
<div style="background:#2a2a4a; border-radius:4px; padding:6px 8px; display:flex; justify-content:space-between;">
<span style="color:#ccc; font-size:10px;">02 Blues</span>
<span style="color:#666; font-size:9px;">Marshall Bluesbreaker</span>
</div>
<div style="background:#3a3a5a; border-radius:4px; padding:6px 8px; display:flex; justify-content:space-between; border:1px solid #4a9;">
<span style="color:#4a9; font-size:10px; font-weight:600;">03 Crunch</span>
<span style="color:#666; font-size:9px;">▶ Playing</span>
</div>
<div style="background:#2a2a4a; border-radius:4px; padding:6px 8px; display:flex; justify-content:space-between;">
<span style="color:#666; font-size:10px; opacity:0.6;">04 Lead</span>
<span style="color:#555; font-size:9px;">——</span>
</div>
</div>
</div>
</div>
<!-- VU Meter component -->
<div class="frame" style="margin-top: 16px;">
<div class="frame-label">VU METERS</div>
<div style="display:flex; gap:24px; align-items:center;">
<div style="flex:1;">
<div style="font-size:9px; color:#777; margin-bottom:4px;">INPUT</div>
<div style="display:flex; gap:2px; align-items:flex-end; height:40px;">
<div style="width:8px; background:#4a9; height:24px; border-radius:2px;"></div>
<div style="width:8px; background:#4a9; height:28px; border-radius:2px;"></div>
<div style="width:8px; background:#4a9; height:20px; border-radius:2px;"></div>
<div style="width:8px; background:#4a9; height:32px; border-radius:2px;"></div>
<div style="width:8px; background:#4a9; height:26px; border-radius:2px;"></div>
<div style="width:8px; background:#4a9; height:16px; border-radius:2px;"></div>
<div style="width:8px; background:#e85; height:10px; border-radius:2px;"></div>
<div style="width:8px; background:#e85; height:6px; border-radius:2px;"></div>
<div style="width:8px; background:#f44; height:3px; border-radius:2px;"></div>
</div>
</div>
<div style="flex:1;">
<div style="font-size:9px; color:#777; margin-bottom:4px;">OUTPUT</div>
<div style="display:flex; gap:2px; align-items:flex-end; height:40px;">
<div style="width:8px; background:#4a9; height:28px; border-radius:2px;"></div>
<div style="width:8px; background:#4a9; height:32px; border-radius:2px;"></div>
<div style="width:8px; background:#4a9; height:36px; border-radius:2px;"></div>
<div style="width:8px; background:#4a9; height:30px; border-radius:2px;"></div>
<div style="width:8px; background:#4a9; height:22px; border-radius:2px;"></div>
<div style="width:8px; background:#4a9; height:14px; border-radius:2px;"></div>
<div style="width:8px; background:#e85; height:8px; border-radius:2px;"></div>
<div style="width:8px; background:#f44; height:4px; border-radius:2px;"></div>
<div style="width:8px; background:#f44; height:2px; border-radius:2px;"></div>
</div>
</div>
<div style="border-left:1px solid #3a3a3a; padding-left:16px;">
<div style="font-size:9px; color:#777;">PEAK</div>
<div style="font-size:18px; color:#4a9; font-weight:700;">-6.2</div>
<div style="font-size:9px; color:#666;">dBu</div>
</div>
</div>
</div>
<!-- Notes -->
<div style="margin-top:16px; padding:12px; background:#2a2a2a; border-radius:6px; border-left: 3px solid #4a9;">
<div style="font-size:10px; color:#888; font-weight:600; margin-bottom:4px;">📝 DESIGN NOTES</div>
<div style="font-size:10px; color:#666; line-height:1.6;">
• Screen: 480×320 @ 4.3" DSI — design at 1x, renders pixel-perfect on the LCD<br>
• Color scheme: dark bg (#1a1a2e) with green accent (#4a9) for active state<br>
• Knobs: 48×48px on touch targets (Fitts-friendly), 24×24px in the mockup here<br>
• FX chain row scrolls horizontally if &gt;6 blocks — indicator dots at bottom<br>
• Status bar: preset name left, CPU/memory right. Red if &gt;80% CPU<br>
• Footswitch row doubles as page indicator (active = current screen)<br>
• 4CM mode shows SEND/RETURN labels on the split point in the FX chain
</div>
</div>
</div>
</body>
</html>
+40 -1
View File
@@ -312,6 +312,13 @@ class AudioPipeline:
# Cached filter coefficients per block
self._coeffs: dict[str, tuple] = {}
# VU meter level tracking — updated on every process() call
# Smoothed RMS levels (0.01.0) read by web server for live VU meters
self._input_level: float = 0.0
self._output_level: float = 0.0
# Smoothing factor: ~50ms time constant at 48kHz/256 block
self._vu_alpha: float = np.exp(-BLOCK_SIZE / (0.05 * SAMPLE_RATE))
logger.info("Audio pipeline initialized (block=%d, sr=%d)",
BLOCK_SIZE, SAMPLE_RATE)
@@ -373,12 +380,28 @@ class AudioPipeline:
def _process_mono(self, audio_in: np.ndarray) -> np.ndarray:
"""Process a mono block through the full chain (all blocks)."""
# Update input VU level (RMS with envelope smoothing)
in_rms = np.sqrt(np.mean(audio_in ** 2) + _EPS)
self._input_level = (
self._input_level * self._vu_alpha
+ in_rms * (1.0 - self._vu_alpha)
)
buf = audio_in.copy()
for idx, entry in enumerate(self._chain):
if entry["bypass"] or not entry["enabled"]:
continue
buf = self._process_single_block(buf, idx, entry)
return buf * self._master_volume
out = buf * self._master_volume
# Update output VU level
out_rms = np.sqrt(np.mean(out ** 2) + _EPS)
self._output_level = (
self._output_level * self._vu_alpha
+ out_rms * (1.0 - self._vu_alpha)
)
return out
def _process_4cm(self, audio_in: np.ndarray) -> np.ndarray:
"""Process stereo block with 4CM split routing.
@@ -397,6 +420,14 @@ class AudioPipeline:
"""
ch0 = audio_in[0, :].copy()
ch1 = audio_in[1, :].copy()
# Update input VU level from the guitar input channel (ch0)
in_rms = np.sqrt(np.mean(ch0 ** 2) + _EPS)
self._input_level = (
self._input_level * self._vu_alpha
+ in_rms * (1.0 - self._vu_alpha)
)
bp = self._routing_breakpoint
for idx, entry in enumerate(self._chain):
@@ -413,6 +444,14 @@ class AudioPipeline:
out = np.zeros_like(audio_in)
out[0, :] = ch0 * self._master_volume
out[1, :] = ch1 * self._master_volume
# Update output VU level from the processed effect return (ch1)
out_rms = np.sqrt(np.mean(out ** 2) + _EPS)
self._output_level = (
self._output_level * self._vu_alpha
+ out_rms * (1.0 - self._vu_alpha)
)
return out
def _process_single_block(self, buf: np.ndarray, idx: int,
+38
View File
@@ -34,6 +34,9 @@ AUTO_SAVE_DELAY_S = 1.0
FACTORY_PRESET_DIR = Path(__file__).resolve().parent.parent.parent / "presets" / "factory"
"""Location of bundled factory preset JSON files."""
FACTORY_IR_DIR = FACTORY_PRESET_DIR / "irs"
"""Location of bundled factory cabinet IR .wav files."""
# ── Serialisation helpers ───────────────────────────────────────────────────
@@ -513,6 +516,41 @@ class PresetManager:
logger.info("Installed %d factory presets", count)
return count
def install_factory_irs(self, overwrite: bool = False) -> int:
"""Install bundled factory cabinet IR .wav files into the IR directory.
Copies IR files from ``FACTORY_IR_DIR`` into the IR loader's default
directory (``~/.pedal/irs/``). Existing files are skipped unless
``overwrite=True``.
Args:
overwrite: If True, overwrite existing IR files with the same name.
Returns:
Number of factory IR files installed.
"""
if not FACTORY_IR_DIR.is_dir():
logger.warning("Factory IR directory not found: %s", FACTORY_IR_DIR)
return 0
from ..dsp.ir_loader import DEFAULT_IR_DIR
dest = DEFAULT_IR_DIR
dest.mkdir(parents=True, exist_ok=True)
count = 0
for src in sorted(FACTORY_IR_DIR.glob("*.wav")):
dst = dest / src.name
if dst.exists() and not overwrite:
logger.debug("Skipping existing IR: %s", dst.name)
continue
shutil.copy2(src, dst)
count += 1
logger.debug("Installed factory IR: %s", dst.name)
if count:
logger.info("Installed %d factory cab IRs into %s", count, dest)
return count
# ── Internal helpers ────────────────────────────────────────────────────
def _preset_path(self, bank: int, program: int) -> Path:
+1
View File
@@ -1,6 +1,7 @@
"""System integration subpackage.
- audio: ALSA/JACK/I2S configuration and lifecycle
- defaults: Default IRs, NAM models, and directory structure
- setup (WIP): First-boot setup scripts
"""
+206
View File
@@ -0,0 +1,206 @@
"""Default NAM models, IRs, and directory structure for the Pi Multi-FX Pedal.
Provides helpers for:
- Getting canonical default directories for IRs and NAM models
- Generating a basic default IR WAV file
- Generating a minimal pass-through NAM model (Linear, identity)
- Ensuring all default directories and fallback files exist at startup
"""
from __future__ import annotations
import json
import logging
from pathlib import Path
import numpy as np
from scipy.io import wavfile
logger = logging.getLogger(__name__)
# ── Canonical paths ────────────────────────────────────────────────────────────
_DEFAULT_IRS_DIR = Path.home() / ".pedal" / "irs" / "default"
_DEFAULT_MODELS_DIR = Path.home() / ".pedal" / "nam" / "default"
_FACTORY_PRESET_DIR = (
Path(__file__).resolve().parent.parent.parent / "presets" / "factory"
)
def get_default_irs_dir() -> Path:
"""Return the canonical directory for default IR files.
This is ``~/.pedal/irs/default/``.
"""
return _DEFAULT_IRS_DIR
def get_default_models_dir() -> Path:
"""Return the canonical directory for default NAM model files.
This is ``~/.pedal/nam/default/``.
"""
return _DEFAULT_MODELS_DIR
# ── Default IR generation ─────────────────────────────────────────────────────
_DEFAULT_IR_SAMPLE_RATE = 48000
_DEFAULT_IR_NUM_SAMPLES = 1024
def generate_default_ir(
path: Path | None = None,
sample_rate: int = _DEFAULT_IR_SAMPLE_RATE,
num_samples: int = _DEFAULT_IR_NUM_SAMPLES,
) -> Path:
"""Generate a simple default impulse response WAV file.
Produces a short exponentially-decaying impulse (not silence) so
convolution will colour the sound subtly rather than mute it.
Parameters
----------
path : Path or None
Destination path. Defaults to ``~/.pedal/irs/default/default_ir.wav``.
sample_rate : int
Sample rate in Hz (default 48000).
num_samples : int
Length of the IR in samples (default 1024, 21 ms @ 48 kHz).
Returns
-------
Path
The path the WAV file was written to.
"""
if path is None:
path = _DEFAULT_IRS_DIR / "default_ir.wav"
path.parent.mkdir(parents=True, exist_ok=True)
# A short decaying impulse: 1 at sample 0, then exponential decay.
# Using a mild decay constant so the IR is audibly non-trivial.
t = np.arange(num_samples, dtype=np.float32)
ir = np.exp(-t / (num_samples * 0.15)) # decay tail
ir[0] = 1.0 # initial impulse
# Normalise to [-1, 1]
peak = np.max(np.abs(ir))
if peak > 0:
ir = ir / peak
# Write as 16-bit PCM WAV
wavfile.write(str(path), sample_rate, (ir * 32767).astype(np.int16))
logger.info("Generated default IR: %s (%d samples, %d Hz)", path, num_samples, sample_rate)
return path
# ── Pass-through NAM model generation ─────────────────────────────────────────
_NAM_PASSTHROUGH = {
"architecture": "Linear",
"config": {
"receptive_field": 1,
"bias": False,
},
"weights": [1.0],
}
def generate_passthrough_nam(path: Path | None = None) -> Path:
"""Generate a minimal pass-through NAM model file (Linear, identity).
The model is a ``Conv1d(1, 1, 1)`` with weight = ``[1.0]`` and no bias.
Loading this model in :class:`~src.dsp.nam_host.NAMHost` results in a
transparent pass-through audio is not modified.
Parameters
----------
path : Path or None
Destination path. Defaults to ``~/.pedal/nam/default/pass_through.nam``.
Returns
-------
Path
The path the .nam file was written to.
"""
if path is None:
path = _DEFAULT_MODELS_DIR / "pass_through.nam"
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
json.dump(_NAM_PASSTHROUGH, f)
logger.info("Generated pass-through NAM model: %s", path)
return path
# ── Factory preset directories ────────────────────────────────────────────────
_BASE_DIRS = [
Path.home() / ".pedal",
Path.home() / ".pedal" / "presets",
Path.home() / ".pedal" / "irs",
Path.home() / ".pedal" / "nam",
]
def _ensure_base_dirs() -> None:
"""Create the base ``~/.pedal`` directory tree if it doesn't exist."""
for d in _BASE_DIRS:
d.mkdir(parents=True, exist_ok=True)
# ── Main entry point ──────────────────────────────────────────────────────────
def ensure_defaults_exist() -> bool:
"""Ensure all default directories, IRs, and NAM models exist on disk.
Creates the following if missing:
* ``~/.pedal/``, ``~/.pedal/presets/``, ``~/.pedal/irs/``, ``~/.pedal/nam/``
* ``~/.pedal/irs/default/default_ir.wav`` (exponentially-decaying impulse)
* ``~/.pedal/nam/default/pass_through.nam`` (Linear identity model)
Returns
-------
bool
``True`` if all resources exist or were successfully created.
"""
try:
_ensure_base_dirs()
# Generate default IR if it doesn't exist
default_ir_path = _DEFAULT_IRS_DIR / "default_ir.wav"
if not default_ir_path.exists():
generate_default_ir(default_ir_path)
else:
logger.debug("Default IR already exists: %s", default_ir_path)
# Generate pass-through NAM model if it doesn't exist
default_nam_path = _DEFAULT_MODELS_DIR / "pass_through.nam"
if not default_nam_path.exists():
generate_passthrough_nam(default_nam_path)
else:
logger.debug("Default NAM model already exists: %s", default_nam_path)
return True
except Exception as exc:
logger.warning("Failed to ensure defaults exist: %s", exc)
return False
# ── Standalone CLI ────────────────────────────────────────────────────────────
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
ok = ensure_defaults_exist()
print(f"Defaults {'OK' if ok else 'FAILED'}")
print(f" IRs dir: {get_default_irs_dir()}")
print(f" Models dir: {get_default_models_dir()}")
if ok:
print(f" IR file: {'EXISTS' if (get_default_irs_dir() / 'default_ir.wav').exists() else 'MISSING'}")
print(f" NAM file: {'EXISTS' if (get_default_models_dir() / 'pass_through.nam').exists() else 'MISSING'}")
+15 -1
View File
@@ -1004,7 +1004,21 @@ class WebServer:
ir = self.deps.ir_loader
if not pm:
return {"connected": False}
return {
"connected": False,
"current_preset": None,
"bypass": False,
"tuner_enabled": False,
"master_volume": 0.8,
"routing_mode": "mono",
"routing_breakpoint": 7,
"nam_loaded": False,
"nam_model": None,
"ir_loaded": False,
"ir_name": None,
"wifi": {},
"bluetooth": {},
}
try:
bank = pm.current_bank
+14
View File
@@ -0,0 +1,14 @@
** Drc report for test.kicad_pcb **
** Created on 2026-06-10T00:49:30-0400 **
** Report includes: Errors, Warnings **
** Found 1 DRC violations **
[invalid_outline]: Board has malformed outline (no edges found on Edge.Cuts layer)
Local override; error
@(0.0000 mm, 0.0000 mm): PCB
** Found 0 unconnected pads **
** Found 0 Footprint errors **
** End of Report **
+14
View File
@@ -0,0 +1,14 @@
** Drc report for test_empty.kicad_pcb **
** Created on 2026-06-10T00:51:38-0400 **
** Report includes: Errors, Warnings **
** Found 1 DRC violations **
[invalid_outline]: Board has malformed outline (no edges found on Edge.Cuts layer)
Local override; error
@(0.0000 mm, 0.0000 mm): PCB
** Found 0 unconnected pads **
** Found 0 Footprint errors **
** End of Report **
+1
View File
@@ -0,0 +1 @@
TEST123
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+73
View File
@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
+22
View File
@@ -0,0 +1,22 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
},
},
])
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ui</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+3561
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
{
"name": "ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-select": "^2.3.0",
"@radix-ui/react-slider": "^1.4.0",
"@radix-ui/react-slot": "^1.2.5",
"@radix-ui/react-switch": "^1.3.0",
"@radix-ui/react-tabs": "^1.1.14",
"react": "^19.2.6",
"react-dom": "^19.2.6"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^24.12.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12"
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

+184
View File
@@ -0,0 +1,184 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}
+122
View File
@@ -0,0 +1,122 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from './assets/vite.svg'
import heroImg from './assets/hero.png'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<>
<section id="center">
<div className="hero">
<img src={heroImg} className="base" width="170" height="179" alt="" />
<img src={reactLogo} className="framework" alt="React logo" />
<img src={viteLogo} className="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>
Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
</p>
</div>
<button
type="button"
className="counter"
onClick={() => setCount((count) => count + 1)}
>
Count is {count}
</button>
</section>
<div className="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img className="logo" src={viteLogo} alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://react.dev/" target="_blank">
<img className="button-icon" src={reactLogo} alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section>
<div className="ticks"></div>
<section id="spacer"></section>
</>
)
}
export default App
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

+36
View File
@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }
+56
View File
@@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
+79
View File
@@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
+22
View File
@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
+158
View File
@@ -0,0 +1,158 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
+26
View File
@@ -0,0 +1,26 @@
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }
+29
View File
@@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
+55
View File
@@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
+84
View File
@@ -0,0 +1,84 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--radius: 0.625rem;
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.141 0.005 285.823);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.141 0.005 285.823);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.274 0.006 286.033);
--input: oklch(0.274 0.006 286.033);
--ring: oklch(0.442 0.017 285.786);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
}
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
+22
View File
@@ -0,0 +1,22 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
proxy: {
'/api': 'http://pedal.local:8080',
'/ws': {
target: 'ws://pedal.local:8080',
ws: true,
},
},
},
})