tuneframes 0.1.0 → 0.2.0

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 (78) hide show
  1. package/LICENSE +167 -199
  2. package/README.md +150 -97
  3. package/examples/example-ai-dj-chill.html +167 -0
  4. package/examples/example-ai-dj-dark.html +167 -0
  5. package/examples/example-ai-dj-energetic.html +167 -0
  6. package/examples/example-ai-dj-happy.html +167 -0
  7. package/examples/example-ai-dj.html +167 -0
  8. package/examples/example-ambient.html +63 -63
  9. package/examples/example-ambient.mp3 +0 -0
  10. package/examples/example-bass.html +48 -0
  11. package/examples/example-lofi.html +45 -45
  12. package/examples/example-lofi.mp3 +0 -0
  13. package/examples/example-minimal.html +21 -19
  14. package/examples/example-minimal.mp3 +0 -0
  15. package/examples/example-orchestral.html +67 -67
  16. package/examples/example-orchestral.mp3 +0 -0
  17. package/examples/example-piano.html +53 -0
  18. package/examples/example-piano.mp3 +0 -0
  19. package/examples/example-techno.html +69 -69
  20. package/examples/example-techno.mp3 +0 -0
  21. package/package.json +42 -24
  22. package/registry/presets/bass-electric.html +67 -0
  23. package/registry/presets/bass-saw.html +22 -0
  24. package/registry/presets/chord-progression.html +22 -0
  25. package/registry/presets/drums-808.html +85 -0
  26. package/registry/presets/drums-lofi.html +35 -0
  27. package/registry/presets/lead-piano.html +26 -0
  28. package/registry/presets/piano-salamander.html +69 -0
  29. package/registry/presets/reverb-warm.html +11 -0
  30. package/registry/samples.json +226 -0
  31. package/skills/audio-ambient/SKILL.md +141 -0
  32. package/skills/audio-ambient/example.html +113 -0
  33. package/skills/audio-boss-battle/SKILL.md +157 -0
  34. package/skills/audio-boss-battle/example.html +185 -0
  35. package/skills/audio-boss-battle/example.mp3 +0 -0
  36. package/skills/audio-chillwave/SKILL.md +142 -0
  37. package/skills/audio-chillwave/example.html +144 -0
  38. package/skills/audio-cinematic/SKILL.md +147 -0
  39. package/skills/audio-cinematic/example.html +123 -0
  40. package/skills/audio-classical/SKILL.md +138 -0
  41. package/skills/audio-classical/example.html +145 -0
  42. package/skills/audio-dnb/SKILL.md +142 -0
  43. package/skills/audio-dnb/example.html +124 -0
  44. package/skills/audio-downtempo/SKILL.md +152 -0
  45. package/skills/audio-downtempo/example.html +164 -0
  46. package/skills/audio-folk/SKILL.md +139 -0
  47. package/skills/audio-folk/example.html +132 -0
  48. package/skills/audio-funk/SKILL.md +149 -0
  49. package/skills/audio-funk/example.html +144 -0
  50. package/skills/audio-future-bass/SKILL.md +163 -0
  51. package/skills/audio-future-bass/example.html +164 -0
  52. package/skills/audio-hip-hop/SKILL.md +133 -0
  53. package/skills/audio-hip-hop/example.html +129 -0
  54. package/skills/audio-house/SKILL.md +147 -0
  55. package/skills/audio-house/example.html +128 -0
  56. package/skills/audio-indie-pop/SKILL.md +150 -0
  57. package/skills/audio-indie-pop/example.html +121 -0
  58. package/skills/audio-jazz/SKILL.md +141 -0
  59. package/skills/audio-jazz/example.html +146 -0
  60. package/skills/audio-lofi/SKILL.md +140 -0
  61. package/skills/audio-lofi/example.html +135 -0
  62. package/skills/audio-minimal/SKILL.md +155 -0
  63. package/skills/audio-minimal/example.html +118 -0
  64. package/skills/audio-orchestral/SKILL.md +156 -0
  65. package/skills/audio-orchestral/example.html +140 -0
  66. package/skills/audio-r-and-b/SKILL.md +134 -0
  67. package/skills/audio-r-and-b/example.html +154 -0
  68. package/skills/audio-r-and-b/example.mp3 +0 -0
  69. package/skills/audio-techno/SKILL.md +140 -0
  70. package/skills/audio-techno/example.html +125 -0
  71. package/skills/audio-trap/SKILL.md +123 -0
  72. package/skills/audio-trap/example.html +119 -0
  73. package/skills/render-retest/example.html +115 -0
  74. package/skills/tuneframes/SKILL.md +221 -0
  75. package/skills/tuneframes-cli/SKILL.md +46 -0
  76. package/skills/verify/example.html +104 -0
  77. package/src/cli.js +261 -89
  78. package/src/render.js +134 -7
package/README.md CHANGED
@@ -1,160 +1,213 @@
1
- # TuneFrames — Agent-Native Music Generation
1
+ # TuneFrames
2
2
 
3
- **Write music compositions in HTML. Render them to MP3 with one CLI command.**
3
+ **Agent-native music generation for AI agents and developers.**
4
4
 
5
- TuneFrames brings the same portability model that Hyperframes (97K npm downloads/month) proved for video — to audio. Every AI agent, Claude Code session, and workflow tool can now compose music without a single line of native audio code.
5
+ Write a single HTML file with Tone.js. Render it to MP3 with one CLI command. No per-render fees. No API keys. Fully deterministic.
6
6
 
7
- ```bash
8
- npx tuneframes init my-track
9
- cd my-track && npx tuneframes render composition.html
10
- # Done. my-track/output.mp3 is ready.
11
- ```
7
+ [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white)](https://discord.gg/6VfDnxv3zC)
8
+ [![npm](https://img.shields.io/npm/dm/tuneframes?logo=npm)](https://www.npmjs.com/package/tuneframes)
9
+ [![GitHub stars](https://img.shields.io/github/stars/Shepherd217/TuneFrames)](https://github.com/Shepherd217/TuneFrames)
12
10
 
13
11
  ---
14
12
 
15
- ## How It Works
13
+ ## What's New in v0.2.0
16
14
 
17
- 1. **Write HTML** with Tone.js same API every web developer already knows
18
- 2. **Add a metadata block** so TuneFrames knows the tempo and duration
19
- 3. **Run `tuneframes render`**Chromium headless renders the composition offline, FFmpeg encodes to MP3/WAV
15
+ - **Sample instruments** real acoustic piano, bass, strings, brass, guitar, vibraphone, flute, and choir via `Tone.Sampler` + the gleitz FluidR3_GM CDN. No extra installs.
16
+ - **22 agent skills** 20 genre skills (lo-fi, jazz, techno, ambient, trap, D&B, classical, folk, funk, hip-hop, and more) plus the 2 core skills. Install any with `npx skills add shepherd217/tuneframes`.
17
+ - **AI DJ example**a single HTML file that renders four different moods (`chill`, `energetic`, `dark`, `happy`) from a URL param. Drop it into any html-anything pipeline as a parameterized audio surface.
20
18
 
21
- ```html
22
- <!DOCTYPE html>
23
- <html>
24
- <head>
25
- <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
26
- </head>
27
- <body>
28
- <div id="tuneframes" style="display:none">{"bpm":120,"duration":"4n"}</div>
29
- <script>
30
- async function main() {
31
- await Tone.start();
32
- const synth = new Tone.Synth().toDestination();
33
- synth.triggerAttackRelease('C4', '4n', 0);
34
- synth.triggerAttackRelease('E4', '4n', Tone.Time('4n').toSeconds());
35
- synth.triggerAttackRelease('G4', '4n', Tone.Time('4n').toSeconds() * 2);
36
- }
37
- </script>
38
- </body>
39
- </html>
19
+ ---
20
+
21
+ ## Option 1: With an AI coding agent (recommended)
22
+
23
+ ```bash
24
+ npx skills add shepherd217/tuneframes
40
25
  ```
41
26
 
42
- ---
27
+ Then describe what you want:
28
+
29
+ ```
30
+ Create a 10-second lo-fi beat with a piano chord progression, jazz brushes, and a walking bass
31
+ ```
32
+
33
+ The agent will write an HTML file and you can render it:
43
34
 
44
- ## Examples
35
+ ```bash
36
+ tuneframes render composition.html --output track.mp3
37
+ ```
45
38
 
46
- | Example | Description | BPM | Duration |
47
- |---------|-------------|-----|----------|
48
- | `minimal` | Single synth melody — the simplest possible TuneFrames composition | 120 | 2s |
49
- | `lofi` | Chord progression, melody, kick and snare — complete lo-fi hip-hop beat | 80 | 10s |
50
- | `techno` | 4-on-the-floor kick, noise hihat, detuned bass, pad chords | 130 | 2s |
51
- | `ambient` | Lush reverb pads, crystalline arpeggios — textural ambient | 60 | 2.5s |
52
- | `orchestral` | Strings, brass, woodwinds in a layered orchestral arrangement | 72 | 2.5s |
39
+ ---
53
40
 
54
- Run any example:
41
+ ## Option 2: CLI only
55
42
 
56
43
  ```bash
57
- npx tuneframes render node_modules/tuneframes/examples/example-lofi.html
44
+ npm install -g tuneframes
45
+ npx tuneframes init my-track
46
+ cd my-track
47
+ # Edit composition.html, then:
48
+ tuneframes render composition.html --output my-track.mp3
58
49
  ```
59
50
 
60
51
  ---
61
52
 
62
- ## API Reference
53
+ ## How it works
63
54
 
64
- ### Metadata Block
55
+ 1. **Write** — Create an HTML file using [Tone.js](https://tonejs.github.io/). `Tone.Offline()` renders the composition to an AudioBuffer with sample-accurate timing.
65
56
 
66
- Add a `<div id="tuneframes">` to the page body to tell TuneFrames how to render:
57
+ 2. **Render** The `tuneframes render` command:
58
+ - Spins up a headless browser
59
+ - Loads your HTML with Tone.js
60
+ - Runs `Tone.Offline()` to get the AudioBuffer
61
+ - Converts to WAV via `audioBufferToWav()`
62
+ - Encodes to MP3 via FFmpeg
67
63
 
68
- ```json
69
- {
70
- "bpm": 120, // Beats per minute (default: 120)
71
- "duration": "4n" // Render duration as Tone.js time (default: "4n")
72
- }
64
+ 3. **Done** — Your audio file, deterministic every time.
65
+
66
+ ---
67
+
68
+ ## Minimal example
69
+
70
+ ```html
71
+ <div id="tuneframes" style="display:none">{"bpm":120,"duration":"4s"}</div>
72
+ <script src="https://unpkg.com/tone@14.7.77/build/Tone.js"></script>
73
+ <script>
74
+ async function main() {
75
+ await Tone.start();
76
+ const synth = new Tone.Synth().toDestination();
77
+ synth.triggerAttackRelease('C4', '2n', 0);
78
+ synth.triggerAttackRelease('E4', '2n', '2n');
79
+ synth.triggerAttackRelease('G4', '2n', '4n');
80
+ synth.triggerAttackRelease('C5', '2n', '6n');
81
+ }
82
+ </script>
73
83
  ```
74
84
 
75
- TuneFrames uses [Tone.js time notation](https://tonejs.github.io/docs/latest/modules/Core.html#Time)`4n` (quarter note), `2n` (half note), `1n` (whole note), `8n` (eighth note), `16n` (sixteenth note), or any numeric value in seconds.
85
+ `tuneframes render track.html --output track.mp3`Tone.js CDN loads automatically.
86
+
87
+ > **Note on duration:** The `"duration"` field in the metadata block uses **seconds** (`"4s"`, `"10s"`). In Tone.js time notation, `n` means "whole note fractions" — `4n` = 4 quarter notes = 2 seconds at 120 BPM. Using literal seconds in metadata avoids this confusion.
88
+
89
+ ---
76
90
 
77
- ### `main()` Function
91
+ ## Sample Instruments
78
92
 
79
- Define an async `main()` function in a `<script>` tag. TuneFrames waits for it to complete, then renders the offline audio buffer.
93
+ TuneFrames v0.2.0 adds `Tone.Sampler` support via the [gleitz FluidR3_GM CDN](https://github.com/gleitz/midi-js-soundfonts) public domain, no extra installs, loads at render time.
94
+
95
+ ### Supported instruments
96
+
97
+ | Instrument | Category | Range |
98
+ |---|---|---|
99
+ | `acoustic_grand_piano` | Piano | A0–C8 |
100
+ | `acoustic_bass` | Bass | C1–G4 |
101
+ | `string_ensemble_1` | Strings | C2–C7 |
102
+ | `brass_section` | Brass | C2–C6 |
103
+ | `acoustic_guitar_nylon` | Guitar | E2–D6 |
104
+ | `vibraphone` | Mallet | C3–C7 |
105
+ | `flute` | Woodwind | C4–A7 |
106
+ | `choir_aahs` | Choir | C3–G5 |
107
+
108
+ Full URL mappings in [`registry/samples.json`](registry/samples.json).
109
+
110
+ ### Code example
80
111
 
81
112
  ```js
82
113
  async function main() {
83
114
  await Tone.start();
84
115
 
85
- const synth = new Tone.Synth().toDestination();
86
- // Schedule all your notes, chords, and patterns here
87
- synth.triggerAttackRelease('C4', '4n', 0);
88
- // ...
116
+ const piano = new Tone.Sampler({
117
+ urls: { A4: 'A4.mp3', C4: 'C4.mp3', 'F#4': 'Fs4.mp3', A5: 'A5.mp3' },
118
+ baseUrl: 'https://gleitz.github.io/midi-js-soundfonts/FluidR3_GM/acoustic_grand_piano-mp3/'
119
+ }).toDestination();
120
+
121
+ // Wait for samples to load before scheduling — this is required
122
+ await Tone.loaded();
123
+
124
+ piano.triggerAttackRelease(['C4','E4','G4'], '2n', 0);
125
+ piano.triggerAttackRelease(['F3','A3','C4'], '2n', Tone.Time('2n').toSeconds());
126
+ piano.triggerAttackRelease(['G3','B3','D4'], '2n', Tone.Time('1n').toSeconds());
127
+ piano.triggerAttackRelease(['C4','E4','G4'], '1n', Tone.Time('1n').toSeconds() + Tone.Time('2n').toSeconds());
89
128
  }
90
129
  ```
91
130
 
92
- ### Instruments
131
+ Ready-to-render presets: [`registry/presets/piano-salamander.html`](registry/presets/piano-salamander.html), [`registry/presets/bass-electric.html`](registry/presets/bass-electric.html).
93
132
 
94
- Tone.js provides a complete instrument library:
133
+ ---
95
134
 
96
- - **Synth / MonoSynth / PolySynth** — basic tone generation
97
- - **MembraneSynth** — kick drums, toms, bass drums
98
- - **NoiseSynth** — white/pink/brown noise for hi-hats, snares, textures
99
- - **MetalSynth** — metallic percussion (cymbals, shakers)
100
- - **Sampler** — load your own WAV/MP3 samples
135
+ ## Skills (22 total)
101
136
 
102
- ### Effects
137
+ Install all TuneFrames skills with one command:
103
138
 
104
- ```js
105
- const reverb = new Tone.Reverb({ decay: 2.5, wet: 0.3 }).toDestination();
106
- const comp = new Tone.Compressor(-12, 2).toDestination();
107
- const delay = new Tone.FeedbackDelay('8n', 0.4).toDestination();
139
+ ```bash
140
+ npx skills add shepherd217/tuneframes
108
141
  ```
109
142
 
143
+ ### Core skills (2)
144
+
145
+ | Skill | Description |
146
+ |---|---|
147
+ | `tuneframes` | Tone.js composition patterns, instruments, common patterns, duration warning |
148
+ | `tuneframes-cli` | CLI command reference, render pipeline, output options |
149
+
150
+ ### Genre skills (20)
151
+
152
+ ambient · boss-battle · chillwave · cinematic · classical · dnb · downtempo · folk · funk · future-bass · hip-hop · house · indie-pop · jazz · lofi · minimal · orchestral · r-and-b · techno · trap
153
+
154
+ Each genre skill includes BPM range, characteristic progressions, drum patterns, instrument configs, and a verified example composition.
155
+
110
156
  ---
111
157
 
112
- ## CLI
158
+ ## CLI reference
113
159
 
114
160
  ```bash
115
- # Render a composition to MP3
116
- tuneframes render <file.html> [--output <out.mp3>]
117
-
118
- # Render to WAV (no re-encoding)
119
- tuneframes render <file.html> --format wav [--output <out.wav>]
161
+ # Render a composition
162
+ tuneframes render my-track.html --output my-track.mp3
120
163
 
121
- # Preview in browser (dev mode with live reload)
122
- tuneframes preview <file.html>
164
+ # Preview in browser (live reload)
165
+ tuneframes preview my-track.html
123
166
 
124
- # Scaffold a new project
167
+ # Scaffold a new track
125
168
  tuneframes init my-track
169
+
170
+ # Add a preset (reverb, drums, bass, chords, piano)
171
+ tuneframes add reverb-warm
172
+ tuneframes add drums-lofi
173
+ tuneframes add bass-saw
174
+ tuneframes add chord-progression
175
+ tuneframes add lead-piano
126
176
  ```
127
177
 
178
+ See [`examples/`](examples/) for full compositions — ambient, lo-fi, techno, orchestral, piano, bass, and the AI DJ parameterized example.
179
+
128
180
  ---
129
181
 
130
- ## TuneFrames vs Hyperframes
182
+ ## Requirements
131
183
 
132
- TuneFrames is the audio sibling of [Hyperframes](https://github.com/cusidoc/hyperframes) — the same portability model, the same agent-first philosophy, but for music instead of video.
184
+ - Node.js 18+
185
+ - FFmpeg (`apt install ffmpeg` or `brew install ffmpeg`)
133
186
 
134
- | | Hyperframes | TuneFrames |
135
- |---|---|---|
136
- | Output | MP4 video | MP3/WAV audio |
137
- | Framework | Remotion (React) | Tone.js |
138
- | Use case | Video generation, animation | Music composition, sound design |
139
- | Deterministic | Yes | Yes |
187
+ ---
188
+
189
+ ## Comparison
190
+
191
+ | | TuneFrames | Hyperframes | Suno API | ElevenLabs |
192
+ |---|---|---|---|---|
193
+ | Open source | ✓ | ✓ | ✗ | ✗ |
194
+ | Per-render fee | None | None | Yes | Yes |
195
+ | Agent-native | ✓ | ✓ | Wrapper | Wrapper |
196
+ | Full audio control | ✓ | ✓ | Limited | Limited |
197
+ | Deterministic output | ✓ | ✓ | ✗ | ✗ |
198
+ | Sample instruments | ✓ | N/A | N/A | N/A |
199
+ | Modality | Audio | Video | Audio | Audio |
140
200
 
141
201
  ---
142
202
 
143
- ## Architecture
203
+ ## See Also
144
204
 
145
- ```
146
- HTML + Tone.js → Chromium (headless, Tone.Offline)
147
- → WAV ( PCM 44.1kHz mono )
148
- → FFmpeg
149
- → MP3 192kbps
150
- ```
205
+ TuneFrames is the audio surface in the html-anything paradigm — write HTML, render anything.
151
206
 
152
- **Tone.Offline** is the key: it renders audio without audio hardware or a browser audio context, producing bit-for-bit identical output every time. Same input HTML = same output MP3, guaranteed.
207
+ - [Hyperframes](https://github.com/Shepherd217/Hyperframes) the video counterpart: write HTML with GSAP/Three.js, render to MP4. Same model, same CLI pattern.
153
208
 
154
209
  ---
155
210
 
156
211
  ## License
157
212
 
158
- Apache 2.0 — use it in any project, commercial or open-source, no strings attached.
159
-
160
- [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
213
+ Apache 2.0
@@ -0,0 +1,167 @@
1
+ <script src="https://unpkg.com/tone@14.7.77/build/Tone.js"></script>
2
+ <script>
3
+ // ─── AI DJ — mood-parameterized Tone.js composition ────────────────────────
4
+ // Renders different music based on ?mood= query param or window.MOOD
5
+ // Supported moods: chill | energetic | dark | happy
6
+ // ──────────────────────────────────────────────────────────────────────────
7
+
8
+ const MOOD = (() => {
9
+ const p = 'chill'; // forced
10
+ return ['chill', 'energetic', 'dark', 'happy'].includes(p) ? p : 'chill';
11
+ })();
12
+
13
+ const CONFIG = {
14
+ chill: {
15
+ bpm: 72, duration: '12s',
16
+ scale: ['D3', 'F3', 'A3', 'C4'],
17
+ chords: [['D3','F3','A3'],['C3','E3','G3'],['D3','F3','A3'],['Bb2','D3','F3']],
18
+ leadNotes: ['D4','F4','A4','C5','D4','Bb3'],
19
+ leadRhythm: ['4n','4n','4n','4n','4n','4n'],
20
+ filterFreq: 800, reverbWet: 0.85, delayWet: 0.4, delayTime: '8n.',
21
+ kickTimes: [0, 2, 4, 6, 8, 10],
22
+ hatTimes: [1, 3, 5, 7, 9, 11],
23
+ snareTimes: [2, 6, 10],
24
+ },
25
+ energetic: {
26
+ bpm: 140, duration: '8s',
27
+ scale: ['C4', 'E4', 'G4', 'A4'],
28
+ chords: [['C4','E4','G4'],['A3','C4','E4'],['F3','A3','C4'],['G3','B3','D4']],
29
+ leadNotes: ['C5','E5','G5','C5','D5','E5'],
30
+ leadRhythm: ['8n','8n','8n','8n','8n','8n'],
31
+ filterFreq: 4000, reverbWet: 0.2, delayWet: 0.1, delayTime: '16n',
32
+ kickTimes: [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5],
33
+ hatTimes: [0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 3.25, 3.5, 3.75, 4, 4.25, 4.5, 4.75, 5, 5.25, 5.5, 5.75, 6, 6.25, 6.5, 6.75, 7, 7.25, 7.5, 7.75],
34
+ snareTimes: [1, 3, 5, 7],
35
+ },
36
+ dark: {
37
+ bpm: 80, duration: '10s',
38
+ scale: ['A2', 'C3', 'E3', 'G3'],
39
+ chords: [['A2','C3','E3'],['E2','G2','B2'],['A2','C3','E3'],['D2','F2','A2']],
40
+ leadNotes: ['A3','G3','E3','C3','A3','E3'],
41
+ leadRhythm: ['2n','2n','2n','2n','2n','2n'],
42
+ filterFreq: 600, reverbWet: 0.9, delayWet: 0.5, delayTime: '4n',
43
+ kickTimes: [0, 3, 6, 9],
44
+ hatTimes: [1.5, 4.5, 7.5],
45
+ snareTimes: [1.5, 5.5, 9.5],
46
+ },
47
+ happy: {
48
+ bpm: 120, duration: '8s',
49
+ scale: ['C4', 'E4', 'G4', 'A4'],
50
+ chords: [['C4','E4','G4'],['F3','A3','C4'],['G3','B3','D4'],['C4','E4','G4']],
51
+ leadNotes: ['C5','E5','G5','E5','D5','C5'],
52
+ leadRhythm: ['8n.','8n.','8n.','8n.','8n.','8n.'],
53
+ filterFreq: 5000, reverbWet: 0.3, delayWet: 0.2, delayTime: '8n',
54
+ kickTimes: [0, 1, 2, 3, 4, 5, 6, 7],
55
+ hatTimes: [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5],
56
+ snareTimes: [0.5, 2.5, 4.5, 6.5],
57
+ },
58
+ };
59
+
60
+ const cfg = CONFIG[MOOD];
61
+
62
+ async function main() {
63
+ await Tone.start();
64
+
65
+ const pad = new Tone.PolySynth(Tone.Synth, {
66
+ oscillator: { type: 'sine' },
67
+ envelope: { attack: 2.0, decay: 1, sustain: 0.8, release: 3 },
68
+ volume: -6,
69
+ }).toDestination();
70
+
71
+ const lead = new Tone.Synth({
72
+ oscillator: { type: MOOD === 'dark' ? 'sawtooth' : MOOD === 'energetic' ? 'square' : 'triangle' },
73
+ envelope: { attack: 0.02, decay: 0.1, sustain: 0.3, release: 0.5 },
74
+ volume: -4,
75
+ }).toDestination();
76
+
77
+ const bass = new Tone.MonoSynth({
78
+ oscillator: { type: 'sawtooth' },
79
+ filter: { Q: 2, type: 'lowpass', frequency: cfg.filterFreq },
80
+ envelope: { attack: 0.01, decay: 0.3, sustain: 0.4, release: 0.4 },
81
+ volume: -8,
82
+ }).toDestination();
83
+
84
+ const reverb = new Tone.Reverb({ decay: 4.5, wet: cfg.reverbWet }).toDestination();
85
+ const delay = new Tone.FeedbackDelay({ delayTime: cfg.delayTime, feedback: 0.3, wet: cfg.delayWet }).toDestination();
86
+ pad.connect(reverb);
87
+ lead.connect(delay);
88
+ delay.toDestination();
89
+
90
+ // Chord pads
91
+ new Tone.Sequence((time, chord) => {
92
+ pad.triggerAttackRelease(chord, '2n', time);
93
+ }, cfg.chords, 0).start(0);
94
+
95
+ // Lead melody
96
+ new Tone.Sequence((time, note) => {
97
+ lead.triggerAttackRelease(note, '8n.', time);
98
+ }, cfg.leadNotes, 0).start(0);
99
+
100
+ // Bass
101
+ new Tone.Sequence((time, note) => {
102
+ bass.triggerAttackRelease(note, '4n', time);
103
+ }, cfg.chords.map(c => c[0]), 0).start(0);
104
+
105
+ // Kick
106
+ const kick = new Tone.MembraneSynth({
107
+ pitchDecay: 0.05, octaves: 6,
108
+ envelope: { attack: 0.001, decay: 0.4, sustain: 0, release: 0.1 },
109
+ volume: MOOD === 'chill' ? -12 : MOOD === 'dark' ? -14 : -6,
110
+ }).toDestination();
111
+ cfg.kickTimes.forEach(t => {
112
+ Tone.Transport.scheduleOnce((time) => {
113
+ kick.triggerAttackRelease('C1', '8n', time);
114
+ }, t);
115
+ });
116
+
117
+ // Hi-hat
118
+ const hihat = new Tone.NoiseSynth({
119
+ noise: { type: 'white' },
120
+ envelope: { attack: 0.001, decay: 0.05, sustain: 0, release: 0.01 },
121
+ volume: MOOD === 'chill' ? -18 : -10,
122
+ }).toDestination();
123
+ cfg.hatTimes.forEach(t => {
124
+ Tone.Transport.scheduleOnce((time) => {
125
+ hihat.triggerAttackRelease('16n', time);
126
+ }, t);
127
+ });
128
+
129
+ // Snare
130
+ const snare = new Tone.NoiseSynth({
131
+ noise: { type: 'pink' },
132
+ envelope: { attack: 0.001, decay: 0.15, sustain: 0, release: 0.1 },
133
+ volume: MOOD === 'chill' ? -16 : MOOD === 'dark' ? -14 : -8,
134
+ }).toDestination();
135
+ cfg.snareTimes.forEach(t => {
136
+ Tone.Transport.scheduleOnce((time) => {
137
+ snare.triggerAttackRelease('8n', time);
138
+ }, t);
139
+ });
140
+
141
+ Tone.Transport.bpm.value = cfg.bpm;
142
+ Tone.Transport.start();
143
+ }
144
+
145
+ window.renderComposition = async function(wavPath) {
146
+ let attempts = 0;
147
+ while (typeof Tone === 'undefined' && attempts < 50) {
148
+ await new Promise(r => setTimeout(r, 100));
149
+ attempts++;
150
+ }
151
+ if (typeof Tone === 'undefined') throw new Error('Tone not loaded');
152
+
153
+ const durationSec = Math.max(Tone.Time(cfg.duration).toSeconds() + 0.5, 2);
154
+
155
+ const buffer = await Tone.Offline(async () => { await main(); }, durationSec, 1, 44100, cfg.bpm);
156
+ const wav = audioBufferToWav(buffer);
157
+ window.writeFile(wavPath, Array.from(new Uint8Array(wav)));
158
+ return wav.byteLength;
159
+ };
160
+ </script>
161
+
162
+ <!-- Metadata driven by JS so each mood gets correct BPM + duration -->
163
+ <script>
164
+ document.open();
165
+ document.write(`<div id="tuneframes" style="display:none">{"bpm":${cfg.bpm},"duration":"${cfg.duration}"}</div>`);
166
+ document.close();
167
+ </script>
@@ -0,0 +1,167 @@
1
+ <script src="https://unpkg.com/tone@14.7.77/build/Tone.js"></script>
2
+ <script>
3
+ // ─── AI DJ — mood-parameterized Tone.js composition ────────────────────────
4
+ // Renders different music based on ?mood= query param or window.MOOD
5
+ // Supported moods: chill | energetic | dark | happy
6
+ // ──────────────────────────────────────────────────────────────────────────
7
+
8
+ const MOOD = (() => {
9
+ const p = 'dark'; // forced
10
+ return ['chill', 'energetic', 'dark', 'happy'].includes(p) ? p : 'chill';
11
+ })();
12
+
13
+ const CONFIG = {
14
+ chill: {
15
+ bpm: 72, duration: '12s',
16
+ scale: ['D3', 'F3', 'A3', 'C4'],
17
+ chords: [['D3','F3','A3'],['C3','E3','G3'],['D3','F3','A3'],['Bb2','D3','F3']],
18
+ leadNotes: ['D4','F4','A4','C5','D4','Bb3'],
19
+ leadRhythm: ['4n','4n','4n','4n','4n','4n'],
20
+ filterFreq: 800, reverbWet: 0.85, delayWet: 0.4, delayTime: '8n.',
21
+ kickTimes: [0, 2, 4, 6, 8, 10],
22
+ hatTimes: [1, 3, 5, 7, 9, 11],
23
+ snareTimes: [2, 6, 10],
24
+ },
25
+ energetic: {
26
+ bpm: 140, duration: '8s',
27
+ scale: ['C4', 'E4', 'G4', 'A4'],
28
+ chords: [['C4','E4','G4'],['A3','C4','E4'],['F3','A3','C4'],['G3','B3','D4']],
29
+ leadNotes: ['C5','E5','G5','C5','D5','E5'],
30
+ leadRhythm: ['8n','8n','8n','8n','8n','8n'],
31
+ filterFreq: 4000, reverbWet: 0.2, delayWet: 0.1, delayTime: '16n',
32
+ kickTimes: [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5],
33
+ hatTimes: [0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 3.25, 3.5, 3.75, 4, 4.25, 4.5, 4.75, 5, 5.25, 5.5, 5.75, 6, 6.25, 6.5, 6.75, 7, 7.25, 7.5, 7.75],
34
+ snareTimes: [1, 3, 5, 7],
35
+ },
36
+ dark: {
37
+ bpm: 80, duration: '10s',
38
+ scale: ['A2', 'C3', 'E3', 'G3'],
39
+ chords: [['A2','C3','E3'],['E2','G2','B2'],['A2','C3','E3'],['D2','F2','A2']],
40
+ leadNotes: ['A3','G3','E3','C3','A3','E3'],
41
+ leadRhythm: ['2n','2n','2n','2n','2n','2n'],
42
+ filterFreq: 600, reverbWet: 0.9, delayWet: 0.5, delayTime: '4n',
43
+ kickTimes: [0, 3, 6, 9],
44
+ hatTimes: [1.5, 4.5, 7.5],
45
+ snareTimes: [1.5, 5.5, 9.5],
46
+ },
47
+ happy: {
48
+ bpm: 120, duration: '8s',
49
+ scale: ['C4', 'E4', 'G4', 'A4'],
50
+ chords: [['C4','E4','G4'],['F3','A3','C4'],['G3','B3','D4'],['C4','E4','G4']],
51
+ leadNotes: ['C5','E5','G5','E5','D5','C5'],
52
+ leadRhythm: ['8n.','8n.','8n.','8n.','8n.','8n.'],
53
+ filterFreq: 5000, reverbWet: 0.3, delayWet: 0.2, delayTime: '8n',
54
+ kickTimes: [0, 1, 2, 3, 4, 5, 6, 7],
55
+ hatTimes: [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5],
56
+ snareTimes: [0.5, 2.5, 4.5, 6.5],
57
+ },
58
+ };
59
+
60
+ const cfg = CONFIG[MOOD];
61
+
62
+ async function main() {
63
+ await Tone.start();
64
+
65
+ const pad = new Tone.PolySynth(Tone.Synth, {
66
+ oscillator: { type: 'sine' },
67
+ envelope: { attack: 2.0, decay: 1, sustain: 0.8, release: 3 },
68
+ volume: -6,
69
+ }).toDestination();
70
+
71
+ const lead = new Tone.Synth({
72
+ oscillator: { type: MOOD === 'dark' ? 'sawtooth' : MOOD === 'energetic' ? 'square' : 'triangle' },
73
+ envelope: { attack: 0.02, decay: 0.1, sustain: 0.3, release: 0.5 },
74
+ volume: -4,
75
+ }).toDestination();
76
+
77
+ const bass = new Tone.MonoSynth({
78
+ oscillator: { type: 'sawtooth' },
79
+ filter: { Q: 2, type: 'lowpass', frequency: cfg.filterFreq },
80
+ envelope: { attack: 0.01, decay: 0.3, sustain: 0.4, release: 0.4 },
81
+ volume: -8,
82
+ }).toDestination();
83
+
84
+ const reverb = new Tone.Reverb({ decay: 4.5, wet: cfg.reverbWet }).toDestination();
85
+ const delay = new Tone.FeedbackDelay({ delayTime: cfg.delayTime, feedback: 0.3, wet: cfg.delayWet }).toDestination();
86
+ pad.connect(reverb);
87
+ lead.connect(delay);
88
+ delay.toDestination();
89
+
90
+ // Chord pads
91
+ new Tone.Sequence((time, chord) => {
92
+ pad.triggerAttackRelease(chord, '2n', time);
93
+ }, cfg.chords, 0).start(0);
94
+
95
+ // Lead melody
96
+ new Tone.Sequence((time, note) => {
97
+ lead.triggerAttackRelease(note, '8n.', time);
98
+ }, cfg.leadNotes, 0).start(0);
99
+
100
+ // Bass
101
+ new Tone.Sequence((time, note) => {
102
+ bass.triggerAttackRelease(note, '4n', time);
103
+ }, cfg.chords.map(c => c[0]), 0).start(0);
104
+
105
+ // Kick
106
+ const kick = new Tone.MembraneSynth({
107
+ pitchDecay: 0.05, octaves: 6,
108
+ envelope: { attack: 0.001, decay: 0.4, sustain: 0, release: 0.1 },
109
+ volume: MOOD === 'chill' ? -12 : MOOD === 'dark' ? -14 : -6,
110
+ }).toDestination();
111
+ cfg.kickTimes.forEach(t => {
112
+ Tone.Transport.scheduleOnce((time) => {
113
+ kick.triggerAttackRelease('C1', '8n', time);
114
+ }, t);
115
+ });
116
+
117
+ // Hi-hat
118
+ const hihat = new Tone.NoiseSynth({
119
+ noise: { type: 'white' },
120
+ envelope: { attack: 0.001, decay: 0.05, sustain: 0, release: 0.01 },
121
+ volume: MOOD === 'chill' ? -18 : -10,
122
+ }).toDestination();
123
+ cfg.hatTimes.forEach(t => {
124
+ Tone.Transport.scheduleOnce((time) => {
125
+ hihat.triggerAttackRelease('16n', time);
126
+ }, t);
127
+ });
128
+
129
+ // Snare
130
+ const snare = new Tone.NoiseSynth({
131
+ noise: { type: 'pink' },
132
+ envelope: { attack: 0.001, decay: 0.15, sustain: 0, release: 0.1 },
133
+ volume: MOOD === 'chill' ? -16 : MOOD === 'dark' ? -14 : -8,
134
+ }).toDestination();
135
+ cfg.snareTimes.forEach(t => {
136
+ Tone.Transport.scheduleOnce((time) => {
137
+ snare.triggerAttackRelease('8n', time);
138
+ }, t);
139
+ });
140
+
141
+ Tone.Transport.bpm.value = cfg.bpm;
142
+ Tone.Transport.start();
143
+ }
144
+
145
+ window.renderComposition = async function(wavPath) {
146
+ let attempts = 0;
147
+ while (typeof Tone === 'undefined' && attempts < 50) {
148
+ await new Promise(r => setTimeout(r, 100));
149
+ attempts++;
150
+ }
151
+ if (typeof Tone === 'undefined') throw new Error('Tone not loaded');
152
+
153
+ const durationSec = Math.max(Tone.Time(cfg.duration).toSeconds() + 0.5, 2);
154
+
155
+ const buffer = await Tone.Offline(async () => { await main(); }, durationSec, 1, 44100, cfg.bpm);
156
+ const wav = audioBufferToWav(buffer);
157
+ window.writeFile(wavPath, Array.from(new Uint8Array(wav)));
158
+ return wav.byteLength;
159
+ };
160
+ </script>
161
+
162
+ <!-- Metadata driven by JS so each mood gets correct BPM + duration -->
163
+ <script>
164
+ document.open();
165
+ document.write(`<div id="tuneframes" style="display:none">{"bpm":${cfg.bpm},"duration":"${cfg.duration}"}</div>`);
166
+ document.close();
167
+ </script>