Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env node
2
3import fs from "fs";
4import { SaxesParser } from "saxes";
5
6class AbletonSessionParser {
7 constructor() {
8 this.tracks = [];
9 this.currentTrack = null;
10 this.currentClip = null;
11 this.currentElement = "";
12 this.currentText = "";
13 this.tempo = 120;
14 }
15
16 parseFile(filePath) {
17 const data = fs.readFileSync(filePath, "utf8");
18 const parser = new SaxesParser({ fragment: false });
19
20 parser.on("opentag", (tag) => {
21 this.currentElement = tag.name;
22
23 if (tag.name === "GroupTrack" || tag.name === "AudioTrack" || tag.name === "MidiTrack") {
24 this.currentTrack = {
25 name: "Track",
26 clips: [],
27 id: tag.attributes?.Id?.value
28 };
29 } else if (tag.name === "ClipSlot" || tag.name === "GroupTrackSlot") {
30 this.currentClip = {
31 id: tag.attributes?.Id?.value,
32 hasClip: false,
33 name: "",
34 length: 0,
35 activity: 0,
36 color: 0
37 };
38 } else if (tag.name === "AudioClip" || tag.name === "MidiClip") {
39 if (this.currentClip) {
40 this.currentClip.hasClip = true;
41 this.currentClip.id = tag.attributes?.Id?.value;
42 this.currentClip.length = parseFloat(tag.attributes?.Length?.value || 1);
43 }
44 } else if (tag.name === "Tempo") {
45 this.tempo = parseFloat(tag.attributes?.Manual?.value || 120);
46 }
47 });
48
49 parser.on("text", (text) => {
50 this.currentText = text.trim();
51 });
52
53 parser.on("closetag", (tag) => {
54 if (tag.name === "EffectiveName" && this.currentText) {
55 if (this.currentTrack && !this.currentClip) {
56 this.currentTrack.name = this.currentText;
57 }
58 } else if (tag.name === "Name" && this.currentText) {
59 if (this.currentClip) {
60 this.currentClip.name = this.currentText;
61 }
62 } else if (tag.name === "ColorIndex" && this.currentClip && this.currentText) {
63 this.currentClip.color = parseInt(this.currentText);
64 } else if (tag.name === "ClipSlot" || tag.name === "GroupTrackSlot") {
65 if (this.currentTrack && this.currentClip) {
66 this.currentTrack.clips.push({ ...this.currentClip });
67 }
68 this.currentClip = null;
69 } else if (tag.name === "GroupTrack" || tag.name === "AudioTrack" || tag.name === "MidiTrack") {
70 if (this.currentTrack) {
71 this.tracks.push({ ...this.currentTrack });
72 }
73 this.currentTrack = null;
74 }
75
76 this.currentText = "";
77 this.currentElement = "";
78 });
79
80 parser.write(data);
81 parser.close();
82
83 return {
84 tracks: this.tracks,
85 tempo: this.tempo
86 };
87 }
88}
89
90class SimpleSessionVisualizer {
91 constructor(sessionData) {
92 this.tracks = sessionData.tracks;
93 this.tempo = sessionData.tempo;
94 this.isPlaying = false;
95 this.beat = 0;
96 this.trackActivity = new Map();
97
98 // Initialize activity
99 this.tracks.forEach((track, i) => {
100 this.trackActivity.set(i, 0);
101 });
102
103 this.setupKeyHandlers();
104 this.startUpdateLoop();
105 }
106
107 setupKeyHandlers() {
108 process.stdin.setRawMode(true);
109 process.stdin.resume();
110 process.stdin.on('data', (key) => {
111 const char = key.toString();
112
113 if (char === '\u0003' || char === 'q') { // Ctrl+C or q
114 process.exit(0);
115 } else if (char === ' ') {
116 this.isPlaying = !this.isPlaying;
117 } else if (char >= '1' && char <= '8') {
118 this.triggerScene(parseInt(char) - 1);
119 } else if (char === 't') {
120 this.triggerRandomClips();
121 } else if (char === 'r') {
122 this.reset();
123 }
124 });
125 }
126
127 render() {
128 console.clear();
129
130 // Header
131 const playIcon = this.isPlaying ? "▶️" : "⏸️";
132 const activeClips = this.getActiveClipCount();
133 console.log(`🎛️ Ableton Live Session View ${playIcon} Beat: ${this.beat.toFixed(1)} | ${this.tempo} BPM | Active: ${activeClips}`);
134 console.log('');
135
136 // Track headers
137 let headerLine = ' ';
138 for (let t = 0; t < Math.min(this.tracks.length, 12); t++) {
139 const track = this.tracks[t];
140 const trackName = track.name.length > 8 ? track.name.substring(0, 8) : track.name;
141 headerLine += trackName.padEnd(10);
142 }
143 console.log(headerLine);
144 console.log('');
145
146 // Session grid (8 scenes)
147 for (let scene = 0; scene < 8; scene++) {
148 let line = `${(scene + 1).toString().padStart(2)} │ `;
149
150 for (let t = 0; t < Math.min(this.tracks.length, 12); t++) {
151 const track = this.tracks[t];
152 const clip = track.clips[scene];
153 const activity = this.trackActivity.get(t) || 0;
154
155 if (clip && clip.hasClip) {
156 const intensity = Math.min(Math.floor(activity / 25), 3);
157 const symbols = ["▁▁▁", "▃▃▃", "▅▅▅", "███"];
158 line += `[${symbols[intensity]}] `;
159 } else {
160 line += "[░░░] ";
161 }
162 }
163 console.log(line);
164 }
165
166 console.log('');
167
168 // Track activity meters
169 for (let i = 0; i < Math.min(this.tracks.length, 12); i++) {
170 const track = this.tracks[i];
171 const activity = this.trackActivity.get(i) || 0;
172 const barLength = 20;
173 const filled = Math.min(Math.floor((activity / 100) * barLength), barLength);
174 const empty = barLength - filled;
175
176 const trackName = track.name.length > 12 ? track.name.substring(0, 12) : track.name;
177 const bar = "█".repeat(filled) + "░".repeat(empty);
178 const percentage = Math.floor(activity);
179
180 console.log(`${trackName.padEnd(13)} │${bar}│ ${percentage.toString().padStart(3)}%`);
181 }
182
183 console.log('');
184
185 // Aggregate output
186 const totalActivity = Array.from(this.trackActivity.values()).reduce((sum, val) => sum + val, 0);
187 const avgActivity = this.trackActivity.size > 0 ? totalActivity / this.trackActivity.size : 0;
188 const aggregateBarLength = 40;
189 const aggregateFilled = Math.min(Math.floor((avgActivity / 100) * aggregateBarLength), aggregateBarLength);
190 const aggregateEmpty = aggregateBarLength - aggregateFilled;
191 const aggregateBar = "█".repeat(aggregateFilled) + "░".repeat(aggregateEmpty);
192
193 console.log(`Aggregate Output: │${aggregateBar}│ ${Math.floor(avgActivity)}%`);
194 console.log('');
195 console.log('Controls: SPACE=play/pause, 1-8=trigger scenes, T=random, R=reset, Q=quit');
196 }
197
198 triggerScene(sceneIndex) {
199 this.tracks.forEach((track, trackIndex) => {
200 const clip = track.clips[sceneIndex];
201 if (clip && clip.hasClip) {
202 const activity = 50 + Math.random() * 50;
203 this.trackActivity.set(trackIndex, activity);
204 }
205 });
206 }
207
208 triggerRandomClips() {
209 this.tracks.forEach((track, trackIndex) => {
210 if (Math.random() < 0.3) {
211 const activity = 30 + Math.random() * 70;
212 this.trackActivity.set(trackIndex, activity);
213 }
214 });
215 }
216
217 getActiveClipCount() {
218 let count = 0;
219 this.trackActivity.forEach((activity) => {
220 if (activity > 10) count++;
221 });
222 return count;
223 }
224
225 reset() {
226 this.trackActivity.forEach((_, index) => {
227 this.trackActivity.set(index, 0);
228 });
229 this.beat = 0;
230 }
231
232 startUpdateLoop() {
233 setInterval(() => {
234 if (this.isPlaying) {
235 this.beat += 0.1;
236 }
237
238 // Decay activity
239 this.trackActivity.forEach((activity, index) => {
240 const newActivity = Math.max(0, activity - 2);
241 this.trackActivity.set(index, newActivity);
242 });
243
244 // Random activity spikes
245 if (Math.random() < 0.05) {
246 const randomTrack = Math.floor(Math.random() * this.tracks.length);
247 const currentActivity = this.trackActivity.get(randomTrack) || 0;
248 const spike = Math.random() * 30;
249 this.trackActivity.set(randomTrack, Math.min(100, currentActivity + spike));
250 }
251
252 this.render();
253 }, 100);
254 }
255}
256
257// Main execution
258const xmlPath = process.argv[2] || "/workspaces/aesthetic-computer/system/public/assets/wipppps/zzzZWAP_extracted.xml";
259
260if (!fs.existsSync(xmlPath)) {
261 console.error(`XML file not found: ${xmlPath}`);
262 console.error("Usage: node ableton-session-simple.mjs [path-to-extracted.xml]");
263 process.exit(1);
264}
265
266console.log("🎵 Parsing Ableton Live project...");
267const parser = new AbletonSessionParser();
268const sessionData = parser.parseFile(xmlPath);
269
270console.log(`📊 Found ${sessionData.tracks.length} tracks`);
271console.log(`🎯 Tempo: ${sessionData.tempo} BPM`);
272console.log("🚀 Starting simple session viewer...");
273console.log("");
274
275new SimpleSessionVisualizer(sessionData);