claudible 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claudible/audio.py +140 -48
- claudible/cli.py +57 -8
- claudible/materials.py +418 -13
- claudible/monitor.py +6 -6
- {claudible-0.1.2.dist-info → claudible-0.1.4.dist-info}/METADATA +42 -9
- claudible-0.1.4.dist-info/RECORD +11 -0
- claudible-0.1.2.dist-info/RECORD +0 -11
- {claudible-0.1.2.dist-info → claudible-0.1.4.dist-info}/WHEEL +0 -0
- {claudible-0.1.2.dist-info → claudible-0.1.4.dist-info}/entry_points.txt +0 -0
- {claudible-0.1.2.dist-info → claudible-0.1.4.dist-info}/top_level.txt +0 -0
claudible/audio.py
CHANGED
|
@@ -26,6 +26,7 @@ class SoundEngine:
|
|
|
26
26
|
self._stream: Optional[sd.OutputStream] = None
|
|
27
27
|
self._running = False
|
|
28
28
|
self._hum_phase = 0.0
|
|
29
|
+
self._last_pitch = 1.0 # portamento tracking
|
|
29
30
|
self._lock = threading.Lock()
|
|
30
31
|
|
|
31
32
|
# Ring buffer for mixing audio
|
|
@@ -33,8 +34,8 @@ class SoundEngine:
|
|
|
33
34
|
self._write_pos = 0
|
|
34
35
|
self._read_pos = 0
|
|
35
36
|
|
|
36
|
-
# Pre-generate
|
|
37
|
-
self._grain_cache =
|
|
37
|
+
# Pre-generate grains spread across registers for tonal variety
|
|
38
|
+
self._grain_cache = self._build_grain_cache()
|
|
38
39
|
self._cache_index = 0
|
|
39
40
|
|
|
40
41
|
def start(self):
|
|
@@ -141,92 +142,183 @@ class SoundEngine:
|
|
|
141
142
|
|
|
142
143
|
# --- Sound generation ---
|
|
143
144
|
|
|
144
|
-
def
|
|
145
|
+
def _build_grain_cache(self) -> list:
|
|
146
|
+
"""Build grain cache with voices at full-octave intervals, filtered by material range."""
|
|
147
|
+
# Each register is a full octave apart.
|
|
148
|
+
# (octave_shift, noise_mult, decay_mult, duration_mult)
|
|
149
|
+
# Multiple character variations per register.
|
|
150
|
+
all_voices = [
|
|
151
|
+
# --- oct -3: sub rumble (3 voices) ---
|
|
152
|
+
(-3, 3.5, 2.5, 1.8), # sub thump
|
|
153
|
+
(-3, 2.0, 3.0, 2.0), # sub knock
|
|
154
|
+
(-3, 2.8, 2.0, 1.6), # sub punch
|
|
155
|
+
|
|
156
|
+
# --- oct -2: deep (3 voices) ---
|
|
157
|
+
(-2, 2.8, 1.8, 1.5), # deep percussive
|
|
158
|
+
(-2, 1.2, 2.0, 1.4), # deep tonal
|
|
159
|
+
(-2, 2.0, 1.5, 1.3), # deep knock
|
|
160
|
+
|
|
161
|
+
# --- oct -1: low (3 voices) ---
|
|
162
|
+
(-1, 1.8, 1.4, 1.2), # low thump
|
|
163
|
+
(-1, 0.8, 1.2, 1.1), # low warm
|
|
164
|
+
(-1, 1.4, 1.6, 1.2), # low snap
|
|
165
|
+
|
|
166
|
+
# --- oct 0: mid (2 voices) ---
|
|
167
|
+
( 0, 1.0, 1.0, 1.0), # mid click
|
|
168
|
+
( 0, 0.6, 0.8, 0.9), # mid soft
|
|
169
|
+
|
|
170
|
+
# --- oct +1: high (3 voices) ---
|
|
171
|
+
( 1, 0.5, 0.6, 0.7), # high ping
|
|
172
|
+
( 1, 0.8, 0.5, 0.7), # high tick
|
|
173
|
+
( 1, 0.3, 0.7, 0.8), # high ring
|
|
174
|
+
|
|
175
|
+
# --- oct +2: bright (3 voices) ---
|
|
176
|
+
( 2, 0.2, 0.4, 0.55), # bright ting
|
|
177
|
+
( 2, 0.5, 0.3, 0.6), # bright click
|
|
178
|
+
( 2, 0.15, 0.5, 0.5), # bright shimmer
|
|
179
|
+
|
|
180
|
+
# --- oct +3: air (3 voices) ---
|
|
181
|
+
( 3, 0.1, 0.25, 0.4), # air sparkle
|
|
182
|
+
( 3, 0.3, 0.2, 0.45), # air tick
|
|
183
|
+
( 3, 0.05, 0.3, 0.35), # air wisp
|
|
184
|
+
]
|
|
185
|
+
oct_min, oct_max = self.material.octave_range
|
|
186
|
+
voices = [v for v in all_voices if oct_min <= v[0] <= oct_max]
|
|
187
|
+
cache = []
|
|
188
|
+
for octave, noise_m, decay_m, dur_m in voices:
|
|
189
|
+
cache.append(self._generate_grain(
|
|
190
|
+
octave_shift=octave,
|
|
191
|
+
noise_mult=noise_m,
|
|
192
|
+
decay_mult=decay_m,
|
|
193
|
+
duration_mult=dur_m,
|
|
194
|
+
))
|
|
195
|
+
return cache
|
|
196
|
+
|
|
197
|
+
def _generate_grain(self, octave_shift: float = 0.0, noise_mult: float = 1.0,
|
|
198
|
+
decay_mult: float = 1.0, duration_mult: float = 1.0) -> np.ndarray:
|
|
145
199
|
"""Generate a single grain sound from the material's physical model."""
|
|
146
200
|
m = self.material
|
|
147
|
-
|
|
148
|
-
duration = m.grain_duration * np.random.uniform(0.85, 1.15)
|
|
201
|
+
duration = m.grain_duration * duration_mult * 0.5 * np.random.uniform(0.85, 1.15)
|
|
149
202
|
n = int(duration * self.SAMPLE_RATE)
|
|
150
203
|
t = np.linspace(0, duration, n, dtype=np.float32)
|
|
151
204
|
|
|
152
205
|
grain = np.zeros(n, dtype=np.float32)
|
|
153
206
|
|
|
154
|
-
#
|
|
155
|
-
base_freq = m.base_freq * (2 **
|
|
207
|
+
# Base frequency shifted by register
|
|
208
|
+
base_freq = m.base_freq * (2 ** octave_shift)
|
|
209
|
+
base_freq *= (2 ** (np.random.uniform(-m.freq_spread, m.freq_spread) / 12))
|
|
156
210
|
|
|
157
|
-
# Generate each partial with its own decay, detuning, and random phase
|
|
158
211
|
for i, (ratio, amp) in enumerate(m.partials):
|
|
159
212
|
detune_cents = np.random.uniform(-m.detune_amount, m.detune_amount)
|
|
160
213
|
freq = base_freq * ratio * (2 ** (detune_cents / 1200))
|
|
161
|
-
# Extra micro-variation per partial
|
|
162
214
|
freq *= np.random.uniform(0.997, 1.003)
|
|
163
215
|
phase = np.random.uniform(0, 2 * np.pi)
|
|
164
216
|
|
|
165
217
|
decay_rate = m.decay_rates[i] if i < len(m.decay_rates) else m.decay_rates[-1]
|
|
218
|
+
decay_rate *= decay_mult
|
|
166
219
|
envelope = np.exp(-decay_rate * t)
|
|
167
220
|
|
|
168
221
|
grain += amp * envelope * np.sin(2 * np.pi * freq * t + phase)
|
|
169
222
|
|
|
170
|
-
# Noise transient
|
|
223
|
+
# Noise transient — louder for low percussive grains
|
|
171
224
|
if m.attack_noise > 0:
|
|
172
225
|
noise = np.random.randn(n).astype(np.float32)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
noise_env = np.exp(-150 * t)
|
|
226
|
+
noise_cutoff = max(m.noise_freq * (0.5 if octave_shift < -0.5 else 1.0), 200)
|
|
227
|
+
noise = self._highpass(noise, noise_cutoff)
|
|
228
|
+
noise_env = np.exp(-150 * t / max(decay_mult, 0.5))
|
|
176
229
|
noise_env *= np.clip(
|
|
177
230
|
1.0 + 0.3 * np.random.randn(n).astype(np.float32) * np.exp(-200 * t),
|
|
178
231
|
0, 1,
|
|
179
232
|
)
|
|
180
|
-
grain += m.attack_noise * 0.3 * noise * noise_env
|
|
233
|
+
grain += m.attack_noise * noise_mult * 0.3 * noise * noise_env
|
|
181
234
|
|
|
182
235
|
# Attack shaping
|
|
183
236
|
attack_samples = max(int(m.attack_ms / 1000 * self.SAMPLE_RATE), 2)
|
|
184
237
|
if attack_samples < n:
|
|
185
238
|
grain[:attack_samples] *= np.linspace(0, 1, attack_samples, dtype=np.float32)
|
|
186
239
|
|
|
187
|
-
# Pitch drop
|
|
188
|
-
if m.pitch_drop
|
|
189
|
-
|
|
240
|
+
# Pitch drop — more dramatic for low grains
|
|
241
|
+
pitch_drop = m.pitch_drop if octave_shift >= 0 else m.pitch_drop ** (1.0 + abs(octave_shift) * 0.3)
|
|
242
|
+
if pitch_drop != 1.0:
|
|
243
|
+
pitch_env = np.linspace(1.0, pitch_drop, n)
|
|
190
244
|
phase_mod = np.cumsum(pitch_env) / np.sum(pitch_env) * n
|
|
191
245
|
indices = np.clip(phase_mod.astype(int), 0, n - 1)
|
|
192
246
|
grain = grain[indices]
|
|
193
247
|
|
|
194
|
-
#
|
|
195
|
-
|
|
248
|
+
# Reverb — less for percussive lows (tighter), more for highs
|
|
249
|
+
reverb_amt = m.reverb_amount * (0.6 if octave_shift < -0.5 else 1.0 + max(octave_shift, 0) * 0.2)
|
|
250
|
+
grain = self._apply_reverb(grain, reverb_amt, m.room_size, m.reverb_damping)
|
|
196
251
|
|
|
197
|
-
#
|
|
198
|
-
|
|
252
|
+
# Small pre-delay + extra tail reverb
|
|
253
|
+
delay_ms = 15
|
|
254
|
+
delay_samples = int(delay_ms * self.SAMPLE_RATE / 1000)
|
|
255
|
+
tail_len = n + delay_samples + int(0.08 * self.SAMPLE_RATE)
|
|
256
|
+
out = np.zeros(tail_len, dtype=np.float32)
|
|
257
|
+
out[delay_samples:delay_samples + len(grain)] = grain
|
|
258
|
+
# Light tail reverb on top
|
|
259
|
+
out = self._apply_reverb(out, 0.15, room_size=1.2, damping=0.4)
|
|
199
260
|
|
|
200
|
-
|
|
201
|
-
peak = np.max(np.abs(grain))
|
|
261
|
+
peak = np.max(np.abs(out))
|
|
202
262
|
if peak > 0:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
263
|
+
out = out / peak * 0.4 * m.volume
|
|
264
|
+
|
|
265
|
+
return out.astype(np.float32)
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def _token_hash(token: str) -> int:
|
|
269
|
+
"""Deterministic hash from token content, stable across sessions."""
|
|
270
|
+
h = 5381
|
|
271
|
+
for c in token:
|
|
272
|
+
h = ((h * 33) ^ ord(c)) & 0xFFFFFFFF
|
|
273
|
+
return h
|
|
274
|
+
|
|
275
|
+
# Minor pentatonic scale degrees in semitones (wraps at octave)
|
|
276
|
+
_SCALE_DEGREES = [0, 3, 5, 7, 10]
|
|
277
|
+
|
|
278
|
+
def _snap_to_scale(self, semitones: float) -> float:
|
|
279
|
+
"""Quantise a semitone offset to the nearest scale degree."""
|
|
280
|
+
octave = int(semitones // 12)
|
|
281
|
+
remainder = semitones % 12
|
|
282
|
+
nearest = min(self._SCALE_DEGREES, key=lambda d: abs(d - remainder))
|
|
283
|
+
return octave * 12 + nearest
|
|
284
|
+
|
|
285
|
+
def play_grain(self, token: str = ""):
|
|
286
|
+
"""Play a grain/sparkle sound, deterministic for a given token."""
|
|
287
|
+
if token:
|
|
288
|
+
h = self._token_hash(token)
|
|
289
|
+
rng = np.random.RandomState(h)
|
|
290
|
+
else:
|
|
291
|
+
rng = np.random
|
|
292
|
+
|
|
293
|
+
idx = rng.randint(len(self._grain_cache))
|
|
211
294
|
grain = self._grain_cache[idx].copy()
|
|
212
295
|
|
|
213
|
-
#
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
296
|
+
# Pitch variation snapped to minor pentatonic scale
|
|
297
|
+
raw_semi = rng.uniform(-1.5, 1.5)
|
|
298
|
+
snapped_semi = self._snap_to_scale(raw_semi)
|
|
299
|
+
pitch_shift = 2 ** (snapped_semi / 12)
|
|
300
|
+
|
|
301
|
+
# Fast portamento: slide from last pitch to new target
|
|
302
|
+
prev_pitch = self._last_pitch
|
|
303
|
+
self._last_pitch = pitch_shift
|
|
304
|
+
|
|
305
|
+
n = len(grain)
|
|
306
|
+
if n > 0 and (abs(pitch_shift - 1.0) > 0.001 or abs(prev_pitch - 1.0) > 0.001):
|
|
307
|
+
# Glide over first 30% of grain, then hold target
|
|
308
|
+
glide_len = int(n * 0.3)
|
|
309
|
+
pitch_env = np.ones(n, dtype=np.float32)
|
|
310
|
+
if glide_len > 0:
|
|
311
|
+
pitch_env[:glide_len] = np.linspace(
|
|
312
|
+
prev_pitch, pitch_shift, glide_len, dtype=np.float32,
|
|
313
|
+
)
|
|
314
|
+
pitch_env[glide_len:] = pitch_shift
|
|
315
|
+
|
|
316
|
+
# Time-varying resample using cumulative phase
|
|
317
|
+
phase = np.cumsum(pitch_env)
|
|
318
|
+
phase = phase / phase[-1] * (n - 1)
|
|
319
|
+
grain = np.interp(phase, np.arange(n), grain).astype(np.float32)
|
|
320
|
+
|
|
321
|
+
grain *= rng.uniform(0.7, 1.0)
|
|
230
322
|
|
|
231
323
|
self._add_to_buffer(grain)
|
|
232
324
|
|
claudible/cli.py
CHANGED
|
@@ -10,10 +10,32 @@ import time
|
|
|
10
10
|
from typing import Optional
|
|
11
11
|
|
|
12
12
|
from .audio import SoundEngine
|
|
13
|
-
from .materials import
|
|
13
|
+
from .materials import (
|
|
14
|
+
get_material, get_random_material, list_materials, list_sound_sets,
|
|
15
|
+
get_sound_set, SOUND_SETS, DEFAULT_SOUND_SET,
|
|
16
|
+
)
|
|
14
17
|
from .monitor import ActivityMonitor
|
|
15
18
|
|
|
16
19
|
|
|
20
|
+
def run_demo(volume: float, sound_set: str = DEFAULT_SOUND_SET):
|
|
21
|
+
"""Play a short demo of every sound character in a set."""
|
|
22
|
+
materials = get_sound_set(sound_set)
|
|
23
|
+
print(f"Claudible character demo [{sound_set}]\n", file=sys.stderr)
|
|
24
|
+
for name, mat in materials.items():
|
|
25
|
+
print(f" {name:10} - {mat.description}", file=sys.stderr)
|
|
26
|
+
engine = SoundEngine(material=mat, volume=volume)
|
|
27
|
+
engine.start()
|
|
28
|
+
# Play a burst of grains then a chime
|
|
29
|
+
for i in range(8):
|
|
30
|
+
engine.play_grain(chr(ord('a') + i))
|
|
31
|
+
time.sleep(0.06)
|
|
32
|
+
time.sleep(0.15)
|
|
33
|
+
engine.play_chime()
|
|
34
|
+
time.sleep(0.6)
|
|
35
|
+
engine.stop()
|
|
36
|
+
print("\nDone.", file=sys.stderr)
|
|
37
|
+
|
|
38
|
+
|
|
17
39
|
def run_pipe_mode(engine: SoundEngine, monitor: ActivityMonitor):
|
|
18
40
|
"""Run in pipe mode, reading from stdin."""
|
|
19
41
|
engine.start()
|
|
@@ -111,10 +133,15 @@ def main():
|
|
|
111
133
|
action='store_true',
|
|
112
134
|
help='Pipe mode: read from stdin instead of wrapping a command',
|
|
113
135
|
)
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
'--set', '-s',
|
|
138
|
+
choices=list_sound_sets(),
|
|
139
|
+
default=DEFAULT_SOUND_SET,
|
|
140
|
+
help=f'Sound set (default: {DEFAULT_SOUND_SET})',
|
|
141
|
+
)
|
|
114
142
|
parser.add_argument(
|
|
115
143
|
'--character', '-c',
|
|
116
|
-
|
|
117
|
-
help='Sound character (default: random)',
|
|
144
|
+
help='Sound character (default: random). Use --list-characters to see options.',
|
|
118
145
|
)
|
|
119
146
|
parser.add_argument(
|
|
120
147
|
'--volume', '-v',
|
|
@@ -138,19 +165,41 @@ def main():
|
|
|
138
165
|
action='store_true',
|
|
139
166
|
help='List available sound characters',
|
|
140
167
|
)
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
'--demo',
|
|
170
|
+
action='store_true',
|
|
171
|
+
help='Play a short demo of each sound character',
|
|
172
|
+
)
|
|
141
173
|
|
|
142
174
|
args = parser.parse_args()
|
|
143
175
|
|
|
176
|
+
sound_set = args.set
|
|
177
|
+
|
|
144
178
|
if args.list_characters:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
print(f" {
|
|
179
|
+
for set_name, materials in SOUND_SETS.items():
|
|
180
|
+
default_tag = " (default)" if set_name == DEFAULT_SOUND_SET else ""
|
|
181
|
+
print(f"\n [{set_name}]{default_tag}\n")
|
|
182
|
+
for name, mat in materials.items():
|
|
183
|
+
print(f" {name:10} - {mat.description}")
|
|
184
|
+
print()
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
if args.demo:
|
|
188
|
+
volume = max(0.0, min(1.0, args.volume))
|
|
189
|
+
run_demo(volume, sound_set)
|
|
148
190
|
return
|
|
149
191
|
|
|
192
|
+
# Validate character against the chosen set
|
|
150
193
|
if args.character:
|
|
151
|
-
|
|
194
|
+
available = list_materials(sound_set)
|
|
195
|
+
if args.character not in available:
|
|
196
|
+
parser.error(
|
|
197
|
+
f"character '{args.character}' not in set '{sound_set}'. "
|
|
198
|
+
f"Available: {', '.join(available)}"
|
|
199
|
+
)
|
|
200
|
+
material = get_material(args.character, sound_set)
|
|
152
201
|
else:
|
|
153
|
-
material = get_random_material()
|
|
202
|
+
material = get_random_material(sound_set)
|
|
154
203
|
|
|
155
204
|
volume = max(0.0, min(1.0, args.volume))
|
|
156
205
|
|
claudible/materials.py
CHANGED
|
@@ -27,12 +27,380 @@ class Material:
|
|
|
27
27
|
attack_ms: float # attack ramp time in milliseconds
|
|
28
28
|
freq_spread: float # semitones of random base freq variation per grain
|
|
29
29
|
volume: float # per-material volume scaling (0-1)
|
|
30
|
+
octave_range: Tuple[int, int] # (min, max) octave shifts for grain cache
|
|
30
31
|
description: str
|
|
31
32
|
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
# --- Original materials (upgraded with physics parameters) ---
|
|
34
|
+
DEFAULT_SOUND_SET = "ambient"
|
|
35
35
|
|
|
36
|
+
# --- Ambient sound set (default) ---
|
|
37
|
+
# Throbbing, textured, overtone-rich with percussive attack.
|
|
38
|
+
# Close partial pairs create beating/throbbing. High attack noise for punch.
|
|
39
|
+
# Kept low — nothing shrill. Dense, gritty texture.
|
|
40
|
+
|
|
41
|
+
AMBIENT_MATERIALS = {
|
|
42
|
+
"drift": Material(
|
|
43
|
+
name="drift",
|
|
44
|
+
base_freq=95,
|
|
45
|
+
partials=[
|
|
46
|
+
(1.0, 1.0), (1.005, 0.95), # beating pair ~0.48Hz throb
|
|
47
|
+
(2.0, 0.5), (2.008, 0.45), # octave beating pair
|
|
48
|
+
(3.01, 0.2), (4.02, 0.1),
|
|
49
|
+
],
|
|
50
|
+
decay_rates=[3, 3.2, 5, 5.3, 8, 11],
|
|
51
|
+
grain_duration=0.16,
|
|
52
|
+
attack_noise=0.35,
|
|
53
|
+
noise_freq=600,
|
|
54
|
+
detune_amount=12,
|
|
55
|
+
reverb_amount=0.6,
|
|
56
|
+
reverb_damping=0.55,
|
|
57
|
+
room_size=1.8,
|
|
58
|
+
pitch_drop=0.997,
|
|
59
|
+
attack_ms=1.5,
|
|
60
|
+
freq_spread=3.0,
|
|
61
|
+
volume=0.7,
|
|
62
|
+
octave_range=(-3, 0),
|
|
63
|
+
description="Low undulating throb, punchy attack into beating pairs",
|
|
64
|
+
),
|
|
65
|
+
|
|
66
|
+
"tide": Material(
|
|
67
|
+
name="tide",
|
|
68
|
+
base_freq=120,
|
|
69
|
+
partials=[
|
|
70
|
+
(1.0, 1.0), (1.003, 0.92), # slow beat
|
|
71
|
+
(1.498, 0.55), (1.502, 0.52), # fifth beating pair
|
|
72
|
+
(2.0, 0.35), (3.0, 0.12),
|
|
73
|
+
],
|
|
74
|
+
decay_rates=[4, 4.1, 6, 6.2, 8, 12],
|
|
75
|
+
grain_duration=0.18,
|
|
76
|
+
attack_noise=0.3,
|
|
77
|
+
noise_freq=500,
|
|
78
|
+
detune_amount=14,
|
|
79
|
+
reverb_amount=0.7,
|
|
80
|
+
reverb_damping=0.5,
|
|
81
|
+
room_size=2.0,
|
|
82
|
+
pitch_drop=0.996,
|
|
83
|
+
attack_ms=1.0,
|
|
84
|
+
freq_spread=4.0,
|
|
85
|
+
volume=0.65,
|
|
86
|
+
octave_range=(-3, 0),
|
|
87
|
+
description="Oceanic wash with percussive bite, wide phase interference",
|
|
88
|
+
),
|
|
89
|
+
|
|
90
|
+
"breath": Material(
|
|
91
|
+
name="breath",
|
|
92
|
+
base_freq=160,
|
|
93
|
+
partials=[
|
|
94
|
+
(1.0, 1.0), (1.006, 0.85), # beating pair
|
|
95
|
+
(2.01, 0.4), (3.02, 0.15),
|
|
96
|
+
],
|
|
97
|
+
decay_rates=[5, 5.2, 8, 12],
|
|
98
|
+
grain_duration=0.14,
|
|
99
|
+
attack_noise=0.4,
|
|
100
|
+
noise_freq=900,
|
|
101
|
+
detune_amount=18,
|
|
102
|
+
reverb_amount=0.55,
|
|
103
|
+
reverb_damping=0.6,
|
|
104
|
+
room_size=1.6,
|
|
105
|
+
pitch_drop=0.998,
|
|
106
|
+
attack_ms=1.2,
|
|
107
|
+
freq_spread=5.0,
|
|
108
|
+
volume=0.6,
|
|
109
|
+
octave_range=(-3, 0),
|
|
110
|
+
description="Gritty exhale, noisy attack decaying into warm throb",
|
|
111
|
+
),
|
|
112
|
+
|
|
113
|
+
"haze": Material(
|
|
114
|
+
name="haze",
|
|
115
|
+
base_freq=110,
|
|
116
|
+
partials=[
|
|
117
|
+
(1.0, 1.0), (1.004, 0.9), # tight beating
|
|
118
|
+
(2.003, 0.6), (2.007, 0.55), # octave beating
|
|
119
|
+
(3.005, 0.3), (4.01, 0.15),
|
|
120
|
+
(5.02, 0.06),
|
|
121
|
+
],
|
|
122
|
+
decay_rates=[3.5, 3.7, 5, 5.2, 7, 10, 14],
|
|
123
|
+
grain_duration=0.15,
|
|
124
|
+
attack_noise=0.32,
|
|
125
|
+
noise_freq=550,
|
|
126
|
+
detune_amount=10,
|
|
127
|
+
reverb_amount=0.65,
|
|
128
|
+
reverb_damping=0.6,
|
|
129
|
+
room_size=1.9,
|
|
130
|
+
pitch_drop=0.997,
|
|
131
|
+
attack_ms=1.0,
|
|
132
|
+
freq_spread=3.5,
|
|
133
|
+
volume=0.65,
|
|
134
|
+
octave_range=(-3, 0),
|
|
135
|
+
description="Dense foggy cluster, percussive onset into thick overtone haze",
|
|
136
|
+
),
|
|
137
|
+
|
|
138
|
+
"pulse": Material(
|
|
139
|
+
name="pulse",
|
|
140
|
+
base_freq=80,
|
|
141
|
+
partials=[
|
|
142
|
+
(1.0, 1.0), (1.008, 0.9), # ~0.64Hz beating = pulsing
|
|
143
|
+
(2.0, 0.55), (2.012, 0.5), # faster beat in octave
|
|
144
|
+
(3.0, 0.25),
|
|
145
|
+
],
|
|
146
|
+
decay_rates=[2.5, 2.6, 4, 4.2, 7],
|
|
147
|
+
grain_duration=0.2,
|
|
148
|
+
attack_noise=0.38,
|
|
149
|
+
noise_freq=400,
|
|
150
|
+
detune_amount=8,
|
|
151
|
+
reverb_amount=0.5,
|
|
152
|
+
reverb_damping=0.65,
|
|
153
|
+
room_size=1.5,
|
|
154
|
+
pitch_drop=0.995,
|
|
155
|
+
attack_ms=0.8,
|
|
156
|
+
freq_spread=2.5,
|
|
157
|
+
volume=0.7,
|
|
158
|
+
octave_range=(-3, 0),
|
|
159
|
+
description="Thumpy rhythmic throb, hard attack into hypnotic beating",
|
|
160
|
+
),
|
|
161
|
+
|
|
162
|
+
"glow": Material(
|
|
163
|
+
name="glow",
|
|
164
|
+
base_freq=145,
|
|
165
|
+
partials=[
|
|
166
|
+
(1.0, 1.0), (1.003, 0.88), # beating pair
|
|
167
|
+
(2.0, 0.6), (3.0, 0.35),
|
|
168
|
+
(4.0, 0.2), (5.0, 0.1),
|
|
169
|
+
],
|
|
170
|
+
decay_rates=[4, 4.2, 5, 6.5, 8, 11],
|
|
171
|
+
grain_duration=0.13,
|
|
172
|
+
attack_noise=0.28,
|
|
173
|
+
noise_freq=700,
|
|
174
|
+
detune_amount=15,
|
|
175
|
+
reverb_amount=0.55,
|
|
176
|
+
reverb_damping=0.45,
|
|
177
|
+
room_size=1.7,
|
|
178
|
+
pitch_drop=0.998,
|
|
179
|
+
attack_ms=1.5,
|
|
180
|
+
freq_spread=4.0,
|
|
181
|
+
volume=0.65,
|
|
182
|
+
octave_range=(-3, 0),
|
|
183
|
+
description="Warm radiant harmonics, punchy onset with rich overtone bloom",
|
|
184
|
+
),
|
|
185
|
+
|
|
186
|
+
"cloud": Material(
|
|
187
|
+
name="cloud",
|
|
188
|
+
base_freq=190,
|
|
189
|
+
partials=[
|
|
190
|
+
(1.0, 1.0), (1.002, 0.85),
|
|
191
|
+
(1.5, 0.4), (2.0, 0.25),
|
|
192
|
+
(2.5, 0.1),
|
|
193
|
+
],
|
|
194
|
+
decay_rates=[5, 5.2, 7, 9, 13],
|
|
195
|
+
grain_duration=0.14,
|
|
196
|
+
attack_noise=0.3,
|
|
197
|
+
noise_freq=800,
|
|
198
|
+
detune_amount=16,
|
|
199
|
+
reverb_amount=0.75,
|
|
200
|
+
reverb_damping=0.4,
|
|
201
|
+
room_size=2.2,
|
|
202
|
+
pitch_drop=0.998,
|
|
203
|
+
attack_ms=1.2,
|
|
204
|
+
freq_spread=5.0,
|
|
205
|
+
volume=0.55,
|
|
206
|
+
octave_range=(-3, 0),
|
|
207
|
+
description="Percussive puff into massive diffuse reverb, textured",
|
|
208
|
+
),
|
|
209
|
+
|
|
210
|
+
"murmur": Material(
|
|
211
|
+
name="murmur",
|
|
212
|
+
base_freq=70,
|
|
213
|
+
partials=[
|
|
214
|
+
(1.0, 1.0), (1.006, 0.88), # low beating
|
|
215
|
+
(2.0, 0.5), (3.0, 0.25),
|
|
216
|
+
(4.01, 0.1),
|
|
217
|
+
],
|
|
218
|
+
decay_rates=[3, 3.2, 5, 8, 12],
|
|
219
|
+
grain_duration=0.17,
|
|
220
|
+
attack_noise=0.35,
|
|
221
|
+
noise_freq=350,
|
|
222
|
+
detune_amount=12,
|
|
223
|
+
reverb_amount=0.5,
|
|
224
|
+
reverb_damping=0.7,
|
|
225
|
+
room_size=1.4,
|
|
226
|
+
pitch_drop=0.994,
|
|
227
|
+
attack_ms=0.8,
|
|
228
|
+
freq_spread=3.0,
|
|
229
|
+
volume=0.7,
|
|
230
|
+
octave_range=(-3, 0),
|
|
231
|
+
description="Deep rumble thump, heavy attack into warm beating murmur",
|
|
232
|
+
),
|
|
233
|
+
|
|
234
|
+
"shimmer": Material(
|
|
235
|
+
name="shimmer",
|
|
236
|
+
base_freq=260,
|
|
237
|
+
partials=[
|
|
238
|
+
(1.0, 1.0), (1.002, 0.9), # slow beating
|
|
239
|
+
(2.001, 0.55), (2.003, 0.52), # octave beating
|
|
240
|
+
(3.0, 0.25), (4.0, 0.1),
|
|
241
|
+
],
|
|
242
|
+
decay_rates=[5, 5.1, 7, 7.2, 10, 14],
|
|
243
|
+
grain_duration=0.11,
|
|
244
|
+
attack_noise=0.25,
|
|
245
|
+
noise_freq=1200,
|
|
246
|
+
detune_amount=10,
|
|
247
|
+
reverb_amount=0.65,
|
|
248
|
+
reverb_damping=0.3,
|
|
249
|
+
room_size=2.0,
|
|
250
|
+
pitch_drop=0.999,
|
|
251
|
+
attack_ms=1.0,
|
|
252
|
+
freq_spread=4.0,
|
|
253
|
+
volume=0.55,
|
|
254
|
+
octave_range=(-3, 0),
|
|
255
|
+
description="Bright-ish tap into shimmering beating overtones",
|
|
256
|
+
),
|
|
257
|
+
|
|
258
|
+
"deep": Material(
|
|
259
|
+
name="deep",
|
|
260
|
+
base_freq=50,
|
|
261
|
+
partials=[
|
|
262
|
+
(1.0, 1.0), (1.01, 0.85), # ~0.5Hz beating
|
|
263
|
+
(2.0, 0.55), (2.015, 0.5), # wider octave beat
|
|
264
|
+
(3.0, 0.2),
|
|
265
|
+
],
|
|
266
|
+
decay_rates=[2, 2.2, 3.5, 3.8, 6],
|
|
267
|
+
grain_duration=0.22,
|
|
268
|
+
attack_noise=0.4,
|
|
269
|
+
noise_freq=250,
|
|
270
|
+
detune_amount=8,
|
|
271
|
+
reverb_amount=0.45,
|
|
272
|
+
reverb_damping=0.8,
|
|
273
|
+
room_size=1.3,
|
|
274
|
+
pitch_drop=0.992,
|
|
275
|
+
attack_ms=0.6,
|
|
276
|
+
freq_spread=2.5,
|
|
277
|
+
volume=0.75,
|
|
278
|
+
octave_range=(-3, 0),
|
|
279
|
+
description="Sub-bass thump, hard hit into very deep slow throb",
|
|
280
|
+
),
|
|
281
|
+
|
|
282
|
+
# --- High register whispers (very quiet, above 3kHz) ---
|
|
283
|
+
|
|
284
|
+
"wisp": Material(
|
|
285
|
+
name="wisp",
|
|
286
|
+
base_freq=3200,
|
|
287
|
+
partials=[
|
|
288
|
+
(1.0, 1.0), (1.002, 0.9), # tight beating
|
|
289
|
+
(2.0, 0.2), (3.01, 0.05),
|
|
290
|
+
],
|
|
291
|
+
decay_rates=[12, 12.5, 20, 30],
|
|
292
|
+
grain_duration=0.06,
|
|
293
|
+
attack_noise=0.15,
|
|
294
|
+
noise_freq=5000,
|
|
295
|
+
detune_amount=4,
|
|
296
|
+
reverb_amount=0.7,
|
|
297
|
+
reverb_damping=0.2,
|
|
298
|
+
room_size=2.0,
|
|
299
|
+
pitch_drop=1.0,
|
|
300
|
+
attack_ms=2.0,
|
|
301
|
+
freq_spread=2.0,
|
|
302
|
+
volume=0.18,
|
|
303
|
+
octave_range=(-1, 1),
|
|
304
|
+
description="Faint crystalline wisp, barely there",
|
|
305
|
+
),
|
|
306
|
+
|
|
307
|
+
"glint": Material(
|
|
308
|
+
name="glint",
|
|
309
|
+
base_freq=3800,
|
|
310
|
+
partials=[
|
|
311
|
+
(1.0, 1.0), (1.5, 0.4), (2.01, 0.15),
|
|
312
|
+
],
|
|
313
|
+
decay_rates=[18, 25, 35],
|
|
314
|
+
grain_duration=0.04,
|
|
315
|
+
attack_noise=0.25,
|
|
316
|
+
noise_freq=6000,
|
|
317
|
+
detune_amount=6,
|
|
318
|
+
reverb_amount=0.65,
|
|
319
|
+
reverb_damping=0.15,
|
|
320
|
+
room_size=1.8,
|
|
321
|
+
pitch_drop=0.998,
|
|
322
|
+
attack_ms=0.8,
|
|
323
|
+
freq_spread=3.0,
|
|
324
|
+
volume=0.15,
|
|
325
|
+
octave_range=(-1, 0),
|
|
326
|
+
description="Tiny metallic glint, quiet and sharp",
|
|
327
|
+
),
|
|
328
|
+
|
|
329
|
+
"arc": Material(
|
|
330
|
+
name="arc",
|
|
331
|
+
base_freq=4200,
|
|
332
|
+
partials=[
|
|
333
|
+
(1.0, 1.0), (1.003, 0.85), # slow beating at high freq
|
|
334
|
+
(1.5, 0.3), (2.0, 0.1),
|
|
335
|
+
],
|
|
336
|
+
decay_rates=[10, 10.5, 16, 24],
|
|
337
|
+
grain_duration=0.07,
|
|
338
|
+
attack_noise=0.1,
|
|
339
|
+
noise_freq=7000,
|
|
340
|
+
detune_amount=3,
|
|
341
|
+
reverb_amount=0.75,
|
|
342
|
+
reverb_damping=0.1,
|
|
343
|
+
room_size=2.2,
|
|
344
|
+
pitch_drop=1.0,
|
|
345
|
+
attack_ms=3.0,
|
|
346
|
+
freq_spread=1.5,
|
|
347
|
+
volume=0.14,
|
|
348
|
+
octave_range=(-1, 0),
|
|
349
|
+
description="High glass arc, delicate beating shimmer",
|
|
350
|
+
),
|
|
351
|
+
|
|
352
|
+
"spark": Material(
|
|
353
|
+
name="spark",
|
|
354
|
+
base_freq=5000,
|
|
355
|
+
partials=[
|
|
356
|
+
(1.0, 1.0), (1.41, 0.35), (2.0, 0.08),
|
|
357
|
+
],
|
|
358
|
+
decay_rates=[22, 30, 45],
|
|
359
|
+
grain_duration=0.03,
|
|
360
|
+
attack_noise=0.3,
|
|
361
|
+
noise_freq=8000,
|
|
362
|
+
detune_amount=8,
|
|
363
|
+
reverb_amount=0.6,
|
|
364
|
+
reverb_damping=0.15,
|
|
365
|
+
room_size=1.6,
|
|
366
|
+
pitch_drop=0.996,
|
|
367
|
+
attack_ms=0.5,
|
|
368
|
+
freq_spread=4.0,
|
|
369
|
+
volume=0.12,
|
|
370
|
+
octave_range=(-1, 0),
|
|
371
|
+
description="Tiny electric spark, fast and faint",
|
|
372
|
+
),
|
|
373
|
+
|
|
374
|
+
"dust": Material(
|
|
375
|
+
name="dust",
|
|
376
|
+
base_freq=3500,
|
|
377
|
+
partials=[
|
|
378
|
+
(1.0, 1.0), (1.001, 0.95), # very tight beating
|
|
379
|
+
(2.003, 0.3), (2.006, 0.28), # octave beating pair
|
|
380
|
+
(3.0, 0.08),
|
|
381
|
+
],
|
|
382
|
+
decay_rates=[8, 8.2, 14, 14.5, 22],
|
|
383
|
+
grain_duration=0.08,
|
|
384
|
+
attack_noise=0.12,
|
|
385
|
+
noise_freq=5500,
|
|
386
|
+
detune_amount=5,
|
|
387
|
+
reverb_amount=0.8,
|
|
388
|
+
reverb_damping=0.1,
|
|
389
|
+
room_size=2.4,
|
|
390
|
+
pitch_drop=1.0,
|
|
391
|
+
attack_ms=4.0,
|
|
392
|
+
freq_spread=2.0,
|
|
393
|
+
volume=0.16,
|
|
394
|
+
octave_range=(-1, 0),
|
|
395
|
+
description="Soft high dust motes, floating reverb texture",
|
|
396
|
+
),
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# --- Material sound set (percussive, physical) ---
|
|
401
|
+
# The original set: crisp, percussive sounds modelled on physical materials.
|
|
402
|
+
|
|
403
|
+
MATERIAL_MATERIALS = {
|
|
36
404
|
"ice": Material(
|
|
37
405
|
name="ice",
|
|
38
406
|
base_freq=2800,
|
|
@@ -49,6 +417,7 @@ MATERIALS = {
|
|
|
49
417
|
attack_ms=0.5,
|
|
50
418
|
freq_spread=4.0,
|
|
51
419
|
volume=0.85,
|
|
420
|
+
octave_range=(-3, 3),
|
|
52
421
|
description="Brittle, very high, fast decay with pitch drop",
|
|
53
422
|
),
|
|
54
423
|
|
|
@@ -68,6 +437,7 @@ MATERIALS = {
|
|
|
68
437
|
attack_ms=0.8,
|
|
69
438
|
freq_spread=3.0,
|
|
70
439
|
volume=0.9,
|
|
440
|
+
octave_range=(-3, 3),
|
|
71
441
|
description="Classic wine glass ping",
|
|
72
442
|
),
|
|
73
443
|
|
|
@@ -87,6 +457,7 @@ MATERIALS = {
|
|
|
87
457
|
attack_ms=1.0,
|
|
88
458
|
freq_spread=2.5,
|
|
89
459
|
volume=0.85,
|
|
460
|
+
octave_range=(-3, 3),
|
|
90
461
|
description="Pure lead crystal with beating from close partial pairs",
|
|
91
462
|
),
|
|
92
463
|
|
|
@@ -106,6 +477,7 @@ MATERIALS = {
|
|
|
106
477
|
attack_ms=1.2,
|
|
107
478
|
freq_spread=3.5,
|
|
108
479
|
volume=1.0,
|
|
480
|
+
octave_range=(-3, 3),
|
|
109
481
|
description="Duller muted earthenware tap",
|
|
110
482
|
),
|
|
111
483
|
|
|
@@ -128,6 +500,7 @@ MATERIALS = {
|
|
|
128
500
|
attack_ms=0.3,
|
|
129
501
|
freq_spread=2.5,
|
|
130
502
|
volume=0.8,
|
|
503
|
+
octave_range=(-3, 3),
|
|
131
504
|
description="Small metallic bell, classic ratios, long ring",
|
|
132
505
|
),
|
|
133
506
|
|
|
@@ -147,6 +520,7 @@ MATERIALS = {
|
|
|
147
520
|
attack_ms=0.3,
|
|
148
521
|
freq_spread=4.0,
|
|
149
522
|
volume=0.8,
|
|
523
|
+
octave_range=(-3, 3),
|
|
150
524
|
description="Water droplet, pitch bend down, liquid",
|
|
151
525
|
),
|
|
152
526
|
|
|
@@ -166,6 +540,7 @@ MATERIALS = {
|
|
|
166
540
|
attack_ms=0.2,
|
|
167
541
|
freq_spread=6.0,
|
|
168
542
|
volume=0.95,
|
|
543
|
+
octave_range=(-3, 3),
|
|
169
544
|
description="Sharp mechanical click, keyboard-like",
|
|
170
545
|
),
|
|
171
546
|
|
|
@@ -190,6 +565,7 @@ MATERIALS = {
|
|
|
190
565
|
attack_ms=0.6,
|
|
191
566
|
freq_spread=4.0,
|
|
192
567
|
volume=1.0,
|
|
568
|
+
octave_range=(-3, 3),
|
|
193
569
|
description="Hollow wooden tap, warm marimba-like resonance",
|
|
194
570
|
),
|
|
195
571
|
|
|
@@ -212,6 +588,7 @@ MATERIALS = {
|
|
|
212
588
|
attack_ms=0.4,
|
|
213
589
|
freq_spread=5.0,
|
|
214
590
|
volume=1.0,
|
|
591
|
+
octave_range=(-3, 3),
|
|
215
592
|
description="Dense slate tap, heavy and earthy",
|
|
216
593
|
),
|
|
217
594
|
|
|
@@ -233,6 +610,7 @@ MATERIALS = {
|
|
|
233
610
|
attack_ms=1.0,
|
|
234
611
|
freq_spread=3.5,
|
|
235
612
|
volume=0.85,
|
|
613
|
+
octave_range=(-3, 3),
|
|
236
614
|
description="Hollow tube resonance, odd harmonics, breathy and airy",
|
|
237
615
|
),
|
|
238
616
|
|
|
@@ -254,6 +632,7 @@ MATERIALS = {
|
|
|
254
632
|
attack_ms=0.3,
|
|
255
633
|
freq_spread=8.0,
|
|
256
634
|
volume=0.9,
|
|
635
|
+
octave_range=(-3, 3),
|
|
257
636
|
description="Warm crackling ember, fire-like with wide pitch scatter",
|
|
258
637
|
),
|
|
259
638
|
|
|
@@ -273,6 +652,7 @@ MATERIALS = {
|
|
|
273
652
|
attack_ms=2.5,
|
|
274
653
|
freq_spread=5.0,
|
|
275
654
|
volume=0.6,
|
|
655
|
+
octave_range=(-3, 3),
|
|
276
656
|
description="Soft breathy whisper, delicate airy texture",
|
|
277
657
|
),
|
|
278
658
|
|
|
@@ -295,6 +675,7 @@ MATERIALS = {
|
|
|
295
675
|
attack_ms=1.5,
|
|
296
676
|
freq_spread=2.0,
|
|
297
677
|
volume=0.75,
|
|
678
|
+
octave_range=(-3, 3),
|
|
298
679
|
description="Swirly ocean interference, dense phase beating",
|
|
299
680
|
),
|
|
300
681
|
|
|
@@ -314,23 +695,47 @@ MATERIALS = {
|
|
|
314
695
|
attack_ms=3.0,
|
|
315
696
|
freq_spread=4.0,
|
|
316
697
|
volume=0.7,
|
|
698
|
+
octave_range=(-3, 3),
|
|
317
699
|
description="Ultra-soft muffled earth, mossy dampness",
|
|
318
700
|
),
|
|
319
701
|
}
|
|
320
702
|
|
|
321
703
|
|
|
322
|
-
|
|
323
|
-
""
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
704
|
+
SOUND_SETS = {
|
|
705
|
+
"ambient": AMBIENT_MATERIALS,
|
|
706
|
+
"material": MATERIAL_MATERIALS,
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
# Default MATERIALS points to the default set for backwards compat
|
|
710
|
+
MATERIALS = SOUND_SETS[DEFAULT_SOUND_SET]
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def list_sound_sets() -> List[str]:
|
|
714
|
+
"""List all available sound set names."""
|
|
715
|
+
return list(SOUND_SETS.keys())
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def get_sound_set(name: str) -> dict:
|
|
719
|
+
"""Get a sound set by name."""
|
|
720
|
+
if name not in SOUND_SETS:
|
|
721
|
+
raise ValueError(f"Unknown sound set: {name}. Available: {list(SOUND_SETS.keys())}")
|
|
722
|
+
return SOUND_SETS[name]
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def get_material(name: str, sound_set: str = DEFAULT_SOUND_SET) -> Material:
|
|
726
|
+
"""Get a material by name from a given sound set."""
|
|
727
|
+
materials = get_sound_set(sound_set)
|
|
728
|
+
if name not in materials:
|
|
729
|
+
raise ValueError(f"Unknown character: {name}. Available: {list(materials.keys())}")
|
|
730
|
+
return materials[name]
|
|
327
731
|
|
|
328
732
|
|
|
329
|
-
def get_random_material() -> Material:
|
|
330
|
-
"""Get a random material."""
|
|
331
|
-
|
|
733
|
+
def get_random_material(sound_set: str = DEFAULT_SOUND_SET) -> Material:
|
|
734
|
+
"""Get a random material from a given sound set."""
|
|
735
|
+
materials = get_sound_set(sound_set)
|
|
736
|
+
return random.choice(list(materials.values()))
|
|
332
737
|
|
|
333
738
|
|
|
334
|
-
def list_materials() -> List[str]:
|
|
335
|
-
"""List all available material names."""
|
|
336
|
-
return list(
|
|
739
|
+
def list_materials(sound_set: str = DEFAULT_SOUND_SET) -> List[str]:
|
|
740
|
+
"""List all available material names in a sound set."""
|
|
741
|
+
return list(get_sound_set(sound_set).keys())
|
claudible/monitor.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""I/O activity tracking and event detection."""
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
from typing import Callable, Optional
|
|
4
|
+
from typing import Callable, Optional, Union
|
|
5
5
|
import threading
|
|
6
6
|
|
|
7
7
|
|
|
@@ -17,7 +17,7 @@ class ActivityMonitor:
|
|
|
17
17
|
|
|
18
18
|
def __init__(
|
|
19
19
|
self,
|
|
20
|
-
on_grain: Callable[[], None],
|
|
20
|
+
on_grain: Callable[[str], None],
|
|
21
21
|
on_chime: Callable[[], None],
|
|
22
22
|
on_attention: Callable[[], None],
|
|
23
23
|
attention_seconds: float = 30.0,
|
|
@@ -68,7 +68,7 @@ class ActivityMonitor:
|
|
|
68
68
|
if not self._reverse_playing:
|
|
69
69
|
self._reverse_playing = True
|
|
70
70
|
self.on_chime()
|
|
71
|
-
self.on_grain()
|
|
71
|
+
self.on_grain("")
|
|
72
72
|
time.sleep(reverse_interval)
|
|
73
73
|
else:
|
|
74
74
|
if self._reverse_playing:
|
|
@@ -109,11 +109,11 @@ class ActivityMonitor:
|
|
|
109
109
|
self._consecutive_newlines = 0
|
|
110
110
|
else:
|
|
111
111
|
self._consecutive_newlines = 0
|
|
112
|
-
self._maybe_trigger_grain()
|
|
112
|
+
self._maybe_trigger_grain(char)
|
|
113
113
|
|
|
114
|
-
def _maybe_trigger_grain(self):
|
|
114
|
+
def _maybe_trigger_grain(self, char: str = ""):
|
|
115
115
|
"""Trigger a grain if enough time has passed (throttling)."""
|
|
116
116
|
now = time.time()
|
|
117
117
|
if now - self._last_grain_time >= self._min_grain_interval:
|
|
118
118
|
self._last_grain_time = now
|
|
119
|
-
self.on_grain()
|
|
119
|
+
self.on_grain(char)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claudible
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary: Ambient audio soundscape feedback for terminal output - an opus for your terminals
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Ambient audio soundscape feedback for terminal output - an opus for your terminals, claude code by default
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/anthropics/claudible
|
|
7
7
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -40,7 +40,9 @@ Requires-Dist: sounddevice>=0.4.0
|
|
|
40
40
|
|
|
41
41
|
*An opus for your terminals.*
|
|
42
42
|
|
|
43
|
-
Possibly the most annoying Claude utility ever made but here it is anyway.
|
|
43
|
+
Possibly the most annoying Claude utility ever made but here it is anyway.
|
|
44
|
+
|
|
45
|
+
Ambient audio soundscape feedback for terminal output. It really shines with a lot of busy busy Claude Code terminals running at the same time.
|
|
44
46
|
|
|
45
47
|
## 🎼 The Idea
|
|
46
48
|
|
|
@@ -64,7 +66,7 @@ pip install claudible
|
|
|
64
66
|
## 🎹 Usage
|
|
65
67
|
|
|
66
68
|
```bash
|
|
67
|
-
# Run claude with audio feedback (default)
|
|
69
|
+
# Run claude with audio feedback (default: ambient sound set)
|
|
68
70
|
claudible
|
|
69
71
|
|
|
70
72
|
# Run a different command
|
|
@@ -74,13 +76,20 @@ claudible "python my_script.py"
|
|
|
74
76
|
some-command 2>&1 | claudible --pipe
|
|
75
77
|
|
|
76
78
|
# Choose a sound character
|
|
77
|
-
claudible --character
|
|
79
|
+
claudible --character drift
|
|
80
|
+
|
|
81
|
+
# Use the percussive material sound set
|
|
82
|
+
claudible --set material -c bell
|
|
78
83
|
|
|
79
84
|
# Adjust volume
|
|
80
85
|
claudible --volume 0.3
|
|
81
86
|
|
|
82
|
-
# List available characters
|
|
87
|
+
# List available characters across all sets
|
|
83
88
|
claudible --list-characters
|
|
89
|
+
|
|
90
|
+
# Demo all sound characters in a set
|
|
91
|
+
claudible --demo
|
|
92
|
+
claudible --demo --set material
|
|
84
93
|
```
|
|
85
94
|
|
|
86
95
|
### 🔇 Reverse Mode
|
|
@@ -110,7 +119,26 @@ npm run dev 2>&1 | claudible --pipe
|
|
|
110
119
|
tail -f /var/log/app.log | claudible --pipe -c droplet
|
|
111
120
|
```
|
|
112
121
|
|
|
113
|
-
## 🎻 Sound
|
|
122
|
+
## 🎻 Sound Sets
|
|
123
|
+
|
|
124
|
+
Claudible ships with two sound sets. The **ambient** set (default) produces soft, throbbing textures with rich overtones. The **material** set has crisp, percussive sounds modelled on physical materials.
|
|
125
|
+
|
|
126
|
+
### Ambient (default) `--set ambient`
|
|
127
|
+
|
|
128
|
+
| Character | | Description |
|
|
129
|
+
|-----------|---|-------------|
|
|
130
|
+
| `drift` | 🌊 | Slow undulating low throb, gentle beating pairs |
|
|
131
|
+
| `tide` | 🌀 | Oceanic wash, phase interference, wide and enveloping |
|
|
132
|
+
| `breath` | 💨 | Soft exhale texture, breathy warmth with filtered noise |
|
|
133
|
+
| `haze` | 🌫️ | Dense foggy overtones, warm and thick with close partial clusters |
|
|
134
|
+
| `pulse` | 💗 | Gentle rhythmic throbbing from detuned pairs, hypnotic |
|
|
135
|
+
| `glow` | 🕯️ | Warm radiant harmonics, rich natural overtone series |
|
|
136
|
+
| `cloud` | ☁️ | Diffuse and soft, massive reverb space, floating |
|
|
137
|
+
| `murmur` | 🫧 | Low gentle rumble, warm harmonic murmur with subtle throb |
|
|
138
|
+
| `shimmer` | ✨ | High ethereal overtones, floating and luminous |
|
|
139
|
+
| `deep` | 🎚️ | Sub-bass throb, felt more than heard, very deep and slow |
|
|
140
|
+
|
|
141
|
+
### Material `--set material`
|
|
114
142
|
|
|
115
143
|
| Character | | Description |
|
|
116
144
|
|-----------|---|-------------|
|
|
@@ -134,11 +162,13 @@ tail -f /var/log/app.log | claudible --pipe -c droplet
|
|
|
134
162
|
| Flag | Description |
|
|
135
163
|
|------|-------------|
|
|
136
164
|
| `--pipe` | 📥 Read from stdin instead of wrapping |
|
|
137
|
-
| `--
|
|
165
|
+
| `--set`, `-s` | 🎼 Sound set: `ambient` (default), `material` |
|
|
166
|
+
| `--character`, `-c` | 🎵 Sound character within the set |
|
|
138
167
|
| `--volume`, `-v` | 🔊 Volume 0.0–1.0 (default: 0.5) |
|
|
139
168
|
| `--attention`, `-a` | ⏰ Silence alert seconds (default: 30) |
|
|
140
169
|
| `--reverse`, `-r` | 🔄 Reverse mode: sound during silence, quiet during output |
|
|
141
|
-
| `--list-characters` | 📋 Show
|
|
170
|
+
| `--list-characters` | 📋 Show all characters across all sets |
|
|
171
|
+
| `--demo` | 🔊 Demo characters in the selected set |
|
|
142
172
|
|
|
143
173
|
## 🛠️ Development
|
|
144
174
|
|
|
@@ -158,6 +188,9 @@ echo "test" | PYTHONPATH=src python3 -m claudible --pipe -c glass
|
|
|
158
188
|
|
|
159
189
|
# List characters
|
|
160
190
|
PYTHONPATH=src python3 -m claudible --list-characters
|
|
191
|
+
|
|
192
|
+
# Demo all characters
|
|
193
|
+
PYTHONPATH=src python3 -m claudible --demo
|
|
161
194
|
```
|
|
162
195
|
|
|
163
196
|
## 📜 License
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
claudible/__init__.py,sha256=xSLL4yfc1p79BWVb65PJBTFMxyJ2PWuu7pCLYcCuOVM,96
|
|
2
|
+
claudible/__main__.py,sha256=5mFnPjtOjk5Hw-mvUhRYtwXVnXFR7ZbXKC27VIXu_dI,106
|
|
3
|
+
claudible/audio.py,sha256=8lHgp74Igh6Hs4HAbTZIRos-LVA0HCxJ71cTlyAlmFk,15945
|
|
4
|
+
claudible/cli.py,sha256=gLGaNDFVf4Bfr9UAH2yZUPeTUVxZ-5OR9DNjEwTRNsI,6549
|
|
5
|
+
claudible/materials.py,sha256=M49vT0oL63Iv1e8qxa2bsz3G6sQgPXwsJ3kAHMggX7g,21039
|
|
6
|
+
claudible/monitor.py,sha256=KAnb0rmnuTDanD7pgls50Oxd3-TlhXv2tUDjX8RkHIg,4106
|
|
7
|
+
claudible-0.1.4.dist-info/METADATA,sha256=qHEjYY2MlrMjnC7aa4oTij0k0N4PpMvNw0Zmvku6MUc,6726
|
|
8
|
+
claudible-0.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
9
|
+
claudible-0.1.4.dist-info/entry_points.txt,sha256=4tBIsIGcdtJ-1N9YZ3MrADyNvPBDcUxVgAnt9IuC7TA,49
|
|
10
|
+
claudible-0.1.4.dist-info/top_level.txt,sha256=4ZB16Aa5ZDS12bmxzrQmAJejjsTgQ9Yozwc0Z3qpJp4,10
|
|
11
|
+
claudible-0.1.4.dist-info/RECORD,,
|
claudible-0.1.2.dist-info/RECORD
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
claudible/__init__.py,sha256=xSLL4yfc1p79BWVb65PJBTFMxyJ2PWuu7pCLYcCuOVM,96
|
|
2
|
-
claudible/__main__.py,sha256=5mFnPjtOjk5Hw-mvUhRYtwXVnXFR7ZbXKC27VIXu_dI,106
|
|
3
|
-
claudible/audio.py,sha256=6Do7_g5SUfJkMGSH_c_XGG4YiC2uakhaDTy8xYAp7rE,12043
|
|
4
|
-
claudible/cli.py,sha256=j14ZnHT7L_efA67U-3hLSqUsgf2rvlMYgs1b6w3X2pU,4803
|
|
5
|
-
claudible/materials.py,sha256=6OsobcWOFvLBCuvzkeWi9C7fYHFnW9RED6nEElK-Ki4,9517
|
|
6
|
-
claudible/monitor.py,sha256=iPCaqdO-CDp1CoIie2VtddUUjG1A_xucbFJ1DPk3MKk,4070
|
|
7
|
-
claudible-0.1.2.dist-info/METADATA,sha256=b38veaujlcjfAzO8G7EaKK5eXWVyb7Qqf5Cojg1nKMw,5095
|
|
8
|
-
claudible-0.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
9
|
-
claudible-0.1.2.dist-info/entry_points.txt,sha256=4tBIsIGcdtJ-1N9YZ3MrADyNvPBDcUxVgAnt9IuC7TA,49
|
|
10
|
-
claudible-0.1.2.dist-info/top_level.txt,sha256=4ZB16Aa5ZDS12bmxzrQmAJejjsTgQ9Yozwc0Z3qpJp4,10
|
|
11
|
-
claudible-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|