Monorepo for Aesthetic.Computer aesthetic.computer
at main 275 lines 8.7 kB view raw
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);