def __init__(self, xml_dict, midi_in, midi_out, ignore_sysex=True, ignore_timing=True, ignore_active_sense=True): """ Calls the MidiInputHandler constructor and initializes the sub class attributes Parameters: * xml_dict: dictionary containing the parsed values from the xml configuration file * midi_in: MIDI IN interface to use * midi_out: MIDI OUT interface to use * ignore_* parameters: see the "_ignore_messages" method """ self.__log.debug("Initializing MidiProcessor") super().__init__(midi_in, midi_out, ignore_sysex, ignore_timing, ignore_active_sense) self._xml_dict = xml_dict self._quit = False self._status = None self._panic_command = [] self._send_bank_names = "F0 7D 00 " self.__log.debug("MidiProcessor Initialized:\n%s", PrettyFormat(self.__dict__))
def _send_system_exclusive(self, message): """ Handles a SysEx (system exlusive) message. By default it just sends it back. Parameters: * message: Contains a list with the bytes of the MIDI Message, ie: [240, 67, 112, 247] (in Hexadecimal: F0 43 70, F7). Please note that if the SysEx message is too long, then this method will be called several times untill the end byte of the SysEx (0xF7) gets sent. Remarks: * Please note that this callback isn't supposed to be overwritten by a subclass. Instead of doing this, create a handler called: _on_system_exclusive (Here I'm assuming that _callback_preffix is equal to "_on_"), then leave the part receiving the SysEx equal; only after you have received the whole SysEx, you should add your post processing. You must also clear the SysEx buffer afterwards """ if not self._receive_sysex(message): self.__log.debug("Sending SysEx message: %s", message) #This means that the end of the SysEx message (0xF7) was detected, #so, no further bytes will be received. Here the SysEx buffer will #be sent and afterwards cleared if (self._midi_out == None) or (self._console_echo): self.__log.info(PrettyFormat(self._sysex_buffer)) if self._midi_out != None: self._midi_out.send_message(self._sysex_buffer) #Clears SysEx buffer self._sysex_buffer = [] #Resets SysEx count to zero self._sysex_chunk = 0 self.__log.debug("SysEx message was sent")
def _get_all_ports(self): """ Gets all the available MIDI IN and Out ports. """ in_ports = [] out_ports = [] if self._open_midi(): self.__log.debug("Getting all MIDI IN ports") in_ports = self._get_midi_ports(self._midi_in) self.__log.debug("Got:\n%s", PrettyFormat(in_ports)) self.__log.debug("Getting all MIDI OUT ports") out_ports = self._get_midi_ports(self._midi_out) self.__log.debug("Got:\n%s", PrettyFormat(out_ports)) self._in_ports = in_ports self._out_ports = out_ports self._free_midi()
def _parse_panic(self): """ Parses the panic mode """ self.__log.debug("Parsing XML node: Panic") node = self._xml_dict.get("Panic") if node != None: if type(node) == str: node_str = node command_file = None else: node_str = node.get('$') command_file = node.get('@File') commad_list = [] if node_str != None: commad_list = self._parse_panic_string(node_str) if (commad_list != []) and command_file: message = "The Panic node only accepts either the inline command or a" \ " 'File' attribute, but not both" self.__log.debug(message) raise Exception(message) if (commad_list == []) and command_file: is_valid, command_str = read_text_file(command_file) if not is_valid: message = "Trouble accessing file: %s" % command_file self.__log.debug(message) raise Exception(message) commad_list = self._parse_panic_string(command_str) self._panic_command = commad_list self.__log.debug("Got:\n%s", PrettyFormat(self._panic_command)) self.__log.debug("Node was parsed")
def _open_port(self, interface_type, midi_port, is_virtual=False): """ Opens the specified MIDI port for the entered midi_callback Parameters: * interface_type: which interface to open: 'input' or 'output' * midi_port: MIDI port used to open the MIDI interface * is_virtual: whether or not the port is virtual Returns: * In case of opening a virtual port, it will return a MIDI interface """ if not is_virtual: self.__log.debug("Opening MIDI port: %s", PrettyFormat(midi_port)) port_name = None client_name = None else: self.__log.debug("Opening Virtual MIDI port") port_name = midi_port midi_port = None client_name = VIRTUAL_PREFFIX[:-1] try: midi_interface = open_midiport(port=midi_port, type_=interface_type, use_virtual=is_virtual, interactive=False, client_name=client_name, port_name=port_name)[0] except: error = traceback.format_exc() self.__log.info(error) self._free_midi() sys.exit() return midi_interface
def __call__(self, event, data=None): self.__log.debug("Executing callback with: %s", PrettyFormat(event)) message, deltatime = event status = message[0] if message[0] != SYSTEM_EXCLUSIVE: new_status = message[0] & 0xF0 if new_status != SYSTEM_EXCLUSIVE: #This will avoid that messages like SONG_START (0XFA) get #wrongly classified as SysEx status = new_status midi_message = self._midi_messages.get(status, None) callback_name = '_send_midi_message' if midi_message is None: if len(self._sysex_buffer) != 0: self.__log.debug("Catched SysEx message chunk") #This means that a SysEx message started callback_name = self._callback_preffix + 'system_exclusive' else: self.__log.debug("Catched unhandled MIDI message") else: self.__log.debug("Catched message: %s", midi_message) callback_name = self._callback_preffix + midi_message callback = getattr(self, callback_name) callback(message) self.__log.debug("Callback was excecuted")
def _get_midi_ports(self, midi_interface): """ Gets the available ports for the specified MIDI interface Parameters: * midi_interface: interface used for listing the ports. It can be either _midi_in or _midi_out. """ self.__log.debug("Getting available MIDI ports") ports = midi_interface.get_ports() self.__log.debug("Got:\n%s", PrettyFormat(ports)) port_index = 0 for port in ports: port_index_str = str(port_index) ports[port_index] = port port_index += 1 self.__log.debug("Fixed port indexes:\n%s", PrettyFormat(ports)) return ports
def _get_formatted_port_list(self, port_list): """ Gets the port list as follows: <port_index>: <port_name> """ self.__log.debug("Getting formatted port list") port_list_tuples = [] for port_index, port_name in enumerate(port_list): port_list_tuples.append(str(port_index + 1) + ": " + port_name) self.__log.debug("Got: %s", PrettyFormat(port_list_tuples)) return '\n\r'.join(port_list_tuples)
def _send_system_exclusive(self, message): """ Overrides the _send_system_exclusive method from MidiInputHandler. """ self.__log.debug("Sending SysEx message: %s", PrettyFormat(message)) if not self._receive_sysex(message) and self._xml_dict["@MidiEcho"]: #This means that the end of the SysEx message (0xF7) was detected, #so, no further bytes will be received. Here the SysEx buffer will #be sent and afterwards cleared self._midi_out.send_message(self._sysex_buffer) #Clears SysEx buffer self._sysex_buffer = [] #Resets SysEx count to zero self._sysex_chunk = 0 self.__log.debug("SysEx was sent")
def __init__(self, midi_in, midi_out, console_echo=False, ignore_sysex=True, ignore_timing=True, ignore_active_sense=True): """ Initializes the class attributes Parameters: * midi_in: MIDI IN interface to use * midi_out: MIDI OUT interface to use. If None, then the messages will be printed into the console * console_echo: if used together with midi_out, then the message will be first printed into the console, then it will be sent * ignore_* parameters: see the "_ignore_messages" method """ self.__log.debug("Initializing MidiInputHandler") self._midi_in = midi_in self._midi_out = midi_out self._console_echo = console_echo self._sysex_buffer = [] self._sysex_chunk = 0 self._ignore_messages(ignore_sysex, ignore_timing, ignore_active_sense) #Sets the main MIDI callback where all preprocessing will be done self._midi_in.set_callback(self) #Creates the built-in callbacks, which will only echo the MIDI message base_callback = getattr(self, '_send_midi_message') self.__log.debug("Setting callbacks") for midi_message in self._midi_messages.values(): callback = base_callback if midi_message == 'system_exclusive': callback = getattr(self, '_send_system_exclusive') message_callback_name = self._callback_preffix + midi_message if not hasattr(self, message_callback_name): #This means that the callback hasn't defined by a subclass, so #it will define here at the superclass setattr(self, message_callback_name, callback) self.__log.debug("Callbacks were set") self.__log.debug("MidiInputHandler was initialized:\n%s", PrettyFormat(self.__dict__))
def __init__(self, args, xsd_schema='conf/MidiBassPedalController.xsd'): """ Initializes the MidiConnector class Parameters: * args: command-line arguments * xsd_schema: path to the xsd schema """ self.__log.debug("Initializing MidiConnector") self._args = args self._xsd_schema = xsd_schema self._midi_in = None self._midi_out = None self._in_ports = [] self._in_port = 0 self._use_virtual_in = False self._out_ports = [] self._out_port = 0 self._use_virtual_out = False self._xml_dict = {} self.__log.debug("MidiConnector was initialized:\n%s", PrettyFormat(self.__dict__))
def _parse_xml_config(self): """ Parses the specified xml configuration file """ self.__log.info("Parsing XML config: %s", self._xsd_schema) exit = False self.__log.debug("Calling XMLSchema11 api") try: xsd_schema = xmlschema.XMLSchema11(self._xsd_schema) except: exit = True error = traceback.format_exc() self.__log.info("Error while parsing xsd file:\n%s\n\n%s", self._xsd_schema, error) if not exit: self.__log.debug("Converting XML schema to dict") try: xml_dict = xsd_schema.to_dict(self._args.config) #A last manual validation must be done here: the InitialBank value must #be less or equal than the total number of banks if xml_dict['@InitialBank'] > len(xml_dict['Bank']): raise Exception("InitialBank is higher than the possible number of " "banks / maximum: " + str(len(xml_dict['Bank'])) + \ ", given value: " + str(xml_dict['@InitialBank'])) self.__log.debug("Got: \n%s", PrettyFormat(xml_dict)) except: exit = True error = traceback.format_exc() message = "Error while parsing xml file:\n%s\n\n%s" % ( self._args.config, error) self.__log.info(message) if exit: self.__log.debug("Unexpected error occured, aborting...") self._free_midi() sys.exit() self._xml_dict = xml_dict
def parse_xml(self): """Parses the xml dict""" self.__log.debug("Parsing xml file") self._current_bank = self._xml_dict['@InitialBank'] - 1 self._previous_pedals = {} self.__log.info("Current Bank: %s", self._xml_dict['@InitialBank']) self._parse_out_channels('BassPedal', self._xml_dict) self._parse_out_channels('Chord', self._xml_dict) self._parse_velocity_transpose("BassPedal", "Velocity", self._xml_dict) self._parse_velocity_transpose("Chord", "Velocity", self._xml_dict) self._parse_velocity_transpose("BassPedal", "Transpose", self._xml_dict) self._parse_velocity_transpose("Chord", "Transpose", self._xml_dict) self._parse_octave(self._xml_dict) if self._xml_dict["@Octave"] == None: self._xml_dict["@Octave"] = 0 #Internally midi channels begin with zero self._xml_dict['@InChannel'] -= 1 self._parse_panic() self._parse_banks() self._parse_start_stop("Start") self._parse_start_stop("Stop") self.__log.debug("Got:\n%s", PrettyFormat(self._xml_dict))
def _send_midi_message(self, message): """ Overrides the _send_midi_message method from MidiInputHandler. """ self.__log.debug("Processing MIDI message: %s", PrettyFormat(message)) messages = [] status = message[0] & 0xF0 channel = message[0] & 0x0F is_controller_message = True note_messages = [] bank_select_messages = [] midi_and_sysex_messages = [] panic_message = [] bank_select = None current_velocity = None if (self._xml_dict['@InChannel'] == channel): current_bank = self._xml_dict['Bank'][self._current_bank] process_bank_select = False current_pedal = None if status in [NOTE_ON, NOTE_OFF]: self.__log.debug( "NOTE message was sent to controller, checking if " "there is a pedal") current_note = message[1] current_pedal = current_bank["@PedalList"].get(current_note) if current_pedal != None: self.__log.debug( "Registered NOTE message was found, processing " "actions") swapped_note_message = False current_velocity = message[2] if (current_velocity == 0) and self._xml_dict["@MinVelocityNoteOff"]: self.__log.debug( "Swapping NOTE ON message with a zero velocity " "to a NOTE OFF message") status = NOTE_OFF swapped_note_message = True note_messages, midi_and_sysex_messages, panic_message, bank_select = \ self._process_note_message(status, current_velocity, swapped_note_message, current_pedal, current_note) process_bank_select = (bank_select != None) else: is_controller_message = False self.__log.debug( "Unregistered NOTE message, going to check MIDI " "echo") elif status == CONTROL_CHANGE: controller = message[1] if controller == self._xml_dict["@BankSelectController"]: process_bank_select = True else: is_controller_message = False self.__log.debug( "CONTROL CHANGE message detected, going to check " "MIDI echo") if process_bank_select: self.__log.debug("SelectBank message was detected, processing " "actions") bank_select_messages = self._process_bank_select( message, bank_select) messages.extend(midi_and_sysex_messages + note_messages + bank_select_messages + panic_message) else: self.__log.debug( "Non controller message was catched, going to check " "MIDI echo") is_controller_message = False if not is_controller_message and self._xml_dict["@MidiEcho"]: self.__log.debug("Midi echo was enabled") messages = [message] elif not self._xml_dict["@MidiEcho"]: self.__log.debug("Midi echo is disabled. Message won't be sent") for message in messages: self.__log.debug("Sending MIDI message: %s", PrettyFormat(message)) self._midi_out.send_message(message)
def _process_note_message(self, status, current_velocity, swapped_note_message, current_pedal, current_note): """ Proceses the note messages for the current pedal Parameters: * status: current MIDI status; either NOTE ON or NOTE OFF * current_velocity: current velocity of the catched note * swapped_note_message: inidicated if this was a NOTE ON message with a velocity of zero, which was changed to NOTE OFF. On this case, the velocity won't be adjusted * current_pedal: current catched pedal * current_note: current MIDI note Returns a tuple with the following elements: * Note messages to send: bass pedal and chord notes. * Other MIDI messages to send: it contains first the General MIDI and SysEx messages, and the the bass and chord notes of other pedals. * Panic message if SendPanic is True. * The last element is the BankSelect message to excecute; it can be: - None: no BankSelect was found or it is not a NOTE OFF message - 'Next': select the next bank - 'Previous': select previous bank - 'Last': select the last bank - 'Quit': quit the controller software - 'Reload': restart the controller; here the whole xml - 'Reboot': reboots the computer or microcontroller running the software - 'Shutdown': shutdowns the system - 'List': sends a SysEx back with the bank names. Those messages are always processed during NOTE OFF and after all other messages. """ note_messages = self._set_note_velocity(current_pedal, status, current_velocity, swapped_note_message) messages = [] midi_and_sysex = current_pedal.get("@MessageList") if (midi_and_sysex != None) and status in midi_and_sysex: messages = midi_and_sysex[status] if (len(note_messages) > 0): if (status == NOTE_ON): if self._xml_dict["@PedalMonophony"]: self.__log.debug( "Only one pedal is allowed at the time, so, notes for " "previous pedals will be muted") self.__log.debug( "Sending NOTE OFF for previous pedals and then " "remove them:\n%s", PrettyFormat(self._previous_pedals)) pedal_list = list(self._previous_pedals.keys()) for pedal_index in pedal_list: pedal = self._previous_pedals[pedal_index]['pedal'] messages.extend( self._set_note_velocity(pedal, NOTE_OFF, current_velocity, False)) self._previous_pedals.pop(pedal_index) self.__log.debug("Adding pedal to previous pedal list") self._previous_pedals[current_note] = { 'pedal': current_pedal, 'velocity': current_velocity } else: if current_note in self._previous_pedals: self.__log.debug("Removing pedal from previous pedal list") self._previous_pedals.pop(current_note) panic_message = [] if status == NOTE_OFF: panic_message = current_pedal.get("@PanicMessage", []) if panic_message != []: self.__log.debug( "Panic message will be sent after processing messages") bank_select = None if status == NOTE_OFF: #The BANK SELECT messages will be processed only on NOTE OFF bank_select = current_pedal.get("@BankSelect") self.__log.debug("Previous pedals after processing:\n%s", PrettyFormat(self._previous_pedals)) return note_messages, messages, panic_message, bank_select
def _process_bank_select(self, message, bank_select): """ Process the BankSelect message for the current pedal Parameters: * message: last message catched by the controller * bank_select: BankSelect operation: - None: no BankSelect was found or it is not a NOTE OFF message - 'Next': select the next bank - 'Previous': select previous bank - 'Last': select the last bank - 'Quit': quit the controller software - 'Reload': restart the controller; here the whole xml - 'Reboot': reboots the computer or microcontroller running the software - 'Shutdown': shutdowns the system - 'List': sends a SysEx back with the bank names. Returns the resulting messages after processing the BankSelect message """ self.__log.debug("Previous pedals before processing Bank Select:\n%s", PrettyFormat(self._previous_pedals)) messages = [] previous_bank = self._current_bank if (bank_select != None): if bank_select not in [ "Quit", "Reload", "Reboot", "Shutdown", "List" ]: self._current_bank = bank_select self.__log.info("Bank changed to: %d", bank_select + 1) elif bank_select == "List": messages = [self._xml_dict["@BanksSysEx"]] else: self._quit = True self._status = bank_select else: select_value = message[2] if select_value < 119: if select_value >= len(self._xml_dict["Bank"]): select_value = len(self._xml_dict["Bank"]) - 1 self._current_bank = select_value self.__log.info("Bank changed to: %d", self._current_bank + 1) else: send_panic = False send_bank_list = False num_banks = len(self._xml_dict["Bank"]) if select_value == 119: messages = [self._xml_dict["@BanksSysEx"]] send_bank_list = True elif select_value == 120: self._current_bank -= 1 elif select_value == 121: self._current_bank += 1 elif select_value == 122: self._current_bank = num_banks - 1 elif select_value == 123: send_panic = True messages = self._panic_command self.__log.debug("Sending software Panic:\n%s", \ PrettyFormat(self._panic_command)) else: self._quit = True self._status = BANK_SELECT_FUNCTIONS[select_value] if not self._quit and not send_bank_list: if self._current_bank < 0: self._current_bank = num_banks - 1 elif self._current_bank >= num_banks: self._current_bank = 0 self.__log.info("Bank changed to: %d", self._current_bank + 1) if previous_bank != self._current_bank: on_bank_change = self._xml_dict.get("@OnBankChange") if on_bank_change != "ContinuePlayback": current_bank = self._xml_dict['Bank'][self._current_bank] pedal_list = current_bank["@PedalList"] self.__log.debug("Processing previous pedals for OnBankChange = %s" % \ on_bank_change) pedal_operation = "Stopping previous pedals playback" if on_bank_change == "QuickChange": pedal_operation = "Replacing previous pedals according to current" + \ " bank" self.__log.debug(pedal_operation) previous_pedals = list(self._previous_pedals.keys()) for pedal_index in previous_pedals: note_messages = [] pedal = self._previous_pedals[pedal_index]['pedal'] current_velocity = self._previous_pedals[pedal_index][ 'velocity'] remove_pedal = False if on_bank_change == "StopPlayback": remove_pedal = True else: #QuickChange new_pedal = pedal_list.get(pedal_index) if (new_pedal == None): remove_pedal = True else: #Replace this pedal self._previous_pedals[pedal_index][ 'pedal'] = new_pedal self._previous_pedals[pedal_index][ 'velocity'] = current_velocity #First NOTE_OFF messages for previous pedal will be sent note_messages = self._set_note_velocity( pedal, NOTE_OFF, current_velocity, False) #Then the NOTE_ON messages for the new pedal will be sent note_messages.extend( self._set_note_velocity( new_pedal, NOTE_ON, current_velocity, False)) if remove_pedal: self._previous_pedals.pop(pedal_index) note_messages = self._set_note_velocity( pedal, NOTE_OFF, current_velocity, False) messages.extend(note_messages) self.__log.debug( "Previous pedals after processing Bank Select:\n%s", PrettyFormat(self._previous_pedals)) return messages
def _parse_port(self, port_list, arg_name): """ Gets the specified port from command line Parameters: * port_list: List of available MIDI ports * arg_name: name of the argument to get. It can be: InPort or OutPort Returns: * A tupple containing: - either a port index or a virtual port string name - either if using a virtual or a real port """ self.__log.debug("Getting: %s from:\n%s", arg_name, PrettyFormat(port_list)) use_virtual = False num_ports = len(port_list) port_value = self._xml_dict.get('@' + arg_name, num_ports) self.__log.debug("Port value: %s", port_value) if (type(port_value) == str) and port_value.isdigit(): port_value = int(port_value) elif type(port_value) == str: is_windows = (platform.system() == "Windows") if port_value.startswith(VIRTUAL_PREFFIX): if not is_windows: #Virtual port only work unser MACOS and Linux. Windows doesn't #supports this. On the last operating system, the Virtual part will be #removed and it will be threatened as a normal port. You can assure #compatibilty between Windows and other OS by creating first the ports #with loopMIDI use_virtual = True elif port_value[-1] != '*': port_value += '*' port_value = port_value[len(VIRTUAL_PREFFIX):] if not use_virtual: self.__log.debug("Searching port") #On this case, a string with part of the name was given, so, it #will be searched in the available ports port_index = 0 port_found = False for port_name in port_list: filtered = fnmatch.filter([port_name], port_value) if filtered != []: port_found = True break port_index += 1 if not port_found: self.__log.info("The %s: %s wasn't found.", arg_name, port_value) self._free_midi() self.__log.debug("Port wasn't found, exiting") sys.exit() port_value = port_index + 1 self.__log.debug("Port was found, index: %d", port_value) else: self.__log.debug("Virutal Port will be used") if not use_virtual: #Internally, port numbers start from 0 because they are in an array port_value -= 1 if port_value >= num_ports: self.__log.info("Invalid port number was supplied") self._free_midi() self.__log.debug("Exiting after getting invalid port") sys.exit() return port_value, use_virtual