From 4a3a2b81781d9c6024632c527f887adb2e03a023 Mon Sep 17 00:00:00 2001 From: "Leone, Mark A [LGS]" Date: Fri, 30 Jan 2026 23:01:58 -0500 Subject: [PATCH 1/6] Initial commit of equalizer files --- equalizer_files/deps | 5 + equalizer_files/eq-input.sh | 203 ++++++++++++++++++ equalizer_files/inputmodule-control | 1 + .../old_scripts/eq-test-bt-working.sh | 190 ++++++++++++++++ equalizer_files/old_scripts/eq-test.sh | 168 +++++++++++++++ .../old_scripts/led-eq-speakers-fixed.sh | 172 +++++++++++++++ .../old_scripts/led-eq-speakers.sh | 176 +++++++++++++++ equalizer_files/visualizer.toml | 38 ++++ 8 files changed, 953 insertions(+) create mode 100644 equalizer_files/deps create mode 100755 equalizer_files/eq-input.sh create mode 120000 equalizer_files/inputmodule-control create mode 100755 equalizer_files/old_scripts/eq-test-bt-working.sh create mode 100755 equalizer_files/old_scripts/eq-test.sh create mode 100755 equalizer_files/old_scripts/led-eq-speakers-fixed.sh create mode 100755 equalizer_files/old_scripts/led-eq-speakers.sh create mode 100644 equalizer_files/visualizer.toml diff --git a/equalizer_files/deps b/equalizer_files/deps new file mode 100644 index 0000000..2785b2c --- /dev/null +++ b/equalizer_files/deps @@ -0,0 +1,5 @@ +Ubuntu/Debian +sudo apt install pipewire pipewire-pulse wireplumber pulseaudio-utils + +Fedora +sudo dnf install pipewire pipewire-pulseaudio wireplumber diff --git a/equalizer_files/eq-input.sh b/equalizer_files/eq-input.sh new file mode 100755 index 0000000..fad0aa1 --- /dev/null +++ b/equalizer_files/eq-input.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ---------------- Config ---------------- +BASE_CMD="inputmodule-control led-matrix" +EQ_CMD="$BASE_CMD --input-eq" +FALLBACK_CMD="$BASE_CMD --random-eq" +CHECK_INTERVAL=1 +EQ_PID="" +CLEANUP_FLAG=0 + +# ---------------- Helpers ---------------- + +log() { echo "[$(date +'%H:%M:%S')] $*"; } + +pactl_ok() { + pactl info >/dev/null 2>&1 +} + +wait_for_pactl() { + for _ in {1..20}; do + pactl_ok && return 0 + sleep 0.25 + done + return 1 +} + +current_sink() { + pactl_ok || return 1 + pactl info | sed -n 's/^Default Sink: //p' +} + +monitor_name_for_sink() { + local sink="$1" + echo "${sink}.monitor" +} + +wait_for_monitor_running() { + local monitor="$1" + for _ in {1..50}; do + # Check if the monitor exists in pactl sources + if pactl list short sources | awk '{print $2}' | grep -qx "$monitor"; then + return 0 + fi + sleep 0.1 + done + return 1 +} + +set_default_source() { + local monitor="$1" + pactl set-default-source "$monitor" || true +} + +sink_type() { + case "$1" in + *headphone*|*Audio_Expansion*) echo "headphones" ;; + *analog-stereo*) echo "speakers" ;; + *bluez*) echo "bluetooth" ;; + *) echo "unknown" ;; + esac +} + +fade_out() { + # Small pause to simulate fade + sleep 0.2 +} + +fade_in() { + sleep 0.2 +} + +notify_sink() { + local type="$1" + log "Notifying sink change: $type" + fade_out + sleep 0.1 + fade_in +} + +start_eq() { + stop_eq + if pactl_ok; then + $EQ_CMD & + else + $FALLBACK_CMD & + fi + EQ_PID=$! +} + +stop_eq() { + # Kill the process we tracked + [[ -n "${EQ_PID:-}" ]] && kill "$EQ_PID" >/dev/null 2>&1 || true + + # Also make sure no stray processes remain + pkill -f "${EQ_CMD}" >/dev/null 2>&1 || true + + # Reset EQ_PID + EQ_PID="" +} + + +start_fallback() { + stop_eq + $FALLBACK_CMD >/dev/null 2>&1 & + EQ_PID=$! +} + +visual_cue() { + local type="$1" + case "$type" in + speakers) + # Show horizontal bar for speakers + ${BASE_CMD} --pattern all-on >/dev/null 2>&1 + ;; + headphones) + # Show vertical bars for headphones + ${BASE_CMD} --pattern zigzag >/dev/null 2>&1 + ;; + bluetooth) + # Unknown source → flash everything briefly + ${BASE_CMD} --pattern gradient >/dev/null 2>&1 + ;; + *) + # Unknown source → flash everything briefly + ${BASE_CMD} --pattern gradient >/dev/null 2>&1 + ;; + esac + + # Keep cue visible briefly + sleep 0.6 +} + + +# ---------------- Cleanup ---------------- + +cleanup() { + if [ $CLEANUP_FLAG -eq 1 ]; then return; fi + CLEANUP_FLAG=1 + log "Restoring original input source: $ORIG_SOURCE" + stop_eq + if pactl_ok; then + pactl set-default-source "$ORIG_SOURCE" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT INT TERM + +# ---------------- Startup ---------------- + +ORIG_SOURCE=$(pactl info | sed -n 's/^Default Source: //p') +[[ -z "$ORIG_SOURCE" ]] && log "No original source found" && exit 1 + +log "Original input source: $ORIG_SOURCE" + +CURRENT_SINK="$(current_sink)" +MONITOR="$(monitor_name_for_sink "$CURRENT_SINK")" + +set_default_source "$MONITOR" + +if wait_for_monitor_running "$MONITOR"; then + log "→ Monitor ready: $MONITOR" + start_eq +else + log "⚠ Monitor not ready, using fallback EQ" + start_fallback +fi + +# ---------------- Main Loop ---------------- + +pactl subscribe | while read -r line; do + case "$line" in + *"on server"*|*"on sink"*) + NEW_SINK="$(current_sink)" + if [[ "$NEW_SINK" != "$CURRENT_SINK" && -n "$NEW_SINK" ]]; then + log "🔄 Sink change detected → $NEW_SINK" + + stop_eq + + SINK_TYPE=$(sink_type "$NEW_SINK") + fade_out + visual_cue "$SINK_TYPE" + fade_in + + MONITOR="$(monitor_name_for_sink "$NEW_SINK")" + log "→ Input now follows: $MONITOR" + set_default_source "$MONITOR" + + if wait_for_monitor_running "$MONITOR"; then + start_eq + else + log "⚠ Monitor never reached RUNNING/IDLE, using fallback" + start_fallback + fi + + CURRENT_SINK="$NEW_SINK" + fi + ;; + esac +done & +SUB_PID=$! + +# Wait for subscription loop +wait "$SUB_PID" diff --git a/equalizer_files/inputmodule-control b/equalizer_files/inputmodule-control new file mode 120000 index 0000000..2b56914 --- /dev/null +++ b/equalizer_files/inputmodule-control @@ -0,0 +1 @@ +/home/mal/git/inputmodule-rs/target/x86_64-unknown-linux-gnu/debug/inputmodule-control \ No newline at end of file diff --git a/equalizer_files/old_scripts/eq-test-bt-working.sh b/equalizer_files/old_scripts/eq-test-bt-working.sh new file mode 100755 index 0000000..ff3affb --- /dev/null +++ b/equalizer_files/old_scripts/eq-test-bt-working.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ---------------- Config ---------------- +EQ_CMD="inputmodule-control led-matrix --input-eq" +FALLBACK_CMD="inputmodule-control led-matrix --random-eq" +CHECK_INTERVAL=1 +EQ_PID="" + +# ---------------- Helpers ---------------- + +log() { echo "[$(date +'%H:%M:%S')] $*"; } + +pactl_ok() { + pactl info >/dev/null 2>&1 +} + +wait_for_pactl() { + for _ in {1..20}; do + pactl_ok && return 0 + sleep 0.25 + done + return 1 +} + +current_sink() { + pactl_ok || return 1 + pactl info | sed -n 's/^Default Sink: //p' +} + +monitor_name_for_sink() { + local sink="$1" + echo "${sink}.monitor" +} + +wait_for_monitor_running() { + local monitor="$1" + for _ in {1..50}; do + # Check if the monitor exists in pactl sources + if pactl list short sources | awk '{print $2}' | grep -qx "$monitor"; then + return 0 + fi + sleep 0.1 + done + return 1 +} + +set_default_source() { + local monitor="$1" + pactl set-default-source "$monitor" || true +} + +sink_type() { + case "$1" in + *headphone*|*headset*) echo "headphones" ;; + *analog-stereo*) echo "speakers" ;; + *) echo "unknown" ;; + esac +} + +fade_out() { + # Small pause to simulate fade + sleep 0.2 +} + +fade_in() { + sleep 0.2 +} + +notify_sink() { + local type="$1" + log "Notifying sink change: $type" + fade_out + sleep 0.1 + fade_in +} + +start_eq() { + stop_eq + if pactl_ok; then + $EQ_CMD & + else + $FALLBACK_CMD & + fi + EQ_PID=$! +} + +stop_eq() { + [[ -n "$EQ_PID" ]] && kill "$EQ_PID" >/dev/null 2>&1 || true + [[ -n "$EQ_PID" ]] && wait "$EQ_PID" 2>/dev/null || true + EQ_PID="" +} + +start_fallback() { + stop_eq + $FALLBACK_CMD >/dev/null 2>&1 & + EQ_PID=$! +} + +visual_cue() { + local type="$1" + + case "$type" in + speakers) + # Show horizontal bar for speakers + inputmodule-control led-matrix --pattern all-on >/dev/null 2>&1 + ;; + headphones) + # Show vertical bars for headphones + inputmodule-control led-matrix --pattern zigzag >/dev/null 2>&1 + ;; + *) + # Unknown source → flash everything briefly + inputmodule-control led-matrix --pattern gradient >/dev/null 2>&1 + ;; + esac + + # Keep cue visible briefly + sleep 2 +} + + +# ---------------- Cleanup ---------------- + +cleanup() { + log "Restoring original input source: $ORIG_SOURCE" + stop_eq + if pactl_ok; then + pactl set-default-source "$ORIG_SOURCE" >/dev/null 2>&1 || true + fi + [[ -n "${EQ_PID:-}" ]] && kill "$EQ_PID" >/dev/null 2>&1 || true +} +trap cleanup EXIT INT TERM + +# ---------------- Startup ---------------- + +ORIG_SOURCE=$(pactl info | sed -n 's/^Default Source: //p') +[[ -z "$ORIG_SOURCE" ]] && log "No original source found" && exit 1 + +log "Original input source: $ORIG_SOURCE" + +CURRENT_SINK="$(current_sink)" +MONITOR="$(monitor_name_for_sink "$CURRENT_SINK")" + +set_default_source "$MONITOR" + +if wait_for_monitor_running "$MONITOR"; then + log "→ Monitor ready: $MONITOR" + start_eq +else + log "⚠ Monitor not ready, using fallback EQ" + start_fallback +fi + +# ---------------- Main Loop ---------------- + +pactl subscribe | while read -r line; do + case "$line" in + *"on server"*|*"on sink"*) + NEW_SINK="$(current_sink)" + if [[ "$NEW_SINK" != "$CURRENT_SINK" && -n "$NEW_SINK" ]]; then + log "🔄 Sink change detected → $NEW_SINK" + + stop_eq + + SINK_TYPE=$(sink_type "$NEW_SINK") + fade_out + visual_cue "$SINK_TYPE" + fade_in + + MONITOR="$(monitor_name_for_sink "$NEW_SINK")" + log "→ Input now follows: $MONITOR" + set_default_source "$MONITOR" + + if wait_for_monitor_running "$MONITOR"; then + start_eq + else + log "⚠ Monitor never reached RUNNING/IDLE, using fallback" + start_fallback + fi + + CURRENT_SINK="$NEW_SINK" + fi + ;; + esac +done & +EQ_PID=$! + +# Wait for subscription loop +wait "$EQ_PID" diff --git a/equalizer_files/old_scripts/eq-test.sh b/equalizer_files/old_scripts/eq-test.sh new file mode 100755 index 0000000..b982dab --- /dev/null +++ b/equalizer_files/old_scripts/eq-test.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +set -euo pipefail + +EQ_CMD="inputmodule-control led-matrix --input-eq" +CHECK_INTERVAL=0.5 # seconds +MONITOR_TIMEOUT=5 # seconds to wait for monitor readiness + +# ---------------- HELPERS ---------------- +log() { echo "[$(date '+%H:%M:%S')] $*"; } + +pactl_ok() { pactl info >/dev/null 2>&1; } + +get_default_sink() { pactl_ok || return 1; pactl info | sed -n 's/^Default Sink: //p'; } +monitor_name_for_sink() { echo "${1}.monitor"; } + +set_default_source() { + local source="$1" + pactl set-default-source "$source" >/dev/null 2>&1 +} + + +sink_type() { + case "$1" in + *headphone*|*headset*) echo "headphones" ;; + *analog-stereo*) echo "speakers" ;; + *) echo "unknown" ;; + esac +} + +wait_for_monitor() { + local monitor="$1" + for _ in {1..40}; do + pactl list short sources | awk '{print $2}' | grep -qx "$monitor" && return 0 + sleep 0.25 + done + return 1 +} + +monitor_state() { + pactl list short sources | awk -v src="$1" '$2 == src {print $7}' +} + +wait_for_monitor_running() { + local monitor="$1" + local max_wait=30 # increase from 2-3s to 30s + local waited=0 + + while ! pactl list short sources | awk '{print $2}' | grep -qx "$monitor"; do + sleep 0.5 + ((waited+=1)) + if (( waited*5 >= max_wait )); then + return 1 + fi + done + + # Optional: extra wait until the monitor actually reports RUNNING + # Could use `pw-top` or PipeWire API for more precision + sleep 10 + return 0 +} + + +fade_out() { + for _ in {1..3}; do + inputmodule-control led-matrix --pattern all-on >/dev/null 2>&1 + sleep 0.05 + inputmodule-control led-matrix --pattern percentage >/dev/null 2>&1 # or your default EQ pattern + + sleep 0.05 + done +} +fade_in() { sleep 0.05; } + +notify_sink() { + case "$1" in + headphones) inputmodule-control led-matrix --pattern percentage >/dev/null 2>&1 ;; + speakers) inputmodule-control led-matrix --pattern double-gradient >/dev/null 2>&1 ;; + *) inputmodule-control led-matrix --pattern all-on >/dev/null 2>&1 + sleep 0.3 + inputmodule-control led-matrix --random-eq >/dev/null 2>&1 ;; +esac +sleep 0.4 +} + +start_eq() { + if pactl_ok; then + $EQ_CMD & + EQ_PID=$! + else + inputmodule-control led-matrix --random-eq >/dev/null 2>&1 & + EQ_PID=$! + fi +} + +# stop_eq() { +# [[ -n "${EQ_PID:-}" ]] && kill "$EQ_PID" >/dev/null 2>&1 || true +# wait "${EQ_PID:-}" 2>/dev/null || true +# EQ_PID="" +# } + +stop_eq() { + if [[ -n "$EQ_PID" ]]; then + kill "$EQ_PID" >/dev/null 2>&1 || true + wait "$EQ_PID" 2>/dev/null || true + EQ_PID="" + fi +} + +start_fallback() { + inputmodule-control led-matrix --random-eq >/dev/null 2>&1 & + EQ_PID=$! +} + +# ---------------- STARTUP ---------------- +ORIG_SOURCE=$(pactl info | sed -n 's/^Default Source: //p') +[[ -z "$ORIG_SOURCE" ]] && exit 1 +log "Original input source: $ORIG_SOURCE" + +EQ_PID="" +CURRENT_SINK="$(get_default_sink)" +MONITOR="$(monitor_name_for_sink "$CURRENT_SINK")" + +if ! wait_for_monitor_running "$MONITOR"; then + echo "⚠ Monitor not ready, retrying in 2s..." + sleep 2 + wait_for_monitor_running "$MONITOR" || start_fallback +fi +set_default_source "$MONITOR" +start_eq + +# ---------------- CLEANUP ---------------- +cleanup() { + log "Restoring original source: $ORIG_SOURCE" + stop_eq + pactl_ok && pactl set-default-source "$ORIG_SOURCE" >/dev/null 2>&1 || true +} +trap cleanup EXIT INT TERM + +# ---------------- MAIN LOOP ---------------- +while true; do + NEW_SINK="$(get_default_sink)" + if [[ "$NEW_SINK" != "$CURRENT_SINK" && -n "$NEW_SINK" ]]; then + log "🔄 Sink change detected → $NEW_SINK" + + SINK_TYPE="$(sink_type "$NEW_SINK")" + MONITOR="$(monitor_name_for_sink "$NEW_SINK")" + log "→ Waiting for monitor: $MONITOR" + + if wait_for_monitor_running "$MONITOR"; then + log "✅ Monitor ready" + set_default_source "$MONITOR" + + fade_out + notify_sink "$SINK_TYPE" + + stop_eq + start_eq + fade_in + else + log "⚠ Monitor never reached RUNNING/IDLE — fallback EQ" + stop_eq + start_fallback + fi + + CURRENT_SINK="$NEW_SINK" + fi + sleep "$CHECK_INTERVAL" +done diff --git a/equalizer_files/old_scripts/led-eq-speakers-fixed.sh b/equalizer_files/old_scripts/led-eq-speakers-fixed.sh new file mode 100755 index 0000000..ffbf574 --- /dev/null +++ b/equalizer_files/old_scripts/led-eq-speakers-fixed.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +set -euo pipefail + +############################ +# CONFIG +############################ + +EQ_CMD="inputmodule-control led-matrix --input-eq" +FALLBACK_CMD="inputmodule-control led-matrix --random-eq" + +CHECK_INTERVAL=1 # seconds between sink polls +MONITOR_TIMEOUT=5 # seconds to wait for monitor readiness + +############################ +# STATE +############################ + +ORIGINAL_SOURCE="" +EQ_PID="" +CURRENT_SINK="" + +############################ +# UTILS +############################ + +log() { + echo "[$(date +%H:%M:%S)] $*" +} + +fade_out() { + for _ in {1..3}; do + inputmodule-control led-matrix --test off >/dev/null 2>&1 + sleep 0.05 + done +} + +fade_in() { + inputmodule-control led-matrix --test off >/dev/null 2>&1 + sleep 0.05 +} + +notify_sink() { + local sink_type="$1" + + case "$sink_type" in + headphones) + inputmodule-control led-matrix --test vertical >/dev/null 2>&1 + ;; + speakers) + inputmodule-control led-matrix --test horizontal >/dev/null 2>&1 + ;; + *) + inputmodule-control led-matrix --test blink >/dev/null 2>&1 + ;; + esac + + sleep 0.4 +} + +get_default_sink() { + pactl info | awk -F': ' '/Default Sink/ {print $2}' +} + +get_default_source() { + pactl info | awk -F': ' '/Default Source/ {print $2}' +} + +set_default_source() { + pactl set-default-source "$1" +} + +monitor_name_for_sink() { + echo "${1}.monitor" +} + +monitor_state() { + pactl list short sources | awk -v src="$1" '$2 == src {print $7}' +} + +wait_for_monitor_running() { + local monitor="$1" + local elapsed=0 + + while (( elapsed < MONITOR_TIMEOUT )); do + state="$(monitor_state "$monitor" || true)" + + case "$state" in + RUNNING|IDLE) + return 0 + ;; + esac + + sleep 1 + ((elapsed++)) + done + + return 1 +} + +stop_eq() { + if [[ -n "${EQ_PID:-}" ]] && kill -0 "$EQ_PID" 2>/dev/null; then + log "Stopping EQ (pid $EQ_PID)" + kill "$EQ_PID" + wait "$EQ_PID" 2>/dev/null || true + fi + EQ_PID="" +} + +start_eq() { + log "Starting EQ" + $EQ_CMD >/dev/null 2>&1 & + EQ_PID=$! +} + +start_fallback() { + log "Starting fallback EQ" + $FALLBACK_CMD >/dev/null 2>&1 & + EQ_PID=$! +} + +cleanup() { + log "Cleaning up" + stop_eq + [[ -n "$ORIGINAL_SOURCE" ]] && set_default_source "$ORIGINAL_SOURCE" + exit 0 +} + +trap cleanup INT TERM + +############################ +# INIT +############################ + +if ! pactl info >/dev/null 2>&1; then + log "pactl unavailable, starting fallback EQ" + start_fallback + wait +fi + +ORIGINAL_SOURCE="$(get_default_source)" +log "Original input source: $ORIGINAL_SOURCE" + +############################ +# MAIN LOOP +############################ + +while true; do + NEW_SINK="$(get_default_sink)" + + if [[ "$NEW_SINK" != "$CURRENT_SINK" ]]; then + log "🔄 Sink change detected → $NEW_SINK" + + stop_eq + + MONITOR="$(monitor_name_for_sink "$NEW_SINK")" + log "→ Input now follows: $MONITOR" + + set_default_source "$MONITOR" + + if wait_for_monitor_running "$MONITOR"; then + log "✅ Monitor ready" + start_eq + else + log "⚠ Monitor never reached RUNNING/IDLE" + start_fallback + fi + + CURRENT_SINK="$NEW_SINK" + fi + + sleep "$CHECK_INTERVAL" +done diff --git a/equalizer_files/old_scripts/led-eq-speakers.sh b/equalizer_files/old_scripts/led-eq-speakers.sh new file mode 100755 index 0000000..0ec7f58 --- /dev/null +++ b/equalizer_files/old_scripts/led-eq-speakers.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ---------------- CONFIG ------------------ +EQ_CMD="inputmodule-control led-matrix --input-eq" +CHECK_INTERVAL=1 # seconds between sink checks + +# ---------------- HELPERS ----------------- + +log() { + echo "$(date +'%H:%M:%S') $*" +} + +# Check if pactl is responding +pactl_ok() { + pactl info >/dev/null 2>&1 +} + +wait_for_pactl() { + for _ in {1..20}; do + pactl_ok && return 0 + sleep 0.25 + done + return 1 +} + +# Get current default sink +get_default_sink() { + pactl_ok || return 1 + pactl info | sed -n 's/^Default Sink: //p' +} + +# Map sink to its monitor source +monitor_name_for_sink() { + local sink="$1" + echo "${sink}.monitor" +} + +# Wait for monitor source to appear +wait_for_monitor_running() { + local monitor="$1" + for _ in {1..20}; do + pactl list short sources | awk '{print $2}' | grep -qx "$monitor" && return 0 + sleep 0.1 + done + return 1 +} + +# Set default source +set_default_source() { + local src="$1" + pactl_ok || return 1 + pactl set-default-source "$src" +} + +# Determine sink type for notifications +sink_type() { + case "$1" in + *headphone*|*headset*) echo "headphones" ;; + *analog-stereo*) echo "speakers" ;; + *) echo "unknown" ;; + esac +} + +# Visual indication of sink change +notify_sink() { + local type="$1" + case "$type" in + headphones) + inputmodule-control led-matrix --test vertical >/dev/null 2>&1 + ;; + speakers) + inputmodule-control led-matrix --test horizontal >/dev/null 2>&1 + ;; + *) + inputmodule-control led-matrix --test blink >/dev/null 2>&1 + ;; + esac + sleep 0.4 +} + +# Fading out LEDs +fade_out() { + for _ in {1..3}; do + inputmodule-control led-matrix --test off >/dev/null 2>&1 + sleep 0.05 + done +} + +# Short pause before EQ resumes +fade_in() { + inputmodule-control led-matrix --test off >/dev/null 2>&1 + sleep 0.05 +} + +# Start EQ or fallback +start_eq() { + if pactl_ok; then + $EQ_CMD & + EQ_PID=$! + else + start_fallback + fi +} + +start_fallback() { + inputmodule-control led-matrix --random-eq >/dev/null 2>&1 & + EQ_PID=$! +} + +# Stop EQ process +stop_eq() { + [[ -n "${EQ_PID:-}" ]] && kill "$EQ_PID" >/dev/null 2>&1 || true + wait "${EQ_PID:-}" 2>/dev/null || true +} + +# Cleanup on exit +cleanup() { + log "Restoring original input source: $ORIG_SOURCE" + stop_eq + + if pactl_ok; then + pactl set-default-source "$ORIG_SOURCE" >/dev/null 2>&1 || true + fi + + [[ -n "${SUB_PID:-}" ]] && kill "$SUB_PID" >/dev/null 2>&1 || true +} +trap cleanup EXIT INT TERM + +# ---------------- STARTUP ----------------- +ORIG_SOURCE=$(pactl info | sed -n 's/^Default Source: //p') +[[ -z "$ORIG_SOURCE" ]] && { log "No default source found"; exit 1; } +log "Original input source: $ORIG_SOURCE" + +CURRENT_SINK="$(get_default_sink)" +MONITOR="$(monitor_name_for_sink "$CURRENT_SINK")" + +set_default_source "$MONITOR" +start_eq + +# ---------------- MAIN LOOP ---------------- +pactl subscribe | while read -r line; do + case "$line" in + *"on server"*|*"on sink"*) + NEW_SINK="$(get_default_sink)" + if [[ "$NEW_SINK" != "$CURRENT_SINK" && -n "$NEW_SINK" ]]; then + log "🔄 Sink change detected → $NEW_SINK" + + stop_eq + + SINK_TYPE="$(sink_type "$NEW_SINK")" + + fade_out + notify_sink "$SINK_TYPE" + fade_in + + MONITOR="$(monitor_name_for_sink "$NEW_SINK")" + log "→ Input now follows: $MONITOR" + set_default_source "$MONITOR" + + # ✅ Avoid exiting due to set -e if monitor is slow + if ! wait_for_monitor_running "$MONITOR"; then + log "⚠ Monitor never reached RUNNING/IDLE, using fallback" + start_fallback + else + start_eq + fi + + CURRENT_SINK="$NEW_SINK" + fi + ;; + esac +done & +SUB_PID=$! + +wait "$SUB_PID" diff --git a/equalizer_files/visualizer.toml b/equalizer_files/visualizer.toml new file mode 100644 index 0000000..a04455e --- /dev/null +++ b/equalizer_files/visualizer.toml @@ -0,0 +1,38 @@ +[audio] +rate = 16000 +fourier.downsample = 5 +recorder = "cpal" +buffer = 16000 +read_size = 256 + +# The following settings are curerntly ignored because not implemented in inputmodule-control +[fft] +length = 1024 +window = "hann" + +[equalizer] +columns = 9 +rows = 34 + +# Logarithmic frequency band edges (Hz) +bands = [ + [30, 60], + [60, 120], + [120, 250], + [250, 500], + [500, 1000], + [1000, 2000], + [2000, 4000], + [4000, 8000], + [8000, 16000], +] + +# Normalize energy per band +normalize = true + +# Smooth vertical motion (important on tall matrices) +attack = 0.6 +decay = 0.85 + +# Clamp noise floor +floor = 0.02 From bddd5957ee9e0946a8b07683319c125ea70c0223 Mon Sep 17 00:00:00 2001 From: "Leone, Mark A [LGS]" Date: Sat, 31 Jan 2026 11:35:01 -0500 Subject: [PATCH 2/6] Script variant designed to work with pipewire dsp filter. Linking filter to source and sink not yet working --- equalizer_files/eq-input-filter.sh | 234 +++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100755 equalizer_files/eq-input-filter.sh diff --git a/equalizer_files/eq-input-filter.sh b/equalizer_files/eq-input-filter.sh new file mode 100755 index 0000000..6896a4e --- /dev/null +++ b/equalizer_files/eq-input-filter.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ---------------- Config ---------------- +BASE_CMD="inputmodule-control led-matrix" +EQ_CMD="$BASE_CMD --input-eq" +FALLBACK_CMD="$BASE_CMD --random-eq" +CHECK_INTERVAL=1 +EQ_PID="" +CLEANUP_FLAG=0 +FILTER_SINK="led_eq_input" +FILTER_SOURCE="led_eq_output" + + +# ---------------- Helpers ---------------- + +log() { echo "[$(date +'%H:%M:%S')] $*"; } + +pactl_ok() { + pactl info >/dev/null 2>&1 +} + +wait_for_pactl() { + for _ in {1..20}; do + pactl_ok && return 0 + sleep 0.25 + done + return 1 +} + +current_sink() { + pactl_ok || return 1 + pactl info | sed -n 's/^Default Sink: //p' +} + +monitor_name_for_sink() { + local sink="$1" + echo "${sink}.monitor" +} + +wait_for_monitor_running() { + local monitor="$1" + for _ in {1..50}; do + # Check if the monitor exists in pactl sources + if pactl list short sources | awk '{print $2}' | grep -qx "$monitor"; then + return 0 + fi + sleep 0.1 + done + return 1 +} + +set_default_source() { + local monitor="$1" + pactl set-default-source "$monitor" || true +} + +sink_type() { + case "$1" in + *headphone*|*Audio_Expansion*) echo "headphones" ;; + *analog-stereo*) echo "speakers" ;; + *bluez*) echo "bluetooth" ;; + *) echo "unknown" ;; + esac +} + +fade_out() { + # Small pause to simulate fade + sleep 0.2 +} + +fade_in() { + sleep 0.2 +} + +notify_sink() { + local type="$1" + log "Notifying sink change: $type" + fade_out + sleep 0.1 + fade_in +} + +start_eq() { + stop_eq + if pactl_ok; then + $EQ_CMD & + else + $FALLBACK_CMD & + fi + EQ_PID=$! +} + +stop_eq() { + # Kill the process we tracked + [[ -n "${EQ_PID:-}" ]] && kill "$EQ_PID" >/dev/null 2>&1 || true + + # Also make sure no stray processes remain + pkill -f "${EQ_CMD}" >/dev/null 2>&1 || true + + # Reset EQ_PID + EQ_PID="" +} + + +start_fallback() { + stop_eq + $FALLBACK_CMD >/dev/null 2>&1 & + EQ_PID=$! +} + +visual_cue() { + local type="$1" + case "$type" in + speakers) + # Show horizontal bar for speakers + ${BASE_CMD} --pattern all-on >/dev/null 2>&1 + ;; + headphones) + # Show vertical bars for headphones + ${BASE_CMD} --pattern zigzag >/dev/null 2>&1 + ;; + bluetooth) + # Unknown source → flash everything briefly + ${BASE_CMD} --pattern gradient >/dev/null 2>&1 + ;; + *) + # Unknown source → flash everything briefly + ${BASE_CMD} --pattern gradient >/dev/null 2>&1 + ;; + esac + + # Keep cue visible briefly + sleep 0.6 +} + +# wire_filter() { +# local monitor="$1" + +# log "→ Linking $monitor → led_eq_input" + +# # Remove any existing links to avoid duplicates +# pw-link -d "$monitor" led_eq_input 2>/dev/null || true + +# # Create the link +# pw-link "$monitor" led_eq_input + +# # Make the filter output the default source +# pactl set-default-source led_eq_output +# } +wire_filter() { + local monitor="$1" + + log "→ Wiring $monitor → led_eq → inputmodule-control" + + # pw-link "${monitor}:monitor_FL" led_eq:input_FL + # pw-link "${monitor}:monitor_FR" led_eq:input_FR + + pw-link alsa_output.pci-0000_c2_00.6.analog-stereo:monitor_FL led_eq:input_FL + pw-link alsa_output.pci-0000_c2_00.6.analog-stereo:monitor_FR led_eq:input_FR + + pactl set-default-source led_eq +} + + +# ---------------- Cleanup ---------------- + +cleanup() { + if [ $CLEANUP_FLAG -eq 1 ]; then return; fi + CLEANUP_FLAG=1 + log "Restoring original input source: $ORIG_SOURCE" + stop_eq + if pactl_ok; then + pactl set-default-source "$ORIG_SOURCE" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT INT TERM + +# ---------------- Startup ---------------- + +ORIG_SOURCE=$(pactl info | sed -n 's/^Default Source: //p') +[[ -z "$ORIG_SOURCE" ]] && log "No original source found" && exit 1 + +log "Original input source: $ORIG_SOURCE" + +CURRENT_SINK="$(current_sink)" +MONITOR="$(monitor_name_for_sink "$CURRENT_SINK")" + +wire_filter "$MONITOR" + +if wait_for_monitor_running "$MONITOR"; then + log "→ Monitor ready: $MONITOR" + start_eq +else + log "⚠ Monitor not ready, using fallback EQ" + start_fallback +fi + +# ---------------- Main Loop ---------------- + +pactl subscribe | while read -r line; do + case "$line" in + *"on server"*|*"on sink"*) + NEW_SINK="$(current_sink)" + if [[ "$NEW_SINK" != "$CURRENT_SINK" && -n "$NEW_SINK" ]]; then + log "🔄 Sink change detected → $NEW_SINK" + + stop_eq + + SINK_TYPE=$(sink_type "$NEW_SINK") + fade_out + visual_cue "$SINK_TYPE" + fade_in + + MONITOR="$(monitor_name_for_sink "$NEW_SINK")" + log "→ Input now follows: $MONITOR" + wire_filter "$MONITOR" + + if wait_for_monitor_running "$MONITOR"; then + start_eq + else + log "⚠ Monitor never reached RUNNING/IDLE, using fallback" + start_fallback + fi + + CURRENT_SINK="$NEW_SINK" + fi + ;; + esac +done & +SUB_PID=$! + +# Wait for subscription loop +wait "$SUB_PID" From 35ebc94508adc3c44474e35e3d91343b23f92db6 Mon Sep 17 00:00:00 2001 From: "Leone, Mark A [LGS]" Date: Mon, 2 Feb 2026 10:52:38 -0500 Subject: [PATCH 3/6] Roadmap working file --- roadmap.md | 344 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 roadmap.md diff --git a/roadmap.md b/roadmap.md new file mode 100644 index 0000000..7efb146 --- /dev/null +++ b/roadmap.md @@ -0,0 +1,344 @@ +# LED Matrix Web Control - Implementation Options + +## Current State + +Two mature codebases exist: + +- **led-matrix** (Python): Background service with system monitoring, plugin architecture, config management, systemd integration +- **dotmatrixtool** (Web): Browser-based drawing tool with Web Serial API, import/export JSON, direct hardware communication + +## Hardware Configuration + +**Framework 16 LED Matrix Panels:** + +- Each panel: 9 columns (width) × 34 rows (height) +- Typical setup: 2 panels (left and right) +- Total logical display when both present: 18 wide × 34 tall +- Physical placement: Can flank keyboard OR sit side-by-side +- Backend always treats as 2 independent 9×34 panels +- Frontend can show "unified view" for drawing alignment (UX only) + +**Quadrant Layout (when 2 panels present):** + +- Top-Left: 9×17 (top half of left panel) +- Bottom-Left: 9×17 (bottom half of left panel) +- Top-Right: 9×17 (top half of right panel) +- Bottom-Right: 9×17 (bottom half of right panel) + +**Single Panel Setup:** + +- Only 2 sections available: top (9×17) and bottom (9×17) + +## Option A: Hybrid Architecture (Python Backend + React Frontend) + +### Architecture Overview + +Python service runs continuously as systemd service. React web app provides control interface and communicates via API. + +### Components to Build + +#### Backend Development (Python) + +1. **FastAPI Server Layer** + +- REST API endpoints for control operations + - WebSocket endpoint for real-time state updates + - Serve React frontend as static files + - CORS configuration for development + +1. **Mode Management System** (Critical - see problem statement) + +- ModeController class with state machine (AUTO | MANUAL | MIXED) + - Hardware access locking (threading.Lock or asyncio.Lock) + - Transition handlers (pause auto mode, resume auto mode) + - Watchdog timer for manual mode timeout + - Per-quadrant locking for MIXED mode (optional) + +1. **API Endpoints** + +- POST /mode/manual - Request manual control + - POST /mode/auto - Release manual control + - GET /mode/status - Current mode and locked quadrants + - POST /manual/draw/left - Send left panel pattern (9×34 array) + - POST /manual/draw/right - Send right panel pattern (9×34 array) + - POST /manual/quadrant - Control specific quadrant + - GET /config - Read current config.yaml + - PUT /config - Update config.yaml + - GET /apps/list - Available apps and plugins + - GET /metrics - Current system metrics + - GET /panels - Detect connected panels (1 or 2) + - WebSocket /ws - Live LED matrix state + +1. **Modified Auto Mode Loop** + +- Check mode_controller.current_mode before drawing + - Pause app cycling when mode == MANUAL + - Resume from correct position when returning to AUTO + +1. **Hardware Access Serialization** + +- Modify DrawingThread to use mode_controller.hardware_lock + - Single point of hardware access + - Queue management during mode transitions + +#### Frontend Development (React) + +1. **Project Setup** + +- Vite + React + TypeScript + - API client library (axios or fetch) + - WebSocket client + - State management (Zustand or React Context) + +1. **Mode Control Interface** + +- Toggle between Auto/Manual modes + - Visual indicator of current mode + - Timeout countdown when in manual mode + - Warning dialog before switching modes + +1. **Auto Mode Configuration Panel** + +- Quadrant selector (4 quadrants: top-left, top-right, bottom-left, bottom-right) + - App dropdown per quadrant (populated from /apps/list) + - Duration slider per app + - Animation toggle per app + - Add/remove apps for time-multiplexing + - Save/load config.yaml + +1. **Manual Control Interface** + +- Quadrant selector for targeted control + - App selector with live application + - Quick preset buttons + +1. **Free Draw Canvas** (from dotmatrixtool) + +- Refactor dotmatrixtool app.js to React component + - Two 9×34 LED grid components (left and right panels) + - Optional "unified view" toggle - displays both as single 18×34 canvas for alignment (UX only) + - Mouse/touch drawing (left click draw, ctrl+click erase) + - Brightness slider + - Color/grayscale picker + - Export to snapshot_files/ + - Send directly to backend API (not Web Serial) + - Always exports/sends as two separate panel arrays (left 9×34, right 9×34) + +1. **System Metrics Dashboard** + +- Real-time display via WebSocket + - CPU utilization graph + - Memory usage + - Disk I/O rates + - Network traffic + - Temperature sensors + - Fan speeds + +1. **System Alerts Configuration** (from notification API design) + +- Define alert triggers + - Route alerts to system tray OR LED matrix + - Priority levels + - Icon/pattern selection per alert type + +1. **Live Preview** + +- Visual representation of LED matrix + - Shows left panel (9×34) and right panel (9×34) if present + - Optional unified view (18×34 when both panels present) + - Updates via WebSocket from Python backend + - Shows actual hardware state + +### Deployment + +- Python service runs as systemd unit +- React app built to static files +- FastAPI serves static files at / +- Access via +- Optional: nginx reverse proxy for production + +### Advantages + +- Leverages existing Python codebase (system monitoring, plugins, hardware control) +- Python's psutil provides accurate system metrics +- Service runs independently of browser +- Systemd integration for auto-start +- NixOS flake already exists +- Can reuse dotmatrixtool canvas component + +### Disadvantages + +- Need to build FastAPI layer +- Need to refactor dotmatrixtool jQuery to React +- More complexity (two languages) + +## Option B: Enhanced Python Service + Embedded dotmatrixtool + +### Architecture Overview + +Python service runs continuously. Serve dotmatrixtool (mostly as-is) with minimal modifications to communicate with Python backend. + +### Components to Build + +#### Backend Development (Python) + +1. **FastAPI Server Layer** + +- Same as Option A + - Serve dotmatrixtool HTML/JS/CSS as static files + +1. **Mode Management System** + +- Same as Option A + - Critical for preventing conflicts + +1. **API Endpoints** + +- POST /mode/manual - Request manual control + - POST /mode/auto - Release manual control + - POST /manual/draw - Receive pattern from dotmatrixtool + - POST /config/quadrant - Configure quadrant app + - GET /config - Current configuration + - GET /metrics - System metrics + - WebSocket /ws - Live state updates + +1. **Modified Auto Mode Loop** + +- Same as Option A + +1. **Hardware Access Serialization** + +- Same as Option A + +#### Frontend Development (Minimal Modifications) + +1. **Modify dotmatrixtool app.js** + +- Add mode selector UI (auto/manual) + - Replace Web Serial API calls with fetch to Python API + - Add quadrant configuration form (4 quadrants across both panels) + - Add app selector dropdowns per quadrant + - Keep existing drawing canvas (left 9×34 and right 9×34) + - Add optional "unified view" toggle for drawing alignment (displays as 18×34) + - Always send data as two separate panel arrays to backend + +1. **Add Control Interface (vanilla JS or minimal framework)** + +- Mode toggle button (Auto/Manual) + - Quadrant configuration panel (4 quadrants: TL, BL, TR, BR) + - App selector per quadrant with duration and animation settings + - Panel detection display (shows 1 or 2 panels connected) + - Save config button + +1. **System Metrics Display** (optional) + +- Basic dashboard using Chart.js or similar + - WebSocket connection to Python backend + +### Deployment + +- Python service runs as systemd unit +- Serves dotmatrixtool files at / +- Access via + +### Advantages + +- Less frontend development (reuse dotmatrixtool mostly as-is) +- No need to refactor jQuery to React +- Simpler tech stack +- Faster to implement +- Still leverages Python backend strengths + +### Disadvantages + +- Limited UI/UX capabilities (jQuery vs React) +- Harder to build complex configuration interface +- Less maintainable frontend code +- System alerts configuration would be basic + +## Comparison Matrix + +### Development Effort + +- **Option A**: Higher (full React app, refactor dotmatrixtool) +- **Option B**: Lower (minimal modifications to dotmatrixtool) + +### User Experience + +- **Option A**: Modern, polished UI with advanced features +- **Option B**: Functional but basic UI + +### Maintainability + +- **Option A**: Better (React component architecture) +- **Option B**: Mixed (Python good, frontend dated) + +### Extensibility + +- **Option A**: Excellent (easy to add features in React) +- **Option B**: Limited (jQuery codebase harder to extend) + +### System Alerts Integration + +- **Option A**: Full implementation of notification API design +- **Option B**: Basic implementation + +### Time to MVP + +- **Option A**: 3-4 weeks +- **Option B**: 1-2 weeks + +## Recommendation + +**Start with Option B, migrate to Option A later:** + +1. Implement Mode Management System (critical for both) +2. Build FastAPI layer with minimal endpoints +3. Modify dotmatrixtool to call Python API instead of Web Serial +4. Add basic quadrant configuration UI +5. Test and validate architecture +6. Later: Migrate frontend to React incrementally + +This approach: + +- Validates the architecture quickly +- Solves the mode management problem first +- Provides working system faster +- Allows React migration when UI needs justify effort + +## Common Requirements (Both Options) + +### Critical Components + +1. **Mode Management System** - Prevents auto/manual conflicts +2. **Hardware Access Locking** - Serializes LED matrix access +3. **API Layer** - Communication between frontend and Python service +4. **Auto Mode Pause/Resume** - Clean state transitions +5. **Manual Mode Timeout** - Safety mechanism + +### Configuration Management + +- Read/write config.yaml via API +- Validate configuration before applying +- Backup previous config on changes +- Reload service configuration without restart + +### Integration with Existing Code + +- led-matrix/monitors.py - Already provides system metrics +- led-matrix/plugins/ - Plugin architecture works as-is +- led-matrix/drawing.py - Needs locking modification for mode management +- led-matrix/config.yaml - Becomes API-editable, already has 4-quadrant structure +- led-matrix/led_system_monitor.py - Already detects 1 or 2 panels via discover_led_devices() +- dotmatrixtool/app.js - Canvas reused in both options (left/right already separate) + +### Panel Configuration Notes + +- Backend always treats panels independently (2× 9×34) +- No "unified mode" in backend code - always 4 quadrants (to be decided) +- "Unified view" is purely a frontend UX feature for drawing alignment +- When drawing spans both panels, frontend sends left data + right data separately (to be decided) +- Existing config.yaml structure already supports this (top-left, bottom-left, top-right, bottom-right) + +### Future development +- dotmatrixtool/app.js / led-matrix/plugins/ Plugin and icon / animation management and installation through API and dotmatixtool. UX friendly \ No newline at end of file From 0bcf8d0331969de8a4e62c3787c8cfdc122341e4 Mon Sep 17 00:00:00 2001 From: "Leone, Mark A [LGS]" Date: Mon, 2 Feb 2026 11:41:01 -0500 Subject: [PATCH 4/6] Cleanup equalizer-filter script. Remove old scripts --- equalizer_files/eq-input-filter.sh | 17 -- .../old_scripts/eq-test-bt-working.sh | 190 ------------------ equalizer_files/old_scripts/eq-test.sh | 168 ---------------- .../old_scripts/led-eq-speakers-fixed.sh | 172 ---------------- .../old_scripts/led-eq-speakers.sh | 176 ---------------- 5 files changed, 723 deletions(-) delete mode 100755 equalizer_files/old_scripts/eq-test-bt-working.sh delete mode 100755 equalizer_files/old_scripts/eq-test.sh delete mode 100755 equalizer_files/old_scripts/led-eq-speakers-fixed.sh delete mode 100755 equalizer_files/old_scripts/led-eq-speakers.sh diff --git a/equalizer_files/eq-input-filter.sh b/equalizer_files/eq-input-filter.sh index 6896a4e..27f2dc4 100755 --- a/equalizer_files/eq-input-filter.sh +++ b/equalizer_files/eq-input-filter.sh @@ -134,28 +134,11 @@ visual_cue() { sleep 0.6 } -# wire_filter() { -# local monitor="$1" - -# log "→ Linking $monitor → led_eq_input" - -# # Remove any existing links to avoid duplicates -# pw-link -d "$monitor" led_eq_input 2>/dev/null || true - -# # Create the link -# pw-link "$monitor" led_eq_input - -# # Make the filter output the default source -# pactl set-default-source led_eq_output -# } wire_filter() { local monitor="$1" log "→ Wiring $monitor → led_eq → inputmodule-control" - # pw-link "${monitor}:monitor_FL" led_eq:input_FL - # pw-link "${monitor}:monitor_FR" led_eq:input_FR - pw-link alsa_output.pci-0000_c2_00.6.analog-stereo:monitor_FL led_eq:input_FL pw-link alsa_output.pci-0000_c2_00.6.analog-stereo:monitor_FR led_eq:input_FR diff --git a/equalizer_files/old_scripts/eq-test-bt-working.sh b/equalizer_files/old_scripts/eq-test-bt-working.sh deleted file mode 100755 index ff3affb..0000000 --- a/equalizer_files/old_scripts/eq-test-bt-working.sh +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ---------------- Config ---------------- -EQ_CMD="inputmodule-control led-matrix --input-eq" -FALLBACK_CMD="inputmodule-control led-matrix --random-eq" -CHECK_INTERVAL=1 -EQ_PID="" - -# ---------------- Helpers ---------------- - -log() { echo "[$(date +'%H:%M:%S')] $*"; } - -pactl_ok() { - pactl info >/dev/null 2>&1 -} - -wait_for_pactl() { - for _ in {1..20}; do - pactl_ok && return 0 - sleep 0.25 - done - return 1 -} - -current_sink() { - pactl_ok || return 1 - pactl info | sed -n 's/^Default Sink: //p' -} - -monitor_name_for_sink() { - local sink="$1" - echo "${sink}.monitor" -} - -wait_for_monitor_running() { - local monitor="$1" - for _ in {1..50}; do - # Check if the monitor exists in pactl sources - if pactl list short sources | awk '{print $2}' | grep -qx "$monitor"; then - return 0 - fi - sleep 0.1 - done - return 1 -} - -set_default_source() { - local monitor="$1" - pactl set-default-source "$monitor" || true -} - -sink_type() { - case "$1" in - *headphone*|*headset*) echo "headphones" ;; - *analog-stereo*) echo "speakers" ;; - *) echo "unknown" ;; - esac -} - -fade_out() { - # Small pause to simulate fade - sleep 0.2 -} - -fade_in() { - sleep 0.2 -} - -notify_sink() { - local type="$1" - log "Notifying sink change: $type" - fade_out - sleep 0.1 - fade_in -} - -start_eq() { - stop_eq - if pactl_ok; then - $EQ_CMD & - else - $FALLBACK_CMD & - fi - EQ_PID=$! -} - -stop_eq() { - [[ -n "$EQ_PID" ]] && kill "$EQ_PID" >/dev/null 2>&1 || true - [[ -n "$EQ_PID" ]] && wait "$EQ_PID" 2>/dev/null || true - EQ_PID="" -} - -start_fallback() { - stop_eq - $FALLBACK_CMD >/dev/null 2>&1 & - EQ_PID=$! -} - -visual_cue() { - local type="$1" - - case "$type" in - speakers) - # Show horizontal bar for speakers - inputmodule-control led-matrix --pattern all-on >/dev/null 2>&1 - ;; - headphones) - # Show vertical bars for headphones - inputmodule-control led-matrix --pattern zigzag >/dev/null 2>&1 - ;; - *) - # Unknown source → flash everything briefly - inputmodule-control led-matrix --pattern gradient >/dev/null 2>&1 - ;; - esac - - # Keep cue visible briefly - sleep 2 -} - - -# ---------------- Cleanup ---------------- - -cleanup() { - log "Restoring original input source: $ORIG_SOURCE" - stop_eq - if pactl_ok; then - pactl set-default-source "$ORIG_SOURCE" >/dev/null 2>&1 || true - fi - [[ -n "${EQ_PID:-}" ]] && kill "$EQ_PID" >/dev/null 2>&1 || true -} -trap cleanup EXIT INT TERM - -# ---------------- Startup ---------------- - -ORIG_SOURCE=$(pactl info | sed -n 's/^Default Source: //p') -[[ -z "$ORIG_SOURCE" ]] && log "No original source found" && exit 1 - -log "Original input source: $ORIG_SOURCE" - -CURRENT_SINK="$(current_sink)" -MONITOR="$(monitor_name_for_sink "$CURRENT_SINK")" - -set_default_source "$MONITOR" - -if wait_for_monitor_running "$MONITOR"; then - log "→ Monitor ready: $MONITOR" - start_eq -else - log "⚠ Monitor not ready, using fallback EQ" - start_fallback -fi - -# ---------------- Main Loop ---------------- - -pactl subscribe | while read -r line; do - case "$line" in - *"on server"*|*"on sink"*) - NEW_SINK="$(current_sink)" - if [[ "$NEW_SINK" != "$CURRENT_SINK" && -n "$NEW_SINK" ]]; then - log "🔄 Sink change detected → $NEW_SINK" - - stop_eq - - SINK_TYPE=$(sink_type "$NEW_SINK") - fade_out - visual_cue "$SINK_TYPE" - fade_in - - MONITOR="$(monitor_name_for_sink "$NEW_SINK")" - log "→ Input now follows: $MONITOR" - set_default_source "$MONITOR" - - if wait_for_monitor_running "$MONITOR"; then - start_eq - else - log "⚠ Monitor never reached RUNNING/IDLE, using fallback" - start_fallback - fi - - CURRENT_SINK="$NEW_SINK" - fi - ;; - esac -done & -EQ_PID=$! - -# Wait for subscription loop -wait "$EQ_PID" diff --git a/equalizer_files/old_scripts/eq-test.sh b/equalizer_files/old_scripts/eq-test.sh deleted file mode 100755 index b982dab..0000000 --- a/equalizer_files/old_scripts/eq-test.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -EQ_CMD="inputmodule-control led-matrix --input-eq" -CHECK_INTERVAL=0.5 # seconds -MONITOR_TIMEOUT=5 # seconds to wait for monitor readiness - -# ---------------- HELPERS ---------------- -log() { echo "[$(date '+%H:%M:%S')] $*"; } - -pactl_ok() { pactl info >/dev/null 2>&1; } - -get_default_sink() { pactl_ok || return 1; pactl info | sed -n 's/^Default Sink: //p'; } -monitor_name_for_sink() { echo "${1}.monitor"; } - -set_default_source() { - local source="$1" - pactl set-default-source "$source" >/dev/null 2>&1 -} - - -sink_type() { - case "$1" in - *headphone*|*headset*) echo "headphones" ;; - *analog-stereo*) echo "speakers" ;; - *) echo "unknown" ;; - esac -} - -wait_for_monitor() { - local monitor="$1" - for _ in {1..40}; do - pactl list short sources | awk '{print $2}' | grep -qx "$monitor" && return 0 - sleep 0.25 - done - return 1 -} - -monitor_state() { - pactl list short sources | awk -v src="$1" '$2 == src {print $7}' -} - -wait_for_monitor_running() { - local monitor="$1" - local max_wait=30 # increase from 2-3s to 30s - local waited=0 - - while ! pactl list short sources | awk '{print $2}' | grep -qx "$monitor"; do - sleep 0.5 - ((waited+=1)) - if (( waited*5 >= max_wait )); then - return 1 - fi - done - - # Optional: extra wait until the monitor actually reports RUNNING - # Could use `pw-top` or PipeWire API for more precision - sleep 10 - return 0 -} - - -fade_out() { - for _ in {1..3}; do - inputmodule-control led-matrix --pattern all-on >/dev/null 2>&1 - sleep 0.05 - inputmodule-control led-matrix --pattern percentage >/dev/null 2>&1 # or your default EQ pattern - - sleep 0.05 - done -} -fade_in() { sleep 0.05; } - -notify_sink() { - case "$1" in - headphones) inputmodule-control led-matrix --pattern percentage >/dev/null 2>&1 ;; - speakers) inputmodule-control led-matrix --pattern double-gradient >/dev/null 2>&1 ;; - *) inputmodule-control led-matrix --pattern all-on >/dev/null 2>&1 - sleep 0.3 - inputmodule-control led-matrix --random-eq >/dev/null 2>&1 ;; -esac -sleep 0.4 -} - -start_eq() { - if pactl_ok; then - $EQ_CMD & - EQ_PID=$! - else - inputmodule-control led-matrix --random-eq >/dev/null 2>&1 & - EQ_PID=$! - fi -} - -# stop_eq() { -# [[ -n "${EQ_PID:-}" ]] && kill "$EQ_PID" >/dev/null 2>&1 || true -# wait "${EQ_PID:-}" 2>/dev/null || true -# EQ_PID="" -# } - -stop_eq() { - if [[ -n "$EQ_PID" ]]; then - kill "$EQ_PID" >/dev/null 2>&1 || true - wait "$EQ_PID" 2>/dev/null || true - EQ_PID="" - fi -} - -start_fallback() { - inputmodule-control led-matrix --random-eq >/dev/null 2>&1 & - EQ_PID=$! -} - -# ---------------- STARTUP ---------------- -ORIG_SOURCE=$(pactl info | sed -n 's/^Default Source: //p') -[[ -z "$ORIG_SOURCE" ]] && exit 1 -log "Original input source: $ORIG_SOURCE" - -EQ_PID="" -CURRENT_SINK="$(get_default_sink)" -MONITOR="$(monitor_name_for_sink "$CURRENT_SINK")" - -if ! wait_for_monitor_running "$MONITOR"; then - echo "⚠ Monitor not ready, retrying in 2s..." - sleep 2 - wait_for_monitor_running "$MONITOR" || start_fallback -fi -set_default_source "$MONITOR" -start_eq - -# ---------------- CLEANUP ---------------- -cleanup() { - log "Restoring original source: $ORIG_SOURCE" - stop_eq - pactl_ok && pactl set-default-source "$ORIG_SOURCE" >/dev/null 2>&1 || true -} -trap cleanup EXIT INT TERM - -# ---------------- MAIN LOOP ---------------- -while true; do - NEW_SINK="$(get_default_sink)" - if [[ "$NEW_SINK" != "$CURRENT_SINK" && -n "$NEW_SINK" ]]; then - log "🔄 Sink change detected → $NEW_SINK" - - SINK_TYPE="$(sink_type "$NEW_SINK")" - MONITOR="$(monitor_name_for_sink "$NEW_SINK")" - log "→ Waiting for monitor: $MONITOR" - - if wait_for_monitor_running "$MONITOR"; then - log "✅ Monitor ready" - set_default_source "$MONITOR" - - fade_out - notify_sink "$SINK_TYPE" - - stop_eq - start_eq - fade_in - else - log "⚠ Monitor never reached RUNNING/IDLE — fallback EQ" - stop_eq - start_fallback - fi - - CURRENT_SINK="$NEW_SINK" - fi - sleep "$CHECK_INTERVAL" -done diff --git a/equalizer_files/old_scripts/led-eq-speakers-fixed.sh b/equalizer_files/old_scripts/led-eq-speakers-fixed.sh deleted file mode 100755 index ffbf574..0000000 --- a/equalizer_files/old_scripts/led-eq-speakers-fixed.sh +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -############################ -# CONFIG -############################ - -EQ_CMD="inputmodule-control led-matrix --input-eq" -FALLBACK_CMD="inputmodule-control led-matrix --random-eq" - -CHECK_INTERVAL=1 # seconds between sink polls -MONITOR_TIMEOUT=5 # seconds to wait for monitor readiness - -############################ -# STATE -############################ - -ORIGINAL_SOURCE="" -EQ_PID="" -CURRENT_SINK="" - -############################ -# UTILS -############################ - -log() { - echo "[$(date +%H:%M:%S)] $*" -} - -fade_out() { - for _ in {1..3}; do - inputmodule-control led-matrix --test off >/dev/null 2>&1 - sleep 0.05 - done -} - -fade_in() { - inputmodule-control led-matrix --test off >/dev/null 2>&1 - sleep 0.05 -} - -notify_sink() { - local sink_type="$1" - - case "$sink_type" in - headphones) - inputmodule-control led-matrix --test vertical >/dev/null 2>&1 - ;; - speakers) - inputmodule-control led-matrix --test horizontal >/dev/null 2>&1 - ;; - *) - inputmodule-control led-matrix --test blink >/dev/null 2>&1 - ;; - esac - - sleep 0.4 -} - -get_default_sink() { - pactl info | awk -F': ' '/Default Sink/ {print $2}' -} - -get_default_source() { - pactl info | awk -F': ' '/Default Source/ {print $2}' -} - -set_default_source() { - pactl set-default-source "$1" -} - -monitor_name_for_sink() { - echo "${1}.monitor" -} - -monitor_state() { - pactl list short sources | awk -v src="$1" '$2 == src {print $7}' -} - -wait_for_monitor_running() { - local monitor="$1" - local elapsed=0 - - while (( elapsed < MONITOR_TIMEOUT )); do - state="$(monitor_state "$monitor" || true)" - - case "$state" in - RUNNING|IDLE) - return 0 - ;; - esac - - sleep 1 - ((elapsed++)) - done - - return 1 -} - -stop_eq() { - if [[ -n "${EQ_PID:-}" ]] && kill -0 "$EQ_PID" 2>/dev/null; then - log "Stopping EQ (pid $EQ_PID)" - kill "$EQ_PID" - wait "$EQ_PID" 2>/dev/null || true - fi - EQ_PID="" -} - -start_eq() { - log "Starting EQ" - $EQ_CMD >/dev/null 2>&1 & - EQ_PID=$! -} - -start_fallback() { - log "Starting fallback EQ" - $FALLBACK_CMD >/dev/null 2>&1 & - EQ_PID=$! -} - -cleanup() { - log "Cleaning up" - stop_eq - [[ -n "$ORIGINAL_SOURCE" ]] && set_default_source "$ORIGINAL_SOURCE" - exit 0 -} - -trap cleanup INT TERM - -############################ -# INIT -############################ - -if ! pactl info >/dev/null 2>&1; then - log "pactl unavailable, starting fallback EQ" - start_fallback - wait -fi - -ORIGINAL_SOURCE="$(get_default_source)" -log "Original input source: $ORIGINAL_SOURCE" - -############################ -# MAIN LOOP -############################ - -while true; do - NEW_SINK="$(get_default_sink)" - - if [[ "$NEW_SINK" != "$CURRENT_SINK" ]]; then - log "🔄 Sink change detected → $NEW_SINK" - - stop_eq - - MONITOR="$(monitor_name_for_sink "$NEW_SINK")" - log "→ Input now follows: $MONITOR" - - set_default_source "$MONITOR" - - if wait_for_monitor_running "$MONITOR"; then - log "✅ Monitor ready" - start_eq - else - log "⚠ Monitor never reached RUNNING/IDLE" - start_fallback - fi - - CURRENT_SINK="$NEW_SINK" - fi - - sleep "$CHECK_INTERVAL" -done diff --git a/equalizer_files/old_scripts/led-eq-speakers.sh b/equalizer_files/old_scripts/led-eq-speakers.sh deleted file mode 100755 index 0ec7f58..0000000 --- a/equalizer_files/old_scripts/led-eq-speakers.sh +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ---------------- CONFIG ------------------ -EQ_CMD="inputmodule-control led-matrix --input-eq" -CHECK_INTERVAL=1 # seconds between sink checks - -# ---------------- HELPERS ----------------- - -log() { - echo "$(date +'%H:%M:%S') $*" -} - -# Check if pactl is responding -pactl_ok() { - pactl info >/dev/null 2>&1 -} - -wait_for_pactl() { - for _ in {1..20}; do - pactl_ok && return 0 - sleep 0.25 - done - return 1 -} - -# Get current default sink -get_default_sink() { - pactl_ok || return 1 - pactl info | sed -n 's/^Default Sink: //p' -} - -# Map sink to its monitor source -monitor_name_for_sink() { - local sink="$1" - echo "${sink}.monitor" -} - -# Wait for monitor source to appear -wait_for_monitor_running() { - local monitor="$1" - for _ in {1..20}; do - pactl list short sources | awk '{print $2}' | grep -qx "$monitor" && return 0 - sleep 0.1 - done - return 1 -} - -# Set default source -set_default_source() { - local src="$1" - pactl_ok || return 1 - pactl set-default-source "$src" -} - -# Determine sink type for notifications -sink_type() { - case "$1" in - *headphone*|*headset*) echo "headphones" ;; - *analog-stereo*) echo "speakers" ;; - *) echo "unknown" ;; - esac -} - -# Visual indication of sink change -notify_sink() { - local type="$1" - case "$type" in - headphones) - inputmodule-control led-matrix --test vertical >/dev/null 2>&1 - ;; - speakers) - inputmodule-control led-matrix --test horizontal >/dev/null 2>&1 - ;; - *) - inputmodule-control led-matrix --test blink >/dev/null 2>&1 - ;; - esac - sleep 0.4 -} - -# Fading out LEDs -fade_out() { - for _ in {1..3}; do - inputmodule-control led-matrix --test off >/dev/null 2>&1 - sleep 0.05 - done -} - -# Short pause before EQ resumes -fade_in() { - inputmodule-control led-matrix --test off >/dev/null 2>&1 - sleep 0.05 -} - -# Start EQ or fallback -start_eq() { - if pactl_ok; then - $EQ_CMD & - EQ_PID=$! - else - start_fallback - fi -} - -start_fallback() { - inputmodule-control led-matrix --random-eq >/dev/null 2>&1 & - EQ_PID=$! -} - -# Stop EQ process -stop_eq() { - [[ -n "${EQ_PID:-}" ]] && kill "$EQ_PID" >/dev/null 2>&1 || true - wait "${EQ_PID:-}" 2>/dev/null || true -} - -# Cleanup on exit -cleanup() { - log "Restoring original input source: $ORIG_SOURCE" - stop_eq - - if pactl_ok; then - pactl set-default-source "$ORIG_SOURCE" >/dev/null 2>&1 || true - fi - - [[ -n "${SUB_PID:-}" ]] && kill "$SUB_PID" >/dev/null 2>&1 || true -} -trap cleanup EXIT INT TERM - -# ---------------- STARTUP ----------------- -ORIG_SOURCE=$(pactl info | sed -n 's/^Default Source: //p') -[[ -z "$ORIG_SOURCE" ]] && { log "No default source found"; exit 1; } -log "Original input source: $ORIG_SOURCE" - -CURRENT_SINK="$(get_default_sink)" -MONITOR="$(monitor_name_for_sink "$CURRENT_SINK")" - -set_default_source "$MONITOR" -start_eq - -# ---------------- MAIN LOOP ---------------- -pactl subscribe | while read -r line; do - case "$line" in - *"on server"*|*"on sink"*) - NEW_SINK="$(get_default_sink)" - if [[ "$NEW_SINK" != "$CURRENT_SINK" && -n "$NEW_SINK" ]]; then - log "🔄 Sink change detected → $NEW_SINK" - - stop_eq - - SINK_TYPE="$(sink_type "$NEW_SINK")" - - fade_out - notify_sink "$SINK_TYPE" - fade_in - - MONITOR="$(monitor_name_for_sink "$NEW_SINK")" - log "→ Input now follows: $MONITOR" - set_default_source "$MONITOR" - - # ✅ Avoid exiting due to set -e if monitor is slow - if ! wait_for_monitor_running "$MONITOR"; then - log "⚠ Monitor never reached RUNNING/IDLE, using fallback" - start_fallback - else - start_eq - fi - - CURRENT_SINK="$NEW_SINK" - fi - ;; - esac -done & -SUB_PID=$! - -wait "$SUB_PID" From 95c0f8f94d6586d5656385c0787cdb24f67c933c Mon Sep 17 00:00:00 2001 From: "Leone, Mark A [LGS]" Date: Mon, 2 Feb 2026 23:02:22 -0500 Subject: [PATCH 5/6] True equalizer, sending sample values from stream. External filter (e.g. easyeffects can be used, or built-in filter with hard-coded parameters --- equalizer_files/eq-input-filter.sh | 217 ----------------------------- equalizer_files/eq-input.sh | 25 ++-- equalizer_files/visualize.py | 95 +++++++++++++ requirements.txt | 4 +- 4 files changed, 111 insertions(+), 230 deletions(-) delete mode 100755 equalizer_files/eq-input-filter.sh create mode 100644 equalizer_files/visualize.py diff --git a/equalizer_files/eq-input-filter.sh b/equalizer_files/eq-input-filter.sh deleted file mode 100755 index 27f2dc4..0000000 --- a/equalizer_files/eq-input-filter.sh +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ---------------- Config ---------------- -BASE_CMD="inputmodule-control led-matrix" -EQ_CMD="$BASE_CMD --input-eq" -FALLBACK_CMD="$BASE_CMD --random-eq" -CHECK_INTERVAL=1 -EQ_PID="" -CLEANUP_FLAG=0 -FILTER_SINK="led_eq_input" -FILTER_SOURCE="led_eq_output" - - -# ---------------- Helpers ---------------- - -log() { echo "[$(date +'%H:%M:%S')] $*"; } - -pactl_ok() { - pactl info >/dev/null 2>&1 -} - -wait_for_pactl() { - for _ in {1..20}; do - pactl_ok && return 0 - sleep 0.25 - done - return 1 -} - -current_sink() { - pactl_ok || return 1 - pactl info | sed -n 's/^Default Sink: //p' -} - -monitor_name_for_sink() { - local sink="$1" - echo "${sink}.monitor" -} - -wait_for_monitor_running() { - local monitor="$1" - for _ in {1..50}; do - # Check if the monitor exists in pactl sources - if pactl list short sources | awk '{print $2}' | grep -qx "$monitor"; then - return 0 - fi - sleep 0.1 - done - return 1 -} - -set_default_source() { - local monitor="$1" - pactl set-default-source "$monitor" || true -} - -sink_type() { - case "$1" in - *headphone*|*Audio_Expansion*) echo "headphones" ;; - *analog-stereo*) echo "speakers" ;; - *bluez*) echo "bluetooth" ;; - *) echo "unknown" ;; - esac -} - -fade_out() { - # Small pause to simulate fade - sleep 0.2 -} - -fade_in() { - sleep 0.2 -} - -notify_sink() { - local type="$1" - log "Notifying sink change: $type" - fade_out - sleep 0.1 - fade_in -} - -start_eq() { - stop_eq - if pactl_ok; then - $EQ_CMD & - else - $FALLBACK_CMD & - fi - EQ_PID=$! -} - -stop_eq() { - # Kill the process we tracked - [[ -n "${EQ_PID:-}" ]] && kill "$EQ_PID" >/dev/null 2>&1 || true - - # Also make sure no stray processes remain - pkill -f "${EQ_CMD}" >/dev/null 2>&1 || true - - # Reset EQ_PID - EQ_PID="" -} - - -start_fallback() { - stop_eq - $FALLBACK_CMD >/dev/null 2>&1 & - EQ_PID=$! -} - -visual_cue() { - local type="$1" - case "$type" in - speakers) - # Show horizontal bar for speakers - ${BASE_CMD} --pattern all-on >/dev/null 2>&1 - ;; - headphones) - # Show vertical bars for headphones - ${BASE_CMD} --pattern zigzag >/dev/null 2>&1 - ;; - bluetooth) - # Unknown source → flash everything briefly - ${BASE_CMD} --pattern gradient >/dev/null 2>&1 - ;; - *) - # Unknown source → flash everything briefly - ${BASE_CMD} --pattern gradient >/dev/null 2>&1 - ;; - esac - - # Keep cue visible briefly - sleep 0.6 -} - -wire_filter() { - local monitor="$1" - - log "→ Wiring $monitor → led_eq → inputmodule-control" - - pw-link alsa_output.pci-0000_c2_00.6.analog-stereo:monitor_FL led_eq:input_FL - pw-link alsa_output.pci-0000_c2_00.6.analog-stereo:monitor_FR led_eq:input_FR - - pactl set-default-source led_eq -} - - -# ---------------- Cleanup ---------------- - -cleanup() { - if [ $CLEANUP_FLAG -eq 1 ]; then return; fi - CLEANUP_FLAG=1 - log "Restoring original input source: $ORIG_SOURCE" - stop_eq - if pactl_ok; then - pactl set-default-source "$ORIG_SOURCE" >/dev/null 2>&1 || true - fi -} -trap cleanup EXIT INT TERM - -# ---------------- Startup ---------------- - -ORIG_SOURCE=$(pactl info | sed -n 's/^Default Source: //p') -[[ -z "$ORIG_SOURCE" ]] && log "No original source found" && exit 1 - -log "Original input source: $ORIG_SOURCE" - -CURRENT_SINK="$(current_sink)" -MONITOR="$(monitor_name_for_sink "$CURRENT_SINK")" - -wire_filter "$MONITOR" - -if wait_for_monitor_running "$MONITOR"; then - log "→ Monitor ready: $MONITOR" - start_eq -else - log "⚠ Monitor not ready, using fallback EQ" - start_fallback -fi - -# ---------------- Main Loop ---------------- - -pactl subscribe | while read -r line; do - case "$line" in - *"on server"*|*"on sink"*) - NEW_SINK="$(current_sink)" - if [[ "$NEW_SINK" != "$CURRENT_SINK" && -n "$NEW_SINK" ]]; then - log "🔄 Sink change detected → $NEW_SINK" - - stop_eq - - SINK_TYPE=$(sink_type "$NEW_SINK") - fade_out - visual_cue "$SINK_TYPE" - fade_in - - MONITOR="$(monitor_name_for_sink "$NEW_SINK")" - log "→ Input now follows: $MONITOR" - wire_filter "$MONITOR" - - if wait_for_monitor_running "$MONITOR"; then - start_eq - else - log "⚠ Monitor never reached RUNNING/IDLE, using fallback" - start_fallback - fi - - CURRENT_SINK="$NEW_SINK" - fi - ;; - esac -done & -SUB_PID=$! - -# Wait for subscription loop -wait "$SUB_PID" diff --git a/equalizer_files/eq-input.sh b/equalizer_files/eq-input.sh index fad0aa1..5b18973 100755 --- a/equalizer_files/eq-input.sh +++ b/equalizer_files/eq-input.sh @@ -138,7 +138,7 @@ cleanup() { if [ $CLEANUP_FLAG -eq 1 ]; then return; fi CLEANUP_FLAG=1 log "Restoring original input source: $ORIG_SOURCE" - stop_eq + # stop_eq if pactl_ok; then pactl set-default-source "$ORIG_SOURCE" >/dev/null 2>&1 || true fi @@ -159,10 +159,10 @@ set_default_source "$MONITOR" if wait_for_monitor_running "$MONITOR"; then log "→ Monitor ready: $MONITOR" - start_eq + # start_eq else log "⚠ Monitor not ready, using fallback EQ" - start_fallback + # start_fallback fi # ---------------- Main Loop ---------------- @@ -174,25 +174,26 @@ pactl subscribe | while read -r line; do if [[ "$NEW_SINK" != "$CURRENT_SINK" && -n "$NEW_SINK" ]]; then log "🔄 Sink change detected → $NEW_SINK" - stop_eq + # stop_eq SINK_TYPE=$(sink_type "$NEW_SINK") - fade_out - visual_cue "$SINK_TYPE" - fade_in + # fade_out + # visual_cue "$SINK_TYPE" + # fade_in MONITOR="$(monitor_name_for_sink "$NEW_SINK")" log "→ Input now follows: $MONITOR" set_default_source "$MONITOR" if wait_for_monitor_running "$MONITOR"; then - start_eq - else - log "⚠ Monitor never reached RUNNING/IDLE, using fallback" - start_fallback + # start_eq + CURRENT_SINK="$NEW_SINK" + # else + # log "⚠ Monitor never reached RUNNING/IDLE, using fallback" + # # start_fallback fi - CURRENT_SINK="$NEW_SINK" + # CURRENT_SINK="$NEW_SINK" fi ;; esac diff --git a/equalizer_files/visualize.py b/equalizer_files/visualize.py new file mode 100644 index 0000000..2092401 --- /dev/null +++ b/equalizer_files/visualize.py @@ -0,0 +1,95 @@ +import numpy as np +import sounddevice as sd # For audio capture +from scipy.signal import butter, sosfiltfilt # For bandpass filters +import subprocess +import time +import os +import threading + +# Configuration +SAMPLE_RATE = 48000 # Match your system's audio rate (check with `pw-dump | grep default.clock.rate`) +CHUNK_SIZE = 1024 # Audio chunk size (adjust for latency; smaller = more responsive, but higher CPU) +UPDATE_RATE = 0.03 # Seconds between LED updates (e.g., ~33 FPS) + +# 9 frequency bands (octave centers, log-spaced from ~32 Hz to ~16 kHz) +BAND_CENTERS = [31.5, 63, 125, 250, 500, 1000, 2000, 4000, 8000] # Hz +Q = 1.414 # For octave bandwidth; increase for narrower bands + +# Pre-compute bandpass filters (second-order sections for stability) +filters = [] +for fc in BAND_CENTERS: + low = fc / Q + high = fc * Q + sos = butter(4, [low, high], btype='band', fs=SAMPLE_RATE, output='sos') # 4th order Butterworth + filters.append(sos) + +# Scale function: Convert RMS to 0-255 integer for LED height (adjust sensitivity) +def scale_rms(rms, min_db=-60, max_db=0): + db = 20 * np.log10(rms + 1e-10) # dBFS + normalized = np.clip((db - min_db) / (max_db - min_db), 0, 1) + return int(normalized * 34) # Assuming --eq takes 0-34 per column + +# Audio callback: Process chunks in real-time +audio_buffer = np.zeros((CHUNK_SIZE, 2), dtype=np.float32) # Stereo buffer +lock = threading.Lock() + +def audio_callback(indata, frames, time_info, status): + global audio_buffer + with lock: + audio_buffer = indata.copy() # Copy latest chunk + +# Function to compute band levels and update LEDs +USE_EXTERNAL_FILTER = os.environ.get("EXTERNAL_FILTER", "false").lower() == 'true' +def update_leds(): + while True: + with lock: + chunk = audio_buffer.mean(axis=1) # mono + + levels = [] + + if USE_EXTERNAL_FILTER: + # EasyEffects is already applying the 9-band EQ upstream + # → We just measure RMS in the same frequency ranges to get post-EQ bucket levels + for sos in filters: # ← still using the same filter definitions! + filtered = sosfiltfilt(sos, chunk) # Measure energy in this band *after* EasyEffects + rms = np.sqrt(np.mean(filtered ** 2)) + level = scale_rms(rms) + levels.append(level) + + else: + # Python mode: apply the filters ourselves (fixed parameters) + for sos in filters: + filtered = sosfiltfilt(sos, chunk) + rms = np.sqrt(np.mean(filtered ** 2)) + level = scale_rms(rms) + levels.append(level) + + cmd = [ + '/usr/local/bin/inputmodule-control', + 'led-matrix', + '--eq', + ] + [str(l) for l in levels] + + subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + time.sleep(UPDATE_RATE) + +stream = sd.InputStream( + samplerate=SAMPLE_RATE, + channels=2, + blocksize=CHUNK_SIZE, + callback=audio_callback, + device='default' # or sd.default.device[0] — this follows pactl default source + # No extra_settings needed +) + +update_thread = threading.Thread(target=update_leds, daemon=True) +update_thread.start() + +with stream: + print("Running... Press Ctrl+C to stop.") + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("Stopped.") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6390033..7bf1b76 100755 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,6 @@ pyyaml >= 6.0.3 pyinstaller requests >= 2.32.5 python-iplocate >= 1.0.0 -python-dotenv >= 1.2.1 \ No newline at end of file +python-dotenv >= 1.2.1 +scipy >= 1.17.0 +sounddevice = 0.5.5 \ No newline at end of file From 88de68dd06fbdd58ddcb7cfe506da98115e7ebfb Mon Sep 17 00:00:00 2001 From: "Leone, Mark A [LGS]" Date: Tue, 3 Feb 2026 13:19:35 -0500 Subject: [PATCH 6/6] Remove equalizer files inadvertently committed --- equalizer_files/deps | 5 - equalizer_files/eq-input.sh | 204 ---------------------------- equalizer_files/inputmodule-control | 1 - equalizer_files/visualize.py | 95 ------------- equalizer_files/visualizer.toml | 38 ------ requirements.txt | 4 +- 6 files changed, 1 insertion(+), 346 deletions(-) delete mode 100644 equalizer_files/deps delete mode 100755 equalizer_files/eq-input.sh delete mode 120000 equalizer_files/inputmodule-control delete mode 100644 equalizer_files/visualize.py delete mode 100644 equalizer_files/visualizer.toml diff --git a/equalizer_files/deps b/equalizer_files/deps deleted file mode 100644 index 2785b2c..0000000 --- a/equalizer_files/deps +++ /dev/null @@ -1,5 +0,0 @@ -Ubuntu/Debian -sudo apt install pipewire pipewire-pulse wireplumber pulseaudio-utils - -Fedora -sudo dnf install pipewire pipewire-pulseaudio wireplumber diff --git a/equalizer_files/eq-input.sh b/equalizer_files/eq-input.sh deleted file mode 100755 index 5b18973..0000000 --- a/equalizer_files/eq-input.sh +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ---------------- Config ---------------- -BASE_CMD="inputmodule-control led-matrix" -EQ_CMD="$BASE_CMD --input-eq" -FALLBACK_CMD="$BASE_CMD --random-eq" -CHECK_INTERVAL=1 -EQ_PID="" -CLEANUP_FLAG=0 - -# ---------------- Helpers ---------------- - -log() { echo "[$(date +'%H:%M:%S')] $*"; } - -pactl_ok() { - pactl info >/dev/null 2>&1 -} - -wait_for_pactl() { - for _ in {1..20}; do - pactl_ok && return 0 - sleep 0.25 - done - return 1 -} - -current_sink() { - pactl_ok || return 1 - pactl info | sed -n 's/^Default Sink: //p' -} - -monitor_name_for_sink() { - local sink="$1" - echo "${sink}.monitor" -} - -wait_for_monitor_running() { - local monitor="$1" - for _ in {1..50}; do - # Check if the monitor exists in pactl sources - if pactl list short sources | awk '{print $2}' | grep -qx "$monitor"; then - return 0 - fi - sleep 0.1 - done - return 1 -} - -set_default_source() { - local monitor="$1" - pactl set-default-source "$monitor" || true -} - -sink_type() { - case "$1" in - *headphone*|*Audio_Expansion*) echo "headphones" ;; - *analog-stereo*) echo "speakers" ;; - *bluez*) echo "bluetooth" ;; - *) echo "unknown" ;; - esac -} - -fade_out() { - # Small pause to simulate fade - sleep 0.2 -} - -fade_in() { - sleep 0.2 -} - -notify_sink() { - local type="$1" - log "Notifying sink change: $type" - fade_out - sleep 0.1 - fade_in -} - -start_eq() { - stop_eq - if pactl_ok; then - $EQ_CMD & - else - $FALLBACK_CMD & - fi - EQ_PID=$! -} - -stop_eq() { - # Kill the process we tracked - [[ -n "${EQ_PID:-}" ]] && kill "$EQ_PID" >/dev/null 2>&1 || true - - # Also make sure no stray processes remain - pkill -f "${EQ_CMD}" >/dev/null 2>&1 || true - - # Reset EQ_PID - EQ_PID="" -} - - -start_fallback() { - stop_eq - $FALLBACK_CMD >/dev/null 2>&1 & - EQ_PID=$! -} - -visual_cue() { - local type="$1" - case "$type" in - speakers) - # Show horizontal bar for speakers - ${BASE_CMD} --pattern all-on >/dev/null 2>&1 - ;; - headphones) - # Show vertical bars for headphones - ${BASE_CMD} --pattern zigzag >/dev/null 2>&1 - ;; - bluetooth) - # Unknown source → flash everything briefly - ${BASE_CMD} --pattern gradient >/dev/null 2>&1 - ;; - *) - # Unknown source → flash everything briefly - ${BASE_CMD} --pattern gradient >/dev/null 2>&1 - ;; - esac - - # Keep cue visible briefly - sleep 0.6 -} - - -# ---------------- Cleanup ---------------- - -cleanup() { - if [ $CLEANUP_FLAG -eq 1 ]; then return; fi - CLEANUP_FLAG=1 - log "Restoring original input source: $ORIG_SOURCE" - # stop_eq - if pactl_ok; then - pactl set-default-source "$ORIG_SOURCE" >/dev/null 2>&1 || true - fi -} -trap cleanup EXIT INT TERM - -# ---------------- Startup ---------------- - -ORIG_SOURCE=$(pactl info | sed -n 's/^Default Source: //p') -[[ -z "$ORIG_SOURCE" ]] && log "No original source found" && exit 1 - -log "Original input source: $ORIG_SOURCE" - -CURRENT_SINK="$(current_sink)" -MONITOR="$(monitor_name_for_sink "$CURRENT_SINK")" - -set_default_source "$MONITOR" - -if wait_for_monitor_running "$MONITOR"; then - log "→ Monitor ready: $MONITOR" - # start_eq -else - log "⚠ Monitor not ready, using fallback EQ" - # start_fallback -fi - -# ---------------- Main Loop ---------------- - -pactl subscribe | while read -r line; do - case "$line" in - *"on server"*|*"on sink"*) - NEW_SINK="$(current_sink)" - if [[ "$NEW_SINK" != "$CURRENT_SINK" && -n "$NEW_SINK" ]]; then - log "🔄 Sink change detected → $NEW_SINK" - - # stop_eq - - SINK_TYPE=$(sink_type "$NEW_SINK") - # fade_out - # visual_cue "$SINK_TYPE" - # fade_in - - MONITOR="$(monitor_name_for_sink "$NEW_SINK")" - log "→ Input now follows: $MONITOR" - set_default_source "$MONITOR" - - if wait_for_monitor_running "$MONITOR"; then - # start_eq - CURRENT_SINK="$NEW_SINK" - # else - # log "⚠ Monitor never reached RUNNING/IDLE, using fallback" - # # start_fallback - fi - - # CURRENT_SINK="$NEW_SINK" - fi - ;; - esac -done & -SUB_PID=$! - -# Wait for subscription loop -wait "$SUB_PID" diff --git a/equalizer_files/inputmodule-control b/equalizer_files/inputmodule-control deleted file mode 120000 index 2b56914..0000000 --- a/equalizer_files/inputmodule-control +++ /dev/null @@ -1 +0,0 @@ -/home/mal/git/inputmodule-rs/target/x86_64-unknown-linux-gnu/debug/inputmodule-control \ No newline at end of file diff --git a/equalizer_files/visualize.py b/equalizer_files/visualize.py deleted file mode 100644 index 2092401..0000000 --- a/equalizer_files/visualize.py +++ /dev/null @@ -1,95 +0,0 @@ -import numpy as np -import sounddevice as sd # For audio capture -from scipy.signal import butter, sosfiltfilt # For bandpass filters -import subprocess -import time -import os -import threading - -# Configuration -SAMPLE_RATE = 48000 # Match your system's audio rate (check with `pw-dump | grep default.clock.rate`) -CHUNK_SIZE = 1024 # Audio chunk size (adjust for latency; smaller = more responsive, but higher CPU) -UPDATE_RATE = 0.03 # Seconds between LED updates (e.g., ~33 FPS) - -# 9 frequency bands (octave centers, log-spaced from ~32 Hz to ~16 kHz) -BAND_CENTERS = [31.5, 63, 125, 250, 500, 1000, 2000, 4000, 8000] # Hz -Q = 1.414 # For octave bandwidth; increase for narrower bands - -# Pre-compute bandpass filters (second-order sections for stability) -filters = [] -for fc in BAND_CENTERS: - low = fc / Q - high = fc * Q - sos = butter(4, [low, high], btype='band', fs=SAMPLE_RATE, output='sos') # 4th order Butterworth - filters.append(sos) - -# Scale function: Convert RMS to 0-255 integer for LED height (adjust sensitivity) -def scale_rms(rms, min_db=-60, max_db=0): - db = 20 * np.log10(rms + 1e-10) # dBFS - normalized = np.clip((db - min_db) / (max_db - min_db), 0, 1) - return int(normalized * 34) # Assuming --eq takes 0-34 per column - -# Audio callback: Process chunks in real-time -audio_buffer = np.zeros((CHUNK_SIZE, 2), dtype=np.float32) # Stereo buffer -lock = threading.Lock() - -def audio_callback(indata, frames, time_info, status): - global audio_buffer - with lock: - audio_buffer = indata.copy() # Copy latest chunk - -# Function to compute band levels and update LEDs -USE_EXTERNAL_FILTER = os.environ.get("EXTERNAL_FILTER", "false").lower() == 'true' -def update_leds(): - while True: - with lock: - chunk = audio_buffer.mean(axis=1) # mono - - levels = [] - - if USE_EXTERNAL_FILTER: - # EasyEffects is already applying the 9-band EQ upstream - # → We just measure RMS in the same frequency ranges to get post-EQ bucket levels - for sos in filters: # ← still using the same filter definitions! - filtered = sosfiltfilt(sos, chunk) # Measure energy in this band *after* EasyEffects - rms = np.sqrt(np.mean(filtered ** 2)) - level = scale_rms(rms) - levels.append(level) - - else: - # Python mode: apply the filters ourselves (fixed parameters) - for sos in filters: - filtered = sosfiltfilt(sos, chunk) - rms = np.sqrt(np.mean(filtered ** 2)) - level = scale_rms(rms) - levels.append(level) - - cmd = [ - '/usr/local/bin/inputmodule-control', - 'led-matrix', - '--eq', - ] + [str(l) for l in levels] - - subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - - time.sleep(UPDATE_RATE) - -stream = sd.InputStream( - samplerate=SAMPLE_RATE, - channels=2, - blocksize=CHUNK_SIZE, - callback=audio_callback, - device='default' # or sd.default.device[0] — this follows pactl default source - # No extra_settings needed -) - -update_thread = threading.Thread(target=update_leds, daemon=True) -update_thread.start() - -with stream: - print("Running... Press Ctrl+C to stop.") - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - print("Stopped.") \ No newline at end of file diff --git a/equalizer_files/visualizer.toml b/equalizer_files/visualizer.toml deleted file mode 100644 index a04455e..0000000 --- a/equalizer_files/visualizer.toml +++ /dev/null @@ -1,38 +0,0 @@ -[audio] -rate = 16000 -fourier.downsample = 5 -recorder = "cpal" -buffer = 16000 -read_size = 256 - -# The following settings are curerntly ignored because not implemented in inputmodule-control -[fft] -length = 1024 -window = "hann" - -[equalizer] -columns = 9 -rows = 34 - -# Logarithmic frequency band edges (Hz) -bands = [ - [30, 60], - [60, 120], - [120, 250], - [250, 500], - [500, 1000], - [1000, 2000], - [2000, 4000], - [4000, 8000], - [8000, 16000], -] - -# Normalize energy per band -normalize = true - -# Smooth vertical motion (important on tall matrices) -attack = 0.6 -decay = 0.85 - -# Clamp noise floor -floor = 0.02 diff --git a/requirements.txt b/requirements.txt index 7bf1b76..6390033 100755 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,4 @@ pyyaml >= 6.0.3 pyinstaller requests >= 2.32.5 python-iplocate >= 1.0.0 -python-dotenv >= 1.2.1 -scipy >= 1.17.0 -sounddevice = 0.5.5 \ No newline at end of file +python-dotenv >= 1.2.1 \ No newline at end of file