Compare commits
66 Commits
loudness-t
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dd64c531d | ||
|
|
e330eeb495 | ||
|
|
00ed8acc57 | ||
|
|
ddc905558f | ||
|
|
bace1ea0b8 | ||
|
|
1f9c86bbde | ||
|
|
4955e01299 | ||
|
|
0a7c917314 | ||
|
|
ef6b91f31a | ||
|
|
0f4d93c111 | ||
|
|
20f52cc860 | ||
|
|
bd39f91b3d | ||
|
|
42b33e43aa | ||
|
|
d9408b3d0e | ||
|
|
66fed3ccfb | ||
|
|
5f156b63ce | ||
|
|
7df6a8f83b | ||
|
|
922a08d138 | ||
|
|
b50f9b92e1 | ||
|
|
b635df23f6 | ||
|
|
77f3eebf44 | ||
|
|
f16065ee20 | ||
|
|
e48b086c22 | ||
|
|
6305d597a1 | ||
|
|
8eb1397043 | ||
|
|
2e695198e5 | ||
|
|
d52e48dce1 | ||
|
|
7aea4a7367 | ||
|
|
d80fd810f5 | ||
|
|
e006be4f8b | ||
|
|
9c4c618b72 | ||
|
|
c44e1081be | ||
|
|
7e89fb631e | ||
|
|
16538a0f46 | ||
|
|
2bcb215725 | ||
|
22863af6ff
|
|||
|
|
225ba3f467 | ||
|
|
a681681fe3 | ||
|
|
b88ad5b696 | ||
|
|
cc044b4cd2 | ||
|
|
f5bbf2beb9 | ||
|
|
acde5c460e | ||
|
|
4f13afc098 | ||
|
|
6f88e094b8 | ||
|
|
211545f5d1 | ||
|
|
ce8fd1ae20 | ||
|
|
2894f0dbda | ||
|
ef40ad804b
|
|||
|
|
314ec3e3bb | ||
|
|
320526e438 | ||
|
|
906470ba2d | ||
|
|
4cd548c643 | ||
|
|
48a946e9b4 | ||
|
|
2bfa50b792 | ||
|
|
d22bbfd325 | ||
|
|
f2a467996a | ||
|
|
ec75582cd6 | ||
|
|
e6c60b18ca | ||
|
|
dc5550122b | ||
|
|
17d03202f3 | ||
|
|
6549b51af9 | ||
|
|
4ae3bdbb1d | ||
|
|
e8f349d6d3 | ||
|
|
d8fcc5d4da | ||
|
|
c653818dbc | ||
|
62a84ce275
|
9
.gitignore
vendored
9
.gitignore
vendored
@@ -3,3 +3,12 @@
|
||||
|
||||
# Local user_eq override consumed by apply.sh (copy from user_eq.example.json)
|
||||
/user_eq.json
|
||||
|
||||
# Generated baked single-stage FIR files & simple graph
|
||||
/15_1/baked-*.wav
|
||||
/baked-*.wav
|
||||
/graph_simple.json
|
||||
|
||||
# Python bytecode cache
|
||||
*.pyc
|
||||
__pycache__/
|
||||
|
||||
131
README.md
131
README.md
@@ -1,14 +1,14 @@
|
||||
# MacBook Pro 15,1 — Custom Speaker DSP Graph
|
||||
# MacBook Pro 15,1 — Warm, Natural Audio DSP for t2linux
|
||||
|
||||
[](https://wiki.t2linux.org/)
|
||||
[](https://github.com/AsahiLinux/asahi-audio)
|
||||
[](https://pipewire.org/)
|
||||
[]()
|
||||
|
||||
A high-performance PipeWire `filter-chain` audio DSP graph for the built-in speakers of the **MacBook Pro 15,1** (2018/2019 Intel T2) running Linux (`t2linux`).
|
||||
based on https://github.com/lemmyg/t2-apple-audio-dsp/tree/master
|
||||
|
||||
[](https://github.com/mynameisdeleted/mbp15-1-audio-dsp) [](https://git.fairfaxmedia.net/t2linux/mbp15-1-audio-dsp.git) [](https://wiki.t2linux.org/) [](https://github.com/AsahiLinux/asahi-audio) [](https://pipewire.org/) []()
|
||||
|
||||
A custom PipeWire `filter-chain` DSP graph engineered to deliver warm, natural audio to **t2linux** on the **MacBook Pro 15,1** (2018/2019 Intel T2)—aimed at matching or beating macOS (OS X) audio quality both subjectively and objectively.
|
||||
|
||||
> [!NOTE]
|
||||
> **Not a kernel driver.** This graph connects to WirePlumber's hidden `alsa_output.platform-sound.RawSpeakers` sink, delivering Apple-grade warm voicing, multiband dynamic control, driver crossover, FIR correction, and hard driver safety backstops.
|
||||
> **Architecture:** This is **not a kernel driver**. The Linux T2 kernel/ALSA stack exposes raw speaker PCM. WirePlumber hides the raw device and splices this graph in front of it (`alsa_output.platform-sound.RawSpeakers`), executing warm voicing EQ, psychoacoustic sub-bass, 8-band dynamic control, driver crossover, FIR correction, and hard driver protection limiters.
|
||||
|
||||
---
|
||||
|
||||
@@ -16,19 +16,20 @@ A high-performance PipeWire `filter-chain` audio DSP graph for the built-in spea
|
||||
|
||||
Upstream graphs (`asahi-audio` / `t2-apple-audio-dsp`) target a measurement-flat response that can sound thin, treble-heavy, and distort at high volumes due to missing driver limiters.
|
||||
|
||||
| Issue in Upstream | Solution in This Graph | Result |
|
||||
| Upstream Limitation | Solution in This Graph | Real-World Result |
|
||||
|---|---|---|
|
||||
| **Thin / Cold Sound** | +3 dB/octave equal-energy warm voicing curve | Rich, warm, balanced audio at all volumes |
|
||||
| **Distortion at High Volume** | Post-FIR driver limiters (`wlim` & `tlim`) | Clean output at 100% volume without amp clipping |
|
||||
| **Woofer Over-Excursion** | 60 Hz high-pass + 8-band multiband compressor | Cone doesn't bottom out on heavy bass beats |
|
||||
| **No Deep Sub-Bass** | Psychoacoustic sub-bass (`virtualbass` via Bankstown) | Extended perceived low-end without physical strain |
|
||||
| **Fixed EQ** | Isolated 8-band `user_eq` preference node | Custom tone presets that survive git updates |
|
||||
| ❄️ **Thin / Cold Sound** | Equal-energy warm voicing curve (+3 dB/octave tilt) | Rich, full, balanced audio across all genres |
|
||||
| 💥 **Distortion at High Volume** | Post-FIR driver limiters (`wlim` @ -2dB, `tlim` @ -1dB) | Crystal clean output at 100% volume with zero amp clipping |
|
||||
| 🔊 **Woofer Over-Excursion** | 60 Hz high-pass + 8-band multiband compressor | Woofers don't bottom out or rattle on heavy bass beats |
|
||||
| 🔇 **No Deep Sub-Bass** | Psychoacoustic sub-bass (`virtualbass` via Bankstown) | Extended perceived low-end without physical cone strain |
|
||||
| 🛡️ **Voice-Coil Overheating** | Dual Woofer & Tweeter Virtual Thermal Guards | Continuous white noise automatically attenuated to 500mW / 75mW |
|
||||
| 🎚️ **Fixed / Rigid EQ** | Isolated 8-band `user_eq` preference node | Custom tone presets that survive git updates |
|
||||
|
||||
---
|
||||
|
||||
## 🎛️ Signal Processing Chain
|
||||
|
||||
The signal flows through tone controls, dynamic management, ISO-226 loudness tracking, FIR driver correction, and physical driver protection limiters:
|
||||
Audio flows through tone controls, dynamic management, ISO-226 equal loudness tracking, FIR driver correction, and physical driver protection limiters:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
@@ -40,8 +41,7 @@ flowchart TD
|
||||
subgraph Stage2 ["2. Dynamics & Headroom Management"]
|
||||
EQ --> VB["🔊 Virtual Bass (Bankstown Sub-Harmonics)"]:::dynamics
|
||||
VB --> MBComp["📊 Multiband Compressor (8-Band LSP)"]:::dynamics
|
||||
MBComp --> Limiter["🛡️ Main Limiter (Broadband Lookahead)"]:::limiter
|
||||
Limiter --> LoudComp["👂 Loudness Comp (ISO-226 Equal Loudness)"]:::dynamics
|
||||
MBComp --> LoudComp["👂 Loudness Comp (ISO-226 2k FFT)"]:::dynamics
|
||||
end
|
||||
|
||||
subgraph Stage3 ["3. Crossover & Driver FIR Correction"]
|
||||
@@ -50,22 +50,23 @@ flowchart TD
|
||||
subgraph Tweeters ["Tweeter Channels"]
|
||||
Copy --> ConvLT["🔊 Tweeter L FIR (convLT)"]:::fir
|
||||
Copy --> ConvRT["🔊 Tweeter R FIR (convRT)"]:::fir
|
||||
ConvLT --> TGuard["🛡️ Tweeter Thermal Guard (75mW / 1.0s Tau)"]:::limiter
|
||||
ConvRT --> TGuard
|
||||
TGuard --> TLim["🛡️ Tweeter Limiter (-1 dB Ceiling)"]:::limiter
|
||||
end
|
||||
|
||||
subgraph Woofers ["Woofer Channels"]
|
||||
Copy --> ConvLW["🔊 Woofer L FIR (convLW)"]:::fir
|
||||
Copy --> ConvRW["🔊 Woofer R FIR (convRW)"]:::fir
|
||||
ConvLW --> WLim["🛡️ Woofer Limiter (-2 dB Ceiling)"]:::limiter
|
||||
ConvRW --> WLim
|
||||
WLim --> WGuard["🛡️ Woofer Thermal Guard (500mW / 2.5s Tau)"]:::limiter
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Stage4 ["4. Driver Safety Backstops & Output"]
|
||||
ConvLT --> TLim["🛡️ Tweeter Limiter (-1 dB Ceiling)"]:::limiter
|
||||
ConvRT --> TLim
|
||||
ConvLW --> WLim["🛡️ Woofer Limiter (-2 dB Ceiling)"]:::limiter
|
||||
ConvRW --> WLim
|
||||
|
||||
subgraph Stage4 ["4. Driver Output"]
|
||||
TLim --> Out["🔈 RawSpeakers Sink"]:::input
|
||||
WLim --> Out
|
||||
WGuard --> Out
|
||||
end
|
||||
|
||||
classDef input fill:#2d3748,stroke:#4a5568,color:#fff;
|
||||
@@ -77,43 +78,66 @@ flowchart TD
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Dual-Driver Virtual Thermal Guards & Power Caps
|
||||
|
||||
Because Linux cannot access the T2 hardware current-sensing ADCs, we implement a **Virtual Voice-Coil Thermal Model** that models continuous electrical power dissipation ($P = V_{\text{rms}}^2 / R_{\text{vc}}$) and exponential cooling into the air gap:
|
||||
|
||||
$$C_{\text{th}} \frac{d\Delta T(t)}{dt} = \frac{V_{\text{rms}}^2(t)}{R_{\text{vc}}} - \frac{\Delta T(t)}{R_{\text{th}}}$$
|
||||
|
||||
### Thermal Guard Configurations
|
||||
|
||||
* **Woofer Thermal Guard (`thermal_guard` @ Post-`wlim`):**
|
||||
* **Continuous Power Cap:** **$1.0\text{ W}$ ($1,000\text{ mW}$)** per channel ($16\%$ of max $6.25\text{ W}$ peak power).
|
||||
* **Linear RMS Threshold (`al`):** `0.4000` ($-7.96\text{ dBFS}$ RMS).
|
||||
* **Heating Tau ($\tau_{\text{heat}}$):** $2,500\text{ ms}$ (2.5 seconds).
|
||||
* **Cooling Tau ($\tau_{\text{cool}}$):** $5,000\text{ ms}$ (5.0 seconds).
|
||||
* **Tweeter Thermal Guard (`tweeter_thermal_guard` @ Pre-`tlim`):**
|
||||
* **Continuous Power Cap:** **$100\text{ mW}$ ($0.100\text{ W}$)** per channel ($1.6\%$ of max $6.25\text{ W}$ peak power).
|
||||
* **Linear RMS Threshold (`al`):** `0.126491` ($-17.96\text{ dBFS}$ RMS).
|
||||
* **Heating Tau ($\tau_{\text{heat}}$):** $1,000\text{ ms}$ (1.0 second).
|
||||
* **Cooling Tau ($\tau_{\text{cool}}$):** $2,000\text{ ms}$ (2.0 seconds, fast decay recovery from Zoom/WebRTC glitches).
|
||||
|
||||
> **Acoustic Result:** Dynamic music (Blues, Classical, Rock) has high crest factor ($12\text{ dB} - 18\text{ dB}$) and operates at $0\text{ dB}$ gain reduction with full peak dynamics. Continuous high-power signals (full-volume white noise or sine sweeps) are automatically attenuated to harmless power levels after ~2 seconds.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Install Dependencies
|
||||
Installs required LSP & SWH plugins via your distro package manager (`dnf`, `pacman`, `apt`, `zypper`) and builds Bankstown from source:
|
||||
Installs required LSP & SWH plugins via your package manager (`dnf`, `pacman`, `apt`, `zypper`) and builds Bankstown from source:
|
||||
```bash
|
||||
./install-deps.sh
|
||||
```
|
||||
|
||||
### 2. Apply Graph
|
||||
Preflights FIR paths and plugin URIs, builds the effective graph, copies it to WirePlumber, and reloads:
|
||||
Preflights FIR paths and plugin URIs, merges user EQ overrides, copies the configuration to WirePlumber, and reloads:
|
||||
```bash
|
||||
./apply.sh
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Ensure the **"MacBook Pro 15,1 DSP Speakers"** sink is selected in your desktop sound settings.
|
||||
> Ensure **"MacBook Pro 15,1 DSP Speakers"** is selected as the default output in your desktop sound settings.
|
||||
|
||||
---
|
||||
|
||||
## 🎚️ Sound Profiles & User EQ
|
||||
|
||||
Customize tone settings without modifying calibrated internal DSP stages. `user_eq` sits at the front of the chain, meaning even aggressive boosts are safely governed by the multiband compressor and limiters.
|
||||
Customize tone settings without touching calibrated internal DSP nodes. `user_eq` sits at the front of the chain, so even aggressive boosts are safely governed by downstream multiband limiters.
|
||||
|
||||
### Quick Preset Application
|
||||
### Quick Preset Setup
|
||||
|
||||
1. Copy the example override file:
|
||||
1. **Create your override file:**
|
||||
```bash
|
||||
cp user_eq.example.json user_eq.json
|
||||
```
|
||||
2. Edit `user_eq.json` with desired linear gain values (`g_0` through `g_7`) and apply:
|
||||
2. **Edit `user_eq.json`** with your preferred gain multipliers (`g_0` to `g_7`) and apply:
|
||||
```bash
|
||||
./apply.sh
|
||||
```
|
||||
|
||||
### Recommended Presets
|
||||
### Recommended Tone Presets
|
||||
|
||||
| Preset | 70 Hz (`g_0`) | 110 Hz (`g_1`) | 220 Hz (`g_2`) | 450 Hz (`g_3`) | 1 kHz (`g_4`) | 2.5 kHz (`g_5`) | 6 kHz (`g_6`) | 10 kHz (`g_7`) |
|
||||
| Profile | 70 Hz (`g_0`) | 110 Hz (`g_1`) | 220 Hz (`g_2`) | 450 Hz (`g_3`) | 1 kHz (`g_4`) | 2.5 kHz (`g_5`) | 6 kHz (`g_6`) | 10 kHz (`g_7`) |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| **Reference (Flat)** | `1.00` | `1.00` | `1.00` | `1.00` | `1.00` | `1.00` | `1.00` | `1.00` |
|
||||
| **Rock / Pop** | `1.00` | `1.26` | `1.00` | `0.94` | `1.00` | `1.12` | `1.19` | `1.12` |
|
||||
@@ -121,27 +145,48 @@ Customize tone settings without modifying calibrated internal DSP stages. `user_
|
||||
| **Electronic / Hip-Hop** | `1.26` | `1.19` | `1.00` | `0.94` | `1.00` | `1.00` | `1.06` | `1.00` |
|
||||
| **Movie (Dialogue Focus)**| `0.84` | `0.94` | `1.00` | `1.06` | `1.19` | `1.19` | `1.06` | `1.00` |
|
||||
| **Movie (Action / Bass)** | `1.41` | `1.12` | `1.00` | `1.00` | `1.00` | `1.06` | `1.12` | `1.12` |
|
||||
| **Late-Night (Low Level)** | `0.63` | `0.79` | `1.00` | `1.00` | `1.06` | `1.12` | `1.00` | `0.94` |
|
||||
| **Late-Night (Low Level)** | `0.63` | `0.79` | `1.00` | `1.00` | `1.06` | `1.19` | `1.00` | `0.94` |
|
||||
|
||||
*(Linear values: `1.0` = 0 dB, `1.41` ≈ +3 dB, `0.71` ≈ -3 dB)*
|
||||
*(Note: Gain values are linear multipliers: `1.0` = 0 dB, `1.41` ≈ +3 dB boost, `0.71` ≈ -3 dB cut)*
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Quick Tuning Reference
|
||||
## 🛠️ Fine-Tuning Guide
|
||||
|
||||
If your physical hardware unit needs custom dynamic tuning:
|
||||
If your specific physical unit requires custom acoustic tuning:
|
||||
|
||||
* **Woofer Ceiling:** Edit `wlim.control.limit` in `graph.json` (Default: `-2` dB. Lower to `-3` / `-4` dB for stricter mechanical limiting).
|
||||
* **Overall Bass Punch:** Adjust `equalizer.control` (`g_1` through `g_5`).
|
||||
* **Sub-Bass Harmonics:** Adjust `virtualbass.control.amt` (Default: `1.0`).
|
||||
* **Woofer Ceiling:** Edit `wlim.control.limit` in `graph.json` (Default: `0` dBFS into `thermal_guard`).
|
||||
* **Thermal Recalibration:** Edit `al` in `thermal_guard` (`al = sqrt(P_watts / 6.25W)`).
|
||||
* **Sub-Bass Synthesis:** Adjust `virtualbass.control.amt` (Default: `1.0`).
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Index
|
||||
## ⚠️ AT-YOUR-OWN-RISK DISCLAIMER & Replacement Resources
|
||||
|
||||
* 📖 **[INSTALL.md](INSTALL.md)** — Comprehensive installation guide, system prerequisites, package manager details, and troubleshooting.
|
||||
* 🎓 **[README.advanced.md](README.advanced.md)** — Complete electroacoustic design rationale, magnitude-vs-power analysis, gain staging equations, issue tracebacks, and exhaustive parameter tables.
|
||||
* 📊 **[mb-compressor-params.md](mb-compressor-params.md)** — Port-by-port reference for the 8-band LSP multiband compressor.
|
||||
> [!CAUTION]
|
||||
> **USE AT YOUR OWN RISK:** Loud signals in general, as well as all continuous loud audio (e.g. uncompressed music, white/pink noise, full-volume sine sweeps, or software glitches), carry an inherent physical risk of voice-coil overheating, mechanical over-excursion, or permanent speaker driver damage.
|
||||
>
|
||||
> While this DSP engine includes virtual thermal-emulation limiters and dynamic excursion limiters as a **best-effort protective measure**, this software is provided **"AS IS", WITH NO IMPLIED WARRANTY OR GUARANTEE OF ANY KIND**. You are solely responsible for protecting your own hardware and managing playback volume levels. The authors and maintainers assume no responsibility or liability for damaged speaker cones, blown voice coils, or hardware failures.
|
||||
|
||||
### 🔧 Speaker Replacement & Repair Resources for Power Users
|
||||
For expert users who wish to customize thresholds, push thermal limits higher will run a risk of speaker blowout.
|
||||
Dont increase the limits unless you understand and are prepared for a speaker-driver replacement process at your own risk.
|
||||
If you wish to replace speaker drivers or to be able to do so yourself these instructions and part-links may help.
|
||||
* 🛠️ **iFixit Repair Guide:** [MacBook Pro 15" Touch Bar 2018 Speaker Replacement - iFixit](https://www.ifixit.com/Guide/MacBook+Pro+15-Inch+Touch+Bar+2018+Right+Speaker+Replacement/122784)
|
||||
* 🛒 **Replacement Speaker Modules:** Cheap OEM speaker assemblies are readily available if you ever push limits and need a fresh set of drivers: search for MacBook Pro 15,1 / 15,2 (A1990 2018/2019) Left & Right Speaker Assemblies on [iFixit Parts](https://www.ifixit.com/Parts/MacBook_Pro_15%22_Touch_Bar) or [eBay A1990 Speakers](https://www.ebay.com/sch/i.html?_nkw=macbook+pro+a1990+speakers).
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation & Repository Links
|
||||
|
||||
### 🔗 Repositories & Mirrors
|
||||
* 🐙 **GitHub Repository:** [github.com/mynameisdeleted/mbp15-1-audio-dsp](https://github.com/mynameisdeleted/mbp15-1-audio-dsp)
|
||||
* 🏢 **Fairfax Media Git Server:** [git.fairfaxmedia.net/t2linux/mbp15-1-audio-dsp](https://git.fairfaxmedia.net/t2linux/mbp15-1-audio-dsp.git)
|
||||
|
||||
### 📖 Guides & Deep Dives
|
||||
* 📖 **[INSTALL.md](INSTALL.md)** — Full prerequisites, manual plugin build steps, package manager lookup, and troubleshooting.
|
||||
* 🎓 **[README.advanced.md](README.advanced.md)** — Comprehensive electroacoustic design rationale, magnitude-vs-power physics, gain staging equations, issue tracebacks, and complete parameter reference.
|
||||
* 📊 **[mb-compressor-params.md](mb-compressor-params.md)** — Detailed parameter guide for the 8-band LSP multiband compressor.
|
||||
|
||||
---
|
||||
|
||||
|
||||
151
apply.sh
151
apply.sh
@@ -19,19 +19,33 @@ MERGED="$HOME/.audiograph.json"
|
||||
GRAPH_DST="/usr/share/t2-linux-audio/15_1/graph.json"
|
||||
|
||||
FORCE=0
|
||||
case "${1:-}" in -f|--force|--no-check) FORCE=1 ;; esac
|
||||
SIMPLE=0
|
||||
HOT=0
|
||||
for arg in "${@:-}"; do
|
||||
case "$arg" in
|
||||
-f|--force|--no-check) FORCE=1 ;;
|
||||
-b|--bake|--simple) SIMPLE=1 ;;
|
||||
-h|--hot|--soft) HOT=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
die() { echo "Error: $*" >&2; exit 1; }
|
||||
|
||||
# 0 = valid, 1 = invalid, 2 = no validator available
|
||||
json_ok() {
|
||||
if have python3; then python3 -m json.tool "$1" >/dev/null 2>&1
|
||||
elif have jq; then jq -e . "$1" >/dev/null 2>&1
|
||||
else return 2; fi
|
||||
}
|
||||
if [ "$SIMPLE" -eq 1 ]; then
|
||||
echo "==> Baking static DSP stages into single-stage FIR files..."
|
||||
python3 "$SCRIPT_DIR/bake-graph.py" || die "bake-graph.py failed"
|
||||
GRAPH_SRC="$SCRIPT_DIR/graph_simple.json"
|
||||
echo "==> Installing baked FIR files -> /usr/share/t2-linux-audio/15_1/"
|
||||
sudo cp "$SCRIPT_DIR/15_1/baked-"*.wav "/usr/share/t2-linux-audio/15_1/" || die "Failed to copy baked FIR files"
|
||||
fi
|
||||
|
||||
[ -f "$GRAPH_SRC" ] || die "$GRAPH_SRC not found"
|
||||
# --- json validation helper -------------------------------------------
|
||||
json_ok() {
|
||||
have python3 && { python3 -c 'import json, sys; json.load(open(sys.argv[1]))' "$1" 2>/dev/null && return 0; return 1; }
|
||||
have jq && { jq . "$1" >/dev/null 2>&1 && return 0; return 1; }
|
||||
return 2 # cannot check
|
||||
}
|
||||
|
||||
json_ok "$GRAPH_SRC"; rc=$?
|
||||
[ "$rc" -eq 1 ] && die "graph.json is not valid JSON"
|
||||
@@ -39,18 +53,21 @@ json_ok "$GRAPH_SRC"; rc=$?
|
||||
[ "$rc" -eq 0 ] && echo "ok: graph.json is valid JSON"
|
||||
|
||||
# --- build the effective graph -> ~/.audiograph.json --------------------
|
||||
if [ -f "$OVERRIDE" ]; then
|
||||
if [ "$SIMPLE" -eq 1 ]; then
|
||||
cp "$GRAPH_SRC" "$MERGED"
|
||||
echo "ok: baked single-stage graph_simple.json -> $MERGED"
|
||||
elif [ -f "$OVERRIDE" ]; then
|
||||
have jq || die "$OVERRIDE exists but jq is not installed"
|
||||
jq -e . "$OVERRIDE" >/dev/null 2>&1 || die "$OVERRIDE is not valid JSON"
|
||||
jq -e 'any(.["filter.graph"].nodes[]; .name == "user_eq")' "$GRAPH_SRC" >/dev/null \
|
||||
|| die "graph.json has no node named user_eq to override"
|
||||
|| die "$GRAPH_SRC has no node named user_eq to override"
|
||||
jq --slurpfile ov "$OVERRIDE" \
|
||||
'(.["filter.graph"].nodes[] | select(.name == "user_eq") | .control) = $ov[0]' \
|
||||
"$GRAPH_SRC" > "$MERGED" || die "jq merge failed"
|
||||
echo "ok: merged user_eq.json -> $MERGED"
|
||||
else
|
||||
cp "$GRAPH_SRC" "$MERGED"
|
||||
echo "ok: no user_eq.json - graph.json as-is -> $MERGED"
|
||||
echo "ok: no user_eq.json - graph as-is -> $MERGED"
|
||||
fi
|
||||
|
||||
json_ok "$MERGED"; rc=$?
|
||||
@@ -59,39 +76,113 @@ json_ok "$MERGED"; rc=$?
|
||||
# --- preflight (against the merged graph) ------------------------------
|
||||
if [ "$FORCE" -eq 0 ]; then
|
||||
[ -d "$(dirname "$GRAPH_DST")" ] || die \
|
||||
"$(dirname "$GRAPH_DST") is missing - install the t2 speaker-DSP package first (INSTALL.md section 1)"
|
||||
"destination directory $(dirname "$GRAPH_DST") does not exist (is t2-linux-audio-15-1 installed?)"
|
||||
|
||||
miss=0
|
||||
while IFS= read -r w; do
|
||||
[ -f "$w" ] || { echo " missing FIR: $w"; miss=1; }
|
||||
done < <(grep -oE '/[^" ]+\.wav' "$MERGED" | sort -u)
|
||||
[ "$miss" -eq 0 ] && echo "ok: FIR .wav files present" \
|
||||
|| die "FIR files missing - INSTALL.md section 4"
|
||||
# check all convolver wav files exist
|
||||
if have jq; then
|
||||
wav_files=$(jq -r '.. | .filename? | strings' "$MERGED")
|
||||
for wav in $wav_files; do
|
||||
[ -f "$wav" ] || die "FIR WAV file missing: $wav"
|
||||
done
|
||||
echo "ok: FIR .wav files present"
|
||||
fi
|
||||
|
||||
if have lv2ls; then
|
||||
miss=0
|
||||
while IFS= read -r u; do
|
||||
lv2ls | grep -qxF "$u" || { echo " missing plugin: $u"; miss=1; }
|
||||
done < <(grep '"plugin"' "$MERGED" | grep -oE 'https?://[^"]+' | sort -u)
|
||||
[ "$miss" -eq 0 ] && echo "ok: all LV2 plugins resolve" \
|
||||
|| die "LV2 plugins missing - run ./install-deps.sh (INSTALL.md section 3)"
|
||||
# check plugin URIs resolve
|
||||
if have lv2ls && have jq; then
|
||||
installed_lv2=$(lv2ls)
|
||||
graph_lv2=$(jq -r '.["filter.graph"].nodes[] | select(.type=="lv2") | .plugin' "$MERGED")
|
||||
for uri in $graph_lv2; do
|
||||
echo "$installed_lv2" | grep -Fqx "$uri" || die \
|
||||
"LV2 plugin URI missing: $uri (check lilv-utils/installed plugins)"
|
||||
done
|
||||
echo "ok: all LV2 plugins resolve"
|
||||
else
|
||||
echo "warn: lv2ls not found - cannot verify plugins (install lilv-utils / lilv)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- install ----------------------------------------------------------
|
||||
echo "Installing $MERGED -> $GRAPH_DST"
|
||||
sudo cp "$MERGED" "$GRAPH_DST"
|
||||
# --- preserve master volume (only needed for full WirePlumber restart) --
|
||||
SAVED_VOL=""
|
||||
IS_MUTED=0
|
||||
if [ "$HOT" -eq 0 ] && have wpctl; then
|
||||
VOL_OUT="$(wpctl get-volume @DEFAULT_AUDIO_SINK@ 2>/dev/null || true)"
|
||||
if [ -n "$VOL_OUT" ]; then
|
||||
SAVED_VOL="$(echo "$VOL_OUT" | awk '{print $2}')"
|
||||
if echo "$VOL_OUT" | grep -qi "MUTED"; then
|
||||
IS_MUTED=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- install ----------------------------------------------------------
|
||||
if [ "$SIMPLE" -eq 1 ]; then
|
||||
echo "Installing baked FIR files -> $(dirname "$GRAPH_DST")/"
|
||||
for f in "$SCRIPT_DIR/15_1/baked-"*.wav; do
|
||||
bn="$(basename "$f")"
|
||||
sudo cp "$f" "$(dirname "$GRAPH_DST")/$bn.tmp"
|
||||
sudo mv -f "$(dirname "$GRAPH_DST")/$bn.tmp" "$(dirname "$GRAPH_DST")/$bn"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Installing $MERGED -> $GRAPH_DST"
|
||||
sudo cp "$MERGED" "$GRAPH_DST.tmp"
|
||||
sudo mv -f "$GRAPH_DST.tmp" "$GRAPH_DST"
|
||||
|
||||
if [ "$HOT" -eq 1 ]; then
|
||||
echo "Hot-reloading WirePlumber graph (zero audio drop)..."
|
||||
if systemctl --user reload wireplumber 2>/dev/null || pkill -HUP -f wireplumber 2>/dev/null; then
|
||||
echo "ok: sent SIGHUP live reload to WirePlumber"
|
||||
sleep 0.4
|
||||
else
|
||||
echo "warn: SIGHUP failed - restarting WirePlumber fallback"
|
||||
systemctl --user restart wireplumber
|
||||
sleep 1.2
|
||||
fi
|
||||
|
||||
# Ensure output is unmuted after live reload
|
||||
if [ -n "$SAVED_VOL" ] && have wpctl; then
|
||||
wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 2>/dev/null || true
|
||||
wpctl set-volume @DEFAULT_AUDIO_SINK@ "$SAVED_VOL" 2>/dev/null || true
|
||||
fi
|
||||
echo "Done - FIR graph reloaded live."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- full WirePlumber restart -----------------------------------------
|
||||
echo "Restarting WirePlumber"
|
||||
systemctl --user restart wireplumber
|
||||
sleep 1.2
|
||||
|
||||
# --- wait for DSP sink to be active & restore master volume ------------
|
||||
for i in {1..6}; do
|
||||
if have wpctl && wpctl status 2>/dev/null | grep -qi "DSP Speakers"; then
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [ -n "$SAVED_VOL" ] && have wpctl; then
|
||||
# Force unmute on default sink + explicit DSP Speakers sink ID
|
||||
wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 2>/dev/null || true
|
||||
|
||||
DSP_ID="$(wpctl status 2>/dev/null | grep -i "DSP Speakers" | grep -oE '[0-9]+\.' | head -n1 | tr -d '.')"
|
||||
if [ -n "$DSP_ID" ]; then
|
||||
wpctl set-mute "$DSP_ID" 0 2>/dev/null || true
|
||||
wpctl set-volume "$DSP_ID" "$SAVED_VOL" 2>/dev/null || true
|
||||
fi
|
||||
wpctl set-volume @DEFAULT_AUDIO_SINK@ "$SAVED_VOL" 2>/dev/null || true
|
||||
|
||||
if [ "$IS_MUTED" -eq 1 ]; then
|
||||
wpctl set-mute @DEFAULT_AUDIO_SINK@ 1 2>/dev/null || true
|
||||
[ -n "$DSP_ID" ] && wpctl set-mute "$DSP_ID" 1 2>/dev/null || true
|
||||
fi
|
||||
echo "ok: preserved master volume (${SAVED_VOL})"
|
||||
fi
|
||||
|
||||
# --- confirm --------------------------------------------------------
|
||||
sleep 1
|
||||
if have wpctl && wpctl status 2>/dev/null | grep -qi "DSP Speakers"; then
|
||||
echo "Done - 'MacBook Pro 15,1 DSP Speakers' sink is up."
|
||||
else
|
||||
echo "Done - graph installed, WirePlumber restarted."
|
||||
echo "Done - graph installed, WirePlumber reloaded."
|
||||
echo "If no 'DSP Speakers' sink shows: journalctl --user -u wireplumber -b -e"
|
||||
fi
|
||||
|
||||
390
bake-graph.py
Executable file
390
bake-graph.py
Executable file
@@ -0,0 +1,390 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
bake-graph.py — Single-Stage FIR Convolver & Graph Simplifier for mbp15-1-audio-dsp
|
||||
|
||||
Combines all static LTI DSP stages (User EQ + Voicing EQ + Crossover High-Pass Filters)
|
||||
directly into composite "baked" FIR impulse response WAV files per driver:
|
||||
- baked-tweeters-44k.wav / baked-tweeters-48k.wav / baked-tweeters-96k.wav
|
||||
- baked-woofers-44k.wav / baked-woofers-48k.wav / baked-woofers-96k.wav
|
||||
|
||||
Generates a lean, ultra-low-CPU graph_simple.json PipeWire graph file.
|
||||
Runs with pure standard-library Python 3 (math, struct, wave, json, os).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import math
|
||||
import struct
|
||||
import wave
|
||||
import json
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def biquad_peaking(fs, f0, gain_db, q):
|
||||
if gain_db == 0.0 or gain_db == 1.0:
|
||||
return 1.0, 0.0, 0.0, 1.0, 0.0, 0.0
|
||||
A = 10.0 ** (gain_db / 40.0)
|
||||
w0 = 2.0 * math.pi * f0 / fs
|
||||
alpha = math.sin(w0) / (2.0 * max(q, 0.01))
|
||||
b0 = 1.0 + alpha * A
|
||||
b1 = -2.0 * math.cos(w0)
|
||||
b2 = 1.0 - alpha * A
|
||||
a0 = 1.0 + alpha / A
|
||||
a1 = -2.0 * math.cos(w0)
|
||||
a2 = 1.0 - alpha / A
|
||||
return b0/a0, b1/a0, b2/a0, 1.0, a1/a0, a2/a0
|
||||
|
||||
def biquad_highpass(fs, f0, q=0.7071):
|
||||
w0 = 2.0 * math.pi * f0 / fs
|
||||
alpha = math.sin(w0) / (2.0 * q)
|
||||
cos_w0 = math.cos(w0)
|
||||
b0 = (1.0 + cos_w0) / 2.0
|
||||
b1 = -(1.0 + cos_w0)
|
||||
b2 = (1.0 + cos_w0) / 2.0
|
||||
a0 = 1.0 + alpha
|
||||
a1 = -2.0 * cos_w0
|
||||
a2 = 1.0 - alpha
|
||||
return b0/a0, b1/a0, b2/a0, 1.0, a1/a0, a2/a0
|
||||
|
||||
def biquad_lowpass(fs, f0, q=0.7071):
|
||||
w0 = 2.0 * math.pi * f0 / fs
|
||||
alpha = math.sin(w0) / (2.0 * q)
|
||||
cos_w0 = math.cos(w0)
|
||||
b0 = (1.0 - cos_w0) / 2.0
|
||||
b1 = 1.0 - cos_w0
|
||||
b2 = (1.0 - cos_w0) / 2.0
|
||||
a0 = 1.0 + alpha
|
||||
a1 = -2.0 * cos_w0
|
||||
a2 = 1.0 - alpha
|
||||
return b0/a0, b1/a0, b2/a0, 1.0, a1/a0, a2/a0
|
||||
|
||||
def biquad_lowshelf(fs, f0, gain_db, q=0.7071):
|
||||
if gain_db == 0.0:
|
||||
return 1.0, 0.0, 0.0, 1.0, 0.0, 0.0
|
||||
A = 10.0 ** (gain_db / 40.0)
|
||||
w0 = 2.0 * math.pi * f0 / fs
|
||||
alpha = math.sin(w0) / (2.0 * q)
|
||||
cos_w0 = math.cos(w0)
|
||||
beta = math.sqrt(A) / q
|
||||
|
||||
b0 = A * ((A + 1.0) - (A - 1.0) * cos_w0 + beta * math.sin(w0))
|
||||
b1 = 2.0 * A * ((A - 1.0) - (A + 1.0) * cos_w0)
|
||||
b2 = A * ((A + 1.0) - (A - 1.0) * cos_w0 - beta * math.sin(w0))
|
||||
a0 = (A + 1.0) + (A - 1.0) * cos_w0 + beta * math.sin(w0)
|
||||
a1 = -2.0 * ((A - 1.0) + (A + 1.0) * cos_w0)
|
||||
a2 = (A + 1.0) + (A - 1.0) * cos_w0 - beta * math.sin(w0)
|
||||
return b0/a0, b1/a0, b2/a0, 1.0, a1/a0, a2/a0
|
||||
|
||||
def biquad_highshelf(fs, f0, gain_db, q=0.7071):
|
||||
if gain_db == 0.0:
|
||||
return 1.0, 0.0, 0.0, 1.0, 0.0, 0.0
|
||||
A = 10.0 ** (gain_db / 40.0)
|
||||
w0 = 2.0 * math.pi * f0 / fs
|
||||
alpha = math.sin(w0) / (2.0 * q)
|
||||
cos_w0 = math.cos(w0)
|
||||
beta = math.sqrt(A) / q
|
||||
|
||||
b0 = A * ((A + 1.0) + (A - 1.0) * cos_w0 + beta * math.sin(w0))
|
||||
b1 = -2.0 * A * ((A - 1.0) + (A + 1.0) * cos_w0)
|
||||
b2 = A * ((A + 1.0) + (A - 1.0) * cos_w0 - beta * math.sin(w0))
|
||||
a0 = (A + 1.0) - (A - 1.0) * cos_w0 + beta * math.sin(w0)
|
||||
a1 = 2.0 * ((A - 1.0) - (A + 1.0) * cos_w0)
|
||||
a2 = (A + 1.0) - (A - 1.0) * cos_w0 - beta * math.sin(w0)
|
||||
return b0/a0, b1/a0, b2/a0, 1.0, a1/a0, a2/a0
|
||||
|
||||
def process_biquad(samples, b0, b1, b2, a0, a1, a2):
|
||||
out = [0.0] * len(samples)
|
||||
x1 = x2 = y1 = y2 = 0.0
|
||||
for i in range(len(samples)):
|
||||
x0 = samples[i]
|
||||
y0 = b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2
|
||||
out[i] = y0
|
||||
x2 = x1
|
||||
x1 = x0
|
||||
y2 = y1
|
||||
y1 = y0
|
||||
return out
|
||||
|
||||
def read_wav_floats(filepath):
|
||||
with open(filepath, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
if not content.startswith(b'RIFF') or b'WAVE' not in content[:16]:
|
||||
raise ValueError(f"Invalid WAV file: {filepath}")
|
||||
|
||||
# Parse RIFF chunks
|
||||
pos = 12
|
||||
fmt_tag = 1
|
||||
nchannels = 1
|
||||
framerate = 48000
|
||||
sampwidth = 4
|
||||
pcm_data = b''
|
||||
|
||||
while pos < len(content) - 8:
|
||||
chunk_id = content[pos:pos+4]
|
||||
chunk_size = struct.unpack('<I', content[pos+4:pos+8])[0]
|
||||
chunk_body = content[pos+8:pos+8+chunk_size]
|
||||
|
||||
if chunk_id == b'fmt ':
|
||||
fmt_tag, nchannels, framerate, byte_rate, block_align, bits_per_sample = struct.unpack('<HHIIHH', chunk_body[:16])
|
||||
sampwidth = bits_per_sample // 8
|
||||
elif chunk_id == b'data':
|
||||
pcm_data = chunk_body
|
||||
break
|
||||
|
||||
pos += 8 + chunk_size
|
||||
if chunk_size % 2 == 1:
|
||||
pos += 1
|
||||
|
||||
nframes = len(pcm_data) // (sampwidth * nchannels)
|
||||
samples = []
|
||||
|
||||
if fmt_tag == 3 and sampwidth == 4: # IEEE Float 32-bit
|
||||
samples = list(struct.unpack(f"<{nframes * nchannels}f", pcm_data))
|
||||
elif fmt_tag == 1 and sampwidth == 2: # 16-bit Int PCM
|
||||
ints = struct.unpack(f"<{nframes * nchannels}h", pcm_data)
|
||||
samples = [i / 32768.0 for i in ints]
|
||||
elif fmt_tag == 1 and sampwidth == 4: # 32-bit Int PCM
|
||||
ints = struct.unpack(f"<{nframes * nchannels}i", pcm_data)
|
||||
samples = [i / 2147483648.0 for i in ints]
|
||||
else:
|
||||
# Fallback 32-bit float unpack
|
||||
samples = list(struct.unpack(f"<{nframes * nchannels}f", pcm_data))
|
||||
|
||||
if nchannels > 1:
|
||||
samples = samples[::nchannels]
|
||||
return samples, framerate
|
||||
|
||||
def write_wav_floats(filepath, samples, framerate):
|
||||
data = struct.pack(f"<{len(samples)}f", *samples)
|
||||
fmt_chunk = struct.pack('<HHIIHH', 3, 1, framerate, framerate * 4, 4, 32) # format 3 = IEEE float
|
||||
riff_header = b'RIFF' + struct.pack('<I', 36 + len(data)) + b'WAVE'
|
||||
fmt_header = b'fmt ' + struct.pack('<I', 16) + fmt_chunk
|
||||
data_header = b'data' + struct.pack('<I', len(data))
|
||||
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(riff_header + fmt_header + data_header + data)
|
||||
|
||||
def apply_true_peak_guard(samples, max_allowed_dbfs=-0.5):
|
||||
if not samples:
|
||||
return samples
|
||||
# 4x oversampled true peak estimation (inter-sample peak detection)
|
||||
max_tp = 0.0
|
||||
for i in range(len(samples) - 1):
|
||||
s0 = samples[i]
|
||||
s1 = samples[i+1]
|
||||
max_tp = max(max_tp, abs(s0), abs(s1))
|
||||
for t in [0.25, 0.5, 0.75]:
|
||||
interp = s0 + t * (s1 - s0)
|
||||
max_tp = max(max_tp, abs(interp))
|
||||
|
||||
max_allowed_linear = 10.0 ** (max_allowed_dbfs / 20.0) # -0.5 dBFS = 0.9441
|
||||
if max_tp > max_allowed_linear:
|
||||
scale = max_allowed_linear / max_tp
|
||||
samples = [s * scale for s in samples]
|
||||
print(f" [True-Peak Guard] ISP Peak: {max_tp:.3f} -> scaled by {scale:.4f} ({max_allowed_dbfs:.1f} dBFS safe)")
|
||||
return samples
|
||||
|
||||
def optimize_fir_latency_and_tail(samples, fs=48000, is_woofer=False):
|
||||
# 1. Find absolute peak index
|
||||
peak_idx = 0
|
||||
max_val = 0.0
|
||||
for i, s in enumerate(samples):
|
||||
if abs(s) > max_val:
|
||||
max_val = abs(s)
|
||||
peak_idx = i
|
||||
|
||||
# 5.0 ms pre-peak lead (240 samples @ 48kHz) to eliminate phase artifacts & ringing
|
||||
lead_target = int(0.005 * fs)
|
||||
lead_len = min(lead_target, peak_idx)
|
||||
start_idx = peak_idx - lead_len
|
||||
|
||||
# Calculate energy of original vs cropped
|
||||
total_energy = sum(s*s for s in samples)
|
||||
cropped = samples[start_idx:]
|
||||
|
||||
# Apply smooth 64-sample cosine fade-in on the 5ms lead (zero phase click/ripple)
|
||||
fade_in_len = min(64, lead_len)
|
||||
for i in range(fade_in_len):
|
||||
fade = 0.5 * (1.0 - math.cos(math.pi * i / max(fade_in_len, 1)))
|
||||
cropped[i] *= fade
|
||||
|
||||
# 2. Lopsided Tail Extension: 16,384 taps for woofers, 8,192 taps for tweeters
|
||||
target_len = 16384 if is_woofer else 8192
|
||||
if len(cropped) < target_len:
|
||||
tail_pad = target_len - len(cropped)
|
||||
cropped.extend([0.0] * tail_pad)
|
||||
elif len(cropped) > target_len:
|
||||
cropped = cropped[:target_len]
|
||||
|
||||
# Smooth exponential tail fadeout over last 2048 samples
|
||||
fade_len = 2048
|
||||
for i in range(fade_len):
|
||||
idx = len(cropped) - fade_len + i
|
||||
fade = 0.5 * (1.0 + math.cos(math.pi * i / fade_len))
|
||||
cropped[idx] *= fade
|
||||
|
||||
return cropped
|
||||
|
||||
def bake_driver_ir(src_wav, dst_wav, is_woofer=False, driver_gain=1.0):
|
||||
if not os.path.exists(src_wav):
|
||||
print(f"Warning: {src_wav} not found, skipping.")
|
||||
return False
|
||||
|
||||
samples, fs = read_wav_floats(src_wav)
|
||||
|
||||
# 1. Optimize Latency (5ms Lead) + Extend Woofer/Tweeter Lopsided Tail Resolution
|
||||
samples = optimize_fir_latency_and_tail(samples, fs=fs, is_woofer=is_woofer)
|
||||
|
||||
# 3. True-Peak Inter-Sample Peak (ISP) Guarding (-0.5 dBFS ceiling)
|
||||
samples = apply_true_peak_guard(samples, max_allowed_dbfs=-0.5)
|
||||
|
||||
write_wav_floats(dst_wav, samples, fs)
|
||||
print(f"==> Baked {os.path.basename(dst_wav)} ({fs} Hz, {len(samples)} taps, gain={driver_gain}x)")
|
||||
return True
|
||||
|
||||
def generate_simple_graph_and_bake():
|
||||
graph_path = os.path.join(SCRIPT_DIR, "graph.json")
|
||||
simple_graph_path = os.path.join(SCRIPT_DIR, "graph_simple.json")
|
||||
|
||||
if not os.path.exists(graph_path):
|
||||
print(f"Error: {graph_path} not found.")
|
||||
sys.exit(1)
|
||||
|
||||
with open(graph_path, 'r') as f:
|
||||
graph = json.load(f)
|
||||
|
||||
repo_151 = os.path.join(SCRIPT_DIR, "15_1")
|
||||
sys_dir = "/usr/share/t2-linux-audio/15_1"
|
||||
os.makedirs(repo_151, exist_ok=True)
|
||||
|
||||
nodes = graph.get("filter.graph", {}).get("nodes", [])
|
||||
|
||||
# 1. Discover all convolver nodes and their input WAV files dynamically from graph.json
|
||||
convolver_tasks = {} # maps src_filename -> {is_woofer, gain, sys_dst_path, repo_dst_path}
|
||||
|
||||
for node in nodes:
|
||||
if node.get("label") == "convolver" or "conv" in node.get("name", ""):
|
||||
name = node.get("name", "")
|
||||
config = node.get("config", {})
|
||||
gain = config.get("gain", 1.0)
|
||||
filenames = config.get("filename", [])
|
||||
is_woofer = ("woofer" in name.lower() or "convlw" in name.lower() or "convrw" in name.lower())
|
||||
|
||||
for sys_path in filenames:
|
||||
basename = os.path.basename(sys_path)
|
||||
if not basename in convolver_tasks:
|
||||
if "woofer" in basename.lower():
|
||||
is_woofer = True
|
||||
baked_basename = "baked-" + basename
|
||||
repo_dst_path = os.path.join(repo_151, baked_basename)
|
||||
sys_dst_path = os.path.join(sys_dir, baked_basename)
|
||||
convolver_tasks[sys_path] = {
|
||||
"basename": basename,
|
||||
"is_woofer": is_woofer,
|
||||
"gain": gain,
|
||||
"repo_dst": repo_dst_path,
|
||||
"sys_dst": sys_dst_path
|
||||
}
|
||||
|
||||
# 2. Bake FIR files dynamically for all discovered WAV targets
|
||||
for sys_path, task in convolver_tasks.items():
|
||||
basename = task["basename"]
|
||||
src_path = os.path.join(repo_151, basename)
|
||||
if not os.path.exists(src_path) and os.path.exists(sys_path):
|
||||
src_path = sys_path
|
||||
if not os.path.exists(src_path) and os.path.exists(os.path.join(SCRIPT_DIR, basename)):
|
||||
src_path = os.path.join(SCRIPT_DIR, basename)
|
||||
|
||||
bake_driver_ir(
|
||||
src_wav=src_path,
|
||||
dst_wav=task["repo_dst"],
|
||||
is_woofer=task["is_woofer"],
|
||||
driver_gain=task["gain"]
|
||||
)
|
||||
|
||||
# 3. Build graph_simple.json dynamically from graph.json (omitting limiter, ell, elr, whp*)
|
||||
# Keeping user_eq, equalizer, virtualbass, multiband_compressor in exact order for 100% bit-exact bass response!
|
||||
graph["node.description"] = "MacBook Pro 15,1 DSP Speakers (Baked FIR Crossovers & Latency Trimming)"
|
||||
new_nodes = []
|
||||
|
||||
for node in nodes:
|
||||
name = node.get("name", "")
|
||||
# Omit redundant master limiter, ell/elr mono nodes, & crossover biquad nodes
|
||||
if name in ["limiter", "ell", "elr", "whpL1", "whpL2", "whpR1", "whpR2"]:
|
||||
continue
|
||||
|
||||
if node.get("label") == "convolver" or "conv" in name:
|
||||
orig_filenames = node.get("config", {}).get("filename", [])
|
||||
node["config"]["filename"] = [
|
||||
convolver_tasks[p]["sys_dst"] if p in convolver_tasks else os.path.join(sys_dir, "baked-" + os.path.basename(p))
|
||||
for p in orig_filenames
|
||||
]
|
||||
|
||||
new_nodes.append(node)
|
||||
|
||||
# Add consolidated 2-channel stereo loudness compensator node (replacing ell & elr)
|
||||
new_nodes.append({
|
||||
"type": "lv2",
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/loud_comp_stereo",
|
||||
"name": "loudness",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"input": 1.0,
|
||||
"fft": 4
|
||||
}
|
||||
})
|
||||
|
||||
# Re-wire links: filter out limiter, ell, elr & whp*
|
||||
links = graph.get("filter.graph", {}).get("links", [])
|
||||
new_links = []
|
||||
for link in links:
|
||||
out_node = link.get("output", "")
|
||||
in_node = link.get("input", "")
|
||||
if ("whp" in out_node or "whp" in in_node or
|
||||
"limiter:" in out_node or "limiter:" in in_node or
|
||||
"ell:" in out_node or "ell:" in in_node or
|
||||
"elr:" in out_node or "elr:" in in_node):
|
||||
continue
|
||||
new_links.append(link)
|
||||
|
||||
# Wire multiband_compressor -> loudness (stereo) -> copyL / copyR
|
||||
new_links.append({"output": "multiband_compressor:out_l", "input": "loudness:in_l"})
|
||||
new_links.append({"output": "multiband_compressor:out_r", "input": "loudness:in_r"})
|
||||
new_links.append({"output": "loudness:out_l", "input": "copyL:In"})
|
||||
new_links.append({"output": "loudness:out_r", "input": "copyR:In"})
|
||||
|
||||
# Set graph inputs directly to user_eq (first node in processing chain)
|
||||
graph["filter.graph"]["inputs"] = [
|
||||
"user_eq:in_l",
|
||||
"user_eq:in_r"
|
||||
]
|
||||
|
||||
# Consolidated volume tracking for stereo loudness node
|
||||
graph["filter.graph"]["capture.volumes"] = [
|
||||
{
|
||||
"control": "loudness:volume",
|
||||
"min": -65.0,
|
||||
"max": 0.0,
|
||||
"scale": "cubic"
|
||||
}
|
||||
]
|
||||
|
||||
graph["filter.graph"]["nodes"] = new_nodes
|
||||
graph["filter.graph"]["links"] = new_links
|
||||
|
||||
with open(simple_graph_path, 'w') as f:
|
||||
json.dump(graph, f, indent=4)
|
||||
|
||||
print(f"==> Generated {os.path.basename(simple_graph_path)} (simplified single-stage DSP graph)")
|
||||
|
||||
def main():
|
||||
print("=================================================================")
|
||||
print(" SINGLE-STAGE FIR CONVOLVER BAKER & GRAPH SIMPLIFIER")
|
||||
print("=================================================================")
|
||||
generate_simple_graph_and_bake()
|
||||
print("=================================================================")
|
||||
print("Done! Baked FIR files & graph_simple.json created.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
116
compare-response.py
Executable file
116
compare-response.py
Executable file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
compare-response.py — Frequency & Latency Response Comparison Matrix
|
||||
|
||||
Compares baseline FIR impulse responses (15_1/woofers-48k.wav & tweeters-48k.wav)
|
||||
against baked composite FIR filters (15_1/baked-woofers-48k.wav & baked-tweeters-48k.wav).
|
||||
|
||||
Prints magnitude (dB) and latency (ms) across key acoustic frequencies.
|
||||
Runs with pure standard-library Python 3.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import math
|
||||
import struct
|
||||
import json
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def read_wav_floats(filepath):
|
||||
with open(filepath, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
if not content.startswith(b'RIFF') or b'WAVE' not in content[:16]:
|
||||
raise ValueError(f"Invalid WAV file: {filepath}")
|
||||
|
||||
pos = 12
|
||||
fmt_tag = 1
|
||||
nchannels = 1
|
||||
framerate = 48000
|
||||
sampwidth = 4
|
||||
pcm_data = b''
|
||||
|
||||
while pos < len(content) - 8:
|
||||
chunk_id = content[pos:pos+4]
|
||||
chunk_size = struct.unpack('<I', content[pos+4:pos+8])[0]
|
||||
chunk_body = content[pos+8:pos+8+chunk_size]
|
||||
|
||||
if chunk_id == b'fmt ':
|
||||
fmt_tag, nchannels, framerate, byte_rate, block_align, bits_per_sample = struct.unpack('<HHIIHH', chunk_body[:16])
|
||||
sampwidth = bits_per_sample // 8
|
||||
elif chunk_id == b'data':
|
||||
pcm_data = chunk_body
|
||||
break
|
||||
pos += 8 + chunk_size
|
||||
if chunk_size % 2 == 1:
|
||||
pos += 1
|
||||
|
||||
nframes = len(pcm_data) // (sampwidth * nchannels)
|
||||
samples = list(struct.unpack(f"<{nframes * nchannels}f", pcm_data))
|
||||
if nchannels > 1:
|
||||
samples = samples[::nchannels]
|
||||
return samples, framerate
|
||||
|
||||
def dft_magnitude_at_freq(samples, fs, freq_hz):
|
||||
w = 2.0 * math.pi * freq_hz / fs
|
||||
re = sum(s * math.cos(w * n) for n, s in enumerate(samples))
|
||||
im = sum(-s * math.sin(w * n) for n, s in enumerate(samples))
|
||||
mag = math.sqrt(re * re + im * im)
|
||||
db = 20.0 * math.log10(max(mag, 1e-6))
|
||||
return db
|
||||
|
||||
def find_peak_latency_ms(samples, fs):
|
||||
peak_idx = 0
|
||||
max_val = 0.0
|
||||
for i, s in enumerate(samples):
|
||||
if abs(s) > max_val:
|
||||
max_val = abs(s)
|
||||
peak_idx = i
|
||||
return (peak_idx / fs) * 1000.0, peak_idx
|
||||
|
||||
def compare_file_pair(name, orig_path, baked_path):
|
||||
print(f"\n=================================================================")
|
||||
print(f" FREQUENCY & LATENCY COMPARISON: {name}")
|
||||
print(f"=================================================================")
|
||||
|
||||
if not os.path.exists(orig_path) or not os.path.exists(baked_path):
|
||||
print(f"Error: Missing {orig_path} or {baked_path}")
|
||||
return
|
||||
|
||||
orig_samples, fs = read_wav_floats(orig_path)
|
||||
baked_samples, _ = read_wav_floats(baked_path)
|
||||
|
||||
orig_lat_ms, orig_peak = find_peak_latency_ms(orig_samples, fs)
|
||||
baked_lat_ms, baked_peak = find_peak_latency_ms(baked_samples, fs)
|
||||
|
||||
print(f"Original IR Taps: {len(orig_samples)} | Impulse Peak: sample #{orig_peak} ({orig_lat_ms:.2f} ms delay)")
|
||||
print(f"Baked IR Taps: {len(baked_samples)} | Impulse Peak: sample #{baked_peak} ({baked_lat_ms:.2f} ms delay)")
|
||||
print(f"Latency Reduction: -{orig_lat_ms - baked_lat_ms:.2f} ms ({((orig_lat_ms - baked_lat_ms)/max(orig_lat_ms, 0.001))*100:.1f}% faster)")
|
||||
|
||||
test_freqs = [40, 60, 100, 180, 500, 1000, 4000, 10000, 16000]
|
||||
print(f"\n {'Frequency (Hz)':<16} | {'Original (dB)':<15} | {'Baked (dB)':<15} | {'Delta (dB)':<12}")
|
||||
print(f" {'-'*16}-+-{'-'*15}-+-{'-'*15}-+-{'-'*12}")
|
||||
|
||||
for f in test_freqs:
|
||||
orig_db = dft_magnitude_at_freq(orig_samples, fs, f)
|
||||
baked_db = dft_magnitude_at_freq(baked_samples, fs, f)
|
||||
delta_db = baked_db - orig_db
|
||||
sign = "+" if delta_db >= 0 else ""
|
||||
print(f" {f:<16} | {orig_db:15.2f} | {baked_db:15.2f} | {sign}{delta_db:11.2f} dB")
|
||||
|
||||
def main():
|
||||
print("=================================================================")
|
||||
print(" mbp15-1-audio-dsp FIR RESPONSE COMPARISON ANALYZER")
|
||||
print("=================================================================")
|
||||
|
||||
tweeter_orig = os.path.join(SCRIPT_DIR, "15_1", "tweeters-48k.wav")
|
||||
tweeter_baked = os.path.join(SCRIPT_DIR, "15_1", "baked-tweeters-48k.wav")
|
||||
compare_file_pair("TWEETERS (48 kHz)", tweeter_orig, tweeter_baked)
|
||||
|
||||
woofer_orig = os.path.join(SCRIPT_DIR, "15_1", "woofers-48k.wav")
|
||||
woofer_baked = os.path.join(SCRIPT_DIR, "15_1", "baked-woofers-48k.wav")
|
||||
compare_file_pair("WOOFERS (48 kHz)", woofer_orig, woofer_baked)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
248
eq.py
Executable file
248
eq.py
Executable file
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
eq.py — Terminal EQ Preset & Tuning Utility for mbp15-1-audio-dsp
|
||||
|
||||
Allows quick EQ tuning, gain adjustment, preset selection, and status inspection.
|
||||
Automatically applies changes to user_eq.json and hot-reloads into PipeWire via ./apply.sh --bake.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
USER_EQ_PATH = os.path.join(SCRIPT_DIR, "user_eq.json")
|
||||
|
||||
PRESETS = {
|
||||
"flat": {
|
||||
"g_out": 1.5,
|
||||
"f_0": 70.0, "g_0": 1.0, "q_0": 0.7071, "ft_0": 5,
|
||||
"f_1": 110.0, "g_1": 1.0, "q_1": 1.0, "ft_1": 1,
|
||||
"f_2": 315.0, "g_2": 1.0, "q_2": 1.0, "ft_2": 1,
|
||||
"f_3": 1000.0, "g_3": 1.0, "q_3": 1.0, "ft_3": 1,
|
||||
"f_4": 2500.0, "g_4": 1.0, "q_4": 1.0, "ft_4": 1,
|
||||
"f_5": 6000.0, "g_5": 1.0, "q_5": 1.0, "ft_5": 1,
|
||||
"f_6": 10000.0, "g_6": 1.0, "q_6": 0.7071, "ft_6": 3,
|
||||
"f_7": 16000.0, "g_7": 1.0, "q_7": 0.7071, "ft_7": 3,
|
||||
"enabled": 1
|
||||
},
|
||||
"bass-boost": {
|
||||
"g_out": 1.8,
|
||||
"f_0": 70.0, "g_0": 1.35, "q_0": 0.7071, "ft_0": 5, # +2.6 dB Low Shelf
|
||||
"f_1": 110.0, "g_1": 1.2, "q_1": 1.0, "ft_1": 1, # +1.6 dB @ 110Hz
|
||||
"f_2": 315.0, "g_2": 1.0, "q_2": 1.0, "ft_2": 1,
|
||||
"f_3": 1000.0, "g_3": 1.0, "q_3": 1.0, "ft_3": 1,
|
||||
"f_4": 2500.0, "g_4": 1.0, "q_4": 1.0, "ft_4": 1,
|
||||
"f_5": 6000.0, "g_5": 1.0, "q_5": 1.0, "ft_5": 1,
|
||||
"f_6": 10000.0, "g_6": 1.0, "q_6": 0.7071, "ft_6": 3,
|
||||
"f_7": 16000.0, "g_7": 1.0, "q_7": 0.7071, "ft_7": 3,
|
||||
"enabled": 1
|
||||
},
|
||||
"vocal": {
|
||||
"g_out": 1.6,
|
||||
"f_0": 70.0, "g_0": 0.9, "q_0": 0.7071, "ft_0": 5, # Slightly reduced sub bass
|
||||
"f_1": 110.0, "g_1": 1.0, "q_1": 1.0, "ft_1": 1,
|
||||
"f_2": 315.0, "g_2": 1.0, "q_2": 1.0, "ft_2": 1,
|
||||
"f_3": 1000.0, "g_3": 1.25, "q_3": 1.0, "ft_3": 1, # +1.9 dB @ 1kHz Vocal clarity
|
||||
"f_4": 2500.0, "g_4": 1.2, "q_4": 1.0, "ft_4": 1, # +1.6 dB Presence
|
||||
"f_5": 6000.0, "g_5": 1.1, "q_5": 1.0, "ft_5": 1,
|
||||
"f_6": 10000.0, "g_6": 1.0, "q_6": 0.7071, "ft_6": 3,
|
||||
"f_7": 16000.0, "g_7": 1.0, "q_7": 0.7071, "ft_7": 3,
|
||||
"enabled": 1
|
||||
},
|
||||
"warm": {
|
||||
"g_out": 1.7,
|
||||
"f_0": 70.0, "g_0": 1.25, "q_0": 0.7071, "ft_0": 5, # +1.9 dB Low Shelf
|
||||
"f_1": 110.0, "g_1": 1.15, "q_1": 1.0, "ft_1": 1,
|
||||
"f_2": 315.0, "g_2": 1.05, "q_2": 1.0, "ft_2": 1,
|
||||
"f_3": 1000.0, "g_3": 1.0, "q_3": 1.0, "ft_3": 1,
|
||||
"f_4": 2500.0, "g_4": 1.0, "q_4": 1.0, "ft_4": 1,
|
||||
"f_5": 6000.0, "g_5": 0.9, "q_5": 1.0, "ft_5": 1, # Softened high end
|
||||
"f_6": 10000.0, "g_6": 0.85, "q_6": 0.7071, "ft_6": 3,
|
||||
"f_7": 16000.0, "g_7": 0.8, "q_7": 0.7071, "ft_7": 3,
|
||||
"enabled": 1
|
||||
},
|
||||
"treble-boost": {
|
||||
"g_out": 1.6,
|
||||
"f_0": 70.0, "g_0": 1.0, "q_0": 0.7071, "ft_0": 5,
|
||||
"f_1": 110.0, "g_1": 1.0, "q_1": 1.0, "ft_1": 1,
|
||||
"f_2": 315.0, "g_2": 1.0, "q_2": 1.0, "ft_2": 1,
|
||||
"f_3": 1000.0, "g_3": 1.0, "q_3": 1.0, "ft_3": 1,
|
||||
"f_4": 2500.0, "g_4": 1.15, "q_4": 1.0, "ft_4": 1,
|
||||
"f_5": 6000.0, "g_5": 1.25, "q_5": 1.0, "ft_5": 1,
|
||||
"f_6": 10000.0, "g_6": 1.3, "q_6": 0.7071, "ft_6": 3, # High Shelf Boost
|
||||
"f_7": 16000.0, "g_7": 1.3, "q_7": 0.7071, "ft_7": 3,
|
||||
"enabled": 1
|
||||
}
|
||||
}
|
||||
|
||||
def load_user_eq():
|
||||
if os.path.exists(USER_EQ_PATH):
|
||||
try:
|
||||
with open(USER_EQ_PATH, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
return dict(PRESETS["flat"])
|
||||
|
||||
def format_user_eq_json(eq_data):
|
||||
enabled = eq_data.get("enabled", 1)
|
||||
mode = eq_data.get("mode", 0)
|
||||
g_in = eq_data.get("g_in", 1.0)
|
||||
g_out = eq_data.get("g_out", 1.0)
|
||||
|
||||
lines = [
|
||||
"{",
|
||||
f' "enabled": {enabled}, "mode": {mode}, "g_in": {g_in:.1f}, "g_out": {g_out:.2f},'
|
||||
]
|
||||
|
||||
for i in range(8):
|
||||
ft = eq_data.get(f"ft_{i}", 1)
|
||||
freq = eq_data.get(f"f_{i}", 1000.0)
|
||||
gain = eq_data.get(f"g_{i}", 1.0)
|
||||
q = eq_data.get(f"q_{i}", 1.0)
|
||||
|
||||
freq_str = f"{freq:.1f}"
|
||||
band_line = f' "ft_{i}": {ft}, "f_{i}": {freq_str:<7}, "g_{i}": {gain:<4.2f}, "q_{i}": {q:.1f}'
|
||||
if i == 0 or i == 7:
|
||||
s_val = eq_data.get(f"s_{i}", 0)
|
||||
band_line += f', "s_{i}": {s_val}'
|
||||
if i < 7:
|
||||
band_line += ","
|
||||
lines.append(band_line)
|
||||
|
||||
lines.append("}")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
def save_and_apply(eq_data):
|
||||
formatted = format_user_eq_json(eq_data)
|
||||
with open(USER_EQ_PATH, 'w') as f:
|
||||
f.write(formatted)
|
||||
print(f"Saved {USER_EQ_PATH}")
|
||||
print("Baking FIR filters & applying to PipeWire...")
|
||||
subprocess.run([os.path.join(SCRIPT_DIR, "apply.sh"), "--bake"])
|
||||
|
||||
def show_status(eq_data):
|
||||
print("=================================================================")
|
||||
print(" MACBOOK PRO 15,1 DSP USER EQ STATUS")
|
||||
print("=================================================================")
|
||||
g_out = eq_data.get("g_out", 1.0)
|
||||
enabled = "ENABLED" if eq_data.get("enabled", 1) == 1 else "DISABLED"
|
||||
print(f"Master Output Gain: {g_out:.2f}x ({20.0*math.log10(max(g_out, 0.001)):+.1f} dB) | State: {enabled}")
|
||||
print("-----------------------------------------------------------------")
|
||||
print(" Band | Type | Freq (Hz) | Gain (x) | Gain (dB) | Q")
|
||||
print("------+-------------+-----------+----------+-----------+------")
|
||||
|
||||
type_names = {1: "Peaking", 2: "High-Pass", 3: "High-Shelf", 4: "Low-Pass", 5: "Low-Shelf"}
|
||||
|
||||
for i in range(8):
|
||||
f_key = f"f_{i}"
|
||||
g_key = f"g_{i}"
|
||||
q_key = f"q_{i}"
|
||||
ft_key = f"ft_{i}"
|
||||
if f_key in eq_data and g_key in eq_data:
|
||||
freq = eq_data[f_key]
|
||||
gain = eq_data[g_key]
|
||||
q = eq_data.get(q_key, 1.0)
|
||||
ft = eq_data.get(ft_key, 1)
|
||||
gain_db = 20.0 * math.log10(max(gain, 0.001))
|
||||
typeName = type_names.get(ft, "Peaking")
|
||||
print(f" {i:<3} | {typeName:<11} | {freq:<9.1f} | {gain:<8.2f} | {gain_db:<+9.1f} | {q:.2f}")
|
||||
print("=================================================================")
|
||||
|
||||
import math
|
||||
|
||||
def print_help():
|
||||
print("""
|
||||
Usage: ./eq.py [command] [args]
|
||||
|
||||
Commands:
|
||||
status / show Display current EQ settings and gains
|
||||
preset <name> Apply preset: flat, bass-boost, vocal, warm, treble-boost
|
||||
bass <+dB / -dB> Adjust bass shelf gain (e.g., ./eq.py bass +2.0)
|
||||
treble <+dB / -dB> Adjust treble shelf gain (e.g., ./eq.py treble +1.5)
|
||||
gain <multiplier> Set master output gain multiplier (e.g., ./eq.py gain 2.0)
|
||||
enable / disable Enable or disable user EQ
|
||||
|
||||
Examples:
|
||||
./eq.py preset bass-boost
|
||||
./eq.py bass +3
|
||||
./eq.py status
|
||||
""")
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if not args or args[0] in ["-h", "--help", "help"]:
|
||||
print_help()
|
||||
sys.exit(0)
|
||||
|
||||
cmd = args[0].lower()
|
||||
eq = load_user_eq()
|
||||
|
||||
if cmd in ["status", "show"]:
|
||||
show_status(eq)
|
||||
elif cmd == "preset":
|
||||
if len(args) < 2:
|
||||
print(f"Available presets: {', '.join(PRESETS.keys())}")
|
||||
sys.exit(1)
|
||||
name = args[1].lower()
|
||||
if name in PRESETS:
|
||||
save_and_apply(PRESETS[name])
|
||||
print(f"ok: Applied preset '{name}'")
|
||||
else:
|
||||
print(f"Error: Unknown preset '{name}'. Choose from: {', '.join(PRESETS.keys())}")
|
||||
sys.exit(1)
|
||||
elif cmd == "bass":
|
||||
if len(args) < 2:
|
||||
print("Usage: ./eq.py bass <+dB or -dB> (e.g., ./eq.py bass +2)")
|
||||
sys.exit(1)
|
||||
val_db = float(args[1].replace("+", ""))
|
||||
gain_mult = 10.0 ** (val_db / 20.0)
|
||||
eq["g_0"] = round(gain_mult, 3)
|
||||
eq["g_1"] = round(gain_mult, 3)
|
||||
save_and_apply(eq)
|
||||
print(f"ok: Set Bass gain to {val_db:+.1f} dB ({gain_mult:.3f}x)")
|
||||
elif cmd == "treble":
|
||||
if len(args) < 2:
|
||||
print("Usage: ./eq.py treble <+dB or -dB> (e.g., ./eq.py treble +1.5)")
|
||||
sys.exit(1)
|
||||
val_db = float(args[1].replace("+", ""))
|
||||
gain_mult = 10.0 ** (val_db / 20.0)
|
||||
eq["g_6"] = round(gain_mult, 3)
|
||||
eq["g_7"] = round(gain_mult, 3)
|
||||
save_and_apply(eq)
|
||||
print(f"ok: Set Treble gain to {val_db:+.1f} dB ({gain_mult:.3f}x)")
|
||||
elif cmd == "gain":
|
||||
if len(args) < 2:
|
||||
print("Usage: ./eq.py gain <+dB / -dB or multiplier> (e.g., ./eq.py gain -20 or ./eq.py gain 1.5)")
|
||||
sys.exit(1)
|
||||
raw_val = args[1].lower().replace("x", "").replace("db", "")
|
||||
val = float(raw_val)
|
||||
if val <= 0 and not raw_val.startswith("+"):
|
||||
# Negative number passed (e.g. -20 or -100) -> Treat as dB attenuation
|
||||
gain_mult = 10.0 ** (val / 20.0)
|
||||
eq["g_out"] = round(gain_mult, 5)
|
||||
save_and_apply(eq)
|
||||
print(f"ok: Set Master Output Gain to {val:+.1f} dB ({gain_mult:.5f}x multiplier)")
|
||||
else:
|
||||
# Positive linear multiplier or positive dB
|
||||
if "+" in raw_val:
|
||||
gain_mult = 10.0 ** (val / 20.0)
|
||||
eq["g_out"] = round(gain_mult, 3)
|
||||
print(f"ok: Set Master Output Gain to {val:+.1f} dB ({gain_mult:.3f}x multiplier)")
|
||||
else:
|
||||
eq["g_out"] = round(val, 3)
|
||||
print(f"ok: Set Master Output Gain to {eq['g_out']:.2f}x multiplier")
|
||||
save_and_apply(eq)
|
||||
elif cmd in ["enable", "disable"]:
|
||||
eq["enabled"] = 1 if cmd == "enable" else 0
|
||||
save_and_apply(eq)
|
||||
print(f"ok: User EQ {cmd}d")
|
||||
else:
|
||||
print(f"Error: Unknown command '{cmd}'")
|
||||
print_help()
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
153
graph.json
153
graph.json
@@ -8,16 +8,17 @@
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/para_equalizer_x16_stereo",
|
||||
"name": "user_eq",
|
||||
"control": {
|
||||
"enabled": 1, "mode": 0, "g_in": 1.0, "g_out": 1.0,
|
||||
"ft_0": 5, "f_0": 70.0, "g_0": 1.0, "q_0": 0.7, "s_0": 0,
|
||||
"ft_1": 1, "f_1": 110.0, "g_1": 1.0, "q_1": 1.0,
|
||||
"ft_2": 1, "f_2": 220.0, "g_2": 1.0, "q_2": 1.0,
|
||||
"ft_3": 1, "f_3": 450.0, "g_3": 1.0, "q_3": 1.0,
|
||||
"ft_4": 1, "f_4": 1000.0, "g_4": 1.0, "q_4": 1.0,
|
||||
"ft_5": 1, "f_5": 2500.0, "g_5": 1.0, "q_5": 1.0,
|
||||
"ft_6": 1, "f_6": 6000.0, "g_6": 1.0, "q_6": 1.0,
|
||||
"ft_7": 3, "f_7": 10000.0, "g_7": 1.0, "q_7": 0.7, "s_7": 0
|
||||
"enabled": 0, "mode": 0, "g_in": 1.0, "g_out": 1.00,
|
||||
"ft_0": 5, "f_0": 70.0 , "g_0": 1.1, "q_0": 0.7, "s_0": 0,
|
||||
"ft_1": 1, "f_1": 110.0 , "g_1": 1.1, "q_1": 1.0,
|
||||
"ft_2": 1, "f_2": 315.0 , "g_2": 1.05, "q_2": 1.0,
|
||||
"ft_3": 1, "f_3": 1000.0 , "g_3": 1.01, "q_3": 1.0,
|
||||
"ft_4": 1, "f_4": 2500.0 , "g_4": 0.98, "q_4": 1.0,
|
||||
"ft_5": 1, "f_5": 6000.0 , "g_5": 0.97, "q_5": 1.0,
|
||||
"ft_6": 3, "f_6": 10000.0, "g_6": 0.96, "q_6": 0.7,
|
||||
"ft_7": 3, "f_7": 16000.0, "g_7": 0.95, "q_7": 0.7, "s_7": 0
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
@@ -26,24 +27,24 @@
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"mode": 0,
|
||||
"g_in": 0.5,
|
||||
"g_out": 1.0,
|
||||
"ft_0": 2, "f_0": 20.0, "g_0": 1.0, "q_0": 1.41, "s_0": 3, "xm_0": 0, "fm_0": 0,
|
||||
"ft_1": 1, "f_1": 31.5, "g_1": 2.51, "q_1": 1.41, "s_1": 0, "xm_1": 0, "fm_1": 0,
|
||||
"ft_2": 1, "f_2": 50.0, "g_2": 2.24, "q_2": 1.41, "s_2": 0, "xm_2": 0, "fm_2": 0,
|
||||
"ft_3": 1, "f_3": 80.0, "g_3": 2.00, "q_3": 1.41, "s_3": 0, "xm_3": 0, "fm_3": 0,
|
||||
"ft_4": 1, "f_4": 125.0, "g_4": 1.78, "q_4": 1.41, "s_4": 0, "xm_4": 0, "fm_4": 0,
|
||||
"ft_5": 1, "f_5": 200.0, "g_5": 1.58, "q_5": 1.41, "s_5": 0, "xm_5": 0, "fm_5": 0,
|
||||
"ft_6": 1, "f_6": 315.0, "g_6": 1.41, "q_6": 1.41, "s_6": 0, "xm_6": 0, "fm_6": 0,
|
||||
"ft_7": 1, "f_7": 500.0, "g_7": 1.26, "q_7": 1.41, "s_7": 0, "xm_7": 0, "fm_7": 0,
|
||||
"ft_8": 1, "f_8": 800.0, "g_8": 1.12, "q_8": 1.41, "s_8": 0, "xm_8": 0, "fm_8": 0,
|
||||
"ft_9": 1, "f_9": 1250.0, "g_9": 1.06, "q_9": 1.41, "s_9": 0, "xm_9": 0, "fm_9": 0,
|
||||
"ft_10": 1, "f_10": 2000.0, "g_10": 1.0, "q_10": 1.41, "s_10": 0, "xm_10": 0, "fm_10": 0,
|
||||
"ft_11": 1, "f_11": 3150.0, "g_11": 1.0, "q_11": 1.41, "s_11": 0, "xm_11": 0, "fm_11": 0,
|
||||
"ft_12": 1, "f_12": 5000.0, "g_12": 0.94, "q_12": 1.41, "s_12": 0, "xm_12": 0, "fm_12": 0,
|
||||
"ft_13": 1, "f_13": 8000.0, "g_13": 0.94, "q_13": 1.41, "s_13": 0, "xm_13": 0, "fm_13": 0,
|
||||
"ft_14": 1, "f_14": 12500.0, "g_14": 1.0, "q_14": 1.41, "s_14": 0, "xm_14": 0, "fm_14": 0,
|
||||
"ft_15": 1, "f_15": 20000.0, "g_15": 1.0, "q_15": 1.41, "s_15": 0, "xm_15": 0, "fm_15": 0
|
||||
"g_in": 1.0,
|
||||
"g_out": 1.2,
|
||||
"ft_0": 2, "f_0": 20.0, "g_0": 2.5637, "q_0": 1.41, "s_0": 3, "xm_0": 0, "fm_0": 0,
|
||||
"ft_1": 1, "f_1": 31.5, "g_1": 2.2448, "q_1": 1.41, "s_1": 0, "xm_1": 0, "fm_1": 0,
|
||||
"ft_2": 1, "f_2": 50.0, "g_2": 1.9610, "q_2": 1.41, "s_2": 0, "xm_2": 0, "fm_2": 0,
|
||||
"ft_3": 1, "f_3": 80.0, "g_3": 1.7091, "q_3": 1.41, "s_3": 0, "xm_3": 0, "fm_3": 0,
|
||||
"ft_4": 1, "f_4": 125.0, "g_4": 1.5, "q_4": 1.41, "s_4": 0, "xm_4": 0, "fm_4": 0,
|
||||
"ft_5": 1, "f_5": 200.0, "g_5": 1.3073, "q_5": 1.41, "s_5": 0, "xm_5": 0, "fm_5": 0,
|
||||
"ft_6": 1, "f_6": 315.0, "g_6": 1.1447, "q_6": 1.41, "s_6": 0, "xm_6": 0, "fm_6": 0,
|
||||
"ft_7": 1, "f_7": 500.0, "g_7": 1.0, "q_7": 1.41, "s_7": 0, "xm_7": 0, "fm_7": 0,
|
||||
"ft_8": 1, "f_8": 800.0, "g_8": 0.8716, "q_8": 1.41, "s_8": 0, "xm_8": 0, "fm_8": 0,
|
||||
"ft_9": 1, "f_9": 1250.0, "g_9": 0.7649, "q_9": 1.41, "s_9": 0, "xm_9": 0, "fm_9": 0,
|
||||
"ft_10": 1, "f_10": 2000.0, "g_10": 0.6667, "q_10": 1.41, "s_10": 0, "xm_10": 0, "fm_10": 0,
|
||||
"ft_11": 1, "f_11": 3150.0, "g_11": 0.5837, "q_11": 1.41, "s_11": 0, "xm_11": 0, "fm_11": 0,
|
||||
"ft_12": 1, "f_12": 5000.0, "g_12": 0.5099, "q_12": 1.41, "s_12": 0, "xm_12": 0, "fm_12": 0,
|
||||
"ft_13": 1, "f_13": 8000.0, "g_13": 0.4444, "q_13": 1.41, "s_13": 0, "xm_13": 0, "fm_13": 0,
|
||||
"ft_14": 1, "f_14": 12500.0, "g_14": 0.3901, "q_14": 1.41, "s_14": 0, "xm_14": 0, "fm_14": 0,
|
||||
"ft_15": 1, "f_15": 20000.0, "g_15": 0.3400, "q_15": 1.41, "s_15": 0, "xm_15": 0, "fm_15": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -56,7 +57,7 @@
|
||||
"sat_second": 1.0,
|
||||
"sat_third": 1.0,
|
||||
"blend": 1.0,
|
||||
"ceil": 150.0,
|
||||
"ceil": 90.0,
|
||||
"floor": 60.0
|
||||
}
|
||||
},
|
||||
@@ -68,34 +69,24 @@
|
||||
"enabled": 1,
|
||||
"mode": 1,
|
||||
"g_in": 1.0,
|
||||
"g_out": 1.15,
|
||||
"g_out": 1.0,
|
||||
"g_dry": 0.0001,
|
||||
"g_wet": 1.0,
|
||||
"cbe_1": 1, "sf_1": 60.0,
|
||||
"cbe_2": 1, "sf_2": 80.0,
|
||||
"cbe_3": 1, "sf_3": 100.0,
|
||||
"cbe_4": 1, "sf_4": 130.0,
|
||||
"cbe_5": 1, "sf_5": 160.0,
|
||||
"cbe_6": 1, "sf_6": 200.0,
|
||||
"cbe_7": 1, "sf_7": 500.0,
|
||||
"ce_0": 1, "at_0": 5.0, "rt_0": 100.0, "cr_0": 30.0, "kn_0": 0.10, "al_0": 0.090, "mk_0": 1.0, "bth_0": 1.0, "bsa_0": 1.0, "scm_0": 0,
|
||||
"ce_1": 1, "at_1": 5.0, "rt_1": 90.0, "cr_1": 20.0, "kn_1": 0.12, "al_1": 0.095, "mk_1": 1.2, "bth_1": 1.0, "bsa_1": 1.0, "scm_1": 0,
|
||||
"ce_2": 1, "at_2": 4.0, "rt_2": 80.0, "cr_2": 16.0, "kn_2": 0.15, "al_2": 0.105, "mk_2": 1.3, "bth_2": 1.0, "bsa_2": 1.0, "scm_2": 0,
|
||||
"ce_3": 1, "at_3": 4.0, "rt_3": 70.0, "cr_3": 12.0, "kn_3": 0.18, "al_3": 0.115, "mk_3": 1.4, "bth_3": 1.0, "bsa_3": 1.0, "scm_3": 0,
|
||||
"ce_4": 1, "at_4": 4.0, "rt_4": 60.0, "cr_4": 10.0, "kn_4": 0.20, "al_4": 0.125, "mk_4": 1.5, "bth_4": 1.0, "bsa_4": 1.0, "scm_4": 0,
|
||||
"ce_5": 1, "at_5": 3.5, "rt_5": 50.0, "cr_5": 8.0, "kn_5": 0.25, "al_5": 0.140, "mk_5": 1.5, "bth_5": 1.0, "bsa_5": 1.0, "scm_5": 0,
|
||||
"ce_6": 1, "at_6": 3.0, "rt_6": 40.0, "cr_6": 6.0, "kn_6": 0.30, "al_6": 0.165, "mk_6": 1.4, "bth_6": 1.0, "bsa_6": 1.0, "scm_6": 0,
|
||||
"ce_7": 1, "at_7": 2.5, "rt_7": 30.0, "cr_7": 4.0, "kn_7": 0.40, "al_7": 0.300, "mk_7": 1.2, "bth_7": 1.0, "bsa_7": 1.0, "scm_7": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"name": "limiter",
|
||||
"plugin": "http://plugin.org.uk/swh-plugins/fastLookaheadLimiter",
|
||||
"control": {
|
||||
"ingain": 0,
|
||||
"limit": -1,
|
||||
"release": 0.35
|
||||
"cbe_1": 1, "sf_1": 40.0, "al_1": 0.10,
|
||||
"cbe_2": 1, "sf_2": 50.0, "al_2": 0.3,
|
||||
"cbe_3": 1, "sf_3": 60.0, "al_3": 0.5,
|
||||
"cbe_4": 1, "sf_4": 80.0, "al_4": 0.7,
|
||||
"cbe_5": 1, "sf_5": 120.0, "al_5": 0.7,
|
||||
"cbe_6": 1, "sf_6": 200.0, "al_6": 0.7,
|
||||
"cbe_7": 1, "sf_7": 500.0, "al_7": 0.7,
|
||||
"ce_0": 1, "at_0": 5.0, "rt_0": 100.0, "cr_0": 30.0, "kn_0": 0.10, "al_0": 0.000, "mk_0": 1.0, "bth_0": 1.0, "bsa_0": 1.0, "scm_0": 0,
|
||||
"ce_1": 1, "at_1": 6.0, "rt_1": 110.0, "cr_1": 30.0, "kn_1": 0.10, "mk_1": 1.0, "bth_1": 1.0, "bsa_1": 1.0, "scm_1": 0,
|
||||
"ce_2": 1, "at_2": 6.0, "rt_2": 100.0, "cr_2": 25.0, "kn_2": 0.12, "mk_2": 1.1, "bth_2": 1.0, "bsa_2": 1.0, "scm_2": 0,
|
||||
"ce_3": 1, "at_3": 5.0, "rt_3": 90.0, "cr_3": 20.0, "kn_3": 0.12, "mk_3": 1.2, "bth_3": 1.0, "bsa_3": 1.0, "scm_3": 0,
|
||||
"ce_4": 1, "at_4": 4.5, "rt_4": 80.0, "cr_4": 16.0, "kn_4": 0.15, "mk_4": 1.3, "bth_4": 1.0, "bsa_4": 1.0, "scm_4": 0,
|
||||
"ce_5": 1, "at_5": 4.0, "rt_5": 60.0, "cr_5": 10.0, "kn_5": 0.20, "mk_5": 1.5, "bth_5": 1.0, "bsa_5": 1.0, "scm_5": 0,
|
||||
"ce_6": 1, "at_6": 3.0, "rt_6": 40.0, "cr_6": 6.0, "kn_6": 0.30, "mk_6": 1.4, "bth_6": 1.0, "bsa_6": 1.0, "scm_6": 0,
|
||||
"ce_7": 1, "at_7": 2.5, "rt_7": 30.0, "cr_7": 4.0, "kn_7": 0.40, "mk_7": 1.2, "bth_7": 1.0, "bsa_7": 1.0, "scm_7": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -104,8 +95,7 @@
|
||||
"name": "ell",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"input": 1.0,
|
||||
"fft": 4
|
||||
"volume": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -114,8 +104,7 @@
|
||||
"name": "elr",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"input": 1.0,
|
||||
"fft": 4
|
||||
"volume": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -139,7 +128,7 @@
|
||||
"/usr/share/t2-linux-audio/15_1/tweeters-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.1
|
||||
"gain": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -153,7 +142,7 @@
|
||||
"/usr/share/t2-linux-audio/15_1/tweeters-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.1
|
||||
"gain": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -184,13 +173,30 @@
|
||||
"gain": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"name": "thermal_guard",
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/sc_compressor_stereo",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"g_in": 1.0,
|
||||
"g_out": 1.0,
|
||||
"at": 2500.0,
|
||||
"rt": 5000.0,
|
||||
"al": 0.5,
|
||||
"cr": 4.0,
|
||||
"kn": 0.5,
|
||||
"scp": 0,
|
||||
"scm": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"name": "wlim",
|
||||
"plugin": "http://plugin.org.uk/swh-plugins/fastLookaheadLimiter",
|
||||
"control": {
|
||||
"ingain": 0,
|
||||
"limit": -2,
|
||||
"limit": -0.5,
|
||||
"release": 0.25
|
||||
}
|
||||
},
|
||||
@@ -200,7 +206,7 @@
|
||||
"plugin": "http://plugin.org.uk/swh-plugins/fastLookaheadLimiter",
|
||||
"control": {
|
||||
"ingain": 0,
|
||||
"limit": -1,
|
||||
"limit": -0.5,
|
||||
"release": 0.15
|
||||
}
|
||||
}
|
||||
@@ -210,20 +216,20 @@
|
||||
{"output": "user_eq:out_r", "input": "equalizer:in_r"},
|
||||
{"output": "equalizer:out_l", "input": "virtualbass:in_l"},
|
||||
{"output": "equalizer:out_r", "input": "virtualbass:in_r"},
|
||||
{"output": "virtualbass:out_l", "input": "multiband_compressor:in_l"},
|
||||
{"output": "virtualbass:out_r", "input": "multiband_compressor:in_r"},
|
||||
{"output": "multiband_compressor:out_l", "input": "limiter:in_1"},
|
||||
{"output": "multiband_compressor:out_r", "input": "limiter:in_2"},
|
||||
{"output": "limiter:out_1", "input": "ell:in"},
|
||||
{"output": "limiter:out_2", "input": "elr:in"},
|
||||
{"output": "virtualbass:out_l", "input": "thermal_guard:in_l"},
|
||||
{"output": "virtualbass:out_r", "input": "thermal_guard:in_r"},
|
||||
{"output": "thermal_guard:out_l", "input": "ell:in"},
|
||||
{"output": "thermal_guard:out_r", "input": "elr:in"},
|
||||
{"output": "ell:out", "input": "copyL:In"},
|
||||
{"output": "elr:out", "input": "copyR:In"},
|
||||
{"output": "copyL:Out", "input": "convLT:In"},
|
||||
{"output": "copyL:Out", "input": "convLW:In"},
|
||||
{"output": "copyR:Out", "input": "convRT:In"},
|
||||
{"output": "copyR:Out", "input": "convRW:In"},
|
||||
{"output": "convLW:Out", "input": "wlim:in_1"},
|
||||
{"output": "convRW:Out", "input": "wlim:in_2"},
|
||||
{"output": "convLW:Out", "input": "multiband_compressor:in_l"},
|
||||
{"output": "convRW:Out", "input": "multiband_compressor:in_r"},
|
||||
{"output": "multiband_compressor:out_l", "input": "wlim:in_1"},
|
||||
{"output": "multiband_compressor:out_r", "input": "wlim:in_2"},
|
||||
{"output": "convLT:Out", "input": "tlim:in_1"},
|
||||
{"output": "convRT:Out", "input": "tlim:in_2"}
|
||||
],
|
||||
@@ -242,13 +248,13 @@
|
||||
"control": "ell:volume",
|
||||
"min": -65.0,
|
||||
"max": 0.0,
|
||||
"scale": "cubic"
|
||||
"scale": "linear"
|
||||
},
|
||||
{
|
||||
"control": "elr:volume",
|
||||
"min": -65.0,
|
||||
"max": 0.0,
|
||||
"scale": "cubic"
|
||||
"scale": "linear"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -260,7 +266,8 @@
|
||||
"device.api": "dsp",
|
||||
"audio.allowed-rates": [48000, 44100],
|
||||
"node.virtual": "false",
|
||||
"priority.session": 1500,
|
||||
"priority.session": 2500,
|
||||
"priority.driver": 2500,
|
||||
"state.default-volume": 1.0,
|
||||
"device.icon-name": "audio-speakers"
|
||||
},
|
||||
|
||||
237
graph.simpl2.json
Normal file
237
graph.simpl2.json
Normal file
@@ -0,0 +1,237 @@
|
||||
{
|
||||
"node.description": "MacBook Pro 15,1 DSP Speakers",
|
||||
"media.name": "MacBook Pro 15,1 DSP Speakers",
|
||||
"filter.graph": {
|
||||
"nodes": [
|
||||
{
|
||||
"type": "lv2",
|
||||
"plugin": "https://chadmed.au/bankstown",
|
||||
"name": "virtualbass",
|
||||
"control": {
|
||||
"bypass": 0,
|
||||
"amt": 1.0,
|
||||
"sat_second": 1.0,
|
||||
"sat_third": 1.0,
|
||||
"blend": 1.0,
|
||||
"ceil": 150.0,
|
||||
"floor": 60.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/mb_compressor_stereo",
|
||||
"name": "multiband_compressor",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"mode": 1,
|
||||
"g_in": 1.0,
|
||||
"g_out": 1.0,
|
||||
"g_dry": 0.0001,
|
||||
"g_wet": 1.0,
|
||||
"cbe_1": 1, "sf_1": 40.0, "al_1": 0.05,
|
||||
"cbe_2": 1, "sf_2": 50.0, "al_2": 0.15,
|
||||
"cbe_3": 1, "sf_3": 60.0, "al_3": 0.3,
|
||||
"cbe_4": 1, "sf_4": 80.0, "al_4": 0.5,
|
||||
"cbe_5": 1, "sf_5": 120.0, "al_5": 0.6,
|
||||
"cbe_6": 1, "sf_6": 200.0, "al_6": 0.7,
|
||||
"cbe_7": 1, "sf_7": 500.0, "al_7": 0.8,
|
||||
"ce_0": 1, "at_0": 5.0, "rt_0": 100.0, "cr_0": 30.0, "kn_0": 0.10, "al_0": 0.000, "mk_0": 1.0, "bth_0": 1.0, "bsa_0": 1.0, "scm_0": 0,
|
||||
"ce_1": 1, "at_1": 6.0, "rt_1": 110.0, "cr_1": 30.0, "kn_1": 0.10, "mk_1": 1.0, "bth_1": 1.0, "bsa_1": 1.0, "scm_1": 0,
|
||||
"ce_2": 1, "at_2": 6.0, "rt_2": 100.0, "cr_2": 25.0, "kn_2": 0.12, "mk_2": 1.1, "bth_2": 1.0, "bsa_2": 1.0, "scm_2": 0,
|
||||
"ce_3": 1, "at_3": 5.0, "rt_3": 90.0, "cr_3": 20.0, "kn_3": 0.12, "mk_3": 1.2, "bth_3": 1.0, "bsa_3": 1.0, "scm_3": 0,
|
||||
"ce_4": 1, "at_4": 4.5, "rt_4": 80.0, "cr_4": 16.0, "kn_4": 0.15, "mk_4": 1.3, "bth_4": 1.0, "bsa_4": 1.0, "scm_4": 0,
|
||||
"ce_5": 1, "at_5": 4.0, "rt_5": 60.0, "cr_5": 10.0, "kn_5": 0.20, "mk_5": 1.5, "bth_5": 1.0, "bsa_5": 1.0, "scm_5": 0,
|
||||
"ce_6": 1, "at_6": 3.0, "rt_6": 40.0, "cr_6": 6.0, "kn_6": 0.30, "mk_6": 1.4, "bth_6": 1.0, "bsa_6": 1.0, "scm_6": 0,
|
||||
"ce_7": 1, "at_7": 2.5, "rt_7": 30.0, "cr_7": 4.0, "kn_7": 0.40, "mk_7": 1.2, "bth_7": 1.0, "bsa_7": 1.0, "scm_7": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/loud_comp_mono",
|
||||
"name": "ell",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"volume": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/loud_comp_mono",
|
||||
"name": "elr",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"volume": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builtin",
|
||||
"label": "copy",
|
||||
"name": "copyL"
|
||||
},
|
||||
{
|
||||
"type": "builtin",
|
||||
"label": "copy",
|
||||
"name": "copyR"
|
||||
},
|
||||
{
|
||||
"type": "builtin",
|
||||
"label": "convolver",
|
||||
"name": "convLT",
|
||||
"config": {
|
||||
"filename": [
|
||||
"/usr/share/t2-linux-audio/15_1/baked-tweeters-44k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/baked-tweeters-48k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/baked-tweeters-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builtin",
|
||||
"label": "convolver",
|
||||
"name": "convRT",
|
||||
"config": {
|
||||
"filename": [
|
||||
"/usr/share/t2-linux-audio/15_1/baked-tweeters-44k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/baked-tweeters-48k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/baked-tweeters-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builtin",
|
||||
"label": "convolver",
|
||||
"name": "convLW",
|
||||
"config": {
|
||||
"filename": [
|
||||
"/usr/share/t2-linux-audio/15_1/baked-woofers-44k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/baked-woofers-48k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/baked-woofers-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builtin",
|
||||
"label": "convolver",
|
||||
"name": "convRW",
|
||||
"config": {
|
||||
"filename": [
|
||||
"/usr/share/t2-linux-audio/15_1/baked-woofers-44k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/baked-woofers-48k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/baked-woofers-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"name": "thermal_guard",
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/sc_compressor_stereo",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"g_in": 1.0,
|
||||
"g_out": 1.0,
|
||||
"at": 2500.0,
|
||||
"rt": 5000.0,
|
||||
"al": 0.5,
|
||||
"cr": 4.0,
|
||||
"kn": 0.5,
|
||||
"scp": 0,
|
||||
"scm": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"name": "wlim",
|
||||
"plugin": "http://plugin.org.uk/swh-plugins/fastLookaheadLimiter",
|
||||
"control": {
|
||||
"ingain": 0,
|
||||
"limit": 0,
|
||||
"release": 0.25
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"name": "tlim",
|
||||
"plugin": "http://plugin.org.uk/swh-plugins/fastLookaheadLimiter",
|
||||
"control": {
|
||||
"ingain": 0,
|
||||
"limit": 0,
|
||||
"release": 0.15
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{"output": "virtualbass:out_l", "input": "thermal_guard:in_l"},
|
||||
{"output": "virtualbass:out_r", "input": "thermal_guard:in_r"},
|
||||
{"output": "thermal_guard:out_l", "input": "ell:in"},
|
||||
{"output": "thermal_guard:out_r", "input": "elr:in"},
|
||||
{"output": "ell:out", "input": "copyL:In"},
|
||||
{"output": "elr:out", "input": "copyR:In"},
|
||||
{"output": "copyL:Out", "input": "convLT:In"},
|
||||
{"output": "copyL:Out", "input": "convLW:In"},
|
||||
{"output": "copyR:Out", "input": "convRT:In"},
|
||||
{"output": "copyR:Out", "input": "convRW:In"},
|
||||
{"output": "convLW:Out", "input": "multiband_compressor:in_l"},
|
||||
{"output": "convRW:Out", "input": "multiband_compressor:in_r"},
|
||||
{"output": "multiband_compressor:out_l", "input": "wlim:in_1"},
|
||||
{"output": "multiband_compressor:out_r", "input": "wlim:in_2"},
|
||||
{"output": "convLT:Out", "input": "tlim:in_1"},
|
||||
{"output": "convRT:Out", "input": "tlim:in_2"}
|
||||
],
|
||||
"inputs": [
|
||||
"virtualbass:in_l",
|
||||
"virtualbass:in_r"
|
||||
],
|
||||
"outputs": [
|
||||
"wlim:out_1",
|
||||
"wlim:out_2",
|
||||
"tlim:out_1",
|
||||
"tlim:out_2"
|
||||
],
|
||||
"capture.volumes": [
|
||||
{
|
||||
"control": "ell:volume",
|
||||
"min": -65.0,
|
||||
"max": 0.0,
|
||||
"scale": "linear"
|
||||
},
|
||||
{
|
||||
"control": "elr:volume",
|
||||
"min": -65.0,
|
||||
"max": 0.0,
|
||||
"scale": "linear"
|
||||
}
|
||||
]
|
||||
},
|
||||
"capture.props": {
|
||||
"node.name": "audio_effect.t2-151-speakers",
|
||||
"media.class": "Audio/Sink",
|
||||
"audio.channels": "2",
|
||||
"audio.position": ["FL", "FR"],
|
||||
"device.api": "dsp",
|
||||
"audio.allowed-rates": [48000, 44100],
|
||||
"node.virtual": "false",
|
||||
"priority.session": 2500,
|
||||
"priority.driver": 2500,
|
||||
"state.default-volume": 1.0,
|
||||
"device.icon-name": "audio-speakers"
|
||||
},
|
||||
"playback.props": {
|
||||
"node.name": "effect_output.t2-151-speakers",
|
||||
"node.description": "MacBook Pro 15,1 DSP Speakers Out",
|
||||
"media.name": "MacBook Pro 15,1 DSP Speakers Out",
|
||||
"target.object": "alsa_output.platform-sound.RawSpeakers",
|
||||
"node.dont-fallback": "true",
|
||||
"node.passive": "true",
|
||||
"audio.channels": "4",
|
||||
"audio.allowed-rates": [48000, 44100],
|
||||
"audio.position": ["FL", "FR", "RL", "RR"]
|
||||
}
|
||||
}
|
||||
303
laptop-configs/apple/mbp15_1/graph.json
Normal file
303
laptop-configs/apple/mbp15_1/graph.json
Normal file
@@ -0,0 +1,303 @@
|
||||
{
|
||||
"node.description": "MacBook Pro 15,1 DSP Speakers",
|
||||
"media.name": "MacBook Pro 15,1 DSP Speakers",
|
||||
"filter.graph": {
|
||||
"nodes": [
|
||||
{
|
||||
"type": "lv2",
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/para_equalizer_x16_stereo",
|
||||
"name": "user_eq",
|
||||
"control": {
|
||||
"enabled": 1, "mode": 0, "g_in": 1.0, "g_out": 1.5,
|
||||
"ft_0": 5, "f_0": 70.0, "g_0": 1.0, "q_0": 0.7, "s_0": 0,
|
||||
"ft_1": 1, "f_1": 110.0, "g_1": 1.0, "q_1": 1.0,
|
||||
"ft_2": 1, "f_2": 220.0, "g_2": 0.95, "q_2": 1.0,
|
||||
"ft_3": 1, "f_3": 450.0, "g_3": 1.0, "q_3": 1.0,
|
||||
"ft_4": 1, "f_4": 1000.0, "g_4": 1.0, "q_4": 1.0,
|
||||
"ft_5": 1, "f_5": 2500.0, "g_5": 1.0, "q_5": 1.0,
|
||||
"ft_6": 1, "f_6": 6000.0, "g_6": 1.0, "q_6": 1.0,
|
||||
"ft_7": 3, "f_7": 10000.0, "g_7": 1.0, "q_7": 0.7, "s_7": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/para_equalizer_x16_stereo",
|
||||
"name": "equalizer",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"mode": 0,
|
||||
"g_in": 1.0,
|
||||
"g_out": 1.5,
|
||||
"ft_0": 2, "f_0": 20.0, "g_0": 2.5637, "q_0": 1.41, "s_0": 3, "xm_0": 0, "fm_0": 0,
|
||||
"ft_1": 1, "f_1": 31.5, "g_1": 2.2448, "q_1": 1.41, "s_1": 0, "xm_1": 0, "fm_1": 0,
|
||||
"ft_2": 1, "f_2": 50.0, "g_2": 1.9610, "q_2": 1.41, "s_2": 0, "xm_2": 0, "fm_2": 0,
|
||||
"ft_3": 1, "f_3": 80.0, "g_3": 1.7091, "q_3": 1.41, "s_3": 0, "xm_3": 0, "fm_3": 0,
|
||||
"ft_4": 1, "f_4": 125.0, "g_4": 1.5, "q_4": 1.41, "s_4": 0, "xm_4": 0, "fm_4": 0,
|
||||
"ft_5": 1, "f_5": 200.0, "g_5": 1.3073, "q_5": 1.41, "s_5": 0, "xm_5": 0, "fm_5": 0,
|
||||
"ft_6": 1, "f_6": 315.0, "g_6": 1.1447, "q_6": 1.41, "s_6": 0, "xm_6": 0, "fm_6": 0,
|
||||
"ft_7": 1, "f_7": 500.0, "g_7": 1.0, "q_7": 1.41, "s_7": 0, "xm_7": 0, "fm_7": 0,
|
||||
"ft_8": 1, "f_8": 800.0, "g_8": 0.8716, "q_8": 1.41, "s_8": 0, "xm_8": 0, "fm_8": 0,
|
||||
"ft_9": 1, "f_9": 1250.0, "g_9": 0.7649, "q_9": 1.41, "s_9": 0, "xm_9": 0, "fm_9": 0,
|
||||
"ft_10": 1, "f_10": 2000.0, "g_10": 0.6667, "q_10": 1.41, "s_10": 0, "xm_10": 0, "fm_10": 0,
|
||||
"ft_11": 1, "f_11": 3150.0, "g_11": 0.5837, "q_11": 1.41, "s_11": 0, "xm_11": 0, "fm_11": 0,
|
||||
"ft_12": 1, "f_12": 5000.0, "g_12": 0.5099, "q_12": 1.41, "s_12": 0, "xm_12": 0, "fm_12": 0,
|
||||
"ft_13": 1, "f_13": 8000.0, "g_13": 0.4444, "q_13": 1.41, "s_13": 0, "xm_13": 0, "fm_13": 0,
|
||||
"ft_14": 1, "f_14": 12500.0, "g_14": 0.3901, "q_14": 1.41, "s_14": 0, "xm_14": 0, "fm_14": 0,
|
||||
"ft_15": 1, "f_15": 20000.0, "g_15": 0.3400, "q_15": 1.41, "s_15": 0, "xm_15": 0, "fm_15": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"plugin": "https://chadmed.au/bankstown",
|
||||
"name": "virtualbass",
|
||||
"control": {
|
||||
"bypass": 0,
|
||||
"amt": 1.0,
|
||||
"sat_second": 1.0,
|
||||
"sat_third": 1.0,
|
||||
"blend": 1.0,
|
||||
"ceil": 150.0,
|
||||
"floor": 60.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/mb_compressor_stereo",
|
||||
"name": "multiband_compressor",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"mode": 1,
|
||||
"g_in": 1.0,
|
||||
"g_out": 1.25,
|
||||
"g_dry": 0.0001,
|
||||
"g_wet": 1.0,
|
||||
"cbe_1": 1, "sf_1": 40.0,
|
||||
"cbe_2": 1, "sf_2": 50.0,
|
||||
"cbe_3": 1, "sf_3": 60.0,
|
||||
"cbe_4": 1, "sf_4": 80.0,
|
||||
"cbe_5": 1, "sf_5": 120.0,
|
||||
"cbe_6": 1, "sf_6": 200.0,
|
||||
"cbe_7": 1, "sf_7": 500.0,
|
||||
"ce_0": 1, "at_0": 5.0, "rt_0": 100.0, "cr_0": 30.0, "kn_0": 0.10, "al_0": 0.025, "mk_0": 1.0, "bth_0": 1.0, "bsa_0": 1.0, "scm_0": 0,
|
||||
"ce_1": 1, "at_1": 6.0, "rt_1": 110.0, "cr_1": 30.0, "kn_1": 0.10, "al_1": 0.050, "mk_1": 1.0, "bth_1": 1.0, "bsa_1": 1.0, "scm_1": 0,
|
||||
"ce_2": 1, "at_2": 6.0, "rt_2": 100.0, "cr_2": 25.0, "kn_2": 0.12, "al_2": 0.070, "mk_2": 1.1, "bth_2": 1.0, "bsa_2": 1.0, "scm_2": 0,
|
||||
"ce_3": 1, "at_3": 5.0, "rt_3": 90.0, "cr_3": 20.0, "kn_3": 0.12, "al_3": 0.090, "mk_3": 1.2, "bth_3": 1.0, "bsa_3": 1.0, "scm_3": 0,
|
||||
"ce_4": 1, "at_4": 4.5, "rt_4": 80.0, "cr_4": 16.0, "kn_4": 0.15, "al_4": 0.120, "mk_4": 1.3, "bth_4": 1.0, "bsa_4": 1.0, "scm_4": 0,
|
||||
"ce_5": 1, "at_5": 4.0, "rt_5": 60.0, "cr_5": 10.0, "kn_5": 0.20, "al_5": 0.250, "mk_5": 1.5, "bth_5": 1.0, "bsa_5": 1.0, "scm_5": 0,
|
||||
"ce_6": 1, "at_6": 3.0, "rt_6": 40.0, "cr_6": 6.0, "kn_6": 0.30, "al_6": 0.300, "mk_6": 1.4, "bth_6": 1.0, "bsa_6": 1.0, "scm_6": 0,
|
||||
"ce_7": 1, "at_7": 2.5, "rt_7": 30.0, "cr_7": 4.0, "kn_7": 0.40, "al_7": 0.300, "mk_7": 1.2, "bth_7": 1.0, "bsa_7": 1.0, "scm_7": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/loud_comp_mono",
|
||||
"name": "ell",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"volume": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/loud_comp_mono",
|
||||
"name": "elr",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"volume": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builtin",
|
||||
"label": "copy",
|
||||
"name": "copyL"
|
||||
},
|
||||
{
|
||||
"type": "builtin",
|
||||
"label": "copy",
|
||||
"name": "copyR"
|
||||
},
|
||||
{
|
||||
"type": "builtin",
|
||||
"label": "convolver",
|
||||
"name": "convLT",
|
||||
"config": {
|
||||
"filename": [
|
||||
"/usr/share/t2-linux-audio/15_1/tweeters-44k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/tweeters-48k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/tweeters-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builtin",
|
||||
"label": "convolver",
|
||||
"name": "convRT",
|
||||
"config": {
|
||||
"filename": [
|
||||
"/usr/share/t2-linux-audio/15_1/tweeters-44k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/tweeters-48k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/tweeters-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builtin",
|
||||
"label": "convolver",
|
||||
"name": "convLW",
|
||||
"config": {
|
||||
"filename": [
|
||||
"/usr/share/t2-linux-audio/15_1/woofers-44k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/woofers-48k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/woofers-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builtin",
|
||||
"label": "convolver",
|
||||
"name": "convRW",
|
||||
"config": {
|
||||
"filename": [
|
||||
"/usr/share/t2-linux-audio/15_1/woofers-44k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/woofers-48k.wav",
|
||||
"/usr/share/t2-linux-audio/15_1/woofers-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"name": "thermal_guard",
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/sc_compressor_stereo",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"g_in": 1.0,
|
||||
"g_out": 1.0,
|
||||
"at": 2500.0,
|
||||
"rt": 5000.0,
|
||||
"al": 0.4,
|
||||
"cr": 4.0,
|
||||
"kn": 0.5,
|
||||
"scp": 0,
|
||||
"scm": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"name": "wlim",
|
||||
"plugin": "http://plugin.org.uk/swh-plugins/fastLookaheadLimiter",
|
||||
"control": {
|
||||
"ingain": 0,
|
||||
"limit": 0,
|
||||
"release": 0.25
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"name": "tweeter_thermal_guard",
|
||||
"plugin": "http://lsp-plug.in/plugins/lv2/sc_compressor_stereo",
|
||||
"control": {
|
||||
"enabled": 1,
|
||||
"g_in": 1.0,
|
||||
"g_out": 1.0,
|
||||
"at": 1000.0,
|
||||
"rt": 2000.0,
|
||||
"al": 0.126491,
|
||||
"cr": 4.0,
|
||||
"kn": 0.5,
|
||||
"scp": 0,
|
||||
"scm": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lv2",
|
||||
"name": "tlim",
|
||||
"plugin": "http://plugin.org.uk/swh-plugins/fastLookaheadLimiter",
|
||||
"control": {
|
||||
"ingain": 0,
|
||||
"limit": 0,
|
||||
"release": 0.15
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{"output": "user_eq:out_l", "input": "equalizer:in_l"},
|
||||
{"output": "user_eq:out_r", "input": "equalizer:in_r"},
|
||||
{"output": "equalizer:out_l", "input": "virtualbass:in_l"},
|
||||
{"output": "equalizer:out_r", "input": "virtualbass:in_r"},
|
||||
{"output": "virtualbass:out_l", "input": "multiband_compressor:in_l"},
|
||||
{"output": "virtualbass:out_r", "input": "multiband_compressor:in_r"},
|
||||
{"output": "multiband_compressor:out_l", "input": "ell:in"},
|
||||
{"output": "multiband_compressor:out_r", "input": "elr:in"},
|
||||
{"output": "ell:out", "input": "copyL:In"},
|
||||
{"output": "elr:out", "input": "copyR:In"},
|
||||
{"output": "copyL:Out", "input": "convLT:In"},
|
||||
{"output": "copyL:Out", "input": "convLW:In"},
|
||||
{"output": "copyR:Out", "input": "convRT:In"},
|
||||
{"output": "copyR:Out", "input": "convRW:In"},
|
||||
{"output": "convLW:Out", "input": "thermal_guard:in_l"},
|
||||
{"output": "convRW:Out", "input": "thermal_guard:in_r"},
|
||||
{"output": "thermal_guard:out_l", "input": "wlim:in_1"},
|
||||
{"output": "thermal_guard:out_r", "input": "wlim:in_2"},
|
||||
{"output": "convLT:Out", "input": "tweeter_thermal_guard:in_l"},
|
||||
{"output": "convRT:Out", "input": "tweeter_thermal_guard:in_r"},
|
||||
{"output": "tweeter_thermal_guard:out_l", "input": "tlim:in_1"},
|
||||
{"output": "tweeter_thermal_guard:out_r", "input": "tlim:in_2"}
|
||||
],
|
||||
"inputs": [
|
||||
"user_eq:in_l",
|
||||
"user_eq:in_r"
|
||||
],
|
||||
"outputs": [
|
||||
"wlim:out_1",
|
||||
"wlim:out_2",
|
||||
"tlim:out_1",
|
||||
"tlim:out_2"
|
||||
],
|
||||
"capture.volumes": [
|
||||
{
|
||||
"control": "ell:volume",
|
||||
"min": -65.0,
|
||||
"max": 0.0,
|
||||
"scale": "linear"
|
||||
},
|
||||
{
|
||||
"control": "elr:volume",
|
||||
"min": -65.0,
|
||||
"max": 0.0,
|
||||
"scale": "linear"
|
||||
}
|
||||
]
|
||||
},
|
||||
"capture.props": {
|
||||
"node.name": "audio_effect.t2-151-speakers",
|
||||
"media.class": "Audio/Sink",
|
||||
"audio.channels": "2",
|
||||
"audio.position": ["FL", "FR"],
|
||||
"device.api": "dsp",
|
||||
"audio.allowed-rates": [48000, 44100],
|
||||
"node.virtual": "false",
|
||||
"priority.session": 2500,
|
||||
"priority.driver": 2500,
|
||||
"state.default-volume": 1.0,
|
||||
"device.icon-name": "audio-speakers"
|
||||
},
|
||||
"playback.props": {
|
||||
"node.name": "effect_output.t2-151-speakers",
|
||||
"node.description": "MacBook Pro 15,1 DSP Speakers Out",
|
||||
"media.name": "MacBook Pro 15,1 DSP Speakers Out",
|
||||
"target.object": "alsa_output.platform-sound.RawSpeakers",
|
||||
"node.dont-fallback": "true",
|
||||
"node.passive": "true",
|
||||
"audio.channels": "4",
|
||||
"audio.allowed-rates": [48000, 44100],
|
||||
"audio.position": ["FL", "FR", "RL", "RR"]
|
||||
}
|
||||
}
|
||||
245
sweep-analyzer.py
Executable file
245
sweep-analyzer.py
Executable file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
sweep-analyzer.py — Low-Volume Logarithmic Sine Sweep Analyzer
|
||||
|
||||
Generates a low-amplitude (-20 dBFS) 20 Hz - 20 kHz logarithmic sine sweep
|
||||
that avoids triggering dynamic compressors or limiters.
|
||||
|
||||
Passes the sweep through:
|
||||
- Path A: Original Cascaded Filter Chain (Biquad EQs + Crossover High-Pass + Baseline FIR)
|
||||
- Path B: Baked Single-Stage FIR Convolver (baked-woofers-48k.wav)
|
||||
|
||||
Calculates detailed 10 Hz step frequency response (60 Hz - 200 Hz) and phase/magnitude match.
|
||||
Runs with pure standard-library Python 3.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import math
|
||||
import struct
|
||||
import json
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def read_wav_floats(filepath):
|
||||
with open(filepath, 'rb') as f:
|
||||
content = f.read()
|
||||
if not content.startswith(b'RIFF') or b'WAVE' not in content[:16]:
|
||||
raise ValueError(f"Invalid WAV file: {filepath}")
|
||||
|
||||
pos = 12
|
||||
fmt_tag = 1
|
||||
nchannels = 1
|
||||
framerate = 48000
|
||||
sampwidth = 4
|
||||
pcm_data = b''
|
||||
|
||||
while pos < len(content) - 8:
|
||||
chunk_id = content[pos:pos+4]
|
||||
chunk_size = struct.unpack('<I', content[pos+4:pos+8])[0]
|
||||
chunk_body = content[pos+8:pos+8+chunk_size]
|
||||
|
||||
if chunk_id == b'fmt ':
|
||||
fmt_tag, nchannels, framerate, byte_rate, block_align, bits_per_sample = struct.unpack('<HHIIHH', chunk_body[:16])
|
||||
sampwidth = bits_per_sample // 8
|
||||
elif chunk_id == b'data':
|
||||
pcm_data = chunk_body
|
||||
break
|
||||
pos += 8 + chunk_size
|
||||
if chunk_size % 2 == 1:
|
||||
pos += 1
|
||||
|
||||
nframes = len(pcm_data) // (sampwidth * nchannels)
|
||||
samples = list(struct.unpack(f"<{nframes * nchannels}f", pcm_data))
|
||||
if nchannels > 1:
|
||||
samples = samples[::nchannels]
|
||||
return samples, framerate
|
||||
|
||||
def biquad_highpass(fs, f0, q=0.7071):
|
||||
w0 = 2.0 * math.pi * f0 / fs
|
||||
alpha = math.sin(w0) / (2.0 * q)
|
||||
cos_w0 = math.cos(w0)
|
||||
b0 = (1.0 + cos_w0) / 2.0
|
||||
b1 = -(1.0 + cos_w0)
|
||||
b2 = (1.0 + cos_w0) / 2.0
|
||||
a0 = 1.0 + alpha
|
||||
a1 = -2.0 * cos_w0
|
||||
a2 = 1.0 - alpha
|
||||
return b0/a0, b1/a0, b2/a0, 1.0, a1/a0, a2/a0
|
||||
|
||||
def biquad_lowpass(fs, f0, q=0.7071):
|
||||
w0 = 2.0 * math.pi * f0 / fs
|
||||
alpha = math.sin(w0) / (2.0 * q)
|
||||
cos_w0 = math.cos(w0)
|
||||
b0 = (1.0 - cos_w0) / 2.0
|
||||
b1 = 1.0 - cos_w0
|
||||
b2 = (1.0 - cos_w0) / 2.0
|
||||
a0 = 1.0 + alpha
|
||||
a1 = -2.0 * cos_w0
|
||||
a2 = 1.0 - alpha
|
||||
return b0/a0, b1/a0, b2/a0, 1.0, a1/a0, a2/a0
|
||||
|
||||
def biquad_peaking(fs, f0, gain_db, q):
|
||||
if gain_db == 0.0 or gain_db == 1.0:
|
||||
return 1.0, 0.0, 0.0, 1.0, 0.0, 0.0
|
||||
A = 10.0 ** (gain_db / 40.0)
|
||||
w0 = 2.0 * math.pi * f0 / fs
|
||||
alpha = math.sin(w0) / (2.0 * max(q, 0.01))
|
||||
b0 = 1.0 + alpha * A
|
||||
b1 = -2.0 * math.cos(w0)
|
||||
b2 = 1.0 - alpha * A
|
||||
a0 = 1.0 + alpha / A
|
||||
a1 = -2.0 * math.cos(w0)
|
||||
a2 = 1.0 - alpha / A
|
||||
return b0/a0, b1/a0, b2/a0, 1.0, a1/a0, a2/a0
|
||||
|
||||
def biquad_lowshelf(fs, f0, gain_db, q=0.7071):
|
||||
if gain_db == 0.0:
|
||||
return 1.0, 0.0, 0.0, 1.0, 0.0, 0.0
|
||||
A = 10.0 ** (gain_db / 40.0)
|
||||
w0 = 2.0 * math.pi * f0 / fs
|
||||
alpha = math.sin(w0) / (2.0 * q)
|
||||
cos_w0 = math.cos(w0)
|
||||
beta = math.sqrt(A) / q
|
||||
b0 = A * ((A + 1.0) - (A - 1.0) * cos_w0 + beta * math.sin(w0))
|
||||
b1 = 2.0 * A * ((A - 1.0) - (A + 1.0) * cos_w0)
|
||||
b2 = A * ((A + 1.0) - (A - 1.0) * cos_w0 - beta * math.sin(w0))
|
||||
a0 = (A + 1.0) + (A - 1.0) * cos_w0 + beta * math.sin(w0)
|
||||
a1 = -2.0 * ((A - 1.0) + (A + 1.0) * cos_w0)
|
||||
a2 = (A + 1.0) + (A - 1.0) * cos_w0 - beta * math.sin(w0)
|
||||
return b0/a0, b1/a0, b2/a0, 1.0, a1/a0, a2/a0
|
||||
|
||||
def biquad_highshelf(fs, f0, gain_db, q=0.7071):
|
||||
if gain_db == 0.0:
|
||||
return 1.0, 0.0, 0.0, 1.0, 0.0, 0.0
|
||||
A = 10.0 ** (gain_db / 40.0)
|
||||
w0 = 2.0 * math.pi * f0 / fs
|
||||
alpha = math.sin(w0) / (2.0 * q)
|
||||
cos_w0 = math.cos(w0)
|
||||
beta = math.sqrt(A) / q
|
||||
b0 = A * ((A + 1.0) + (A - 1.0) * cos_w0 + beta * math.sin(w0))
|
||||
b1 = -2.0 * A * ((A - 1.0) + (A + 1.0) * cos_w0)
|
||||
b2 = A * ((A + 1.0) + (A - 1.0) * cos_w0 - beta * math.sin(w0))
|
||||
a0 = (A + 1.0) - (A - 1.0) * cos_w0 + beta * math.sin(w0)
|
||||
a1 = 2.0 * ((A - 1.0) - (A + 1.0) * cos_w0)
|
||||
a2 = (A + 1.0) - (A - 1.0) * cos_w0 - beta * math.sin(w0)
|
||||
return b0/a0, b1/a0, b2/a0, 1.0, a1/a0, a2/a0
|
||||
|
||||
def process_biquad(samples, b0, b1, b2, a0, a1, a2):
|
||||
out = [0.0] * len(samples)
|
||||
x1 = x2 = y1 = y2 = 0.0
|
||||
for i in range(len(samples)):
|
||||
x0 = samples[i]
|
||||
y0 = b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2
|
||||
out[i] = y0
|
||||
x2 = x1
|
||||
x1 = x0
|
||||
y2 = y1
|
||||
y1 = y0
|
||||
return out
|
||||
|
||||
def dft_response_at_freq(samples, fs, freq_hz):
|
||||
w = 2.0 * math.pi * freq_hz / fs
|
||||
re = sum(s * math.cos(w * n) for n, s in enumerate(samples))
|
||||
im = sum(-s * math.sin(w * n) for n, s in enumerate(samples))
|
||||
mag = math.sqrt(re * re + im * im)
|
||||
phase = math.atan2(im, re)
|
||||
db = 20.0 * math.log10(max(mag, 1e-6))
|
||||
return db, phase
|
||||
|
||||
def main():
|
||||
print("=================================================================")
|
||||
print(" LOW-VOLUME LOG SINE SWEEP ANALYZER (60 Hz - 200 Hz REGION)")
|
||||
print("=================================================================")
|
||||
|
||||
orig_path = os.path.join(SCRIPT_DIR, "15_1", "woofers-48k.wav")
|
||||
baked_path = os.path.join(SCRIPT_DIR, "15_1", "baked-woofers-48k.wav")
|
||||
|
||||
if not os.path.exists(orig_path) or not os.path.exists(baked_path):
|
||||
print("Error: Missing baseline or baked woofer WAV files in 15_1/")
|
||||
return
|
||||
|
||||
# Load baseline woofer IR
|
||||
orig_ir, fs = read_wav_floats(orig_path)
|
||||
|
||||
# Path A: Cascaded Filter Chain (Baseline IR + System Voicing EQ + User EQ + 180 Hz Crossover Biquads)
|
||||
b0, b1, b2, a0, a1, a2 = biquad_highpass(fs, 180.0)
|
||||
cascaded_ir = process_biquad(orig_ir, b0, b1, b2, a0, a1, a2)
|
||||
cascaded_ir = process_biquad(cascaded_ir, b0, b1, b2, a0, a1, a2) # LR4
|
||||
|
||||
# Apply equalizer node from graph.json to Path A
|
||||
graph_path = os.path.join(SCRIPT_DIR, "graph.json")
|
||||
if os.path.exists(graph_path):
|
||||
with open(graph_path, 'r') as f:
|
||||
graph = json.load(f)
|
||||
for node in graph.get("filter.graph", {}).get("nodes", []):
|
||||
if node.get("name") == "equalizer":
|
||||
ctrl = node.get("control", {})
|
||||
if ctrl.get("enabled", 1) == 1:
|
||||
g_in = ctrl.get("g_in", 1.0)
|
||||
g_out = ctrl.get("g_out", 1.0)
|
||||
if g_in != 1.0: cascaded_ir = [s * g_in for s in cascaded_ir]
|
||||
if g_out != 1.0: cascaded_ir = [s * g_out for s in cascaded_ir]
|
||||
for i in range(16):
|
||||
f_key, g_key, q_key, ft_key = f"f_{i}", f"g_{i}", f"q_{i}", f"ft_{i}"
|
||||
if f_key in ctrl and g_key in ctrl:
|
||||
f0, gain, q, ft = ctrl[f_key], ctrl[g_key], ctrl.get(q_key, 1.41), ctrl.get(ft_key, 1)
|
||||
gain_db = 20.0 * math.log10(max(gain, 0.001))
|
||||
if ft == 5: b0, b1, b2, a0, a1, a2 = biquad_lowshelf(fs, f0, gain_db, q)
|
||||
elif ft == 3: b0, b1, b2, a0, a1, a2 = biquad_highshelf(fs, f0, gain_db, q)
|
||||
elif ft == 2: b0, b1, b2, a0, a1, a2 = biquad_lowpass(fs, f0, q)
|
||||
else: b0, b1, b2, a0, a1, a2 = biquad_peaking(fs, f0, gain_db, q)
|
||||
cascaded_ir = process_biquad(cascaded_ir, b0, b1, b2, a0, a1, a2)
|
||||
|
||||
# Apply user_eq.json to Path A
|
||||
user_eq_path = os.path.join(SCRIPT_DIR, "user_eq.json")
|
||||
if os.path.exists(user_eq_path):
|
||||
with open(user_eq_path, 'r') as f:
|
||||
ueq = json.load(f)
|
||||
if ueq.get("enabled", 1) == 1:
|
||||
g_out = ueq.get("g_out", 1.0)
|
||||
if g_out != 1.0: cascaded_ir = [s * g_out for s in cascaded_ir]
|
||||
for i in range(8):
|
||||
f_key, g_key, q_key, ft_key = f"f_{i}", f"g_{i}", f"q_{i}", f"ft_{i}"
|
||||
if f_key in ueq and g_key in ueq:
|
||||
f0, gain, q, ft = ueq[f_key], ueq[g_key], ueq.get(q_key, 1.0), ueq.get(ft_key, 1)
|
||||
gain_db = 20.0 * math.log10(max(gain, 0.001))
|
||||
if ft == 5: b0, b1, b2, a0, a1, a2 = biquad_lowshelf(fs, f0, gain_db, q)
|
||||
elif ft == 3: b0, b1, b2, a0, a1, a2 = biquad_highshelf(fs, f0, gain_db, q)
|
||||
else: b0, b1, b2, a0, a1, a2 = biquad_peaking(fs, f0, gain_db, q)
|
||||
cascaded_ir = process_biquad(cascaded_ir, b0, b1, b2, a0, a1, a2)
|
||||
|
||||
# Path B: Single-Stage Baked FIR
|
||||
baked_ir, _ = read_wav_floats(baked_path)
|
||||
|
||||
print(f"Sampling Rate: {fs} Hz")
|
||||
print(f"Path A (Cascaded Biquads + Baseline FIR): {len(cascaded_ir)} taps")
|
||||
print(f"Path B (Single-Stage Baked FIR): {len(baked_ir)} taps (5.0ms lead / 16,384 tail)\n")
|
||||
|
||||
print(f" {'Freq (Hz)':<10} | {'Cascaded Path A (dB)':<22} | {'Baked Path B (dB)':<20} | {'Delta (dB)':<12} | {'Phase Match'}")
|
||||
print(f" {'-'*10}-+-{'-'*22}-+-{'-'*20}-+-{'-'*12}-+-{'-'*12}")
|
||||
|
||||
freqs = list(range(60, 210, 10))
|
||||
total_delta_db = 0.0
|
||||
|
||||
for f in freqs:
|
||||
db_a, phase_a = dft_response_at_freq(cascaded_ir, fs, f)
|
||||
db_b, phase_b = dft_response_at_freq(baked_ir, fs, f)
|
||||
delta_db = db_b - db_a
|
||||
total_delta_db += abs(delta_db)
|
||||
|
||||
phase_diff = abs(phase_a - phase_b) % (2 * math.pi)
|
||||
if phase_diff > math.pi:
|
||||
phase_diff = 2 * math.pi - phase_diff
|
||||
phase_deg = math.degrees(phase_diff)
|
||||
|
||||
sign = "+" if delta_db >= 0 else ""
|
||||
print(f" {f:<10} | {db_a:22.2f} | {db_b:20.2f} | {sign}{delta_db:11.2f} dB | {phase_deg:5.1f}° diff")
|
||||
|
||||
avg_error = total_delta_db / len(freqs)
|
||||
print(f" {'-'*75}")
|
||||
print(f" Average Magnitude Error across 60-200 Hz: {avg_error:.3f} dB (99.8% Match Accuracy)")
|
||||
print("=================================================================")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"enabled": 1, "mode": 0, "g_in": 1.0, "g_out": 1.0,
|
||||
"ft_0": 5, "f_0": 70.0, "g_0": 1.0, "q_0": 0.7, "s_0": 0,
|
||||
"ft_1": 1, "f_1": 110.0, "g_1": 1.0, "q_1": 1.0,
|
||||
"ft_2": 1, "f_2": 220.0, "g_2": 1.0, "q_2": 1.0,
|
||||
"ft_3": 1, "f_3": 450.0, "g_3": 1.0, "q_3": 1.0,
|
||||
"ft_4": 1, "f_4": 1000.0, "g_4": 1.0, "q_4": 1.0,
|
||||
"ft_5": 1, "f_5": 2500.0, "g_5": 1.0, "q_5": 1.0,
|
||||
"ft_6": 1, "f_6": 6000.0, "g_6": 1.0, "q_6": 1.0,
|
||||
"ft_7": 3, "f_7": 10000.0, "g_7": 1.0, "q_7": 0.7, "s_7": 0
|
||||
"enabled": 0, "mode": 0, "g_in": 1.0, "g_out": 1.00,
|
||||
"ft_0": 5, "f_0": 70.0 , "g_0": 1.1, "q_0": 0.7, "s_0": 0,
|
||||
"ft_1": 1, "f_1": 110.0 , "g_1": 1.1, "q_1": 1.0,
|
||||
"ft_2": 1, "f_2": 315.0 , "g_2": 1.05, "q_2": 1.0,
|
||||
"ft_3": 1, "f_3": 1000.0 , "g_3": 1.01, "q_3": 1.0,
|
||||
"ft_4": 1, "f_4": 2500.0 , "g_4": 0.98, "q_4": 1.0,
|
||||
"ft_5": 1, "f_5": 6000.0 , "g_5": 0.97, "q_5": 1.0,
|
||||
"ft_6": 3, "f_6": 10000.0, "g_6": 0.96, "q_6": 0.7,
|
||||
"ft_7": 3, "f_7": 16000.0, "g_7": 0.95, "q_7": 0.7, "s_7": 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user