Showing posts with label midi scripting. Show all posts
Showing posts with label midi scripting. Show all posts

Wednesday, February 26, 2025

sending midi cc 64 on press and cc 65 when held for 1 second - piano sustain pedal midi scripting

For use in Reapers super 8 looper. Experimented with using on release instead of on press (like in loopy pro) but the travel distance of this piano sustain pedal is too large to be accurate so I had to remove my double and triple tap functions, still awesome being able to double your midi inputs for 0$ was hoping to get all the functions mapped though.. looking at the m vave chocolate foot switch though looks pretty cool for 50$


import time
import rtmidi
from rtmidi.midiconstants import CONTROL_CHANGE

# Configuration - Customized for single pedal operation
SUSTAIN_CC = 64          # Default sustain pedal CC - Used for both functions
RECORD_TOGGLE_CC = 64    # Record/Play/Overdub (same as sustain)
CLEAR_LOOP_CC = 65       # Clear loop CC to be triggered by holding sustain

MIDI_CHANNEL = 0         # MIDI channel (0-15)
HOLD_THRESHOLD = 1.0     # Time in seconds required to hold sustain to clear loop

class LooperPedalController:
    def __init__(self):
        # Set up MIDI input/output
        self.midi_in = rtmidi.MidiIn()
        self.midi_out = rtmidi.MidiOut()
        
        # State variables
        self.is_recording = False
        self.is_playing = False
        self.pedal_down = False
        self.press_start_time = 0
        self.action_triggered = False
        
        self.setup_midi()
        
    def setup_midi(self):
        # List available ports
        in_ports = self.midi_in.get_ports()
        out_ports = self.midi_out.get_ports()
        
        print("Available MIDI input ports:")
        for i, port in enumerate(in_ports):
            print(f"  {i}: {port}")
            
        print("Available MIDI output ports:")
        for i, port in enumerate(out_ports):
            print(f"  {i}: {port}")
        
        # Let user select ports
        in_port = int(input("Select MIDI input port number: "))
        out_port = int(input("Select MIDI output port number: "))
        
        # Open ports
        self.midi_in.open_port(in_port)
        self.midi_out.open_port(out_port)
        
        # Set callback
        self.midi_in.set_callback(self.midi_callback)
        
    def midi_callback(self, event, data=None):
        message, delta_time = event
        
        # Process Control Change messages for the sustain pedal
        if len(message) == 3 and message[0] == CONTROL_CHANGE | MIDI_CHANNEL and message[1] == SUSTAIN_CC:
            value = message[2]
            current_time = time.time()
            
            if value >= 64:  # Pedal down
                self.handle_pedal_down(current_time)
            else:  # Pedal up
                self.handle_pedal_up(current_time)
    
    def handle_pedal_down(self, current_time):
        # Record when the pedal was pressed
        self.press_start_time = current_time
        self.pedal_down = True
        self.action_triggered = False
        
    def handle_pedal_up(self, current_time):
        duration = current_time - self.press_start_time
        self.pedal_down = False
        
        # If action was already triggered (by hold), do nothing more
        if self.action_triggered:
            return
            
        # If pedal was pressed briefly (not held), trigger record/play/overdub
        if duration < HOLD_THRESHOLD:
            self.handle_record_toggle()
    
    def handle_record_toggle(self):
        # Toggle between record, play, and overdub
        print("Quick press - Record/Play/Overdub toggle")
        self.send_cc(RECORD_TOGGLE_CC, 127)
        time.sleep(0.05)
        self.send_cc(RECORD_TOGGLE_CC, 0)
        
        # Update internal state (for display purposes)
        if not self.is_recording and not self.is_playing:
            self.is_recording = True
            print("State: Recording")
        elif self.is_recording:
            self.is_recording = False
            self.is_playing = True
            print("State: Playing")
        else:
            self.is_recording = True
            print("State: Overdubbing")
    
    def handle_clear_loop(self):
        print("Hold detected - Clearing loop (sending CC 65)")
        self.send_cc(CLEAR_LOOP_CC, 127)
        time.sleep(0.05)
        self.send_cc(CLEAR_LOOP_CC, 0)
        
        # Update internal state
        self.is_recording = False
        self.is_playing = False
        print("State: Cleared")
        self.action_triggered = True
        
    def send_cc(self, cc_number, value):
        message = [CONTROL_CHANGE | MIDI_CHANNEL, cc_number, value]
        self.midi_out.send_message(message)
    
    def run(self):
        print("\n=== Single Pedal Looper Controller ===")
        print(f"- Quick press CC {SUSTAIN_CC}: Record/Play/Overdub")
        print(f"- Hold CC {SUSTAIN_CC} for {HOLD_THRESHOLD}+ sec: Clear loop (sends CC {CLEAR_LOOP_CC})")
        print("=====================================\n")
        print("Current state: Stopped")
        
        try:
            # Keep the script running
            while True:
                time.sleep(0.05)  # Check frequently for responsive hold detection
                
                # Check for hold while pedal is down
                if self.pedal_down and not self.action_triggered:
                    current_time = time.time()
                    if current_time - self.press_start_time >= HOLD_THRESHOLD:
                        self.handle_clear_loop()
                        
        except KeyboardInterrupt:
            print("Exiting...")

if __name__ == "__main__":
    controller = LooperPedalController()
    controller.run()