import mido import argparse import sys import os def set_all_velocities(input_path, new_velocity, ignored_channels=None, verbose=False): """ Sets the velocity of all note_on and note_off messages in a MIDI file to a specified value, ignoring specified channels, and saves the modified MIDI file with '_velfix' appended to the original filename. Args: input_path (str): Path to the input MIDI file. new_velocity (int): The new velocity value (0-127) to set. ignored_channels (set[int], optional): Set of MIDI channels to ignore (1-16). Defaults to None. verbose (bool): If True, prints detailed processing information. """ try: midi = mido.MidiFile(input_path) if verbose: print(f"Loaded MIDI file '{input_path}' successfully.") except IOError: print(f"Error: Cannot open input MIDI file '{input_path}'. Please check the path.") sys.exit(1) except mido.KeySignatureError as e: print(f"Error reading MIDI file: {e}") sys.exit(1) total_messages = 0 modified_messages = 0 ignored_messages = 0 for track in midi.tracks: for msg in track: total_messages += 1 # Check if the message has a velocity attribute and a channel if msg.type in ['note_on', 'note_off'] and msg.type == 'note_on' or msg.type == 'note_off': # Some messages might not have a channel attribute if hasattr(msg, 'channel'): # MIDI channels are 0-15 in mido, so convert ignored_channels from 1-16 to 0-15 msg_channel = msg.channel + 1 # Convert to 1-based for comparison if ignored_channels and msg_channel in ignored_channels: ignored_messages += 1 continue # Skip modifying this message # Modify velocity if not ignored if msg.velocity != new_velocity: msg.velocity = new_velocity modified_messages += 1 # Generate the output file name by appending '_velfix' before the file extension base, ext = os.path.splitext(input_path) output_path = f"{base}_velfix{ext}" try: midi.save(output_path) if verbose: print(f"Saved modified MIDI to '{output_path}'.") print(f"Total messages processed: {total_messages}") print(f"Total messages modified: {modified_messages}") if ignored_channels: print(f"Total messages ignored (channels {sorted(ignored_channels)}): {ignored_messages}") else: print(f"Successfully saved modified MIDI to '{output_path}' with all velocities set to {new_velocity}.") if ignored_channels: print(f"Ignored velocities on channels: {sorted(ignored_channels)}.") except IOError: print(f"Error: Cannot write to output file '{output_path}'. Please check the path and permissions.") sys.exit(1) def validate_velocity(value): """ Validates that the velocity is an integer between 0 and 127. Args: value (str): The velocity value as a string. Returns: int: The validated velocity as an integer. Raises: argparse.ArgumentTypeError: If the value is not within the valid range. """ try: ivalue = int(value) except ValueError: raise argparse.ArgumentTypeError(f"Velocity must be an integer between 0 and 127. '{value}' is invalid.") if ivalue < 0 or ivalue > 127: raise argparse.ArgumentTypeError(f"Velocity must be between 0 and 127. '{ivalue}' is out of range.") return ivalue def validate_channels(value): """ Validates that the channels are integers between 1 and 16. Args: value (str): Comma-separated channel numbers as a string. Returns: set[int]: A set of validated channel numbers. Raises: argparse.ArgumentTypeError: If any channel is not within the valid range. """ try: channels = set(int(ch.strip()) for ch in value.split(',') if ch.strip() != '') except ValueError: raise argparse.ArgumentTypeError(f"Channels must be integers between 1 and 16, separated by commas. '{value}' is invalid.") for ch in channels: if ch < 1 or ch > 16: raise argparse.ArgumentTypeError(f"Channel numbers must be between 1 and 16. '{ch}' is out of range.") return channels def parse_arguments(): """ Parses command-line arguments. Returns: argparse.Namespace: The parsed arguments. """ parser = argparse.ArgumentParser( description=( "Set all velocities in a MIDI file to a specified number, optionally ignoring specified channels, " "and save with '_velfix' appended to the filename." ) ) parser.add_argument('input_midi', type=str, help='Path to the input MIDI file.') parser.add_argument('velocity', type=validate_velocity, help='New velocity value (0-127).') parser.add_argument( 'ignore_channels', type=validate_channels, nargs='?', default=None, help='(Optional) Comma-separated list of MIDI channels to ignore (e.g., "4,9"). Channels are 1-16.' ) parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output.') return parser.parse_args() def main(): args = parse_arguments() # Check if input file exists if not os.path.isfile(args.input_midi): print(f"Error: The input file '{args.input_midi}' does not exist.") sys.exit(1) set_all_velocities( args.input_midi, args.velocity, ignored_channels=args.ignore_channels, verbose=args.verbose ) if __name__ == "__main__": main()