Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 462 lines 13 kB view raw
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);