Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env node
2
3import blessed from "blessed";
4import fs from "fs";
5import { SaxesParser } from "saxes";
6
7class AbletonSessionParser {
8 constructor() {
9 this.tracks = [];
10 this.currentTrack = null;
11 this.currentClip = null;
12 this.clipIdToSlot = new Map();
13 this.currentElement = "";
14 this.currentText = "";
15 this.tempo = 120;
16 }
17
18 parseFile(filePath) {
19 const data = fs.readFileSync(filePath, "utf8");
20 const parser = new SaxesParser({ fragment: false });
21
22 parser.on("opentag", (tag) => {
23 this.currentElement = tag.name;
24
25 if (tag.name === "GroupTrack" || tag.name === "AudioTrack" || tag.name === "MidiTrack") {
26 this.currentTrack = {
27 name: "Track",
28 clips: [],
29 id: tag.attributes?.Id?.value
30 };
31 } else if (tag.name === "ClipSlot" || tag.name === "GroupTrackSlot") {
32 this.currentClip = {
33 id: tag.attributes?.Id?.value,
34 hasClip: false,
35 name: "",
36 length: 0,
37 activity: 0,
38 color: 0
39 };
40 } else if (tag.name === "AudioClip" || tag.name === "MidiClip") {
41 if (this.currentClip) {
42 this.currentClip.hasClip = true;
43 this.currentClip.id = tag.attributes?.Id?.value;
44 this.currentClip.length = parseFloat(tag.attributes?.Length?.value || 1);
45 }
46 } else if (tag.name === "Tempo") {
47 this.tempo = parseFloat(tag.attributes?.Manual?.value || 120);
48 }
49 });
50
51 parser.on("text", (text) => {
52 this.currentText = text.trim();
53 });
54
55 parser.on("closetag", (tag) => {
56 if (tag.name === "EffectiveName" && this.currentText) {
57 if (this.currentTrack && !this.currentClip) {
58 this.currentTrack.name = this.currentText;
59 }
60 } else if (tag.name === "Name" && this.currentText) {
61 if (this.currentClip) {
62 this.currentClip.name = this.currentText;
63 }
64 } else if (tag.name === "ColorIndex" && this.currentClip && this.currentText) {
65 this.currentClip.color = parseInt(this.currentText);
66 } else if (tag.name === "ClipSlot" || tag.name === "GroupTrackSlot") {
67 if (this.currentTrack && this.currentClip) {
68 this.currentTrack.clips.push({ ...this.currentClip });
69 if (this.currentClip.id) {
70 this.clipIdToSlot.set(this.currentClip.id, {
71 trackIndex: this.tracks.length,
72 clipIndex: this.currentTrack.clips.length - 1
73 });
74 }
75 }
76 this.currentClip = null;
77 } else if (tag.name === "GroupTrack" || tag.name === "AudioTrack" || tag.name === "MidiTrack") {
78 if (this.currentTrack) {
79 this.tracks.push({ ...this.currentTrack });
80 }
81 this.currentTrack = null;
82 }
83
84 this.currentText = "";
85 this.currentElement = "";
86 });
87
88 parser.write(data);
89 parser.close();
90
91 return {
92 tracks: this.tracks,
93 tempo: this.tempo,
94 clipIdToSlot: this.clipIdToSlot
95 };
96 }
97}
98
99class BlessedSessionVisualizer {
100 constructor(sessionData) {
101 this.tracks = sessionData.tracks;
102 this.tempo = sessionData.tempo;
103 this.clipIdToSlot = sessionData.clipIdToSlot;
104 this.isPlaying = true; // Auto-play enabled
105 this.beat = 0;
106 this.trackActivity = new Map();
107 this.aggregateOutput = 0;
108 this.sceneColors = ["red", "yellow", "green", "blue", "magenta", "cyan", "white", "gray"];
109
110 // Initialize activity
111 this.tracks.forEach((track, i) => {
112 this.trackActivity.set(i, 0);
113 });
114
115 this.setupUI();
116 this.setupKeyHandlers();
117 this.startUpdateLoop();
118 }
119
120 setupUI() {
121 // Create screen
122 this.screen = blessed.screen({
123 smartCSR: true,
124 title: "Ableton Live Session View"
125 });
126
127 // Header box
128 this.headerBox = blessed.box({
129 parent: this.screen,
130 top: 0,
131 left: 0,
132 width: "100%",
133 height: 3,
134 content: "",
135 tags: true,
136 border: {
137 type: "line"
138 },
139 style: {
140 fg: "white",
141 bg: "black",
142 border: {
143 fg: "cyan"
144 }
145 }
146 });
147
148 // Session grid box
149 this.gridBox = blessed.box({
150 parent: this.screen,
151 top: 3,
152 left: 0,
153 width: "75%",
154 height: "60%",
155 content: "",
156 tags: true,
157 border: {
158 type: "line"
159 },
160 style: {
161 fg: "white",
162 bg: "black",
163 border: {
164 fg: "green"
165 }
166 }
167 });
168
169 // Track activity box
170 this.activityBox = blessed.box({
171 parent: this.screen,
172 top: 3,
173 left: "75%",
174 width: "25%",
175 height: "60%",
176 content: "",
177 tags: true,
178 border: {
179 type: "line"
180 },
181 style: {
182 fg: "white",
183 bg: "black",
184 border: {
185 fg: "yellow"
186 }
187 }
188 });
189
190 // Aggregate output box
191 this.outputBox = blessed.box({
192 parent: this.screen,
193 top: "63%",
194 left: 0,
195 width: "100%",
196 height: 5,
197 content: "",
198 tags: true,
199 border: {
200 type: "line"
201 },
202 style: {
203 fg: "white",
204 bg: "black",
205 border: {
206 fg: "magenta"
207 }
208 }
209 });
210
211 // Controls box
212 this.controlsBox = blessed.box({
213 parent: this.screen,
214 top: "68%",
215 left: 0,
216 width: "100%",
217 height: 3,
218 content: "{center}{bold}Controls: SPACE=play/pause, 1-8=trigger scenes, T=random trigger, Q=quit{/bold}{/center}",
219 tags: true,
220 border: {
221 type: "line"
222 },
223 style: {
224 fg: "white",
225 bg: "black",
226 border: {
227 fg: "white"
228 }
229 }
230 });
231
232 this.screen.render();
233 }
234
235 setupKeyHandlers() {
236 this.screen.key(["escape", "q", "C-c"], () => {
237 this.screen.destroy();
238 process.exit(0);
239 });
240
241 this.screen.key("space", () => {
242 this.isPlaying = !this.isPlaying;
243 });
244
245 // Scene triggers
246 for (let i = 1; i <= 8; i++) {
247 this.screen.key(i.toString(), () => {
248 this.triggerScene(i - 1);
249 });
250 }
251
252 this.screen.key("t", () => {
253 this.triggerRandomClips();
254 });
255
256 this.screen.key("r", () => {
257 this.reset();
258 });
259 }
260
261 updateHeader() {
262 const playIcon = this.isPlaying ? "▶️" : "⏸️";
263 const activeClips = this.getActiveClipCount();
264
265 this.headerBox.setContent(
266 `{center}{bold}🎛️ Ableton Live Session View{/bold}{/center}\n` +
267 `{center}${playIcon} Beat: ${this.beat.toFixed(2)} | Tempo: ${this.tempo} BPM | Active Clips: ${activeClips}{/center}`
268 );
269 }
270
271 updateSessionGrid() {
272 const maxScenes = 8;
273 const maxTracksPerLine = 16;
274 let content = "{bold}Session Grid:{/bold}\n";
275
276 // Track headers
277 let headerLine = " ";
278 for (let t = 0; t < Math.min(this.tracks.length, maxTracksPerLine); t++) {
279 const track = this.tracks[t];
280 const trackName = track.name.length > 8 ? track.name.substring(0, 8) : track.name;
281 headerLine += trackName.padEnd(10);
282 }
283 content += headerLine + "\n";
284
285 // Scene rows
286 for (let scene = 0; scene < maxScenes; scene++) {
287 let line = `${(scene + 1).toString().padStart(2)} │ `;
288
289 for (let t = 0; t < Math.min(this.tracks.length, maxTracksPerLine); t++) {
290 const track = this.tracks[t];
291 const clip = track.clips[scene];
292 const activity = this.trackActivity.get(t) || 0;
293
294 if (clip && clip.hasClip) {
295 const intensity = Math.min(Math.floor(activity / 25), 3);
296 const symbols = ["▁▁▁", "▃▃▃", "▅▅▅", "███"];
297 const colors = ["gray", "yellow", "green", "red"];
298 line += `{${colors[intensity]}-fg}[${symbols[intensity]}]{/${colors[intensity]}-fg} `;
299 } else {
300 line += "{gray-fg}[░░░]{/gray-fg} ";
301 }
302
303 if (Math.random() < 0.1) {
304 line += " · ";
305 }
306 }
307
308 content += line + "\n";
309 }
310
311 this.gridBox.setContent(content);
312 }
313
314 updateTrackActivity() {
315 let content = "{bold}Track Activity:{/bold}\n";
316
317 for (let i = 0; i < this.tracks.length; i++) {
318 const track = this.tracks[i];
319 const activity = this.trackActivity.get(i) || 0;
320 const barLength = 20;
321 const filled = Math.min(Math.floor((activity / 100) * barLength), barLength);
322 const empty = barLength - filled;
323
324 const trackName = track.name.length > 10 ? track.name.substring(0, 10) : track.name;
325 const bar = "█".repeat(filled) + "░".repeat(empty);
326 const percentage = Math.floor(activity);
327
328 let color = "gray";
329 if (activity > 75) color = "red";
330 else if (activity > 50) color = "yellow";
331 else if (activity > 25) color = "green";
332
333 content += `${trackName.padEnd(11)}│{${color}-fg}${bar}{/${color}-fg}│ ${percentage.toString().padStart(3)}%\n`;
334 }
335
336 this.activityBox.setContent(content);
337 }
338
339 updateAggregateOutput() {
340 const barLength = 60;
341 const filled = Math.min(Math.floor((this.aggregateOutput / 100) * barLength), barLength);
342 const empty = barLength - filled;
343
344 let color = "gray";
345 if (this.aggregateOutput > 75) color = "red";
346 else if (this.aggregateOutput > 50) color = "yellow";
347 else if (this.aggregateOutput > 25) color = "green";
348
349 const bar = "█".repeat(filled) + "░".repeat(empty);
350
351 this.outputBox.setContent(
352 `{bold}Aggregate Output:{/bold}\n` +
353 `{${color}-fg}${bar}{/${color}-fg}\n` +
354 `{center}Level: ${Math.floor(this.aggregateOutput)}%{/center}`
355 );
356 }
357
358 triggerScene(sceneIndex) {
359 this.tracks.forEach((track, trackIndex) => {
360 const clip = track.clips[sceneIndex];
361 if (clip && clip.hasClip) {
362 const activity = 50 + Math.random() * 50;
363 this.trackActivity.set(trackIndex, activity);
364 }
365 });
366 this.updateAggregateActivity();
367 }
368
369 triggerRandomClips() {
370 this.tracks.forEach((track, trackIndex) => {
371 if (Math.random() < 0.3) {
372 const activity = 30 + Math.random() * 70;
373 this.trackActivity.set(trackIndex, activity);
374 }
375 });
376 this.updateAggregateActivity();
377 }
378
379 updateAggregateActivity() {
380 let total = 0;
381 let count = 0;
382
383 this.trackActivity.forEach((activity) => {
384 if (activity > 0) {
385 total += activity;
386 count++;
387 }
388 });
389
390 this.aggregateOutput = count > 0 ? total / count : 0;
391 }
392
393 getActiveClipCount() {
394 let count = 0;
395 this.trackActivity.forEach((activity) => {
396 if (activity > 10) count++;
397 });
398 return count;
399 }
400
401 reset() {
402 this.trackActivity.forEach((_, index) => {
403 this.trackActivity.set(index, 0);
404 });
405 this.aggregateOutput = 0;
406 this.beat = 0;
407 }
408
409 startUpdateLoop() {
410 setInterval(() => {
411 if (this.isPlaying) {
412 this.beat += 0.25; // Faster beat progression
413 }
414
415 // Decay activity
416 this.trackActivity.forEach((activity, index) => {
417 const newActivity = Math.max(0, activity - 3); // Faster decay
418 this.trackActivity.set(index, newActivity);
419 });
420
421 // More frequent random activity spikes
422 if (Math.random() < 0.15) { // Increased from 0.05 to 0.15
423 const randomTrack = Math.floor(Math.random() * this.tracks.length);
424 const currentActivity = this.trackActivity.get(randomTrack) || 0;
425 const spike = Math.random() * 40; // Bigger spikes
426 this.trackActivity.set(randomTrack, Math.min(100, currentActivity + spike));
427 }
428
429 // Auto-trigger scenes occasionally
430 if (Math.random() < 0.08) { // Random scene triggers
431 const randomScene = Math.floor(Math.random() * 8);
432 this.triggerScene(randomScene);
433 }
434
435 this.updateAggregateActivity();
436 this.updateHeader();
437 this.updateSessionGrid();
438 this.updateTrackActivity();
439 this.updateAggregateOutput();
440 this.screen.render();
441 }, 50); // Faster updates: 50ms instead of 100ms
442 }
443}
444
445// Main execution
446const xmlPath = process.argv[2] || "/workspaces/aesthetic-computer/reference/extracted.xml";
447
448if (!fs.existsSync(xmlPath)) {
449 console.error(`XML file not found: ${xmlPath}`);
450 console.error("Usage: node ableton-session-viewer-blessed.mjs [path-to-extracted.xml]");
451 process.exit(1);
452}
453
454console.log("🎵 Parsing Ableton Live project...");
455const parser = new AbletonSessionParser();
456const sessionData = parser.parseFile(xmlPath);
457
458console.log(`📊 Found ${sessionData.tracks.length} tracks`);
459console.log(`🎯 Tempo: ${sessionData.tempo} BPM`);
460
461console.log("🚀 Starting blessed session viewer...");
462new BlessedSessionVisualizer(sessionData);