The anatomy of a fader cap

Overall Geometric Topology & Curvature

  • Base Volume: Begin with a standard rectangular cuboid. The lateral sides (left and right) remain strictly planar and parallel to each other.

  • The Primary Saddle (Concave): The top face features a deep, concave cylindrical boolean subtraction running along the transverse axis. This creates the central “scoop” or saddle where the finger rests. The nadir of this curve sits precisely at the center point of the top plane.

  • The Crests and Downward Trail (Convex): The front and back edges of the concave saddle do not end in sharp corners. Instead, they transition into high-radius convex fillets. These rounded crests roll smoothly downward, transforming the horizontal top surface into the vertical front and back faces. This continuous curve creates a soft, sweeping drop-off on both ends of the longitudinal axis.

  • OLYMPUS DIGITAL CAMERA

     

 

Surface Topography: The Grip Grooves

  • Geometry/Displacement: Embedded entirely within the concave saddle are a series of parallel, shallow horizontal striations.

  • Orientation: These grooves run along the transverse axis (perpendicular to the fader’s sliding direction).

  • Shading Execution: In a PBR workflow, these should be handled via a normal map or displacement map using a subtle sine-wave profile. They are designed to catch grazing light, creating alternating horizontal micro-bands of soft specular highlights and self-shadowing (ambient occlusion) within the deeper scoop.

The Indicator Line Wrap

  • UV Projection: The stark indicator line requires a continuous UV projection mapped along the longitudinal axis.

  • Path of the Wrap: The solid band originates on the flat, lower-front vertical face. It sweeps seamlessly up and over the front convex crest, plunges directly through the center of the ribbed concave saddle (bisecting the horizontal grooves at a perfect 90-degree perpendicular angle), climbs up the rear convex crest, and drops down to terminate on the rear vertical face.

  • Material Contrast: The painted line must have an independent material ID or a mask. It requires a high-albedo, purely diffuse surface (Roughness 0.9+) with zero specularity, ensuring the line remains readable and does not reflect light the way the base plastic does.

Material & PBR Shading Instructions

  • Base Material: Dielectric polymer (injection-molded plastic).

    Index of Refraction (IOR): ~1.45

  • Roughness: Set to medium-low (approx. 0.35). The plastic should exhibit a slight sheen. Specular reflections should be visible but slightly blurred, simulating a micro-texture that prevents a perfect, mirror-like gloss.

  • Specular Highlights: The primary lighting interaction will occur on the front and back convex crests. These tight curves should act as sharp specular catchers, grabbing overhead lights and creating bright, curved horizontal glints that define the upper boundaries of the fader cap.

  • Shadowing: Soft shadowing and heavy ambient occlusion should pool in the central concave depression, contrasting with the highly illuminated convex lips.

Rotary Selector Switch (SelectorSwitch)

Rotary Selector Switch (SelectorSwitch)

The `SelectorSwitch` is a high-fidelity Tkinter Canvas-based widget designed to model discrete multi-position controls. It mimics the behavior of physical rotary switches found on industrial equipment, laboratory instruments, and high-end audio gear.

Continue reading

MDP – Multi Dimensional Panner

MDP – Multi Dimensional Panner

Demo: https://like.audio/MDP/

## Overview

The **Multi-Dimensional Panner (MDP)** is an advanced user interface concept designed for spatial audio mixing, object-based panning (e.g., Dolby Atmos), and complex parameter control. It extends the traditional “Linear Travelling Potentiometer” (LTP) by placing it within a free-floating, rotatable widget on a 2D plane.

Continue reading

CMDP: Circular Motion Displacement Potentiometer

CMDP: Circular Motion Displacement Potentiometer

DEMO: http://like.audio/CMDP

# CMDP: Circular Motion Displacement Potentiometer

Overview
The **Circular Motion Displacement Potentiometer (CMDP)** is a novel user interface concept designed for spatial audio mixing, microphone array management, and multidimensional sound control. It combines the precision of linear faders with the intuitive spatial organization of a polar coordinate system, allowing users to visualize and manipulate sound sources in a 360-degree field.

 

Continue reading

The Great Un-Boxing: Audio’s Transition from Signal to State

The Great Un-Boxing: Audio’s Transition from Signal to State

For decades, the broadcast world was defined by physics. We built facilities based on the “Box Theory”: distinct, dedicated hardware units connected by copper. The workflow was linear and tangible. If you wanted to process a signal, you pushed it out of one box, down a wire, and into another. The cable was the truth; if the patch was made, the audio flowed.

Today, we are witnessing the dissolution of the box.

The industry is currently navigating a violent shift from Signal Flow to Data Orchestration. In this new paradigm, the “box” is often a skeuomorphic illusion—a user interface designed to comfort us while the real work happens in the abstract.

From Pushing to Sharing

The fundamental difference lies in how information moves. In the hardware world, we “pushed” signals. Source A drove a current to Destination B. It was active and directional.

In the software world of IP and virtualization, we do not push; we share. The modern audio engine is effectively a system of memory management. One process writes audio data to a shared block of memory (a ring buffer), and another process reads it. The “wire” has been replaced by a memory pointer. We are no longer limited by the number of physical ports on a chassis, but by the read/write speed of RAM and the efficiency of the CPU.

The Asynchronous Challenge

This transition forces us to confront the chaos of computing. Hardware audio is isochronous—it flows at a perfectly locked heartbeat (48kHz). Software and cloud infrastructure are inherently asynchronous. Packets arrive in bursts; CPUs pause to handle background tasks; networks jitter.

The modern broadcast engineer’s challenge is no longer just “routing audio.” It is artificially forcing non-deterministic systems (clouds, servers, VMs) to behave with the deterministic precision of a copper wire. We are trading voltage drops for buffer underruns.

The “Point Z” Architecture

Perhaps the most radical shift is in topology. The line from Point A (Microphone) to Point B (Speaker) is no longer straight.

We are moving toward a “Point A → Cloud → Point Z → Point B” architecture. The “interface layer” is now a complex orchestration of logic that hops between cloud providers, containers, and edge devices before ever returning to the listener’s ear. The signal might traverse three different data centers to undergo AI processing or localized insertion, creating a web of dependencies that “Box Thinking” can never fully map.

The era of the soldering iron is giving way to the era of the stack. We are no longer building chains of hardware; we are architecting systems of logic. The broadcast facility of the future isn’t a room full of racks—it is a negotiated agreement between asynchronous services, sharing memory in the dark.

Rust Headless 96kHz Audio Console

Architecting a Scalable, Headless Audio Console in Rust

In the world of professional audio—spanning broadcast, cinema, and large-scale live events—the mixing console is the heart of the operation. Traditionally, these have been massive hardware monoliths. Today, however, the industry is shifting toward headless, scalable audio engines that run on standard server hardware, controlled remotely by software endpoints.

This article proposes the architecture for Titan-96k, a scalable, 32-bit floating-point audio mixing engine written in Rust. It is designed to handle everything from a simple podcast setup to complex 7.1.4 immersive audio workflows, controlled entirely via MQTT.

Continue reading

Linear Traveling Potentiometer – Software

February is Toronto AES Audio Engineering Society Member showcase….

The **Linear Travelling Potentiometer (LTP)** is a hybrid user interface widget designed to save screen real estate while maximizing control density. It integrates a rotary knob directly onto the cap of a linear fader, allowing simultaneous control of two related parameters (e.g., Level and Pan, or Send Level and Send Pan) in a single compact footprint.

Back in 2013, I presented a concept at the AES Toronto meeting called the “Linear Traveling Potentiometer” (LTP).

demo: https://like.audio/LTP/

The idea was simple but mechanically complex: Combine a linear fader and a rotary potentiometer into a single, fluid control. Two motions, one component.

I even had a prototype in a “black bag” that I let people feel without seeing. The goal was to control intensity (volume) and position (pan) simultaneously—a single-point coordinate system for surround sound and spatial audio.
For years, this existed mostly as hardware prototypes and sketches. But the vision never went away.

Now, nearly 15 years later, I have finally recreated my vision purely in software. Click, grab it like a fader… Move up for volume and sideways to pan…

I’ve brought the “Two in One” concept to life digitally. No moving parts, just the physics of the original idea translated into code.

It’s been a long road from that first presentation to this software build. Sometimes the technology just needs to catch up to the idea.



 

# builder_audio/dynamic_gui_create_custom_LTP.py
#
# A Linear Traveling Potentiometer (LTP) widget.
# Acts as a vertical fader, but the cap is a rotatable knob.
# Control + Drag rotates the knob (-100 to 100).
# Standard Drag moves the fader vertically.
# “Freestyle” mode allows adjusting both axes simultaneously (vertical=fader, horizontal=rotation).
#
# Author: Anthony Peter Kuzub
# Blog: www.Like.audio (Contributor to this project)
#
# Professional services for customizing and tailoring this software to your specific
# application can be negotiated. There is no charge to use, modify, or fork this software.
#
# Build Log: https://like.audio/category/software/spectrum-scanner/
# Source Code: https://github.com/APKaudio/
# Feature Requests can be emailed to i @ like . audio
#
# Version 20250821.200641.1
import tkinter as tk
from tkinter import ttk
import math
from managers.configini.config_reader import Config
app_constants = Config.get_instance()
# — Default Configuration Constants —
DEFAULT_LTP_WIDTH = 100
DEFAULT_MIN_VAL = 0.0
DEFAULT_MAX_VAL = 100.0
DEFAULT_LOG_EXPONENT = 1.0
DEFAULT_BORDER_WIDTH = 0
DEFAULT_BORDER_COLOR = “black”
DEFAULT_TICK_SIZE_RATIO = 0.2
DEFAULT_TICK_FONT_FAMILY = “Helvetica”
DEFAULT_TICK_FONT_SIZE = 10
DEFAULT_TICK_COLOR = “light grey”
DEFAULT_VALUE_FOLLOW = True
DEFAULT_VALUE_HIGHLIGHT_COLOR = “#f4902c”
DEFAULT_CAP_RADIUS = 18 # Radius of the knob cap (Increased by 20% from 15)
ROTATION_MIN = 100.0
ROTATION_MAX = 100.0
# ———————————————
from workers.logger.logger import debug_logger
from workers.logger.log_utils import _get_log_args
from workers.styling.style import THEMES, DEFAULT_THEME
from workers.handlers.widget_event_binder import bind_variable_trace
class CustomLTPFrame(tk.Frame):
def __init__(
self,
master,
config,
path,
state_mirror_engine,
base_mqtt_topic,
subscriber_router,
):
colors = THEMES.get(DEFAULT_THEME, THEMES[“dark”])
fader_style = colors.get(“fader_style”, {})
self.bg_color = colors.get(“bg”, “#2b2b2b”)
self.accent_color = colors.get(“accent”, “#33A1FD”)
self.neutral_color = colors.get(“neutral”, “#dcdcdc”)
self.track_col = colors.get(“secondary”, “#444444”)
self.handle_col = colors.get(“fg”, “#dcdcdc”)
self.text_col = colors.get(“fg”, “#dcdcdc”)
self.min_val = float(config.get(“value_min”, DEFAULT_MIN_VAL))
self.max_val = float(config.get(“value_max”, DEFAULT_MAX_VAL))
self.log_exponent = float(config.get(“log_exponent”, DEFAULT_LOG_EXPONENT))
self.reff_point = float(
config.get(“reff_point”, (self.min_val + self.max_val) / 2.0)
)
self.border_width = int(config.get(“border_width”, DEFAULT_BORDER_WIDTH))
self.border_color = config.get(“border_color”, DEFAULT_BORDER_COLOR)
# Cap Styling
self.cap_radius = int(config.get(“cap_radius”, DEFAULT_CAP_RADIUS))
self.cap_color = config.get(“cap_color”, self.handle_col)
self.cap_outline_color = config.get(“cap_outline_color”, self.track_col)
# Custom styling
self.tick_size = config.get(“tick_size”, fader_style.get(“tick_size”, DEFAULT_TICK_SIZE_RATIO))
tick_font_family = config.get(“tick_font_family”, fader_style.get(“tick_font_family”, DEFAULT_TICK_FONT_FAMILY))
tick_font_size = config.get(“tick_font_size”, fader_style.get(“tick_font_size”, DEFAULT_TICK_FONT_SIZE))
self.tick_font = (tick_font_family, tick_font_size)
self.tick_color = config.get(“tick_color”, fader_style.get(“tick_color”, DEFAULT_TICK_COLOR))
self.value_follow = config.get(“value_follow”, fader_style.get(“value_follow”, DEFAULT_VALUE_FOLLOW))
self.value_highlight_color = config.get(“value_highlight_color”, fader_style.get(“value_highlight_color”, DEFAULT_VALUE_HIGHLIGHT_COLOR))
self.value_color = config.get(“value_color”, self.text_col)
super().__init__(
master,
bg=self.bg_color,
bd=self.border_width,
relief=“solid”,
highlightbackground=self.border_color,
highlightthickness=self.border_width,
)
self.path = path
self.state_mirror_engine = state_mirror_engine
self.base_mqtt_topic = base_mqtt_topic
self.subscriber_router = subscriber_router
self.config = config
self.freestyle = config.get(“freestyle”, False)
# Initialize Variables
# Linear Value (Standard Fader)
self.linear_var = tk.DoubleVar(value=float(config.get(“value_default”, (self.min_val + self.max_val)/2)))
# Rotation Value (Knob)
self.rotation_var = tk.DoubleVar(value=float(config.get(“rotation_default”, 0.0)))
self.temp_entry = None
# Register Widgets with State Mirror Engine
if self.state_mirror_engine and self.path:
# Register main linear variable
self._register_sub_widget(“linear”, self.linear_var)
# Register rotation variable
self._register_sub_widget(“rotation”, self.rotation_var)
# Bind traces for Redraw
self.linear_var.trace_add(“write”, self._request_redraw)
self.rotation_var.trace_add(“write”, self._request_redraw)
def _register_sub_widget(self, suffix, variable):
if suffix == “linear”:
target_path = self.path
else:
target_path = f{self.path}/{suffix}
sub_config = self.config.copy()
sub_config[“path”] = target_path
if suffix == “rotation”:
sub_config[“value_min”] = ROTATION_MIN
sub_config[“value_max”] = ROTATION_MAX
self.state_mirror_engine.register_widget(target_path, variable, self.base_mqtt_topic, sub_config)
callback = lambda: self.state_mirror_engine.broadcast_gui_change_to_mqtt(target_path)
bind_variable_trace(variable, callback)
topic = self.state_mirror_engine.get_widget_topic(target_path)
if topic:
self.subscriber_router.subscribe_to_topic(topic, self.state_mirror_engine.sync_incoming_mqtt_to_gui)
self.state_mirror_engine.initialize_widget_state(target_path)
def _request_redraw(self, *args):
self.event_generate(“<<RedrawLTP>>”)
def _open_manual_entry(self, event, target_var, min_v, max_v):
if self.temp_entry and self.temp_entry.winfo_exists():
return
self.temp_entry = tk.Entry(self, width=8, justify=“center”)
self.temp_entry.place(x=event.x 20, y=event.y 10)
current_val = target_var.get()
self.temp_entry.insert(0, str(current_val))
self.temp_entry.select_range(0, tk.END)
self.temp_entry.focus_set()
submit_cmd = lambda e: self._submit_manual_entry(e, target_var, min_v, max_v)
self.temp_entry.bind(“<Return>”, submit_cmd)
self.temp_entry.bind(“<FocusOut>”, submit_cmd)
self.temp_entry.bind(“<Escape>”, self._destroy_manual_entry)
def _submit_manual_entry(self, event, target_var, min_v, max_v):
raw_value = self.temp_entry.get()
try:
new_value = float(raw_value)
if min_v <= new_value <= max_v:
target_var.set(new_value)
else:
if app_constants.global_settings[“debug_enabled”]:
debug_logger(
message=f“⚠️ Value {new_value} out of bounds! Ignoring.”,
**_get_log_args(),
)
except ValueError:
pass
self._destroy_manual_entry(event)
def _destroy_manual_entry(self, event):
if self.temp_entry and self.temp_entry.winfo_exists():
self.temp_entry.destroy()
self.temp_entry = None
class CustomLTPCreatorMixin:
def _create_custom_ltp(self, parent_widget, config_data, **kwargs):
label = config_data.get(“label_active”)
config = config_data
path = config_data.get(“path”)
layout_config = config.get(“layout”, {})
font_size = layout_config.get(“font”, 10)
custom_font = (“Helvetica”, font_size)
custom_colour = layout_config.get(“colour”, None)
state_mirror_engine = self.state_mirror_engine
subscriber_router = self.subscriber_router
base_mqtt_topic_from_path = kwargs.get(“base_mqtt_topic_from_path”)
colors = THEMES.get(DEFAULT_THEME, THEMES[“dark”])
bg_color = colors.get(“bg”, “#2b2b2b”)
secondary_color = colors.get(“secondary”, “#444444”)
frame = CustomLTPFrame(
parent_widget,
config=config,
path=path,
state_mirror_engine=state_mirror_engine,
base_mqtt_topic=base_mqtt_topic_from_path,
subscriber_router=subscriber_router,
)
if label:
lbl = tk.Label(frame, text=label, font=custom_font, background=bg_color, foreground=colors.get(“fg”, “#dcdcdc”))
if custom_colour:
lbl.configure(foreground=custom_colour)
lbl.pack(side=tk.TOP, pady=(0, 5))
width = layout_config.get(“width”, DEFAULT_LTP_WIDTH)
height = layout_config.get(“height”, 300)
canvas = tk.Canvas(
frame, width=width, height=height, bg=bg_color, highlightthickness=0
)
canvas.pack(fill=tk.BOTH, expand=True)
canvas.update_idletasks()
# Visual State
visual_props = {“secondary”: secondary_color}
hover_color = “#999999”
# Interaction State
drag_state = {“start_x”: 0, “start_y”: 0, “start_val_lin”: 0, “start_val_rot”: 0, “active”: False}
def on_press(event):
drag_state[“active”] = True
drag_state[“start_x”] = event.x
drag_state[“start_y”] = event.y
drag_state[“start_val_lin”] = frame.linear_var.get()
drag_state[“start_val_rot”] = frame.rotation_var.get()
if not (event.state & 0x0004): # No Ctrl
update_linear_from_y(event.y)
def on_drag(event):
if not drag_state[“active”]:
return
is_ctrl = event.state & 0x0004
if frame.freestyle:
# Vertical adjusts fader, Horizontal adjusts rotation
update_linear_from_y(event.y)
update_rotation_from_x(event.x)
elif is_ctrl:
# Rotation Mode (Vertical drag for rotation)
dy = drag_state[“start_y”] event.y
sensitivity = 2.0
new_rot = drag_state[“start_val_rot”] + (dy * sensitivity)
new_rot = max(ROTATION_MIN, min(ROTATION_MAX, new_rot))
frame.rotation_var.set(new_rot)
else:
# Linear Mode
update_linear_from_y(event.y)
def on_release(event):
drag_state[“active”] = False
def update_linear_from_y(y):
h = canvas.winfo_height()
norm_y = (y 20) / (h 40)
norm_y = 1.0 max(0.0, min(1.0, norm_y))
log_norm_pos = norm_y**frame.log_exponent
current_value = frame.min_val + log_norm_pos * (
frame.max_val frame.min_val
)
frame.linear_var.set(current_value)
def update_rotation_from_x(x):
w = canvas.winfo_width()
# Rotation area same length as fader height (h-40)
# Center it at the current rail? Or just relative to the whole canvas width.
# User said “area to move it left or right should be about the same length as the fader”
h = canvas.winfo_height()
fader_len = h 40
# Start rotation from center of canvas
cx = w / 2
# Offset from center
dx = x cx
# Map dx to rotation value. fader_len total width for full rotation range?
# So range is [-fader_len/2, fader_len/2]
norm_x = dx / (fader_len / 2.0)
norm_x = max(1.0, min(1.0, norm_x))
new_rot = norm_x * 100.0 # ROTATION_MAX
frame.rotation_var.set(new_rot)
def on_alt_click(event):
frame._open_manual_entry(event, frame.linear_var, frame.min_val, frame.max_val)
def redraw(*args):
current_w = canvas.winfo_width()
current_h = canvas.winfo_height()
if current_w <= 1: current_w = width
if current_h <= 1: current_h = height
_draw_ltp_vertical(
frame,
canvas,
current_w,
current_h,
visual_props[“secondary”]
)
frame.bind(“<<RedrawLTP>>”, redraw)
# Initial Draw
redraw()
# Bindings
canvas.bind(“<Button-1>”, on_press)
canvas.bind(“<B1-Motion>”, on_drag)
canvas.bind(“<ButtonRelease-1>”, on_release)
canvas.bind(“<Alt-Button-1>”, on_alt_click)
canvas.bind(“<Configure>”, lambda e: redraw())
def on_enter(event):
visual_props[“secondary”] = hover_color
redraw()
def on_leave(event):
visual_props[“secondary”] = secondary_color
redraw()
canvas.bind(“<Enter>”, on_enter)
canvas.bind(“<Leave>”, on_leave)
return frame
def _draw_ltp_vertical(frame, canvas, width, height, current_secondary):
canvas.delete(“all”)
cx = width / 2
# 1. Track Line
canvas.create_line(
cx, 20, cx, height 20,
fill=current_secondary, width=4, capstyle=tk.ROUND
)
# 2. Calculate Handle Position (Linear)
value_lin = frame.linear_var.get()
norm_value = (
(value_lin frame.min_val)
/ (frame.max_val frame.min_val)
if (frame.max_val frame.min_val) != 0
else 0
)
norm_value = max(0.0, min(1.0, norm_value))
display_norm_pos = norm_value ** (1.0 / frame.log_exponent)
handle_y = (height 40) * (1.0 display_norm_pos) + 20
# 3. Fill Line (from bottom to handle)
canvas.create_line(
cx + 2.5, height 20, cx + 2.5, handle_y,
fill=frame.value_highlight_color, width=5, capstyle=tk.ROUND
)
# 4. Draw Rotatable Cap (Knob)
rot_val = frame.rotation_var.get()
# 0 val = -90 degrees (Up)
angle_deg = 90 + (rot_val / 100.0) * 135.0
angle_rad = math.radians(angle_deg)
radius = frame.cap_radius
# Cap Circle
canvas.create_oval(
cx radius, handle_y radius,
cx + radius, handle_y + radius,
fill=frame.cap_color, outline=frame.cap_outline_color, width=2
)
# Pointer Line
pointer_len = radius * 0.8
px = cx + pointer_len * math.cos(angle_rad)
py = handle_y + pointer_len * math.sin(angle_rad)
canvas.create_line(
cx, handle_y, px, py,
fill=frame.cap_outline_color, width=2, capstyle=tk.ROUND
)
# Values Text
if frame.value_follow:
# Linear Value to the right
canvas.create_text(
cx + radius + 10,
handle_y,
text=f{value_lin:.1f},
fill=frame.value_color,
anchor=“w”,
font=(“Helvetica”, 8)
)
# Rotation Value to the left
canvas.create_text(
cx radius 10,
handle_y,
text=f“R:{rot_val:.0f},
fill=frame.value_color,
anchor=“e”,
font=(“Helvetica”, 8)
)

## Core Concepts

### 1. Hybrid Control
* **Linear Fader (Y-Axis):** The vertical position of the cap controls the primary value (typically Volume, Level, or Depth).
* **Rotary Knob (Rotation):** A knob embedded in the fader cap controls a secondary value (typically Pan, Param, or Intensity).
* **Unified Interaction:** Both controls are accessible from the same visual element, reducing mouse travel and UI clutter.

### 2. Interaction Modes
The LTP supports distinct interaction modes to prevent accidental changes:

* **Standard Mode:**
* **Drag Handle:** Adjusts Linear Value only.
* **Alt/Option + Drag:** Adjusts Rotary Value only (Linear position is locked).
* **Scroll:** Fine-tune Linear Value.
* **Alt/Option + Scroll:** Fine-tune Rotary Value.

* **Freestyle Mode:**
* Dragging the handle adjusts **BOTH** Linear and Rotary values simultaneously based on 2D mouse movement. This allows for gestural control (e.g., “throwing” a sound into a corner).

* **Pan Latch:**
* **Double-Click** the cap to engage “Pan Latch”.
* In this state, horizontal mouse movement adjusts the Rotary value without needing to hold a modifier key.
* Click or drag again to disengage.

### 3. Visual Feedback
* **Linear:** Position of the cap along the vertical track.
* **Rotary:** Orientation of the indicator line on the cap.
* **Active State:** The knob glows (default blue/orange) when Rotary control is active (via modifier, latch, or freestyle mode).
* **Pointer:** When adjusting rotation, the indicator line extends (10x length) to provide precise visual feedback, then retracts on release.

## Use Cases

* **Channel Strips:** Volume (Linear) + Pan (Rotary).
* **Effect Sends:** Send Level (Linear) + Pre/Post Toggle or Send Pan (Rotary).
* **Synthesizers:** Cutoff (Linear) + Resonance (Rotary).
* **Spatial Audio:** Distance (Linear) + Azimuth (Rotary – mapped to circular motion).

## Implementations

### HTML5 Demo (`index.html`)
A standalone web-based demonstration using HTML5 Canvas.
* **Features:** Multi-touch support (1 finger slide, 2 finger twist/pan), keyboard modifiers, responsive layout.
* **Theme:** Dark mode with high-contrast UI.

## The Open Concept License

This project is released under **The Open Concept License**.

* **Freedom:** You are free to use, modify, and distribute this work.
* **Attribution:** Explicit credit to **Anthony Kuzub** must be given in all derivative works.
* **Nomenclature:** Implementations must strictly use the terms **LTP (Linear Travelling Potentiometer)**, **GCA (Ganged Controlled Array)**, and **MDP (Multi-Dimensional Panner)** where applicable.
* **Warranty:** Provided “As Is” without warranty.

*(See the full license text in the application footer or source code.)*

 

Optimizing Data Acquisition: The Architecture of GET, SET, RIG, and NAB

High-Throughput Instrument Control Protocol

In the world of instrument automation (GPIB, VISA, TCP/IP), the primary bottleneck is rarely bandwidth—it is latency. Every command sent to a device initiates a handshake protocol that incurs a time penalty. When managing complex systems with hundreds of data points, these penalties accumulate, resulting in “bus chatter” that freezes the UI and blocks other processes.

Continue reading

The Mixer, My Grandfather, and the Looming Crisis of Unfixable Electronics

💡 The Mixer, My Grandfather, and the Looming Crisis of Unfixable Electronics

My weekend project—a powered mixer for a friend—was a powerful, hands-on lesson in the changing nature of electronics and the fight for the Right to Repair.

For a friend, I made an exception to my usual “no bench work” rule. The diagnosis was classic: a blown channel, likely from speakers incorrectly wired in parallel. Instead of a minimal patch job, I opted for a full refurbishment, the way I was taught: new, high-quality Panasonic FC caps and fresh, matched transistors. A labour of love, not profit. Continue reading

Immersive audio demonstration recordings

From Artist’s Intent to Technician’s Choice

In a world full of immersive buzzwords and increasingly complex production techniques, the recording artist’s original intentions can quickly become filtered through the lens of the technician’s execution.

I’ve been thinking about this a lot recently. I just acquired something that powerfully inspired my career in music—a piece of music heard the way it was truly intended before we fully grasped how to record and mix effectively in stereo. It was raw, immediate, and utterly captivating.

I feel we’re in a similar transition zone right now with immersive content production. We’re in the “stereo demo” phase of this new sonic dimension. We’re still learning the rules, and sometimes, the sheer capability of the technology overshadows the artistic purpose. The power of immersive sound shouldn’t just be about where we can place a sound, but where the story or the emotion demands it.

It brings me back to the core inspiration.

Putting the Mechanics into Quantum Mechanics

As we explore the frontier of quantum computing, we’re not just grappling with abstract concepts like superposition and entanglement—we’re engineering systems that manipulate light, matter, and energy at their most fundamental levels. In many ways, this feels like a return to analog principles, where computation is continuous rather than discrete.

A Return to Analog Thinking

Continue reading

Rescuing Your Old Tapes: A Guide to Cassette Tape Restoration

Rescuing Your Old Tapes: A Guide to Cassette Tape Restoration

For those with treasured audio recordings on old cassette tapes from the 1970s and 80s, discovering they no longer play correctly can be heartbreaking. A common issue is the tape slipping and dragging, which can manifest as a screeching sound or simply an inability to move past the capstan. This frustrating problem is often a symptom of a condition known as “sticky-shed syndrome”, and fortunately, it’s one that can be fixed. 

Understanding Sticky-Shed Syndrome

Continue reading

Open Air – Zone Awareness Processor

Creating a memorable logo? Here are a few key tips I’ve found helpful:

Iteration is Key: Don’t expect perfection on the first try. Explore multiple concepts and refine the strongest ones. Each version teaches you something!

 

“Jam” on Ideas: Brainstorm freely! No idea is a bad idea in the initial stages. Let your creativity flow and see what unexpected directions you can take.

Fail Faster: the more iterations that aren’t it, get you close to it.

Specificity Matters: The more specific you are about a brand’s essence, values, and target audience, the better your logo will represent you. Clearly define what you want to communicate visually.

What are your go-to tips for logo design? Share them in the comments! #logodesign #branding #designthinking #visualidentity #AI

Continue reading

Python spectrum analyzer to CSV extract for Agilent N9340B

import pyvisa
import time
import csv
from datetime import datetime
import os
import argparse
import sys

# ------------------------------------------------------------------------------
# Command-line argument parsing
# This section defines and parses command-line arguments, allowing users to
# customize the scan parameters (filename, frequency range, step size) when
# running the script.
# ------------------------------------------------------------------------------
parser = argparse.ArgumentParser(description="Spectrum Analyzer Sweep and CSV Export")

# Define an argument for the prefix of the output CSV filename
parser.add_argument('--SCANname', type=str, default="25kz scan ",
                    help='Prefix for the output CSV filename')

# Define an argument for the start frequency
parser.add_argument('--startFreq', type=float, default=400e6,
                    help='Start frequency in Hz')

# Define an argument for the end frequency
parser.add_argument('--endFreq', type=float, default=650e6,
                    help='End frequency in Hz')

# Define an argument for the step size
parser.add_argument('--stepSize', type=float, default=25000,
                    help='Step size in Hz')
                    
# Add an argument to choose who is running the program (apk or zap)
parser.add_argument('--user', type=str, choices=['apk', 'zap'], default='zap',
                    help='Specify who is running the program: "apk" or "zap". Default is "zap".')


# Parse the arguments provided by the user
args = parser.parse_args()

# Assign parsed arguments to variables for easy access
file_prefix = args.SCANname
start_freq = args.startFreq
end_freq = args.endFreq
step = args.stepSize
user_running = args.user

# Define the waiting time in seconds
WAIT_TIME_SECONDS = 300 # 5 minutes

# ------------------------------------------------------------------------------
# Main program loop
# The entire scanning process will now run continuously with a delay.
# ------------------------------------------------------------------------------
while True:
    # --------------------------------------------------------------------------
    # VISA connection setup
    # This section establishes communication with the spectrum analyzer using the
    # PyVISA library, opens the specified instrument resource, and performs initial
    # configuration commands.
    # --------------------------------------------------------------------------
    # Define the VISA address of the spectrum analyzer. This typically identifies
    # the instrument on the bus (e.g., USB, LAN, GPIB).
    # Define the VISA address of the spectrum analyzer. This typically identifies
    # the instrument on the bus (e.g., USB, LAN, GPIB).
    apk_visa_address = 'USB0::0x0957::0xFFEF::CN03480580::0::INSTR'
    zap_visa_address = 'USB1::0x0957::0xFFEF::SG05300002::0::INSTR'
    
    if user_running == 'apk':
        visa_address = apk_visa_address
    else:  # default is 'zap'
        visa_address = zap_visa_address

    # Create a ResourceManager object, which is the entry point for PyVISA.
    rm = pyvisa.ResourceManager()

    try:
        # Open the connection to the specified instrument resource.
        inst = rm.open_resource(visa_address)
        print(f"Connected to instrument at {visa_address}")

        # Clear the instrument's status byte and error queue.
        inst.write("*CLS")
        # Reset the instrument to its default settings.
        inst.write("*RST")
        # Query the Operation Complete (OPC) bit to ensure the previous commands have
        # finished executing before proceeding. This is important for synchronization.
        inst.query("*OPC?")


        inst.write(":POWer:GAIN ON")
        print("Preamplifier turned ON.")
        inst.write(":POWer:GAIN 1") # '1' is equivalent to 'ON'
        print("Preamplifier turned ON for high sensitivity.")


        # Configure the display: Set Y-axis scale to logarithmic (dBm).
        inst.write(":DISP:WIND:TRAC:Y:SCAL LOG")
        # Configure the display: Set the reference level for the Y-axis.
        inst.write(":DISP:WIND:TRAC:Y:RLEV -30")
        # Enable Marker 1. Markers are used to read values at specific frequencies.
        inst.write(":CALC:MARK1 ON")
        # Set Marker 1 mode to position, meaning it can be moved to a specific frequency.
        inst.write(":CALC:MARK1:MODE POS")
        # Activate Marker 1, making it ready for use.
        inst.write(":CALC:MARK1:ACT")

        # Set the instrument to single sweep mode.
        # This ensures that after each :INIT:IMM command, the instrument performs one
        # sweep and then holds the trace data until another sweep is initiated.
        inst.write(":INITiate:CONTinuous OFF")

        # Pause execution for 2 seconds to allow the instrument to settle after configuration.
        time.sleep(2)

        # --------------------------------------------------------------------------
        # File & directory setup
        # This section prepares the output directory and generates a unique filename
        # for the CSV export based on the current timestamp and user-defined prefix.
        # --------------------------------------------------------------------------
        # Define the directory where scan results will be saved.
        # It creates a subdirectory named "N9340 Scans" in the current working directory.
        scan_dir = os.path.join(os.getcwd(), "N9340 Scans")
        # Create the directory if it doesn't already exist. `exist_ok=True` prevents
        # an error if the directory already exists.
        os.makedirs(scan_dir, exist_ok=True)

        # Generate a timestamp for the filename to ensure uniqueness.
        timestamp = datetime.now().strftime("%Y%m%d_%H-%M-%S")
        # Construct the full path for the output CSV file.
        filename = os.path.join(scan_dir, f"{file_prefix}--{timestamp}.csv")

        # --------------------------------------------------------------------------
        # Sweep and write to CSV
        # This is the core logic of the script, performing the frequency sweep in
        # segments, reading data from the spectrum analyzer, and writing it to the CSV.
        # --------------------------------------------------------------------------
        # Define the width of each frequency segment for sweeping.
        # Sweeping in segments helps manage memory and performance on some instruments.
        segment_width = 10_000_000  # 10 MHz

        # Convert step size to integer, as some instrument commands might expect integers.
        step_int = int(step)
        # Convert end frequency to integer, for consistent comparison in loops.
        scan_limit = int(end_freq)

        # Open the CSV file in write mode (`'w'`). `newline=''` prevents extra blank rows.
        with open(filename, mode='w', newline='') as csvfile:
            # Create a CSV writer object.
            writer = csv.writer(csvfile)
            # Initialize the start of the current frequency block.
            current_block_start = int(start_freq)

            # Loop through frequency blocks until the end frequency is reached.
            while current_block_start < scan_limit:
                # Calculate the end frequency for the current block.
                current_block_stop = current_block_start + segment_width
                # Ensure the block stop doesn't exceed the overall scan limit.
                if current_block_stop > scan_limit:
                    current_block_stop = scan_limit

                # Print the current sweep range to the console for user feedback.
                print(f"Sweeping range {current_block_start / 1e6:.3f} to {current_block_stop / 1e6:.3f} MHz")

                # Set the start frequency for the instrument's sweep.
                inst.write(f":FREQ:START {current_block_start}")
                # Set the stop frequency for the instrument's sweep.
                inst.write(f":FREQ:STOP {current_block_stop}")
                # Initiate a single immediate sweep.
                inst.write(":INIT:IMM")
                # Query Operation Complete to ensure the sweep has finished before reading markers.
                # This replaces the fixed time.sleep(2) for more robust synchronization.
                inst.query("*OPC?")

                # Initialize the current frequency for data point collection within the block.
                current_freq = current_block_start
                # Loop through each frequency step within the current block.
                while current_freq <= current_block_stop:
                    # Set Marker 1 to the current frequency.
                    inst.write(f":CALC:MARK1:X {current_freq}")
                    # Query the Y-axis value (level in dBm) at Marker 1's position.
                    # .strip() removes any leading/trailing whitespace or newline characters.
                    level_raw = inst.query(":CALC:MARK1:Y?").strip()

                    try:
                        # Attempt to convert the raw level string to a float.
                        level = float(level_raw)
                        # Format the level to one decimal place for consistent output.
                        level_formatted = f"{level:.1f}"
                        # Convert frequency from Hz to MHz for readability.
                        freq_mhz = current_freq / 1_000_000
                        # Print the frequency and level to the console.
                        print(f"{freq_mhz:.3f} MHz : {level_formatted} dBm")
                        # Write the frequency and formatted level to the CSV file.
                        writer.writerow([freq_mhz, level_formatted])

                    except ValueError:
                        # If the raw level cannot be converted to a float (e.g., if it's an error message),
                        # use the raw string directly.
                        level_formatted = level_raw
                        # Optionally, you might want to log this error or write a placeholder.
                        print(f"Warning: Could not parse level '{level_raw}' at {current_freq / 1e6:.3f} MHz")
                        writer.writerow([current_freq / 1_000_000, level_formatted])

                    # Increment the current frequency by the step size.
                    current_freq += step_int

                # Move to the start of the next block.
                current_block_start = current_block_stop

    except pyvisa.VisaIOError as e:
        print(f"VISA Error: Could not connect to or communicate with the instrument: {e}")
        print("Please ensure the instrument is connected and the VISA address is correct.")
        # Decide if you want to exit or retry after a connection error
        # For now, it will proceed to the wait and then try again.
    except Exception as e:
        print(f"An unexpected error occurred during the scan: {e}")
        # Continue to the wait or exit if the error is critical
    finally:
        # ----------------------------------------------------------------------
        # Cleanup
        # This section ensures that the instrument is returned to a safe state and
        # the VISA connection is properly closed after the scan is complete.
        # ----------------------------------------------------------------------
        if 'inst' in locals() and inst.session != 0: # Check if inst object exists and is not closed
            try:
                # Attempt to send the instrument to local control.
                inst.write("SYST:LOC")
            except pyvisa.VisaIOError:
                pass # Ignore if command is not supported or connection is already broken
            finally:
                inst.close()
                print("Instrument connection closed.")
        
        # Print a confirmation message indicating the scan completion and output file.
        if 'filename' in locals(): # Only print if filename was successfully created
            print(f"\nScan complete. Results saved to '{filename}'")

    # --------------------------------------------------------------------------
    # Countdown and Interruptible Wait
    # --------------------------------------------------------------------------
    print("\n" + "="*50)
    print(f"Next scan in {WAIT_TIME_SECONDS // 60} minutes.")
    print("Press Ctrl+C at any time during the countdown to interact.")
    print("="*50)

    seconds_remaining = WAIT_TIME_SECONDS
    skip_wait = False

    while seconds_remaining > 0:
        minutes = seconds_remaining // 60
        seconds = seconds_remaining % 60
        # Print countdown, overwriting the same line
        sys.stdout.write(f"\rTime until next scan: {minutes:02d}:{seconds:02d} ")
        sys.stdout.flush() # Ensure the output is immediately written to the console

        try:
            time.sleep(1)
        except KeyboardInterrupt:
            sys.stdout.write("\n") # Move to a new line after Ctrl+C
            sys.stdout.flush()
            choice = input("Countdown interrupted. (S)kip wait, (Q)uit program, or (R)esume countdown? ").strip().lower()
            if choice == 's':
                skip_wait = True
                print("Skipping remaining wait time. Starting next scan shortly...")
                break # Exit the countdown loop
            elif choice == 'q':
                print("Exiting program.")
                sys.exit(0) # Exit the entire script
            else:
                print("Resuming countdown...")
                # Continue the loop from where it left off

        seconds_remaining -= 1

    if not skip_wait:
        # Clear the last countdown line
        sys.stdout.write("\r" + " "*50 + "\r")
        sys.stdout.flush()
        print("Starting next scan now!")
    
    print("\n" + "="*50 + "\n") # Add some spacing for clarity between cycles

The Redder, The Better 🚦

The Redder, The Better – A phrase used in audio engineering to describe how an audio signal is often considered optimal when the volume unit (VU) meter or LED peak meter enters the red zone. This indicates a strong signal level, improving the signal-to-noise ratio (SNR) by ensuring the desired audio remains well above the noise floor. In analog systems, slight red-zone peaks can add warmth and presence, while in digital systems, red peaks indicate maximum headroom before potential distortion or clipping. 🚦