@mthines/reaper-mcp 0.5.0 → 0.6.0-beta.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/main.js +420 -0
- package/package.json +1 -1
- package/reaper/mcp_bridge.lua +790 -0
package/main.js
CHANGED
|
@@ -517,6 +517,424 @@ function registerAnalysisTools(server) {
|
|
|
517
517
|
);
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
+
// apps/reaper-mcp-server/src/tools/midi.ts
|
|
521
|
+
import { z as z10 } from "zod/v4";
|
|
522
|
+
function registerMidiTools(server) {
|
|
523
|
+
server.tool(
|
|
524
|
+
"create_midi_item",
|
|
525
|
+
"Create an empty MIDI item on a track at a given time range (seconds). Use this before inserting notes.",
|
|
526
|
+
{
|
|
527
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
528
|
+
startPosition: z10.coerce.number().min(0).describe("Start position in seconds from project start"),
|
|
529
|
+
endPosition: z10.coerce.number().min(0).describe("End position in seconds from project start")
|
|
530
|
+
},
|
|
531
|
+
async ({ trackIndex, startPosition, endPosition }) => {
|
|
532
|
+
const res = await sendCommand("create_midi_item", { trackIndex, startPosition, endPosition });
|
|
533
|
+
if (!res.success) {
|
|
534
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
535
|
+
}
|
|
536
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
537
|
+
}
|
|
538
|
+
);
|
|
539
|
+
server.tool(
|
|
540
|
+
"list_midi_items",
|
|
541
|
+
"List all MIDI items on a track with position, length, and note/CC counts",
|
|
542
|
+
{
|
|
543
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index")
|
|
544
|
+
},
|
|
545
|
+
async ({ trackIndex }) => {
|
|
546
|
+
const res = await sendCommand("list_midi_items", { trackIndex });
|
|
547
|
+
if (!res.success) {
|
|
548
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
549
|
+
}
|
|
550
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
server.tool(
|
|
554
|
+
"get_midi_notes",
|
|
555
|
+
"Get all MIDI notes in a MIDI item. Returns pitch (0-127, 60=C4), velocity (0-127), position and duration in beats from item start.",
|
|
556
|
+
{
|
|
557
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
558
|
+
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track")
|
|
559
|
+
},
|
|
560
|
+
async ({ trackIndex, itemIndex }) => {
|
|
561
|
+
const res = await sendCommand("get_midi_notes", { trackIndex, itemIndex });
|
|
562
|
+
if (!res.success) {
|
|
563
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
564
|
+
}
|
|
565
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
server.tool(
|
|
569
|
+
"insert_midi_note",
|
|
570
|
+
"Insert a single MIDI note. Pitch: 0-127 (60=C4/Middle C, 48=C3, 72=C5). Velocity: 1-127 (64=medium, 100=strong, 127=max). Position/duration in beats from item start (1.0=quarter, 0.5=eighth, 0.25=sixteenth).",
|
|
571
|
+
{
|
|
572
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
573
|
+
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track"),
|
|
574
|
+
pitch: z10.coerce.number().min(0).max(127).describe("MIDI note number (60=C4/Middle C)"),
|
|
575
|
+
velocity: z10.coerce.number().min(1).max(127).describe("Note velocity (1-127)"),
|
|
576
|
+
startPosition: z10.coerce.number().min(0).describe("Start position in beats from item start"),
|
|
577
|
+
duration: z10.coerce.number().min(0).describe("Duration in beats (1.0=quarter note)"),
|
|
578
|
+
channel: z10.coerce.number().min(0).max(15).optional().describe("MIDI channel 0-15 (default 0)")
|
|
579
|
+
},
|
|
580
|
+
async ({ trackIndex, itemIndex, pitch, velocity, startPosition, duration, channel }) => {
|
|
581
|
+
const res = await sendCommand("insert_midi_note", {
|
|
582
|
+
trackIndex,
|
|
583
|
+
itemIndex,
|
|
584
|
+
pitch,
|
|
585
|
+
velocity,
|
|
586
|
+
startPosition,
|
|
587
|
+
duration,
|
|
588
|
+
channel: channel ?? 0
|
|
589
|
+
});
|
|
590
|
+
if (!res.success) {
|
|
591
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
592
|
+
}
|
|
593
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
server.tool(
|
|
597
|
+
"insert_midi_notes",
|
|
598
|
+
'Batch insert multiple MIDI notes. Pass a JSON array of notes as a string. Each note: { "pitch": 60, "velocity": 100, "startPosition": 0.0, "duration": 1.0, "channel": 0 }. Positions/durations in beats from item start.',
|
|
599
|
+
{
|
|
600
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
601
|
+
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track"),
|
|
602
|
+
notes: z10.string().describe('JSON array string of notes: [{"pitch":60,"velocity":100,"startPosition":0,"duration":1,"channel":0}, ...]')
|
|
603
|
+
},
|
|
604
|
+
async ({ trackIndex, itemIndex, notes }) => {
|
|
605
|
+
const res = await sendCommand("insert_midi_notes", { trackIndex, itemIndex, notes });
|
|
606
|
+
if (!res.success) {
|
|
607
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
608
|
+
}
|
|
609
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
server.tool(
|
|
613
|
+
"edit_midi_note",
|
|
614
|
+
"Edit an existing MIDI note by index. Only provided fields are changed. Positions/durations in beats from item start.",
|
|
615
|
+
{
|
|
616
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
617
|
+
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track"),
|
|
618
|
+
noteIndex: z10.coerce.number().min(0).describe("0-based note index"),
|
|
619
|
+
pitch: z10.coerce.number().min(0).max(127).optional().describe("New pitch (0-127)"),
|
|
620
|
+
velocity: z10.coerce.number().min(1).max(127).optional().describe("New velocity (1-127)"),
|
|
621
|
+
startPosition: z10.coerce.number().min(0).optional().describe("New start position in beats from item start"),
|
|
622
|
+
duration: z10.coerce.number().min(0).optional().describe("New duration in beats"),
|
|
623
|
+
channel: z10.coerce.number().min(0).max(15).optional().describe("New MIDI channel (0-15)")
|
|
624
|
+
},
|
|
625
|
+
async ({ trackIndex, itemIndex, noteIndex, pitch, velocity, startPosition, duration, channel }) => {
|
|
626
|
+
const res = await sendCommand("edit_midi_note", {
|
|
627
|
+
trackIndex,
|
|
628
|
+
itemIndex,
|
|
629
|
+
noteIndex,
|
|
630
|
+
pitch,
|
|
631
|
+
velocity,
|
|
632
|
+
startPosition,
|
|
633
|
+
duration,
|
|
634
|
+
channel
|
|
635
|
+
});
|
|
636
|
+
if (!res.success) {
|
|
637
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
638
|
+
}
|
|
639
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
server.tool(
|
|
643
|
+
"delete_midi_note",
|
|
644
|
+
"Delete a MIDI note by index from a MIDI item",
|
|
645
|
+
{
|
|
646
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
647
|
+
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track"),
|
|
648
|
+
noteIndex: z10.coerce.number().min(0).describe("0-based note index to delete")
|
|
649
|
+
},
|
|
650
|
+
async ({ trackIndex, itemIndex, noteIndex }) => {
|
|
651
|
+
const res = await sendCommand("delete_midi_note", { trackIndex, itemIndex, noteIndex });
|
|
652
|
+
if (!res.success) {
|
|
653
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
654
|
+
}
|
|
655
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
656
|
+
}
|
|
657
|
+
);
|
|
658
|
+
server.tool(
|
|
659
|
+
"get_midi_cc",
|
|
660
|
+
"Get CC (continuous controller) events from a MIDI item. Optionally filter by CC number (e.g. 1=modulation, 7=volume, 10=pan, 11=expression, 64=sustain).",
|
|
661
|
+
{
|
|
662
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
663
|
+
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track"),
|
|
664
|
+
ccNumber: z10.coerce.number().min(0).max(127).optional().describe("Optional: filter by CC number (0-127)")
|
|
665
|
+
},
|
|
666
|
+
async ({ trackIndex, itemIndex, ccNumber }) => {
|
|
667
|
+
const res = await sendCommand("get_midi_cc", { trackIndex, itemIndex, ccNumber });
|
|
668
|
+
if (!res.success) {
|
|
669
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
670
|
+
}
|
|
671
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
672
|
+
}
|
|
673
|
+
);
|
|
674
|
+
server.tool(
|
|
675
|
+
"insert_midi_cc",
|
|
676
|
+
"Insert a CC (continuous controller) event. Common CC numbers: 1=modulation, 7=volume, 10=pan, 11=expression, 64=sustain, 74=filter cutoff.",
|
|
677
|
+
{
|
|
678
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
679
|
+
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track"),
|
|
680
|
+
ccNumber: z10.coerce.number().min(0).max(127).describe("CC number (0-127)"),
|
|
681
|
+
value: z10.coerce.number().min(0).max(127).describe("CC value (0-127)"),
|
|
682
|
+
position: z10.coerce.number().min(0).describe("Position in beats from item start"),
|
|
683
|
+
channel: z10.coerce.number().min(0).max(15).optional().describe("MIDI channel 0-15 (default 0)")
|
|
684
|
+
},
|
|
685
|
+
async ({ trackIndex, itemIndex, ccNumber, value, position, channel }) => {
|
|
686
|
+
const res = await sendCommand("insert_midi_cc", {
|
|
687
|
+
trackIndex,
|
|
688
|
+
itemIndex,
|
|
689
|
+
ccNumber,
|
|
690
|
+
value,
|
|
691
|
+
position,
|
|
692
|
+
channel: channel ?? 0
|
|
693
|
+
});
|
|
694
|
+
if (!res.success) {
|
|
695
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
696
|
+
}
|
|
697
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
698
|
+
}
|
|
699
|
+
);
|
|
700
|
+
server.tool(
|
|
701
|
+
"delete_midi_cc",
|
|
702
|
+
"Delete a CC event by index from a MIDI item",
|
|
703
|
+
{
|
|
704
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
705
|
+
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track"),
|
|
706
|
+
ccIndex: z10.coerce.number().min(0).describe("0-based CC event index to delete")
|
|
707
|
+
},
|
|
708
|
+
async ({ trackIndex, itemIndex, ccIndex }) => {
|
|
709
|
+
const res = await sendCommand("delete_midi_cc", { trackIndex, itemIndex, ccIndex });
|
|
710
|
+
if (!res.success) {
|
|
711
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
712
|
+
}
|
|
713
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
714
|
+
}
|
|
715
|
+
);
|
|
716
|
+
server.tool(
|
|
717
|
+
"get_midi_item_properties",
|
|
718
|
+
"Get detailed properties of a MIDI item including position, length, note count, CC count, mute state, and loop setting",
|
|
719
|
+
{
|
|
720
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
721
|
+
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track")
|
|
722
|
+
},
|
|
723
|
+
async ({ trackIndex, itemIndex }) => {
|
|
724
|
+
const res = await sendCommand("get_midi_item_properties", { trackIndex, itemIndex });
|
|
725
|
+
if (!res.success) {
|
|
726
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
727
|
+
}
|
|
728
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
server.tool(
|
|
732
|
+
"set_midi_item_properties",
|
|
733
|
+
"Set MIDI item properties: position (seconds), length (seconds), mute, loop source",
|
|
734
|
+
{
|
|
735
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
736
|
+
itemIndex: z10.coerce.number().min(0).describe("0-based item index on the track"),
|
|
737
|
+
position: z10.coerce.number().min(0).optional().describe("New position in seconds from project start"),
|
|
738
|
+
length: z10.coerce.number().min(0).optional().describe("New length in seconds"),
|
|
739
|
+
mute: z10.coerce.number().min(0).max(1).optional().describe("Mute state (0=unmuted, 1=muted)"),
|
|
740
|
+
loopSource: z10.coerce.number().min(0).max(1).optional().describe("Loop source (0=no loop, 1=loop)")
|
|
741
|
+
},
|
|
742
|
+
async ({ trackIndex, itemIndex, position, length, mute, loopSource }) => {
|
|
743
|
+
const res = await sendCommand("set_midi_item_properties", {
|
|
744
|
+
trackIndex,
|
|
745
|
+
itemIndex,
|
|
746
|
+
position,
|
|
747
|
+
length,
|
|
748
|
+
mute,
|
|
749
|
+
loopSource
|
|
750
|
+
});
|
|
751
|
+
if (!res.success) {
|
|
752
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
753
|
+
}
|
|
754
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
755
|
+
}
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// apps/reaper-mcp-server/src/tools/media.ts
|
|
760
|
+
import { z as z11 } from "zod/v4";
|
|
761
|
+
function registerMediaTools(server) {
|
|
762
|
+
server.tool(
|
|
763
|
+
"list_media_items",
|
|
764
|
+
"List all media items on a track with position, length, name, volume, mute state, and whether it is MIDI or audio",
|
|
765
|
+
{
|
|
766
|
+
trackIndex: z11.coerce.number().min(0).describe("0-based track index")
|
|
767
|
+
},
|
|
768
|
+
async ({ trackIndex }) => {
|
|
769
|
+
const res = await sendCommand("list_media_items", { trackIndex });
|
|
770
|
+
if (!res.success) {
|
|
771
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
772
|
+
}
|
|
773
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
server.tool(
|
|
777
|
+
"get_media_item_properties",
|
|
778
|
+
"Get detailed properties of a media item: position, length, volume, fades, play rate, take info, source file",
|
|
779
|
+
{
|
|
780
|
+
trackIndex: z11.coerce.number().min(0).describe("0-based track index"),
|
|
781
|
+
itemIndex: z11.coerce.number().min(0).describe("0-based item index on the track")
|
|
782
|
+
},
|
|
783
|
+
async ({ trackIndex, itemIndex }) => {
|
|
784
|
+
const res = await sendCommand("get_media_item_properties", { trackIndex, itemIndex });
|
|
785
|
+
if (!res.success) {
|
|
786
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
787
|
+
}
|
|
788
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
789
|
+
}
|
|
790
|
+
);
|
|
791
|
+
server.tool(
|
|
792
|
+
"set_media_item_properties",
|
|
793
|
+
"Set media item properties: position (seconds), length (seconds), volume (dB), mute, fade in/out lengths, play rate",
|
|
794
|
+
{
|
|
795
|
+
trackIndex: z11.coerce.number().min(0).describe("0-based track index"),
|
|
796
|
+
itemIndex: z11.coerce.number().min(0).describe("0-based item index on the track"),
|
|
797
|
+
position: z11.coerce.number().min(0).optional().describe("New position in seconds"),
|
|
798
|
+
length: z11.coerce.number().min(0).optional().describe("New length in seconds"),
|
|
799
|
+
volume: z11.coerce.number().optional().describe("New volume in dB (0 = unity gain)"),
|
|
800
|
+
mute: z11.coerce.number().min(0).max(1).optional().describe("Mute state (0=unmuted, 1=muted)"),
|
|
801
|
+
fadeInLength: z11.coerce.number().min(0).optional().describe("Fade-in length in seconds"),
|
|
802
|
+
fadeOutLength: z11.coerce.number().min(0).optional().describe("Fade-out length in seconds"),
|
|
803
|
+
playRate: z11.coerce.number().min(0.1).max(10).optional().describe("Playback rate (1.0=normal, 0.5=half, 2.0=double)")
|
|
804
|
+
},
|
|
805
|
+
async ({ trackIndex, itemIndex, position, length, volume, mute, fadeInLength, fadeOutLength, playRate }) => {
|
|
806
|
+
const res = await sendCommand("set_media_item_properties", {
|
|
807
|
+
trackIndex,
|
|
808
|
+
itemIndex,
|
|
809
|
+
position,
|
|
810
|
+
length,
|
|
811
|
+
volume,
|
|
812
|
+
mute,
|
|
813
|
+
fadeInLength,
|
|
814
|
+
fadeOutLength,
|
|
815
|
+
playRate
|
|
816
|
+
});
|
|
817
|
+
if (!res.success) {
|
|
818
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
819
|
+
}
|
|
820
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
821
|
+
}
|
|
822
|
+
);
|
|
823
|
+
server.tool(
|
|
824
|
+
"split_media_item",
|
|
825
|
+
"Split a media item at a given position (absolute project time in seconds). Returns info about both resulting items.",
|
|
826
|
+
{
|
|
827
|
+
trackIndex: z11.coerce.number().min(0).describe("0-based track index"),
|
|
828
|
+
itemIndex: z11.coerce.number().min(0).describe("0-based item index on the track"),
|
|
829
|
+
position: z11.coerce.number().min(0).describe("Split position in seconds (absolute project time)")
|
|
830
|
+
},
|
|
831
|
+
async ({ trackIndex, itemIndex, position }) => {
|
|
832
|
+
const res = await sendCommand("split_media_item", { trackIndex, itemIndex, position });
|
|
833
|
+
if (!res.success) {
|
|
834
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
835
|
+
}
|
|
836
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
837
|
+
}
|
|
838
|
+
);
|
|
839
|
+
server.tool(
|
|
840
|
+
"delete_media_item",
|
|
841
|
+
"Delete a media item from a track",
|
|
842
|
+
{
|
|
843
|
+
trackIndex: z11.coerce.number().min(0).describe("0-based track index"),
|
|
844
|
+
itemIndex: z11.coerce.number().min(0).describe("0-based item index on the track")
|
|
845
|
+
},
|
|
846
|
+
async ({ trackIndex, itemIndex }) => {
|
|
847
|
+
const res = await sendCommand("delete_media_item", { trackIndex, itemIndex });
|
|
848
|
+
if (!res.success) {
|
|
849
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
850
|
+
}
|
|
851
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
server.tool(
|
|
855
|
+
"move_media_item",
|
|
856
|
+
"Move a media item to a new position and/or a different track",
|
|
857
|
+
{
|
|
858
|
+
trackIndex: z11.coerce.number().min(0).describe("0-based track index (current track)"),
|
|
859
|
+
itemIndex: z11.coerce.number().min(0).describe("0-based item index on the track"),
|
|
860
|
+
newPosition: z11.coerce.number().min(0).optional().describe("New position in seconds"),
|
|
861
|
+
newTrackIndex: z11.coerce.number().min(0).optional().describe("Move to this track (0-based index)")
|
|
862
|
+
},
|
|
863
|
+
async ({ trackIndex, itemIndex, newPosition, newTrackIndex }) => {
|
|
864
|
+
const res = await sendCommand("move_media_item", { trackIndex, itemIndex, newPosition, newTrackIndex });
|
|
865
|
+
if (!res.success) {
|
|
866
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
867
|
+
}
|
|
868
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
869
|
+
}
|
|
870
|
+
);
|
|
871
|
+
server.tool(
|
|
872
|
+
"trim_media_item",
|
|
873
|
+
"Trim a media item edges. Positive trimStart trims from the beginning (makes item shorter/later). Positive trimEnd trims from the end (makes item shorter). Negative values extend.",
|
|
874
|
+
{
|
|
875
|
+
trackIndex: z11.coerce.number().min(0).describe("0-based track index"),
|
|
876
|
+
itemIndex: z11.coerce.number().min(0).describe("0-based item index on the track"),
|
|
877
|
+
trimStart: z11.coerce.number().optional().describe("Seconds to trim from start (positive=trim in, negative=extend)"),
|
|
878
|
+
trimEnd: z11.coerce.number().optional().describe("Seconds to trim from end (positive=trim in, negative=extend)")
|
|
879
|
+
},
|
|
880
|
+
async ({ trackIndex, itemIndex, trimStart, trimEnd }) => {
|
|
881
|
+
const res = await sendCommand("trim_media_item", { trackIndex, itemIndex, trimStart, trimEnd });
|
|
882
|
+
if (!res.success) {
|
|
883
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
884
|
+
}
|
|
885
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
886
|
+
}
|
|
887
|
+
);
|
|
888
|
+
server.tool(
|
|
889
|
+
"add_stretch_marker",
|
|
890
|
+
"Add a stretch marker to a media item for time-stretching audio. Position is within the item (seconds from item start). Source position is the corresponding position in the original audio.",
|
|
891
|
+
{
|
|
892
|
+
trackIndex: z11.coerce.number().min(0).describe("0-based track index"),
|
|
893
|
+
itemIndex: z11.coerce.number().min(0).describe("0-based item index on the track"),
|
|
894
|
+
position: z11.coerce.number().min(0).describe("Position within the item in seconds (from item start)"),
|
|
895
|
+
sourcePosition: z11.coerce.number().min(0).optional().describe("Position in source audio in seconds (defaults to same as position)")
|
|
896
|
+
},
|
|
897
|
+
async ({ trackIndex, itemIndex, position, sourcePosition }) => {
|
|
898
|
+
const res = await sendCommand("add_stretch_marker", { trackIndex, itemIndex, position, sourcePosition });
|
|
899
|
+
if (!res.success) {
|
|
900
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
901
|
+
}
|
|
902
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
903
|
+
}
|
|
904
|
+
);
|
|
905
|
+
server.tool(
|
|
906
|
+
"get_stretch_markers",
|
|
907
|
+
"List all stretch markers in a media item with their positions and source positions",
|
|
908
|
+
{
|
|
909
|
+
trackIndex: z11.coerce.number().min(0).describe("0-based track index"),
|
|
910
|
+
itemIndex: z11.coerce.number().min(0).describe("0-based item index on the track")
|
|
911
|
+
},
|
|
912
|
+
async ({ trackIndex, itemIndex }) => {
|
|
913
|
+
const res = await sendCommand("get_stretch_markers", { trackIndex, itemIndex });
|
|
914
|
+
if (!res.success) {
|
|
915
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
916
|
+
}
|
|
917
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
918
|
+
}
|
|
919
|
+
);
|
|
920
|
+
server.tool(
|
|
921
|
+
"delete_stretch_marker",
|
|
922
|
+
"Delete a stretch marker by index from a media item",
|
|
923
|
+
{
|
|
924
|
+
trackIndex: z11.coerce.number().min(0).describe("0-based track index"),
|
|
925
|
+
itemIndex: z11.coerce.number().min(0).describe("0-based item index on the track"),
|
|
926
|
+
markerIndex: z11.coerce.number().min(0).describe("0-based stretch marker index")
|
|
927
|
+
},
|
|
928
|
+
async ({ trackIndex, itemIndex, markerIndex }) => {
|
|
929
|
+
const res = await sendCommand("delete_stretch_marker", { trackIndex, itemIndex, markerIndex });
|
|
930
|
+
if (!res.success) {
|
|
931
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
932
|
+
}
|
|
933
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
934
|
+
}
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
520
938
|
// apps/reaper-mcp-server/src/server.ts
|
|
521
939
|
function createServer() {
|
|
522
940
|
const server = new McpServer({
|
|
@@ -533,6 +951,8 @@ function createServer() {
|
|
|
533
951
|
registerSnapshotTools(server);
|
|
534
952
|
registerRoutingTools(server);
|
|
535
953
|
registerAnalysisTools(server);
|
|
954
|
+
registerMidiTools(server);
|
|
955
|
+
registerMediaTools(server);
|
|
536
956
|
return server;
|
|
537
957
|
}
|
|
538
958
|
|
package/package.json
CHANGED
package/reaper/mcp_bridge.lua
CHANGED
|
@@ -58,6 +58,30 @@ local function json_decode(str)
|
|
|
58
58
|
return obj
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
-- Fallback JSON array-of-objects parser for when CF_Json_Parse is unavailable.
|
|
62
|
+
-- Handles: [{"key":val,...}, {"key":val,...}, ...]
|
|
63
|
+
local function json_decode_array(str)
|
|
64
|
+
if reaper.CF_Json_Parse then
|
|
65
|
+
local ok, val = reaper.CF_Json_Parse(str)
|
|
66
|
+
if ok then return val end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
-- Fallback: extract each {...} from the array and parse individually
|
|
70
|
+
local arr = {}
|
|
71
|
+
for obj_str in str:gmatch("%b{}") do
|
|
72
|
+
local obj = {}
|
|
73
|
+
for k, v in obj_str:gmatch('"([^"]+)"%s*:%s*"([^"]*)"') do
|
|
74
|
+
obj[k] = v
|
|
75
|
+
end
|
|
76
|
+
for k, v in obj_str:gmatch('"([^"]+)"%s*:%s*(-?%d+%.?%d*)') do
|
|
77
|
+
if not obj[k] then obj[k] = tonumber(v) end
|
|
78
|
+
end
|
|
79
|
+
arr[#arr + 1] = obj
|
|
80
|
+
end
|
|
81
|
+
if #arr > 0 then return arr end
|
|
82
|
+
return nil
|
|
83
|
+
end
|
|
84
|
+
|
|
61
85
|
local function json_encode(obj)
|
|
62
86
|
-- Simple JSON encoder for our response objects
|
|
63
87
|
local parts = {}
|
|
@@ -1009,6 +1033,772 @@ function handlers.read_track_crest(params)
|
|
|
1009
1033
|
}
|
|
1010
1034
|
end
|
|
1011
1035
|
|
|
1036
|
+
-- =============================================================================
|
|
1037
|
+
-- MIDI editing handlers
|
|
1038
|
+
-- =============================================================================
|
|
1039
|
+
|
|
1040
|
+
-- Helper: get a MIDI take from track/item indices, with validation
|
|
1041
|
+
local function get_midi_take(params)
|
|
1042
|
+
local track_idx = params.trackIndex
|
|
1043
|
+
local item_idx = params.itemIndex
|
|
1044
|
+
if track_idx == nil then return nil, nil, nil, "trackIndex required" end
|
|
1045
|
+
if item_idx == nil then return nil, nil, nil, "itemIndex required" end
|
|
1046
|
+
|
|
1047
|
+
local track = reaper.GetTrack(0, track_idx)
|
|
1048
|
+
if not track then return nil, nil, nil, "Track " .. track_idx .. " not found" end
|
|
1049
|
+
|
|
1050
|
+
local item_count = reaper.CountTrackMediaItems(track)
|
|
1051
|
+
if item_idx >= item_count then
|
|
1052
|
+
return nil, nil, nil, "Item " .. item_idx .. " not found (track has " .. item_count .. " items)"
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
local item = reaper.GetTrackMediaItem(track, item_idx)
|
|
1056
|
+
if not item then return nil, nil, nil, "Item " .. item_idx .. " not found" end
|
|
1057
|
+
|
|
1058
|
+
local take = reaper.GetActiveTake(item)
|
|
1059
|
+
if not take then return nil, nil, nil, "Item has no active take" end
|
|
1060
|
+
|
|
1061
|
+
if not reaper.TakeIsMIDI(take) then
|
|
1062
|
+
return nil, nil, nil, "Item is not a MIDI item"
|
|
1063
|
+
end
|
|
1064
|
+
|
|
1065
|
+
return track, item, take, nil
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
-- Helper: get a media item from track/item indices, with validation
|
|
1069
|
+
local function get_media_item(params)
|
|
1070
|
+
local track_idx = params.trackIndex
|
|
1071
|
+
local item_idx = params.itemIndex
|
|
1072
|
+
if track_idx == nil then return nil, nil, "trackIndex required" end
|
|
1073
|
+
if item_idx == nil then return nil, nil, "itemIndex required" end
|
|
1074
|
+
|
|
1075
|
+
local track = reaper.GetTrack(0, track_idx)
|
|
1076
|
+
if not track then return nil, nil, "Track " .. track_idx .. " not found" end
|
|
1077
|
+
|
|
1078
|
+
local item_count = reaper.CountTrackMediaItems(track)
|
|
1079
|
+
if item_idx >= item_count then
|
|
1080
|
+
return nil, nil, "Item " .. item_idx .. " not found (track has " .. item_count .. " items)"
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
local item = reaper.GetTrackMediaItem(track, item_idx)
|
|
1084
|
+
if not item then return nil, nil, "Item " .. item_idx .. " not found" end
|
|
1085
|
+
|
|
1086
|
+
return track, item, nil
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
-- Helper: convert beats from item start to PPQ ticks
|
|
1090
|
+
local function beats_to_ppq(take, item, beats)
|
|
1091
|
+
local item_start = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
|
|
1092
|
+
-- Convert beats to project time (using project tempo), then to PPQ
|
|
1093
|
+
local proj_time = item_start + reaper.TimeMap2_QNToTime(0, reaper.TimeMap2_timeToQN(0, item_start) + beats) - item_start
|
|
1094
|
+
return reaper.MIDI_GetPPQPosFromProjTime(take, proj_time)
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
-- Helper: convert PPQ ticks to beats from item start
|
|
1098
|
+
local function ppq_to_beats(take, item)
|
|
1099
|
+
local item_start = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
|
|
1100
|
+
local item_start_qn = reaper.TimeMap2_timeToQN(0, item_start)
|
|
1101
|
+
return function(ppq)
|
|
1102
|
+
local proj_time = reaper.MIDI_GetProjTimeFromPPQPos(take, ppq)
|
|
1103
|
+
local proj_qn = reaper.TimeMap2_timeToQN(0, proj_time)
|
|
1104
|
+
return proj_qn - item_start_qn
|
|
1105
|
+
end
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
function handlers.create_midi_item(params)
|
|
1109
|
+
local track_idx = params.trackIndex
|
|
1110
|
+
local start_pos = params.startPosition
|
|
1111
|
+
local end_pos = params.endPosition
|
|
1112
|
+
if track_idx == nil then return nil, "trackIndex required" end
|
|
1113
|
+
if not start_pos or not end_pos then return nil, "startPosition and endPosition required" end
|
|
1114
|
+
if end_pos <= start_pos then return nil, "endPosition must be greater than startPosition" end
|
|
1115
|
+
|
|
1116
|
+
local track = reaper.GetTrack(0, track_idx)
|
|
1117
|
+
if not track then return nil, "Track " .. track_idx .. " not found" end
|
|
1118
|
+
|
|
1119
|
+
local item = reaper.CreateNewMIDIItemInProj(track, start_pos, end_pos)
|
|
1120
|
+
if not item then return nil, "Failed to create MIDI item" end
|
|
1121
|
+
|
|
1122
|
+
reaper.Undo_BeginBlock()
|
|
1123
|
+
reaper.Undo_EndBlock("MCP: Create MIDI item", -1)
|
|
1124
|
+
|
|
1125
|
+
-- Find the index of the new item on the track
|
|
1126
|
+
local item_count = reaper.CountTrackMediaItems(track)
|
|
1127
|
+
local new_idx = -1
|
|
1128
|
+
for i = 0, item_count - 1 do
|
|
1129
|
+
if reaper.GetTrackMediaItem(track, i) == item then
|
|
1130
|
+
new_idx = i
|
|
1131
|
+
break
|
|
1132
|
+
end
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
reaper.UpdateArrange()
|
|
1136
|
+
|
|
1137
|
+
return {
|
|
1138
|
+
trackIndex = track_idx,
|
|
1139
|
+
itemIndex = new_idx,
|
|
1140
|
+
position = start_pos,
|
|
1141
|
+
length = end_pos - start_pos,
|
|
1142
|
+
}
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
function handlers.list_midi_items(params)
|
|
1146
|
+
local track_idx = params.trackIndex
|
|
1147
|
+
if track_idx == nil then return nil, "trackIndex required" end
|
|
1148
|
+
|
|
1149
|
+
local track = reaper.GetTrack(0, track_idx)
|
|
1150
|
+
if not track then return nil, "Track " .. track_idx .. " not found" end
|
|
1151
|
+
|
|
1152
|
+
local items = {}
|
|
1153
|
+
local item_count = reaper.CountTrackMediaItems(track)
|
|
1154
|
+
for i = 0, item_count - 1 do
|
|
1155
|
+
local item = reaper.GetTrackMediaItem(track, i)
|
|
1156
|
+
local take = reaper.GetActiveTake(item)
|
|
1157
|
+
if take and reaper.TakeIsMIDI(take) then
|
|
1158
|
+
local _, note_cnt, cc_cnt, _ = reaper.MIDI_CountEvts(take)
|
|
1159
|
+
items[#items + 1] = {
|
|
1160
|
+
itemIndex = i,
|
|
1161
|
+
position = reaper.GetMediaItemInfo_Value(item, "D_POSITION"),
|
|
1162
|
+
length = reaper.GetMediaItemInfo_Value(item, "D_LENGTH"),
|
|
1163
|
+
noteCount = note_cnt,
|
|
1164
|
+
ccCount = cc_cnt,
|
|
1165
|
+
muted = reaper.GetMediaItemInfo_Value(item, "B_MUTE") ~= 0,
|
|
1166
|
+
}
|
|
1167
|
+
end
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
return { trackIndex = track_idx, items = items, total = #items }
|
|
1171
|
+
end
|
|
1172
|
+
|
|
1173
|
+
function handlers.get_midi_notes(params)
|
|
1174
|
+
local track, item, take, err = get_midi_take(params)
|
|
1175
|
+
if err then return nil, err end
|
|
1176
|
+
|
|
1177
|
+
local _, note_cnt, _, _ = reaper.MIDI_CountEvts(take)
|
|
1178
|
+
local to_beats = ppq_to_beats(take, item)
|
|
1179
|
+
local notes = {}
|
|
1180
|
+
|
|
1181
|
+
for i = 0, note_cnt - 1 do
|
|
1182
|
+
local _, sel, muted, start_ppq, end_ppq, chan, pitch, vel = reaper.MIDI_GetNote(take, i)
|
|
1183
|
+
local start_beats = to_beats(start_ppq)
|
|
1184
|
+
local end_beats = to_beats(end_ppq)
|
|
1185
|
+
notes[#notes + 1] = {
|
|
1186
|
+
noteIndex = i,
|
|
1187
|
+
pitch = pitch,
|
|
1188
|
+
velocity = vel,
|
|
1189
|
+
startPosition = start_beats,
|
|
1190
|
+
duration = end_beats - start_beats,
|
|
1191
|
+
channel = chan,
|
|
1192
|
+
selected = sel,
|
|
1193
|
+
muted = muted,
|
|
1194
|
+
}
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
return { trackIndex = params.trackIndex, itemIndex = params.itemIndex, notes = notes, total = #notes }
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1200
|
+
function handlers.insert_midi_note(params)
|
|
1201
|
+
local track, item, take, err = get_midi_take(params)
|
|
1202
|
+
if err then return nil, err end
|
|
1203
|
+
|
|
1204
|
+
local pitch = params.pitch
|
|
1205
|
+
local vel = params.velocity
|
|
1206
|
+
local start_pos = params.startPosition
|
|
1207
|
+
local dur = params.duration
|
|
1208
|
+
local chan = params.channel or 0
|
|
1209
|
+
|
|
1210
|
+
if not pitch or not vel or not start_pos or not dur then
|
|
1211
|
+
return nil, "pitch, velocity, startPosition, and duration required"
|
|
1212
|
+
end
|
|
1213
|
+
|
|
1214
|
+
-- Clamp values
|
|
1215
|
+
pitch = math.max(0, math.min(127, math.floor(pitch)))
|
|
1216
|
+
vel = math.max(1, math.min(127, math.floor(vel)))
|
|
1217
|
+
chan = math.max(0, math.min(15, math.floor(chan)))
|
|
1218
|
+
|
|
1219
|
+
local start_ppq = beats_to_ppq(take, item, start_pos)
|
|
1220
|
+
local end_ppq = beats_to_ppq(take, item, start_pos + dur)
|
|
1221
|
+
|
|
1222
|
+
reaper.Undo_BeginBlock()
|
|
1223
|
+
reaper.MIDI_InsertNote(take, false, false, start_ppq, end_ppq, chan, pitch, vel, false)
|
|
1224
|
+
reaper.MIDI_Sort(take)
|
|
1225
|
+
reaper.Undo_EndBlock("MCP: Insert MIDI note", -1)
|
|
1226
|
+
|
|
1227
|
+
reaper.UpdateArrange()
|
|
1228
|
+
|
|
1229
|
+
local _, note_cnt, _, _ = reaper.MIDI_CountEvts(take)
|
|
1230
|
+
return { success = true, noteCount = note_cnt }
|
|
1231
|
+
end
|
|
1232
|
+
|
|
1233
|
+
function handlers.insert_midi_notes(params)
|
|
1234
|
+
local track, item, take, err = get_midi_take(params)
|
|
1235
|
+
if err then return nil, err end
|
|
1236
|
+
|
|
1237
|
+
local notes_str = params.notes
|
|
1238
|
+
if not notes_str or notes_str == "" then return nil, "notes JSON string required" end
|
|
1239
|
+
|
|
1240
|
+
-- Parse notes JSON array (uses dedicated array parser with fallback)
|
|
1241
|
+
local notes_list = json_decode_array(notes_str)
|
|
1242
|
+
if not notes_list or #notes_list == 0 then
|
|
1243
|
+
return nil, "Failed to parse notes JSON array. Expected: [{\"pitch\":60,\"velocity\":100,\"startPosition\":0,\"duration\":1}, ...]"
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1246
|
+
reaper.Undo_BeginBlock()
|
|
1247
|
+
local inserted = 0
|
|
1248
|
+
for _, note in ipairs(notes_list) do
|
|
1249
|
+
local pitch = note.pitch
|
|
1250
|
+
local vel = note.velocity
|
|
1251
|
+
local start_pos = note.startPosition
|
|
1252
|
+
local dur = note.duration
|
|
1253
|
+
local chan = note.channel or 0
|
|
1254
|
+
|
|
1255
|
+
if pitch and vel and start_pos and dur then
|
|
1256
|
+
pitch = math.max(0, math.min(127, math.floor(pitch)))
|
|
1257
|
+
vel = math.max(1, math.min(127, math.floor(vel)))
|
|
1258
|
+
chan = math.max(0, math.min(15, math.floor(chan)))
|
|
1259
|
+
|
|
1260
|
+
local start_ppq = beats_to_ppq(take, item, start_pos)
|
|
1261
|
+
local end_ppq = beats_to_ppq(take, item, start_pos + dur)
|
|
1262
|
+
|
|
1263
|
+
reaper.MIDI_InsertNote(take, false, false, start_ppq, end_ppq, chan, pitch, vel, true)
|
|
1264
|
+
inserted = inserted + 1
|
|
1265
|
+
end
|
|
1266
|
+
end
|
|
1267
|
+
reaper.MIDI_Sort(take)
|
|
1268
|
+
reaper.Undo_EndBlock("MCP: Insert " .. inserted .. " MIDI notes", -1)
|
|
1269
|
+
|
|
1270
|
+
reaper.UpdateArrange()
|
|
1271
|
+
|
|
1272
|
+
local _, note_cnt, _, _ = reaper.MIDI_CountEvts(take)
|
|
1273
|
+
return { success = true, inserted = inserted, noteCount = note_cnt }
|
|
1274
|
+
end
|
|
1275
|
+
|
|
1276
|
+
function handlers.edit_midi_note(params)
|
|
1277
|
+
local track, item, take, err = get_midi_take(params)
|
|
1278
|
+
if err then return nil, err end
|
|
1279
|
+
|
|
1280
|
+
local note_idx = params.noteIndex
|
|
1281
|
+
if note_idx == nil then return nil, "noteIndex required" end
|
|
1282
|
+
|
|
1283
|
+
local _, note_cnt, _, _ = reaper.MIDI_CountEvts(take)
|
|
1284
|
+
if note_idx >= note_cnt then
|
|
1285
|
+
return nil, "Note index " .. note_idx .. " out of range (item has " .. note_cnt .. " notes)"
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
-- Get current note values
|
|
1289
|
+
local _, sel, muted, cur_start_ppq, cur_end_ppq, cur_chan, cur_pitch, cur_vel = reaper.MIDI_GetNote(take, note_idx)
|
|
1290
|
+
|
|
1291
|
+
-- Apply changes (nil means keep current)
|
|
1292
|
+
local new_pitch = params.pitch and math.max(0, math.min(127, math.floor(params.pitch))) or nil
|
|
1293
|
+
local new_vel = params.velocity and math.max(1, math.min(127, math.floor(params.velocity))) or nil
|
|
1294
|
+
local new_chan = params.channel and math.max(0, math.min(15, math.floor(params.channel))) or nil
|
|
1295
|
+
|
|
1296
|
+
local new_start_ppq = nil
|
|
1297
|
+
local new_end_ppq = nil
|
|
1298
|
+
if params.startPosition ~= nil then
|
|
1299
|
+
new_start_ppq = beats_to_ppq(take, item, params.startPosition)
|
|
1300
|
+
if params.duration ~= nil then
|
|
1301
|
+
new_end_ppq = beats_to_ppq(take, item, params.startPosition + params.duration)
|
|
1302
|
+
else
|
|
1303
|
+
-- Keep same duration
|
|
1304
|
+
local dur_ppq = cur_end_ppq - cur_start_ppq
|
|
1305
|
+
new_end_ppq = new_start_ppq + dur_ppq
|
|
1306
|
+
end
|
|
1307
|
+
elseif params.duration ~= nil then
|
|
1308
|
+
-- Change duration only, keep start
|
|
1309
|
+
local to_beats = ppq_to_beats(take, item)
|
|
1310
|
+
local cur_start_beats = to_beats(cur_start_ppq)
|
|
1311
|
+
new_end_ppq = beats_to_ppq(take, item, cur_start_beats + params.duration)
|
|
1312
|
+
end
|
|
1313
|
+
|
|
1314
|
+
reaper.Undo_BeginBlock()
|
|
1315
|
+
reaper.MIDI_SetNote(take, note_idx, sel, muted, new_start_ppq, new_end_ppq, new_chan, new_pitch, new_vel, false)
|
|
1316
|
+
reaper.MIDI_Sort(take)
|
|
1317
|
+
reaper.Undo_EndBlock("MCP: Edit MIDI note " .. note_idx, -1)
|
|
1318
|
+
|
|
1319
|
+
reaper.UpdateArrange()
|
|
1320
|
+
|
|
1321
|
+
return { success = true, noteIndex = note_idx }
|
|
1322
|
+
end
|
|
1323
|
+
|
|
1324
|
+
function handlers.delete_midi_note(params)
|
|
1325
|
+
local track, item, take, err = get_midi_take(params)
|
|
1326
|
+
if err then return nil, err end
|
|
1327
|
+
|
|
1328
|
+
local note_idx = params.noteIndex
|
|
1329
|
+
if note_idx == nil then return nil, "noteIndex required" end
|
|
1330
|
+
|
|
1331
|
+
local _, note_cnt, _, _ = reaper.MIDI_CountEvts(take)
|
|
1332
|
+
if note_idx >= note_cnt then
|
|
1333
|
+
return nil, "Note index " .. note_idx .. " out of range (item has " .. note_cnt .. " notes)"
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1336
|
+
reaper.Undo_BeginBlock()
|
|
1337
|
+
reaper.MIDI_DeleteNote(take, note_idx)
|
|
1338
|
+
reaper.Undo_EndBlock("MCP: Delete MIDI note " .. note_idx, -1)
|
|
1339
|
+
|
|
1340
|
+
reaper.UpdateArrange()
|
|
1341
|
+
|
|
1342
|
+
local _, new_cnt, _, _ = reaper.MIDI_CountEvts(take)
|
|
1343
|
+
return { success = true, noteCount = new_cnt }
|
|
1344
|
+
end
|
|
1345
|
+
|
|
1346
|
+
function handlers.get_midi_cc(params)
|
|
1347
|
+
local track, item, take, err = get_midi_take(params)
|
|
1348
|
+
if err then return nil, err end
|
|
1349
|
+
|
|
1350
|
+
local _, _, cc_cnt, _ = reaper.MIDI_CountEvts(take)
|
|
1351
|
+
local to_beats = ppq_to_beats(take, item)
|
|
1352
|
+
local cc_filter = params.ccNumber
|
|
1353
|
+
local events = {}
|
|
1354
|
+
|
|
1355
|
+
for i = 0, cc_cnt - 1 do
|
|
1356
|
+
local _, sel, muted, ppq, chanmsg, chan, msg2, msg3 = reaper.MIDI_GetCC(take, i)
|
|
1357
|
+
-- msg2 = CC number, msg3 = CC value (for standard CC messages, chanmsg=176)
|
|
1358
|
+
if chanmsg == 176 then -- standard CC
|
|
1359
|
+
if cc_filter == nil or msg2 == cc_filter then
|
|
1360
|
+
events[#events + 1] = {
|
|
1361
|
+
ccIndex = i,
|
|
1362
|
+
ccNumber = msg2,
|
|
1363
|
+
value = msg3,
|
|
1364
|
+
position = to_beats(ppq),
|
|
1365
|
+
channel = chan,
|
|
1366
|
+
}
|
|
1367
|
+
end
|
|
1368
|
+
end
|
|
1369
|
+
end
|
|
1370
|
+
|
|
1371
|
+
return { trackIndex = params.trackIndex, itemIndex = params.itemIndex, events = events, total = #events }
|
|
1372
|
+
end
|
|
1373
|
+
|
|
1374
|
+
function handlers.insert_midi_cc(params)
|
|
1375
|
+
local track, item, take, err = get_midi_take(params)
|
|
1376
|
+
if err then return nil, err end
|
|
1377
|
+
|
|
1378
|
+
local cc_num = params.ccNumber
|
|
1379
|
+
local value = params.value
|
|
1380
|
+
local pos = params.position
|
|
1381
|
+
local chan = params.channel or 0
|
|
1382
|
+
|
|
1383
|
+
if not cc_num or not value or not pos then
|
|
1384
|
+
return nil, "ccNumber, value, and position required"
|
|
1385
|
+
end
|
|
1386
|
+
|
|
1387
|
+
cc_num = math.max(0, math.min(127, math.floor(cc_num)))
|
|
1388
|
+
value = math.max(0, math.min(127, math.floor(value)))
|
|
1389
|
+
chan = math.max(0, math.min(15, math.floor(chan)))
|
|
1390
|
+
|
|
1391
|
+
local ppq = beats_to_ppq(take, item, pos)
|
|
1392
|
+
|
|
1393
|
+
reaper.Undo_BeginBlock()
|
|
1394
|
+
-- chanmsg 176 = 0xB0 = CC message
|
|
1395
|
+
reaper.MIDI_InsertCC(take, false, false, ppq, 176, chan, cc_num, value)
|
|
1396
|
+
reaper.MIDI_Sort(take)
|
|
1397
|
+
reaper.Undo_EndBlock("MCP: Insert MIDI CC " .. cc_num, -1)
|
|
1398
|
+
|
|
1399
|
+
reaper.UpdateArrange()
|
|
1400
|
+
|
|
1401
|
+
local _, _, cc_cnt, _ = reaper.MIDI_CountEvts(take)
|
|
1402
|
+
return { success = true, ccCount = cc_cnt }
|
|
1403
|
+
end
|
|
1404
|
+
|
|
1405
|
+
function handlers.delete_midi_cc(params)
|
|
1406
|
+
local track, item, take, err = get_midi_take(params)
|
|
1407
|
+
if err then return nil, err end
|
|
1408
|
+
|
|
1409
|
+
local cc_idx = params.ccIndex
|
|
1410
|
+
if cc_idx == nil then return nil, "ccIndex required" end
|
|
1411
|
+
|
|
1412
|
+
local _, _, cc_cnt, _ = reaper.MIDI_CountEvts(take)
|
|
1413
|
+
if cc_idx >= cc_cnt then
|
|
1414
|
+
return nil, "CC index " .. cc_idx .. " out of range (item has " .. cc_cnt .. " CC events)"
|
|
1415
|
+
end
|
|
1416
|
+
|
|
1417
|
+
reaper.Undo_BeginBlock()
|
|
1418
|
+
reaper.MIDI_DeleteCC(take, cc_idx)
|
|
1419
|
+
reaper.Undo_EndBlock("MCP: Delete MIDI CC " .. cc_idx, -1)
|
|
1420
|
+
|
|
1421
|
+
reaper.UpdateArrange()
|
|
1422
|
+
|
|
1423
|
+
local _, _, new_cnt, _ = reaper.MIDI_CountEvts(take)
|
|
1424
|
+
return { success = true, ccCount = new_cnt }
|
|
1425
|
+
end
|
|
1426
|
+
|
|
1427
|
+
function handlers.get_midi_item_properties(params)
|
|
1428
|
+
local track, item, take, err = get_midi_take(params)
|
|
1429
|
+
if err then return nil, err end
|
|
1430
|
+
|
|
1431
|
+
local _, note_cnt, cc_cnt, _ = reaper.MIDI_CountEvts(take)
|
|
1432
|
+
|
|
1433
|
+
return {
|
|
1434
|
+
trackIndex = params.trackIndex,
|
|
1435
|
+
itemIndex = params.itemIndex,
|
|
1436
|
+
position = reaper.GetMediaItemInfo_Value(item, "D_POSITION"),
|
|
1437
|
+
length = reaper.GetMediaItemInfo_Value(item, "D_LENGTH"),
|
|
1438
|
+
noteCount = note_cnt,
|
|
1439
|
+
ccCount = cc_cnt,
|
|
1440
|
+
muted = reaper.GetMediaItemInfo_Value(item, "B_MUTE") ~= 0,
|
|
1441
|
+
loopSource = reaper.GetMediaItemInfo_Value(item, "B_LOOPSRC") ~= 0,
|
|
1442
|
+
}
|
|
1443
|
+
end
|
|
1444
|
+
|
|
1445
|
+
function handlers.set_midi_item_properties(params)
|
|
1446
|
+
local track, item, take, err = get_midi_take(params)
|
|
1447
|
+
if err then return nil, err end
|
|
1448
|
+
|
|
1449
|
+
reaper.Undo_BeginBlock()
|
|
1450
|
+
|
|
1451
|
+
if params.position ~= nil then
|
|
1452
|
+
reaper.SetMediaItemInfo_Value(item, "D_POSITION", params.position)
|
|
1453
|
+
end
|
|
1454
|
+
if params.length ~= nil then
|
|
1455
|
+
reaper.SetMediaItemInfo_Value(item, "D_LENGTH", params.length)
|
|
1456
|
+
end
|
|
1457
|
+
if params.mute ~= nil then
|
|
1458
|
+
reaper.SetMediaItemInfo_Value(item, "B_MUTE", params.mute)
|
|
1459
|
+
end
|
|
1460
|
+
if params.loopSource ~= nil then
|
|
1461
|
+
reaper.SetMediaItemInfo_Value(item, "B_LOOPSRC", params.loopSource)
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
reaper.Undo_EndBlock("MCP: Set MIDI item properties", -1)
|
|
1465
|
+
reaper.UpdateArrange()
|
|
1466
|
+
|
|
1467
|
+
return { success = true, trackIndex = params.trackIndex, itemIndex = params.itemIndex }
|
|
1468
|
+
end
|
|
1469
|
+
|
|
1470
|
+
-- =============================================================================
|
|
1471
|
+
-- Media item editing handlers
|
|
1472
|
+
-- =============================================================================
|
|
1473
|
+
|
|
1474
|
+
function handlers.list_media_items(params)
|
|
1475
|
+
local track_idx = params.trackIndex
|
|
1476
|
+
if track_idx == nil then return nil, "trackIndex required" end
|
|
1477
|
+
|
|
1478
|
+
local track = reaper.GetTrack(0, track_idx)
|
|
1479
|
+
if not track then return nil, "Track " .. track_idx .. " not found" end
|
|
1480
|
+
|
|
1481
|
+
local items = {}
|
|
1482
|
+
local item_count = reaper.CountTrackMediaItems(track)
|
|
1483
|
+
for i = 0, item_count - 1 do
|
|
1484
|
+
local item = reaper.GetTrackMediaItem(track, i)
|
|
1485
|
+
local take = reaper.GetActiveTake(item)
|
|
1486
|
+
local take_name = ""
|
|
1487
|
+
local is_midi = false
|
|
1488
|
+
if take then
|
|
1489
|
+
take_name = reaper.GetTakeName(take) or ""
|
|
1490
|
+
is_midi = reaper.TakeIsMIDI(take)
|
|
1491
|
+
end
|
|
1492
|
+
|
|
1493
|
+
local vol_raw = reaper.GetMediaItemInfo_Value(item, "D_VOL")
|
|
1494
|
+
items[#items + 1] = {
|
|
1495
|
+
itemIndex = i,
|
|
1496
|
+
position = reaper.GetMediaItemInfo_Value(item, "D_POSITION"),
|
|
1497
|
+
length = reaper.GetMediaItemInfo_Value(item, "D_LENGTH"),
|
|
1498
|
+
name = take_name,
|
|
1499
|
+
volume = to_db(vol_raw),
|
|
1500
|
+
muted = reaper.GetMediaItemInfo_Value(item, "B_MUTE") ~= 0,
|
|
1501
|
+
fadeInLength = reaper.GetMediaItemInfo_Value(item, "D_FADEINLEN"),
|
|
1502
|
+
fadeOutLength = reaper.GetMediaItemInfo_Value(item, "D_FADEOUTLEN"),
|
|
1503
|
+
playRate = take and reaper.GetMediaItemTakeInfo_Value(take, "D_PLAYRATE") or 1.0,
|
|
1504
|
+
isMidi = is_midi,
|
|
1505
|
+
takeName = take_name,
|
|
1506
|
+
}
|
|
1507
|
+
end
|
|
1508
|
+
|
|
1509
|
+
return { trackIndex = track_idx, items = items, total = #items }
|
|
1510
|
+
end
|
|
1511
|
+
|
|
1512
|
+
function handlers.get_media_item_properties(params)
|
|
1513
|
+
local track, item, err = get_media_item(params)
|
|
1514
|
+
if err then return nil, err end
|
|
1515
|
+
|
|
1516
|
+
local take = reaper.GetActiveTake(item)
|
|
1517
|
+
local take_name = ""
|
|
1518
|
+
local is_midi = false
|
|
1519
|
+
local play_rate = 1.0
|
|
1520
|
+
local pitch = 0.0
|
|
1521
|
+
local start_offset = 0.0
|
|
1522
|
+
local source_file = ""
|
|
1523
|
+
|
|
1524
|
+
if take then
|
|
1525
|
+
take_name = reaper.GetTakeName(take) or ""
|
|
1526
|
+
is_midi = reaper.TakeIsMIDI(take)
|
|
1527
|
+
play_rate = reaper.GetMediaItemTakeInfo_Value(take, "D_PLAYRATE")
|
|
1528
|
+
pitch = reaper.GetMediaItemTakeInfo_Value(take, "D_PITCH")
|
|
1529
|
+
start_offset = reaper.GetMediaItemTakeInfo_Value(take, "D_STARTOFFS")
|
|
1530
|
+
local source = reaper.GetMediaItemTake_Source(take)
|
|
1531
|
+
if source then
|
|
1532
|
+
local _, src_fn = reaper.GetMediaSourceFileName(source, "")
|
|
1533
|
+
source_file = src_fn or ""
|
|
1534
|
+
end
|
|
1535
|
+
end
|
|
1536
|
+
|
|
1537
|
+
local vol_raw = reaper.GetMediaItemInfo_Value(item, "D_VOL")
|
|
1538
|
+
|
|
1539
|
+
return {
|
|
1540
|
+
trackIndex = params.trackIndex,
|
|
1541
|
+
itemIndex = params.itemIndex,
|
|
1542
|
+
position = reaper.GetMediaItemInfo_Value(item, "D_POSITION"),
|
|
1543
|
+
length = reaper.GetMediaItemInfo_Value(item, "D_LENGTH"),
|
|
1544
|
+
name = take_name,
|
|
1545
|
+
volume = to_db(vol_raw),
|
|
1546
|
+
volumeRaw = vol_raw,
|
|
1547
|
+
muted = reaper.GetMediaItemInfo_Value(item, "B_MUTE") ~= 0,
|
|
1548
|
+
fadeInLength = reaper.GetMediaItemInfo_Value(item, "D_FADEINLEN"),
|
|
1549
|
+
fadeOutLength = reaper.GetMediaItemInfo_Value(item, "D_FADEOUTLEN"),
|
|
1550
|
+
fadeInShape = reaper.GetMediaItemInfo_Value(item, "C_FADEINSHAPE"),
|
|
1551
|
+
fadeOutShape = reaper.GetMediaItemInfo_Value(item, "C_FADEOUTSHAPE"),
|
|
1552
|
+
playRate = play_rate,
|
|
1553
|
+
pitch = pitch,
|
|
1554
|
+
startOffset = start_offset,
|
|
1555
|
+
loopSource = reaper.GetMediaItemInfo_Value(item, "B_LOOPSRC") ~= 0,
|
|
1556
|
+
isMidi = is_midi,
|
|
1557
|
+
takeName = take_name,
|
|
1558
|
+
sourceFile = source_file,
|
|
1559
|
+
locked = reaper.GetMediaItemInfo_Value(item, "C_LOCK") ~= 0,
|
|
1560
|
+
}
|
|
1561
|
+
end
|
|
1562
|
+
|
|
1563
|
+
function handlers.set_media_item_properties(params)
|
|
1564
|
+
local track, item, err = get_media_item(params)
|
|
1565
|
+
if err then return nil, err end
|
|
1566
|
+
|
|
1567
|
+
reaper.Undo_BeginBlock()
|
|
1568
|
+
|
|
1569
|
+
if params.position ~= nil then
|
|
1570
|
+
reaper.SetMediaItemInfo_Value(item, "D_POSITION", params.position)
|
|
1571
|
+
end
|
|
1572
|
+
if params.length ~= nil then
|
|
1573
|
+
reaper.SetMediaItemInfo_Value(item, "D_LENGTH", params.length)
|
|
1574
|
+
end
|
|
1575
|
+
if params.volume ~= nil then
|
|
1576
|
+
reaper.SetMediaItemInfo_Value(item, "D_VOL", from_db(params.volume))
|
|
1577
|
+
end
|
|
1578
|
+
if params.mute ~= nil then
|
|
1579
|
+
reaper.SetMediaItemInfo_Value(item, "B_MUTE", params.mute)
|
|
1580
|
+
end
|
|
1581
|
+
if params.fadeInLength ~= nil then
|
|
1582
|
+
reaper.SetMediaItemInfo_Value(item, "D_FADEINLEN", params.fadeInLength)
|
|
1583
|
+
end
|
|
1584
|
+
if params.fadeOutLength ~= nil then
|
|
1585
|
+
reaper.SetMediaItemInfo_Value(item, "D_FADEOUTLEN", params.fadeOutLength)
|
|
1586
|
+
end
|
|
1587
|
+
if params.playRate ~= nil then
|
|
1588
|
+
local take = reaper.GetActiveTake(item)
|
|
1589
|
+
if take then
|
|
1590
|
+
reaper.SetMediaItemTakeInfo_Value(take, "D_PLAYRATE", params.playRate)
|
|
1591
|
+
end
|
|
1592
|
+
end
|
|
1593
|
+
|
|
1594
|
+
reaper.Undo_EndBlock("MCP: Set media item properties", -1)
|
|
1595
|
+
reaper.UpdateArrange()
|
|
1596
|
+
|
|
1597
|
+
return { success = true, trackIndex = params.trackIndex, itemIndex = params.itemIndex }
|
|
1598
|
+
end
|
|
1599
|
+
|
|
1600
|
+
function handlers.split_media_item(params)
|
|
1601
|
+
local track, item, err = get_media_item(params)
|
|
1602
|
+
if err then return nil, err end
|
|
1603
|
+
|
|
1604
|
+
local position = params.position
|
|
1605
|
+
if not position then return nil, "position required" end
|
|
1606
|
+
|
|
1607
|
+
local item_start = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
|
|
1608
|
+
local item_end = item_start + reaper.GetMediaItemInfo_Value(item, "D_LENGTH")
|
|
1609
|
+
if position <= item_start or position >= item_end then
|
|
1610
|
+
return nil, "Split position must be within item bounds (" .. item_start .. "s - " .. item_end .. "s)"
|
|
1611
|
+
end
|
|
1612
|
+
|
|
1613
|
+
reaper.Undo_BeginBlock()
|
|
1614
|
+
local right_item = reaper.SplitMediaItem(item, position)
|
|
1615
|
+
reaper.Undo_EndBlock("MCP: Split media item", -1)
|
|
1616
|
+
|
|
1617
|
+
if not right_item then return nil, "Failed to split item at position " .. position end
|
|
1618
|
+
|
|
1619
|
+
reaper.UpdateArrange()
|
|
1620
|
+
|
|
1621
|
+
return {
|
|
1622
|
+
success = true,
|
|
1623
|
+
leftItem = {
|
|
1624
|
+
position = reaper.GetMediaItemInfo_Value(item, "D_POSITION"),
|
|
1625
|
+
length = reaper.GetMediaItemInfo_Value(item, "D_LENGTH"),
|
|
1626
|
+
},
|
|
1627
|
+
rightItem = {
|
|
1628
|
+
position = reaper.GetMediaItemInfo_Value(right_item, "D_POSITION"),
|
|
1629
|
+
length = reaper.GetMediaItemInfo_Value(right_item, "D_LENGTH"),
|
|
1630
|
+
},
|
|
1631
|
+
}
|
|
1632
|
+
end
|
|
1633
|
+
|
|
1634
|
+
function handlers.delete_media_item(params)
|
|
1635
|
+
local track, item, err = get_media_item(params)
|
|
1636
|
+
if err then return nil, err end
|
|
1637
|
+
|
|
1638
|
+
reaper.Undo_BeginBlock()
|
|
1639
|
+
local ok = reaper.DeleteTrackMediaItem(track, item)
|
|
1640
|
+
reaper.Undo_EndBlock("MCP: Delete media item", -1)
|
|
1641
|
+
|
|
1642
|
+
if not ok then return nil, "Failed to delete item" end
|
|
1643
|
+
|
|
1644
|
+
reaper.UpdateArrange()
|
|
1645
|
+
|
|
1646
|
+
return { success = true, trackIndex = params.trackIndex, itemIndex = params.itemIndex }
|
|
1647
|
+
end
|
|
1648
|
+
|
|
1649
|
+
function handlers.move_media_item(params)
|
|
1650
|
+
local track, item, err = get_media_item(params)
|
|
1651
|
+
if err then return nil, err end
|
|
1652
|
+
|
|
1653
|
+
-- Validate destination track before starting undo block
|
|
1654
|
+
if params.newTrackIndex ~= nil then
|
|
1655
|
+
local dest_track = reaper.GetTrack(0, params.newTrackIndex)
|
|
1656
|
+
if not dest_track then
|
|
1657
|
+
return nil, "Destination track " .. params.newTrackIndex .. " not found"
|
|
1658
|
+
end
|
|
1659
|
+
end
|
|
1660
|
+
|
|
1661
|
+
reaper.Undo_BeginBlock()
|
|
1662
|
+
|
|
1663
|
+
-- Move track first, then set position (MoveMediaItemToTrack preserves position)
|
|
1664
|
+
if params.newTrackIndex ~= nil then
|
|
1665
|
+
local dest_track = reaper.GetTrack(0, params.newTrackIndex)
|
|
1666
|
+
reaper.MoveMediaItemToTrack(item, dest_track)
|
|
1667
|
+
end
|
|
1668
|
+
|
|
1669
|
+
if params.newPosition ~= nil then
|
|
1670
|
+
reaper.SetMediaItemInfo_Value(item, "D_POSITION", params.newPosition)
|
|
1671
|
+
end
|
|
1672
|
+
|
|
1673
|
+
reaper.Undo_EndBlock("MCP: Move media item", -1)
|
|
1674
|
+
reaper.UpdateArrange()
|
|
1675
|
+
|
|
1676
|
+
return {
|
|
1677
|
+
success = true,
|
|
1678
|
+
position = reaper.GetMediaItemInfo_Value(item, "D_POSITION"),
|
|
1679
|
+
trackIndex = params.newTrackIndex or params.trackIndex,
|
|
1680
|
+
}
|
|
1681
|
+
end
|
|
1682
|
+
|
|
1683
|
+
function handlers.trim_media_item(params)
|
|
1684
|
+
local track, item, err = get_media_item(params)
|
|
1685
|
+
if err then return nil, err end
|
|
1686
|
+
|
|
1687
|
+
local pos = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
|
|
1688
|
+
local len = reaper.GetMediaItemInfo_Value(item, "D_LENGTH")
|
|
1689
|
+
|
|
1690
|
+
-- Validate both trims upfront before applying either
|
|
1691
|
+
local trim_start = (params.trimStart ~= nil and params.trimStart ~= 0) and params.trimStart or 0
|
|
1692
|
+
local trim_end = (params.trimEnd ~= nil and params.trimEnd ~= 0) and params.trimEnd or 0
|
|
1693
|
+
local new_len = len - trim_start - trim_end
|
|
1694
|
+
if new_len <= 0 then
|
|
1695
|
+
return nil, "Trim would result in zero or negative length (current: " .. len .. "s, trimStart: " .. trim_start .. "s, trimEnd: " .. trim_end .. "s)"
|
|
1696
|
+
end
|
|
1697
|
+
|
|
1698
|
+
reaper.Undo_BeginBlock()
|
|
1699
|
+
|
|
1700
|
+
local take = reaper.GetActiveTake(item)
|
|
1701
|
+
|
|
1702
|
+
if trim_start ~= 0 then
|
|
1703
|
+
pos = pos + trim_start
|
|
1704
|
+
reaper.SetMediaItemInfo_Value(item, "D_POSITION", pos)
|
|
1705
|
+
-- Adjust take start offset
|
|
1706
|
+
if take then
|
|
1707
|
+
local offset = reaper.GetMediaItemTakeInfo_Value(take, "D_STARTOFFS")
|
|
1708
|
+
reaper.SetMediaItemTakeInfo_Value(take, "D_STARTOFFS", offset + trim_start)
|
|
1709
|
+
end
|
|
1710
|
+
end
|
|
1711
|
+
|
|
1712
|
+
reaper.SetMediaItemInfo_Value(item, "D_LENGTH", new_len)
|
|
1713
|
+
len = new_len
|
|
1714
|
+
|
|
1715
|
+
reaper.Undo_EndBlock("MCP: Trim media item", -1)
|
|
1716
|
+
reaper.UpdateArrange()
|
|
1717
|
+
|
|
1718
|
+
return {
|
|
1719
|
+
success = true,
|
|
1720
|
+
position = pos,
|
|
1721
|
+
length = len,
|
|
1722
|
+
}
|
|
1723
|
+
end
|
|
1724
|
+
|
|
1725
|
+
function handlers.add_stretch_marker(params)
|
|
1726
|
+
local track, item, err = get_media_item(params)
|
|
1727
|
+
if err then return nil, err end
|
|
1728
|
+
|
|
1729
|
+
local position = params.position
|
|
1730
|
+
if not position then return nil, "position required" end
|
|
1731
|
+
|
|
1732
|
+
local take = reaper.GetActiveTake(item)
|
|
1733
|
+
if not take then return nil, "Item has no active take" end
|
|
1734
|
+
|
|
1735
|
+
local src_pos = params.sourcePosition or position
|
|
1736
|
+
|
|
1737
|
+
reaper.Undo_BeginBlock()
|
|
1738
|
+
local idx = reaper.SetTakeStretchMarker(take, -1, position, src_pos)
|
|
1739
|
+
reaper.Undo_EndBlock("MCP: Add stretch marker", -1)
|
|
1740
|
+
|
|
1741
|
+
if idx < 0 then return nil, "Failed to add stretch marker" end
|
|
1742
|
+
|
|
1743
|
+
reaper.UpdateItemInProject(item)
|
|
1744
|
+
reaper.UpdateArrange()
|
|
1745
|
+
|
|
1746
|
+
return {
|
|
1747
|
+
success = true,
|
|
1748
|
+
markerIndex = idx,
|
|
1749
|
+
position = position,
|
|
1750
|
+
sourcePosition = src_pos,
|
|
1751
|
+
totalMarkers = reaper.GetTakeNumStretchMarkers(take),
|
|
1752
|
+
}
|
|
1753
|
+
end
|
|
1754
|
+
|
|
1755
|
+
function handlers.get_stretch_markers(params)
|
|
1756
|
+
local track, item, err = get_media_item(params)
|
|
1757
|
+
if err then return nil, err end
|
|
1758
|
+
|
|
1759
|
+
local take = reaper.GetActiveTake(item)
|
|
1760
|
+
if not take then return nil, "Item has no active take" end
|
|
1761
|
+
|
|
1762
|
+
local count = reaper.GetTakeNumStretchMarkers(take)
|
|
1763
|
+
local markers = {}
|
|
1764
|
+
|
|
1765
|
+
for i = 0, count - 1 do
|
|
1766
|
+
local _, pos, src_pos = reaper.GetTakeStretchMarker(take, i)
|
|
1767
|
+
markers[#markers + 1] = {
|
|
1768
|
+
index = i,
|
|
1769
|
+
position = pos,
|
|
1770
|
+
sourcePosition = src_pos,
|
|
1771
|
+
}
|
|
1772
|
+
end
|
|
1773
|
+
|
|
1774
|
+
return { trackIndex = params.trackIndex, itemIndex = params.itemIndex, markers = markers, total = count }
|
|
1775
|
+
end
|
|
1776
|
+
|
|
1777
|
+
function handlers.delete_stretch_marker(params)
|
|
1778
|
+
local track, item, err = get_media_item(params)
|
|
1779
|
+
if err then return nil, err end
|
|
1780
|
+
|
|
1781
|
+
local marker_idx = params.markerIndex
|
|
1782
|
+
if marker_idx == nil then return nil, "markerIndex required" end
|
|
1783
|
+
|
|
1784
|
+
local take = reaper.GetActiveTake(item)
|
|
1785
|
+
if not take then return nil, "Item has no active take" end
|
|
1786
|
+
|
|
1787
|
+
local count = reaper.GetTakeNumStretchMarkers(take)
|
|
1788
|
+
if marker_idx >= count then
|
|
1789
|
+
return nil, "Marker index " .. marker_idx .. " out of range (item has " .. count .. " markers)"
|
|
1790
|
+
end
|
|
1791
|
+
|
|
1792
|
+
reaper.Undo_BeginBlock()
|
|
1793
|
+
reaper.DeleteTakeStretchMarkers(take, marker_idx, 1)
|
|
1794
|
+
reaper.Undo_EndBlock("MCP: Delete stretch marker", -1)
|
|
1795
|
+
|
|
1796
|
+
reaper.UpdateItemInProject(item)
|
|
1797
|
+
reaper.UpdateArrange()
|
|
1798
|
+
|
|
1799
|
+
return { success = true, totalMarkers = reaper.GetTakeNumStretchMarkers(take) }
|
|
1800
|
+
end
|
|
1801
|
+
|
|
1012
1802
|
-- =============================================================================
|
|
1013
1803
|
-- Command dispatcher
|
|
1014
1804
|
-- =============================================================================
|