tuneframes 0.1.1 → 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 +166 -166
- package/README.md +158 -55
- 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 +47 -47
- package/examples/example-lofi.html +45 -45
- package/examples/example-lofi.mp3 +0 -0
- package/examples/example-minimal.html +20 -20
- package/examples/example-orchestral.html +67 -67
- package/examples/example-orchestral.mp3 +0 -0
- package/examples/example-piano.html +52 -52
- 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 -37
- 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 +260 -151
- package/src/render.js +134 -7
- package/examples/example-bass.mp3 +0 -0
- package/examples/example-demo-beat.wav +0 -0
- package/examples/example-orchestral.wav +0 -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>
|
|
@@ -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 = 'energetic'; // 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 = 'happy'; // 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>
|