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:
@@ -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"
|
|
||||||
)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user