Remove clip_record_notes and clip_note_info experimental tools

Real-time MIDI recording via OSC is too unreliable for accurate note
placement due to OSC round-trip latency and lack of direct note editing
in the DrivenByMoss API. Also removes associated helper functions and
scratch test/diagnostic scripts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 19:33:58 +02:00
parent 7125a2ce4c
commit 1a7d669cda
-160
View File
@@ -1,6 +1,5 @@
"""MCP tools for Bitwig Studio cursor clip controls via DrivenByMoss OSC.""" """MCP tools for Bitwig Studio cursor clip controls via DrivenByMoss OSC."""
import time
from typing import Any from typing import Any
from bitwig_mcp.osc_client import get_client from bitwig_mcp.osc_client import get_client
@@ -106,162 +105,3 @@ def register(mcp) -> None:
"color": _v(c.get_state("/clip/color")), "color": _v(c.get_state("/clip/color")),
"pinned": _v(c.get_state("/clip/pinned")), "pinned": _v(c.get_state("/clip/pinned")),
} }
@mcp.tool()
def clip_record_notes(
track_index: int,
clip_index: int,
notes: list,
length_beats: float = 4.0,
channel: int = 1,
bpm: float | None = None,
) -> dict:
"""Create a clip and write MIDI notes into it using timed real-time recording.
DrivenByMoss OSC has no direct note-editing API, so this tool works by:
1. Creating an empty clip of the requested length
2. Arming the track and sending /record to put the clip in recording mode
3. Waiting for isRecording=1 confirmation before starting the note timer
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:
- note: int — MIDI note number (0-127, 60=C4, 62=D4, 64=E4, 65=F4, 67=G4, 69=A4, 71=B4)
- velocity: int — 1-127 (default 100)
- start_beat: float — beat position from start of clip (0.0 = first beat)
- duration_beats: float — note length in beats (default 0.25 = 16th note)
track_index: 1-8 (which track to place the clip on)
clip_index: 1-8 (which slot in the track's clip launcher)
length_beats: total clip length in beats (4.0 = 1 bar in 4/4, 8.0 = 2 bars)
channel: MIDI channel 1-16 (default 1)
bpm: tempo override; if omitted, reads current Bitwig tempo from state
Example — a simple C major arpeggio over 4 beats:
notes=[
{"note": 60, "velocity": 100, "start_beat": 0.0, "duration_beats": 0.5},
{"note": 64, "velocity": 90, "start_beat": 1.0, "duration_beats": 0.5},
{"note": 67, "velocity": 95, "start_beat": 2.0, "duration_beats": 0.5},
{"note": 72, "velocity": 85, "start_beat": 3.0, "duration_beats": 1.0},
]
"""
c = get_client()
# Resolve BPM
if bpm is None:
tempo_state = c.get_state("/tempo/raw")
bpm = float(tempo_state[0]) if tempo_state else 120.0
beat_seconds = 60.0 / bpm
clip_seconds = length_beats * beat_seconds
# Validate notes
for n in notes:
if not (0 <= n["note"] <= 127):
raise ValueError(f"note {n['note']} out of range 0-127")
if n.get("start_beat", 0) >= length_beats:
raise ValueError(f"start_beat {n['start_beat']} >= clip length {length_beats}")
# 1. Select the track and arm it
c.cmd(f"/track/{track_index}/select")
time.sleep(0.05)
c.cmd(f"/track/{track_index}/recarm", 1)
time.sleep(0.05)
# 2. Set immediate launch quantization
c.cmd("/launcher/defaultQuantization", "NONE")
time.sleep(0.05)
# 3. Create empty clip (overwrites any existing content)
c.cmd(f"/track/{track_index}/clip/{clip_index}/create", length_beats)
time.sleep(0.2)
# 4. Send record command — puts the clip into active recording mode
c.cmd(f"/track/{track_index}/clip/{clip_index}/record")
# 5. Wait for isRecording=1 (up to 3s)
t_start = time.monotonic()
recording_confirmed = False
deadline = t_start + 3.0
while time.monotonic() < deadline:
rec = c.get_state(f"/track/{track_index}/clip/{clip_index}/isRecording")
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()
break
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)
events = []
for n in notes:
note = int(n["note"])
vel = int(n.get("velocity", 100))
start_s = float(n.get("start_beat", 0.0)) * beat_seconds
dur_s = float(n.get("duration_beats", 0.25)) * beat_seconds
end_s = min(start_s + dur_s, clip_seconds - 0.02)
events.append((start_s, note, vel))
events.append((end_s, note, 0))
events.sort(key=lambda e: e[0])
# 7. Send events at precise beat-aligned times
for t_event, note, vel in events:
elapsed = time.monotonic() - t_start
wait = t_event - elapsed
if wait > 0:
time.sleep(wait)
c.cmd(f"/vkb_midi/{channel}/note/{note}", vel)
# 8. Wait for the clip to finish its first loop, then stop recording
elapsed = time.monotonic() - t_start
remaining = clip_seconds - elapsed
if remaining > 0:
time.sleep(remaining + 0.05)
# 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)
# 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")
return {
"ok": True,
"recording_confirmed": recording_confirmed,
"track_index": track_index,
"clip_index": clip_index,
"bpm": bpm,
"length_beats": length_beats,
"clip_duration_seconds": round(clip_seconds, 3),
"notes_written": len(notes),
"channel": channel,
}
@mcp.tool()
def clip_note_info() -> str:
"""Return a reference of MIDI note numbers for common notes.
Use these values in the 'note' field of clip_record_notes.
"""
return (
"Octave 3: C=48 D=50 E=52 F=53 G=55 A=57 B=59\n"
"Octave 4: C=60 D=62 E=64 F=65 G=67 A=69 B=71 (middle C = 60)\n"
"Octave 5: C=72 D=74 E=76 F=77 G=79 A=81 B=83\n"
"Sharps/flats add or subtract 1 (e.g. C#4=61, Bb4=70)\n"
"Drum GM: Kick=36 Snare=38 HiHat(closed)=42 HiHat(open)=46 "
"Crash=49 Ride=51 Tom(hi)=48 Tom(mid)=45 Tom(lo)=41"
)