claudible 0.1.3__py3-none-any.whl → 0.1.5__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 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
@@ -142,11 +143,11 @@ class SoundEngine:
142
143
  # --- Sound generation ---
143
144
 
144
145
  def _build_grain_cache(self) -> list:
145
- """Build grain cache with voices at full-octave intervals, biased low + high."""
146
+ """Build grain cache with voices at full-octave intervals, filtered by material range."""
146
147
  # Each register is a full octave apart.
147
148
  # (octave_shift, noise_mult, decay_mult, duration_mult)
148
149
  # Multiple character variations per register.
149
- voices = [
150
+ all_voices = [
150
151
  # --- oct -3: sub rumble (3 voices) ---
151
152
  (-3, 3.5, 2.5, 1.8), # sub thump
152
153
  (-3, 2.0, 3.0, 2.0), # sub knock
@@ -181,6 +182,8 @@ class SoundEngine:
181
182
  ( 3, 0.3, 0.2, 0.45), # air tick
182
183
  ( 3, 0.05, 0.3, 0.35), # air wisp
183
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]
184
187
  cache = []
185
188
  for octave, noise_m, decay_m, dur_m in voices:
186
189
  cache.append(self._generate_grain(
@@ -269,6 +272,16 @@ class SoundEngine:
269
272
  h = ((h * 33) ^ ord(c)) & 0xFFFFFFFF
270
273
  return h
271
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
+
272
285
  def play_grain(self, token: str = ""):
273
286
  """Play a grain/sparkle sound, deterministic for a given token."""
274
287
  if token:
@@ -280,16 +293,30 @@ class SoundEngine:
280
293
  idx = rng.randint(len(self._grain_cache))
281
294
  grain = self._grain_cache[idx].copy()
282
295
 
283
- # Narrow pitch micro-variation (±1.5 semitones) stays in its register
284
- pitch_shift = 2 ** (rng.uniform(-1.5, 1.5) / 12)
285
- if abs(pitch_shift - 1.0) > 0.001:
286
- new_len = int(len(grain) / pitch_shift)
287
- if new_len > 0:
288
- grain = np.interp(
289
- np.linspace(0, len(grain) - 1, new_len),
290
- np.arange(len(grain)),
291
- grain,
292
- ).astype(np.float32)
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)
293
320
 
294
321
  grain *= rng.uniform(0.7, 1.0)
295
322
 
claudible/cli.py CHANGED
@@ -10,14 +10,18 @@ import time
10
10
  from typing import Optional
11
11
 
12
12
  from .audio import SoundEngine
13
- from .materials import get_material, get_random_material, list_materials, MATERIALS
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
 
17
- def run_demo(volume: float):
18
- """Play a short demo of every sound character."""
19
- print("Claudible character demo\n", file=sys.stderr)
20
- for name, mat in MATERIALS.items():
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():
21
25
  print(f" {name:10} - {mat.description}", file=sys.stderr)
22
26
  engine = SoundEngine(material=mat, volume=volume)
23
27
  engine.start()
@@ -129,10 +133,15 @@ def main():
129
133
  action='store_true',
130
134
  help='Pipe mode: read from stdin instead of wrapping a command',
131
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
+ )
132
142
  parser.add_argument(
133
143
  '--character', '-c',
134
- choices=list_materials(),
135
- help='Sound character (default: random)',
144
+ help='Sound character (default: random). Use --list-characters to see options.',
136
145
  )
137
146
  parser.add_argument(
138
147
  '--volume', '-v',
@@ -164,21 +173,33 @@ def main():
164
173
 
165
174
  args = parser.parse_args()
166
175
 
176
+ sound_set = args.set
177
+
167
178
  if args.list_characters:
168
- print("Available sound characters:\n")
169
- for name, mat in MATERIALS.items():
170
- print(f" {name:10} - {mat.description}")
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()
171
185
  return
172
186
 
173
187
  if args.demo:
174
188
  volume = max(0.0, min(1.0, args.volume))
175
- run_demo(volume)
189
+ run_demo(volume, sound_set)
176
190
  return
177
191
 
192
+ # Validate character against the chosen set
178
193
  if args.character:
179
- material = get_material(args.character)
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)
180
201
  else:
181
- material = get_random_material()
202
+ material = get_random_material(sound_set)
182
203
 
183
204
  volume = max(0.0, min(1.0, args.volume))
184
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
- MATERIALS = {
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
- def get_material(name: str) -> Material:
323
- """Get a material by name."""
324
- if name not in MATERIALS:
325
- raise ValueError(f"Unknown material: {name}. Available: {list(MATERIALS.keys())}")
326
- return MATERIALS[name]
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
- return random.choice(list(MATERIALS.values()))
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(MATERIALS.keys())
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())
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claudible
3
- Version: 0.1.3
4
- Summary: Ambient audio soundscape feedback for terminal output - an opus for your terminals
3
+ Version: 0.1.5
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. Ambient audio soundscape feedback for terminal output.
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 when used 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,16 +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 crystal
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
84
89
 
85
- # Demo all sound characters
90
+ # Demo all sound characters in a set
86
91
  claudible --demo
92
+ claudible --demo --set material
87
93
  ```
88
94
 
89
95
  ### 🔇 Reverse Mode
@@ -113,7 +119,26 @@ npm run dev 2>&1 | claudible --pipe
113
119
  tail -f /var/log/app.log | claudible --pipe -c droplet
114
120
  ```
115
121
 
116
- ## 🎻 Sound Characters
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`
117
142
 
118
143
  | Character | | Description |
119
144
  |-----------|---|-------------|
@@ -137,12 +162,13 @@ tail -f /var/log/app.log | claudible --pipe -c droplet
137
162
  | Flag | Description |
138
163
  |------|-------------|
139
164
  | `--pipe` | 📥 Read from stdin instead of wrapping |
140
- | `--character`, `-c` | 🎵 Sound character |
165
+ | `--set`, `-s` | 🎼 Sound set: `ambient` (default), `material` |
166
+ | `--character`, `-c` | 🎵 Sound character within the set |
141
167
  | `--volume`, `-v` | 🔊 Volume 0.0–1.0 (default: 0.5) |
142
168
  | `--attention`, `-a` | ⏰ Silence alert seconds (default: 30) |
143
169
  | `--reverse`, `-r` | 🔄 Reverse mode: sound during silence, quiet during output |
144
- | `--list-characters` | 📋 Show presets |
145
- | `--demo` | 🔊 Play a short demo of each sound character |
170
+ | `--list-characters` | 📋 Show all characters across all sets |
171
+ | `--demo` | 🔊 Demo characters in the selected set |
146
172
 
147
173
  ## 🛠️ Development
148
174
 
@@ -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.5.dist-info/METADATA,sha256=PcTB4TJgiYVPqMB1xQH32-VIpZkkyghZ0Xr09yeO9ms,6736
8
+ claudible-0.1.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
9
+ claudible-0.1.5.dist-info/entry_points.txt,sha256=4tBIsIGcdtJ-1N9YZ3MrADyNvPBDcUxVgAnt9IuC7TA,49
10
+ claudible-0.1.5.dist-info/top_level.txt,sha256=4ZB16Aa5ZDS12bmxzrQmAJejjsTgQ9Yozwc0Z3qpJp4,10
11
+ claudible-0.1.5.dist-info/RECORD,,
@@ -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=NXINffeLQlH89JJli-QF29WyYtpPl1BjcmfStvJ7geE,14741
4
- claudible/cli.py,sha256=R7WttU-idl_S4yBx_bXKKRtftkdBS2_ioiNgZQ68a4w,5665
5
- claudible/materials.py,sha256=6OsobcWOFvLBCuvzkeWi9C7fYHFnW9RED6nEElK-Ki4,9517
6
- claudible/monitor.py,sha256=KAnb0rmnuTDanD7pgls50Oxd3-TlhXv2tUDjX8RkHIg,4106
7
- claudible-0.1.3.dist-info/METADATA,sha256=SjWWWyDZIwGt1eQmcbQr_8UMCeCILUlS1fYb0mKrL10,5269
8
- claudible-0.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
9
- claudible-0.1.3.dist-info/entry_points.txt,sha256=4tBIsIGcdtJ-1N9YZ3MrADyNvPBDcUxVgAnt9IuC7TA,49
10
- claudible-0.1.3.dist-info/top_level.txt,sha256=4ZB16Aa5ZDS12bmxzrQmAJejjsTgQ9Yozwc0Z3qpJp4,10
11
- claudible-0.1.3.dist-info/RECORD,,