sonic-forge 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.
Files changed (59) hide show
  1. sonic_forge-0.1.0/.gitignore +27 -0
  2. sonic_forge-0.1.0/MANIFEST.md +14 -0
  3. sonic_forge-0.1.0/PKG-INFO +27 -0
  4. sonic_forge-0.1.0/bytebeat-dj.sh +256 -0
  5. sonic_forge-0.1.0/bytebeat-rating.md +7 -0
  6. sonic_forge-0.1.0/chuck-music/cathedral-drift.ck +123 -0
  7. sonic_forge-0.1.0/chuck-music/coding-ambient.ck +186 -0
  8. sonic_forge-0.1.0/chuck-music/coding-flow.ck +255 -0
  9. sonic_forge-0.1.0/chuck-music/dark-ambient.ck +297 -0
  10. sonic_forge-0.1.0/chuck-music/dark-industrial.ck +330 -0
  11. sonic_forge-0.1.0/chuck-music/dark-space.ck +457 -0
  12. sonic_forge-0.1.0/chuck-music/dark-techno.ck +421 -0
  13. sonic_forge-0.1.0/chuck-music/euro-rave.ck +447 -0
  14. sonic_forge-0.1.0/chuck-music/gameboy-evolve.ck +327 -0
  15. sonic_forge-0.1.0/chuck-music/play.sh +52 -0
  16. sonic_forge-0.1.0/chuck-music/singing-bowls.ck +196 -0
  17. sonic_forge-0.1.0/chuck-music/space-synth.ck +261 -0
  18. sonic_forge-0.1.0/pattern-engine/.gitignore +4 -0
  19. sonic_forge-0.1.0/pattern-engine/DSL-text-2-music-yaml.md +265 -0
  20. sonic_forge-0.1.0/pattern-engine/HOW-WE-DID-IT.md +132 -0
  21. sonic_forge-0.1.0/pattern-engine/MATERIALS-BAY-PLAN.md +146 -0
  22. sonic_forge-0.1.0/pattern-engine/acid_session.py +144 -0
  23. sonic_forge-0.1.0/pattern-engine/captains_log_short.yaml +86 -0
  24. sonic_forge-0.1.0/pattern-engine/captains_log_song.yaml +141 -0
  25. sonic_forge-0.1.0/pattern-engine/dance.sh +242 -0
  26. sonic_forge-0.1.0/pattern-engine/demos/play-demos.sh +174 -0
  27. sonic_forge-0.1.0/pattern-engine/demos-vibevoice/play-demos.sh +184 -0
  28. sonic_forge-0.1.0/pattern-engine/midnight.yaml +88 -0
  29. sonic_forge-0.1.0/pattern-engine/my_song.yaml +87 -0
  30. sonic_forge-0.1.0/pattern-engine/render_vibevoice_demos.py +306 -0
  31. sonic_forge-0.1.0/pattern-engine/songfile.py +339 -0
  32. sonic_forge-0.1.0/pattern-engine/songs.py +350 -0
  33. sonic_forge-0.1.0/pattern-engine/templates.py +527 -0
  34. sonic_forge-0.1.0/pattern-engine/tidal.py +1072 -0
  35. sonic_forge-0.1.0/pattern-engine/trance.py +123 -0
  36. sonic_forge-0.1.0/pattern-engine/trance1_1.py +187 -0
  37. sonic_forge-0.1.0/pattern-engine/trance1_1.yaml +88 -0
  38. sonic_forge-0.1.0/pattern-engine/trance2.py +208 -0
  39. sonic_forge-0.1.0/pattern-engine/tts-comparison/.gitignore +6 -0
  40. sonic_forge-0.1.0/pattern-engine/tts-comparison/REPORT.md +160 -0
  41. sonic_forge-0.1.0/pattern-engine/tts-comparison/generate_samples.py +481 -0
  42. sonic_forge-0.1.0/pattern-engine/tts-comparison/play-comparison.sh +341 -0
  43. sonic_forge-0.1.0/pattern-engine/tts-comparison/robotize.py +336 -0
  44. sonic_forge-0.1.0/pyproject.toml +25 -0
  45. sonic_forge-0.1.0/sound-demo-tour.sh +89 -0
  46. sonic_forge-0.1.0/sound-rating.md +25 -0
  47. sonic_forge-0.1.0/src/sonic_forge/__init__.py +3 -0
  48. sonic_forge-0.1.0/src/sonic_forge/cli.py +63 -0
  49. sonic_forge-0.1.0/src/sonic_forge/robotize.py +336 -0
  50. sonic_forge-0.1.0/src/sonic_forge/songfile.py +332 -0
  51. sonic_forge-0.1.0/src/sonic_forge/songs.py +350 -0
  52. sonic_forge-0.1.0/src/sonic_forge/templates.py +527 -0
  53. sonic_forge-0.1.0/src/sonic_forge/tidal.py +1072 -0
  54. sonic_forge-0.1.0/strudel-node/REPORT.md +148 -0
  55. sonic_forge-0.1.0/strudel-node/inspect-events.mjs +28 -0
  56. sonic_forge-0.1.0/strudel-node/package-lock.json +201 -0
  57. sonic_forge-0.1.0/strudel-node/package.json +18 -0
  58. sonic_forge-0.1.0/strudel-node/play.mjs +250 -0
  59. sonic_forge-0.1.0/strudel-node/test-patterns.mjs +45 -0
@@ -0,0 +1,27 @@
1
+ # Audio (generated on demand)
2
+ *.wav
3
+ *.aiff
4
+ *.mp3
5
+ *.ogg
6
+
7
+ # Models (downloaded on first use)
8
+ *.onnx
9
+ *.bin
10
+ *.safetensors
11
+ kokoro-models/
12
+ chattts-assets/
13
+
14
+ # Python
15
+ __pycache__/
16
+ *.pyc
17
+ *.egg-info/
18
+ dist/
19
+ build/
20
+ .venv/
21
+ venv/
22
+
23
+ # Node
24
+ node_modules/
25
+
26
+ # OS
27
+ .DS_Store
@@ -0,0 +1,14 @@
1
+ # MATERIALS-BAY: sonic-forge
2
+
3
+ **Created:** 2026-03-11 15:12
4
+ **Source labs:** pattern-engine, chuck-music, strudel-node, bytebeat-dj.sh, sound-demo-tour.sh, bytebeat-rating.md, sound-rating.md
5
+
6
+ ## Contents
7
+
8
+ - **pattern-engine/** — from `LABS/pattern-engine/`
9
+ - **chuck-music/** — from `LABS/chuck-music/`
10
+ - **strudel-node/** — from `LABS/strudel-node/`
11
+ - **bytebeat-dj.sh/** — from `LABS/bytebeat-dj.sh/`
12
+ - **sound-demo-tour.sh/** — from `LABS/sound-demo-tour.sh/`
13
+ - **bytebeat-rating.md/** — from `LABS/bytebeat-rating.md/`
14
+ - **sound-rating.md/** — from `LABS/sound-rating.md/`
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: sonic-forge
3
+ Version: 0.1.0
4
+ Summary: Bytebeat music DSL + multi-engine TTS voice system — beamed from starforge
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: pyyaml
7
+ Requires-Dist: typer
8
+ Provides-Extra: all
9
+ Requires-Dist: f5-tts-mlx; extra == 'all'
10
+ Requires-Dist: kokoro-onnx; extra == 'all'
11
+ Requires-Dist: numpy; extra == 'all'
12
+ Requires-Dist: onnxruntime; extra == 'all'
13
+ Requires-Dist: soundfile; extra == 'all'
14
+ Provides-Extra: kokoro
15
+ Requires-Dist: kokoro-onnx; extra == 'kokoro'
16
+ Requires-Dist: numpy; extra == 'kokoro'
17
+ Requires-Dist: onnxruntime; extra == 'kokoro'
18
+ Requires-Dist: soundfile; extra == 'kokoro'
19
+ Provides-Extra: robot
20
+ Requires-Dist: numpy; extra == 'robot'
21
+ Requires-Dist: soundfile; extra == 'robot'
22
+ Provides-Extra: tts
23
+ Requires-Dist: f5-tts-mlx; extra == 'tts'
24
+ Requires-Dist: kokoro-onnx; extra == 'tts'
25
+ Requires-Dist: numpy; extra == 'tts'
26
+ Requires-Dist: onnxruntime; extra == 'tts'
27
+ Requires-Dist: soundfile; extra == 'tts'
@@ -0,0 +1,256 @@
1
+ #!/bin/bash
2
+ # Bytebeat DJ — interactive player with speed, volume, and track controls
3
+ # Writes Python to temp file so curses gets proper terminal access
4
+
5
+ VENV_PYTHON="/Users/t/clients/starship/starforge/venv/bin/python"
6
+
7
+ # Clean up any stale temp files, then create fresh one
8
+ rm -f /tmp/bytebeat_dj_*.py
9
+ TMPSCRIPT="/tmp/bytebeat_dj_$$.py"
10
+
11
+ cat > "$TMPSCRIPT" << 'PYEOF'
12
+ import wave, curses, subprocess, os, sys, time, threading
13
+
14
+ TRACKS = [
15
+ ("classic", "t*(t>>8|t>>9)&46&t>>8^(t&t>>13|t>>6)",
16
+ lambda t: (t*(((t>>12)|(t>>8)))&(46&(t>>8)))^((t&(t>>13))|(t>>6))),
17
+ ("melody", "t*((t>>9)|(t>>13))&16",
18
+ lambda t: t*((t>>9)|(t>>13))&16),
19
+ ("bass", "t&(t>>8)",
20
+ lambda t: t&(t>>8)),
21
+ ("rhythm", "(t*(t>>5|t>>8))>>(t>>16)",
22
+ lambda t: (t*((t>>5)|(t>>8)))>>(t>>16) if (t>>16) < 32 else 0),
23
+ ("chaos", "(t*5&t>>7)|(t*3&t>>10)",
24
+ lambda t: (t*5&(t>>7))|(t*3&(t>>10))),
25
+ ("crowd", "(t<<1)^((t<<1)+(t>>7)&t>>12)|t>>7",
26
+ lambda t: ((t<<1)^((t<<1)+(t>>7)&t>>12))|t>>(4-(1^7&(t>>19)))|t>>7),
27
+ ("sierpinski", "t&t>>8",
28
+ lambda t: t&t>>8),
29
+ ("42melody", "t*(42&t>>10)",
30
+ lambda t: (t*(42&t>>10))&255),
31
+ ("glitch", "t*((t>>12|t>>8)&63&t>>4)",
32
+ lambda t: t*((t>>12|t>>8)&63&t>>4)),
33
+ ("cathedral", "(t>>6|t|t>>(t>>16))*10+((t>>11)&7)",
34
+ lambda t: (t>>6|t|t>>(t>>16))*10+((t>>11)&7)),
35
+ ("gameboy", "t*9&t>>4|t*5&t>>7|t*3&t>>10",
36
+ lambda t: (t*9&t>>4|t*5&t>>7|t*3&t>>10)-1),
37
+ ("underwater", "(t*(t>>5|t>>8))>>(t>>16)",
38
+ lambda t: (t*(t>>5|t>>8))>>(t>>16)),
39
+ ("alien", "t*(((t>>9)^((t>>9)-1)^1)%13)",
40
+ lambda t: t*(((t>>9)^((t>>9)-1)^1)%13)),
41
+ ]
42
+
43
+ SPEEDS = [
44
+ (2000, "0.25x"), (3000, "0.38x"), (4000, "0.5x"),
45
+ (5000, "0.63x"), (6000, "0.75x"), (7000, "0.88x"),
46
+ (8000, "1.0x"), (9000, "1.13x"), (10000, "1.25x"),
47
+ (12000, "1.5x"), (14000, "1.75x"), (16000, "2.0x"),
48
+ (20000, "2.5x"), (24000, "3.0x"), (32000, "4.0x"),
49
+ ]
50
+
51
+ current_track = 0
52
+ speed_idx = 6
53
+ volume = 0.5
54
+ afplay_proc = None
55
+ generating = False
56
+ WAV_PATH = "/tmp/bytebeat_dj.wav"
57
+ DURATION = 600 # 10 minutes — bytebeat is infinite math, let it ride
58
+
59
+ def generate_wav(track_idx, rate):
60
+ global generating
61
+ generating = True
62
+ name, _, func = TRACKS[track_idx]
63
+ samples = rate * DURATION
64
+ with wave.open(WAV_PATH, 'w') as f:
65
+ f.setnchannels(1)
66
+ f.setsampwidth(1)
67
+ f.setframerate(rate)
68
+ buf = bytearray(samples)
69
+ for t in range(samples):
70
+ try:
71
+ buf[t] = func(t) & 255
72
+ except:
73
+ buf[t] = 128
74
+ f.writeframes(bytes(buf))
75
+ generating = False
76
+
77
+ def kill_player():
78
+ global afplay_proc
79
+ if afplay_proc and afplay_proc.poll() is None:
80
+ afplay_proc.terminate()
81
+ try:
82
+ afplay_proc.wait(timeout=1)
83
+ except:
84
+ afplay_proc.kill()
85
+ afplay_proc = None
86
+
87
+ def start_playback():
88
+ global afplay_proc
89
+ kill_player()
90
+ generate_wav(current_track, SPEEDS[speed_idx][0])
91
+ afplay_proc = subprocess.Popen(
92
+ ["afplay", "-v", str(volume), WAV_PATH],
93
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
94
+ )
95
+
96
+ def adjust_volume():
97
+ """Restart playback with new volume (no regeneration needed)."""
98
+ global afplay_proc
99
+ kill_player()
100
+ if os.path.exists(WAV_PATH):
101
+ afplay_proc = subprocess.Popen(
102
+ ["afplay", "-v", str(volume), WAV_PATH],
103
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
104
+ )
105
+
106
+ def main(stdscr):
107
+ global current_track, speed_idx, volume, afplay_proc
108
+
109
+ curses.curs_set(0)
110
+ curses.start_color()
111
+ curses.use_default_colors()
112
+ curses.init_pair(1, curses.COLOR_YELLOW, -1)
113
+ curses.init_pair(2, curses.COLOR_CYAN, -1)
114
+ curses.init_pair(3, curses.COLOR_GREEN, -1)
115
+ curses.init_pair(4, curses.COLOR_RED, -1)
116
+ curses.init_pair(5, curses.COLOR_MAGENTA, -1)
117
+ curses.init_pair(6, curses.COLOR_WHITE, -1)
118
+
119
+ stdscr.nodelay(True)
120
+ stdscr.keypad(True)
121
+
122
+ start_playback()
123
+
124
+ while True:
125
+ stdscr.erase()
126
+ h, w = stdscr.getmaxyx()
127
+ name, formula_str, _ = TRACKS[current_track]
128
+ rate, slabel = SPEEDS[speed_idx]
129
+
130
+ # Title
131
+ title = " BYTEBEAT DJ "
132
+ cx = max(0, (w - len(title)) // 2)
133
+ try:
134
+ stdscr.addstr(1, cx, title, curses.A_BOLD | curses.color_pair(1))
135
+ except curses.error:
136
+ pass
137
+
138
+ def safe_add(y, x, text, attr=0):
139
+ try:
140
+ if y < h - 1 and x < w:
141
+ stdscr.addstr(y, x, text[:w-x-1], attr)
142
+ except curses.error:
143
+ pass
144
+
145
+ y = 3
146
+ safe_add(y, 2, f"Track {current_track+1}/{len(TRACKS)}", curses.color_pair(2))
147
+ y += 1
148
+ safe_add(y, 4, name, curses.A_BOLD | curses.color_pair(3))
149
+ y += 1
150
+ fdisp = formula_str if len(formula_str) < w - 8 else formula_str[:w-11] + "..."
151
+ safe_add(y, 4, fdisp, curses.color_pair(6))
152
+
153
+ # Speed bar
154
+ y += 2
155
+ bar_w = min(30, w - 25)
156
+ if bar_w > 5:
157
+ safe_add(y, 2, "Speed:", curses.color_pair(5))
158
+ pos = int((speed_idx / max(1, len(SPEEDS)-1)) * bar_w)
159
+ bar = "[" + "=" * pos + "O" + "-" * (bar_w - pos) + "]"
160
+ safe_add(y, 9, bar, curses.color_pair(5))
161
+ safe_add(y, 10 + bar_w, f" {slabel} ({rate}Hz)", curses.color_pair(6))
162
+
163
+ # Volume bar
164
+ y += 1
165
+ if bar_w > 5:
166
+ safe_add(y, 2, "Vol: ", curses.color_pair(4))
167
+ vol_pos = int(min(1.0, volume) * bar_w)
168
+ vol_bar = "[" + "=" * vol_pos + "O" + "-" * (bar_w - vol_pos) + "]"
169
+ safe_add(y, 9, vol_bar, curses.color_pair(4))
170
+ safe_add(y, 10 + bar_w, f" {int(volume*100)}%", curses.color_pair(6))
171
+
172
+ # Status
173
+ y += 2
174
+ playing = afplay_proc and afplay_proc.poll() is None
175
+ if generating:
176
+ safe_add(y, 4, "generating...", curses.color_pair(1))
177
+ elif playing:
178
+ tick = int(time.time() * 4) % 4
179
+ frames = ["♪ ♫ ", " ♪ ♫ ", " ♪ ♫", " ♫ ♪ "]
180
+ safe_add(y, 4, f"{frames[tick]} PLAYING", curses.color_pair(3))
181
+ else:
182
+ safe_add(y, 4, "stopped — SPACE to play", curses.color_pair(4))
183
+
184
+ # Track list
185
+ y += 2
186
+ safe_add(y, 2, "Tracks:", curses.color_pair(2))
187
+ y += 1
188
+ for i, (tname, _, _) in enumerate(TRACKS):
189
+ if y >= h - 4:
190
+ break
191
+ marker = " >> " if i == current_track else " "
192
+ attr = curses.A_BOLD | curses.color_pair(3) if i == current_track else curses.color_pair(6)
193
+ safe_add(y, 2, f"{marker}{i+1:2d}. {tname}", attr)
194
+ y += 1
195
+
196
+ # Controls
197
+ if h > 3:
198
+ safe_add(h-2, 2, "< > track ^ v speed +/- vol SPACE restart 1-9 jump q quit", curses.color_pair(6))
199
+
200
+ stdscr.refresh()
201
+
202
+ # Input — drain all pending keys, act on last meaningful one
203
+ key = -1
204
+ while True:
205
+ k = stdscr.getch()
206
+ if k == -1:
207
+ break
208
+ key = k
209
+
210
+ if key == -1:
211
+ time.sleep(0.05)
212
+ # Auto-loop if track ended
213
+ if not generating and afplay_proc and afplay_proc.poll() is not None:
214
+ start_playback()
215
+ continue
216
+
217
+ if key == ord('q') or key == ord('Q'):
218
+ kill_player()
219
+ break
220
+ elif key == curses.KEY_RIGHT or key == ord('l'):
221
+ current_track = (current_track + 1) % len(TRACKS)
222
+ start_playback()
223
+ elif key == curses.KEY_LEFT or key == ord('h'):
224
+ current_track = (current_track - 1) % len(TRACKS)
225
+ start_playback()
226
+ elif key == curses.KEY_UP or key == ord('k'):
227
+ if speed_idx < len(SPEEDS) - 1:
228
+ speed_idx += 1
229
+ start_playback()
230
+ elif key == curses.KEY_DOWN or key == ord('j'):
231
+ if speed_idx > 0:
232
+ speed_idx -= 1
233
+ start_playback()
234
+ elif key == ord('+') or key == ord('='):
235
+ volume = min(2.0, round(volume + 0.1, 1))
236
+ adjust_volume()
237
+ elif key == ord('-') or key == ord('_'):
238
+ volume = max(0.0, round(volume - 0.1, 1))
239
+ adjust_volume()
240
+ elif key == ord(' '):
241
+ start_playback()
242
+ elif ord('1') <= key <= ord('9'):
243
+ idx = key - ord('1')
244
+ if idx < len(TRACKS):
245
+ current_track = idx
246
+ start_playback()
247
+ elif key == ord('0'):
248
+ if len(TRACKS) > 9:
249
+ current_track = 9
250
+ start_playback()
251
+
252
+ curses.wrapper(main)
253
+ PYEOF
254
+
255
+ exec "$VENV_PYTHON" "$TMPSCRIPT"
256
+ rm -f "$TMPSCRIPT"
@@ -0,0 +1,7 @@
1
+ # Bytebeat Rating — Mathematical Music Tour
2
+
3
+ Each formula generates audio from pure math: `f(t)` where t is a sample counter.
4
+ The output byte (0-255) IS the audio waveform. No instruments, no samples — just math.
5
+
6
+ | # | Name | Formula | Like? | Volume (1-10) | Notes |
7
+ |---|------|---------|-------|---------------|-------|
@@ -0,0 +1,123 @@
1
+ // Starforge Cathedral Drift — deep reverb pads that slowly transform
2
+ // Inspired by your love of the "cathedral" bytebeat formula
3
+ // Run: chuck LABS/chuck-music/cathedral-drift.ck
4
+
5
+ Gain master => NRev reverb => dac;
6
+ 0.15 => reverb.mix; // heavy reverb for cathedral feel
7
+ 0.3 => master.gain;
8
+
9
+ // === DRONE: low fundamental that shifts ===
10
+ fun void drone() {
11
+ SawOsc saw1 => LPF filt => Gain droneGain => master;
12
+ SawOsc saw2 => filt;
13
+ 0.06 => droneGain.gain;
14
+ 300.0 => filt.freq;
15
+ 4.0 => filt.Q;
16
+
17
+ [36, 38, 41, 43, 34, 31] @=> int roots[]; // deep roots
18
+ 0 => int idx;
19
+
20
+ while (true) {
21
+ Std.mtof(roots[idx]) => float targetFreq;
22
+ saw1.freq() => float startFreq;
23
+ if (startFreq < 10.0) targetFreq => startFreq;
24
+
25
+ // Glacial glide to new note
26
+ for (0 => int i; i < 200; i++) {
27
+ startFreq + (targetFreq - startFreq) * (i / 200.0) => saw1.freq;
28
+ (startFreq + (targetFreq - startFreq) * (i / 200.0)) * 1.005 => saw2.freq;
29
+ 25::ms => now;
30
+ }
31
+
32
+ // Hold
33
+ Math.random2(3000, 8000)::ms => now;
34
+
35
+ (idx + Math.random2(1, 3)) % roots.size() => idx;
36
+ }
37
+ }
38
+
39
+ // === BELLS: sine harmonics that ring and fade ===
40
+ fun void bell(float freq, float vol, dur length) {
41
+ SinOsc s1 => Gain g => master;
42
+ SinOsc s2 => g;
43
+ SinOsc s3 => g;
44
+ freq => s1.freq;
45
+ freq * 2.756 => s2.freq; // inharmonic partial (bell-like)
46
+ freq * 5.404 => s3.freq; // higher inharmonic
47
+ vol => g.gain;
48
+
49
+ // Exponential decay
50
+ length / 50 => dur step;
51
+ for (0 => int i; i < 50; i++) {
52
+ vol * Math.pow(0.92, i $ float) => g.gain;
53
+ step => now;
54
+ }
55
+ 0.0 => g.gain;
56
+ }
57
+
58
+ fun void bells() {
59
+ [60, 63, 67, 70, 72, 75, 79, 84] @=> int notes[];
60
+
61
+ while (true) {
62
+ // Sparse — sometimes play, sometimes rest
63
+ if (Math.random2f(0.0, 1.0) < 0.35) {
64
+ Math.random2(0, notes.size()-1) => int idx;
65
+ spork ~ bell(Std.mtof(notes[idx]),
66
+ Math.random2f(0.02, 0.05),
67
+ Math.random2(2000, 5000)::ms);
68
+ }
69
+ Math.random2(800, 3000)::ms => now;
70
+ }
71
+ }
72
+
73
+ // === MORE BELLS: second bell layer with different tuning ===
74
+ fun void bellsLow() {
75
+ // Lower register bells with longer decay
76
+ [48, 51, 53, 55, 58, 60] @=> int notes[];
77
+
78
+ while (true) {
79
+ if (Math.random2f(0.0, 1.0) < 0.25) {
80
+ Math.random2(0, notes.size()-1) => int idx;
81
+ spork ~ bell(Std.mtof(notes[idx]),
82
+ Math.random2f(0.015, 0.035),
83
+ Math.random2(3000, 7000)::ms);
84
+ }
85
+ Math.random2(1500, 5000)::ms => now;
86
+ }
87
+ }
88
+
89
+ // === SUB PULSE: very deep, slow ===
90
+ fun void subPulse() {
91
+ SinOsc sub => Gain subGain => master;
92
+ 0.0 => subGain.gain;
93
+
94
+ [29, 31, 34, 36] @=> int notes[]; // very low
95
+ 0 => int idx;
96
+
97
+ while (true) {
98
+ Std.mtof(notes[idx]) => sub.freq;
99
+
100
+ // Slow fade in/out
101
+ for (0 => int i; i < 60; i++) {
102
+ 0.1 * Math.sin(Math.PI * i / 60.0) => subGain.gain;
103
+ 50::ms => now;
104
+ }
105
+ 0.0 => subGain.gain;
106
+
107
+ Math.random2(2000, 6000)::ms => now;
108
+ (idx + 1) % notes.size() => idx;
109
+ }
110
+ }
111
+
112
+ // === LAUNCH ===
113
+ <<< " Starforge Cathedral Drift", "" >>>;
114
+ <<< " Deep reverb ambient — slow transformation", "" >>>;
115
+ <<< " Ctrl-C to stop", "" >>>;
116
+ <<< "" >>>;
117
+
118
+ spork ~ drone();
119
+ spork ~ bells();
120
+ spork ~ bellsLow();
121
+ spork ~ subPulse();
122
+
123
+ while (true) 1::second => now;
@@ -0,0 +1,186 @@
1
+ // Starforge Coding Ambient — evolving generative music for terminal coding
2
+ // Run: chuck LABS/chuck-music/coding-ambient.ck
3
+ // Ctrl-C to stop
4
+
5
+ // === MASTER OUTPUT ===
6
+ Gain master => NRev reverb => dac;
7
+ 0.08 => reverb.mix;
8
+ 0.35 => master.gain;
9
+
10
+ // === PAD: slow evolving chords ===
11
+ fun void pad() {
12
+ // Pentatonic minor scale degrees (dark, spacey)
13
+ [48, 51, 53, 55, 58, 60, 63, 65, 67, 70] @=> int notes[];
14
+
15
+ // Two detuned oscillators for thickness
16
+ TriOsc p1 => LPF filt1 => Gain padGain => master;
17
+ TriOsc p2 => LPF filt2 => padGain;
18
+ 0.12 => padGain.gain;
19
+ 800.0 => filt1.freq;
20
+ 750.0 => filt2.freq;
21
+ 3.0 => filt1.Q;
22
+ 3.0 => filt2.Q;
23
+
24
+ while (true) {
25
+ // Pick two notes for a chord
26
+ Math.random2(0, notes.size()-1) => int idx1;
27
+ (idx1 + Math.random2(2, 4)) % notes.size() => int idx2;
28
+
29
+ Std.mtof(notes[idx1]) => p1.freq;
30
+ Std.mtof(notes[idx2]) * 1.003 => p2.freq; // slight detune
31
+
32
+ // Slow filter sweep
33
+ Math.random2f(400.0, 1200.0) => float targetFreq;
34
+ filt1.freq() => float startFreq;
35
+ for (0 => int i; i < 100; i++) {
36
+ startFreq + (targetFreq - startFreq) * (i / 100.0) => filt1.freq;
37
+ startFreq + (targetFreq - startFreq) * (i / 100.0) * 0.95 => filt2.freq;
38
+ 40::ms => now;
39
+ }
40
+ Math.random2(2000, 5000)::ms => now;
41
+ }
42
+ }
43
+
44
+ // === PULSE: rhythmic element, euclidean-inspired ===
45
+ fun int[] bjorklund(int k, int n) {
46
+ // Euclidean rhythm generator
47
+ int result[n];
48
+ if (k >= n) {
49
+ for (0 => int i; i < n; i++) 1 => result[i];
50
+ return result;
51
+ }
52
+ if (k == 0) {
53
+ for (0 => int i; i < n; i++) 0 => result[i];
54
+ return result;
55
+ }
56
+
57
+ int pattern[n];
58
+ float bucket;
59
+ 0.0 => bucket;
60
+ k $ float / n => float slope;
61
+ for (0 => int i; i < n; i++) {
62
+ bucket + slope => bucket;
63
+ if (bucket >= 1.0) {
64
+ 1 => pattern[i];
65
+ bucket - 1.0 => bucket;
66
+ } else {
67
+ 0 => pattern[i];
68
+ }
69
+ }
70
+ return pattern;
71
+ }
72
+
73
+ fun void pulse() {
74
+ SqrOsc sq => LPF filt => Gain pulseGain => master;
75
+ 0.06 => pulseGain.gain;
76
+ 2000.0 => filt.freq;
77
+ 2.0 => filt.Q;
78
+
79
+ // Evolving euclidean parameters
80
+ 3 => int k;
81
+ 8 => int n;
82
+
83
+ [36, 38, 41, 43, 46] @=> int bassNotes[]; // low pentatonic
84
+ 0 => int noteIdx;
85
+
86
+ 0 => int cycleCount;
87
+
88
+ while (true) {
89
+ bjorklund(k, n) @=> int rhythm[];
90
+
91
+ for (0 => int i; i < rhythm.size(); i++) {
92
+ if (rhythm[i]) {
93
+ Std.mtof(bassNotes[noteIdx]) => sq.freq;
94
+ 0.06 => pulseGain.gain;
95
+
96
+ // Quick decay
97
+ for (0 => int d; d < 10; d++) {
98
+ pulseGain.gain() * 0.85 => pulseGain.gain;
99
+ 15::ms => now;
100
+ }
101
+ } else {
102
+ 0.0 => pulseGain.gain;
103
+ 150::ms => now;
104
+ }
105
+ }
106
+
107
+ cycleCount++;
108
+ // Evolve every 4 cycles
109
+ if (cycleCount % 4 == 0) {
110
+ Math.random2(2, 5) => k;
111
+ Math.random2(7, 13) => n;
112
+ (noteIdx + Math.random2(1, 3)) % bassNotes.size() => noteIdx;
113
+ }
114
+ }
115
+ }
116
+
117
+ // === HIHAT: gentle tick pattern ===
118
+ fun void hihat() {
119
+ Noise noise => BPF bp => Gain hhGain => master;
120
+ 8000.0 => bp.freq;
121
+ 4.0 => bp.Q;
122
+ 0.0 => hhGain.gain;
123
+
124
+ while (true) {
125
+ // Random density: sometimes busy, sometimes sparse
126
+ Math.random2(2, 6) => int density;
127
+
128
+ for (0 => int i; i < 16; i++) {
129
+ if (Math.random2(0, 8) < density) {
130
+ Math.random2f(0.02, 0.06) => hhGain.gain;
131
+ 5::ms => now;
132
+ 0.0 => hhGain.gain;
133
+ Math.random2(80, 200)::ms => now;
134
+ } else {
135
+ Math.random2(100, 250)::ms => now;
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ // === MELODY: sparse, floating notes ===
142
+ fun void melody() {
143
+ SinOsc mel => LPF filt => Gain melGain => master;
144
+ 0.0 => melGain.gain;
145
+ 3000.0 => filt.freq;
146
+
147
+ [60, 63, 65, 67, 70, 72, 75, 77] @=> int notes[]; // high pentatonic
148
+
149
+ while (true) {
150
+ // 40% chance of playing a note
151
+ if (Math.random2f(0.0, 1.0) < 0.4) {
152
+ Math.random2(0, notes.size()-1) => int idx;
153
+ Std.mtof(notes[idx]) => mel.freq;
154
+
155
+ // Gentle attack
156
+ for (0 => int a; a < 20; a++) {
157
+ (a / 20.0) * Math.random2f(0.04, 0.08) => melGain.gain;
158
+ 8::ms => now;
159
+ }
160
+ // Hold
161
+ Math.random2(200, 600)::ms => now;
162
+ // Gentle release
163
+ melGain.gain() => float vol;
164
+ for (0 => int r; r < 30; r++) {
165
+ vol * (1.0 - r / 30.0) => melGain.gain;
166
+ 10::ms => now;
167
+ }
168
+ 0.0 => melGain.gain;
169
+ }
170
+
171
+ Math.random2(500, 2000)::ms => now;
172
+ }
173
+ }
174
+
175
+ // === LAUNCH ALL SHREDS ===
176
+ <<< " Starforge Coding Ambient", "" >>>;
177
+ <<< " Ctrl-C to stop", "" >>>;
178
+ <<< "" >>>;
179
+
180
+ spork ~ pad();
181
+ spork ~ pulse();
182
+ spork ~ hihat();
183
+ spork ~ melody();
184
+
185
+ // Keep main shred alive
186
+ while (true) 1::second => now;