claudible 0.1.0__tar.gz

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.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: claudible
3
+ Version: 0.1.0
4
+ Summary: Ambient audio soundscape feedback for terminal output - an opus for your terminals
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/qwertykeith/claudible
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Environment :: Console
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Multimedia :: Sound/Audio
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: numpy>=1.20.0
23
+ Requires-Dist: scipy>=1.7.0
24
+ Requires-Dist: sounddevice>=0.4.0
25
+
26
+ # claudible
27
+
28
+ *An opus for your terminals.*
29
+
30
+ Possibly the most annoying Claude utility ever made but here it is anyway. Ambient audio soundscape feedback for terminal output.
31
+
32
+ ## The Idea
33
+
34
+ Imagine you are working in a factory. Each Claude Code session is a machine that must be attended to. When it's working, you hear it - crystalline sparkles as text flows, soft chimes when tasks complete. When it goes quiet, you know something needs your attention.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install claudible
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```bash
45
+ # Run claude with audio feedback (default)
46
+ claudible
47
+
48
+ # Run a different command
49
+ claudible "python my_script.py"
50
+
51
+ # Pipe mode
52
+ some-command 2>&1 | claudible --pipe
53
+
54
+ # Choose a sound character
55
+ claudible --character crystal
56
+
57
+ # Adjust volume
58
+ claudible --volume 0.3
59
+
60
+ # List available characters
61
+ claudible --list-characters
62
+ ```
63
+
64
+ ### Reverse Mode
65
+
66
+ In reverse mode, claudible is silent while output is flowing and plays ambient sound during silence. Useful when you want to know a task is *waiting* rather than *working*.
67
+
68
+ ```bash
69
+ # Ambient grains play when Claude is idle/waiting for you
70
+ claudible --reverse
71
+
72
+ # Combine with a character
73
+ claudible --reverse -c shell
74
+ ```
75
+
76
+ ## Works With Any CLI
77
+
78
+ Built for Claude Code, but claudible works with anything that produces terminal output.
79
+
80
+ ```bash
81
+ # Aider
82
+ claudible "aider"
83
+
84
+ # Watch a dev server
85
+ npm run dev 2>&1 | claudible --pipe
86
+
87
+ # Monitor logs
88
+ tail -f /var/log/app.log | claudible --pipe -c droplet
89
+ ```
90
+
91
+ ## Sound Characters
92
+
93
+ | Character | Description |
94
+ |-----------|-------------|
95
+ | `ice` | Brittle, very high, fast decay with pitch drop |
96
+ | `glass` | Classic wine glass ping |
97
+ | `crystal` | Pure lead crystal with beating from close partial pairs |
98
+ | `ceramic` | Duller muted earthenware tap |
99
+ | `bell` | Small metallic bell, classic ratios, long ring |
100
+ | `droplet` | Water droplet, pitch bend down, liquid |
101
+ | `click` | Sharp mechanical click, keyboard-like |
102
+ | `wood` | Hollow wooden tap, warm marimba-like resonance |
103
+ | `stone` | Dense slate tap, heavy and earthy |
104
+ | `bamboo` | Hollow tube resonance, odd harmonics, breathy and airy |
105
+ | `ember` | Warm crackling ember, fire-like with wide pitch scatter |
106
+ | `silk` | Soft breathy whisper, delicate airy texture |
107
+ | `shell` | Swirly ocean interference, dense phase beating |
108
+ | `moss` | Ultra-soft muffled earth, mossy dampness |
109
+
110
+ ## Options
111
+
112
+ | Flag | Description |
113
+ |------|-------------|
114
+ | `--pipe` | Read from stdin instead of wrapping |
115
+ | `--character`, `-c` | Sound character |
116
+ | `--volume`, `-v` | Volume 0.0-1.0 (default: 0.5) |
117
+ | `--attention`, `-a` | Silence alert seconds (default: 30) |
118
+ | `--reverse`, `-r` | Reverse mode: sound during silence, quiet during output |
119
+ | `--list-characters` | Show presets |
120
+
121
+ ## Development
122
+
123
+ Test locally without installing:
124
+
125
+ ```bash
126
+ cd claudible
127
+
128
+ # Run wrapping claude (Ctrl+C to stop)
129
+ PYTHONPATH=src python3 -m claudible
130
+
131
+ # Wrap a different command
132
+ PYTHONPATH=src python3 -m claudible "ls -la"
133
+
134
+ # Pipe mode
135
+ echo "test" | PYTHONPATH=src python3 -m claudible --pipe -c glass
136
+
137
+ # List characters
138
+ PYTHONPATH=src python3 -m claudible --list-characters
139
+ ```
140
+
141
+ ## License
142
+
143
+ MIT
@@ -0,0 +1,118 @@
1
+ # claudible
2
+
3
+ *An opus for your terminals.*
4
+
5
+ Possibly the most annoying Claude utility ever made but here it is anyway. Ambient audio soundscape feedback for terminal output.
6
+
7
+ ## The Idea
8
+
9
+ Imagine you are working in a factory. Each Claude Code session is a machine that must be attended to. When it's working, you hear it - crystalline sparkles as text flows, soft chimes when tasks complete. When it goes quiet, you know something needs your attention.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install claudible
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ # Run claude with audio feedback (default)
21
+ claudible
22
+
23
+ # Run a different command
24
+ claudible "python my_script.py"
25
+
26
+ # Pipe mode
27
+ some-command 2>&1 | claudible --pipe
28
+
29
+ # Choose a sound character
30
+ claudible --character crystal
31
+
32
+ # Adjust volume
33
+ claudible --volume 0.3
34
+
35
+ # List available characters
36
+ claudible --list-characters
37
+ ```
38
+
39
+ ### Reverse Mode
40
+
41
+ In reverse mode, claudible is silent while output is flowing and plays ambient sound during silence. Useful when you want to know a task is *waiting* rather than *working*.
42
+
43
+ ```bash
44
+ # Ambient grains play when Claude is idle/waiting for you
45
+ claudible --reverse
46
+
47
+ # Combine with a character
48
+ claudible --reverse -c shell
49
+ ```
50
+
51
+ ## Works With Any CLI
52
+
53
+ Built for Claude Code, but claudible works with anything that produces terminal output.
54
+
55
+ ```bash
56
+ # Aider
57
+ claudible "aider"
58
+
59
+ # Watch a dev server
60
+ npm run dev 2>&1 | claudible --pipe
61
+
62
+ # Monitor logs
63
+ tail -f /var/log/app.log | claudible --pipe -c droplet
64
+ ```
65
+
66
+ ## Sound Characters
67
+
68
+ | Character | Description |
69
+ |-----------|-------------|
70
+ | `ice` | Brittle, very high, fast decay with pitch drop |
71
+ | `glass` | Classic wine glass ping |
72
+ | `crystal` | Pure lead crystal with beating from close partial pairs |
73
+ | `ceramic` | Duller muted earthenware tap |
74
+ | `bell` | Small metallic bell, classic ratios, long ring |
75
+ | `droplet` | Water droplet, pitch bend down, liquid |
76
+ | `click` | Sharp mechanical click, keyboard-like |
77
+ | `wood` | Hollow wooden tap, warm marimba-like resonance |
78
+ | `stone` | Dense slate tap, heavy and earthy |
79
+ | `bamboo` | Hollow tube resonance, odd harmonics, breathy and airy |
80
+ | `ember` | Warm crackling ember, fire-like with wide pitch scatter |
81
+ | `silk` | Soft breathy whisper, delicate airy texture |
82
+ | `shell` | Swirly ocean interference, dense phase beating |
83
+ | `moss` | Ultra-soft muffled earth, mossy dampness |
84
+
85
+ ## Options
86
+
87
+ | Flag | Description |
88
+ |------|-------------|
89
+ | `--pipe` | Read from stdin instead of wrapping |
90
+ | `--character`, `-c` | Sound character |
91
+ | `--volume`, `-v` | Volume 0.0-1.0 (default: 0.5) |
92
+ | `--attention`, `-a` | Silence alert seconds (default: 30) |
93
+ | `--reverse`, `-r` | Reverse mode: sound during silence, quiet during output |
94
+ | `--list-characters` | Show presets |
95
+
96
+ ## Development
97
+
98
+ Test locally without installing:
99
+
100
+ ```bash
101
+ cd claudible
102
+
103
+ # Run wrapping claude (Ctrl+C to stop)
104
+ PYTHONPATH=src python3 -m claudible
105
+
106
+ # Wrap a different command
107
+ PYTHONPATH=src python3 -m claudible "ls -la"
108
+
109
+ # Pipe mode
110
+ echo "test" | PYTHONPATH=src python3 -m claudible --pipe -c glass
111
+
112
+ # List characters
113
+ PYTHONPATH=src python3 -m claudible --list-characters
114
+ ```
115
+
116
+ ## License
117
+
118
+ MIT
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "claudible"
7
+ version = "0.1.0"
8
+ description = "Ambient audio soundscape feedback for terminal output - an opus for your terminals"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.8"
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.8",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Multimedia :: Sound/Audio",
25
+ "Topic :: Utilities",
26
+ ]
27
+ dependencies = [
28
+ "numpy>=1.20.0",
29
+ "scipy>=1.7.0",
30
+ "sounddevice>=0.4.0",
31
+ ]
32
+
33
+ [project.scripts]
34
+ claudible = "claudible.cli:main"
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/qwertykeith/claudible"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Claudible - ambient audio soundscape feedback for terminal output."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Allow running as python -m claudible."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,309 @@
1
+ """Sound engine for procedural audio generation."""
2
+
3
+ import numpy as np
4
+ import sounddevice as sd
5
+ from typing import Optional
6
+ import threading
7
+
8
+ try:
9
+ from scipy.signal import butter, lfilter
10
+ _HAS_SCIPY = True
11
+ except ImportError:
12
+ _HAS_SCIPY = False
13
+
14
+ from .materials import Material, get_random_material
15
+
16
+
17
+ class SoundEngine:
18
+ """Generates and plays procedural sounds based on physical material models."""
19
+
20
+ SAMPLE_RATE = 44100
21
+ BUFFER_SIZE = 44100 * 2 # 2 seconds of audio buffer
22
+
23
+ def __init__(self, material: Optional[Material] = None, volume: float = 0.5):
24
+ self.material = material or get_random_material()
25
+ self.volume = volume
26
+ self._stream: Optional[sd.OutputStream] = None
27
+ self._running = False
28
+ self._hum_phase = 0.0
29
+ self._lock = threading.Lock()
30
+
31
+ # Ring buffer for mixing audio
32
+ self._buffer = np.zeros(self.BUFFER_SIZE, dtype=np.float32)
33
+ self._write_pos = 0
34
+ self._read_pos = 0
35
+
36
+ # Pre-generate grain variations for efficiency
37
+ self._grain_cache = [self._generate_grain() for _ in range(8)]
38
+ self._cache_index = 0
39
+
40
+ def start(self):
41
+ """Start the audio output stream."""
42
+ self._running = True
43
+ self._stream = sd.OutputStream(
44
+ samplerate=self.SAMPLE_RATE,
45
+ channels=1,
46
+ callback=self._audio_callback,
47
+ blocksize=1024,
48
+ )
49
+ self._stream.start()
50
+
51
+ def stop(self):
52
+ """Stop the audio output stream."""
53
+ self._running = False
54
+ if self._stream:
55
+ self._stream.stop()
56
+ self._stream.close()
57
+ self._stream = None
58
+
59
+ # --- DSP utilities ---
60
+
61
+ def _highpass(self, data: np.ndarray, cutoff: float) -> np.ndarray:
62
+ """High-pass filter. Uses scipy if available, else differentiation."""
63
+ if _HAS_SCIPY:
64
+ nyq = self.SAMPLE_RATE / 2
65
+ norm = min(cutoff / nyq, 0.99)
66
+ if norm <= 0:
67
+ return data
68
+ b, a = butter(2, norm, btype='high')
69
+ return lfilter(b, a, data).astype(np.float32)
70
+ # Fallback: repeated differentiation
71
+ out = np.diff(data, prepend=data[0])
72
+ return np.diff(out, prepend=out[0]).astype(np.float32)
73
+
74
+ def _lowpass(self, data: np.ndarray, cutoff: float) -> np.ndarray:
75
+ """Low-pass filter. Uses scipy if available, else moving average."""
76
+ if _HAS_SCIPY:
77
+ nyq = self.SAMPLE_RATE / 2
78
+ norm = min(cutoff / nyq, 0.99)
79
+ if norm <= 0:
80
+ return np.zeros_like(data)
81
+ b, a = butter(2, norm, btype='low')
82
+ return lfilter(b, a, data).astype(np.float32)
83
+ # Fallback: simple moving average
84
+ n = max(int(self.SAMPLE_RATE / cutoff / 2), 1)
85
+ kernel = np.ones(n, dtype=np.float32) / n
86
+ return np.convolve(data, kernel, mode='same').astype(np.float32)
87
+
88
+ def _apply_reverb(self, signal: np.ndarray, amount: float,
89
+ room_size: float = 1.0, damping: float = 0.3) -> np.ndarray:
90
+ """Multi-tap reverb with high-frequency damping on later reflections."""
91
+ if amount <= 0:
92
+ return signal
93
+ base_delays_ms = [23, 37, 53, 79, 113, 149]
94
+ out = signal.copy()
95
+ for i, d in enumerate(base_delays_ms):
96
+ delay_samples = int(d * room_size * self.SAMPLE_RATE / 1000)
97
+ if delay_samples >= len(signal) or delay_samples <= 0:
98
+ continue
99
+ tap = np.zeros_like(signal)
100
+ tap[delay_samples:] = signal[:-delay_samples]
101
+ # Damp high frequencies in later reflections
102
+ if i > 1 and damping > 0:
103
+ cutoff = max(4000 - i * 500 * damping, 500)
104
+ tap = self._lowpass(tap, cutoff)
105
+ out += tap * amount * (0.55 ** i)
106
+ return out / max(1.0 + amount * 1.5, 1.0)
107
+
108
+ # --- Audio callback ---
109
+
110
+ def _audio_callback(self, outdata, frames, time, status):
111
+ """Audio stream callback - mixes queued sounds with background hum."""
112
+ output = np.zeros(frames, dtype=np.float32)
113
+
114
+ # Background hum: multiple detuned oscillators with slow wobble
115
+ t = np.arange(frames) / self.SAMPLE_RATE
116
+ phase_inc = 2 * np.pi * 55.0 / self.SAMPLE_RATE * frames
117
+ wobble = 0.002 * np.sin(2 * np.pi * 0.08 * t + self._hum_phase * 0.01)
118
+
119
+ for freq, amp in [(55.0, 0.007), (55.15, 0.006), (54.85, 0.006), (110.05, 0.001)]:
120
+ output += amp * np.sin(
121
+ 2 * np.pi * freq * (1 + wobble) * t + self._hum_phase * (freq / 55.0)
122
+ ).astype(np.float32)
123
+
124
+ self._hum_phase = (self._hum_phase + phase_inc) % (2 * np.pi)
125
+
126
+ # Read from ring buffer
127
+ with self._lock:
128
+ for i in range(frames):
129
+ output[i] += self._buffer[self._read_pos]
130
+ self._buffer[self._read_pos] = 0.0
131
+ self._read_pos = (self._read_pos + 1) % self.BUFFER_SIZE
132
+
133
+ outdata[:] = (output * self.volume).reshape(-1, 1)
134
+
135
+ def _add_to_buffer(self, samples: np.ndarray):
136
+ """Add samples to the ring buffer (mixes with existing)."""
137
+ with self._lock:
138
+ for i, sample in enumerate(samples):
139
+ pos = (self._read_pos + i) % self.BUFFER_SIZE
140
+ self._buffer[pos] += sample
141
+
142
+ # --- Sound generation ---
143
+
144
+ def _generate_grain(self) -> np.ndarray:
145
+ """Generate a single grain sound from the material's physical model."""
146
+ m = self.material
147
+ # Vary duration per grain
148
+ duration = m.grain_duration * np.random.uniform(0.85, 1.15)
149
+ n = int(duration * self.SAMPLE_RATE)
150
+ t = np.linspace(0, duration, n, dtype=np.float32)
151
+
152
+ grain = np.zeros(n, dtype=np.float32)
153
+
154
+ # Randomise base frequency within the material's spread
155
+ base_freq = m.base_freq * (2 ** (np.random.uniform(-m.freq_spread, m.freq_spread) / 12))
156
+
157
+ # Generate each partial with its own decay, detuning, and random phase
158
+ for i, (ratio, amp) in enumerate(m.partials):
159
+ detune_cents = np.random.uniform(-m.detune_amount, m.detune_amount)
160
+ freq = base_freq * ratio * (2 ** (detune_cents / 1200))
161
+ # Extra micro-variation per partial
162
+ freq *= np.random.uniform(0.997, 1.003)
163
+ phase = np.random.uniform(0, 2 * np.pi)
164
+
165
+ decay_rate = m.decay_rates[i] if i < len(m.decay_rates) else m.decay_rates[-1]
166
+ envelope = np.exp(-decay_rate * t)
167
+
168
+ grain += amp * envelope * np.sin(2 * np.pi * freq * t + phase)
169
+
170
+ # Noise transient (the "tick" of physical impact)
171
+ if m.attack_noise > 0:
172
+ noise = np.random.randn(n).astype(np.float32)
173
+ noise = self._highpass(noise, m.noise_freq)
174
+ # Shape: fast attack, very fast decay with micro-variation
175
+ noise_env = np.exp(-150 * t)
176
+ noise_env *= np.clip(
177
+ 1.0 + 0.3 * np.random.randn(n).astype(np.float32) * np.exp(-200 * t),
178
+ 0, 1,
179
+ )
180
+ grain += m.attack_noise * 0.3 * noise * noise_env
181
+
182
+ # Attack shaping
183
+ attack_samples = max(int(m.attack_ms / 1000 * self.SAMPLE_RATE), 2)
184
+ if attack_samples < n:
185
+ grain[:attack_samples] *= np.linspace(0, 1, attack_samples, dtype=np.float32)
186
+
187
+ # Pitch drop envelope (physical: pitch drops as energy dissipates)
188
+ if m.pitch_drop != 1.0:
189
+ pitch_env = np.linspace(1.0, m.pitch_drop, n)
190
+ phase_mod = np.cumsum(pitch_env) / np.sum(pitch_env) * n
191
+ indices = np.clip(phase_mod.astype(int), 0, n - 1)
192
+ grain = grain[indices]
193
+
194
+ # Multi-tap reverb with HF damping
195
+ grain = self._apply_reverb(grain, m.reverb_amount, m.room_size, m.reverb_damping)
196
+
197
+ # Truncate reverb tail back to grain length
198
+ grain = grain[:n]
199
+
200
+ # Normalise with per-material volume
201
+ peak = np.max(np.abs(grain))
202
+ if peak > 0:
203
+ grain = grain / peak * 0.4 * m.volume
204
+
205
+ return grain.astype(np.float32)
206
+
207
+ def play_grain(self):
208
+ """Play a grain/sparkle sound."""
209
+ # Pick a random cached grain
210
+ idx = np.random.randint(len(self._grain_cache))
211
+ grain = self._grain_cache[idx].copy()
212
+
213
+ # Per-play pitch variation via resampling (±4 semitones)
214
+ pitch_shift = 2 ** (np.random.uniform(-4, 4) / 12)
215
+ if abs(pitch_shift - 1.0) > 0.001:
216
+ new_len = int(len(grain) / pitch_shift)
217
+ if new_len > 0:
218
+ grain = np.interp(
219
+ np.linspace(0, len(grain) - 1, new_len),
220
+ np.arange(len(grain)),
221
+ grain,
222
+ ).astype(np.float32)
223
+
224
+ # Random amplitude variation
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()
230
+
231
+ self._add_to_buffer(grain)
232
+
233
+ def play_chime(self):
234
+ """Play a soft completion chime using the material's tonal character."""
235
+ m = self.material
236
+ duration = 0.35
237
+ n = int(duration * self.SAMPLE_RATE)
238
+ t = np.linspace(0, duration, n, dtype=np.float32)
239
+
240
+ chime = np.zeros(n, dtype=np.float32)
241
+
242
+ # Use the material's own partials at a lower register for the chime
243
+ base = m.base_freq * 0.5
244
+ for i, (ratio, amp) in enumerate(m.partials[:4]):
245
+ freq = base * ratio * np.random.uniform(0.998, 1.002)
246
+ phase = np.random.uniform(0, 2 * np.pi)
247
+ decay_rate = m.decay_rates[i] if i < len(m.decay_rates) else m.decay_rates[-1]
248
+ # Slower decay for sustained chime
249
+ envelope = np.exp(-decay_rate * 0.25 * t) * np.sin(np.pi * t / duration)
250
+ chime += amp * 0.3 * np.sin(2 * np.pi * freq * t + phase) * envelope
251
+
252
+ # Softer noise transient
253
+ if m.attack_noise > 0:
254
+ noise = np.random.randn(n).astype(np.float32)
255
+ noise = self._highpass(noise, m.noise_freq)
256
+ chime += m.attack_noise * 0.15 * noise * np.exp(-50 * t)
257
+
258
+ # More reverb for chimes
259
+ chime = self._apply_reverb(
260
+ chime, m.reverb_amount * 1.3,
261
+ room_size=m.room_size * 1.2,
262
+ damping=m.reverb_damping,
263
+ )
264
+ chime = chime[:n]
265
+
266
+ peak = np.max(np.abs(chime))
267
+ if peak > 0:
268
+ chime = chime / peak * 0.35 * m.volume
269
+
270
+ self._add_to_buffer(chime.astype(np.float32))
271
+
272
+ def play_attention(self):
273
+ """Play a gentle attention signal using the material's tonal character."""
274
+ m = self.material
275
+ duration = 0.8
276
+ n = int(duration * self.SAMPLE_RATE)
277
+ t = np.linspace(0, duration, n, dtype=np.float32)
278
+
279
+ signal = np.zeros(n, dtype=np.float32)
280
+
281
+ # Two-note rising pattern using material's frequency
282
+ note_samples = n // 2
283
+ for i, pitch_mult in enumerate([1.0, 1.25]): # Root and major third
284
+ start = i * note_samples
285
+ end = start + note_samples
286
+ nt = t[start:end] - t[start]
287
+ freq = m.base_freq * 0.4 * pitch_mult
288
+
289
+ envelope = np.sin(np.pi * nt / (duration / 2)) ** 2
290
+ # Use a couple of the material's partials
291
+ for j, (ratio, amp) in enumerate(m.partials[:2]):
292
+ pfreq = freq * ratio * np.random.uniform(0.998, 1.002)
293
+ signal[start:end] += 0.2 * amp * np.sin(
294
+ 2 * np.pi * pfreq * nt + np.random.uniform(0, 2 * np.pi)
295
+ ) * envelope
296
+
297
+ # Gentle reverb
298
+ signal = self._apply_reverb(
299
+ signal, m.reverb_amount * 1.5,
300
+ room_size=m.room_size * 1.3,
301
+ damping=m.reverb_damping,
302
+ )
303
+ signal = signal[:n]
304
+
305
+ peak = np.max(np.abs(signal))
306
+ if peak > 0:
307
+ signal = signal / peak * 0.3 * m.volume
308
+
309
+ self._add_to_buffer(signal.astype(np.float32))