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
package/src/cli.js
CHANGED
|
@@ -1,152 +1,261 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* TuneFrames CLI
|
|
4
|
-
*
|
|
5
|
-
* Commands:
|
|
6
|
-
* tuneframes render <file.html> [--output track.mp3]
|
|
7
|
-
* tuneframes init <project-name>
|
|
8
|
-
* tuneframes preview <file.html>
|
|
9
|
-
* tuneframes validate <file.html>
|
|
10
|
-
* tuneframes add <preset-name>
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const { render } = require('./render');
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
const path = require('path');
|
|
16
|
-
const { spawn } = require('child_process');
|
|
17
|
-
|
|
18
|
-
const PRESETS = ['drums-lofi', 'reverb-warm', 'chord-progression', 'bass-saw', 'lead-piano'];
|
|
19
|
-
|
|
20
|
-
async function main() {
|
|
21
|
-
const [,, cmd, ...args] = process.argv;
|
|
22
|
-
|
|
23
|
-
switch (cmd) {
|
|
24
|
-
case 'render': {
|
|
25
|
-
const inputFile = args[0];
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
console.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
if (!
|
|
127
|
-
console.error('
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* TuneFrames CLI
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* tuneframes render <file.html> [--output track.mp3]
|
|
7
|
+
* tuneframes init <project-name>
|
|
8
|
+
* tuneframes preview <file.html>
|
|
9
|
+
* tuneframes validate <file.html>
|
|
10
|
+
* tuneframes add <preset-name>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { render } = require('./render');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { spawn } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const PRESETS = ['drums-lofi', 'reverb-warm', 'chord-progression', 'bass-saw', 'lead-piano'];
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const [,, cmd, ...args] = process.argv;
|
|
22
|
+
|
|
23
|
+
switch (cmd) {
|
|
24
|
+
case 'render': {
|
|
25
|
+
const inputFile = args[0];
|
|
26
|
+
const equalsArg = args.find(a => a.startsWith('--output='));
|
|
27
|
+
const separateIdx = args.indexOf('--output');
|
|
28
|
+
const formatIdx = args.indexOf('--format');
|
|
29
|
+
const outputFile = equalsArg
|
|
30
|
+
? equalsArg.split('=')[1]
|
|
31
|
+
: separateIdx !== -1 && args[separateIdx + 1] && !args[separateIdx + 1].startsWith('--')
|
|
32
|
+
? args[separateIdx + 1]
|
|
33
|
+
: inputFile.replace(/\.html$/, '.mp3');
|
|
34
|
+
const outputFormat = formatIdx !== -1 && args[formatIdx + 1] ? args[formatIdx + 1] : 'mp3';
|
|
35
|
+
|
|
36
|
+
const timeoutEqArg = args.find(a => a.startsWith('--timeout='));
|
|
37
|
+
const timeoutIdx = args.indexOf('--timeout');
|
|
38
|
+
const timeoutSec = timeoutEqArg
|
|
39
|
+
? parseInt(timeoutEqArg.split('=')[1], 10)
|
|
40
|
+
: timeoutIdx !== -1 && args[timeoutIdx + 1] && !args[timeoutIdx + 1].startsWith('--')
|
|
41
|
+
? parseInt(args[timeoutIdx + 1], 10)
|
|
42
|
+
: 60;
|
|
43
|
+
const timeoutMs = timeoutSec * 1000;
|
|
44
|
+
|
|
45
|
+
if (!inputFile) {
|
|
46
|
+
console.error('Usage: tuneframes render <file.html> [--output=track.mp3] [--format wav] [--timeout 60]');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(inputFile)) {
|
|
51
|
+
console.error(`File not found: ${inputFile}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(`Rendering ${inputFile} → ${outputFile}`);
|
|
56
|
+
const result = await render(inputFile, outputFile, outputFormat, timeoutMs);
|
|
57
|
+
console.log(`Done. Output: ${result.output}`);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case 'init': {
|
|
62
|
+
const [projectName] = args;
|
|
63
|
+
if (!projectName) {
|
|
64
|
+
console.error('Usage: tuneframes init <project-name>');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const dir = path.join(process.cwd(), projectName);
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
const example = fs.readFileSync(
|
|
72
|
+
path.join(__dirname, '../examples/example-lofi.html'), 'utf8'
|
|
73
|
+
);
|
|
74
|
+
fs.writeFileSync(path.join(dir, 'composition.html'), example);
|
|
75
|
+
fs.writeFileSync(path.join(dir, 'README.md'),
|
|
76
|
+
`# ${projectName}\n\nRun: tuneframes render composition.html\n`);
|
|
77
|
+
|
|
78
|
+
console.log(`Initialized ${projectName}/`);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case 'preview': {
|
|
83
|
+
const inputFile = args[0];
|
|
84
|
+
if (!inputFile) {
|
|
85
|
+
console.error('Usage: tuneframes preview <file.html>');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const url = `file://${path.resolve(inputFile)}`;
|
|
89
|
+
console.log(`Preview: ${url}`);
|
|
90
|
+
if (process.platform === 'darwin') {
|
|
91
|
+
spawn('open', [url]);
|
|
92
|
+
} else if (process.platform === 'linux') {
|
|
93
|
+
spawn('xdg-open', [url]);
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case 'validate': {
|
|
99
|
+
const inputFile = args[0];
|
|
100
|
+
if (!inputFile) {
|
|
101
|
+
console.error('Usage: tuneframes validate <file.html>');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
if (!fs.existsSync(inputFile)) {
|
|
105
|
+
console.error(`File not found: ${inputFile}`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const tmpOut = `/tmp/tuneframes-validate-${Date.now()}.mp3`;
|
|
110
|
+
try {
|
|
111
|
+
await render(inputFile, tmpOut);
|
|
112
|
+
const stats = fs.statSync(tmpOut);
|
|
113
|
+
if (stats.size < 5000) {
|
|
114
|
+
console.error(`FAIL: ${inputFile} rendered to ${stats.size} bytes (< 5000)`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
console.log(`OK: ${inputFile} — ${stats.size} bytes`);
|
|
118
|
+
} finally {
|
|
119
|
+
if (fs.existsSync(tmpOut)) fs.unlinkSync(tmpOut);
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case 'lint': {
|
|
125
|
+
const inputFile = args[0];
|
|
126
|
+
if (!inputFile) {
|
|
127
|
+
console.error('Usage: tuneframes lint <file.html>');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
if (!fs.existsSync(inputFile)) {
|
|
131
|
+
console.error(`File not found: ${inputFile}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const content = fs.readFileSync(inputFile, 'utf8');
|
|
136
|
+
const errors = [];
|
|
137
|
+
|
|
138
|
+
// Check 1: must have <div id="tuneframes">
|
|
139
|
+
if (!content.includes('id="tuneframes"')) {
|
|
140
|
+
errors.push('Missing <div id="tuneframes"> metadata block');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check 2: metadata must be valid JSON
|
|
144
|
+
const metaMatch = content.match(/id="tuneframes"[^>]*>([^<]+)<\/div>/);
|
|
145
|
+
if (metaMatch) {
|
|
146
|
+
try {
|
|
147
|
+
JSON.parse(metaMatch[1].trim());
|
|
148
|
+
} catch (e) {
|
|
149
|
+
errors.push(`Invalid JSON in metadata block: ${e.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check 3: must load Tone.js
|
|
154
|
+
if (!content.includes('Tone.js') && !content.includes('tone')) {
|
|
155
|
+
errors.push('No Tone.js script reference found');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check 4: must have async function main()
|
|
159
|
+
if (!content.includes('async function main()')) {
|
|
160
|
+
errors.push('Missing async function main()');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check 5: main() should contain await Tone.start()
|
|
164
|
+
if (content.includes('async function main()') && !content.includes('Tone.start()')) {
|
|
165
|
+
errors.push('main() should call await Tone.start()');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (errors.length === 0) {
|
|
169
|
+
console.log(`OK: ${inputFile}`);
|
|
170
|
+
} else {
|
|
171
|
+
errors.forEach(e => console.error(`FAIL: ${e}`));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'add': {
|
|
178
|
+
const [presetName] = args;
|
|
179
|
+
if (!presetName) {
|
|
180
|
+
console.error(`Usage: tuneframes add <preset>\nAvailable: ${PRESETS.join(', ')}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
if (!PRESETS.includes(presetName)) {
|
|
184
|
+
console.error(`Unknown preset: "${presetName}"\nAvailable presets: ${PRESETS.join(', ')}\n\nFor CDN soundfont instruments (Tone.Sampler), run: tuneframes instruments`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const presetPath = path.join(__dirname, `../registry/presets/${presetName}.html`);
|
|
189
|
+
const content = fs.readFileSync(presetPath, 'utf8');
|
|
190
|
+
|
|
191
|
+
// Append to composition.html in current directory
|
|
192
|
+
const compPath = path.join(process.cwd(), 'composition.html');
|
|
193
|
+
if (!fs.existsSync(compPath)) {
|
|
194
|
+
console.error('No composition.html found. Run tuneframes init first.');
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const existing = fs.readFileSync(compPath, 'utf8');
|
|
199
|
+
const insertMarker = '</script>';
|
|
200
|
+
if (existing.includes(insertMarker)) {
|
|
201
|
+
const newContent = existing.replace(insertMarker, content + '\n' + insertMarker);
|
|
202
|
+
fs.writeFileSync(compPath, newContent);
|
|
203
|
+
} else {
|
|
204
|
+
fs.appendFileSync(compPath, '\n' + content);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log(`Added ${presetName} to composition.html`);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case 'instruments': {
|
|
212
|
+
const GLEITZ_BASE = 'https://gleitz.github.io/midi-js-soundfonts/MusyngKite/{instrument}-mp3/{note}.mp3';
|
|
213
|
+
const DRUMS_CDN = 'https://dave4mpls.github.io/midi-js-soundfonts-with-drums/FluidR3_GM/drums-mp3/{note}.mp3';
|
|
214
|
+
|
|
215
|
+
console.log('gleitz CDN instruments (MusyngKite, high quality)');
|
|
216
|
+
console.log(`URL pattern: ${GLEITZ_BASE}\n`);
|
|
217
|
+
|
|
218
|
+
const categories = {
|
|
219
|
+
'Piano': ['acoustic_grand_piano','bright_acoustic_piano','electric_grand_piano','honkytonk_piano','electric_piano_1','electric_piano_2','harpsichord','clavinet'],
|
|
220
|
+
'Chromatic Perc': ['celesta','glockenspiel','music_box','vibraphone','marimba','xylophone','tubular_bells','dulcimer'],
|
|
221
|
+
'Organ': ['drawbar_organ','percussive_organ','rock_organ','church_organ','reed_organ','accordion','harmonica','tango_accordion'],
|
|
222
|
+
'Guitar': ['acoustic_guitar_nylon','acoustic_guitar_steel','electric_guitar_jazz','electric_guitar_clean','electric_guitar_muted','overdriven_guitar','distortion_guitar','guitar_harmonics'],
|
|
223
|
+
'Bass': ['acoustic_bass','electric_bass_finger','electric_bass_pick','fretless_bass','slap_bass_1','slap_bass_2','synth_bass_1','synth_bass_2'],
|
|
224
|
+
'Strings': ['violin','viola','cello','contrabass','tremolo_strings','pizzicato_strings','orchestral_harp','timpani'],
|
|
225
|
+
'Ensemble': ['string_ensemble_1','string_ensemble_2','synth_strings_1','synth_strings_2','choir_aahs','voice_oohs','synth_choir','orchestra_hit'],
|
|
226
|
+
'Brass': ['trumpet','trombone','tuba','muted_trumpet','french_horn','brass_section','synth_brass_1','synth_brass_2'],
|
|
227
|
+
'Reed': ['soprano_sax','alto_sax','tenor_sax','baritone_sax','oboe','english_horn','bassoon','clarinet'],
|
|
228
|
+
'Pipe': ['piccolo','flute','recorder','pan_flute','blown_bottle','shakuhachi','whistle','ocarina'],
|
|
229
|
+
'Synth Lead': ['lead_1_square','lead_2_sawtooth','lead_3_calliope','lead_4_chiff','lead_5_charang','lead_6_voice','lead_7_fifths','lead_8_bass_and_lead'],
|
|
230
|
+
'Synth Pad': ['pad_1_new_age','pad_2_warm','pad_3_polysynth','pad_4_choir','pad_5_bowed','pad_6_metallic','pad_7_halo','pad_8_sweep'],
|
|
231
|
+
'Synth FX': ['fx_1_rain','fx_2_soundtrack','fx_3_crystal','fx_4_atmosphere','fx_5_brightness','fx_6_goblins','fx_7_echoes','fx_8_scifi'],
|
|
232
|
+
'Ethnic': ['sitar','banjo','shamisen','koto','kalimba','bagpipe','fiddle','shanai'],
|
|
233
|
+
'Percussive': ['tinkle_bell','agogo','steel_drums','woodblock','taiko_drum','melodic_tom','synth_drum','reverse_cymbal'],
|
|
234
|
+
'Sound FX': ['guitar_fret_noise','breath_noise','seashore','bird_tweet','telephone_ring','helicopter','applause','gunshot'],
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
for (const [cat, list] of Object.entries(categories)) {
|
|
238
|
+
console.log(`${cat}:`);
|
|
239
|
+
console.log(' ' + list.join(' '));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log('\nDrums (dave4mpls fork — standard gleitz does not include GM drums):');
|
|
243
|
+
console.log(` URL: ${DRUMS_CDN}`);
|
|
244
|
+
console.log(' Trigger notes: C2=Kick D2=Snare Gb2=Hi-Hat(closed) Bb2=Hi-Hat(open) Db3=Crash Eb3=Ride');
|
|
245
|
+
|
|
246
|
+
console.log('\nNotes:');
|
|
247
|
+
console.log(' - All 88 notes present per instrument (A0–C8), flat notation only (Ab4.mp3, not G#4.mp3)');
|
|
248
|
+
console.log(' - Tone.Sampler accepts both Ab4 and G#4 as url keys — either works');
|
|
249
|
+
console.log(' - Set window.TUNEFRAMES_READY = Tone.loaded() so the renderer waits for samples');
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
default:
|
|
254
|
+
console.log(`TuneFrames CLI\n\nCommands:\n tuneframes render <file.html> Render composition to MP3\n tuneframes init <name> Initialize new project\n tuneframes preview <file.html> Open in browser\n tuneframes validate <file.html> Validate composition (headless test render)\n tuneframes lint <file.html> Lint composition (static HTML analysis)\n tuneframes add <preset> Add preset to composition.html\n tuneframes instruments List gleitz CDN instruments for Tone.Sampler\n\nPresets: ${PRESETS.join(', ')}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
main().catch(err => {
|
|
259
|
+
console.error('Error:', err.message);
|
|
260
|
+
process.exit(1);
|
|
152
261
|
});
|
package/src/render.js
CHANGED
|
@@ -37,6 +37,60 @@ const HELPER_SCRIPT = `
|
|
|
37
37
|
|
|
38
38
|
window.audioBufferToWav = audioBufferToWav;
|
|
39
39
|
|
|
40
|
+
// Tone.js CDN bundle defines exports as non-writable, non-configurable getter
|
|
41
|
+
// properties (rollup live-binding pattern). Direct assignment and Object.defineProperty
|
|
42
|
+
// both fail to override them. Proxy on window.Tone is the only reliable intercept.
|
|
43
|
+
//
|
|
44
|
+
// AudioWorklet effects (Freeverb, BitCrusher, Chebyshev) fail in headless offline
|
|
45
|
+
// rendering because worklet modules cannot load. The Proxy returns unity-gain
|
|
46
|
+
// passthroughs for those classes when inside a Tone.Offline() call (_inOff flag).
|
|
47
|
+
//
|
|
48
|
+
// Reverb tracking is also done in the Proxy get trap (rather than Tone.Reverb = ...)
|
|
49
|
+
// because Tone.Reverb is a non-configurable no-setter accessor — setting it would
|
|
50
|
+
// violate Proxy invariants. Instances pushed to window._tfRev; awaited after main().
|
|
51
|
+
window._patchToneWorklets = function() {
|
|
52
|
+
if (typeof Tone === 'undefined' || window._tonePatched) return;
|
|
53
|
+
window._tonePatched = true;
|
|
54
|
+
|
|
55
|
+
const _orig = window.Tone;
|
|
56
|
+
const _origOffline = _orig.Offline;
|
|
57
|
+
|
|
58
|
+
const _mkPass = () => class extends _orig.Gain {
|
|
59
|
+
constructor() { super(1); }
|
|
60
|
+
// Reverb-compat stubs: some skills call await reverb.generate() or await reverb.ready.
|
|
61
|
+
generate() { return Promise.resolve(this); }
|
|
62
|
+
get ready() { return Promise.resolve(this); }
|
|
63
|
+
};
|
|
64
|
+
// Reverb included: its IR generation calls the module-internal Offline() which
|
|
65
|
+
// creates a nested offline context and corrupts the outer timeline. Passthrough
|
|
66
|
+
// eliminates the nested context entirely. Audio flows through; dry-signal-only
|
|
67
|
+
// is acceptable for a headless render test.
|
|
68
|
+
const _fx = { Freeverb: _mkPass(), BitCrusher: _mkPass(), Chebyshev: _mkPass(), Reverb: _mkPass() };
|
|
69
|
+
|
|
70
|
+
let _inOff = false;
|
|
71
|
+
|
|
72
|
+
const _wOffline = async function(cb, ...args) {
|
|
73
|
+
_inOff = true;
|
|
74
|
+
try { return await _origOffline(cb, ...args); }
|
|
75
|
+
finally { _inOff = false; }
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
window.Tone = new Proxy(_orig, {
|
|
79
|
+
get(t, p) {
|
|
80
|
+
if (p === 'Offline') return _wOffline;
|
|
81
|
+
if (_inOff && p in _fx) return _fx[p];
|
|
82
|
+
return t[p];
|
|
83
|
+
},
|
|
84
|
+
set(t, p, v) {
|
|
85
|
+
// Return false for non-configurable no-setter accessors to honor Proxy invariant.
|
|
86
|
+
const d = Object.getOwnPropertyDescriptor(t, p);
|
|
87
|
+
if (d && !d.configurable && d.get && !d.set) return false;
|
|
88
|
+
try { t[p] = v; } catch(e) {}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
40
94
|
window.renderComposition = async function(wavPath) {
|
|
41
95
|
// Wait for Tone.js to be ready
|
|
42
96
|
let attempts = 0;
|
|
@@ -47,21 +101,79 @@ const HELPER_SCRIPT = `
|
|
|
47
101
|
if (typeof Tone === 'undefined') throw new Error('Tone not loaded');
|
|
48
102
|
if (typeof window.audioBufferToWav !== 'function') throw new Error('audioBufferToWav not injected');
|
|
49
103
|
|
|
50
|
-
|
|
104
|
+
window._patchToneWorklets();
|
|
105
|
+
|
|
106
|
+
// Wait for TUNEFRAMES_READY if composition uses Tone.Sampler / CDN samples
|
|
107
|
+
if ('TUNEFRAMES_READY' in window) {
|
|
108
|
+
const signal = window.TUNEFRAMES_READY;
|
|
109
|
+
if (signal && typeof signal.then === 'function') {
|
|
110
|
+
await signal;
|
|
111
|
+
} else {
|
|
112
|
+
let waited = 0;
|
|
113
|
+
while (!window.TUNEFRAMES_READY && waited < 120000) {
|
|
114
|
+
await new Promise(r => setTimeout(r, 200));
|
|
115
|
+
waited += 200;
|
|
116
|
+
}
|
|
117
|
+
if (!window.TUNEFRAMES_READY) throw new Error('TUNEFRAMES_READY timed out after 120s');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let bpm = 120, duration = '12s';
|
|
51
122
|
const metaEl = document.getElementById('tuneframes');
|
|
52
123
|
if (metaEl) {
|
|
124
|
+
// Support both JSON textContent and data-* attributes
|
|
53
125
|
try {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
126
|
+
const txt = metaEl.textContent.trim();
|
|
127
|
+
if (txt) {
|
|
128
|
+
const meta = JSON.parse(txt);
|
|
129
|
+
bpm = meta.bpm || 120;
|
|
130
|
+
duration = meta.duration || '12s';
|
|
131
|
+
}
|
|
57
132
|
} catch(e) {}
|
|
133
|
+
if (metaEl.dataset.bpm) bpm = parseInt(metaEl.dataset.bpm, 10) || bpm;
|
|
134
|
+
if (metaEl.dataset.duration) duration = metaEl.dataset.duration || duration;
|
|
58
135
|
}
|
|
59
136
|
|
|
60
137
|
const durationSec = Math.max(Tone.Time(duration).toSeconds() + 0.5, 2);
|
|
61
138
|
console.log('TuneFrames: rendering', durationSec, 's at BPM', bpm);
|
|
62
139
|
|
|
63
140
|
const audioBuffer = await Tone.Offline(async () => {
|
|
141
|
+
// Fix: MetalSynth.triggerAttackRelease calls triggerAttack(computedTime, velocity)
|
|
142
|
+
// but triggerAttack(note, time, velocity) treats first arg as note and second as time,
|
|
143
|
+
// so toSeconds(velocity=undefined) = 0 for every trigger. Replace TAR to call
|
|
144
|
+
// _triggerEnvelopeAttack/_triggerEnvelopeRelease directly with the correct time.
|
|
145
|
+
// Also bumps same-time triggers by 0.1ms to prevent StateTimeline ordering errors.
|
|
146
|
+
if (typeof Tone !== 'undefined' && Tone.MetalSynth) {
|
|
147
|
+
Tone.MetalSynth.prototype.triggerAttackRelease = function(duration, time, velocity) {
|
|
148
|
+
const rawT = (time === undefined || time === null || +time <= 0) ? 0.05 : +time;
|
|
149
|
+
const vel = velocity !== undefined ? velocity : 1;
|
|
150
|
+
const dur = Tone.Time(duration).toSeconds();
|
|
151
|
+
let safeT = rawT;
|
|
152
|
+
try {
|
|
153
|
+
if (this._oscillators && this._oscillators[0] && this._oscillators[0]._state) {
|
|
154
|
+
const tl = this._oscillators[0]._state._timeline;
|
|
155
|
+
if (tl && tl.length > 0 && safeT <= tl[tl.length - 1].time) {
|
|
156
|
+
safeT = tl[tl.length - 1].time + 0.0001;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch(e) {}
|
|
160
|
+
this._triggerEnvelopeAttack(safeT, vel);
|
|
161
|
+
// Release envelope only; stopping oscillators adds StateTimeline stop events
|
|
162
|
+
// that block same-period re-triggers on rapid percussion patterns.
|
|
163
|
+
if (this.envelope && typeof this.envelope.triggerRelease === 'function') {
|
|
164
|
+
this.envelope.triggerRelease(safeT + dur);
|
|
165
|
+
}
|
|
166
|
+
return this;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
64
170
|
if (typeof main === 'function') await main();
|
|
171
|
+
|
|
172
|
+
// Auto-start Transport so Tone.Sequence / Tone.Part patterns play.
|
|
173
|
+
// Skills that already call Transport.start() inside main() are unaffected.
|
|
174
|
+
if (typeof Tone !== 'undefined' && Tone.Transport && Tone.Transport.state !== 'started') {
|
|
175
|
+
Tone.Transport.start(0);
|
|
176
|
+
}
|
|
65
177
|
}, durationSec, 1, 44100, bpm);
|
|
66
178
|
|
|
67
179
|
console.log('TuneFrames: buffer ready', audioBuffer.duration, audioBuffer.length);
|
|
@@ -76,10 +188,12 @@ const HELPER_SCRIPT = `
|
|
|
76
188
|
})();
|
|
77
189
|
`;
|
|
78
190
|
|
|
79
|
-
async function render(compositionPath, outputPath, format = 'mp3') {
|
|
191
|
+
async function render(compositionPath, outputPath, format = 'mp3', timeout = 60000) {
|
|
80
192
|
const browser = await chromium.launch({ headless: true });
|
|
81
193
|
const page = await browser.newPage();
|
|
82
194
|
|
|
195
|
+
page.setDefaultTimeout(timeout + 60000);
|
|
196
|
+
|
|
83
197
|
page.on('pageerror', err => console.error('BROWSER ERR:', err.message));
|
|
84
198
|
page.on('console', msg => console.log('BROWSER:', msg.type(), msg.text()));
|
|
85
199
|
|
|
@@ -93,15 +207,28 @@ async function render(compositionPath, outputPath, format = 'mp3') {
|
|
|
93
207
|
await page.addInitScript({ content: HELPER_SCRIPT });
|
|
94
208
|
|
|
95
209
|
await page.goto(`file://${path.resolve(compositionPath)}`, {
|
|
96
|
-
waitUntil: 'domcontentloaded', timeout
|
|
210
|
+
waitUntil: 'domcontentloaded', timeout
|
|
97
211
|
});
|
|
98
212
|
|
|
99
213
|
// Wait for Tone.js to actually be available
|
|
100
214
|
await page.waitForFunction(() => typeof Tone !== 'undefined', { timeout: 15000 });
|
|
101
|
-
|
|
215
|
+
|
|
216
|
+
// Apply global patches before any renderComposition runs.
|
|
217
|
+
// This covers skills that define their own renderComposition (e.g. audio-downtempo).
|
|
218
|
+
// Apply global patches before any renderComposition runs.
|
|
219
|
+
await page.evaluate(() => {
|
|
220
|
+
if (typeof window._patchToneWorklets === 'function') window._patchToneWorklets();
|
|
221
|
+
});
|
|
222
|
+
|
|
102
223
|
// Also wait for our renderComposition to be defined
|
|
103
224
|
await page.waitForFunction(() => typeof window.renderComposition === 'function', { timeout: 15000 });
|
|
104
225
|
|
|
226
|
+
// Check for TUNEFRAMES_READY signal (indicates Tone.Sampler / CDN sample usage)
|
|
227
|
+
const needsSampleWait = await page.evaluate(() => 'TUNEFRAMES_READY' in window);
|
|
228
|
+
if (needsSampleWait) {
|
|
229
|
+
console.log('Waiting for samples to load...');
|
|
230
|
+
}
|
|
231
|
+
|
|
105
232
|
const wavPath = path.resolve(outputPath.replace(/\.[^.]+$/, '.wav'));
|
|
106
233
|
|
|
107
234
|
console.log('Starting render...');
|
|
Binary file
|
|
Binary file
|
|
Binary file
|