690 lines
24 KiB
Python
690 lines
24 KiB
Python
"""
|
|
Advanced Automation Engine for AbletonMCP_AI
|
|
|
|
Creates professional automation curves including:
|
|
- Filter sweeps (build-ups, breakdowns)
|
|
- Sidechain compression curves
|
|
- Send automation (reverb/delay)
|
|
- Volume ramps and fades
|
|
- Complete build-up automation packages
|
|
|
|
Author: Agent 6 - Advanced Automation Specialist
|
|
"""
|
|
|
|
import math
|
|
from typing import Dict, List, Tuple, Optional, Any
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
|
|
|
|
class CurveType(Enum):
|
|
"""Types of automation curves."""
|
|
LINEAR = "linear"
|
|
EXPONENTIAL = "exponential"
|
|
LOGARITHMIC = "logarithmic"
|
|
S_CURVE = "s_curve"
|
|
BEZIER = "bezier"
|
|
STEP = "step"
|
|
|
|
|
|
@dataclass
|
|
class AutomationPoint:
|
|
"""Represents a single automation point."""
|
|
time: float # In beats (can be fractional for precise positioning)
|
|
value: float
|
|
curve_type: str = "linear" # "linear", "bezier", "s_curve"
|
|
|
|
|
|
class AdvancedAutomation:
|
|
"""
|
|
Advanced automation engine for creating professional curves.
|
|
|
|
Handles:
|
|
- Filter sweeps (exponential/logarithmic)
|
|
- Sidechain ducking curves
|
|
- Send automation (reverb/delay)
|
|
- Volume ramps
|
|
- Build-up automation packages
|
|
"""
|
|
|
|
def __init__(self, live_bridge):
|
|
"""
|
|
Initialize with LiveBridge for direct Ableton API access.
|
|
|
|
Args:
|
|
live_bridge: LiveBridge instance for executing automation commands
|
|
"""
|
|
self.live_bridge = live_bridge
|
|
self._automation_history = []
|
|
|
|
def _generate_curve_points(
|
|
self,
|
|
start_val: float,
|
|
end_val: float,
|
|
start_time: float,
|
|
end_time: float,
|
|
num_points: int = 16,
|
|
curve_type: str = "linear"
|
|
) -> List[AutomationPoint]:
|
|
"""
|
|
Generate automation points following a curve.
|
|
|
|
Args:
|
|
start_val: Starting value
|
|
end_val: Ending value
|
|
start_time: Starting time in beats
|
|
end_time: Ending time in beats
|
|
num_points: Number of control points to generate
|
|
curve_type: Type of curve (linear, exponential, logarithmic, s_curve)
|
|
|
|
Returns:
|
|
List of AutomationPoint objects
|
|
"""
|
|
points = []
|
|
duration = end_time - start_time
|
|
|
|
for i in range(num_points + 1):
|
|
t = i / num_points # Normalized time 0-1
|
|
|
|
# Apply curve function
|
|
if curve_type == "linear":
|
|
factor = t
|
|
elif curve_type == "exponential":
|
|
# Exponential curve: starts slow, ends fast
|
|
factor = t ** 2 if start_val < end_val else 1 - (1 - t) ** 2
|
|
if start_val > end_val:
|
|
factor = 1 - (1 - t) ** 2
|
|
else:
|
|
factor = t ** 2
|
|
elif curve_type == "logarithmic":
|
|
# Logarithmic curve: starts fast, ends slow
|
|
factor = math.sqrt(t) if start_val < end_val else 1 - math.sqrt(1 - t)
|
|
if start_val > end_val:
|
|
factor = 1 - math.sqrt(1 - t)
|
|
else:
|
|
factor = math.sqrt(t)
|
|
elif curve_type == "s_curve":
|
|
# S-curve: smooth ease-in-out
|
|
factor = t ** 2 * (3 - 2 * t)
|
|
elif curve_type == "bezier":
|
|
# Cubic bezier approximation
|
|
factor = t ** 3 * (10 - 15 * t + 6 * t ** 2)
|
|
else:
|
|
factor = t
|
|
|
|
# Calculate value
|
|
if curve_type == "exponential" and start_val < end_val:
|
|
# For exponential upward sweeps, use proper log scale
|
|
if start_val > 0 and end_val > 0:
|
|
log_start = math.log10(start_val)
|
|
log_end = math.log10(end_val)
|
|
value = 10 ** (log_start + factor * (log_end - log_start))
|
|
else:
|
|
value = start_val + factor * (end_val - start_val)
|
|
else:
|
|
value = start_val + factor * (end_val - start_val)
|
|
|
|
time = start_time + t * duration
|
|
points.append(AutomationPoint(time=time, value=value, curve_type=curve_type))
|
|
|
|
return points
|
|
|
|
def create_filter_sweep(
|
|
self,
|
|
track_index: int,
|
|
start_bar: float,
|
|
end_bar: float,
|
|
start_freq: float = 200,
|
|
end_freq: float = 20000,
|
|
curve_type: str = "exponential",
|
|
filter_device: str = "Auto Filter",
|
|
parameter_name: str = "Frequency"
|
|
) -> Dict:
|
|
"""
|
|
Create a filter sweep automation curve.
|
|
|
|
Args:
|
|
track_index: Target track index
|
|
start_bar: Starting bar position
|
|
end_bar: Ending bar position
|
|
start_freq: Starting frequency in Hz (default 200)
|
|
end_freq: Ending frequency in Hz (default 20000)
|
|
curve_type: Curve type - "linear", "exponential", "logarithmic", "s_curve"
|
|
filter_device: Name of filter device (default "Auto Filter")
|
|
parameter_name: Parameter to automate (default "Frequency")
|
|
|
|
Returns:
|
|
Dict with automation details and status
|
|
"""
|
|
try:
|
|
# Convert bars to beats
|
|
start_beats = start_bar * 4
|
|
end_beats = end_bar * 4
|
|
|
|
# Generate exponential curve points for natural filter sweep
|
|
points = self._generate_curve_points(
|
|
start_val=start_freq,
|
|
end_val=end_freq,
|
|
start_time=start_beats,
|
|
end_time=end_beats,
|
|
num_points=24, # More points for smooth sweep
|
|
curve_type=curve_type
|
|
)
|
|
|
|
# Convert to format expected by LiveBridge
|
|
automation_points = [[p.time, p.value] for p in points]
|
|
|
|
# Apply automation via LiveBridge
|
|
result = self.live_bridge.add_automation(
|
|
track_index=track_index,
|
|
parameter_name=parameter_name,
|
|
points=automation_points,
|
|
device_name=filter_device
|
|
)
|
|
|
|
sweep_data = {
|
|
"track_index": track_index,
|
|
"device": filter_device,
|
|
"parameter": parameter_name,
|
|
"start_bar": start_bar,
|
|
"end_bar": end_bar,
|
|
"start_freq": start_freq,
|
|
"end_freq": end_freq,
|
|
"curve_type": curve_type,
|
|
"point_count": len(points),
|
|
"status": "success" if result else "failed"
|
|
}
|
|
|
|
self._automation_history.append(sweep_data)
|
|
return sweep_data
|
|
|
|
except Exception as e:
|
|
return {
|
|
"track_index": track_index,
|
|
"status": "error",
|
|
"error": str(e)
|
|
}
|
|
|
|
def create_sidechain_curve(
|
|
self,
|
|
track_index: int,
|
|
ducking_amount: float = 0.7,
|
|
attack_ms: float = 5,
|
|
release_ms: float = 100,
|
|
every_n_bars: float = 1.0,
|
|
total_bars: float = 16.0
|
|
) -> Dict:
|
|
"""
|
|
Create sidechain compression ducking curves synced to the beat.
|
|
|
|
Args:
|
|
track_index: Target track index (bass/synths)
|
|
ducking_amount: Amount of ducking 0.0-1.0 (0.7 = -12dB reduction)
|
|
attack_ms: Attack time in milliseconds (default 5)
|
|
release_ms: Release time in milliseconds (default 100)
|
|
every_n_bars: Duck every N bars (default 1.0 = every bar)
|
|
total_bars: Total duration in bars (default 16)
|
|
|
|
Returns:
|
|
Dict with automation details and status
|
|
"""
|
|
try:
|
|
# Calculate BPM-dependent times
|
|
bpm = 95 # Assume default, could be fetched from Live
|
|
ms_per_beat = 60000 / bpm
|
|
|
|
# Convert ms to beats
|
|
attack_beats = attack_ms / ms_per_beat
|
|
release_beats = release_ms / ms_per_beat
|
|
|
|
# Ducking value (1.0 = no ducking, lower = more reduction)
|
|
duck_val = 1.0 - ducking_amount
|
|
|
|
points = []
|
|
bar = 0.0
|
|
|
|
while bar < total_bars:
|
|
bar_start = bar * 4 # Convert to beats
|
|
|
|
# Start of duck (on the beat)
|
|
points.append([bar_start, 1.0])
|
|
|
|
# Attack phase (quick drop)
|
|
points.append([bar_start + attack_beats, duck_val])
|
|
|
|
# Sustain (hold duck)
|
|
sustain_end = bar_start + (2 * 4 * every_n_bars) / 3 # 2/3 through
|
|
points.append([sustain_end, duck_val])
|
|
|
|
# Release phase (recover)
|
|
bar_end = (bar + every_n_bars) * 4
|
|
points.append([bar_end, 1.0])
|
|
|
|
bar += every_n_bars
|
|
|
|
# Apply volume automation via LiveBridge
|
|
result = self.live_bridge.add_automation(
|
|
track_index=track_index,
|
|
parameter_name="volume",
|
|
points=points
|
|
)
|
|
|
|
sidechain_data = {
|
|
"track_index": track_index,
|
|
"ducking_amount": ducking_amount,
|
|
"attack_ms": attack_ms,
|
|
"release_ms": release_ms,
|
|
"every_n_bars": every_n_bars,
|
|
"total_bars": total_bars,
|
|
"point_count": len(points),
|
|
"status": "success" if result else "failed"
|
|
}
|
|
|
|
self._automation_history.append(sidechain_data)
|
|
return sidechain_data
|
|
|
|
except Exception as e:
|
|
return {
|
|
"track_index": track_index,
|
|
"status": "error",
|
|
"error": str(e)
|
|
}
|
|
|
|
def create_send_automation(
|
|
self,
|
|
track_index: int,
|
|
send_index: int,
|
|
points: List[Tuple[float, float]],
|
|
curve_type: str = "linear"
|
|
) -> Dict:
|
|
"""
|
|
Create send level automation (reverb/delay sends).
|
|
|
|
Args:
|
|
track_index: Target track index
|
|
send_index: Index of the return track (0 for A, 1 for B, etc.)
|
|
points: List of (bar_position, send_amount) tuples
|
|
Send amount should be 0.0-1.0
|
|
curve_type: Interpolation curve type
|
|
|
|
Returns:
|
|
Dict with automation details and status
|
|
"""
|
|
try:
|
|
# Convert bar positions to beats
|
|
automation_points = []
|
|
for bar_pos, amount in points:
|
|
beats = bar_pos * 4
|
|
automation_points.append([beats, amount])
|
|
|
|
# Apply send automation via LiveBridge
|
|
result = self.live_bridge.add_automation(
|
|
track_index=track_index,
|
|
parameter_name="send",
|
|
points=automation_points,
|
|
send_index=send_index
|
|
)
|
|
|
|
send_data = {
|
|
"track_index": track_index,
|
|
"send_index": send_index,
|
|
"send_letter": chr(ord('A') + send_index),
|
|
"point_count": len(points),
|
|
"curve_type": curve_type,
|
|
"status": "success" if result else "failed"
|
|
}
|
|
|
|
self._automation_history.append(send_data)
|
|
return send_data
|
|
|
|
except Exception as e:
|
|
return {
|
|
"track_index": track_index,
|
|
"send_index": send_index,
|
|
"status": "error",
|
|
"error": str(e)
|
|
}
|
|
|
|
def create_volume_ramp(
|
|
self,
|
|
track_index: int,
|
|
start_bar: float,
|
|
end_bar: float,
|
|
start_vol: float,
|
|
end_vol: float,
|
|
curve_type: str = "linear"
|
|
) -> Dict:
|
|
"""
|
|
Create a volume ramp/fade automation.
|
|
|
|
Args:
|
|
track_index: Target track index
|
|
start_bar: Starting bar position
|
|
end_bar: Ending bar position
|
|
start_vol: Starting volume (0.0-1.0, where 1.0 = 0dB)
|
|
end_vol: Ending volume (0.0-1.0)
|
|
curve_type: Curve type for the ramp
|
|
|
|
Returns:
|
|
Dict with automation details and status
|
|
"""
|
|
try:
|
|
start_beats = start_bar * 4
|
|
end_beats = end_bar * 4
|
|
|
|
# Generate curve points
|
|
points = self._generate_curve_points(
|
|
start_val=start_vol,
|
|
end_val=end_vol,
|
|
start_time=start_beats,
|
|
end_time=end_beats,
|
|
num_points=16,
|
|
curve_type=curve_type
|
|
)
|
|
|
|
automation_points = [[p.time, p.value] for p in points]
|
|
|
|
# Apply volume automation
|
|
result = self.live_bridge.add_automation(
|
|
track_index=track_index,
|
|
parameter_name="volume",
|
|
points=automation_points
|
|
)
|
|
|
|
ramp_data = {
|
|
"track_index": track_index,
|
|
"start_bar": start_bar,
|
|
"end_bar": end_bar,
|
|
"start_vol": start_vol,
|
|
"end_vol": end_vol,
|
|
"curve_type": curve_type,
|
|
"point_count": len(points),
|
|
"status": "success" if result else "failed"
|
|
}
|
|
|
|
self._automation_history.append(ramp_data)
|
|
return ramp_data
|
|
|
|
except Exception as e:
|
|
return {
|
|
"track_index": track_index,
|
|
"status": "error",
|
|
"error": str(e)
|
|
}
|
|
|
|
def add_build_up_automation(
|
|
self,
|
|
track_indices: List[int],
|
|
build_start: float,
|
|
drop_position: float,
|
|
include_filter: bool = True,
|
|
filter_end_freq: float = 15000,
|
|
include_volume: bool = True,
|
|
volume_boost: float = 0.1,
|
|
include_reverb_send: bool = True,
|
|
reverb_send_final: float = 0.6
|
|
) -> Dict:
|
|
"""
|
|
Create a complete build-up automation package.
|
|
|
|
Includes:
|
|
- Filter sweeps (opening up to drop)
|
|
- Volume ramps (slight boost leading to drop)
|
|
- Reverb send increases (wash before drop)
|
|
|
|
Args:
|
|
track_indices: List of track indices to apply automation to
|
|
build_start: Starting bar of build section
|
|
drop_position: Bar position where drop hits
|
|
include_filter: Add filter sweep (default True)
|
|
filter_end_freq: Filter frequency at drop (default 15000)
|
|
include_volume: Add volume automation (default True)
|
|
volume_boost: Volume increase during build (default 0.1)
|
|
include_reverb_send: Add reverb send automation (default True)
|
|
reverb_send_final: Reverb send amount at drop (default 0.6)
|
|
|
|
Returns:
|
|
Dict with complete build-up automation details
|
|
"""
|
|
results = {
|
|
"track_count": len(track_indices),
|
|
"build_start": build_start,
|
|
"drop_position": drop_position,
|
|
"track_automations": [],
|
|
"status": "success"
|
|
}
|
|
|
|
try:
|
|
for track_index in track_indices:
|
|
track_results = {
|
|
"track_index": track_index,
|
|
"automations": []
|
|
}
|
|
|
|
# 1. Filter sweep (opening up)
|
|
if include_filter:
|
|
filter_result = self.create_filter_sweep(
|
|
track_index=track_index,
|
|
start_bar=build_start,
|
|
end_bar=drop_position,
|
|
start_freq=400,
|
|
end_freq=filter_end_freq,
|
|
curve_type="exponential"
|
|
)
|
|
track_results["automations"].append({
|
|
"type": "filter_sweep",
|
|
"result": filter_result
|
|
})
|
|
|
|
# 2. Volume ramp (slight boost)
|
|
if include_volume:
|
|
vol_result = self.create_volume_ramp(
|
|
track_index=track_index,
|
|
start_bar=build_start,
|
|
end_bar=drop_position - 0.5, # Peak just before drop
|
|
start_vol=0.85,
|
|
end_vol=0.85 + volume_boost,
|
|
curve_type="s_curve"
|
|
)
|
|
track_results["automations"].append({
|
|
"type": "volume_ramp",
|
|
"result": vol_result
|
|
})
|
|
|
|
# Drop spike then normalize
|
|
spike_result = self.create_volume_ramp(
|
|
track_index=track_index,
|
|
start_bar=drop_position - 0.25,
|
|
end_bar=drop_position + 0.25,
|
|
start_vol=0.85 + volume_boost,
|
|
end_vol=0.85,
|
|
curve_type="linear"
|
|
)
|
|
track_results["automations"].append({
|
|
"type": "volume_spike",
|
|
"result": spike_result
|
|
})
|
|
|
|
# 3. Reverb send increase
|
|
if include_reverb_send:
|
|
# Reverb wash building up
|
|
reverb_points = [
|
|
(build_start, 0.1),
|
|
(build_start + (drop_position - build_start) * 0.5, 0.3),
|
|
(drop_position - 0.5, reverb_send_final),
|
|
(drop_position, 0.15) # Drop reverb at impact
|
|
]
|
|
|
|
reverb_result = self.create_send_automation(
|
|
track_index=track_index,
|
|
send_index=0, # Return A (Reverb)
|
|
points=reverb_points,
|
|
curve_type="exponential"
|
|
)
|
|
track_results["automations"].append({
|
|
"type": "reverb_send",
|
|
"result": reverb_result
|
|
})
|
|
|
|
results["track_automations"].append(track_results)
|
|
|
|
return results
|
|
|
|
except Exception as e:
|
|
results["status"] = "error"
|
|
results["error"] = str(e)
|
|
return results
|
|
|
|
def create_drop_impact(
|
|
self,
|
|
track_index: int,
|
|
drop_bar: float,
|
|
impact_duration: float = 0.5,
|
|
impact_boost_db: float = 3.0
|
|
) -> Dict:
|
|
"""
|
|
Create a volume spike automation for drop impact.
|
|
|
|
Args:
|
|
track_index: Target track index
|
|
drop_bar: Bar position of the drop
|
|
impact_duration: Duration of impact spike in bars (default 0.5)
|
|
impact_boost_db: Boost in dB (default 3.0)
|
|
|
|
Returns:
|
|
Dict with impact automation details
|
|
"""
|
|
try:
|
|
# Convert dB boost to linear
|
|
impact_boost_linear = 10 ** (impact_boost_db / 20)
|
|
|
|
# Create impact curve
|
|
base_vol = 0.85
|
|
peak_vol = min(base_vol * impact_boost_linear, 1.0)
|
|
|
|
impact_start = drop_bar
|
|
impact_peak = drop_bar + 0.125 # Peak at 1/8th note
|
|
impact_end = drop_bar + impact_duration
|
|
|
|
points = [
|
|
[impact_start * 4, base_vol],
|
|
[impact_peak * 4, peak_vol],
|
|
[impact_end * 4, base_vol]
|
|
]
|
|
|
|
result = self.live_bridge.add_automation(
|
|
track_index=track_index,
|
|
parameter_name="volume",
|
|
points=points
|
|
)
|
|
|
|
return {
|
|
"track_index": track_index,
|
|
"drop_bar": drop_bar,
|
|
"impact_duration": impact_duration,
|
|
"impact_boost_db": impact_boost_db,
|
|
"peak_vol": peak_vol,
|
|
"status": "success" if result else "failed"
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"track_index": track_index,
|
|
"status": "error",
|
|
"error": str(e)
|
|
}
|
|
|
|
def create_breakdown_automation(
|
|
self,
|
|
track_indices: List[int],
|
|
breakdown_start: float,
|
|
breakdown_end: float,
|
|
reverb_increase: float = 0.5
|
|
) -> Dict:
|
|
"""
|
|
Create automation for breakdown sections (reverb swells, filter).
|
|
|
|
Args:
|
|
track_indices: List of track indices
|
|
breakdown_start: Starting bar of breakdown
|
|
breakdown_end: Ending bar of breakdown
|
|
reverb_increase: Amount to increase reverb sends (default 0.5)
|
|
|
|
Returns:
|
|
Dict with breakdown automation details
|
|
"""
|
|
results = {
|
|
"section": "breakdown",
|
|
"tracks": [],
|
|
"status": "success"
|
|
}
|
|
|
|
try:
|
|
for track_index in track_indices:
|
|
track_data = {"track_index": track_index, "automations": []}
|
|
|
|
# Filter closing for breakdown
|
|
filter_close = self.create_filter_sweep(
|
|
track_index=track_index,
|
|
start_bar=breakdown_start,
|
|
end_bar=breakdown_start + 2,
|
|
start_freq=20000,
|
|
end_freq=800,
|
|
curve_type="exponential"
|
|
)
|
|
track_data["automations"].append({
|
|
"type": "filter_close",
|
|
"result": filter_close
|
|
})
|
|
|
|
# Reverb swell
|
|
mid_point = (breakdown_start + breakdown_end) / 2
|
|
reverb_points = [
|
|
(breakdown_start, 0.1),
|
|
(mid_point, 0.1 + reverb_increase),
|
|
(breakdown_end - 1, 0.1 + reverb_increase),
|
|
(breakdown_end, 0.1)
|
|
]
|
|
|
|
reverb_result = self.create_send_automation(
|
|
track_index=track_index,
|
|
send_index=0,
|
|
points=reverb_points,
|
|
curve_type="s_curve"
|
|
)
|
|
track_data["automations"].append({
|
|
"type": "reverb_swell",
|
|
"result": reverb_result
|
|
})
|
|
|
|
# Filter reopen at end
|
|
filter_open = self.create_filter_sweep(
|
|
track_index=track_index,
|
|
start_bar=breakdown_end - 2,
|
|
end_bar=breakdown_end,
|
|
start_freq=800,
|
|
end_freq=20000,
|
|
curve_type="exponential"
|
|
)
|
|
track_data["automations"].append({
|
|
"type": "filter_reopen",
|
|
"result": filter_open
|
|
})
|
|
|
|
results["tracks"].append(track_data)
|
|
|
|
return results
|
|
|
|
except Exception as e:
|
|
results["status"] = "error"
|
|
results["error"] = str(e)
|
|
return results
|
|
|
|
def get_automation_history(self) -> List[Dict]:
|
|
"""Return history of all automation operations."""
|
|
return self._automation_history.copy()
|
|
|
|
def clear_history(self):
|
|
"""Clear automation history."""
|
|
self._automation_history = []
|