Compare commits
22 Commits
loudness-t
...
true_pink
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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__/
|
||||
|
||||
68
README.md
68
README.md
@@ -1,14 +1,11 @@
|
||||
# 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/)
|
||||
[]()
|
||||
[](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 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`).
|
||||
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 +13,19 @@ 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 |
|
||||
| 🎚️ **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
|
||||
@@ -80,40 +77,40 @@ flowchart TD
|
||||
## 🚀 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` |
|
||||
@@ -123,25 +120,30 @@ Customize tone settings without modifying calibrated internal DSP stages. `user_
|
||||
| **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` |
|
||||
|
||||
*(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: `-2` dB. Lower to `-3` / `-4` dB for stricter mechanical protection).
|
||||
* **Bass Drive:** Adjust `equalizer.control` (`g_1` through `g_5`).
|
||||
* **Sub-Bass Synthesis:** Adjust `virtualbass.control.amt` (Default: `1.0`).
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Index
|
||||
## 📚 Documentation & Repository Links
|
||||
|
||||
* 📖 **[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.
|
||||
### 🔗 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()
|
||||
90
graph.json
90
graph.json
@@ -8,7 +8,7 @@
|
||||
"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,
|
||||
"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": 1.0, "q_2": 1.0,
|
||||
@@ -27,23 +27,27 @@
|
||||
"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_out": 1.5,
|
||||
"__comment__":"(for f>100) g_N = sqrt(2)^(log(200/f_N)/log(2))*g_5",
|
||||
"__comment2__":"for f<100 g_N virtualbase takes",
|
||||
"__comment3__":"note the excessive bass gains require a solid multiband compressor",
|
||||
"__comment4__":" macbook speakers dont pass f_N<80, thus these frequences are 100% virtualbass",
|
||||
"ft_0": 2, "f_0": 20.0, "g_0": 3.0, "q_0": 1.41, "s_0": 3, "xm_0": 0, "fm_0": 0,
|
||||
"ft_1": 1, "f_1": 31.5, "g_1": 3.0, "q_1": 1.41, "s_1": 0, "xm_1": 0, "fm_1": 0,
|
||||
"ft_2": 1, "f_2": 50.0, "g_2": 3.5, "q_2": 1.41, "s_2": 0, "xm_2": 0, "fm_2": 0,
|
||||
"ft_3": 1, "f_3": 80.0, "g_3": 3.2, "q_3": 1.41, "s_3": 0, "xm_3": 0, "fm_3": 0,
|
||||
"ft_4": 1, "f_4": 125.0, "g_4": 2, "q_4": 1.41, "s_4": 0, "xm_4": 0, "fm_4": 0,
|
||||
"ft_5": 1, "f_5": 200.0, "g_5": 1.6, "q_5": 1.41, "s_5": 0, "xm_5": 0, "fm_5": 0,
|
||||
"ft_6": 1, "f_6": 315.0, "g_6": 1.275, "q_6": 1.41, "s_6": 0, "xm_6": 0, "fm_6": 0,
|
||||
"ft_7": 1, "f_7": 500.0, "g_7": 1.01, "q_7": 1.41, "s_7": 0, "xm_7": 0, "fm_7": 0,
|
||||
"ft_8": 1, "f_8": 800.0, "g_8": 0.8, "q_8": 1.41, "s_8": 0, "xm_8": 0, "fm_8": 0,
|
||||
"ft_9": 1, "f_9": 1250.0, "g_9": 0.64, "q_9": 1.41, "s_9": 0, "xm_9": 0, "fm_9": 0,
|
||||
"ft_10": 1, "f_10": 2000.0, "g_10": 0.5, "q_10": 1.41, "s_10": 0, "xm_10": 0, "fm_10": 0,
|
||||
"ft_11": 1, "f_11": 3150.0, "g_11": 0.4032, "q_11": 1.41, "s_11": 0, "xm_11": 0, "fm_11": 0,
|
||||
"ft_12": 1, "f_12": 5000.0, "g_12": 0.32, "q_12": 1.41, "s_12": 0, "xm_12": 0, "fm_12": 0,
|
||||
"ft_13": 1, "f_13": 8000.0, "g_13": 0.253, "q_13": 1.41, "s_13": 0, "xm_13": 0, "fm_13": 0,
|
||||
"ft_14": 1, "f_14": 12500.0, "g_14": 0.2, "q_14": 1.41, "s_14": 0, "xm_14": 0, "fm_14": 0,
|
||||
"ft_15": 1, "f_15": 20000.0, "g_15": 0.2, "q_15": 1.41, "s_15": 0, "xm_15": 0, "fm_15": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -71,20 +75,20 @@
|
||||
"g_out": 1.15,
|
||||
"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_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.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_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
|
||||
}
|
||||
},
|
||||
@@ -95,7 +99,7 @@
|
||||
"control": {
|
||||
"ingain": 0,
|
||||
"limit": -1,
|
||||
"release": 0.35
|
||||
"release": 0.15
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -139,7 +143,7 @@
|
||||
"/usr/share/t2-linux-audio/15_1/tweeters-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.1
|
||||
"gain": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -153,7 +157,7 @@
|
||||
"/usr/share/t2-linux-audio/15_1/tweeters-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.1
|
||||
"gain": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -167,7 +171,7 @@
|
||||
"/usr/share/t2-linux-audio/15_1/woofers-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.2
|
||||
"gain": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -181,7 +185,7 @@
|
||||
"/usr/share/t2-linux-audio/15_1/woofers-96k.wav"
|
||||
],
|
||||
"channel": 0,
|
||||
"gain": 1.2
|
||||
"gain": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -190,7 +194,7 @@
|
||||
"plugin": "http://plugin.org.uk/swh-plugins/fastLookaheadLimiter",
|
||||
"control": {
|
||||
"ingain": 0,
|
||||
"limit": -2,
|
||||
"limit": 0,
|
||||
"release": 0.25
|
||||
}
|
||||
},
|
||||
@@ -200,18 +204,18 @@
|
||||
"plugin": "http://plugin.org.uk/swh-plugins/fastLookaheadLimiter",
|
||||
"control": {
|
||||
"ingain": 0,
|
||||
"limit": -1,
|
||||
"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": "user_eq:out_l", "input": "virtualbass:in_l"},
|
||||
{"output": "user_eq:out_r", "input": "virtualbass:in_r"},
|
||||
{"output": "virtualbass:out_l", "input": "equalizer:in_l"},
|
||||
{"output": "virtualbass:out_r", "input": "equalizer:in_r"},
|
||||
{"output": "equalizer:out_l", "input": "multiband_compressor:in_l"},
|
||||
{"output": "equalizer: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"},
|
||||
|
||||
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,17 @@
|
||||
{
|
||||
"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
|
||||
"__comment__": "Eq curve for user tweaking.",
|
||||
"__comment2__": "please note that mathmatically equal power per hz is natural for a large stereo",
|
||||
"__comment3__": "laptop speakers benefit from not pushing their bass that hard.",
|
||||
"__comment4__": "this eq curve is a compromise between the two.",
|
||||
"__comment5__": "if you want to tweak it, please do so in small increments, and test with your own ears.",
|
||||
"__comment6__": "g_out = 2.4 will overdrive bass hard for lower frequencies where our multiband compressor becomes essential.",
|
||||
"enabled": 1, "mode": 0, "g_in": 1.0, "g_out": 2.40,
|
||||
"ft_0": 5, "f_0": 70.0 , "g_0": 0.8, "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": 315.0 , "g_2": 1.2, "q_2": 1.0,
|
||||
"ft_3": 1, "f_3": 1000.0 , "g_3": 1.4, "q_3": 1.0,
|
||||
"ft_4": 1, "f_4": 2500.0 , "g_4": 1.6, "q_4": 1.0,
|
||||
"ft_5": 1, "f_5": 6000.0 , "g_5": 1.8, "q_5": 1.0,
|
||||
"ft_6": 3, "f_6": 10000.0, "g_6": 2.0, "q_6": 0.7,
|
||||
"ft_7": 3, "f_7": 16000.0, "g_7": 2.5, "q_7": 0.7, "s_7": 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user