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>
This commit is contained in:
177
AbletonMCP_AI/MCP_Server/template_analyzer.py
Normal file
177
AbletonMCP_AI/MCP_Server/template_analyzer.py
Normal file
@@ -0,0 +1,177 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user