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.
- package/LICENSE +167 -199
- package/README.md +150 -97
- package/examples/example-ai-dj-chill.html +167 -0
- package/examples/example-ai-dj-dark.html +167 -0
- package/examples/example-ai-dj-energetic.html +167 -0
- package/examples/example-ai-dj-happy.html +167 -0
- package/examples/example-ai-dj.html +167 -0
- package/examples/example-ambient.html +63 -63
- package/examples/example-ambient.mp3 +0 -0
- package/examples/example-bass.html +48 -0
- package/examples/example-lofi.html +45 -45
- package/examples/example-lofi.mp3 +0 -0
- package/examples/example-minimal.html +21 -19
- package/examples/example-minimal.mp3 +0 -0
- package/examples/example-orchestral.html +67 -67
- package/examples/example-orchestral.mp3 +0 -0
- package/examples/example-piano.html +53 -0
- package/examples/example-piano.mp3 +0 -0
- package/examples/example-techno.html +69 -69
- package/examples/example-techno.mp3 +0 -0
- package/package.json +42 -24
- package/registry/presets/bass-electric.html +67 -0
- package/registry/presets/bass-saw.html +22 -0
- package/registry/presets/chord-progression.html +22 -0
- package/registry/presets/drums-808.html +85 -0
- package/registry/presets/drums-lofi.html +35 -0
- package/registry/presets/lead-piano.html +26 -0
- package/registry/presets/piano-salamander.html +69 -0
- package/registry/presets/reverb-warm.html +11 -0
- package/registry/samples.json +226 -0
- package/skills/audio-ambient/SKILL.md +141 -0
- package/skills/audio-ambient/example.html +113 -0
- package/skills/audio-boss-battle/SKILL.md +157 -0
- package/skills/audio-boss-battle/example.html +185 -0
- package/skills/audio-boss-battle/example.mp3 +0 -0
- package/skills/audio-chillwave/SKILL.md +142 -0
- package/skills/audio-chillwave/example.html +144 -0
- package/skills/audio-cinematic/SKILL.md +147 -0
- package/skills/audio-cinematic/example.html +123 -0
- package/skills/audio-classical/SKILL.md +138 -0
- package/skills/audio-classical/example.html +145 -0
- package/skills/audio-dnb/SKILL.md +142 -0
- package/skills/audio-dnb/example.html +124 -0
- package/skills/audio-downtempo/SKILL.md +152 -0
- package/skills/audio-downtempo/example.html +164 -0
- package/skills/audio-folk/SKILL.md +139 -0
- package/skills/audio-folk/example.html +132 -0
- package/skills/audio-funk/SKILL.md +149 -0
- package/skills/audio-funk/example.html +144 -0
- package/skills/audio-future-bass/SKILL.md +163 -0
- package/skills/audio-future-bass/example.html +164 -0
- package/skills/audio-hip-hop/SKILL.md +133 -0
- package/skills/audio-hip-hop/example.html +129 -0
- package/skills/audio-house/SKILL.md +147 -0
- package/skills/audio-house/example.html +128 -0
- package/skills/audio-indie-pop/SKILL.md +150 -0
- package/skills/audio-indie-pop/example.html +121 -0
- package/skills/audio-jazz/SKILL.md +141 -0
- package/skills/audio-jazz/example.html +146 -0
- package/skills/audio-lofi/SKILL.md +140 -0
- package/skills/audio-lofi/example.html +135 -0
- package/skills/audio-minimal/SKILL.md +155 -0
- package/skills/audio-minimal/example.html +118 -0
- package/skills/audio-orchestral/SKILL.md +156 -0
- package/skills/audio-orchestral/example.html +140 -0
- package/skills/audio-r-and-b/SKILL.md +134 -0
- package/skills/audio-r-and-b/example.html +154 -0
- package/skills/audio-r-and-b/example.mp3 +0 -0
- package/skills/audio-techno/SKILL.md +140 -0
- package/skills/audio-techno/example.html +125 -0
- package/skills/audio-trap/SKILL.md +123 -0
- package/skills/audio-trap/example.html +119 -0
- package/skills/render-retest/example.html +115 -0
- package/skills/tuneframes/SKILL.md +221 -0
- package/skills/tuneframes-cli/SKILL.md +46 -0
- package/skills/verify/example.html +104 -0
- package/src/cli.js +261 -89
- package/src/render.js +134 -7
package/README.md
CHANGED
|
@@ -1,160 +1,213 @@
|
|
|
1
|
-
# TuneFrames
|
|
1
|
+
# TuneFrames
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Agent-native music generation — for AI agents and developers.**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# Done. my-track/output.mp3 is ready.
|
|
11
|
-
```
|
|
7
|
+
[](https://discord.gg/6VfDnxv3zC)
|
|
8
|
+
[](https://www.npmjs.com/package/tuneframes)
|
|
9
|
+
[](https://github.com/Shepherd217/TuneFrames)
|
|
12
10
|
|
|
13
11
|
---
|
|
14
12
|
|
|
15
|
-
##
|
|
13
|
+
## What's New in v0.2.0
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
35
|
+
```bash
|
|
36
|
+
tuneframes render composition.html --output track.mp3
|
|
37
|
+
```
|
|
45
38
|
|
|
46
|
-
|
|
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
|
-
|
|
41
|
+
## Option 2: CLI only
|
|
55
42
|
|
|
56
43
|
```bash
|
|
57
|
-
|
|
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
|
-
##
|
|
53
|
+
## How it works
|
|
63
54
|
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
## Sample Instruments
|
|
78
92
|
|
|
79
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
+
---
|
|
95
134
|
|
|
96
|
-
|
|
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
|
-
|
|
137
|
+
Install all TuneFrames skills with one command:
|
|
103
138
|
|
|
104
|
-
```
|
|
105
|
-
|
|
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
|
|
116
|
-
tuneframes render
|
|
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 (
|
|
122
|
-
tuneframes preview
|
|
164
|
+
# Preview in browser (live reload)
|
|
165
|
+
tuneframes preview my-track.html
|
|
123
166
|
|
|
124
|
-
# Scaffold a new
|
|
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
|
-
##
|
|
182
|
+
## Requirements
|
|
131
183
|
|
|
132
|
-
|
|
184
|
+
- Node.js 18+
|
|
185
|
+
- FFmpeg (`apt install ffmpeg` or `brew install ffmpeg`)
|
|
133
186
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
|
139
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
[](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>
|