Fix clip_record_notes: use /record + isRecording polling instead of overdub
Switch from the launcher-overdub approach (which never actually recorded notes) to arming the track and sending /track/N/clip/N/record, then waiting for isRecording=1 before starting the MIDI event timer. Adds all-notes-off at loop end to prevent stuck notes, and reports recording_confirmed in the return value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -120,9 +120,10 @@ def register(mcp) -> None:
|
|||||||
|
|
||||||
DrivenByMoss OSC has no direct note-editing API, so this tool works by:
|
DrivenByMoss OSC has no direct note-editing API, so this tool works by:
|
||||||
1. Creating an empty clip of the requested length
|
1. Creating an empty clip of the requested length
|
||||||
2. Enabling launcher overdub so the clip records while playing
|
2. Arming the track and sending /record to put the clip in recording mode
|
||||||
3. Launching the clip and sending precisely timed MIDI events via vkb_midi
|
3. Waiting for isRecording=1 confirmation before starting the note timer
|
||||||
4. Waiting for one full loop, then disabling overdub
|
4. Sending precisely timed vkb_midi events for each note on/off
|
||||||
|
5. Letting the clip finish one loop (recording stops automatically)
|
||||||
|
|
||||||
notes: list of dicts, each with:
|
notes: list of dicts, each with:
|
||||||
- note: int — MIDI note number (0-127, 60=C4, 62=D4, 64=E4, 65=F4, 67=G4, 69=A4, 71=B4)
|
- note: int — MIDI note number (0-127, 60=C4, 62=D4, 64=E4, 65=F4, 67=G4, 69=A4, 71=B4)
|
||||||
@@ -161,34 +162,45 @@ def register(mcp) -> None:
|
|||||||
if n.get("start_beat", 0) >= length_beats:
|
if n.get("start_beat", 0) >= length_beats:
|
||||||
raise ValueError(f"start_beat {n['start_beat']} >= clip length {length_beats}")
|
raise ValueError(f"start_beat {n['start_beat']} >= clip length {length_beats}")
|
||||||
|
|
||||||
# 1. Create empty clip on the target slot
|
# 1. Select the track and arm it
|
||||||
c.cmd(f"/track/{track_index}/clip/{clip_index}/create", length_beats)
|
|
||||||
time.sleep(0.15)
|
|
||||||
|
|
||||||
# 2. Select the track so it becomes the active cursor track
|
|
||||||
c.cmd(f"/track/{track_index}/select")
|
c.cmd(f"/track/{track_index}/select")
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
c.cmd(f"/track/{track_index}/recarm", 1)
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
# 3. Set launch quantization to None for immediate start
|
# 2. Set immediate launch quantization
|
||||||
c.cmd("/launcher/defaultQuantization", "NONE")
|
c.cmd("/launcher/defaultQuantization", "NONE")
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
|
||||||
# 4. Enable launcher overdub — clips record while playing
|
# 3. Create empty clip (overwrites any existing content)
|
||||||
c.cmd("/overdub/launcher", 1)
|
c.cmd(f"/track/{track_index}/clip/{clip_index}/create", length_beats)
|
||||||
time.sleep(0.05)
|
time.sleep(0.2)
|
||||||
|
|
||||||
# 5. Launch the clip and wait for it to actually start playing
|
# 4. Send record command — puts the clip into active recording mode
|
||||||
c.cmd(f"/track/{track_index}/clip/{clip_index}/launch")
|
c.cmd(f"/track/{track_index}/clip/{clip_index}/record")
|
||||||
|
|
||||||
|
# 5. Wait for isRecording=1 (up to 3s)
|
||||||
t_start = time.monotonic()
|
t_start = time.monotonic()
|
||||||
|
recording_confirmed = False
|
||||||
deadline = t_start + 3.0
|
deadline = t_start + 3.0
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
playing = c.get_state(f"/track/{track_index}/clip/{clip_index}/isPlaying")
|
rec = c.get_state(f"/track/{track_index}/clip/{clip_index}/isRecording")
|
||||||
if playing and playing[0]:
|
if rec and rec[0]:
|
||||||
|
recording_confirmed = True
|
||||||
|
t_start = time.monotonic() # reset timer to actual recording start
|
||||||
|
break
|
||||||
|
# Also accept isPlaying (some Bitwig versions go straight to playing+overdub)
|
||||||
|
play = c.get_state(f"/track/{track_index}/clip/{clip_index}/isPlaying")
|
||||||
|
if play and play[0]:
|
||||||
|
recording_confirmed = True
|
||||||
t_start = time.monotonic()
|
t_start = time.monotonic()
|
||||||
break
|
break
|
||||||
time.sleep(0.005)
|
time.sleep(0.005)
|
||||||
|
|
||||||
|
if not recording_confirmed:
|
||||||
|
# Fallback: proceed anyway with a small buffer for latency
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
# 6. Build sorted event list: (time_seconds, note, velocity)
|
# 6. Build sorted event list: (time_seconds, note, velocity)
|
||||||
events = []
|
events = []
|
||||||
for n in notes:
|
for n in notes:
|
||||||
@@ -196,15 +208,13 @@ def register(mcp) -> None:
|
|||||||
vel = int(n.get("velocity", 100))
|
vel = int(n.get("velocity", 100))
|
||||||
start_s = float(n.get("start_beat", 0.0)) * beat_seconds
|
start_s = float(n.get("start_beat", 0.0)) * beat_seconds
|
||||||
dur_s = float(n.get("duration_beats", 0.25)) * beat_seconds
|
dur_s = float(n.get("duration_beats", 0.25)) * beat_seconds
|
||||||
end_s = start_s + dur_s
|
end_s = min(start_s + dur_s, clip_seconds - 0.02)
|
||||||
# Keep note-off inside the clip (avoid firing after loop wraps)
|
|
||||||
end_s = min(end_s, clip_seconds - 0.01)
|
|
||||||
events.append((start_s, note, vel))
|
events.append((start_s, note, vel))
|
||||||
events.append((end_s, note, 0)) # velocity 0 = note off
|
events.append((end_s, note, 0))
|
||||||
|
|
||||||
events.sort(key=lambda e: e[0])
|
events.sort(key=lambda e: e[0])
|
||||||
|
|
||||||
# 7. Send events at the right times
|
# 7. Send events at precise beat-aligned times
|
||||||
for t_event, note, vel in events:
|
for t_event, note, vel in events:
|
||||||
elapsed = time.monotonic() - t_start
|
elapsed = time.monotonic() - t_start
|
||||||
wait = t_event - elapsed
|
wait = t_event - elapsed
|
||||||
@@ -212,19 +222,26 @@ def register(mcp) -> None:
|
|||||||
time.sleep(wait)
|
time.sleep(wait)
|
||||||
c.cmd(f"/vkb_midi/{channel}/note/{note}", vel)
|
c.cmd(f"/vkb_midi/{channel}/note/{note}", vel)
|
||||||
|
|
||||||
# 8. Wait until the clip has completed one full loop, then stop overdub
|
# 8. Wait for the clip to finish its first loop, then stop recording
|
||||||
elapsed = time.monotonic() - t_start
|
elapsed = time.monotonic() - t_start
|
||||||
remaining = clip_seconds - elapsed
|
remaining = clip_seconds - elapsed
|
||||||
if remaining > 0:
|
if remaining > 0:
|
||||||
time.sleep(remaining + 0.08)
|
time.sleep(remaining + 0.05)
|
||||||
|
|
||||||
c.cmd("/overdub/launcher", 0)
|
# Send all-notes-off before stopping to prevent stuck notes
|
||||||
|
for n in notes:
|
||||||
|
c.cmd(f"/vkb_midi/{channel}/note/{int(n['note'])}", 0)
|
||||||
|
|
||||||
# Restore default quantization
|
# Launch (toggles recording → playing) to finalise the clip
|
||||||
|
c.cmd(f"/track/{track_index}/clip/{clip_index}/launch")
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Restore default launch quantization
|
||||||
c.cmd("/launcher/defaultQuantization", "1")
|
c.cmd("/launcher/defaultQuantization", "1")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
|
"recording_confirmed": recording_confirmed,
|
||||||
"track_index": track_index,
|
"track_index": track_index,
|
||||||
"clip_index": clip_index,
|
"clip_index": clip_index,
|
||||||
"bpm": bpm,
|
"bpm": bpm,
|
||||||
|
|||||||
Reference in New Issue
Block a user