You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
154 lines
5.7 KiB
154 lines
5.7 KiB
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()
|