""" 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())