Modeling the Noise: Building a Tinnitus Generator in Python
I was in Prague this past January, on my way to my sister’s wedding in Wrocław. Since I had already traveled that far, I decided to visit a few places before heading to Poland. Walking in the cold made me drink far more coffee than usual, and the mix of caffeine, low temperatures, and fatigue made my musical tinnitus noticeably louder.
A way to rest the mind from all the traveling, architecture, languages, sightseeing, NFC payments, and other tourist logistics was to focus for a while on these neuro-related annoyances like the non-stop, detuned Dimmu Borgir-esque synth-violin I’d been hearing for days.
If tinnitus is basically the brain generating phantom tones, perhaps could I model it digitally? Maybe I could even vaguely reproduce its texture. The general idea is to treat it as a signal synthesis problem: a generator plus a modulator or filtering system.
I’m not a neurologist (just a tinnitus-suffering engineer, so take this with a generous “probably-maybe” disclaimer), but "musical tinnitus" is a rather elegant name for what’s essentially the perception of structured, sometimes harmonic noise. In this simplified model, the generator (perhaps the auditory cortex or, more likely, a damaged 8th cranial nerve) produces a kind of internal noise. The modulators (probably cortical processes) then shape amplitude, harmonic balance, and rhythm, possibly influenced by memory traces or cross-modal perception, such as synesthesia.
So the goal is to represent that system as a combination of:
carrier + harmonics + tremolo + stochastic noise
A minimal working version that just alternates two tones looks like this:
import numpy as np
import sounddevice as sd
def generate_metallic_tinnitus(duration=5, sample_rate=44100):
# Parameters
noise_duration = duration
mod_freq = 8 # Hz, tremolo frequency
mod_depth = 0.5 # Reduced tremolo intensity for clearer tone
# Define the notes (frequencies in Hz)
notes = {
"D": 294,
"F": 349
}
t = np.linspace(0, noise_duration, int(sample_rate * noise_duration), endpoint=False)
# Generate the step function for frequency switching
freq_step_interval = 0.67
step_signal = np.floor(t / freq_step_interval) % 2
carrier_freq = np.where(step_signal == 0, notes["D"], notes["F"])
# Generate the carrier signal
carrier = np.sin(2 * np.pi * np.cumsum(carrier_freq) / sample_rate)
# Modified harmonic matrix for metallic sound
# Format: [(harmonic_multiplier, intensity)]
harmonics = [
(1, 0.5), # Fundamental - increased
(2, 0.7), # 2nd harmonic - increased
(3, 0.2), # 3rd harmonic - reduced
(4, 0.6), # 4th harmonic - increased
(5, 0.1), # 5th harmonic - reduced
(6, 0.4), # 6th harmonic
(8, 0.5), # 8th harmonic - even harmonics emphasized
(10, 0.3), # 10th harmonic - added higher even harmonic
(12, 0.2), # 12th harmonic - added higher even harmonic
(16, 0.15) # 16th harmonic - very high harmonic for metallic character
]
# Added faster harmonic modulation for more "shimmering" effect
harmonic_modulation = 1 + 0.15 * np.sin(2 * np.pi * 0.5 * t) + 0.1 * np.sin(2 * np.pi * 1.5 * t)
# Reduced white noise
white_noise = np.random.normal(0, 0.1, t.shape) # Reduced noise amplitude
# Generate the harmonic signal
harmonic_signal = np.zeros_like(t)
for multiplier, intensity in harmonics:
harmonic_signal += intensity * harmonic_modulation * np.sin(2 * np.pi * multiplier * np.cumsum(carrier_freq) / sample_rate)
# Modified mix: less noise, more harmonics
shaped_signal = 0.05 * white_noise + 0.95 * harmonic_signal
# Add amplitude modulation (tremolo effect)
tremolo = 1 + mod_depth * np.sin(2 * np.pi * mod_freq * t)
modulated_signal = shaped_signal * tremolo
# Add a slight phase modulation for extra metallic character
phase_mod = 0.1 * np.sin(2 * np.pi * 3 * t)
modulated_signal = modulated_signal * (1 + phase_mod)
# Normalize
modulated_signal = modulated_signal / np.max(np.abs(modulated_signal))
return modulated_signal, sample_rate
# Generate and play
tinnitus_sound, sample_rate = generate_metallic_tinnitus(duration=10)
sd.play(tinnitus_sound, samplerate=sample_rate)
sd.wait()
| Component | Real-world analogy | In the code |
|---|---|---|
| Carrier | Base neural tone | Alternating between D (294 Hz) and F (349 Hz) |
| Harmonics | Overtones & resonances | Sum of sinusoids with different intensities |
| Noise | Random neural firing | Reduced white noise |
| Tremolo | Modulation of perception | 8 Hz amplitude modulation |
| Metallic timbre | Nonlinear or phase artifacts | Phase modulation + emphasized even harmonics |
From Oscillators to Filters
The previous model treated tinnitus as generated tones modulated over time. But there’s another way to look at it, as if the brain filtered noise until patterns emerge.
When the auditory system misfires, it might act as if random neural noises were “music”, using memory and perception filters to impose structure, shaping the random noise: so it seems that tinnitus it’s not pure tone generation, it’s filtered noise, shaped by what the brain expects to hear.
You can think of it like this:
[ White Noise Generator ]
│
▼
[ Memory & Perception Filters ]
(previous melodies, attention,
current ambient sounds, fatigue)
│
▼
[ Simplified Synthesis ]
├─ carrier (base tone)
├─ harmonics (timbre)
├─ tremolo (slow amplitude modulation)
└─ residual noise "bleed"
│
▼
[ Perceived Metallic / Musical Tinnitus ]
Prettier Mermaid diagram:
Here’s a quick experiment using Butterworth filters over white noise:
# Simplified excerpt
white_noise = np.random.normal(0, 2, len(t))
b, a = signal.butter(2, [low_freq, high_freq], btype='bandpass', fs=sample_rate)
filtered = signal.lfilter(b, a, white_noise)
By chaining a few of these filters at harmonic frequencies, applying tremolo, and leaving a bit of noise in the mix, the result sounds eerily similar to the “musical” tinnitus tones I was hearing in Prague.
A working example would be the following code:
import numpy as np
from scipy import signal
import sounddevice as sd
def create_timbre_profile(**kwargs):
"""
Create or modify a timbre profile with customizable parameters
Parameters (defaults for 'violin'):
- odd_harmonic_scaling: float, scaling factor for odd harmonics (default: 1.2)
- even_harmonic_scaling: float, scaling factor for even harmonics (default: 0.8)
- harmonic_falloff: float, how quickly harmonics decrease with frequency (default: 0.6)
- noise_amount: float, amount of noise in the final mix (default: 0.03)
- bandwidth_base: float, base filter bandwidth (default: 45)
- bandwidth_scaling: float, how bandwidth changes with frequency (default: 1.15)
- detuning_amount: float, random detuning intensity (default: 0.012)
- tremolo_depth: float, depth of tremolo effect (default: 0.35)
- tremolo_freq: float, frequency of tremolo effect (default: 6)
"""
profile = {
'odd_harmonic_scaling': 1.5,
'even_harmonic_scaling': 0.5,
'harmonic_falloff': 0.1,
'noise_amount': 0.01,
'bandwidth_base': 10,
'bandwidth_scaling': 1.0,
'detuning_amount': 0.02,
'tremolo_depth': 0.1,
'tremolo_freq': 10
}
profile.update(kwargs)
return profile
def get_harmonic_scaling(harmonic_number, profile):
"""Calculate harmonic scaling based on profile parameters"""
is_even = harmonic_number % 2 == 0
scaling = (profile['even_harmonic_scaling'] if is_even
else profile['odd_harmonic_scaling'])
falloff = harmonic_number ** profile['harmonic_falloff']
return scaling / falloff
def smooth_interpolate(freqs, t, duration, transition_time=0.33):
"""Create smooth frequency transitions using exponential interpolation"""
samples = len(t)
freq_signal = np.zeros(samples)
segments = len(freqs)
for i in range(segments):
start_idx = int(i * samples / segments)
end_idx = int((i + 1) * samples / segments)
segment_t = np.linspace(0, 1, end_idx - start_idx)
tau = 0.33
transition = freqs[i] + (freqs[(i + 1) % segments] - freqs[i]) * (1 - np.exp(-segment_t / tau))
freq_signal[start_idx:end_idx] = transition
return freq_signal
def generate_filtered_tinnitus(duration=20, sample_rate=48000, frequencies=None, timbre_profile=None):
"""
Generate filtered tinnitus sound with customizable timbre profile
Parameters:
- duration: float, duration in seconds
- sample_rate: int, sample rate in Hz
- frequencies: list of float, frequencies to cycle through
- timbre_profile: dict, timbre parameters (if None, uses default violin profile)
"""
if frequencies is None:
frequencies = [440, 440, 330]
if timbre_profile is None:
timbre_profile = create_timbre_profile()
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
white_noise = np.random.normal(0, 2, len(t))
def create_resonant_filter(center_freq, bandwidth):
low_freq = max(1, center_freq - bandwidth / 2)
high_freq = min(sample_rate / 2 - 1, center_freq + bandwidth / 2)
b, a = signal.butter(2, [low_freq, high_freq], btype='bandpass', fs=sample_rate)
return b, a
current_freq = smooth_interpolate(frequencies, t, duration)
n_harmonics = 8
output_signal = np.zeros_like(white_noise)
chunk_size = sample_rate
n_chunks = len(t) // chunk_size + 1
for chunk in range(n_chunks):
start_idx = chunk * chunk_size
end_idx = min((chunk + 1) * chunk_size, len(t))
if start_idx >= len(t):
break
chunk_output = np.zeros(end_idx - start_idx)
chunk_noise = white_noise[start_idx:end_idx]
chunk_freq = current_freq[start_idx:end_idx]
for harmonic in range(1, n_harmonics + 1):
harmonic_freq = chunk_freq * harmonic
if np.max(harmonic_freq) < sample_rate / 2:
avg_freq = np.mean(harmonic_freq)
bandwidth = timbre_profile['bandwidth_base'] * (timbre_profile['bandwidth_scaling'] ** harmonic)
b, a = create_resonant_filter(avg_freq, bandwidth)
detuning_factor = 1 + timbre_profile['detuning_amount'] * (np.random.random() - 0.5)
harmonic_signal = signal.lfilter(b, a, chunk_noise) * detuning_factor
scaling = get_harmonic_scaling(harmonic, timbre_profile) * 2.0
chunk_output += harmonic_signal * scaling
output_signal[start_idx:end_idx] = chunk_output
tremolo = 1 + timbre_profile['tremolo_depth'] * np.sin(2 * np.pi * timbre_profile['tremolo_freq'] * t)
modulated_signal = output_signal * tremolo
final_signal = ((1 - timbre_profile['noise_amount']) * modulated_signal +
timbre_profile['noise_amount'] * white_noise)
final_signal = final_signal / np.max(np.abs(final_signal)) * 0.8
return final_signal, sample_rate
# Example usage:
frequencies = [830.61, 440.00, 659.25, 698.46]
timbre_profile = create_timbre_profile()
tinnitus_sound, sample_rate = generate_filtered_tinnitus(
duration=20,
frequencies=frequencies,
timbre_profile=timbre_profile
)
sd.play(tinnitus_sound, samplerate=sample_rate)
sd.wait()
In reality the process is highly non-linear: perception, feedback, and adaptation all interact in ways far more complex than any simple model. Still, this approximation (a generator shaped by modulators and filters) maps surprisingly well to how tinnitus might emerge from overactive neural loops feeding back into each other.
This project was mostly a way to turn an annoyance into an object of study, something to analyze while resting and waiting for a pair of socks to dry.
Anyone could fork it and build a small GUI with tunable harmonics, noise bleed, envelope modulation, and other parameters to explore how perception morphs under different “neural” settings.