2026-10-22 10:31:33 -04:00
|
|
|
"""Status panel additions to AbletonOSC (installed by tools/abletonosc/install.sh on the Tracks Mac).
|
|
|
|
|
|
|
|
|
|
/live/statuspanel/move_note <track name> <beat> <pitch> <new beat>
|
|
|
|
|
Moves the note of `pitch` that starts at arrangement `beat` on the arrangement MIDI clip of the track
|
|
|
|
|
called `track name` so it starts at `new beat`, keeping where it ends (the next cue is not touched).
|
|
|
|
|
Answers (<track name>, <beat>, <pitch>, <moved: 0 or 1>). Undo in Live (Cmd+Z) puts it back.
|
2026-10-26 14:12:38 -04:00
|
|
|
|
|
|
|
|
START's consolidating (ableton_start.py) finds an arrangement clip by <track index> (Live's, no returns),
|
2026-10-26 15:26:07 -04:00
|
|
|
<track name> (any track of that name when the index has moved) and the <beat> it starts at. A track with no name of
|
|
|
|
|
its own shows its number in Live ("56-Piano" is "5-Piano" in the set file) and its clip's name ("Piano [2026-10-26
|
|
|
|
|
113835]" after a consolidate, "Piano (Freeze)" after a flatten), so only the name in between is compared:
|
2026-10-26 14:12:38 -04:00
|
|
|
/live/statuspanel/clip <index> <name> <beat>
|
|
|
|
|
Answers (<index>, <name>, <beat>, <found>, <pitch_coarse>, <warping>, <file path>).
|
|
|
|
|
/live/statuspanel/select_clip <index> <name> <beat>
|
2026-10-26 15:26:07 -04:00
|
|
|
Selects the clip in the arrangement (Edit > Consolidate then works on it), unfolding the groups it is in (Live
|
|
|
|
|
cannot select a track inside a folded group). Answers (..., <found>).
|
|
|
|
|
/live/statuspanel/refold
|
|
|
|
|
Folds the groups select_clip unfolded back up. Answers (<groups folded>).
|
|
|
|
|
/live/statuspanel/drop_tails <index> <name>
|
|
|
|
|
Deletes the "... (Freeze tail)" clips Flatten Track leaves after a clip, and takes "(Freeze)" out of the track's
|
|
|
|
|
name (back to Live's own name). Answers (<index>, <name>, <deleted>, <track name>); deleted is -1 when this
|
|
|
|
|
Live cannot delete a clip from a script.
|
|
|
|
|
/live/statuspanel/set_clip <index> <name> <beat> <pitch_coarse> <warping> [<new clip name>]
|
|
|
|
|
Sets the clip's transposition, then its warp (and its name). Answers (..., <found>, <pitch_coarse>, <warping>).
|
2026-10-22 10:31:33 -04:00
|
|
|
"""
|
|
|
|
|
import logging
|
2026-10-26 15:26:07 -04:00
|
|
|
import re
|
2026-10-22 10:31:33 -04:00
|
|
|
|
2026-10-26 14:12:38 -04:00
|
|
|
import Live
|
|
|
|
|
|
2026-10-22 10:31:33 -04:00
|
|
|
logger = logging.getLogger("abletonosc")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def register(handler):
|
|
|
|
|
def move_note(params):
|
|
|
|
|
song = handler.song
|
|
|
|
|
name, beat, pitch, new = str(params[0]), float(params[1]), int(params[2]), float(params[3])
|
|
|
|
|
track = next((t for t in song.tracks if t.name.strip().upper() == name.strip().upper() and t.has_midi_input), None)
|
|
|
|
|
moved = 0
|
|
|
|
|
for clip in (track.arrangement_clips if track else []):
|
|
|
|
|
if not (clip.start_time - 1e-6 <= beat < clip.end_time):
|
|
|
|
|
continue
|
|
|
|
|
zero = clip.start_time - (clip.loop_start if clip.looping else clip.start_marker)
|
|
|
|
|
local, new_local = beat - zero, new - zero
|
|
|
|
|
notes = clip.get_notes_extended(pitch, 1, local - 0.002, 0.004)
|
|
|
|
|
for n in notes:
|
|
|
|
|
end = n.start_time + n.duration
|
|
|
|
|
if new_local < end:
|
|
|
|
|
n.start_time, n.duration = new_local, end - new_local
|
|
|
|
|
moved += 1
|
|
|
|
|
if moved:
|
|
|
|
|
clip.apply_note_modifications(notes)
|
|
|
|
|
break
|
|
|
|
|
logger.info("statuspanel: %s note %d at %s -> %s: %d moved", name, pitch, beat, new, moved)
|
|
|
|
|
return name, beat, pitch, moved
|
|
|
|
|
|
|
|
|
|
handler.osc_server.add_handler("/live/statuspanel/move_note", move_note)
|
2026-10-26 14:12:38 -04:00
|
|
|
|
2026-10-26 15:26:07 -04:00
|
|
|
def bare(n):
|
|
|
|
|
# "56-Piano [2026-10-26 113835]" and "56-Piano (Freeze)" are the file's "5-Piano"
|
|
|
|
|
n = re.sub(r"^\s*\d+\s*-\s*", "", str(n))
|
|
|
|
|
n = re.sub(r"\[.*?\]|\(freeze\)", " ", n, flags=re.I)
|
|
|
|
|
return " ".join(n.split()).upper()
|
|
|
|
|
|
2026-10-26 14:12:38 -04:00
|
|
|
def find(params):
|
|
|
|
|
song = handler.song
|
2026-10-26 15:26:07 -04:00
|
|
|
i, name, beat = int(params[0]), bare(params[1]), float(params[2])
|
2026-10-26 14:12:38 -04:00
|
|
|
ts = list(song.tracks)
|
2026-10-26 15:26:07 -04:00
|
|
|
order = ([ts[i]] if 0 <= i < len(ts) and bare(ts[i].name) == name else []) + \
|
|
|
|
|
[t for t in ts if bare(t.name) == name]
|
2026-10-26 14:12:38 -04:00
|
|
|
for t in order:
|
|
|
|
|
try:
|
|
|
|
|
arr = t.arrangement_clips
|
|
|
|
|
except Exception:
|
|
|
|
|
continue
|
|
|
|
|
for c in arr:
|
|
|
|
|
if abs(c.start_time - beat) < 0.01:
|
|
|
|
|
return t, c
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
|
def info(c):
|
|
|
|
|
if c is None:
|
|
|
|
|
return 0, 0, 0, ""
|
|
|
|
|
return 1, int(c.pitch_coarse), int(c.warping), str(c.file_path)
|
|
|
|
|
|
|
|
|
|
def clip(params):
|
|
|
|
|
_, c = find(params)
|
|
|
|
|
return (int(params[0]), str(params[1]), float(params[2])) + info(c)
|
|
|
|
|
|
2026-10-26 15:26:07 -04:00
|
|
|
unfolded = []
|
|
|
|
|
|
2026-10-26 14:12:38 -04:00
|
|
|
def select_clip(params):
|
|
|
|
|
t, c = find(params)
|
|
|
|
|
if c is not None:
|
|
|
|
|
song = handler.song
|
2026-10-26 15:26:07 -04:00
|
|
|
g = t.group_track if t.is_grouped else None
|
|
|
|
|
while g is not None:
|
|
|
|
|
if g.fold_state:
|
|
|
|
|
g.fold_state = 0
|
|
|
|
|
unfolded.append(g)
|
|
|
|
|
g = g.group_track if g.is_grouped else None
|
2026-10-26 14:12:38 -04:00
|
|
|
view = Live.Application.get_application().view
|
|
|
|
|
view.show_view("Arranger")
|
|
|
|
|
view.focus_view("Arranger")
|
|
|
|
|
song.view.selected_track = t
|
|
|
|
|
song.view.detail_clip = c
|
|
|
|
|
view.focus_view("Arranger")
|
|
|
|
|
logger.info("statuspanel: select %s at %s: %s", params[1], params[2], c is not None)
|
|
|
|
|
return int(params[0]), str(params[1]), float(params[2]), int(c is not None)
|
|
|
|
|
|
2026-10-26 15:26:07 -04:00
|
|
|
def refold(params):
|
|
|
|
|
n = 0
|
|
|
|
|
while unfolded:
|
|
|
|
|
g = unfolded.pop()
|
|
|
|
|
try:
|
|
|
|
|
g.fold_state = 1
|
|
|
|
|
n += 1
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
return (n,)
|
|
|
|
|
|
2026-10-26 14:12:38 -04:00
|
|
|
def set_clip(params):
|
|
|
|
|
_, c = find(params)
|
|
|
|
|
if c is not None:
|
|
|
|
|
c.pitch_coarse = int(params[3])
|
|
|
|
|
c.warping = bool(int(params[4]))
|
2026-10-26 15:26:07 -04:00
|
|
|
if len(params) > 5 and str(params[5]):
|
|
|
|
|
c.name = str(params[5])
|
2026-10-26 14:12:38 -04:00
|
|
|
logger.info("statuspanel: %s at %s: pitch %s warp %s", params[1], params[2], params[3], params[4])
|
|
|
|
|
return (int(params[0]), str(params[1]), float(params[2])) + info(c)[:3]
|
|
|
|
|
|
|
|
|
|
handler.osc_server.add_handler("/live/statuspanel/clip", clip)
|
|
|
|
|
handler.osc_server.add_handler("/live/statuspanel/select_clip", select_clip)
|
|
|
|
|
handler.osc_server.add_handler("/live/statuspanel/set_clip", set_clip)
|
2026-10-26 15:26:07 -04:00
|
|
|
handler.osc_server.add_handler("/live/statuspanel/refold", refold)
|
|
|
|
|
|
|
|
|
|
def drop_tails(params):
|
|
|
|
|
i, name = int(params[0]), bare(params[1])
|
|
|
|
|
ts = list(handler.song.tracks)
|
|
|
|
|
order = ([ts[i]] if 0 <= i < len(ts) and bare(ts[i].name) == name else []) + [t for t in ts if bare(t.name) == name]
|
|
|
|
|
if not order:
|
|
|
|
|
return i, str(params[1]), 0, ""
|
|
|
|
|
t = order[0]
|
|
|
|
|
n = -1
|
|
|
|
|
if hasattr(t, "delete_clip"):
|
|
|
|
|
n = 0
|
|
|
|
|
for c in list(t.arrangement_clips):
|
|
|
|
|
if c.name.endswith("(Freeze tail)"):
|
|
|
|
|
t.delete_clip(c)
|
|
|
|
|
n += 1
|
|
|
|
|
if "(Freeze)" in t.name:
|
|
|
|
|
t.name = "" # Live's own name again, from the renamed clip
|
|
|
|
|
if "(Freeze)" in t.name:
|
|
|
|
|
t.name = re.sub(r"\s*\(Freeze\)", "", t.name)
|
|
|
|
|
logger.info("statuspanel: %s: %d freeze tails deleted, now %s", params[1], n, t.name)
|
|
|
|
|
return i, str(params[1]), n, str(t.name)
|
|
|
|
|
|
|
|
|
|
handler.osc_server.add_handler("/live/statuspanel/drop_tails", drop_tails)
|