Files
ableton-mcp-ai/AbletonMCP_AI_BAK_20260328_200801/MCP_Server/template_analyzer.py
renato97 6ec8663954 Initial commit: AbletonMCP-AI complete system
- MCP Server with audio fallback, sample management
- Song generator with bus routing
- Reference listener and audio resampler
- Vector-based sample search
- Master chain with limiter and calibration
- Fix: Audio fallback now works without M4L
- Fix: Full song detection in sample loader

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 22:53:10 -03:00

178 lines
6.1 KiB
Python

from __future__ import annotations
import argparse
import gzip
import json
from collections import Counter
from pathlib import Path
import xml.etree.ElementTree as ET
def _node_name(node: ET.Element | None) -> str:
if node is None:
return ""
for tag in ("EffectiveName", "UserName", "Name"):
child = node.find(tag)
if child is not None:
value = child.attrib.get("Value", "")
if value:
return value
return node.attrib.get("Value", "")
def _device_name(device: ET.Element) -> str:
if device.tag == "PluginDevice":
info = device.find("PluginDesc/VstPluginInfo")
if info is None:
info = device.find("PluginDesc/AuPluginInfo")
if info is not None:
plug = info.find("PlugName")
if plug is not None and plug.attrib.get("Value"):
return plug.attrib["Value"]
return device.tag
def _session_clip_count(track: ET.Element) -> int:
count = 0
for slot in track.findall("./DeviceChain/MainSequencer/ClipSlotList/ClipSlot"):
if slot.find("Value/MidiClip") is not None or slot.find("Value/AudioClip") is not None:
count += 1
return count
def _arrangement_clip_count(track: ET.Element) -> int:
return len(track.findall(".//MainSequencer//MidiClip")) + len(
track.findall(".//MainSequencer//AudioClip")
)
def _tempo_value(live_set: ET.Element) -> float | None:
node = live_set.find(".//Tempo/Manual")
if node is None:
return None
try:
return float(node.attrib.get("Value", "0"))
except ValueError:
return None
def _locator_summary(live_set: ET.Element) -> list[dict[str, float | str | None]]:
locators: list[tuple[float, str]] = []
for locator in live_set.findall(".//Locators/Locators/Locator"):
try:
time = float(locator.find("Time").attrib.get("Value", "0"))
except (AttributeError, ValueError):
time = 0.0
name = _node_name(locator.find("Name"))
locators.append((time, name))
locators.sort(key=lambda item: item[0])
summary: list[dict[str, float | str | None]] = []
for index, (time, name) in enumerate(locators):
next_time = locators[index + 1][0] if index + 1 < len(locators) else None
summary.append(
{
"time_beats": time,
"name": name,
"section_length_beats": None if next_time is None else next_time - time,
}
)
return summary
def _arrangement_length_beats(root: ET.Element) -> float:
max_end = 0.0
for clip in root.findall(".//MidiClip") + root.findall(".//AudioClip"):
current_end = clip.find("CurrentEnd")
start = clip.attrib.get("Time")
if current_end is None or start is None:
continue
try:
end = float(start) + float(current_end.attrib.get("Value", "0"))
except ValueError:
continue
max_end = max(max_end, end)
return max_end
def analyze_set(als_path: Path) -> dict:
with gzip.open(als_path, "rb") as handle:
root = ET.parse(handle).getroot()
live_set = root.find("LiveSet")
if live_set is None:
raise ValueError(f"Invalid ALS file: {als_path}")
tracks = list(live_set.find("Tracks") or [])
track_summaries = []
device_counter: Counter[str] = Counter()
for track in tracks:
devices = track.findall("./DeviceChain/DeviceChain/Devices/*")
device_names = [_device_name(device) for device in devices]
device_counter.update(device_names)
track_summaries.append(
{
"type": track.tag,
"name": _node_name(track.find("Name")),
"group_id": track.find("TrackGroupId").attrib.get("Value", "")
if track.find("TrackGroupId") is not None
else "",
"session_clip_count": _session_clip_count(track),
"arrangement_clip_count": _arrangement_clip_count(track),
"devices": device_names,
}
)
automation_events = 0
for automation in root.findall(".//ArrangerAutomation"):
automation_events += len(automation.findall(".//FloatEvent"))
automation_events += len(automation.findall(".//EnumEvent"))
automation_events += len(automation.findall(".//BoolEvent"))
return {
"file": str(als_path),
"tempo": _tempo_value(live_set),
"track_type_counts": dict(Counter(track.tag for track in tracks)),
"scene_count": len(live_set.findall("./SceneNames/Scene")),
"locators": _locator_summary(live_set),
"arrangement_length_beats": _arrangement_length_beats(root),
"automation_event_count": automation_events,
"top_devices": dict(device_counter.most_common(16)),
"tracks": track_summaries,
}
def main() -> None:
parser = argparse.ArgumentParser(description="Analyze Ableton .als templates.")
parser.add_argument("path", nargs="?", default=".", help="Folder containing .als files")
parser.add_argument("--json", action="store_true", help="Emit JSON")
args = parser.parse_args()
base = Path(args.path).resolve()
results = [analyze_set(path) for path in sorted(base.rglob("*.als"))]
if args.json:
print(json.dumps(results, indent=2))
return
for result in results:
print(f"=== {Path(result['file']).name} ===")
print(f"tempo: {result['tempo']}")
print(f"tracks: {result['track_type_counts']}")
print(f"scenes: {result['scene_count']}")
print(f"arrangement_length_beats: {result['arrangement_length_beats']}")
print(f"automation_event_count: {result['automation_event_count']}")
print("locators:")
for locator in result["locators"]:
print(
f" - {locator['time_beats']:>6} {locator['name']}"
f" len={locator['section_length_beats']}"
)
print("top_devices:")
for name, count in result["top_devices"].items():
print(f" - {name}: {count}")
print()
if __name__ == "__main__":
main()