From dac7ec2a5ae112d0a65f790953b09a1f3f59b781 Mon Sep 17 00:00:00 2001 From: Administrator Date: Mon, 13 Apr 2026 16:06:53 -0300 Subject: [PATCH] Add duplicate_clip command + fix get_notes signature for audio clips --- AbletonMCP_AI/__init__.py | 90 ++++++++++++++++++++++++++++++ AbletonMCP_AI/mcp_server/server.py | 25 +++++++++ 2 files changed, 115 insertions(+) diff --git a/AbletonMCP_AI/__init__.py b/AbletonMCP_AI/__init__.py index 2682679..290075b 100644 --- a/AbletonMCP_AI/__init__.py +++ b/AbletonMCP_AI/__init__.py @@ -1559,6 +1559,96 @@ class _AbletonMCP(ControlSurface): raise Exception("Failed to load sample: %s" % str(e)) return {"loaded": False} + def _cmd_duplicate_clip(self, source_track, source_clip, target_track, target_clip, **kw): + """Duplicate/clone a clip from one slot to another in Session View. + + Args: + source_track: Source track index + source_clip: Source clip slot index + target_track: Target track index (can be same as source) + target_clip: Target clip slot index + """ + try: + src_track_idx = int(source_track) + src_clip_idx = int(source_clip) + tgt_track_idx = int(target_track) + tgt_clip_idx = int(target_clip) + + src_track = self._song.tracks[src_track_idx] + src_slot = src_track.clip_slots[src_clip_idx] + + if not src_slot.has_clip: + return {"duplicated": False, "error": "Source slot has no clip"} + + src_clip = src_slot.clip + tgt_track = self._song.tracks[tgt_track_idx] + tgt_slot = tgt_track.clip_slots[tgt_clip_idx] + + # Clear target if occupied + if tgt_slot.has_clip: + tgt_slot.delete_clip() + + # Detect clip type: try MIDI first, fallback to audio + is_midi = False + notes = [] + try: + # MIDI clips have get_notes with 4 required params + notes_data = src_clip.get_notes(0, 0, src_clip.length if hasattr(src_clip, "length") else 4.0, 128) + notes = list(notes_data) + is_midi = True + except Exception: + is_midi = False # It's an audio clip + + # Duplicate based on clip type + if is_midi and notes: + # MIDI clip - copy notes + result = self._cmd_generate_midi_clip( + tgt_track_idx, tgt_clip_idx, + notes=[{ + "pitch": n[0], "start_time": n[1], + "duration": n[2], "velocity": n[3], "mute": n[4] + } for n in notes] + ) + # Copy name + if result.get("created") and tgt_slot.has_clip: + tgt_slot.clip.name = src_clip.name + " (copy)" + return {"duplicated": True, "type": "midi", "result": result} + else: + # Audio clip - copy file reference and properties + # Get the file path from the source + file_path = None + + # Try multiple methods to get the file path + if hasattr(src_clip, "file_path"): + file_path = src_clip.file_path + elif hasattr(src_clip, "sample"): + sample = src_clip.sample + if hasattr(sample, "file_path"): + file_path = sample.file_path + + # Alternative: try to get from audio clip properties + if not file_path and hasattr(src_clip, "external_device"): + ext = src_clip.external_device + if hasattr(ext, "file_path"): + file_path = ext.file_path + + if file_path: + result = self._cmd_load_sample_to_clip( + tgt_track_idx, tgt_clip_idx, file_path + ) + # Copy warp settings + if result.get("loaded") and tgt_slot.has_clip: + tgt_slot.clip.name = src_clip.name + " (copy)" + if hasattr(src_clip, "warping") and hasattr(tgt_slot.clip, "warping"): + tgt_slot.clip.warping = src_clip.warping + return {"duplicated": True, "type": "audio", "result": result} + else: + return {"duplicated": False, "error": "Could not get audio file path from source clip"} + + except Exception as e: + self.log_message("Error duplicating clip: %s" % str(e)) + return {"duplicated": False, "error": str(e)} + def _cmd_load_sample_to_drum_rack_pad(self, track_index, pad_note, sample_path, **kw): """T012: Load a sample into a specific Drum Rack pad (MIDI note).""" import os diff --git a/AbletonMCP_AI/mcp_server/server.py b/AbletonMCP_AI/mcp_server/server.py index 40cd519..cd59e74 100644 --- a/AbletonMCP_AI/mcp_server/server.py +++ b/AbletonMCP_AI/mcp_server/server.py @@ -78,6 +78,7 @@ TIMEOUTS = { "generate_complete_reggaeton": 60.0, "generate_from_reference": 60.0, "load_sample_to_clip": 15.0, + "duplicate_clip": 15.0, "create_arrangement_audio_clip": 20.0, "set_warp_markers": 15.0, "reverse_clip": 10.0, @@ -1123,6 +1124,29 @@ def load_sample_to_clip(ctx: Context, track_index: int, clip_index: int, sample_ return _ok(resp) if resp.get("status") == "success" else _err(resp.get("message")) +@mcp.tool() +def duplicate_clip(ctx: Context, source_track: int, source_clip: int, + target_track: int, target_clip: int) -> str: + """Duplicate/clone a clip from one Session View slot to another. + + Args: + source_track: Source track index + source_clip: Source clip slot index + target_track: Target track index (can be same as source) + target_clip: Target clip slot index + + Returns: + JSON with duplication status and clip info. + """ + resp = _send_to_ableton( + "duplicate_clip", + {"source_track": source_track, "source_clip": source_clip, + "target_track": target_track, "target_clip": target_clip}, + timeout=TIMEOUTS["duplicate_clip"] + ) + return _ok(resp) if resp.get("status") == "success" else _err(resp.get("message")) + + @mcp.tool() def load_sample_to_drum_rack(ctx: Context, track_index: int, sample_path: str, pad_note: int = 36) -> str: @@ -4063,6 +4087,7 @@ def help(ctx: Context, tool_name: str = "") -> str: # Arrangement "create_arrangement_audio_pattern": {"description": "Crea clips de audio en Arrangement View", "category": "Arrangement", "params": [{"name": "track_index", "type": "int"}, {"name": "file_path", "type": "str"}, {"name": "positions", "type": "list", "default": [0]}, {"name": "name", "type": "str", "optional": True}], "example": "create_arrangement_audio_pattern(track_index=0, file_path='...', positions=[0, 4, 8])"}, "load_sample_to_clip": {"description": "Carga sample en clip de Session View", "category": "Arrangement", "params": [{"name": "track_index", "type": "int"}, {"name": "clip_index", "type": "int"}, {"name": "sample_path", "type": "str"}], "example": "load_sample_to_clip(track_index=0, clip_index=0, sample_path='...')"}, + "duplicate_clip": {"description": "Duplica un clip a otro slot de Session View", "category": "Arrangement", "params": [{"name": "source_track", "type": "int"}, {"name": "source_clip", "type": "int"}, {"name": "target_track", "type": "int"}, {"name": "target_clip", "type": "int"}], "example": "duplicate_clip(source_track=0, source_clip=0, target_track=0, target_clip=1)"}, "load_sample_to_drum_rack": {"description": "Carga sample en pad de Drum Rack", "category": "Arrangement", "params": [{"name": "track_index", "type": "int"}, {"name": "sample_path", "type": "str"}, {"name": "pad_note", "type": "int", "default": 36}], "example": "load_sample_to_drum_rack(track_index=0, sample_path='...', pad_note=36)"}, "set_warp_markers": {"description": "Configura marcadores de warp", "category": "Arrangement", "params": [{"name": "track_index", "type": "int"}, {"name": "clip_index", "type": "int"}, {"name": "markers", "type": "list"}], "example": "set_warp_markers(track_index=0, clip_index=0, markers=[...])"}, "reverse_clip": {"description": "Invierte un clip", "category": "Arrangement", "params": [{"name": "track_index", "type": "int"}, {"name": "clip_index", "type": "int"}], "example": "reverse_clip(track_index=0, clip_index=0)"},