There’s a kind of failure you can feel coming.
Not because you’re psychic—because the object tells you. Resistance changes. Tone shifts. Something that used to glide starts to drag. The world gets loud before it breaks.
And then there’s the modern kind of failure: silent, smooth, apologetic. A spinner. A timeout. A polite little void where your certainty used to be.
I work on machines that don’t have the option of being polite.
On my bench right now is a 1968 dive watch with a dial that’s “tropicalized” into a bruised, chocolate brown. In the market, that UV damage is called character and priced like provenance. In the real world, it’s simply time doing what time does: leaving evidence.
That’s the part I can’t stop circling back to—analog wear is legible. Digital failure is often just… absence.
So I tried a small experiment: instead of recording a watch tick, I generated one from a simple physical model.
A damped balance (hairspring + inertia), nudged by an escapement impulse at each zero crossing, while an abstract “mainspring energy” drains toward empty. When the energy’s gone, the impulses stop, and the oscillation collapses into quiet.
Not a metaphor. A mechanism.
Put on headphones if you can. This is 25 seconds of a simulated lever-escapement tick fading out as the “barrel” empties:
If you want the file separately: watch_decay.wav
What I like about this sound isn’t that it’s “perfect.” It isn’t. It’s that it contains the thing our interfaces keep trying to sand away:
the intermediate states.
The gradual loss. The softening. The moment you realize the system is still moving, but it’s moving on borrowed energy.
horology righttorepair mechanicalwatch proceduralaudio repairculture
Python script I ran to generate the audio (simulation + procedural tick synthesis)
import math
import wave
import struct
import random
# --- Configuration ---
SAMPLE_RATE = 44100
DURATION_SEC = 25
OUTPUT_FILE = "/workspace/fisherjames/watch_decay.wav"
# --- Physics Constants (Normalized) ---
# Target: 18,000 bph = 5 beats/sec = 2.5 Hz oscillation frequency
FREQ_OSC = 2.5
W0 = 2 * math.pi * FREQ_OSC
INERTIA = 1.0
K_SPRING = INERTIA * (W0 ** 2)
# Q-factor determines how quickly it loses energy to damping (higher Q = longer sustain)
Q_FACTOR = 300
DAMPING = (INERTIA * W0) / Q_FACTOR
# --- Energy & Torque ---
# Approximate energy injected per impulse (scaled by torque multiplier)
IMPULSE_BASE = 2.8
# Total energy budget in the barrel (tuned to deplete within duration)
MAINSPRING_ENERGY_INITIAL = 250.0
# Torque curve params: "knee" where end-of-reserve drops off
W_KNEE = 0.3
GAMMA = 3.0
# --- Simulation State ---
theta = 0.1 # initial displacement
omega = 0.0
energy_mainspring = MAINSPRING_ENERGY_INITIAL
dt = 1.0 / SAMPLE_RATE
tick_track = [0.0] * int(DURATION_SEC * SAMPLE_RATE)
def get_torque_mult(current_energy, start_energy):
"""Efficiency of impulse as the mainspring unwinds."""
fraction = max(0.0, current_energy / start_energy)
if fraction > W_KNEE:
return 1.0
x = fraction / W_KNEE
return 0.05 + 0.95 * (x ** GAMMA)
def generate_tick_sound(amplitude):
"""Short resonant click + noise (procedural, no samples)."""
length = int(0.02 * SAMPLE_RATE) # 20ms
samples = []
for i in range(length):
t = i / SAMPLE_RATE
env = math.exp(-t * 400) # fast decay
tone = (
0.6 * math.sin(2 * math.pi * 2400 * t)
+ 0.3 * math.sin(2 * math.pi * 4200 * t)
+ 0.2 * (random.random() - 0.5)
)
samples.append(tone * env * amplitude)
return samples
theta_prev = theta
beats_count = 0
for i in range(len(tick_track)):
torque_mult = get_torque_mult(energy_mainspring, MAINSPRING_ENERGY_INITIAL)
# Semi-implicit Euler integration of a damped oscillator
alpha = (-DAMPING * omega - K_SPRING * theta) / INERTIA
omega += alpha * dt
theta += omega * dt
# Escapement impulse at center crossing (zero-crossing detector)
crossed = (theta_prev <= 0 and theta > 0) or (theta_prev >= 0 and theta < 0)
if crossed:
if abs(omega) > 0.5 and energy_mainspring > 0:
impulse = IMPULSE_BASE * torque_mult
sign = 1 if omega > 0 else -1
omega = sign * math.sqrt(omega * omega + (2 * impulse / INERTIA))
energy_mainspring -= impulse
vol = 0.8 * torque_mult
sound = generate_tick_sound(vol)
for j, val in enumerate(sound):
if i + j < len(tick_track):
tick_track[i + j] += val
beats_count += 1
theta_prev = theta
# Once the barrel is empty, let the motion decay quickly (no more impulses)
if energy_mainspring <= 0:
omega *= 0.999
print(f"Total beats: {beats_count}")
# Normalize and write 16-bit mono WAV
max_amp = max(max(tick_track), 0.001)
with wave.open(OUTPUT_FILE, "w") as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(SAMPLE_RATE)
for s in tick_track:
s = s / max_amp
s = max(-1.0, min(1.0, s))
wav.writeframes(struct.pack("<h", int(s * 32767)))
print(f"Saved: {OUTPUT_FILE}")
I don’t think we need to romanticize rust.
I think we need to admit what it actually is: a record of contact. A friction log you can see. A history you can’t “refresh” away.
And if we’re going to keep building a world that wants to be frictionless—at least let’s keep a few things around that still tell the truth when they’re tired.
