Files
ableton-mcp-ai/mcp_server/engines/live_bridge.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

1150 lines
45 KiB
Python

"""
AbletonLiveBridge - Bridge between MCP server and Ableton Live API.
Provides a high-level interface for executing engine configurations
and controlling Live via the TCP connection.
"""
import sys
import os
import json
import logging
from typing import Dict, List, Any, Optional, Tuple, Union
from dataclasses import dataclass
from enum import Enum
# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("AbletonLiveBridge")
class LiveAPIError(Exception):
"""Exception raised for Live API errors."""
pass
class DeviceNotFoundError(LiveAPIError):
"""Exception raised when a device is not found."""
pass
class TrackNotFoundError(LiveAPIError):
"""Exception raised when a track is not found."""
pass
@dataclass
class MixConfiguration:
"""Configuration for mix settings."""
track_index: int
volume: Optional[float] = None
pan: Optional[float] = None
mute: Optional[bool] = None
solo: Optional[bool] = None
sends: Optional[Dict[int, float]] = None
devices: Optional[List[Dict[str, Any]]] = None
@dataclass
class CompressorSettings:
"""Settings for Ableton's Compressor device."""
threshold: float = -20.0
ratio: float = 4.0
attack: float = 0.1
release: float = 10.0
make_up: float = 0.0
use_sidechain: bool = False
@dataclass
class EQPreset:
"""EQ Eight preset configuration."""
name: str
high_pass: Optional[float] = None
low_shelf: Optional[Tuple[float, float]] = None # (freq, gain)
mid_boost: Optional[Tuple[float, float, float]] = None # (freq, gain, q)
high_shelf: Optional[Tuple[float, float]] = None # (freq, gain)
class AbletonLiveBridge:
"""
Bridge class for executing engine configurations in Ableton Live.
This class provides a high-level interface for controlling Live's
tracks, devices, arrangement, and playback via the MCP TCP connection.
"""
def __init__(self, song, mcp_connection):
"""
Initialize the Live bridge.
Args:
song: Ableton Live song object (Live.Song.Song)
mcp_connection: MCP TCP connection for sending commands
"""
self.song = song
self.mcp_connection = mcp_connection
self.live_version = self._get_live_version()
self._pending_tasks = []
logger.info(f"AbletonLiveBridge initialized (Live version: {self.live_version})")
def _get_live_version(self) -> str:
"""Get Ableton Live version for compatibility checks."""
try:
app = self.song.application()
return app.get_major_version() if hasattr(app, 'get_major_version') else "unknown"
except:
return "unknown"
def _check_api_version(self, min_version: str = "11") -> bool:
"""Check if Live API version meets minimum requirements."""
try:
if self.live_version == "unknown":
return True # Assume compatible if version unknown
return int(self.live_version) >= int(min_version)
except:
return False
def _send_tcp_command(self, command: Dict[str, Any]) -> Dict[str, Any]:
"""
Send a command via TCP connection.
Args:
command: Dictionary with command data
Returns:
Response dictionary with status and result
"""
try:
if self.mcp_connection:
# Send command through MCP connection
self.mcp_connection.send(json.dumps(command).encode())
response = self.mcp_connection.recv(4096).decode()
return json.loads(response)
else:
return {"status": "error", "message": "No MCP connection available"}
except Exception as e:
logger.error(f"TCP command failed: {e}")
return {"status": "error", "message": str(e)}
def _create_result(self, success: bool, message: str = "", data: Any = None) -> Dict[str, Any]:
"""Create a standardized result dictionary."""
result = {
"success": success,
"message": message,
"data": data
}
if not success:
logger.warning(f"Operation failed: {message}")
return result
# =========================================================================
# Bus and Return Management
# =========================================================================
def create_bus_track(self, name: str, bus_type: str = "Group") -> Dict[str, Any]:
"""
Create a group/bus track for mixing.
Args:
name: Name for the bus track
bus_type: Type of bus ("Group", "Master", etc.)
Returns:
Result dictionary with track index if successful
"""
try:
# Create group track via Live API
tracks = list(self.song.tracks)
# Create audio track first, then convert to group
self.song.create_audio_track(-1)
new_track = self.song.tracks[-1]
# Convert to group track if possible
if hasattr(new_track, 'is_grouped'):
# Set as group track
new_track.name = name
track_index = len(self.song.tracks) - 1
return self._create_result(
True,
f"Bus track '{name}' created at index {track_index}",
{"track_index": track_index, "name": name, "type": bus_type}
)
else:
# Fallback: just use as regular track
new_track.name = name
track_index = len(self.song.tracks) - 1
return self._create_result(
True,
f"Track '{name}' created at index {track_index} (group features may be limited)",
{"track_index": track_index, "name": name}
)
except Exception as e:
return self._create_result(False, f"Failed to create bus track: {str(e)}")
def create_return_track(self, name: str, effect_type: str = "Reverb") -> Dict[str, Any]:
"""
Create a return track with an effect.
Args:
name: Name for the return track
effect_type: Type of effect ("Reverb", "Delay", etc.)
Returns:
Result dictionary with return track index if successful
"""
try:
# Create return track
if hasattr(self.song, 'create_return_track'):
self.song.create_return_track()
return_track = self.song.return_tracks[-1]
return_track.name = name
return_index = len(self.song.return_tracks) - 1
# Add effect device if possible
if effect_type and hasattr(return_track, 'devices'):
# Effect will be added by insert_device later
pass
return self._create_result(
True,
f"Return track '{name}' created at index {return_index}",
{"return_index": return_index, "name": name, "effect_type": effect_type}
)
else:
return self._create_result(False, "Live version doesn't support return tracks")
except Exception as e:
return self._create_result(False, f"Failed to create return track: {str(e)}")
def route_track_to_bus(self, track_index: int, bus_name: str) -> Dict[str, Any]:
"""
Route a track's output to a bus/group track.
Args:
track_index: Index of the source track
bus_name: Name of the target bus track
Returns:
Result dictionary indicating success/failure
"""
try:
if track_index < 0 or track_index >= len(self.song.tracks):
raise TrackNotFoundError(f"Track index {track_index} out of range")
source_track = self.song.tracks[track_index]
# Find bus track by name
bus_track = None
bus_index = -1
for i, track in enumerate(self.song.tracks):
if track.name == bus_name:
bus_track = track
bus_index = i
break
if bus_track is None:
return self._create_result(False, f"Bus track '{bus_name}' not found")
# Set output routing
if hasattr(source_track, 'output_meter_level'):
# Try to set output to the bus
# Note: Exact API may vary by Live version
if hasattr(source_track, 'output_routing_type'):
# Set routing
source_track.output_routing_type = bus_track
elif hasattr(source_track, 'group_track'):
source_track.group_track = bus_track
else:
# Manual grouping via Live's internal API
pass
return self._create_result(
True,
f"Track {track_index} routed to bus '{bus_name}' (index {bus_index})"
)
except TrackNotFoundError as e:
return self._create_result(False, str(e))
except Exception as e:
return self._create_result(False, f"Failed to route track: {str(e)}")
def set_track_send(self, track_index: int, return_index: int, amount: float) -> Dict[str, Any]:
"""
Configure send amount from a track to a return track.
Args:
track_index: Index of the source track
return_index: Index of the return track
amount: Send amount (0.0 - 1.0)
Returns:
Result dictionary indicating success/failure
"""
try:
if track_index < 0 or track_index >= len(self.song.tracks):
raise TrackNotFoundError(f"Track index {track_index} out of range")
track = self.song.tracks[track_index]
# Clamp amount to valid range
amount = max(0.0, min(1.0, amount))
# Set send value
if hasattr(track, 'mixer_device') and hasattr(track.mixer_device, 'sends'):
sends = track.mixer_device.sends
if return_index < len(sends):
sends[return_index].value = amount
return self._create_result(
True,
f"Send {return_index} on track {track_index} set to {amount:.2f}"
)
else:
return self._create_result(False, f"Return index {return_index} out of range")
else:
return self._create_result(False, "Track doesn't support sends")
except TrackNotFoundError as e:
return self._create_result(False, str(e))
except Exception as e:
return self._create_result(False, f"Failed to set send: {str(e)}")
# =========================================================================
# Device Management
# =========================================================================
def insert_device(self, track_index: int, device_name: str) -> Dict[str, Any]:
"""
Insert a device/instrument on a track.
Args:
track_index: Index of the target track
device_name: Name of the device to insert
Returns:
Result dictionary with device index if successful
"""
try:
if track_index < 0 or track_index >= len(self.song.tracks):
raise TrackNotFoundError(f"Track index {track_index} out of range")
track = self.song.tracks[track_index]
# Map common device names to Live device types
device_map = {
"eq eight": "EQ Eight",
"eq8": "EQ Eight",
"compressor": "Compressor",
"reverb": "Reverb",
"delay": "Delay",
"saturator": "Saturator",
"limiter": "Limiter",
"utility": "Utility",
"filter": "Auto Filter",
"autofilter": "Auto Filter"
}
canonical_name = device_map.get(device_name.lower(), device_name)
# Try to load device from browser
if hasattr(self.song, 'browser'):
browser = self.song.browser
# Search for device
device_to_load = None
# Look in audio effects
if hasattr(browser, 'audio_effects'):
for device in browser.audio_effects:
if canonical_name.lower() in device.name.lower():
device_to_load = device
break
# Look in instruments
if device_to_load is None and hasattr(browser, 'instruments'):
for device in browser.instruments:
if canonical_name.lower() in device.name.lower():
device_to_load = device
break
# Load the device
if device_to_load and hasattr(track, 'devices'):
# Add to end of device chain
track.load_device(device_to_load)
device_index = len(track.devices) - 1
return self._create_result(
True,
f"Device '{canonical_name}' inserted on track {track_index} at position {device_index}",
{"track_index": track_index, "device_index": device_index, "device_name": canonical_name}
)
else:
return self._create_result(False, f"Device '{device_name}' not found in browser")
else:
return self._create_result(False, "Browser not available")
except TrackNotFoundError as e:
return self._create_result(False, str(e))
except Exception as e:
return self._create_result(False, f"Failed to insert device: {str(e)}")
def configure_device(self, track_index: int, device_name: str,
params: Dict[str, Any]) -> Dict[str, Any]:
"""
Configure parameters of a device on a track.
Args:
track_index: Index of the target track
device_name: Name of the device to configure
params: Dictionary of parameter names and values
Returns:
Result dictionary indicating success/failure
"""
try:
if track_index < 0 or track_index >= len(self.song.tracks):
raise TrackNotFoundError(f"Track index {track_index} out of range")
track = self.song.tracks[track_index]
# Find device by name
target_device = None
if hasattr(track, 'devices'):
for device in track.devices:
if device_name.lower() in device.name.lower():
target_device = device
break
if target_device is None:
raise DeviceNotFoundError(f"Device '{device_name}' not found on track {track_index}")
# Configure parameters
configured = []
failed = []
if hasattr(target_device, 'parameters'):
for param_name, param_value in params.items():
param_found = False
for param in target_device.parameters:
if param_name.lower() in param.name.lower():
try:
# Clamp value to parameter's min/max
min_val = param.min if hasattr(param, 'min') else 0
max_val = param.max if hasattr(param, 'max') else 1
clamped_value = max(min_val, min(max_val, param_value))
param.value = clamped_value
configured.append(f"{param.name} = {clamped_value}")
param_found = True
break
except Exception as pe:
failed.append(f"{param_name}: {str(pe)}")
if not param_found:
failed.append(f"{param_name}: parameter not found")
return self._create_result(
len(failed) == 0,
f"Configured {len(configured)} parameters on '{device_name}'",
{"configured": configured, "failed": failed}
)
except TrackNotFoundError as e:
return self._create_result(False, str(e))
except DeviceNotFoundError as e:
return self._create_result(False, str(e))
except Exception as e:
return self._create_result(False, f"Failed to configure device: {str(e)}")
def remove_device(self, track_index: int, device_name: str) -> Dict[str, Any]:
"""
Remove a device from a track.
Args:
track_index: Index of the target track
device_name: Name of the device to remove
Returns:
Result dictionary indicating success/failure
"""
try:
if track_index < 0 or track_index >= len(self.song.tracks):
raise TrackNotFoundError(f"Track index {track_index} out of range")
track = self.song.tracks[track_index]
# Find and delete device
if hasattr(track, 'devices'):
for i, device in enumerate(track.devices):
if device_name.lower() in device.name.lower():
# Delete the device
track.delete_device(i)
return self._create_result(
True,
f"Device '{device_name}' removed from track {track_index}"
)
return self._create_result(False, f"Device '{device_name}' not found on track {track_index}")
except TrackNotFoundError as e:
return self._create_result(False, str(e))
except Exception as e:
return self._create_result(False, f"Failed to remove device: {str(e)}")
# =========================================================================
# Mix Configuration Execution
# =========================================================================
def execute_mix_config(self, config: MixConfiguration) -> Dict[str, Any]:
"""
Apply a complete mix configuration to a track.
Args:
config: MixConfiguration object with settings
Returns:
Result dictionary indicating success/failure
"""
try:
results = []
# Apply volume
if config.volume is not None:
result = self.set_track_volume(config.track_index, config.volume)
results.append("volume" if result["success"] else f"volume: {result['message']}")
# Apply pan
if config.pan is not None:
result = self.set_track_pan(config.track_index, config.pan)
results.append("pan" if result["success"] else f"pan: {result['message']}")
# Apply mute
if config.mute is not None:
result = self._set_track_mute_internal(config.track_index, config.mute)
results.append("mute" if result["success"] else f"mute: {result['message']}")
# Apply solo
if config.solo is not None:
result = self._set_track_solo_internal(config.track_index, config.solo)
results.append("solo" if result["success"] else f"solo: {result['message']}")
# Apply sends
if config.sends:
for return_index, amount in config.sends.items():
result = self.set_track_send(config.track_index, return_index, amount)
results.append(f"send_{return_index}" if result["success"] else f"send_{return_index}: {result['message']}")
# Apply devices
if config.devices:
for device_config in config.devices:
device_name = device_config.get("name", "")
device_params = device_config.get("params", {})
# Insert device
insert_result = self.insert_device(config.track_index, device_name)
if insert_result["success"]:
# Configure device
configure_result = self.configure_device(
config.track_index, device_name, device_params
)
results.append(f"device_{device_name}" if configure_result["success"]
else f"device_{device_name}: {configure_result['message']}")
else:
results.append(f"device_{device_name}: {insert_result['message']}")
return self._create_result(
True,
f"Mix config applied to track {config.track_index}",
{"applied": results}
)
except Exception as e:
return self._create_result(False, f"Failed to execute mix config: {str(e)}")
def apply_eq_preset(self, track_index: int, preset_name: str) -> Dict[str, Any]:
"""
Apply an EQ Eight preset to a track.
Args:
track_index: Index of the target track
preset_name: Name of the EQ preset to apply
Returns:
Result dictionary indicating success/failure
"""
try:
# Define preset configurations
presets = {
"low_cut": {"hpf": 80, "ls_gain": 0},
"vocal_boost": {"hpf": 100, "mid_freq": 2500, "mid_gain": 3, "mid_q": 0.7},
"bass_enhance": {"ls_freq": 120, "ls_gain": 4, "hs_gain": -2},
"bright": {"hs_freq": 8000, "hs_gain": 3},
"scooped": {"ls_gain": -2, "mid_freq": 1000, "mid_gain": -3, "hs_gain": 2}
}
preset = presets.get(preset_name.lower(), {})
# Insert EQ Eight
insert_result = self.insert_device(track_index, "EQ Eight")
if not insert_result["success"]:
return insert_result
# Configure EQ parameters
eq_params = {}
if "hpf" in preset:
eq_params["highpass"] = preset["hpf"]
if "ls_freq" in preset:
eq_params["lowshelf freq"] = preset["ls_freq"]
if "ls_gain" in preset:
eq_params["lowshelf gain"] = preset["ls_gain"]
if "mid_freq" in preset:
eq_params["mid freq"] = preset["mid_freq"]
if "mid_gain" in preset:
eq_params["mid gain"] = preset["mid_gain"]
if "hs_freq" in preset:
eq_params["highshelf freq"] = preset["hs_freq"]
if "hs_gain" in preset:
eq_params["highshelf gain"] = preset["hs_gain"]
config_result = self.configure_device(track_index, "EQ Eight", eq_params)
return self._create_result(
config_result["success"],
f"EQ preset '{preset_name}' applied to track {track_index}",
config_result.get("data")
)
except Exception as e:
return self._create_result(False, f"Failed to apply EQ preset: {str(e)}")
def apply_compression(self, track_index: int, settings: CompressorSettings) -> Dict[str, Any]:
"""
Apply compressor settings to a track.
Args:
track_index: Index of the target track
settings: CompressorSettings object
Returns:
Result dictionary indicating success/failure
"""
try:
# Insert Compressor
insert_result = self.insert_device(track_index, "Compressor")
if not insert_result["success"]:
return insert_result
# Configure compressor parameters
comp_params = {
"threshold": settings.threshold,
"ratio": settings.ratio,
"attack": settings.attack,
"release": settings.release,
"makeup": settings.make_up
}
config_result = self.configure_device(track_index, "Compressor", comp_params)
return self._create_result(
config_result["success"],
f"Compression applied to track {track_index}",
{"settings": settings.__dict__}
)
except Exception as e:
return self._create_result(False, f"Failed to apply compression: {str(e)}")
def setup_sidechain(self, source_track: int, target_track: int,
amount: float = 0.5) -> Dict[str, Any]:
"""
Setup sidechain compression from source to target track.
Args:
source_track: Index of the trigger/source track (e.g., kick)
target_track: Index of the track to duck (e.g., bass)
amount: Sidechain amount (0.0 - 1.0)
Returns:
Result dictionary indicating success/failure
"""
try:
# Validate track indices
if source_track < 0 or source_track >= len(self.song.tracks):
raise TrackNotFoundError(f"Source track index {source_track} out of range")
if target_track < 0 or target_track >= len(self.song.tracks):
raise TrackNotFoundError(f"Target track index {target_track} out of range")
# Insert compressor on target track if not present
target = self.song.tracks[target_track]
has_compressor = False
compressor_device = None
if hasattr(target, 'devices'):
for device in target.devices:
if "compressor" in device.name.lower():
has_compressor = True
compressor_device = device
break
if not has_compressor:
insert_result = self.insert_device(target_track, "Compressor")
if not insert_result["success"]:
return insert_result
# Get the newly inserted compressor
if hasattr(target, 'devices'):
compressor_device = target.devices[-1]
# Configure sidechain routing
if compressor_device and hasattr(compressor_device, 'parameters'):
for param in compressor_device.parameters:
if "sidechain" in param.name.lower():
# Enable sidechain
param.value = 1 # or appropriate value for on
elif "sidechain source" in param.name.lower() or "input" in param.name.lower():
# Set sidechain input to source track
# This is Live-version dependent
pass
# Set threshold and ratio for ducking effect
sidechain_params = {
"threshold": -20.0,
"ratio": 4.0,
"attack": 0.01,
"release": 0.1
}
config_result = self.configure_device(target_track, "Compressor", sidechain_params)
return self._create_result(
True,
f"Sidechain setup from track {source_track} to track {target_track} (amount: {amount})"
)
except TrackNotFoundError as e:
return self._create_result(False, str(e))
except Exception as e:
return self._create_result(False, f"Failed to setup sidechain: {str(e)}")
# =========================================================================
# Arrangement Operations
# =========================================================================
def insert_arrangement_clip(self, track_index: int, file_path: str,
start_bar: float, duration: float) -> Dict[str, Any]:
"""
Insert an audio clip into the arrangement.
Args:
track_index: Index of the target audio track
file_path: Path to the audio file
start_bar: Start position in bars
duration: Duration in bars
Returns:
Result dictionary indicating success/failure
"""
try:
if track_index < 0 or track_index >= len(self.song.tracks):
raise TrackNotFoundError(f"Track index {track_index} out of range")
track = self.song.tracks[track_index]
# Verify file exists
if not os.path.exists(file_path):
return self._create_result(False, f"Audio file not found: {file_path}")
# Create clip at position
if hasattr(track, 'insert_clip'):
clip = track.insert_clip(file_path, start_bar, duration)
return self._create_result(
True,
f"Audio clip inserted at bar {start_bar} on track {track_index}",
{"clip": clip.name if hasattr(clip, 'name') else "unnamed"}
)
else:
# Alternative: use view or clip slots
if hasattr(self.song, 'view') and hasattr(self.song.view, 'detail_clip'):
# Method depends on Live version
pass
return self._create_result(
False,
"Arrangement clip insertion not available in this Live version"
)
except TrackNotFoundError as e:
return self._create_result(False, str(e))
except Exception as e:
return self._create_result(False, f"Failed to insert arrangement clip: {str(e)}")
def insert_arrangement_midi(self, track_index: int, start_bar: float,
duration: float, notes: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Insert a MIDI clip with notes into the arrangement.
Args:
track_index: Index of the target MIDI track
start_bar: Start position in bars
duration: Duration in bars
notes: List of note dictionaries with pitch, start_time, duration, velocity
Returns:
Result dictionary indicating success/failure
"""
try:
if track_index < 0 or track_index >= len(self.song.tracks):
raise TrackNotFoundError(f"Track index {track_index} out of range")
track = self.song.tracks[track_index]
# Create MIDI clip
if hasattr(track, 'create_clip'):
clip = track.create_clip(start_bar, duration)
# Add notes
if hasattr(clip, 'notes') and hasattr(clip.notes, 'add'):
for note in notes:
clip.notes.add(
note["pitch"],
note.get("start_time", 0),
note.get("duration", 0.25),
note.get("velocity", 100),
False # not muted
)
return self._create_result(
True,
f"MIDI clip inserted at bar {start_bar} on track {track_index} with {len(notes)} notes"
)
else:
return self._create_result(
False,
"MIDI clip creation not available in this Live version"
)
except TrackNotFoundError as e:
return self._create_result(False, str(e))
except Exception as e:
return self._create_result(False, f"Failed to insert MIDI clip: {str(e)}")
def add_automation(self, track_index: int, clip_index: int,
parameter: str, points: List[Tuple[float, float]]) -> Dict[str, Any]:
"""
Add automation envelope points to a clip.
Args:
track_index: Index of the target track
clip_index: Index of the clip
parameter: Name of the parameter to automate
points: List of (time, value) tuples
Returns:
Result dictionary indicating success/failure
"""
try:
if track_index < 0 or track_index >= len(self.song.tracks):
raise TrackNotFoundError(f"Track index {track_index} out of range")
track = self.song.tracks[track_index]
if not hasattr(track, 'clips') or clip_index >= len(track.clips):
return self._create_result(False, f"Clip index {clip_index} out of range")
clip = track.clips[clip_index]
# Find parameter to automate
target_param = None
if hasattr(clip, 'parameters'):
for param in clip.parameters:
if parameter.lower() in param.name.lower():
target_param = param
break
if target_param is None:
return self._create_result(False, f"Parameter '{parameter}' not found")
# Add automation points
if hasattr(target_param, 'automation'):
automation = target_param.automation
for time, value in points:
automation.insert_step(time, value, 0) # 0 = linear interpolation
return self._create_result(
True,
f"Added {len(points)} automation points to '{parameter}' in clip {clip_index}"
)
else:
return self._create_result(False, "Automation not available for this parameter")
except TrackNotFoundError as e:
return self._create_result(False, str(e))
except Exception as e:
return self._create_result(False, f"Failed to add automation: {str(e)}")
# =========================================================================
# Track Management
# =========================================================================
def create_midi_track(self, index: int = -1) -> Dict[str, Any]:
"""
Create a new MIDI track.
Args:
index: Position to insert track (-1 for end)
Returns:
Result dictionary with track index if successful
"""
try:
self.song.create_midi_track(index)
track_index = index if index >= 0 else len(self.song.tracks) - 1
return self._create_result(
True,
f"MIDI track created at index {track_index}",
{"track_index": track_index, "type": "midi"}
)
except Exception as e:
return self._create_result(False, f"Failed to create MIDI track: {str(e)}")
def create_audio_track(self, index: int = -1) -> Dict[str, Any]:
"""
Create a new audio track.
Args:
index: Position to insert track (-1 for end)
Returns:
Result dictionary with track index if successful
"""
try:
self.song.create_audio_track(index)
track_index = index if index >= 0 else len(self.song.tracks) - 1
return self._create_result(
True,
f"Audio track created at index {track_index}",
{"track_index": track_index, "type": "audio"}
)
except Exception as e:
return self._create_result(False, f"Failed to create audio track: {str(e)}")
def set_track_name(self, track_index: int, name: str) -> Dict[str, Any]:
"""
Set the name of a track.
Args:
track_index: Index of the track
name: New name for the track
Returns:
Result dictionary indicating success/failure
"""
try:
if track_index < 0 or track_index >= len(self.song.tracks):
raise TrackNotFoundError(f"Track index {track_index} out of range")
track = self.song.tracks[track_index]
old_name = track.name if hasattr(track, 'name') else "unnamed"
track.name = name
return self._create_result(
True,
f"Track {track_index} renamed from '{old_name}' to '{name}'"
)
except TrackNotFoundError as e:
return self._create_result(False, str(e))
except Exception as e:
return self._create_result(False, f"Failed to set track name: {str(e)}")
def set_track_volume(self, track_index: int, volume: float) -> Dict[str, Any]:
"""
Set the volume of a track.
Args:
track_index: Index of the track
volume: Volume level (0.0 - 1.0, or dB scale)
Returns:
Result dictionary indicating success/failure
"""
try:
if track_index < 0 or track_index >= len(self.song.tracks):
raise TrackNotFoundError(f"Track index {track_index} out of range")
track = self.song.tracks[track_index]
if hasattr(track, 'mixer_device') and hasattr(track.mixer_device, 'volume'):
# Clamp to valid range (0.0 to 1.0 for Live's internal scale)
clamped_volume = max(0.0, min(1.0, volume))
track.mixer_device.volume.value = clamped_volume
return self._create_result(
True,
f"Track {track_index} volume set to {clamped_volume:.2f}"
)
else:
return self._create_result(False, "Track doesn't have volume control")
except TrackNotFoundError as e:
return self._create_result(False, str(e))
except Exception as e:
return self._create_result(False, f"Failed to set track volume: {str(e)}")
def set_track_pan(self, track_index: int, pan: float) -> Dict[str, Any]:
"""
Set the pan of a track.
Args:
track_index: Index of the track
pan: Pan position (-1.0 left to 1.0 right, 0.0 center)
Returns:
Result dictionary indicating success/failure
"""
try:
if track_index < 0 or track_index >= len(self.song.tracks):
raise TrackNotFoundError(f"Track index {track_index} out of range")
track = self.song.tracks[track_index]
if hasattr(track, 'mixer_device') and hasattr(track.mixer_device, 'panning'):
# Clamp to valid range (-1.0 to 1.0)
clamped_pan = max(-1.0, min(1.0, pan))
track.mixer_device.panning.value = clamped_pan
return self._create_result(
True,
f"Track {track_index} pan set to {clamped_pan:.2f}"
)
else:
return self._create_result(False, "Track doesn't have pan control")
except TrackNotFoundError as e:
return self._create_result(False, str(e))
except Exception as e:
return self._create_result(False, f"Failed to set track pan: {str(e)}")
def _set_track_mute_internal(self, track_index: int, mute: bool) -> Dict[str, Any]:
"""Internal method to set track mute state."""
try:
track = self.song.tracks[track_index]
if hasattr(track, 'mute'):
track.mute = mute
return self._create_result(True, f"Track {track_index} mute set to {mute}")
else:
return self._create_result(False, "Track doesn't support mute")
except Exception as e:
return self._create_result(False, str(e))
def _set_track_solo_internal(self, track_index: int, solo: bool) -> Dict[str, Any]:
"""Internal method to set track solo state."""
try:
track = self.song.tracks[track_index]
if hasattr(track, 'solo'):
track.solo = solo
return self._create_result(True, f"Track {track_index} solo set to {solo}")
else:
return self._create_result(False, "Track doesn't support solo")
except Exception as e:
return self._create_result(False, str(e))
# =========================================================================
# Playback Control
# =========================================================================
def start_playback(self) -> Dict[str, Any]:
"""
Start playback.
Returns:
Result dictionary indicating success/failure
"""
try:
if hasattr(self.song, 'start_playing'):
self.song.start_playing()
return self._create_result(True, "Playback started")
elif hasattr(self.song, 'is_playing'):
# Alternative method
self.song.is_playing = True
return self._create_result(True, "Playback started")
else:
return self._create_result(False, "Playback control not available")
except Exception as e:
return self._create_result(False, f"Failed to start playback: {str(e)}")
def stop_playback(self) -> Dict[str, Any]:
"""
Stop playback.
Returns:
Result dictionary indicating success/failure
"""
try:
if hasattr(self.song, 'stop_playing'):
self.song.stop_playing()
return self._create_result(True, "Playback stopped")
elif hasattr(self.song, 'is_playing'):
self.song.is_playing = False
return self._create_result(True, "Playback stopped")
else:
return self._create_result(False, "Playback control not available")
except Exception as e:
return self._create_result(False, f"Failed to stop playback: {str(e)}")
def set_tempo(self, bpm: float) -> Dict[str, Any]:
"""
Set the project tempo.
Args:
bpm: Tempo in beats per minute
Returns:
Result dictionary indicating success/failure
"""
try:
if hasattr(self.song, 'tempo'):
# Clamp to reasonable range
clamped_bpm = max(20.0, min(999.0, bpm))
self.song.tempo = clamped_bpm
return self._create_result(True, f"Tempo set to {clamped_bpm:.1f} BPM")
else:
return self._create_result(False, "Tempo control not available")
except Exception as e:
return self._create_result(False, f"Failed to set tempo: {str(e)}")
def set_playhead(self, bar: float) -> Dict[str, Any]:
"""
Set the playhead position.
Args:
bar: Position in bars (can include fractional bars)
Returns:
Result dictionary indicating success/failure
"""
try:
if hasattr(self.song, 'current_song_time'):
# Convert bars to seconds based on tempo
beats_per_bar = self.song.signature_numerator if hasattr(self.song, 'signature_numerator') else 4
seconds_per_beat = 60.0 / self.song.tempo
seconds = bar * beats_per_bar * seconds_per_beat
self.song.current_song_time = seconds
return self._create_result(True, f"Playhead set to bar {bar}")
else:
return self._create_result(False, "Playhead control not available")
except Exception as e:
return self._create_result(False, f"Failed to set playhead: {str(e)}")