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
This commit is contained in:
996
mcp_server/engines/bus_architecture.py
Normal file
996
mcp_server/engines/bus_architecture.py
Normal file
@@ -0,0 +1,996 @@
|
||||
"""
|
||||
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())
|
||||
Reference in New Issue
Block a user