Source code for jdxi_editor.devel.scan_all_ports

#!/usr/bin/env python3
"""
Comprehensive MIDI Port Scanner

This script scans ALL available MIDI ports to find where your controller
is actually sending messages.
"""

import sys
import time

import rtmidi

from jdxi_editor.midi.message import MidiMessage
from picomidi import BitMask, MidiStatus


[docs] class AllPortScanner: def __init__(self):
[docs] self.message_count = 0
[docs] self.start_time = None
[docs] self.callback_called = False
[docs] self.active_ports = []
[docs] self.midi_instances = []
[docs] def midi_callback_factory(self, port_name): """Create a callback function for a specific port""" def midi_callback(message, data): self.callback_called = True message_data, timestamp = message self.message_count += 1 # Format timestamp elapsed = time.time() - self.start_time if self.start_time else 0 timestamp_str = f"{elapsed:.3f}s" # Raw message data print(f"[{timestamp_str}] FROM {port_name}: {message_data}") # Decode MIDI message decoded = self.decode_midi_message(message_data) print(f"[{timestamp_str}] #{self.message_count:03d} {decoded}") print("-" * 50) return midi_callback
[docs] def decode_midi_message(self, message_data): """Decode MIDI message into human-readable format""" if not message_data: return "Empty message" # Convert to list of integers data = list(message_data) if len(data) < 2: return f"Raw: {data}" status = data[0] channel = (status & BitMask.LOW_4_BITS) + 1 message_type = status & MidiMessage.MIDI_STATUS_MASK if message_type == MidiStatus.NOTE_ON: # Note On note = data[1] velocity = data[2] if len(data) > 2 else 0 note_name = self.get_note_name(note) return f"Note On Ch{channel:2d} {note_name} (vel={velocity})" elif message_type == MidiStatus.NOTE_OFF: # Note Off note = data[1] velocity = data[2] if len(data) > 2 else 0 note_name = self.get_note_name(note) return f"Note Off Ch{channel:2d} {note_name} (vel={velocity})" elif message_type == MidiStatus.CONTROL_CHANGE: # Control Change controller = data[1] value = data[2] if len(data) > 2 else 0 return f"CC{controller:2d} Ch{channel:2d} Value={value:3d} ({self.get_cc_name(controller)})" elif message_type == MidiStatus.PROGRAM_CHANGE: # Program Change program = data[1] return f"Prog Chg Ch{channel:2d} Program={program}" elif message_type == MidiStatus.CHANNEL_AFTERTOUCH: # Channel Aftertouch pressure = data[1] return f"Aftertouch Ch{channel:2d} Pressure={pressure}" elif message_type == 0xE0: # Pitch Bend lsb = data[1] msb = data[2] if len(data) > 2 else 0 bend_value = ((msb << 7) | lsb) - 8192 return f"Pitch Bend Ch{channel:2d} Value={bend_value:+5d}" elif status == MidiMessage.MIDI_STATUS_MASK: # SysEx return f"SysEx ({len(data)} bytes): {' '.join(f'{b:02X}' for b in data)}" else: return f"Unknown: {' '.join(f'{b:02X}' for b in data)}"
[docs] def get_note_name(self, note_number): """Convert MIDI note number to note name""" notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] octave = (note_number // 12) - 1 note = notes[note_number % 12] return f"{note}{octave}"
[docs] def get_cc_name(self, controller): """Get common control change names""" cc_names = { 1: "Mod Wheel", 7: "Volume", 10: "Pan", 11: "Expression", 41: "General Purpose 1", 64: "Sustain", 65: "Portamento", 66: "Sostenuto", 67: "Soft Pedal", 68: "Legato", 69: "Hold 2", 70: "Sound Variation", 71: "Resonance", 72: "Release Time", 73: "Attack Time", 74: "Cutoff", 75: "Decay Time", 76: "Vibrato Rate", 77: "Vibrato Depth", 78: "Vibrato Delay", } return cc_names.get(controller, f"CC{controller}")
[docs] def list_all_ports(self): """List all available MIDI input ports""" print("=== All Available MIDI Input Ports ===") midi_in = rtmidi.RtMidiIn() port_count = midi_in.getPortCount() ports = [] for i in range(port_count): port_name = midi_in.getPortName(i) ports.append(port_name) print(f" {i}: {port_name}") return ports
[docs] def scan_all_ports(self, ports): """Scan all ports simultaneously for MIDI messages""" print(f"\n=== Scanning ALL {len(ports)} Ports Simultaneously ===") print("Move controls on your SINCO VMX8 controller...") print("Monitoring for 15 seconds...\n") self.start_time = time.time() self.message_count = 0 self.callback_called = False # Open all ports for i, port_name in enumerate(ports): try: midi_in = rtmidi.RtMidiIn() midi_in.openPort(i) midi_in.setCallback(self.midi_callback_factory(port_name)) midi_in.ignoreTypes(False, True, True) self.midi_instances.append(midi_in) self.active_ports.append(port_name) print(f"✅ Opened port {i}: {port_name}") except Exception as e: print(f"❌ Failed to open port {i} ({port_name}): {e}") if not self.active_ports: print("❌ No ports could be opened") return print(f"\nMonitoring {len(self.active_ports)} active ports...") # Monitor for 15 seconds start_time = time.time() while time.time() - start_time < 15: time.sleep(0.1) # Close all ports for midi_in in self.midi_instances: try: if midi_in.isPortOpen(): midi_in.closePort() except Exception: pass print(f"\n=== Scan Complete ===") if self.callback_called: print(f"✅ Received {self.message_count} messages total") else: print("❌ No MIDI messages received on any port") print("\nTroubleshooting suggestions:") print("1. Check if your controller is powered on") print("2. Try different controls (faders, knobs, buttons)") print("3. Check if the controller has a MIDI mode or setting") print("4. Try unplugging and reconnecting the USB cable") print("5. Check if the controller needs drivers") print("6. Try a different USB port")
[docs] def main(): print("Comprehensive MIDI Port Scanner") print("=" * 50) scanner = AllPortScanner() # List all ports ports = scanner.list_all_ports() if not ports: print("❌ No MIDI input ports available") return # Scan all ports scanner.scan_all_ports(ports)
if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\nExiting...") except Exception as e: print(f"Fatal error: {e}") sys.exit(1)