mirror of
https://github.com/Khendi1/PVS.git
synced 2026-02-13 14:05:33 +01:00
532 lines
21 KiB
Python
532 lines
21 KiB
Python
import mido
|
|
import time
|
|
from config import params
|
|
import mido
|
|
import threading
|
|
import time
|
|
|
|
"""
|
|
A class to handle the processing of MIDI messages,
|
|
including mapping, acceleration, and smoothing.
|
|
"""
|
|
class MidiProcessor:
|
|
def __init__(self, min_midi=0, max_midi=127,
|
|
base_smoothing=0.05, acceleration_factor=0.05):
|
|
"""
|
|
Initializes the MIDI processor with mapping and smoothing parameters.
|
|
|
|
Args:
|
|
min_midi (int): Minimum raw MIDI CC value (typically 0).
|
|
max_midi (int): Maximum raw MIDI CC value (typically 127).
|
|
min_output (int/float): Minimum value for the mapped output range.
|
|
max_output (int/float): Maximum value for the mapped output range.
|
|
base_smoothing (float): The base smoothing factor (alpha) for
|
|
exponential smoothing. A smaller value (e.g., 0.01)
|
|
means more smoothing (slower response) at low speeds.
|
|
Should be between 0 and 1.
|
|
acceleration_factor (float): Multiplier for how much input speed
|
|
increases the smoothing factor. Higher values
|
|
mean more "acceleration" or responsiveness
|
|
to rapid movements.
|
|
"""
|
|
self.min_midi = min_midi
|
|
self.max_midi = max_midi
|
|
|
|
self.base_smoothing = base_smoothing
|
|
self.acceleration_factor = acceleration_factor
|
|
self.time_epsilon = 0.001 # Small value to prevent division by zero in time_delta calculations
|
|
|
|
# State variables for processing
|
|
self.current_mapped_value = 0.0
|
|
self.last_midi_value = None
|
|
self.last_message_abs_time = time.time()
|
|
|
|
|
|
def map_range(self, value, out_min, out_max):
|
|
"""
|
|
Maps a value from one numerical range to another.
|
|
|
|
Args:
|
|
value (float): The input value to map.
|
|
in_min (float): The minimum of the input range.
|
|
in_max (float): The maximum of the input range.
|
|
out_min (float): The minimum of the output range.
|
|
out_max (float): The maximum of the output range.
|
|
|
|
Returns:
|
|
float: The mapped value in the output range.
|
|
"""
|
|
return (value - self.min_midi) * (out_max - out_min) / (self.max_midi - self.min_midi) + out_min
|
|
|
|
|
|
def process_message(self, control, value, min_output=-150, max_output=150):
|
|
"""
|
|
Processes an incoming MIDI message. If it's a control change message,
|
|
it applies mapping, acceleration, and smoothing to its value.
|
|
|
|
Args:
|
|
msg (mido.Message): The incoming MIDI message.
|
|
|
|
Returns:
|
|
float or None: The processed (smoothed and accelerated) value if
|
|
it's a control change message, otherwise None.
|
|
"""
|
|
midi_cc_value = value
|
|
|
|
# Calculate actual time delta between messages for speed calculation
|
|
current_abs_time = time.time()
|
|
time_delta = current_abs_time - self.last_message_abs_time
|
|
self.last_message_abs_time = current_abs_time
|
|
|
|
# Calculate change in MIDI value
|
|
midi_value_change = 0
|
|
if self.last_midi_value is not None:
|
|
midi_value_change = midi_cc_value - self.last_midi_value
|
|
self.last_midi_value = midi_cc_value
|
|
|
|
# Map raw MIDI CC value to the target output range (-150 to 150)
|
|
target_mapped_value = self.map_range(midi_cc_value, min_output, max_output)
|
|
|
|
# Calculate dynamic smoothing factor based on input speed
|
|
# Input speed is defined as the absolute change in MIDI value per unit of time.
|
|
# A higher speed (larger abs(midi_value_change) and smaller time_delta)
|
|
# should lead to a higher effective_smoothing_factor (less smoothing, faster response).
|
|
|
|
effective_time_delta = max(time_delta, self.time_epsilon) # Prevent division by zero
|
|
input_speed = abs(midi_value_change) / effective_time_delta
|
|
|
|
# Adjust smoothing factor (alpha): faster input means less smoothing (higher alpha)
|
|
# The dynamic_alpha will range from base_smoothing to 1.0.
|
|
dynamic_alpha = min(1.0, self.base_smoothing + (input_speed * self.acceleration_factor))
|
|
|
|
# Apply exponential smoothing to the current mapped value
|
|
# current_mapped_value = alpha * new_value + (1 - alpha) * old_smoothed_value
|
|
self.current_mapped_value = (dynamic_alpha * target_mapped_value) + \
|
|
((1 - dynamic_alpha) * self.current_mapped_value)
|
|
|
|
# print(f"MIDI CC {control}: Raw={value}, Target={target_mapped_value:.2f}, "
|
|
# f"Smoothed={self.current_mapped_value:.2f}, TimeDelta={time_delta:.4f}, "
|
|
# f"InputSpeed={input_speed:.2f}, Alpha={dynamic_alpha:.2f}")
|
|
|
|
return self.current_mapped_value
|
|
|
|
"""
|
|
A generic class to handle MIDI input, process messages, and modify params
|
|
This class runs in a separate thread to continuously listen for MIDI messages
|
|
from the provided controller. See the SMC_Mixer class for specific mappings.
|
|
"""
|
|
class MidiInputController:
|
|
def __init__(self, port_name=None, controller=None):
|
|
"""
|
|
Initializes the MidiController instance.
|
|
|
|
Args:
|
|
port_name (str): The name of the MIDI input port to open.
|
|
"""
|
|
|
|
# Select the MIDI controller to use.
|
|
self.controller = controller
|
|
|
|
# Use the controller's default port name if it exists,
|
|
# otherwise prompt the user to select a port.
|
|
self.port_name = self.controller.port_name if hasattr(self.controller, 'port_name') else self.select_port()
|
|
|
|
# create a thread to handle MIDI input
|
|
self.thread_stop = False
|
|
self.thread = threading.Thread(target=self.input_thread_handler)
|
|
self.thread.daemon = True
|
|
self.thread.start()
|
|
|
|
def select_port(self):
|
|
"""
|
|
Selects a MIDI input port by name.
|
|
|
|
Args:
|
|
port_name (str): The name of the MIDI input port to select.
|
|
|
|
Returns:
|
|
mido.Input: The opened MIDI input port.
|
|
"""
|
|
|
|
# Get a list of all available MIDI input port names on the system
|
|
input_ports = mido.get_input_names()
|
|
|
|
if not input_ports:
|
|
print("No MIDI input ports found. Please ensure your MIDI device is connected and drivers are installed.")
|
|
return
|
|
|
|
print("\nAvailable MIDI Input Ports:")
|
|
for i, port_name in enumerate(input_ports):
|
|
print(f"{i}: {port_name}")
|
|
|
|
# Prompt the user to select a MIDI port from the list
|
|
selected_port_index = -1
|
|
while selected_port_index < 0 or selected_port_index >= len(input_ports):
|
|
try:
|
|
choice = input(f"Enter the number of the MIDI input port to use (0-{len(input_ports)-1}): ")
|
|
selected_port_index = int(choice)
|
|
except ValueError:
|
|
print("Invalid input. Please enter a number.")
|
|
if selected_port_index < 0 or selected_port_index >= len(input_ports):
|
|
print("Invalid port number. Please try again.")
|
|
|
|
chosen_port_name = input_ports[selected_port_index]
|
|
print(f"\nSelected port: {chosen_port_name}")
|
|
return chosen_port_name
|
|
|
|
def set_values(self, control, value):
|
|
"""
|
|
Sets the values for the specified MIDI control number.
|
|
|
|
Args:
|
|
control (int): The MIDI control number.
|
|
value (int): The value of the MIDI control message.
|
|
"""
|
|
if self.controller is not None:
|
|
self.controller.set_values(control, value)
|
|
else:
|
|
print("No controller set to handle MIDI messages.")
|
|
|
|
def input_thread_handler(self):
|
|
"""
|
|
Handles MIDI input in a separate thread.
|
|
Opens the specified MIDI input port and uses the MidiProcessor instance
|
|
to process incoming messages.
|
|
"""
|
|
|
|
try:
|
|
with mido.open_input(self.port_name) as inport:
|
|
print(f"MIDI input thread started for port: {inport.name}")
|
|
print("Listening for MIDI messages... (Press Ctrl+C in the main terminal to stop)")
|
|
|
|
# Continuously listen for messages until the thread_stop flag is set
|
|
for msg in inport:
|
|
if self.thread_stop:
|
|
print(f"Stopping MIDI input for port: {inport.name}\n")
|
|
break
|
|
|
|
# get the control number from the message
|
|
control = msg.control if hasattr(msg, 'control') else None
|
|
if control is None:
|
|
print(f"Received non-control message: {msg}")
|
|
continue
|
|
|
|
self.set_values(control, msg.value)
|
|
|
|
except ValueError as e:
|
|
print(f"Error: Could not open MIDI port '{self.port_name}'. {e}")
|
|
print("Please ensure the port name is correct and the device is connected.\n")
|
|
except Exception as e:
|
|
print(f"An unexpected error occurred in the MIDI thread: {e}\n")
|
|
finally:
|
|
print(f"MIDI input thread for '{self.port_name}' has terminated.\n")
|
|
|
|
"""
|
|
A class to represent the SMC-Mixer and the
|
|
mapping of encoders, faders, and buttons to pages of parameters.
|
|
"""
|
|
class SMC_Mixer:
|
|
"""
|
|
encoders: 30-37
|
|
faders: 40-47
|
|
|
|
m: 20-27 next mode (blur, noise, fractal, etc.)
|
|
s: 28-35 (DO NOT USE 30-35)
|
|
r: 36-43 (DO NOT USE 36-67, 40-43)
|
|
square: 44-51 (DO NOT USE 44-47)
|
|
|
|
play 52 save
|
|
pause 53 load random from save
|
|
record 54 load random params
|
|
|
|
reverse 55 load previous encoder page
|
|
forward 56 load next encoder page
|
|
|
|
previous 57 load previous fader page
|
|
next 58 load next fader page
|
|
|
|
up 59
|
|
down 60
|
|
|
|
left 61
|
|
right 62
|
|
|
|
"""
|
|
def __init__(self):
|
|
"""
|
|
Initializes the SMC_Mixer instance.
|
|
"""
|
|
|
|
self.MIN = 0
|
|
self.MAX = 127
|
|
|
|
self.port_name = "SMC-Mixer 0"
|
|
self.processor = MidiProcessor(min_midi=self.MIN, max_midi=self.MAX,
|
|
base_smoothing=0.05, acceleration_factor=0.05)
|
|
|
|
# each dict entry is a page of parameters
|
|
self.fader_params = {
|
|
0: [ 'frame_blend', 'metaballs_feedback', 'smooth_coloring_max_field', 'threshold', 'radius_multiplier', 'speed_multiplier', 'num_metaballs', 'metaball_zoom'],
|
|
1: ['alpha', 'temporal_filter', 'x_sync_speed', 'x_sync_freq', 'x_sync_amp', 'y_sync_speed', 'y_sync_freq', 'y_sync_amp'],
|
|
2: ['hue_shift', 'sat_shift', 'val_shift', 'val_threshold', 'val_hue_shift', 'contrast', 'hue_invert', 'hue_invert_angle'],
|
|
}
|
|
self.fader_config = self.fader_params.get(0, [])
|
|
|
|
self.encoder_params = {
|
|
0: ['frame_blend', 'metaball_skew_angle', 'metaball_skew_intensity', 'metaball_hue', 'metaballs_saturation', 'metaballs_value', 'metaballs_contrast', 'metaballs_brightness'],
|
|
1: ['hue_shift', 'sat_shift', 'val_shift', 'zoom', 'r_shift', 'x_shift', 'y_shift', 'blur_kernel_size', ''],
|
|
2: ['alpha', 'temporal_filter', 'blur_kernel_size', 'zoom', 'r_shift', 'x_shift', 'y_shift', ''],
|
|
}
|
|
self.encoder_config = self.encoder_params.get(0, [])
|
|
|
|
# self.set_button_params = {}
|
|
|
|
def set_values(self, control, value):
|
|
"""
|
|
Maps the MIDI control messages to the SMC-Mixer.
|
|
This function is a placeholder for actual mapping logic.
|
|
|
|
Args:
|
|
control (int): The MIDI control number.
|
|
value (int): The value of the MIDI control message.
|
|
"""
|
|
if control in range(30, 38):
|
|
self.set_encoder_param(control, value)
|
|
elif control in range(40, 48):
|
|
self.set_fader_param(control, value)
|
|
elif control in range(20, 36):
|
|
self.set_button_param(control, value)
|
|
|
|
def set_fader_param(self, control, value):
|
|
"""
|
|
Maps the MIDI faders to the SMC-Mixer.
|
|
This function is a placeholder for actual mapping logic.
|
|
"""
|
|
|
|
index = control % 10
|
|
param = params.get(self.fader_config[index])
|
|
|
|
if param is None:
|
|
print(f"Warning: No parameter found for control {control} in fader_config.")
|
|
return
|
|
|
|
min, max = param.min_max()
|
|
value = self.processor.process_message(control, value, min, max)
|
|
|
|
params.set(self.fader_config[index], value)
|
|
|
|
print(f"{self.fader_config[index]}: {value} (MIDI value: {value})")
|
|
|
|
def set_encoder_param(self, control, value):
|
|
"""
|
|
Maps the MIDI encoders to the SMC-Mixer.
|
|
This function is a placeholder for actual mapping logic.
|
|
"""
|
|
index = control % 10
|
|
param = params.get(self.encoder_config[index])
|
|
|
|
if param is None:
|
|
print(f"Warning: No parameter found for channel {control} in encoder_config.")
|
|
return
|
|
|
|
min, max = param.min_max()
|
|
value = self.processor.process_message(control, value, min, max)
|
|
params.set(self.encoder_config[index], value)
|
|
|
|
print(f"{self.encoder_config[index]}: {value} (MIDI value: {value})")
|
|
|
|
def set_button_param(self, control, value):
|
|
"""
|
|
Maps the MIDI buttons to the SMC-Mixer.
|
|
This function is a placeholder for actual mapping logic.
|
|
"""
|
|
print("Mapping buttons... (this is a placeholder function)")
|
|
|
|
|
|
class MidiMix:
|
|
|
|
def __init__(self):
|
|
"""
|
|
Initializes the SMC_Mixer instance.
|
|
"""
|
|
|
|
self.MIN = 0
|
|
self.MAX = 127
|
|
|
|
self.port_name = "MIDI Mix 2"
|
|
self.processor = MidiProcessor(min_midi=self.MIN, max_midi=self.MAX,
|
|
base_smoothing=0.05, acceleration_factor=0.05)
|
|
|
|
self.fader_controls = [19, 23, 27, 31, 49, 53, 57, 61, 62] # Faders 1-8
|
|
self.pot_controls = [16, 17, 18, 20, 21, 22, 24, 25, 26, 28,29,30, 46, 47, 48, 50, 51, 52, 54, 55, 56, 58, 59, 60] # Encoders 1-9
|
|
self.button_controls = [1, 3, 4, 6, 7, 9, 10, 12, 13, 15, 16, 18, 19,21, 22, 24]
|
|
|
|
# each dict entry is a page of parameters
|
|
self.fader_params = {
|
|
1: ['alpha', 'temporal_filter', 'hue_shift', 'sat_shift', 'val_shift', 'contrast', 'brightness', 'blur_kernel_size', 'frame_blend'],
|
|
# 2: ['hue_shift', 'sat_shift', 'val_shift', 'val_threshold', 'val_hue_shift', 'contrast', 'hue_invert', 'hue_invert_angle'],
|
|
}
|
|
self.fader_config = self.fader_params.get(1, [])
|
|
|
|
self.encoder_params = {
|
|
0: ['sequence', 'pattern_type', 'pattern_mod', 'pattern_r', 'pattern_g', 'pattern_b', "x_sync_amp", 'x_sync_freq', 'x_sync_speed', 'y_sync_amp', 'y_sync_freq', 'y_sync_speed', 'x_shift', 'hue_invert_angle', 'hue_invert_strength', 'y_shift', 'val_threshold', 'val_hue_shift', 'r_shift', 'noise_type', 'noise_intensity', 'zoom', 'reflection_mode', 'blur_type'],
|
|
1: ['hue_shift', 'contrast', 'brightness', 'sat_shift', 'val_threshold', 'val_hue_shift', 'val_shift', 'hue_invert_angle', 'hue_invert_strength', 'x_shift', 'noise_type', 'noise_intensity', 'y_shift', 'blur_type', 'blur_kernel_size', 'zoom', 'x_sync_speed', 'y_sync_speed', 'r_shift', 'x_sync_freq', 'y_sync_freq', 'reflection_mode', 'x_sync_amp', 'y_sync_amp'],
|
|
2: ["x_sync_amp", 'x_sync_freq', 'x_sync_speed', 'y_sync_amp', 'y_sync_freq', 'y_sync_speed', 'x_shift', 'pattern_type', 'pattern_mod', 'y_shift', 'solarize_threshold', 'posterize_levels', 'r_shift', 'hue_invert_angle', 'hue_invert_strength', 'zoom', 'val_threshold', 'val_hue_shift', 'reflection_mode', 'noise_type', 'noise_intensity', 'sequence', 'sharpen_intensity', 'blur_type'] }
|
|
self.encoder_config = self.encoder_params.get(2, [])
|
|
|
|
# self.set_button_params = {}
|
|
|
|
def set_values(self, control, value):
|
|
"""
|
|
Maps the MIDI control messages to the SMC-Mixer.
|
|
This function is a placeholder for actual mapping logic.
|
|
|
|
Args:
|
|
control (int): The MIDI control number.
|
|
value (int): The value of the MIDI control message.
|
|
"""
|
|
if control in self.fader_controls:
|
|
self.set_fader_param(control, value)
|
|
elif control in self.pot_controls:
|
|
self.set_encoder_param(control, value)
|
|
elif control in self.button_controls:
|
|
self.set_button_param(control, value)
|
|
|
|
def set_fader_param(self, control, value):
|
|
"""
|
|
Maps the MIDI faders to the SMC-Mixer.
|
|
This function is a placeholder for actual mapping logic.
|
|
"""
|
|
|
|
index = self.fader_controls.index(control)
|
|
|
|
param = params.get(self.fader_config[index])
|
|
|
|
if param is None:
|
|
print(f"Warning: No parameter found for control {control} in fader_config.")
|
|
return
|
|
|
|
min, max = param.min_max()
|
|
value = self.processor.process_message(control, value, min, max)
|
|
|
|
params.set(self.fader_config[index], value)
|
|
|
|
print(f"{self.fader_config[index]}: {value} (MIDI value: {value})")
|
|
|
|
def set_encoder_param(self, control, value):
|
|
"""
|
|
Maps the MIDI encoders to the SMC-Mixer.
|
|
This function is a placeholder for actual mapping logic.
|
|
"""
|
|
index = self.pot_controls.index(control) # get the index of the control in the pot_controls list
|
|
|
|
param = params.get(self.encoder_config[index]) # get the parameter by name using the index
|
|
|
|
if param is None:
|
|
print(f"Warning: No parameter found for channel {control} in encoder_config.")
|
|
return
|
|
|
|
min, max = param.min_max()
|
|
value = self.processor.process_message(control, value, min, max)
|
|
params.set(self.encoder_config[index], value)
|
|
|
|
print(f"{self.encoder_config[index]}: {value} (MIDI value: {value})")
|
|
|
|
def set_button_param(self, control, value):
|
|
"""
|
|
Maps the MIDI buttons to the SMC-Mixer.
|
|
This function is a placeholder for actual mapping logic.
|
|
"""
|
|
print("Mapping buttons... (this is a placeholder function)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
"""
|
|
Main function to list available MIDI input ports, prompt the user to select one,
|
|
start the MIDI input processing thread, and manage its lifecycle.
|
|
"""
|
|
|
|
controller = MidiInputController(controller=MidiMix)
|
|
|
|
print("\nMain program is running. The MIDI thread is listening in the background.")
|
|
print("Press Ctrl+C to stop the MIDI thread and exit the program.")
|
|
|
|
listen_to_midi_device()
|
|
|
|
try:
|
|
# Keep the main thread alive indefinitely so the MIDI input thread can run.
|
|
# It sleeps periodically to prevent busy-waiting.
|
|
while True:
|
|
time.sleep(1)
|
|
except KeyboardInterrupt:
|
|
print("\nCtrl+C detected. Signaling MIDI thread to stop...")
|
|
controller.thread_stop = True
|
|
# Wait for the MIDI thread to finish, with a timeout
|
|
controller.thread.join(timeout=5)
|
|
if controller.thread.is_alive():
|
|
print("MIDI thread did not terminate gracefully. Forcing exit.")
|
|
else:
|
|
print("MIDI thread stopped successfully.")
|
|
finally:
|
|
print("Exiting main program.")
|
|
|
|
|
|
# Import the mido library for MIDI communication
|
|
import mido
|
|
import time
|
|
|
|
def test_ports():
|
|
"""
|
|
Lists available MIDI input devices.
|
|
"""
|
|
input_ports = mido.get_input_names()
|
|
|
|
if not input_ports:
|
|
print("No MIDI input devices found.")
|
|
print("Please ensure your MIDI device is connected and recognized by your system.")
|
|
return
|
|
|
|
print("Available MIDI input devices:")
|
|
for i, port_name in enumerate(input_ports):
|
|
print(f" {i}: {port_name}")
|
|
|
|
def listen_to_midi_device():
|
|
"""
|
|
Connects to the first available MIDI input device and prints incoming messages.
|
|
"""
|
|
input_ports = mido.get_input_names()
|
|
|
|
if not input_ports:
|
|
print("No MIDI input devices found.")
|
|
print("Please ensure your MIDI device is connected and recognized by your system.")
|
|
return
|
|
|
|
print("Available MIDI input devices:")
|
|
for i, port_name in enumerate(input_ports):
|
|
print(f" {i}: {port_name}")
|
|
|
|
# Attempt to connect to the first available input port
|
|
try:
|
|
# You can modify this to let the user select a port, e.g.,
|
|
# selected_index = int(input("Enter the number of the device to listen to: "))
|
|
# port_name_to_open = input_ports[selected_index]
|
|
|
|
port_name_to_open = input_ports[0] # Automatically pick the first one
|
|
|
|
print(f"\nAttempting to open MIDI input port: '{port_name_to_open}'")
|
|
with mido.open_input(port_name_to_open) as inport:
|
|
print(f"Successfully opened '{inport.name}'. Listening for MIDI messages...")
|
|
print("Press Ctrl+C to stop listening.")
|
|
|
|
# Loop indefinitely to listen for messages
|
|
for msg in inport:
|
|
print(f"Received MIDI message: {msg}")
|
|
|
|
except IndexError:
|
|
print("Invalid device selection. Please run the script again and choose a valid number.")
|
|
except Exception as e:
|
|
print(f"An error occurred: {e}")
|
|
finally:
|
|
print("\nStopped listening to MIDI messages.")
|
|
|