claudible 0.1.2__py3-none-any.whl → 0.1.3__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 +104 -39
- claudible/cli.py +28 -0
- claudible/monitor.py +6 -6
- {claudible-0.1.2.dist-info → claudible-0.1.3.dist-info}/METADATA +8 -1
- claudible-0.1.3.dist-info/RECORD +11 -0
- claudible-0.1.2.dist-info/RECORD +0 -11
- {claudible-0.1.2.dist-info → claudible-0.1.3.dist-info}/WHEEL +0 -0
- {claudible-0.1.2.dist-info → claudible-0.1.3.dist-info}/entry_points.txt +0 -0
- {claudible-0.1.2.dist-info → claudible-0.1.3.dist-info}/top_level.txt +0 -0
claudible/audio.py
CHANGED
|
@@ -33,8 +33,8 @@ class SoundEngine:
|
|
|
33
33
|
self._write_pos = 0
|
|
34
34
|
self._read_pos = 0
|
|
35
35
|
|
|
36
|
-
# Pre-generate
|
|
37
|
-
self._grain_cache =
|
|
36
|
+
# Pre-generate grains spread across registers for tonal variety
|
|
37
|
+
self._grain_cache = self._build_grain_cache()
|
|
38
38
|
self._cache_index = 0
|
|
39
39
|
|
|
40
40
|
def start(self):
|
|
@@ -141,77 +141,147 @@ class SoundEngine:
|
|
|
141
141
|
|
|
142
142
|
# --- Sound generation ---
|
|
143
143
|
|
|
144
|
-
def
|
|
144
|
+
def _build_grain_cache(self) -> list:
|
|
145
|
+
"""Build grain cache with voices at full-octave intervals, biased low + high."""
|
|
146
|
+
# Each register is a full octave apart.
|
|
147
|
+
# (octave_shift, noise_mult, decay_mult, duration_mult)
|
|
148
|
+
# Multiple character variations per register.
|
|
149
|
+
voices = [
|
|
150
|
+
# --- oct -3: sub rumble (3 voices) ---
|
|
151
|
+
(-3, 3.5, 2.5, 1.8), # sub thump
|
|
152
|
+
(-3, 2.0, 3.0, 2.0), # sub knock
|
|
153
|
+
(-3, 2.8, 2.0, 1.6), # sub punch
|
|
154
|
+
|
|
155
|
+
# --- oct -2: deep (3 voices) ---
|
|
156
|
+
(-2, 2.8, 1.8, 1.5), # deep percussive
|
|
157
|
+
(-2, 1.2, 2.0, 1.4), # deep tonal
|
|
158
|
+
(-2, 2.0, 1.5, 1.3), # deep knock
|
|
159
|
+
|
|
160
|
+
# --- oct -1: low (3 voices) ---
|
|
161
|
+
(-1, 1.8, 1.4, 1.2), # low thump
|
|
162
|
+
(-1, 0.8, 1.2, 1.1), # low warm
|
|
163
|
+
(-1, 1.4, 1.6, 1.2), # low snap
|
|
164
|
+
|
|
165
|
+
# --- oct 0: mid (2 voices) ---
|
|
166
|
+
( 0, 1.0, 1.0, 1.0), # mid click
|
|
167
|
+
( 0, 0.6, 0.8, 0.9), # mid soft
|
|
168
|
+
|
|
169
|
+
# --- oct +1: high (3 voices) ---
|
|
170
|
+
( 1, 0.5, 0.6, 0.7), # high ping
|
|
171
|
+
( 1, 0.8, 0.5, 0.7), # high tick
|
|
172
|
+
( 1, 0.3, 0.7, 0.8), # high ring
|
|
173
|
+
|
|
174
|
+
# --- oct +2: bright (3 voices) ---
|
|
175
|
+
( 2, 0.2, 0.4, 0.55), # bright ting
|
|
176
|
+
( 2, 0.5, 0.3, 0.6), # bright click
|
|
177
|
+
( 2, 0.15, 0.5, 0.5), # bright shimmer
|
|
178
|
+
|
|
179
|
+
# --- oct +3: air (3 voices) ---
|
|
180
|
+
( 3, 0.1, 0.25, 0.4), # air sparkle
|
|
181
|
+
( 3, 0.3, 0.2, 0.45), # air tick
|
|
182
|
+
( 3, 0.05, 0.3, 0.35), # air wisp
|
|
183
|
+
]
|
|
184
|
+
cache = []
|
|
185
|
+
for octave, noise_m, decay_m, dur_m in voices:
|
|
186
|
+
cache.append(self._generate_grain(
|
|
187
|
+
octave_shift=octave,
|
|
188
|
+
noise_mult=noise_m,
|
|
189
|
+
decay_mult=decay_m,
|
|
190
|
+
duration_mult=dur_m,
|
|
191
|
+
))
|
|
192
|
+
return cache
|
|
193
|
+
|
|
194
|
+
def _generate_grain(self, octave_shift: float = 0.0, noise_mult: float = 1.0,
|
|
195
|
+
decay_mult: float = 1.0, duration_mult: float = 1.0) -> np.ndarray:
|
|
145
196
|
"""Generate a single grain sound from the material's physical model."""
|
|
146
197
|
m = self.material
|
|
147
|
-
|
|
148
|
-
duration = m.grain_duration * np.random.uniform(0.85, 1.15)
|
|
198
|
+
duration = m.grain_duration * duration_mult * 0.5 * np.random.uniform(0.85, 1.15)
|
|
149
199
|
n = int(duration * self.SAMPLE_RATE)
|
|
150
200
|
t = np.linspace(0, duration, n, dtype=np.float32)
|
|
151
201
|
|
|
152
202
|
grain = np.zeros(n, dtype=np.float32)
|
|
153
203
|
|
|
154
|
-
#
|
|
155
|
-
base_freq = m.base_freq * (2 **
|
|
204
|
+
# Base frequency shifted by register
|
|
205
|
+
base_freq = m.base_freq * (2 ** octave_shift)
|
|
206
|
+
base_freq *= (2 ** (np.random.uniform(-m.freq_spread, m.freq_spread) / 12))
|
|
156
207
|
|
|
157
|
-
# Generate each partial with its own decay, detuning, and random phase
|
|
158
208
|
for i, (ratio, amp) in enumerate(m.partials):
|
|
159
209
|
detune_cents = np.random.uniform(-m.detune_amount, m.detune_amount)
|
|
160
210
|
freq = base_freq * ratio * (2 ** (detune_cents / 1200))
|
|
161
|
-
# Extra micro-variation per partial
|
|
162
211
|
freq *= np.random.uniform(0.997, 1.003)
|
|
163
212
|
phase = np.random.uniform(0, 2 * np.pi)
|
|
164
213
|
|
|
165
214
|
decay_rate = m.decay_rates[i] if i < len(m.decay_rates) else m.decay_rates[-1]
|
|
215
|
+
decay_rate *= decay_mult
|
|
166
216
|
envelope = np.exp(-decay_rate * t)
|
|
167
217
|
|
|
168
218
|
grain += amp * envelope * np.sin(2 * np.pi * freq * t + phase)
|
|
169
219
|
|
|
170
|
-
# Noise transient
|
|
220
|
+
# Noise transient — louder for low percussive grains
|
|
171
221
|
if m.attack_noise > 0:
|
|
172
222
|
noise = np.random.randn(n).astype(np.float32)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
noise_env = np.exp(-150 * t)
|
|
223
|
+
noise_cutoff = max(m.noise_freq * (0.5 if octave_shift < -0.5 else 1.0), 200)
|
|
224
|
+
noise = self._highpass(noise, noise_cutoff)
|
|
225
|
+
noise_env = np.exp(-150 * t / max(decay_mult, 0.5))
|
|
176
226
|
noise_env *= np.clip(
|
|
177
227
|
1.0 + 0.3 * np.random.randn(n).astype(np.float32) * np.exp(-200 * t),
|
|
178
228
|
0, 1,
|
|
179
229
|
)
|
|
180
|
-
grain += m.attack_noise * 0.3 * noise * noise_env
|
|
230
|
+
grain += m.attack_noise * noise_mult * 0.3 * noise * noise_env
|
|
181
231
|
|
|
182
232
|
# Attack shaping
|
|
183
233
|
attack_samples = max(int(m.attack_ms / 1000 * self.SAMPLE_RATE), 2)
|
|
184
234
|
if attack_samples < n:
|
|
185
235
|
grain[:attack_samples] *= np.linspace(0, 1, attack_samples, dtype=np.float32)
|
|
186
236
|
|
|
187
|
-
# Pitch drop
|
|
188
|
-
if m.pitch_drop
|
|
189
|
-
|
|
237
|
+
# Pitch drop — more dramatic for low grains
|
|
238
|
+
pitch_drop = m.pitch_drop if octave_shift >= 0 else m.pitch_drop ** (1.0 + abs(octave_shift) * 0.3)
|
|
239
|
+
if pitch_drop != 1.0:
|
|
240
|
+
pitch_env = np.linspace(1.0, pitch_drop, n)
|
|
190
241
|
phase_mod = np.cumsum(pitch_env) / np.sum(pitch_env) * n
|
|
191
242
|
indices = np.clip(phase_mod.astype(int), 0, n - 1)
|
|
192
243
|
grain = grain[indices]
|
|
193
244
|
|
|
194
|
-
#
|
|
195
|
-
|
|
245
|
+
# Reverb — less for percussive lows (tighter), more for highs
|
|
246
|
+
reverb_amt = m.reverb_amount * (0.6 if octave_shift < -0.5 else 1.0 + max(octave_shift, 0) * 0.2)
|
|
247
|
+
grain = self._apply_reverb(grain, reverb_amt, m.room_size, m.reverb_damping)
|
|
196
248
|
|
|
197
|
-
#
|
|
198
|
-
|
|
249
|
+
# Small pre-delay + extra tail reverb
|
|
250
|
+
delay_ms = 15
|
|
251
|
+
delay_samples = int(delay_ms * self.SAMPLE_RATE / 1000)
|
|
252
|
+
tail_len = n + delay_samples + int(0.08 * self.SAMPLE_RATE)
|
|
253
|
+
out = np.zeros(tail_len, dtype=np.float32)
|
|
254
|
+
out[delay_samples:delay_samples + len(grain)] = grain
|
|
255
|
+
# Light tail reverb on top
|
|
256
|
+
out = self._apply_reverb(out, 0.15, room_size=1.2, damping=0.4)
|
|
199
257
|
|
|
200
|
-
|
|
201
|
-
peak = np.max(np.abs(grain))
|
|
258
|
+
peak = np.max(np.abs(out))
|
|
202
259
|
if peak > 0:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
260
|
+
out = out / peak * 0.4 * m.volume
|
|
261
|
+
|
|
262
|
+
return out.astype(np.float32)
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def _token_hash(token: str) -> int:
|
|
266
|
+
"""Deterministic hash from token content, stable across sessions."""
|
|
267
|
+
h = 5381
|
|
268
|
+
for c in token:
|
|
269
|
+
h = ((h * 33) ^ ord(c)) & 0xFFFFFFFF
|
|
270
|
+
return h
|
|
271
|
+
|
|
272
|
+
def play_grain(self, token: str = ""):
|
|
273
|
+
"""Play a grain/sparkle sound, deterministic for a given token."""
|
|
274
|
+
if token:
|
|
275
|
+
h = self._token_hash(token)
|
|
276
|
+
rng = np.random.RandomState(h)
|
|
277
|
+
else:
|
|
278
|
+
rng = np.random
|
|
279
|
+
|
|
280
|
+
idx = rng.randint(len(self._grain_cache))
|
|
211
281
|
grain = self._grain_cache[idx].copy()
|
|
212
282
|
|
|
213
|
-
#
|
|
214
|
-
pitch_shift = 2 ** (
|
|
283
|
+
# Narrow pitch micro-variation (±1.5 semitones) — stays in its register
|
|
284
|
+
pitch_shift = 2 ** (rng.uniform(-1.5, 1.5) / 12)
|
|
215
285
|
if abs(pitch_shift - 1.0) > 0.001:
|
|
216
286
|
new_len = int(len(grain) / pitch_shift)
|
|
217
287
|
if new_len > 0:
|
|
@@ -221,12 +291,7 @@ class SoundEngine:
|
|
|
221
291
|
grain,
|
|
222
292
|
).astype(np.float32)
|
|
223
293
|
|
|
224
|
-
|
|
225
|
-
grain *= np.random.uniform(0.6, 1.0)
|
|
226
|
-
|
|
227
|
-
# Regenerate cached grains aggressively for variety
|
|
228
|
-
if np.random.random() < 0.5:
|
|
229
|
-
self._grain_cache[np.random.randint(len(self._grain_cache))] = self._generate_grain()
|
|
294
|
+
grain *= rng.uniform(0.7, 1.0)
|
|
230
295
|
|
|
231
296
|
self._add_to_buffer(grain)
|
|
232
297
|
|
claudible/cli.py
CHANGED
|
@@ -14,6 +14,24 @@ from .materials import get_material, get_random_material, list_materials, MATERI
|
|
|
14
14
|
from .monitor import ActivityMonitor
|
|
15
15
|
|
|
16
16
|
|
|
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():
|
|
21
|
+
print(f" {name:10} - {mat.description}", file=sys.stderr)
|
|
22
|
+
engine = SoundEngine(material=mat, volume=volume)
|
|
23
|
+
engine.start()
|
|
24
|
+
# Play a burst of grains then a chime
|
|
25
|
+
for i in range(8):
|
|
26
|
+
engine.play_grain(chr(ord('a') + i))
|
|
27
|
+
time.sleep(0.06)
|
|
28
|
+
time.sleep(0.15)
|
|
29
|
+
engine.play_chime()
|
|
30
|
+
time.sleep(0.6)
|
|
31
|
+
engine.stop()
|
|
32
|
+
print("\nDone.", file=sys.stderr)
|
|
33
|
+
|
|
34
|
+
|
|
17
35
|
def run_pipe_mode(engine: SoundEngine, monitor: ActivityMonitor):
|
|
18
36
|
"""Run in pipe mode, reading from stdin."""
|
|
19
37
|
engine.start()
|
|
@@ -138,6 +156,11 @@ def main():
|
|
|
138
156
|
action='store_true',
|
|
139
157
|
help='List available sound characters',
|
|
140
158
|
)
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
'--demo',
|
|
161
|
+
action='store_true',
|
|
162
|
+
help='Play a short demo of each sound character',
|
|
163
|
+
)
|
|
141
164
|
|
|
142
165
|
args = parser.parse_args()
|
|
143
166
|
|
|
@@ -147,6 +170,11 @@ def main():
|
|
|
147
170
|
print(f" {name:10} - {mat.description}")
|
|
148
171
|
return
|
|
149
172
|
|
|
173
|
+
if args.demo:
|
|
174
|
+
volume = max(0.0, min(1.0, args.volume))
|
|
175
|
+
run_demo(volume)
|
|
176
|
+
return
|
|
177
|
+
|
|
150
178
|
if args.character:
|
|
151
179
|
material = get_material(args.character)
|
|
152
180
|
else:
|
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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claudible
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Ambient audio soundscape feedback for terminal output - an opus for your terminals
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/anthropics/claudible
|
|
@@ -81,6 +81,9 @@ claudible --volume 0.3
|
|
|
81
81
|
|
|
82
82
|
# List available characters
|
|
83
83
|
claudible --list-characters
|
|
84
|
+
|
|
85
|
+
# Demo all sound characters
|
|
86
|
+
claudible --demo
|
|
84
87
|
```
|
|
85
88
|
|
|
86
89
|
### 🔇 Reverse Mode
|
|
@@ -139,6 +142,7 @@ tail -f /var/log/app.log | claudible --pipe -c droplet
|
|
|
139
142
|
| `--attention`, `-a` | ⏰ Silence alert seconds (default: 30) |
|
|
140
143
|
| `--reverse`, `-r` | 🔄 Reverse mode: sound during silence, quiet during output |
|
|
141
144
|
| `--list-characters` | 📋 Show presets |
|
|
145
|
+
| `--demo` | 🔊 Play a short demo of each sound character |
|
|
142
146
|
|
|
143
147
|
## 🛠️ Development
|
|
144
148
|
|
|
@@ -158,6 +162,9 @@ echo "test" | PYTHONPATH=src python3 -m claudible --pipe -c glass
|
|
|
158
162
|
|
|
159
163
|
# List characters
|
|
160
164
|
PYTHONPATH=src python3 -m claudible --list-characters
|
|
165
|
+
|
|
166
|
+
# Demo all characters
|
|
167
|
+
PYTHONPATH=src python3 -m claudible --demo
|
|
161
168
|
```
|
|
162
169
|
|
|
163
170
|
## 📜 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=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,,
|
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
|