From 135b919eabd030d267ca734f9536a22693771626 Mon Sep 17 00:00:00 2001 From: mynameisdeleted Date: Tue, 1 Sep 2026 13:57:57 -0400 Subject: [PATCH] refactor: enhance FIR baking process and simplify graph generation with biquad extraction --- apply.sh | 31 +-- bake-graph.py | 146 ++++++++++---- graph_merged.json | 502 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 630 insertions(+), 49 deletions(-) create mode 100644 graph_merged.json diff --git a/apply.sh b/apply.sh index b060e77..08517e5 100755 --- a/apply.sh +++ b/apply.sh @@ -33,14 +33,6 @@ done have() { command -v "$1" >/dev/null 2>&1; } die() { echo "Error: $*" >&2; exit 1; } -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/" 2>/dev/null || true -fi - # --- 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; } @@ -53,7 +45,8 @@ json_ok "$GRAPH_SRC"; rc=$? [ "$rc" -eq 2 ] && echo "warn: no python3/jq - skipping JSON validation" [ "$rc" -eq 0 ] && echo "ok: graph.json is valid JSON" -# --- build the effective graph -> ~/.audiograph.json -------------------- +# --- build the effective graph ------------------------------------------ +MERGED_SRC="$SCRIPT_DIR/graph_merged.json" if [ -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" @@ -61,13 +54,25 @@ if [ -f "$OVERRIDE" ]; then || 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" + "$GRAPH_SRC" > "$MERGED_SRC" || die "jq merge failed" + echo "ok: merged user_eq.json -> $MERGED_SRC" else - cp "$GRAPH_SRC" "$MERGED" - echo "ok: no user_eq.json - graph as-is -> $MERGED" + cp "$GRAPH_SRC" "$MERGED_SRC" + echo "ok: no user_eq.json - graph as-is -> $MERGED_SRC" fi +if [ "$SIMPLE" -eq 1 ]; then + echo "==> Baking static DSP stages into single-stage FIR files..." + python3 "$SCRIPT_DIR/bake-graph.py" "$MERGED_SRC" || 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 -f "$SCRIPT_DIR/laptop-configs/apple/mbp15_1/baked-"*.wav "/usr/share/t2-linux-audio/15_1/" 2>/dev/null || true +else + GRAPH_SRC="$MERGED_SRC" +fi + +cp "$GRAPH_SRC" "$MERGED" + json_ok "$MERGED"; rc=$? [ "$rc" -eq 1 ] && die "merged graph $MERGED is not valid JSON" diff --git a/bake-graph.py b/bake-graph.py index c837b19..5822679 100755 --- a/bake-graph.py +++ b/bake-graph.py @@ -272,24 +272,67 @@ def apply_pink_noise_target_filter(samples, fs=48000, f_ref=1000.0): pink_samples = [ifft_vals[k].real / pad_len for k in range(N)] return pink_samples -def bake_driver_ir(src_wav, dst_wav, is_woofer=False, driver_gain=1.0): +def extract_biquads_from_node(node, fs): + biquads = [] + control = node.get("control", {}) + if control.get("enabled", 1) == 0: + return biquads + + for i in range(16): + f_key = f"f_{i}" + g_key = f"g_{i}" + q_key = f"q_{i}" + ft_key = f"ft_{i}" + if f_key not in control: + break + f = control[f_key] + g_lin = control.get(g_key, 1.0) + q = control.get(q_key, 0.7071) + ft = control.get(ft_key, 1) + + if g_lin <= 0.0001: + gain_db = -80.0 + else: + gain_db = 20.0 * math.log10(g_lin) + + if abs(gain_db) < 0.001 or ft == 0: + continue + + if ft == 1: + coeffs = biquad_peaking(fs, f, gain_db, q) + elif ft == 3: + coeffs = biquad_highshelf(fs, f, gain_db, q) + elif ft == 5: + coeffs = biquad_lowshelf(fs, f, gain_db, q) + else: + continue + + biquads.append(coeffs) + return biquads + +def bake_driver_ir(src_wav, dst_wav, is_woofer=False, driver_gain=1.0, biquads=[]): 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 + # 1. Convolve all static LTI biquad stages (User EQ + Voicing EQ) directly into driver IR + for coeffs in biquads: + b0, b1, b2, a0, a1, a2 = coeffs + samples = apply_biquad_to_samples(b0, b1, b2, a0, a1, a2, samples) + + # 2. Optimize Latency (5ms Lead) + Extend Woofer/Tweeter Lopsided Tail Resolution samples = optimize_fir_latency_and_tail(samples, fs=fs, is_woofer=is_woofer) - # 2. True-Peak Inter-Sample Peak (ISP) Guarding (-0.5 dBFS ceiling) + # 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)") + print(f"==> Baked {os.path.basename(dst_wav)} ({fs} Hz, {len(samples)} taps, {len(biquads)} biquads convolved)") return True -def generate_simple_graph_and_bake(profile_dir=None): +def generate_simple_graph_and_bake(profile_dir=None, input_graph_path=None): if not profile_dir: try: import detect_hardware @@ -300,9 +343,12 @@ def generate_simple_graph_and_bake(profile_dir=None): if not profile_dir or not os.path.exists(profile_dir): profile_dir = os.path.join(SCRIPT_DIR, "15_1") - graph_path = os.path.join(profile_dir, "graph.json") - if not os.path.exists(graph_path): - graph_path = os.path.join(SCRIPT_DIR, "graph.json") + if input_graph_path and os.path.exists(input_graph_path): + graph_path = input_graph_path + else: + graph_path = os.path.join(profile_dir, "graph.json") + if not os.path.exists(graph_path): + graph_path = os.path.join(SCRIPT_DIR, "graph.json") simple_graph_path = os.path.join(SCRIPT_DIR, "graph_simple.json") @@ -320,7 +366,7 @@ def generate_simple_graph_and_bake(profile_dir=None): 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} + convolver_tasks = {} # maps raw_basename -> task dict for node in nodes: if node.get("label") == "convolver" or "conv" in node.get("name", ""): @@ -331,69 +377,96 @@ def generate_simple_graph_and_bake(profile_dir=None): 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(): + raw_basename = os.path.basename(sys_path).replace("baked-", "") + if not raw_basename in convolver_tasks: + if "woofer" in raw_basename.lower(): is_woofer = True - baked_basename = "baked-" + basename + baked_basename = "baked-" + raw_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, + convolver_tasks[raw_basename] = { + "raw_basename": raw_basename, + "baked_basename": baked_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) + # 2. Discover all static biquad EQ nodes (user_eq and equalizer) to convolve into FIR targets + biquad_nodes = [node for node in nodes if node.get("name") in ["user_eq", "equalizer"]] + + # 3. Bake FIR files dynamically for all discovered WAV targets from PURE raw measurement source files + for raw_basename, task in convolver_tasks.items(): + src_path = os.path.join(repo_151, raw_basename) + if not os.path.exists(src_path): + src_path = os.path.join(SCRIPT_DIR, "15_1", raw_basename) + if not os.path.exists(src_path): + src_path = os.path.join(SCRIPT_DIR, raw_basename) + if not os.path.exists(src_path): + src_path = os.path.join(sys_dir, raw_basename) + + if not os.path.exists(src_path): + print(f"Warning: Raw measurement source {raw_basename} not found in {repo_151} or {sys_dir}.") + continue + + # Read src_path to determine sample rate fs for exact biquad coefficient generation + _, fs = read_wav_floats(src_path) + biquads = [] + for bnode in biquad_nodes: + biquads.extend(extract_biquads_from_node(bnode, fs)) bake_driver_ir( src_wav=src_path, dst_wav=task["repo_dst"], is_woofer=task["is_woofer"], - driver_gain=task["gain"] + driver_gain=task["gain"], + biquads=biquads ) - # 3. Build graph_simple.json dynamically from graph.json + # 3. Build graph_simple.json dynamically from graph.json (omitting user_eq, equalizer, and whp* nodes) graph["node.description"] = "MacBook Pro 15,1 DSP Speakers (Baked FIR Crossovers & Latency Trimming)" new_nodes = [] for node in nodes: name = node.get("name", "") - if name in ["whpL1", "whpL2", "whpR1", "whpR2"]: + # Omit user_eq, equalizer (baked into FIRs), and whp* crossover nodes + if name in ["user_eq", "equalizer", "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 - ] + baked_fns = [] + for p in orig_filenames: + rb = os.path.basename(p).replace("baked-", "") + if rb in convolver_tasks: + baked_fns.append(convolver_tasks[rb]["sys_dst"]) + else: + baked_fns.append(os.path.join(sys_dir, "baked-" + rb)) + node["config"]["filename"] = baked_fns new_nodes.append(node) - # Re-wire links: filter out removed whp* crossover nodes + # Re-wire links: filter out removed user_eq, equalizer & whp* crossover links 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: + if ("user_eq" in out_node or "user_eq" in in_node or + "equalizer" in out_node or "equalizer" in in_node or + "whp" in out_node or "whp" in in_node): continue new_links.append(link) - # Set graph inputs directly to user_eq (first node in processing chain) + # Wire virtualbass -> loudness (stereo) + new_links.append({"output": "virtualbass:out_l", "input": "loudness:in_l"}) + new_links.append({"output": "virtualbass:out_r", "input": "loudness:in_r"}) + + # Set graph inputs directly to virtualbass (first active node in simplified processing chain) graph["filter.graph"]["inputs"] = [ - "user_eq:in_l", - "user_eq:in_r" + "virtualbass:in_l", + "virtualbass:in_r" ] # Remove capture.volumes from filter.graph if present @@ -424,7 +497,8 @@ def main(): print("=================================================================") print(" SINGLE-STAGE FIR CONVOLVER BAKER & GRAPH SIMPLIFIER") print("=================================================================") - generate_simple_graph_and_bake() + input_graph = sys.argv[1] if len(sys.argv) > 1 else None + generate_simple_graph_and_bake(input_graph_path=input_graph) print("=================================================================") print("Done! Baked FIR files & graph_simple.json created.") diff --git a/graph_merged.json b/graph_merged.json new file mode 100644 index 0000000..e3162e2 --- /dev/null +++ b/graph_merged.json @@ -0,0 +1,502 @@ +{ + "node.description": "MacBook Pro 15,1 Audio DSP", + "media.name": "MacBook Pro 15,1 Audio DSP", + "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.50, + "ft_0": 5, + "f_0": 70.0, + "g_0": 1.00, + "q_0": 0.7, + "s_0": 0, + "ft_1": 1, + "f_1": 110.0, + "g_1": 1.05, + "q_1": 1.0, + "ft_2": 1, + "f_2": 315.0, + "g_2": 1.03, + "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": 1.00, + "q_4": 1.0, + "ft_5": 1, + "f_5": 6000.0, + "g_5": 0.98, + "q_5": 1.0, + "ft_6": 3, + "f_6": 10000.0, + "g_6": 0.95, + "q_6": 0.7, + "ft_7": 3, + "f_7": 16000.0, + "g_7": 0.98, + "q_7": 0.7, + "s_7": 0 + } + }, + { + "type": "lv2", + "plugin": "https://chadmed.au/bankstown", + "name": "virtualbass", + "control": { + "amt": 1.4, + "floor": 40.0, + "ceil": 120.0, + "final_hp": 55.0, + "sat_second": 1.0, + "sat_third": 1.0, + "blend": 0.30 + } + }, + { + "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.0, + "ft_0": 5, + "f_0": 40.0, + "g_0": 2.83, + "q_0": 0.70, + "ft_1": 1, + "f_1": 80.0, + "g_1": 2.00, + "q_1": 1.00, + "ft_2": 1, + "f_2": 160.0, + "g_2": 1.41, + "q_2": 1.00, + "ft_3": 1, + "f_3": 320.0, + "g_3": 1.00, + "q_3": 1.00, + "ft_4": 1, + "f_4": 640.0, + "g_4": 0.71, + "q_4": 1.00, + "ft_5": 1, + "f_5": 1280.0, + "g_5": 0.50, + "q_5": 1.00, + "ft_6": 1, + "f_6": 2560.0, + "g_6": 0.35, + "q_6": 1.00, + "ft_7": 3, + "f_7": 12000.0, + "g_7": 0.24, + "q_7": 0.70 + } + }, + { + "type": "lv2", + "plugin": "http://lsp-plug.in/plugins/lv2/loud_comp_stereo", + "name": "loudness", + "control": { + "enabled": 1, + "input": 1.0, + "fft": 4 + } + }, + { + "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", + "plugin": "http://lsp-plug.in/plugins/lv2/mb_compressor_stereo", + "name": "wmb_comp", + "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, + "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.95, + "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.92, + "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.90, + "mk_2": 1.0, + "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.88, + "mk_3": 1.0, + "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.85, + "mk_4": 1.0, + "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.80, + "mk_5": 1.0, + "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.80, + "mk_6": 1.0, + "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.80, + "mk_7": 1.0, + "bth_7": 1.0, + "bsa_7": 1.0, + "scm_7": 0 + } + }, + { + "type": "lv2", + "plugin": "http://lsp-plug.in/plugins/lv2/mb_compressor_stereo", + "name": "tmb_comp", + "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": 2500.0, + "cbe_2": 1, + "sf_2": 6000.0, + "cbe_3": 1, + "sf_3": 12000.0, + "ce_0": 1, + "at_0": 3.0, + "rt_0": 50.0, + "cr_0": 8.0, + "kn_0": 0.20, + "al_0": 0.85, + "mk_0": 1.0, + "bth_0": 1.0, + "bsa_0": 1.0, + "scm_0": 0, + "ce_1": 1, + "at_1": 2.5, + "rt_1": 40.0, + "cr_1": 6.0, + "kn_1": 0.30, + "al_1": 0.80, + "mk_1": 1.0, + "bth_1": 1.0, + "bsa_1": 1.0, + "scm_1": 0, + "ce_2": 1, + "at_2": 2.0, + "rt_2": 30.0, + "cr_2": 4.0, + "kn_2": 0.40, + "al_2": 0.75, + "mk_2": 1.0, + "bth_2": 1.0, + "bsa_2": 1.0, + "scm_2": 0, + "ce_3": 1, + "at_3": 1.5, + "rt_3": 20.0, + "cr_3": 3.0, + "kn_3": 0.50, + "al_3": 0.70, + "mk_3": 1.0, + "bth_3": 1.0, + "bsa_3": 1.0, + "scm_3": 0 + } + }, + { + "type": "lv2", + "name": "wlim", + "plugin": "http://plugin.org.uk/swh-plugins/fastLookaheadLimiter", + "control": { + "ingain": 0, + "limit": -2, + "release": 0.25 + } + }, + { + "type": "lv2", + "name": "tlim", + "plugin": "http://plugin.org.uk/swh-plugins/fastLookaheadLimiter", + "control": { + "ingain": 0, + "limit": -1, + "release": 0.15 + } + } + ], + "links": [ + { + "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": "loudness:in_l" + }, + { + "output": "equalizer:out_r", + "input": "loudness:in_r" + }, + { + "output": "loudness:out_l", + "input": "copyL:In" + }, + { + "output": "loudness:out_r", + "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": "wmb_comp:in_l" + }, + { + "output": "convRW:Out", + "input": "wmb_comp:in_r" + }, + { + "output": "wmb_comp:out_l", + "input": "wlim:in_1" + }, + { + "output": "wmb_comp:out_r", + "input": "wlim:in_2" + }, + { + "output": "convLT:Out", + "input": "tmb_comp:in_l" + }, + { + "output": "convRT:Out", + "input": "tmb_comp:in_r" + }, + { + "output": "tmb_comp:out_l", + "input": "tlim:in_1" + }, + { + "output": "tmb_comp: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.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" + ] + } +}