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()

No comments:

Post a Comment