Showing posts with label python. Show all posts
Showing posts with label python. 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()

Friday, February 21, 2025

python to send midi ccs using usb snes gampad

super 8 preset and logitech f310 to cc mapping

instead of starting on cc 64 I started mapping from cc65 so I could use my piano sustain pedal to trigger loops and use the dpad to select the loops.
x doubles, a halves the length.
the middle guide (logitech) button dumps the loops to the timeline (add to project)
L button kills all the loops

Originally I did it with this 2$ ali express gamepad, later I wanted to get it working with this logitech ps1 style controller that has more buttons (more possibilities).

dropbox link to snes-midicc.py

Using It within Super 8

my super 8 cc mapping

import pygame
import rtmidi
import time
import sys

def initialize_midi():
    midi_out = rtmidi.MidiOut()
    available_ports = midi_out.get_ports()
    
    if not available_ports:
        print("No MIDI output ports found. Creating virtual port 'Gamepad MIDI'...")
        midi_out.open_virtual_port("Gamepad MIDI")
    else:
        print("Available MIDI ports:", available_ports)
        midi_out.open_port(0)
        print(f"Connected to: {available_ports[0]}")
    
    return midi_out

def initialize_joystick():
    pygame.init()
    pygame.joystick.init()
    
    if pygame.joystick.get_count() == 0:
        print("No gamepad detected!")
        sys.exit(1)
    
    joystick = pygame.joystick.Joystick(0)
    joystick.init()
    print(f"Detected gamepad: {joystick.get_name()}")
    return joystick

def main():
    midi_out = initialize_midi()
    joystick = initialize_joystick()
    
    button_states = {
        'left': False,
        'right': False,
        'up': False,
        'down': False
    }
    
    prev_buttons = [False] * joystick.get_numbuttons()
    
    print("Gamepad to MIDI mapper running. Press Ctrl+C to exit.")
    
    try:
        while True:
            for event in pygame.event.get():
                if event.type == pygame.JOYAXISMOTION:
                    if event.axis == 0:  # Left/Right
                        if event.value < -0.5 and not button_states['left']:
                            midi_out.send_message([0xB0, 64, 127])
                            button_states['left'] = True
                        elif event.value > -0.5 and button_states['left']:
                            midi_out.send_message([0xB0, 64, 0])
                            button_states['left'] = False
                            
                        if event.value > 0.5 and not button_states['right']:
                            midi_out.send_message([0xB0, 66, 127])
                            button_states['right'] = True
                        elif event.value < 0.5 and button_states['right']:
                            midi_out.send_message([0xB0, 66, 0])
                            button_states['right'] = False
                            
                    elif event.axis == 1:  # Up/Down
                        if event.value < -0.5 and not button_states['up']:
                            midi_out.send_message([0xB0, 65, 127])
                            button_states['up'] = True
                        elif event.value > -0.5 and button_states['up']:
                            midi_out.send_message([0xB0, 65, 0])
                            button_states['up'] = False
                            
                        if event.value > 0.5 and not button_states['down']:
                            midi_out.send_message([0xB0, 67, 127])
                            button_states['down'] = True
                        elif event.value < 0.5 and button_states['down']:
                            midi_out.send_message([0xB0, 67, 0])
                            button_states['down'] = False
                
                elif event.type == pygame.JOYBUTTONDOWN:
                    button_num = event.button
                    cc_number = 68 + button_num
                    midi_out.send_message([0xB0, cc_number, 127])
                    prev_buttons[button_num] = True
                    
                elif event.type == pygame.JOYBUTTONUP:
                    button_num = event.button
                    cc_number = 68 + button_num
                    midi_out.send_message([0xB0, cc_number, 0])
                    prev_buttons[button_num] = False
                    
            time.sleep(0.001)
            
    except KeyboardInterrupt:
        print("\nExiting...")
    finally:
        midi_out.close_port()
        pygame.quit()

if __name__ == "__main__":
    main()

original attempt

starts from cc64 and up
blue X = cc69
red A = cc70
yellow b = cc71
green y = cc72
select = cc77
start = cc78
Lt = cc73
Rt = cc74
up/down = 0/ 126
left/right = 0/126
I want to fix this so each direction sends on its own cc as momentary but this is fine for now


import pygame
import mido

MIDI_CC_START = 64

# Initialize MIDI output
midi_out = mido.open_output(mido.get_output_names()[0])

def send_midi_cc(cc, value):
    msg = mido.Message('control_change', control=cc, value=value)
    midi_out.send(msg)

def main():
    pygame.init()
    pygame.joystick.init()
    
    if pygame.joystick.get_count() == 0:
        print("No gamepad detected.")
        return

    joystick = pygame.joystick.Joystick(0)
    joystick.init()
    
    print(f"Gamepad detected: {joystick.get_name()}")
    print("Press Ctrl+C to exit.")
    
    prev_axes = [0] * joystick.get_numaxes()
    prev_buttons = [0] * joystick.get_numbuttons()
    prev_hats = [(0, 0)] * joystick.get_numhats()
    
    try:
        while True:
            pygame.event.pump()
            
            # Read axis values
            axes = [joystick.get_axis(i) for i in range(joystick.get_numaxes())]
            for i, value in enumerate(axes):
                if abs(value) > 0.1 and abs(value - prev_axes[i]) > 0.01:
                    send_midi_cc(MIDI_CC_START + i, int((value + 1) / 2 * 127))  # Normalize to 0-127
                prev_axes[i] = value
            
            # Read button states
            buttons = [joystick.get_button(i) for i in range(joystick.get_numbuttons())]
            for i, pressed in enumerate(buttons):
                cc_number = MIDI_CC_START + 5 + i  # Buttons start after D-pad CCs
                if pressed and not prev_buttons[i]:
                    send_midi_cc(cc_number, 127)
                elif not pressed and prev_buttons[i]:
                    send_midi_cc(cc_number, 0)
                prev_buttons[i] = pressed
            
            # Read hat (D-pad) states (momentary, dedicated CCs)
            hats = [joystick.get_hat(i) for i in range(joystick.get_numhats())]
            for i, hat in enumerate(hats):
                left_cc = 64
                up_cc = 65
                right_cc = 66
                down_cc = 67
                
                if hat[0] == -1 and prev_hats[i][0] != -1:
                    send_midi_cc(left_cc, 127)
                elif hat[0] != -1 and prev_hats[i][0] == -1:
                    send_midi_cc(left_cc, 0)
                
                if hat[1] == 1 and prev_hats[i][1] != 1:
                    send_midi_cc(up_cc, 127)
                elif hat[1] != 1 and prev_hats[i][1] == 1:
                    send_midi_cc(up_cc, 0)
                
                if hat[0] == 1 and prev_hats[i][0] != 1:
                    send_midi_cc(right_cc, 127)
                elif hat[0] != 1 and prev_hats[i][0] == 1:
                    send_midi_cc(right_cc, 0)
                
                if hat[1] == -1 and prev_hats[i][1] != -1:
                    send_midi_cc(down_cc, 127)
                elif hat[1] != -1 and prev_hats[i][1] == -1:
                    send_midi_cc(down_cc, 0)
                
                prev_hats[i] = hat
            
            pygame.time.wait(100)  # Reduce CPU usage
    except KeyboardInterrupt:
        print("\nExiting...")
    finally:
        joystick.quit()
        pygame.quit()
        midi_out.close()

if __name__ == "__main__":
    main()

Saturday, December 7, 2024

Controll Hue On Linux Using Python

 Redshift Wasn't working for me on linux mint because geoclue couldn't find location. I added the coordinates to the config and it still didn't work in redshift-gtk. I ran the CLI version with the location as arguments and it worked but the shift was pretty weak. Looked into alternatives, xrandr can adjust the hue using gamma. Heres a python script that uses tkinter to create a GUI to manipulate the values. I added brightness and limited it to 0.10 min (FOR SAFETY).




import tkinter as tk
import subprocess

# Function to update the gamma values using xrandr
def update_display(brightness, r, g, b):
    # Get the output name (e.g., HDMI-1, eDP-1)
    output = subprocess.getoutput("xrandr | grep ' connected' | cut -d' ' -f1")
    
    # Construct the xrandr command to change brightness and gamma for red, green, and blue channels
    command = f"xrandr --output {output} --brightness {brightness} --gamma {r}:{g}:{b}"
    
    # Run the command using subprocess
    subprocess.run(command, shell=True)

# Function to handle the slider change and update the display
def on_slider_change(val):
    # Get the values from the sliders (brightness, r, g, b)
    brightness = slider_brightness.get()
    r = float(slider_r.get())
    g = float(slider_g.get())
    b = float(slider_b.get())
    
    # Update the display settings based on the slider values
    update_display(brightness, r, g, b)

# Function to reset the gamma and brightness back to default
def reset_display():
    # Get the output name
    output = subprocess.getoutput("xrandr | grep ' connected' | cut -d' ' -f1")
    
    # Reset to default gamma (1.0:1.0:1.0) and brightness (1.0)
    command = f"xrandr --output {output} --brightness 1.0 --gamma 1.0:1.0:1.0"
    subprocess.run(command, shell=True)

# Create the main Tkinter window
root = tk.Tk()
root.title("Gamma and Brightness Control")

# Set up slider for Brightness (range 0.1 to 1.0)
slider_brightness = tk.Scale(root, from_=0.1, to_=1.0, orient="horizontal", label="Brightness", resolution=0.01, command=on_slider_change)
slider_brightness.set(1.0)  # Set initial value to 1 (default brightness)
slider_brightness.pack()

# Set up sliders for Red, Green, and Blue
slider_r = tk.Scale(root, from_=0.5, to_=2.0, orient="horizontal", label="Red", resolution=0.01, command=on_slider_change)
slider_r.set(1.0)  # Set initial value to 1 (default gamma)
slider_r.pack()

slider_g = tk.Scale(root, from_=0.5, to_=2.0, orient="horizontal", label="Green", resolution=0.01, command=on_slider_change)
slider_g.set(1.0)
slider_g.pack()

slider_b = tk.Scale(root, from_=0.5, to_=2.0, orient="horizontal", label="Blue", resolution=0.01, command=on_slider_change)
slider_b.set(1.0)
slider_b.pack()

# When the Tkinter window is closed, reset gamma and brightness to default
root.protocol("WM_DELETE_WINDOW", lambda: (reset_display(), root.destroy()))

# Start the Tkinter event loop
root.mainloop()

This one is a curses TUI instead of tkinter


import curses
import subprocess

# Function to update the brightness using xrandr
def update_display(brightness):
    # Get the output name (e.g., HDMI-1, eDP-1)
    output = subprocess.getoutput("xrandr | grep ' connected' | cut -d' ' -f1")
    
    # Construct the xrandr command to change brightness
    command = f"xrandr --output {output} --brightness {brightness}"
    
    # Run the command using subprocess
    subprocess.run(command, shell=True)

# Function to handle the slider change and update the display
def update_slider(brightness, direction):
    if direction == 'up':
        brightness = min(brightness + 0.10, 1.0)  # max value for brightness is 1.0
    elif direction == 'down':
        brightness = max(brightness - 0.10, 0.20)  # min value for brightness is 0.20
    return brightness

def draw_slider(window, y, x, brightness, min_value, max_value):
    # Create the vertical slider representation
    slider_length = 10
    filled_length = int((brightness - min_value) / (max_value - min_value) * slider_length)
    
    # Draw the slider on the terminal
    window.addstr(y, x, "Brightness:")
    for i in range(slider_length):
        if i == slider_length - 1 - filled_length:
            window.addstr(y + i, x + 15, "*")  # This represents the slider's current value
        else:
            window.addstr(y + i, x + 15, "|")
    
    # Add value next to the slider
    window.addstr(y + slider_length + 1, x + 15, f"Value: {brightness:.2f}")

def main(stdscr):
    curses.curs_set(0)  # Hide cursor
    stdscr.nodelay(1)  # Make getch non-blocking
    stdscr.timeout(100)  # Refresh every 100ms

    # Initial value for brightness slider
    brightness = 1.0
    min_value = 0.20
    max_value = 1.0

    while True:
        stdscr.clear()

        # Draw the vertical brightness slider
        draw_slider(stdscr, 2, 2, brightness, min_value, max_value)

        # Instructions
        stdscr.addstr(14, 2, "Use Up/Down arrows to change brightness, q to quit")

        # Refresh the screen
        stdscr.refresh()

        key = stdscr.getch()

        if key == ord('q'):
            break
        elif key == curses.KEY_UP:
            brightness = update_slider(brightness, 'up')
        elif key == curses.KEY_DOWN:
            brightness = update_slider(brightness, 'down')

        # Update the display based on the brightness value
        update_display(brightness)

# Run the curses application
curses.wrapper(main)

Friday, November 29, 2024

breakbeat archive

some classics
archive breaks

script to grab every break from that site in one go. (instead of having to click download 38 times)

I learned about exponential backoff while trying to figure this out

Warped, stretched and joined them using Reaper into one big file called

everybreakbeat-160bpm.wav

so you can chop anywhere on a downbeat at either 80 or 160bpm and get something going.

ArmandoDrumBreaks

is a youtube channel with a bunch of perfecly cut loops on it i recently found out about early this week.

Thursday, November 9, 2023

convert pdf to plain text

I wanted to spice up my typing practise on monkeytype by using pdfs.
heres a python script that takes the file paths as inputs and spits out a plain text version you can copy into monkeytype.

import PyPDF2

def pdf_to_text(pdf_path, output_text_path):
    text = ""
    
    with open(pdf_path, 'rb') as file:
        pdf_reader = PyPDF2.PdfReader(file)
        
        for page_num in range(len(pdf_reader.pages)):
            page = pdf_reader.pages[page_num]
            text += page.extract_text()
    
    with open(output_text_path, 'w', encoding='utf-8') as output_file:
        output_file.write(text)

# Take input for the PDF file and output text file
pdf_path = input("Enter the path to the PDF file: ")
output_text_path = input("Enter the path for the output text file: ")

pdf_to_text(pdf_path, output_text_path)

print(f'Text extracted from PDF and saved to {output_text_path}')

okay so after i did that i found an easier way:
using some free software called calibre this can convert ebook formats from CLI but its giving me some troubles il have to come back to this later.

  1. I set an alias for ebook-convert
  2. trying to get a bash script to take the input args as variables for the input and output paths.

Wednesday, October 18, 2023

how to set up a pip venv

sudo apt install python3-venv python3 -m venv myenv source myenv/bin/activate pip install package-name deactivate

Tuesday, June 20, 2023

Web scraping python

Scrape and dump to SQLite using python 

Cool tutorial on scraping Wikipedia and dumping it to an SQLite database using the mechanical soup library in python

Link to article

https://morioh.com/p/052da2fd3781?f=