Files
ableton-mcp-ai/mcp_server/engines/bus_architecture.py
OpenCode Agent 5ce8187c65 feat: Implement senior audio injection with 5 fallback methods
- Add _cmd_create_arrangement_audio_pattern with 5-method fallback chain
- Method 1: track.insert_arrangement_clip() [Live 12+]
- Method 2: track.create_audio_clip() [Live 11+]
- Method 3: arrangement_clips.add_new_clip() [Live 12+]
- Method 4: Session->duplicate_clip_to_arrangement [Legacy]
- Method 5: Session->Recording [Universal]

- Add _cmd_duplicate_clip_to_arrangement for session-to-arrangement workflow
- Update skills documentation
- Verified: 3 clips created at positions [0, 4, 8] in Arrangement View

Closes: Audio injection in Arrangement View
2026-04-12 14:02:32 -03:00

997 lines
32 KiB
Python

"""
Professional Bus and Return Architecture for AbletonMCP_AI
Implements professional mixing architecture with:
- Bus groups (drums, bass, music, vocal, fx)
- Return tracks with effects (space/reverb, echo/delay, heat/saturation, glue/compression)
- Role-based mix profiles
- Master chain processing
"""
from __future__ import absolute_import, print_function, unicode_literals
# =============================================================================
# BUS GAIN CALIBRATION
# =============================================================================
BUS_GAIN_CALIBRATION = {
'drums': {
'volume': 0.92,
'compressor_threshold': -16.0,
'compressor_ratio': 4.0,
'saturator_drive': 0.6,
'pan': 0.0
},
'bass': {
'volume': 0.88,
'compressor_threshold': -18.0,
'compressor_ratio': 3.0,
'saturator_drive': 0.4,
'pan': 0.0
},
'music': {
'volume': 0.85,
'compressor_threshold': -20.0,
'compressor_ratio': 2.5,
'pan': 0.0
},
'vocal': {
'volume': 0.82,
'compressor_threshold': -16.0,
'compressor_ratio': 3.0,
'pan': 0.0
},
'fx': {
'volume': 0.78,
'compressor_threshold': -22.0,
'compressor_ratio': 2.0,
'pan': 0.0
}
}
# =============================================================================
# RETURN TRACK CONFIGURATION
# =============================================================================
RETURN_CONFIG = {
'space': { # Reverb
'device': 'Reverb',
'default_params': {
'PreDelay': 20.0,
'DecayTime': 2500.0,
'Size': 0.7,
'DryWet': 0.3
}
},
'echo': { # Delay
'device': 'Delay',
'default_params': {
'DelayTime': '1/8',
'Feedback': 0.35,
'DryWet': 0.25
}
},
'heat': { # Saturation
'device': 'Saturator',
'default_params': {
'Drive': 6.0,
'Type': 0, # Analog
'DryWet': 0.2
}
},
'glue': { # Bus Compression
'device': 'Compressor',
'default_params': {
'Threshold': -20.0,
'Ratio': 2.0,
'Attack': 10.0,
'Release': 100.0,
'DryWet': 0.15
}
}
}
# =============================================================================
# ROLE MIX PROFILES
# =============================================================================
ROLE_MIX = {
'kick': {
'volume': 0.85,
'pan': 0.0,
'sends': {'glue': 0.08},
'bus': 'drums'
},
'snare': {
'volume': 0.82,
'pan': 0.0,
'sends': {'space': 0.12, 'echo': 0.05, 'glue': 0.10},
'bus': 'drums'
},
'clap': {
'volume': 0.78,
'pan': 0.0,
'sends': {'space': 0.14, 'echo': 0.04, 'heat': 0.02, 'glue': 0.10},
'bus': 'drums'
},
'hat_closed': {
'volume': 0.72,
'pan': 0.15,
'sends': {'space': 0.08, 'glue': 0.05},
'bus': 'drums'
},
'hat_open': {
'volume': 0.75,
'pan': -0.15,
'sends': {'space': 0.15, 'glue': 0.06},
'bus': 'drums'
},
'bass': {
'volume': 0.78,
'pan': 0.0,
'sends': {'heat': 0.04, 'glue': 0.12},
'bus': 'bass'
},
'sub_bass': {
'volume': 0.80,
'pan': 0.0,
'sends': {'glue': 0.10},
'bus': 'bass'
},
'lead': {
'volume': 0.76,
'pan': 0.25,
'sends': {'space': 0.20, 'echo': 0.15, 'glue': 0.08},
'bus': 'music'
},
'pad': {
'volume': 0.70,
'pan': -0.20,
'sends': {'space': 0.35, 'echo': 0.10, 'glue': 0.06},
'bus': 'music'
},
'pluck': {
'volume': 0.74,
'pan': 0.30,
'sends': {'space': 0.18, 'echo': 0.12, 'glue': 0.07},
'bus': 'music'
},
'chords': {
'volume': 0.72,
'pan': 0.0,
'sends': {'space': 0.25, 'echo': 0.08, 'glue': 0.07},
'bus': 'music'
},
'fx': {
'volume': 0.68,
'pan': 0.0,
'sends': {'space': 0.40, 'echo': 0.20},
'bus': 'fx'
},
'vocal': {
'volume': 0.80,
'pan': 0.0,
'sends': {'space': 0.25, 'echo': 0.12, 'heat': 0.03, 'glue': 0.10},
'bus': 'vocal'
}
}
# =============================================================================
# MASTER CHAIN CONFIGURATION
# =============================================================================
MASTER_CHAIN = {
'eq': {
'device': 'EQEight',
'params': {
'GainLow': 0.0,
'FreqLowest': 30.0,
'GainMid': 0.0,
'GainHigh': 0.0
}
},
'compressor': {
'device': 'Compressor',
'params': {
'Threshold': -6.0,
'Ratio': 2.0,
'Attack': 3.0,
'Release': 60.0,
'DryWet': 100.0
}
},
'limiter': {
'device': 'Limiter',
'params': {
'Gain': 0.0,
'Ceiling': -0.3
}
}
}
# =============================================================================
# BUS ARCHITECTURE IMPLEMENTATION
# =============================================================================
class BusArchitecture:
"""Professional bus and return architecture manager."""
def __init__(self, ableton_conn):
"""
Initialize with Ableton connection.
Args:
ableton_conn: The Ableton Live connection (self from __init__.py)
"""
self.conn = ableton_conn
self._song = ableton_conn._song if hasattr(ableton_conn, '_song') else None
self._bus_indices = {} # bus_name -> track_index
self._return_indices = {} # return_name -> return_track_index
def create_bus_track(self, bus_name, bus_type='audio'):
"""
Creates a bus (group) track for submixing.
Args:
bus_name: Name for the bus track (e.g., "BUS Drums")
bus_type: 'audio' or 'midi' (default 'audio')
Returns:
dict: Creation status with track_index
"""
if self._song is None:
return {"error": "No song connection available"}
try:
# Create appropriate track type
if bus_type.lower() == 'midi':
self._song.create_midi_track(-1)
else:
self._song.create_audio_track(-1)
idx = len(self._song.tracks) - 1
track = self._song.tracks[idx]
track.name = str(bus_name)
# Store the index
self._bus_indices[bus_name] = idx
return {
"bus_created": True,
"track_index": idx,
"bus_name": str(bus_name),
"bus_type": bus_type
}
except Exception as e:
return {
"bus_created": False,
"error": str(e),
"bus_name": str(bus_name)
}
def create_return_track(self, return_name, effect_type=None):
"""
Creates a return track with optional effect.
Args:
return_name: Name for the return track (e.g., "Reverb", "Delay")
effect_type: Effect device name to insert (e.g., "Reverb", "Delay")
Returns:
dict: Creation status with return_track_index
"""
if self._song is None:
return {"error": "No song connection available"}
try:
# Create return track using Live API
if hasattr(self._song, 'create_return_track'):
self._song.create_return_track(-1)
else:
# Fallback: create audio track and use as return
self._song.create_audio_track(-1)
# Return tracks are after regular tracks in Live
if hasattr(self._song, 'return_tracks'):
idx = len(self._song.return_tracks) - 1
return_track = self._song.return_tracks[idx]
else:
# Fallback: use last created track
idx = len(self._song.tracks) - 1
return_track = self._song.tracks[idx]
return_track.name = str(return_name)
# Store the index
self._return_indices[return_name] = idx
result = {
"return_created": True,
"return_index": idx,
"return_name": str(return_name)
}
# Insert effect if specified
if effect_type:
device_result = self._insert_device_on_return(idx, effect_type)
result["device_inserted"] = device_result
return result
except Exception as e:
return {
"return_created": False,
"error": str(e),
"return_name": str(return_name)
}
def _insert_device_on_return(self, return_index, device_name):
"""Insert a device on a return track."""
try:
if hasattr(self._song, 'return_tracks'):
track = self._song.return_tracks[return_index]
else:
track = self._song.tracks[return_index]
# Use the connection's device insertion if available
if hasattr(self.conn, '_browser_load_device'):
return self.conn._browser_load_device(track, device_name)
return False
except Exception as e:
return False
def route_track_to_bus(self, track_index, bus_name):
"""
Routes a track's output to a bus track.
In Ableton Live, this is typically done by grouping tracks or setting
output routing. Since direct API routing is limited, this sets up
the conceptual routing and returns guidance.
Args:
track_index: Index of the source track
bus_name: Name of the bus track to route to
Returns:
dict: Routing status
"""
if self._song is None:
return {"error": "No song connection available"}
try:
src_idx = int(track_index)
src_track = self._song.tracks[src_idx]
# Find the bus track
bus_idx = None
bus_track = None
# Check our stored indices first
if bus_name in self._bus_indices:
bus_idx = self._bus_indices[bus_name]
bus_track = self._song.tracks[bus_idx]
else:
# Search by name
for i, t in enumerate(self._song.tracks):
if bus_name.lower() in str(t.name).lower():
bus_idx = i
bus_track = t
break
if bus_track is None:
return {
"routed": False,
"error": "Bus track '%s' not found" % bus_name
}
# Try to configure output routing through mixer device
# Note: Full output routing API varies by Live version
mixer = src_track.mixer_device
# Attempt to set up sends to the bus if available
sends_configured = 0
if hasattr(mixer, 'sends'):
for send in mixer.sends:
if hasattr(send, 'target_track') and send.target_track == bus_track:
# Send already targets this bus
sends_configured += 1
break
# Try output routing if available
output_set = False
if hasattr(src_track, 'output_routing_type'):
# Some Live versions support this
try:
src_track.output_routing_type = bus_track
output_set = True
except:
pass
elif hasattr(src_track, 'output_routing_channel'):
try:
src_track.output_routing_channel = bus_track
output_set = True
except:
pass
return {
"routed": True,
"track_index": src_idx,
"track_name": str(src_track.name),
"bus_index": bus_idx,
"bus_name": str(bus_name),
"output_routing_set": output_set,
"sends_configured": sends_configured,
"note": "Manual grouping in Live may be needed for complete bus routing"
}
except Exception as e:
return {
"routed": False,
"track_index": track_index,
"error": str(e)
}
def set_track_send(self, track_index, return_name, amount):
"""
Sets send amount from a track to a return track.
Args:
track_index: Index of the source track
return_name: Name of the return track
amount: Send amount 0.0-1.0
Returns:
dict: Send configuration status
"""
if self._song is None:
return {"error": "No song connection available"}
try:
track_idx = int(track_index)
track = self._song.tracks[track_idx]
send_amount = float(amount)
# Find return track index
return_idx = None
if return_name in self._return_indices:
return_idx = self._return_indices[return_name]
else:
# Search in return tracks
if hasattr(self._song, 'return_tracks'):
for i, rt in enumerate(self._song.return_tracks):
if return_name.lower() in str(rt.name).lower():
return_idx = i
break
if return_idx is None:
return {
"send_set": False,
"error": "Return track '%s' not found" % return_name
}
# Configure send via mixer device
mixer = track.mixer_device
sends_configured = 0
if hasattr(mixer, 'sends') and return_idx < len(mixer.sends):
send = mixer.sends[return_idx]
if hasattr(send, 'value'):
send.value = send_amount
sends_configured = 1
return {
"send_set": sends_configured > 0,
"track_index": track_idx,
"track_name": str(track.name),
"return_name": str(return_name),
"return_index": return_idx,
"amount": send_amount,
"sends_configured": sends_configured
}
except Exception as e:
return {
"send_set": False,
"track_index": track_index,
"error": str(e)
}
def configure_bus_gain(self, bus_name):
"""
Configure bus track with professional gain calibration settings.
Args:
bus_name: Name of the bus (must match BUS_GAIN_CALIBRATION keys)
Returns:
dict: Configuration status
"""
if bus_name not in BUS_GAIN_CALIBRATION:
return {
"configured": False,
"error": "Unknown bus name '%s'. Valid: %s" % (bus_name, list(BUS_GAIN_CALIBRATION.keys()))
}
config = BUS_GAIN_CALIBRATION[bus_name]
# Find the bus track
bus_idx = self._bus_indices.get(bus_name)
if bus_idx is None:
# Search by name pattern
for i, t in enumerate(self._song.tracks):
if bus_name.lower() in str(t.name).lower() or ('bus' in str(t.name).lower() and bus_name.lower() in str(t.name).lower()):
bus_idx = i
break
if bus_idx is None:
return {
"configured": False,
"error": "Bus track '%s' not found" % bus_name
}
try:
track = self._song.tracks[bus_idx]
# Set volume
track.mixer_device.volume.value = config['volume']
# Set pan
track.mixer_device.panning.value = config['pan']
return {
"configured": True,
"bus_name": bus_name,
"bus_index": bus_idx,
"volume": config['volume'],
"pan": config['pan'],
"note": "Compressor and saturator settings available for manual application"
}
except Exception as e:
return {
"configured": False,
"bus_name": bus_name,
"error": str(e)
}
def configure_return_effect(self, return_name):
"""
Configure return track effect with default parameters.
Args:
return_name: Name of the return (must match RETURN_CONFIG keys)
Returns:
dict: Configuration status
"""
if return_name not in RETURN_CONFIG:
return {
"configured": False,
"error": "Unknown return name '%s'. Valid: %s" % (return_name, list(RETURN_CONFIG.keys()))
}
config = RETURN_CONFIG[return_name]
# Find the return track
return_idx = self._return_indices.get(return_name)
if return_idx is None:
# Search in return tracks
if hasattr(self._song, 'return_tracks'):
for i, rt in enumerate(self._song.return_tracks):
if return_name.lower() in str(rt.name).lower():
return_idx = i
break
if return_idx is None:
return {
"configured": False,
"error": "Return track '%s' not found" % return_name
}
try:
# Get the return track
if hasattr(self._song, 'return_tracks'):
track = self._song.return_tracks[return_idx]
else:
track = self._song.tracks[return_idx]
# Find the effect device
device = None
for d in track.devices:
if config['device'].lower() in str(d.name).lower():
device = d
break
if device is None:
return {
"configured": False,
"return_name": return_name,
"error": "Device '%s' not found on return track" % config['device']
}
# Configure parameters
params_set = 0
if hasattr(device, 'parameters'):
for param in device.parameters:
param_name = str(param.name)
for key, value in config['default_params'].items():
if key in param_name:
try:
if isinstance(value, str):
# Handle string values like '1/8' for delay time
# This may need manual adjustment in Live
pass
else:
param.value = float(value)
params_set += 1
except Exception:
pass
break
return {
"configured": True,
"return_name": return_name,
"return_index": return_idx,
"device": config['device'],
"parameters_set": params_set,
"target_params": list(config['default_params'].keys())
}
except Exception as e:
return {
"configured": False,
"return_name": return_name,
"error": str(e)
}
def apply_role_mix(self, track_index, role):
"""
Apply role-based mix settings to a track.
Args:
track_index: Index of the track
role: Role name (must match ROLE_MIX keys)
Returns:
dict: Application status
"""
if role not in ROLE_MIX:
return {
"applied": False,
"error": "Unknown role '%s'. Valid: %s" % (role, list(ROLE_MIX.keys()))
}
config = ROLE_MIX[role]
try:
track_idx = int(track_index)
track = self._song.tracks[track_idx]
# Set volume
track.mixer_device.volume.value = config['volume']
# Set pan
track.mixer_device.panning.value = config['pan']
# Configure sends
sends_configured = []
for return_name, amount in config['sends'].items():
result = self.set_track_send(track_idx, return_name, amount)
sends_configured.append({
"return": return_name,
"amount": amount,
"status": result.get("send_set", False)
})
return {
"applied": True,
"track_index": track_idx,
"track_name": str(track.name),
"role": role,
"volume": config['volume'],
"pan": config['pan'],
"target_bus": config['bus'],
"sends": sends_configured
}
except Exception as e:
return {
"applied": False,
"track_index": track_index,
"role": role,
"error": str(e)
}
def configure_master_chain(self):
"""
Configure master track with professional mastering chain.
Returns:
dict: Configuration status
"""
try:
master = self._song.master_track
devices_found = {}
# Check for existing devices
for chain_type, chain_config in MASTER_CHAIN.items():
device_name = chain_config['device']
device = None
for d in master.devices:
if device_name.lower() in str(d.name).lower():
device = d
break
devices_found[chain_type] = {
"device": device_name,
"found": device is not None,
"name": str(device.name) if device else None
}
# Configure parameters if device exists
if device and hasattr(device, 'parameters'):
params_set = 0
for param in device.parameters:
param_name = str(param.name)
for key, value in chain_config['params'].items():
if key in param_name:
try:
param.value = float(value)
params_set += 1
except Exception:
pass
break
devices_found[chain_type]["params_set"] = params_set
return {
"configured": True,
"master_track": "Master",
"devices": devices_found,
"recommendation": "Add EQ Eight, Compressor, and Limiter to master if not present"
}
except Exception as e:
return {
"configured": False,
"error": str(e)
}
# =============================================================================
# MODULE-LEVEL FUNCTIONS (for direct use)
# =============================================================================
def create_bus_track(ableton_conn, bus_name, bus_type='audio'):
"""
Creates a group/bus track.
Args:
ableton_conn: The Ableton Live connection
bus_name: Name for the bus track
bus_type: 'audio' or 'midi'
Returns:
dict: Creation status
"""
arch = BusArchitecture(ableton_conn)
return arch.create_bus_track(bus_name, bus_type)
def create_return_track(ableton_conn, return_name, effect_type=None):
"""
Creates a return track with effect.
Args:
ableton_conn: The Ableton Live connection
return_name: Name for the return track
effect_type: Effect device name to insert
Returns:
dict: Creation status
"""
arch = BusArchitecture(ableton_conn)
return arch.create_return_track(return_name, effect_type)
def route_track_to_bus(ableton_conn, track_index, bus_name):
"""
Routes a track to a bus.
Args:
ableton_conn: The Ableton Live connection
track_index: Index of the source track
bus_name: Name of the bus track
Returns:
dict: Routing status
"""
arch = BusArchitecture(ableton_conn)
return arch.route_track_to_bus(track_index, bus_name)
def set_track_send(ableton_conn, track_index, return_name, amount):
"""
Sets send amount to return track.
Args:
ableton_conn: The Ableton Live connection
track_index: Index of the source track
return_name: Name of the return track
amount: Send amount 0.0-1.0
Returns:
dict: Send configuration status
"""
arch = BusArchitecture(ableton_conn)
return arch.set_track_send(track_index, return_name, amount)
def apply_professional_mix(ableton_conn, track_assignments):
"""
Applies complete professional mix architecture.
This is the main entry point for setting up a professional mix:
1. Creates buses (drums, bass, music, vocal, fx)
2. Creates returns (space, echo, heat, glue)
3. Routes tracks to appropriate buses
4. Sets send levels per role
5. Applies master chain configuration
6. Configures bus gain calibration
Args:
ableton_conn: The Ableton Live connection
track_assignments: List of dicts with 'track_index', 'role', 'bus'
Example: [
{"track_index": 0, "role": "kick", "bus": "drums"},
{"track_index": 1, "role": "bass", "bus": "bass"},
]
Returns:
dict: Complete mix application status
"""
arch = BusArchitecture(ableton_conn)
results = {
"buses_created": [],
"returns_created": [],
"tracks_routed": [],
"sends_configured": [],
"master_configured": False,
"errors": []
}
try:
# 1. Create buses
bus_names = ['drums', 'bass', 'music', 'vocal', 'fx']
for bus_name in bus_names:
bus_result = arch.create_bus_track("BUS %s" % bus_name.capitalize())
if bus_result.get("bus_created"):
results["buses_created"].append(bus_result)
# Configure bus gain
gain_result = arch.configure_bus_gain(bus_name)
if gain_result.get("configured"):
results["buses_created"][-1]["gain_configured"] = True
else:
results["errors"].append("Bus %s: %s" % (bus_name, bus_result.get("error", "Unknown error")))
# 2. Create returns with effects
for return_name, config in RETURN_CONFIG.items():
return_result = arch.create_return_track(
return_name.capitalize(),
effect_type=config['device']
)
if return_result.get("return_created"):
results["returns_created"].append(return_result)
# Configure return effect
effect_result = arch.configure_return_effect(return_name)
if effect_result.get("configured"):
results["returns_created"][-1]["effect_configured"] = True
else:
results["errors"].append("Return %s: %s" % (return_name, return_result.get("error", "Unknown error")))
# 3. Route tracks and apply role mix
for assignment in track_assignments:
track_idx = assignment.get("track_index")
role = assignment.get("role")
bus = assignment.get("bus")
if track_idx is None or role is None:
continue
# Apply role mix (includes sends)
mix_result = arch.apply_role_mix(track_idx, role)
if mix_result.get("applied"):
results["tracks_routed"].append(mix_result)
else:
results["errors"].append("Track %s role %s: %s" % (track_idx, role, mix_result.get("error")))
# Route to bus if specified
if bus:
route_result = arch.route_track_to_bus(track_idx, "BUS %s" % bus.capitalize())
if route_result.get("routed"):
results["tracks_routed"][-1]["bus_routed"] = True
# 4. Configure master chain
master_result = arch.configure_master_chain()
results["master_configured"] = master_result.get("configured", False)
results["master_details"] = master_result
# Summary
results["summary"] = {
"buses": len(results["buses_created"]),
"returns": len(results["returns_created"]),
"tracks_processed": len(results["tracks_routed"]),
"errors": len(results["errors"])
}
return results
except Exception as e:
results["errors"].append("Fatal error: %s" % str(e))
return results
def get_bus_config(bus_name):
"""
Get bus configuration by name.
Args:
bus_name: Name of the bus (e.g., 'drums', 'bass')
Returns:
dict: Bus configuration or None
"""
return BUS_GAIN_CALIBRATION.get(bus_name)
def get_return_config(return_name):
"""
Get return track configuration by name.
Args:
return_name: Name of the return (e.g., 'space', 'echo')
Returns:
dict: Return configuration or None
"""
return RETURN_CONFIG.get(return_name)
def get_role_mix(role):
"""
Get role mix profile.
Args:
role: Role name (e.g., 'kick', 'bass', 'lead')
Returns:
dict: Role mix configuration or None
"""
return ROLE_MIX.get(role)
def get_master_chain():
"""
Get master chain configuration.
Returns:
dict: Master chain configuration
"""
return MASTER_CHAIN
def list_available_buses():
"""List all available bus names."""
return list(BUS_GAIN_CALIBRATION.keys())
def list_available_returns():
"""List all available return names."""
return list(RETURN_CONFIG.keys())
def list_available_roles():
"""List all available role names."""
return list(ROLE_MIX.keys())