Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

/live/clip/get/notes should return all notes #87

Closed
wants to merge 7 commits into from
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,8 @@ Represents an audio or MIDI clip. Can be used to start/stop clips, and query/mod
| /live/clip/fire | track_id, clip_id | | Start clip playback |
| /live/clip/stop | track_id, clip_id | | Stop clip playback |
| /live/clip/duplicate_loop | track_id, clip_id | | Duplicates clip loop |
| /live/clip/get/notes | track_id, clip_id | track_id, clip_id, pitch, start_time, duration, velocity, mute, [pitch, start_time...] | Query the notes in a given clip. |
| /live/clip/get/notes | track_id, clip_id, [from_time, from_pitch, time_span, pitch_span] | track_id, clip_id, pitch, start_time, duration, velocity, mute, [pitch, start_time...] | Query the notes in a given clip. |
| /live/clip/get/notes_range | track_id, clip_id, [from_time, from_pitch, time_span, pitch_span] | track_id, clip_id, min_start_time, max_end_time, min_pitch, max_pitch | Query the note range within a given clip. This is helpful in conjunction with /live/clip/get/notes to paginate and handle a large number of notes |
Copy link
Owner

Choose a reason for hiding this comment

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

I think a neater approach here would be just to have the single /live/clip/get/notes endpoint, with the final 4 parameters as optional (min_start_time, max_end_time, min_pitch, max_pitch) - would that work? That would then also have symmetry with /live/clip/remove/notes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ideoforms This api was added as a temporary fix for #88, before PR #89 was made

If there are too many notes, /live/clip/get/notes would fail. To address this, /live/clip/get/notes_range was introduced to determine the range of notes, and later /live/clip/get/notes could be called with optional parameters [from_time, from_pitch, time_span, pitch_span] for pagination purposes.

After merging PR #89, this API may no longer be required. However, it would still be beneficial to retain it for those who prefer to implement pagination.

Copy link
Owner

Choose a reason for hiding this comment

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

Thanks, will have another think on this once #89 is merged 👍

| /live/clip/add/notes | track_id, clip_id, pitch, start_time, duration, velocity, mute, ... | | Add new MIDI notes to a clip. pitch is MIDI note index, start_time and duration are beats in floats, velocity is MIDI velocity index, mute is true/false |
| /live/clip/remove/notes | start_pitch, pitch_span, start_time, time_span | | Remove notes from a clip in a given range of pitches and times. |
| /live/clip/get/color | track_id, clip_id | track_id, clip_id, color | Get clip color |
Expand All @@ -370,10 +371,14 @@ Represents an audio or MIDI clip. Can be used to start/stop clips, and query/mod
| /live/clip/get/playing_position | track_id, clip_id | track_id, clip_id, playing_position | Get clip's playing position |
| /live/clip/start_listen/playing_position | track_id, clip_id | | Start listening for clip's playing position. Replies are sent to /live/clip/get/playing_position, with args: track_id, clip_id, playing_position |
| /live/clip/stop_listen/playing_position | track_id, clip_id | | Stop listening for clip's playing position. |
| /live/clip/get/loop_start | track_id, clip_id | track_id, clip_id, loop_start | Get clip's loop start |
| /live/clip/set/loop_start | track_id, clip_id, loop_start | track_id, clip_id, loop_start | Set clip's loop start |
| /live/clip/get/loop_end | track_id, clip_id | track_id, clip_id, loop_end | Get clip's loop end |
| /live/clip/set/loop_end | track_id, clip_id, loop_end | track_id, clip_id, loop_end | Set clip's loop end |
| /live/clip/get/loop_start | track_id, clip_id | track_id, clip_id, loop_start | Get clip's loop start, expressed in floating-point beats
| /live/clip/set/loop_start | track_id, clip_id, loop_start | track_id, clip_id, loop_start | Set clip's loop start, expressed in floating-point beats |
| /live/clip/get/loop_end | track_id, clip_id | track_id, clip_id, loop_end | Get clip's loop end, expressed in floating-point beats |
| /live/clip/set/loop_end | track_id, clip_id, loop_end | track_id, clip_id, loop_end | Set clip's loop end, expressed in floating-point beats |
| /live/clip/get/start_marker | track_id, clip_id | track_id, clip_id, start_marker | Get clip's marker start, expressed in floating-point beats |
| /live/clip/set/start_marker | track_id, clip_id, start_marker | track_id, clip_id, start_maker | Set clip's marker start, expressed in floating-point beats |
| /live/clip/get/end_marker | track_id, clip_id | track_id, clip_id, end_marker | Get clip's marker end, expressed in floating-point beats |
| /live/clip/set/end_marker | track_id, clip_id, end_marker | track_id, clip_id, end_marker | Set clip's marker end, expressed in floating-point beats |
</details>

---
Expand Down
55 changes: 51 additions & 4 deletions abletonosc/clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ def clip_callback(params: Tuple[Any]) -> Tuple:
"looping",
"loop_start",
"loop_end",
"warping"
"warping",
"start_marker",
"end_marker"
]

for method in methods:
Expand All @@ -106,9 +108,53 @@ def clip_callback(params: Tuple[Any]) -> Tuple:
self.osc_server.add_handler("/live/clip/set/%s" % prop,
create_clip_callback(self._set_property, prop))

def clip_get_notes_helper(clip, params: Tuple[Any] = ()):
if clip is None:
self.logger.info("Trying to get notes from an empty clip")
return ()
# Define default values
default_min_from_time = -16000
default_max_time_span = 1000000 # Ableton clip max length is 24 hours. This is more than enough at over 200bpm
# These numbers were came up after a bunch of try and error. They look arbitrary but it works.
# I have tried different comnination of min and max time including using sys.maxsize, sys.float_info.min, sys.float_info.max
# clip.end_marker, clip_start_marker... but those don't work well. Notes are still missing
# https://github.com/ideoforms/AbletonOSC/issues/86


# Check if parameters are provided in the params tuple
if len(params) == 4:
Copy link
Owner

Choose a reason for hiding this comment

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

Should flag an error if params has a length other than 4, as otherwise this would silently fail

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ideoforms In the else statement, default parameters were used. Would you prefer to throw an error instead?

from_time, from_pitch, time_span, pitch_span = params
else:
from_time, from_pitch, time_span, pitch_span = default_min_from_time, 0, default_max_time_span, 128

return clip.get_notes(from_time, from_pitch, time_span, pitch_span)

def clip_get_notes(clip, params: Tuple[Any] = ()):
notes = clip.get_notes(0, 0, clip.length, 127)
return tuple(item for sublist in notes for item in sublist)
notes = clip_get_notes_helper(clip, params)
return tuple(item for note in notes for item in note)

def clip_get_notes_range(clip, params: Tuple[Any] = ()):
notes = clip_get_notes_helper(clip, params)

min_start_time = float('inf') # Initialize with a large value
max_end_time = float('-inf') # Initialize with a small value
min_pitch = float('inf') # Initialize with a large value
max_pitch = float('-inf') # Initialize with a small value

for note in notes:
pitch, start_time, duration, velocity, mute = note

# Update smallest start time
min_start_time = min(min_start_time, start_time)

# Update largest end time (start_time + duration)
max_end_time = max(max_end_time, start_time + duration)

# Update minimum and maximum pitch
min_pitch = min(min_pitch, pitch)
max_pitch = max(max_pitch, pitch)

return (min_start_time, max_end_time, min_pitch, max_pitch)

def clip_add_notes(clip, params: Tuple[Any] = ()):
notes = []
Expand All @@ -127,6 +173,7 @@ def clip_remove_notes(clip, params: Tuple[Any] = ()):
clip.remove_notes_extended(start_pitch, pitch_span, start_time, time_span)

self.osc_server.add_handler("/live/clip/get/notes", create_clip_callback(clip_get_notes))
self.osc_server.add_handler("/live/clip/get/notes_range", create_clip_callback(clip_get_notes_range))
self.osc_server.add_handler("/live/clip/add/notes", create_clip_callback(clip_add_notes))
self.osc_server.add_handler("/live/clip/remove/notes", create_clip_callback(clip_remove_notes))

Expand Down Expand Up @@ -208,4 +255,4 @@ def _build_clip_name_cache(self):
clip_notes_str = re.sub("[1-9]", "", clip_notes_str)
clip_notes_list = clip_notes_str.split("-")
clip_notes_list = [note_name_to_midi(name) for name in clip_notes_list]
self._clip_notes_cache[-1][-1] = clip_notes_list
self._clip_notes_cache[-1][-1] = clip_notes_list