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
@@ -0,0 +1,119 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>TuneFrames — Trap</title>
6
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
7
+ </head>
8
+ <body>
9
+ <!-- TuneFrames Metadata -->
10
+ <div id="tuneframes" style="display:none">{"bpm":140,"duration":"12s"}</div>
11
+
12
+ <script>
13
+ async function main() {
14
+ await Tone.start();
15
+ Tone.Transport.bpm.value = 140;
16
+
17
+ // --- MASTER BUS ---
18
+ const masterLimiter = new Tone.Limiter(-1).toDestination();
19
+ const masterGain = new Tone.Gain(0.85).connect(masterLimiter);
20
+
21
+ // --- 808 SUB — MembraneSynth with long pitchDecay for the signature glide ---
22
+ const subSaturate = new Tone.Distortion({ distortion: 0.12, wet: 0.25 }).connect(masterGain);
23
+ const subGain = new Tone.Gain(1.1).connect(subSaturate);
24
+ const sub = new Tone.MembraneSynth({
25
+ pitchDecay: 0.48, // the 808 "fall" — longer = more dramatic
26
+ octaves: 3, // pitch range of the glide
27
+ envelope: { attack: 0.001, decay: 1.1, sustain: 0, release: 0.5 }
28
+ }).connect(subGain);
29
+
30
+ // --- SNARE — punchy white noise, dry ---
31
+ const snareVerb = new Tone.Reverb({ decay: 0.7, wet: 0.18 }).connect(masterGain);
32
+ const snareGain = new Tone.Gain(0.7).connect(snareVerb);
33
+ const snare = new Tone.NoiseSynth({
34
+ noise: { type: "white" },
35
+ envelope: { attack: 0.001, decay: 0.16, sustain: 0, release: 0.05 }
36
+ }).connect(snareGain);
37
+
38
+ // --- HI-HAT — very tight, high frequency ---
39
+ const hatGain = new Tone.Gain(0.42).connect(masterGain);
40
+ const hat = new Tone.MetalSynth({
41
+ frequency: 820,
42
+ envelope: { attack: 0.001, decay: 0.028, release: 0.008 },
43
+ harmonicity: 5.1,
44
+ modulationIndex: 32,
45
+ resonance: 5200,
46
+ octaves: 2
47
+ }).connect(hatGain);
48
+
49
+ // --- STRINGS — atmospheric PolySynth, heavy reverb ---
50
+ const stringsVerb = new Tone.Reverb({ decay: 4.5, wet: 0.65 }).connect(masterGain);
51
+ const stringsGain = new Tone.Gain(0.5).connect(stringsVerb);
52
+ const strings = new Tone.PolySynth(Tone.Synth, {
53
+ oscillator: { type: "sawtooth4" },
54
+ envelope: { attack: 0.5, decay: 1.2, sustain: 0.65, release: 2.0 },
55
+ volume: -2
56
+ }).connect(stringsGain);
57
+
58
+ // --- 808 PATTERN — half-time feel, beat 1 of every bar ---
59
+ // Pattern: hit on beat 1, ghost on beat 3.5
60
+ const subPattern = new Tone.Sequence((time, note) => {
61
+ if (note === "C2") {
62
+ sub.triggerAttackRelease("C2", "2n", time);
63
+ } else if (note === "G1") {
64
+ sub.triggerAttackRelease("G1", "4n", time);
65
+ }
66
+ }, ["C2", null, null, null, null, null, "G1", null,
67
+ "C2", null, null, null, null, null, null, null], "16n");
68
+ subPattern.start(0);
69
+
70
+ // --- SNARE PATTERN — half-time: beat 3 of the bar (step 8 of 16) ---
71
+ const snareSeq = new Tone.Sequence((time, val) => {
72
+ if (val) snare.triggerAttackRelease("16n", time);
73
+ }, [null, null, null, null, null, null, null, null,
74
+ 1, null, null, null, null, null, null, null], "16n");
75
+ snareSeq.start(0);
76
+
77
+ // --- HI-HAT ROLLS — the trap signature ---
78
+ // 32nd-note grid: velocity variation creates rolls and accents
79
+ // Pattern evolves every bar: starts sparse, builds to dense rolls
80
+ let hatStep = 0;
81
+
82
+ // Bar 1-2 velocity map: sparse 16th pattern (every 2 32nds)
83
+ const hatBar1 = [
84
+ 0.8, 0, 0.4, 0, 0.8, 0, 0.4, 0,
85
+ 0.8, 0, 0.4, 0, 0.8, 0.3, 0.9, 0.3
86
+ ];
87
+ // Bar 3-4: 16th rolls with triplet accent flurry
88
+ const hatBar2 = [
89
+ 0.8, 0.3, 0.6, 0.2, 0.8, 0.3, 0.5, 0.2,
90
+ 0.7, 0.3, 0.6, 0.2, 0.9, 0.5, 0.8, 0.5
91
+ ];
92
+ // Bar 5-6: dense 32nd roll building to drop
93
+ const hatBar3 = [
94
+ 0.6, 0.3, 0.7, 0.3, 0.6, 0.4, 0.8, 0.3,
95
+ 0.6, 0.4, 0.7, 0.5, 0.8, 0.6, 0.9, 0.7
96
+ ];
97
+
98
+ // Merge all bars into one 96-step (6-bar) loop for 32nds at 140bpm
99
+ const fullHatPattern = [...hatBar1, ...hatBar1, ...hatBar2, ...hatBar2, ...hatBar3, ...hatBar3];
100
+
101
+ const hatSeq = new Tone.Sequence((time, vel) => {
102
+ if (vel > 0) hat.triggerAttackRelease("32n", time, vel);
103
+ }, fullHatPattern, "32n");
104
+ hatSeq.start(0);
105
+
106
+ // --- STRINGS — atmospheric minor chord, enters at bar 3 ---
107
+ // Cm: C3-Eb3-G3 + high B4 tension note
108
+ const stringNotes = ["C3", "Eb3", "G3", "Bb3"];
109
+ strings.triggerAttackRelease(stringNotes, "2m", "2m");
110
+ // Resolution chord at bar 5
111
+ strings.triggerAttackRelease(["Ab2", "C3", "Eb3", "G3"], "2m", "4m");
112
+ strings.triggerAttackRelease(["C3", "Eb3", "G3", "Bb3"], "2m", "6m");
113
+
114
+ }
115
+
116
+ main();
117
+ </script>
118
+ </body>
119
+ </html>
@@ -0,0 +1,115 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>TuneFrames — Render Retest</title>
6
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
7
+ </head>
8
+ <body>
9
+ <!-- TuneFrames Metadata -->
10
+ <div id="tuneframes" style="display:none">{"bpm":120,"duration":"10s"}</div>
11
+
12
+ <script>
13
+ async function main() {
14
+ await Tone.start();
15
+
16
+ // 120 BPM: quarter note = 0.5s, bar = 2.0s, 10s = 5 bars exactly.
17
+ // All note times are absolute seconds from 0. No Tone.now(), no Transport.start().
18
+ const beat = 0.5;
19
+ const bar = 2.0;
20
+
21
+ // --- MASTER BUS ---
22
+ const limiter = new Tone.Limiter(-1).toDestination();
23
+ const masterGain = new Tone.Gain(0.82).connect(limiter);
24
+
25
+ // --- REVERB connected to master ---
26
+ const reverb = new Tone.Reverb({ decay: 2.5, wet: 0.30 }).connect(masterGain);
27
+
28
+ // --- DELAY connected to master (parallel with reverb, not chained) ---
29
+ const delay = new Tone.FeedbackDelay(beat * 0.5, 0.25).connect(masterGain);
30
+
31
+ // --- KICK (MembraneSynth) ---
32
+ const kickGain = new Tone.Gain(0.88).connect(masterGain);
33
+ const kick = new Tone.MembraneSynth({
34
+ pitchDecay: 0.06,
35
+ octaves: 9,
36
+ envelope: { attack: 0.001, decay: 0.34, sustain: 0, release: 0.1 }
37
+ }).connect(kickGain);
38
+
39
+ // --- HI-HAT (MetalSynth) ---
40
+ // MetalSynth.triggerAttackRelease(duration, time, velocity) -- no pitch arg
41
+ const hatGain = new Tone.Gain(0.36).connect(masterGain);
42
+ const hat = new Tone.MetalSynth({
43
+ frequency: 440,
44
+ envelope: { attack: 0.001, decay: 0.05, release: 0.01 },
45
+ harmonicity: 5.1,
46
+ modulationIndex: 32,
47
+ resonance: 4000,
48
+ octaves: 1.5
49
+ }).connect(hatGain);
50
+
51
+ // --- CHORDS (PolySynth) through reverb ---
52
+ const padGain = new Tone.Gain(0.60).connect(reverb);
53
+ const pad = new Tone.PolySynth(Tone.Synth, {
54
+ oscillator: { type: "sine" },
55
+ envelope: { attack: 0.40, decay: 0.5, sustain: 0.60, release: 1.8 },
56
+ volume: -8
57
+ }).connect(padGain);
58
+
59
+ // --- MELODY (Synth) through delay + reverb ---
60
+ const melGain = new Tone.Gain(0.72).connect(delay);
61
+ melGain.connect(reverb);
62
+ const mel = new Tone.Synth({
63
+ oscillator: { type: "triangle" },
64
+ envelope: { attack: 0.012, decay: 0.20, sustain: 0.45, release: 0.65 },
65
+ volume: -5
66
+ }).connect(melGain);
67
+
68
+ // --- KICK: 4-on-the-floor, every 0.5s, 20 hits, last at 9.5s ---
69
+ for (let i = 0; i < 20; i++) {
70
+ kick.triggerAttackRelease("C1", "8n", i * beat);
71
+ }
72
+
73
+ // --- HI-HAT: 8th notes every 0.25s, on-beat louder, 40 hits, last at 9.75s ---
74
+ for (let i = 0; i < 40; i++) {
75
+ const vel = (i % 2 === 0) ? 0.60 : 0.35;
76
+ hat.triggerAttackRelease("16n", i * (beat * 0.5), vel);
77
+ }
78
+
79
+ // --- CHORDS: Dm -> Bb -> F -> C, one bar each, enter at bar 2 (2s) ---
80
+ // "1n" = whole note = 2.0s at 120 BPM = exactly one bar
81
+ const progression = [
82
+ { notes: ["D2","F2","A2"], time: bar }, // Dm @ 2s
83
+ { notes: ["Bb2","D3","F3"], time: bar * 2 }, // Bb @ 4s
84
+ { notes: ["F2","A2","C3"], time: bar * 3 }, // F @ 6s
85
+ { notes: ["C3","E3","G3"], time: bar * 4 }, // C @ 8s
86
+ ];
87
+ progression.forEach(({ notes, time }) => {
88
+ pad.triggerAttackRelease(notes, "1n", time);
89
+ });
90
+
91
+ // --- MELODY: D minor pentatonic, enters at bar 3 (4s), last note at 9.0s ---
92
+ const melody = [
93
+ { note: "F4", dur: "4n", t: 4.0 },
94
+ { note: "E4", dur: "8n", t: 4.5 },
95
+ { note: "D4", dur: "4n", t: 5.0 },
96
+ { note: "A4", dur: "4n", t: 5.5 },
97
+ { note: "G4", dur: "2n", t: 6.0 },
98
+ { note: "F4", dur: "4n", t: 7.0 },
99
+ { note: "E4", dur: "8n", t: 7.5 },
100
+ { note: "D4", dur: "4n", t: 8.0 },
101
+ { note: "F4", dur: "8n", t: 8.5 },
102
+ { note: "A4", dur: "4n", t: 9.0 },
103
+ ];
104
+ melody.forEach(({ note, dur, t }) => {
105
+ mel.triggerAttackRelease(note, dur, t);
106
+ });
107
+
108
+ // NOTE: do NOT call Tone.Transport.start() here.
109
+ // Tone.Offline manages the transport internally.
110
+ }
111
+
112
+ main();
113
+ </script>
114
+ </body>
115
+ </html>
@@ -0,0 +1,221 @@
1
+ ---
2
+ name: tuneframes
3
+ description: Write music compositions in HTML using Tone.js. Use when asked to create music, audio, sound effects, beats, or any audio generation task.
4
+ ---
5
+
6
+ # TuneFrames
7
+
8
+ Open-source music generation: write HTML with Tone.js, render to MP3 with one CLI command. Built for AI agents — no native audio code, no per-render fees, fully deterministic.
9
+
10
+ ## When to Use This Skill
11
+
12
+ - User asks for music, a beat, soundtrack, sound effects, or audio
13
+ - User describes a musical mood (lofi, ambient, techno, orchestral) and wants output
14
+ - User wants audio for a video, game, or application
15
+ - User wants to generate music programmatically in an automated pipeline
16
+
17
+ ## Core Authoring Pattern
18
+
19
+ Every TuneFrames composition is a single HTML file:
20
+
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":"4s"}</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
+ }
35
+ </script>
36
+ </body>
37
+ </html>
38
+ ```
39
+
40
+ ## Metadata Block
41
+
42
+ ```html
43
+ <div id="tuneframes" style="display:none">{"bpm":120,"duration":"4s"}</div>
44
+ ```
45
+
46
+ - **bpm**: beats per minute (default: 120)
47
+ - **duration**: render length in **seconds** — use `"4s"`, `"10s"`, etc. Do NOT use Tone.js note values like `"4n"` for duration (see warning below)
48
+
49
+ ## The `main()` Function Rules
50
+
51
+ 1. Must be `async function main()`
52
+ 2. Must start with `await Tone.start()`
53
+ 3. All notes are scheduled relative to transport time 0
54
+ 4. Tone.Offline renders deterministically — same HTML = identical MP3 every time
55
+
56
+ ## Instruments
57
+
58
+ ```js
59
+ // Basic tone generation
60
+ const synth = new Tone.Synth().toDestination();
61
+ const mono = new Tone.MonoSynth().toDestination();
62
+ const poly = new Tone.PolySynth(Tone.Synth).toDestination();
63
+
64
+ // Drums
65
+ const kick = new Tone.MembraneSynth().toDestination(); // Kick drum
66
+ const noise = new Tone.NoiseSynth().toDestination(); // Hi-hats, snares
67
+
68
+ // Effects
69
+ const reverb = new Tone.Reverb({ decay: 2.5, wet: 0.3 }).toDestination();
70
+ const delay = new Tone.FeedbackDelay('8n', 0.4).toDestination();
71
+ const comp = new Tone.Compressor(-12, 2).toDestination();
72
+ const chorus = new Tone.Chorus(4, 2.5, 0.5).toDestination();
73
+ ```
74
+
75
+ ## Common Patterns
76
+
77
+ **Note timing:**
78
+ ```js
79
+ const beat = Tone.Time('4n').toSeconds();
80
+ synth.triggerAttackRelease('C4', '4n', 0); // Beat 1
81
+ synth.triggerAttackRelease('E4', '4n', beat); // Beat 2
82
+ synth.triggerAttackRelease('G4', '4n', beat * 2); // Beat 3
83
+ synth.triggerAttackRelease('C5', '4n', beat * 3); // Beat 4
84
+ ```
85
+
86
+ **Chord progression:**
87
+ ```js
88
+ const pad = new Tone.PolySynth(Tone.Synth).toDestination();
89
+ const chords = [
90
+ ['D3','F3','A3'], // Dm
91
+ ['C3','E3','G3'], // C
92
+ ['Bb2','D3','F3'], // Bb
93
+ ['A2','C3','E3'], // Am
94
+ ];
95
+ const measure = Tone.Time('1n').toSeconds();
96
+ chords.forEach((chord, i) => {
97
+ pad.triggerAttackRelease(chord, '1n', i * measure);
98
+ });
99
+ ```
100
+
101
+ **Drum pattern:**
102
+ ```js
103
+ const kick = new Tone.MembraneSynth().toDestination();
104
+ const snare = new Tone.NoiseSynth().toDestination();
105
+ const beat = Tone.Time('4n').toSeconds();
106
+
107
+ for (let i = 0; i < 4; i++) {
108
+ kick.triggerAttackRelease('C1', '4n', i * beat); // Kick on 1-4
109
+ snare.triggerAttackRelease('C3', '4n', (i + 0.5) * beat); // Snare on 2,4
110
+ }
111
+ ```
112
+
113
+ **Lofi beat (full example):**
114
+ ```js
115
+ const kick = new Tone.MembraneSynth().toDestination();
116
+ const snare = new Tone.NoiseSynth().toDestination();
117
+ const hihat = new Tone.NoiseSynth().toDestination();
118
+ const piano = new Tone.PolySynth(Tone.Synth).toDestination();
119
+ const reverb = new Tone.Reverb({ decay: 1.5, wet: 0.4 }).toDestination();
120
+
121
+ const beat = Tone.Time('4n').toSeconds();
122
+ const bar = beat * 4;
123
+
124
+ // Kick: four on the floor
125
+ for (let i = 0; i < 4; i++) kick.triggerAttackRelease('C1', '4n', i * beat);
126
+
127
+ // Snare: 2 and 4
128
+ [1, 3].forEach(i => snare.triggerAttackRelease('C3', '4n', i * beat));
129
+
130
+ // Hi-hat: eighth notes, quiet
131
+ hihat.volume.value = -12;
132
+ for (let i = 0; i < 8; i++) hihat.triggerAttackRelease('16n', i * beat * 0.5);
133
+
134
+ // Piano: chord progression
135
+ const progression = [['D3','F3','A3'],['C3','E3','G3']];
136
+ progression.forEach((chord, i) => {
137
+ piano.connect(reverb);
138
+ piano.triggerAttackRelease(chord, '2n', i * bar);
139
+ });
140
+ ```
141
+
142
+ ## ⚠️ Duration Warning
143
+
144
+ **Use literal seconds (`"4s"`, `"10s"`) for the metadata duration.**
145
+
146
+ Tone.js note notation (`4n`, `2n`, `1n`) is a fraction of a whole note at the given BPM:
147
+ - `1n` = 1 whole note = 4 beats → at 120 BPM, 1n = 2.0s
148
+ - `2n` = 1/2 whole note = 2 beats → at 120 BPM, 2n = 1.0s
149
+ - `4n` = 1/4 whole note = 1 beat → at 120 BPM, 4n = 0.5s
150
+ - `8n` = 1/8 whole note = 1/2 beat → at 120 BPM, 8n = 0.25s
151
+
152
+ This is a common source of clipped renders. Always use seconds in the metadata block.
153
+
154
+ ## CLI Reference
155
+
156
+ ```bash
157
+ tuneframes render <file.html> [--output <out.mp3>] # Render to MP3
158
+ tuneframes render <file.html> --format wav [--output <out.wav>]
159
+ tuneframes preview <file.html> # Live preview in browser
160
+ tuneframes init <name> # Scaffold new project
161
+ ```
162
+
163
+ ---
164
+
165
+ # TuneFrames — Sample Instruments
166
+
167
+ Extends the core TuneFrames skill with real instrument samples via CDN.
168
+
169
+ ## Sample Pattern (Tone.Sampler + gleitz CDN)
170
+
171
+ Every sample composition must:
172
+ 1. Create Tone.Sampler instances with CDN baseUrl
173
+ 2. Call `await Tone.loaded()` before scheduling any notes
174
+ 3. Then schedule all notes using the loaded sampler
175
+
176
+ ```js
177
+ async function main() {
178
+ await Tone.start();
179
+
180
+ // Load piano from CDN
181
+ const piano = new Tone.Sampler({
182
+ urls: { A4: 'A4.mp3', C4: 'C4.mp3', 'F#4': 'Fs4.mp3', A5: 'A5.mp3' },
183
+ baseUrl: 'https://gleitz.github.io/midi-js-soundfonts/FluidR3_GM/acoustic_grand_piano-mp3/'
184
+ }).toDestination();
185
+
186
+ // CRITICAL: wait for samples to load before scheduling
187
+ await Tone.loaded();
188
+
189
+ // Now schedule notes
190
+ piano.triggerAttackRelease('C4', '4n', 0);
191
+ piano.triggerAttackRelease('E4', '4n', '4n');
192
+ piano.triggerAttackRelease('G4', '2n', '2n');
193
+ }
194
+ ```
195
+
196
+ ## Available Instruments (gleitz FluidR3_GM)
197
+
198
+ Full registry: `registry/samples.json` — includes all URL mappings and per-instrument notes.
199
+
200
+ | Key | CDN path suffix | Category | Best range |
201
+ |-----|----------------|----------|------------|
202
+ | `acoustic_grand_piano` | `acoustic_grand_piano-mp3` | Piano | A0–C8 |
203
+ | `acoustic_bass` | `acoustic_bass-mp3` | Bass | C1–C4 |
204
+ | `string_ensemble_1` | `string_ensemble_1-mp3` | Strings | C2–C7 |
205
+ | `brass_section` | `brass_section-mp3` | Brass | C2–C6 |
206
+ | `acoustic_guitar_nylon` | `acoustic_guitar_nylon-mp3` | Guitar | Fs2–C6 |
207
+ | `vibraphone` | `vibraphone-mp3` | Mallet | C3–C7 |
208
+ | `flute` | `flute-mp3` | Woodwind | C4–A7 |
209
+ | `choir_aahs` | `choir_aahs-mp3` | Choir | C3–G5 |
210
+
211
+ CDN base: `https://gleitz.github.io/midi-js-soundfonts/FluidR3_GM/{instrument}-mp3/`
212
+
213
+ Sharp notes use `s` suffix in filenames: F#4 → `Fs4.mp3`, D#3 → `Ds3.mp3`.
214
+
215
+ ## Presets
216
+
217
+ Ready-to-render compositions in `registry/presets/`:
218
+
219
+ - `piano-salamander.html` — Am–F–C–G chord progression, acoustic grand piano, 8s
220
+ - `drums-808.html` — Trap 808 pattern, MembraneSynth + MetalSynth (no CDN), 7s
221
+ - `bass-electric.html` — Walking bass line in C minor, acoustic bass, 6s
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: tuneframes-cli
3
+ description: CLI commands for TuneFrames — render, preview, init, validate. Load this skill when working with the tuneframes CLI.
4
+ ---
5
+
6
+ # TuneFrames CLI
7
+
8
+ ## Commands
9
+
10
+ ```bash
11
+ # Render composition to MP3
12
+ tuneframes render <file.html> [--output <out.mp3>]
13
+
14
+ # Render to WAV (no re-encoding)
15
+ tuneframes render <file.html> --format wav [--output <out.wav>]
16
+
17
+ # Preview in browser (live reload)
18
+ tuneframes preview <file.html>
19
+
20
+ # Scaffold a new project
21
+ tuneframes init my-track
22
+
23
+ # Validate a composition (headless render test)
24
+ tuneframes validate <file.html>
25
+ ```
26
+
27
+ ## Rendering Pipeline
28
+
29
+ ```
30
+ HTML + Tone.js → Chromium (headless, Tone.Offline)
31
+ → WAV (PCM 44.1kHz mono)
32
+ → FFmpeg
33
+ → MP3 192kbps
34
+ ```
35
+
36
+ Tone.Offline renders without audio hardware. Same input HTML = identical output MP3. This is what makes TuneFrames safe for automated pipelines.
37
+
38
+ ## Requirements
39
+
40
+ - Node.js >= 18
41
+ - FFmpeg (install via: `apt install ffmpeg` or `brew install ffmpeg`)
42
+
43
+ ## Output Locations
44
+
45
+ Default: input file's directory with same basename, `.mp3` extension.
46
+ `--output` flag overrides: `tuneframes render in.html --output /tmp/out.mp3`
@@ -0,0 +1,104 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>TuneFrames — Verify</title>
6
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
7
+ </head>
8
+ <body>
9
+ <div id="tuneframes" style="display:none">{"bpm":120,"duration":"10s"}</div>
10
+ <script>
11
+ // Verify — upbeat electronic, C major, I-vi-IV-V-I arc. "System checked out: PASS."
12
+ //
13
+ // Instrument choices follow confirmed-working pattern (techno/lofi examples):
14
+ // - Tone.Reverb without await — generate() resolves before startRendering() inside Offline
15
+ // - Tone.MembraneSynth kick — confirmed working in example-techno.html
16
+ // - Tone.NoiseSynth hi-hat — confirmed working; MetalSynth fails inside Tone.Offline
17
+ // (MetalSynth's FM oscillators use sustain:0 early-stop scheduling which creates
18
+ // a state-timeline ordering conflict on re-trigger in the Offline context)
19
+ // - main() NOT called from HTML — render.js calls it inside Tone.Offline()
20
+
21
+ async function main() {
22
+ await Tone.start();
23
+
24
+ const beat = Tone.Time('4n').toSeconds(); // 0.5s at 120 BPM
25
+ const bar = beat * 4; // 2.0s
26
+
27
+ // Effects
28
+ const reverb = new Tone.Reverb({ decay: 2.2, wet: 0.28 }).toDestination();
29
+ const delay = new Tone.FeedbackDelay({ delayTime: beat, feedback: 0.28, wet: 0.22 }).toDestination();
30
+
31
+ // Chord pad — PolySynth(Synth) → reverb
32
+ const pad = new Tone.PolySynth(Tone.Synth, {
33
+ oscillator: { type: 'triangle' },
34
+ envelope: { attack: 0.08, decay: 0.40, sustain: 0.60, release: 1.20 }
35
+ }).connect(reverb);
36
+ pad.volume.value = -10;
37
+
38
+ // Melody — Synth → delay
39
+ const mel = new Tone.Synth({
40
+ oscillator: { type: 'sawtooth' },
41
+ envelope: { attack: 0.01, decay: 0.15, sustain: 0.40, release: 0.30 }
42
+ }).connect(delay);
43
+ mel.volume.value = -9;
44
+
45
+ // Kick — MembraneSynth
46
+ const kick = new Tone.MembraneSynth({
47
+ pitchDecay: 0.05,
48
+ octaves: 6,
49
+ oscillator: { type: 'sine' },
50
+ envelope: { attack: 0.001, decay: 0.28, sustain: 0, release: 0.08 }
51
+ }).toDestination();
52
+ kick.volume.value = -5;
53
+
54
+ // Hi-hat — NoiseSynth (white noise burst, similar timbre to metallic hat)
55
+ // NoiseSynth works inside Tone.Offline; MetalSynth does not in v14.8.49
56
+ const hihat = new Tone.NoiseSynth({
57
+ noise: { type: 'white' },
58
+ envelope: { attack: 0.001, decay: 0.05, sustain: 0, release: 0.01 }
59
+ }).toDestination();
60
+ hihat.volume.value = -16;
61
+
62
+ // Kick: 4-on-the-floor, 20 hits across 10s
63
+ for (var i = 0; i < 20; i++) {
64
+ kick.triggerAttackRelease('C1', '8n', i * beat);
65
+ }
66
+
67
+ // Hi-hat: off-beat 8th notes, 19 hits
68
+ for (var j = 0; j < 19; j++) {
69
+ var vel = j % 2 === 0 ? 0.60 : 0.32;
70
+ hihat.triggerAttackRelease('16n', beat * 0.5 + j * beat, vel);
71
+ }
72
+
73
+ // Chords: I-vi-IV-V-I (C-Am-F-G-C), one per bar
74
+ [
75
+ { notes: ['C4', 'E4', 'G4'], t: bar * 0 },
76
+ { notes: ['A3', 'C4', 'E4'], t: bar * 1 },
77
+ { notes: ['F3', 'A3', 'C4'], t: bar * 2 },
78
+ { notes: ['G3', 'B3', 'D4'], t: bar * 3 },
79
+ { notes: ['C4', 'E4', 'G4'], t: bar * 4 },
80
+ ].forEach(function(c) {
81
+ pad.triggerAttackRelease(c.notes, '1n', c.t);
82
+ });
83
+
84
+ // Melody: question phrase (bars 2-3), answer resolves to tonic (bars 4-5)
85
+ var e = beat * 0.5; // eighth-note duration
86
+ [
87
+ [bar + beat * 0, 'E5', e],
88
+ [bar + beat * 1, 'G5', e],
89
+ [bar + beat * 2, 'A5', beat],
90
+ [bar * 2 + beat * 0, 'G5', e],
91
+ [bar * 2 + beat * 1, 'F5', e],
92
+ [bar * 2 + beat * 2, 'E5', beat],
93
+ [bar * 3 + beat * 0, 'D5', e],
94
+ [bar * 3 + beat * 1, 'E5', e],
95
+ [bar * 3 + beat * 2, 'F5', e],
96
+ [bar * 3 + beat * 3, 'G5', e],
97
+ [bar * 4, 'C5', beat * 3],
98
+ ].forEach(function(n) {
99
+ mel.triggerAttackRelease(n[1], n[2], n[0]);
100
+ });
101
+ }
102
+ </script>
103
+ </body>
104
+ </html>