Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 58 additions & 9 deletions AbletonMCP_Remote_Script/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ def _process_command(self, command):
elif command_type == "get_track_info":
track_index = params.get("track_index", 0)
response["result"] = self._get_track_info(track_index)
elif command_type == "get_notes_from_clip":
track_index = params.get("track_index", 0)
clip_index = params.get("clip_index", 0)
response["result"] = self._get_notes_from_clip(track_index, clip_index)
# Commands that modify Live's state should be scheduled on the main thread
elif command_type in ["create_midi_track", "set_track_name",
"create_clip", "add_notes_to_clip", "set_clip_name",
Expand Down Expand Up @@ -486,19 +490,19 @@ def _add_notes_to_clip(self, track_index, clip_index, notes):
try:
if track_index < 0 or track_index >= len(self._song.tracks):
raise IndexError("Track index out of range")

track = self._song.tracks[track_index]

if clip_index < 0 or clip_index >= len(track.clip_slots):
raise IndexError("Clip index out of range")

clip_slot = track.clip_slots[clip_index]

if not clip_slot.has_clip:
raise Exception("No clip in slot")

clip = clip_slot.clip

# Convert note data to Live's format
live_notes = []
for note in notes:
Expand All @@ -507,19 +511,64 @@ def _add_notes_to_clip(self, track_index, clip_index, notes):
duration = note.get("duration", 0.25)
velocity = note.get("velocity", 100)
mute = note.get("mute", False)

live_notes.append((pitch, start_time, duration, velocity, mute))

# Add the notes
clip.set_notes(tuple(live_notes))

result = {
"note_count": len(notes)
}
return result
except Exception as e:
self.log_message("Error adding notes to clip: " + str(e))
raise

def _get_notes_from_clip(self, track_index, clip_index):
"""Get all MIDI notes from a clip"""
try:
if track_index < 0 or track_index >= len(self._song.tracks):
raise IndexError("Track index out of range")

track = self._song.tracks[track_index]

if clip_index < 0 or clip_index >= len(track.clip_slots):
raise IndexError("Clip index out of range")

clip_slot = track.clip_slots[clip_index]

if not clip_slot.has_clip:
raise Exception("No clip in slot")

clip = clip_slot.clip

# Get all notes from the clip
# get_notes_extended(from_pitch, pitch_span, from_time, time_span)
# Get all pitches (0-128) for the full clip length
notes_tuple = clip.get_notes_extended(0, 128, 0.0, clip.length)

# Convert to list of dictionaries
notes = []
for note in notes_tuple:
notes.append({
"pitch": note.pitch,
"start_time": note.start_time,
"duration": note.duration,
"velocity": note.velocity,
"mute": note.mute
})

result = {
"clip_name": clip.name,
"length": clip.length,
"note_count": len(notes),
"notes": notes
}
return result
except Exception as e:
self.log_message("Error getting notes from clip: " + str(e))
raise
Comment on lines +528 to +571

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, understand the repository structure
find . -type f -name "*.py" | head -20

Repository: ahujasid/ableton-mcp

Length of output: 180


🏁 Script executed:

# Check if there's documentation or other references to clip types
rg "MIDI|audio|clip" --type py -B 2 -A 2 | head -100

Repository: ahujasid/ableton-mcp

Length of output: 5082


🏁 Script executed:

# Search for other uses of get_notes_extended or similar API calls
rg "get_notes" --type py

Repository: ahujasid/ableton-mcp

Length of output: 878


🏁 Script executed:

# Look for hasattr or capability checks in the codebase
rg "hasattr" --type py

Repository: ahujasid/ableton-mcp

Length of output: 4220


🏁 Script executed:

# Check the full context of the __init__.py file around the method
wc -l AbletonMCP_Remote_Script/__init__.py

Repository: ahujasid/ableton-mcp

Length of output: 105


🏁 Script executed:

# Let's examine the context around _get_notes_from_clip and how clips are created
sed -n '520,580p' AbletonMCP_Remote_Script/__init__.py

Repository: ahujasid/ableton-mcp

Length of output: 2210


🏁 Script executed:

# Check for create_clip, create_audio_track, create_midi_track implementations
rg "def create" AbletonMCP_Remote_Script/__init__.py -A 10 | head -80

Repository: ahujasid/ableton-mcp

Length of output: 481


🏁 Script executed:

# Search for any clip type checking patterns
rg "is_midi|is_audio|clip_type" AbletonMCP_Remote_Script/__init__.py

Repository: ahujasid/ableton-mcp

Length of output: 175


🏁 Script executed:

# Check how tracks are differentiated (MIDI vs Audio)
rg "MIDI|Audio" AbletonMCP_Remote_Script/__init__.py | grep -i "track\|clip" | head -20

Repository: ahujasid/ableton-mcp

Length of output: 351


🏁 Script executed:

# Let's look at the create_clip method to see if there's a clip type distinction
rg "def _create_clip" AbletonMCP_Remote_Script/__init__.py -A 30

Repository: ahujasid/ableton-mcp

Length of output: 1262


🏁 Script executed:

# Check if there's any audio clip handling
rg "audio.*clip|clip.*audio" AbletonMCP_Remote_Script/__init__.py -i

Repository: ahujasid/ableton-mcp

Length of output: 46


🏁 Script executed:

# Check the _add_notes_to_clip method to see if it does any type checking
sed -n '480,530p' AbletonMCP_Remote_Script/__init__.py

Repository: ahujasid/ableton-mcp

Length of output: 1795


🏁 Script executed:

# Search for any Ableton Live documentation or comments about clip types
rg "class.*Clip|clip_type|MidiClip|AudioClip" AbletonMCP_Remote_Script/__init__.py

Repository: ahujasid/ableton-mcp

Length of output: 46


🏁 Script executed:

# Let me check if there's any way to determine if a clip is a MIDI clip
# Looking for has_audio, is_midi, or similar attributes
rg "track\\.has|track\\.is|clip\\." AbletonMCP_Remote_Script/__init__.py | grep -E "audio|midi" | head -20

Repository: ahujasid/ableton-mcp

Length of output: 175


🏁 Script executed:

# Check if Ableton clips have a type property or similar
rg "get_notes|set_notes" AbletonMCP_Remote_Script/__init__.py -B 3 -A 3

Repository: ahujasid/ableton-mcp

Length of output: 1628


🏁 Script executed:

# Search for any documentation or comments about Ableton Live API versions
head -50 AbletonMCP_Remote_Script/__init__.py

Repository: ahujasid/ableton-mcp

Length of output: 1476


🏁 Script executed:

# Let me check if clips can only be created on specific track types
rg "_create_clip|track_type|has_audio_input|has_midi_input" AbletonMCP_Remote_Script/__init__.py -B 2 -A 2 | head -50

Repository: ahujasid/ableton-mcp

Length of output: 855


🏁 Script executed:

# Let's check the server.py to see if there's any validation before calling _get_notes_from_clip
rg "get_notes_from_clip" MCP_Server/server.py -B 5 -A 5

Repository: ahujasid/ableton-mcp

Length of output: 1187


🏁 Script executed:

# Let me check if there's any way that audio clips could end up in clip_slots
# Look for how clips are loaded or imported
rg "load|import|audio" AbletonMCP_Remote_Script/__init__.py | grep -i "clip" | head -10

Repository: ahujasid/ableton-mcp

Length of output: 46


🏁 Script executed:

# Check if clip_slot can contain different types of clips (MIDI vs audio)
rg "clip_slot\|has_clip" AbletonMCP_Remote_Script/__init__.py -B 2 -A 2 | head -40

Repository: ahujasid/ableton-mcp

Length of output: 46


🏁 Script executed:

# Search for any documentation or examples about Ableton Live API clip types
web_search
Ableton Live API get_notes_extended method MIDI clip audio clip

Repository: ahujasid/ableton-mcp

Length of output: 160


🌐 Web query:

Ableton Live API get_notes_extended MIDI clip audio clip documentation

💡 Result:

get_notes_extended is part of Live’s Clip API (Live Object Model). Key points:

  • It’s for MIDI clips only (audio clips have no MIDI notes). (docs.cycling74.com)
  • Signature: get_notes_extended(from_pitch, pitch_span, from_time, time_span) — you may also pass a single dict with those keys. Returns a list/dict of note dictionaries. (docs.cycling74.com)
  • Each returned note dictionary can include: note_id, pitch, start_time (beats, clip absolute time), duration (beats), velocity, mute, probability, velocity_deviation, release_velocity (and you can request a subset of fields to improve performance). Available since Live 11. (docs.cycling74.com)
  • Related newer methods (Live 11+): get_selected_notes_extended, get_all_notes_extended, add_new_notes, apply_note_modifications, remove_notes_extended, get_notes_by_id, remove_notes_by_id. These replace older get_notes / set_notes APIs. (cycling74.com)

If you want the full field list and exact parameter formats, see the Clip section of the Live Object Model (Cycling '74) and Ableton’s Note API docs. (docs.cycling74.com)

Would you like direct links to example code (Max/JS/Python) using get_notes_extended?

Citations:


🌐 Web query:

Ableton Live Python API clip types has_audio_input has_midi_input

💡 Result:

Short answer: has_audio_input and has_midi_input are boolean Track properties in Live’s API (Live Object Model) that indicate whether a track accepts audio input (true for audio tracks) or MIDI input (true for MIDI tracks). Clips themselves are either audio or MIDI (and ClipSlot.create_clip/create_midi_clip only creates MIDI clips via the API); you cannot programmatically create audio clips through the standard Live Object Model. (docs.cycling74.com)

Sources

  • Live Object Model — Track (properties list, includes has_audio_input / has_midi_input). [1]. (docs.cycling74.com)
  • StackOverflow / decompiled Live API: ClipSlot.create_clip is for MIDI clips; audio-clip creation is not exposed. [2]. (stackoverflow.com)

Citations:


🏁 Script executed:

# Check if there's a better way to determine clip type (is_audio_clip, clip_type, etc.)
rg "is_audio|is_midi|clip_type" --type py

Repository: ahujasid/ableton-mcp

Length of output: 249


Add clip type check to prevent confusing errors on audio clips.

clip.get_notes_extended() is a MIDI-only method in the Ableton Live API. If a user loads an audio clip into a slot, this will fail with an unhelpful AttributeError. A simple hasattr(clip, "get_notes_extended") check provides a clearer error path and aligns with the defensive checks used elsewhere in this file.

Proposed fix
 def _get_notes_from_clip(self, track_index, clip_index):
     """Get all MIDI notes from a clip"""
     try:
@@
             clip = clip_slot.clip
+
+            # Fail fast if this isn't a MIDI clip
+            if not hasattr(clip, "get_notes_extended"):
+                raise TypeError("Target clip does not support MIDI note extraction (expected a MIDI clip)")
@@
             notes_tuple = clip.get_notes_extended(0, 128, 0.0, clip.length)
🧰 Tools
🪛 Ruff (0.14.10)

532-532: Abstract raise to an inner function

(TRY301)


532-532: Avoid specifying long messages outside the exception class

(TRY003)


537-537: Abstract raise to an inner function

(TRY301)


537-537: Avoid specifying long messages outside the exception class

(TRY003)


542-542: Abstract raise to an inner function

(TRY301)


542-542: Create your own exception

(TRY002)


542-542: Avoid specifying long messages outside the exception class

(TRY003)


568-568: Consider moving this statement to an else block

(TRY300)

🤖 Prompt for AI Agents
In @AbletonMCP_Remote_Script/__init__.py around lines 528 - 571, The
_get_notes_from_clip method currently calls clip.get_notes_extended which exists
only for MIDI clips and will raise an unclear AttributeError for audio clips;
add a defensive check using hasattr(clip, "get_notes_extended") after obtaining
clip (and before calling get_notes_extended) and if it returns False log a clear
message (e.g., "Clip is not a MIDI clip") and raise an appropriate Exception (or
IndexError/TypeError consistent with other checks) so callers get a helpful
error rather than an AttributeError.


def _set_clip_name(self, track_index, clip_index, name):
"""Set the name of a clip"""
Expand Down
Loading