Created
August 23, 2024 04:20
-
-
Save do0m-nametaken/2bc285e3aa3164545ed5d3a85ca47c2c to your computer and use it in GitHub Desktop.
Quick n dirty Python script for creating pitch wheel events to channels in a midi file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Quick n dirty Python script for creating pitch wheel events to channels in a midi file | |
# Made approx. in late 2023 | |
import mido | |
import easing_functions | |
import math | |
def oscillate_function(waveform, t, amplitude, period, phase_shift=0, vertical_shift=0): | |
t += phase_shift | |
functions = { | |
"sine": amplitude * math.sin( (2 * math.pi / period) * t) | |
,"triangle": ((amplitude*math.pi) / 5) * math.asin(math.sin((2 * math.pi / period) * t)) | |
,"sawtooth": ((2*amplitude) / math.pi) * math.atan(math.tan((2 * math.pi / period) * t)) | |
,"square": amplitude * math.copysign(1, math.sin( (2 * math.pi / period) * t))} | |
return functions[waveform] + vertical_shift | |
def get_user_input(prompt, default=None): | |
user_input = input(prompt) | |
if user_input: return user_input | |
return default | |
def number_isbounded(value, num_min=None, num_max=None): | |
if num_min is not None and value < num_min: | |
print(f"!!! Input was less than the minimum ({str(num_min)})!") | |
return False | |
elif num_max is not None and value > num_max: | |
print(f"!!! Input was more than the maximum ({str(num_max)})!") | |
return False | |
return True | |
def get_integer_input(prompt, default=None, min=None, max=None): | |
user_input = input(prompt) | |
if not user_input: | |
if default is not None: return default | |
print("- Input cannot be blank!") | |
return get_integer_input(prompt, default, min, max) | |
try: | |
if not number_isbounded(int(user_input), min, max): | |
return get_integer_input(prompt, default, min, max) | |
else: | |
return int(user_input) | |
except: | |
print("!!! Invalid input!") | |
return get_integer_input(prompt, default, min, max) | |
def get_float_input(prompt, default=None, min=None, max=None): | |
user_input = input(prompt) | |
if not user_input: | |
if default is not None: return default | |
print("!!! Input cannot be blank!") | |
return get_float_input(prompt, default, min, max) | |
try: | |
if not number_isbounded(float(user_input), min, max): | |
return get_float_input(prompt, default, min, max) | |
else: | |
return float(user_input) | |
except ValueError: | |
print("!!! Invalid input!") | |
return get_float_input(prompt, default, min, max) | |
def get_eval_input(prompt, default=None, expected_types=[], number_min=None, number_max=None): | |
try: | |
user_input = eval(input(prompt)) | |
except: | |
print("!!! Unevaluable input!") | |
return get_eval_input(prompt, default, expected_types, number_min, number_max) | |
if not type(expected_types) == tuple and type(user_input) != expected_types: | |
print("!!! Unexpected value type!") | |
return get_eval_input(prompt, default, expected_types, number_min, number_max) | |
if type(user_input) not in expected_types: | |
print("!!! Unexpected value type!") | |
return get_eval_input(prompt, default, expected_types, number_min, number_max) | |
if type(user_input) in (int, float) and not number_isbounded(user_input, number_min, number_max): | |
return get_eval_input(prompt, default, expected_types, number_min, number_max) | |
return user_input | |
def get_start_tick_input(): | |
stprompt = "\nEnter the start tick/s\n" \ | |
"(You can enter multiple start and end ticks at once by separating them with commas)\n" \ | |
"-->" | |
input = get_eval_input(stprompt, expected_types=(int, tuple), number_min=0) | |
if type(input) == tuple: | |
for st in input: | |
if type(st) != int and not number_isbounded(st, min=0): return get_start_tick_input() | |
return input | |
def get_end_tick_input(): | |
etprompt = "\nEnter the end tick" + "s.\nENTER THE RESPECTIVE END TICKS FOR YOUR PREVIOUS INPUT!\n-->" | |
input = get_eval_input(etprompt, expected_types=(int, tuple), number_min=0) | |
if type(input) == tuple: | |
for st in input: | |
if type(st) != int and not number_isbounded(st, min=0): return get_start_tick_input() | |
return input | |
def get_function_input(): | |
function = get_user_input( | |
"\nEnter the easing function to use.\n" \ | |
"\tcan either be:\n" \ | |
"\t\t- any easing function available in the easing_functions library\n" \ | |
"\t\t- any of these aliases: 'gt bend', 'gt bend release', 'linear'\n" \ | |
"-->") | |
aliases = { | |
"gt bend": easing_functions.SineEaseIn, | |
"gt bend release": easing_functions.SineEaseOut, | |
"linear": easing_functions.LinearInOut} | |
functionlist = [ | |
'BackEaseIn', 'BackEaseInOut', 'BackEaseOut', | |
'BounceEaseIn', 'BounceEaseInOut', 'BounceEaseOut', | |
'CircularEaseIn', 'CircularEaseInOut', 'CircularEaseOut', | |
'CubicEaseIn', 'CubicEaseInOut', 'CubicEaseOut', | |
'ElasticEaseIn', 'ElasticEaseInOut', 'ElasticEaseOut', | |
'ExponentialEaseIn', 'ExponentialEaseInOut', 'ExponentialEaseOut', | |
'LinearInOut', 'QuadEaseIn', 'QuadEaseInOut', | |
'QuadEaseOut', 'QuarticEaseIn', 'QuarticEaseInOut', | |
'QuarticEaseOut', 'QuinticEaseIn', 'QuinticEaseInOut', | |
'QuinticEaseOut', 'SineEaseIn', 'SineEaseInOut', 'SineEaseOut'] | |
if function is None: | |
print("!!! Input cannot be blank!") | |
return get_function_input() | |
elif function in functionlist: | |
return eval("easing_functions." + function) | |
elif function in aliases: | |
return aliases[function] | |
else: | |
print("!!! Invalid input!") | |
return get_function_input() | |
def get_increment_input(): | |
# I got lazy with this one | |
inp = get_user_input( | |
"Input the increment value if you want to create pitch bend events in steps\n" \ | |
"(input wil be evaluated as code):" | |
) | |
if not inp: | |
return None | |
else: | |
return int(eval(inp)) | |
def get_oscillate_input(pitchwheel_range): | |
prompts = { | |
"usevibrato": "\nUse vibrato?\n('Y' for YES, NO if else)\n-->", | |
"tempo": "\nEnter the tempo of the song at the time of the start tick\n-->", | |
"wave": "\nWhat should the waveform be?\n" \ | |
"\tPress...\n" \ | |
"\t\t- '1' for a SINE wave\t(DEFAULT)\n" \ | |
"\t\t- '2' for a TRIANGLE wave\n" \ | |
"\t\t- '3' for a SAWTOOTH wave\n" \ | |
"\t\t- '4' for a SQUARE wave\n" \ | |
"-->", | |
"vibrato_rate": "\nEnter the vibrato rate in MILLISECONDS\n(DEFAULT = 120)\n-->", | |
"vibrato_depth": "\nEnter the vibrato depth in CENTS\n(DEFAULT = 20)\n-->", | |
"phase_start": "\nWhere should the vibrato wave start?\n" \ | |
"\tPress...\n" \ | |
"\t\t- '1' for the bottom of the wave\n" \ | |
"\t\t- '2' for the middle of the wave\t(DEFAULT)\n" \ | |
"\t\t- '3' for the peak of the wave\n" \ | |
"-->", | |
"vertical_start": "\nWhere should the vibrato wave align with the center pitch vertically?\n" \ | |
"\tPress...\n" \ | |
"\t\t- '1' for 1 amplitude LOWER\n" \ | |
"\t\t- '2' to leave unchanged\t(DEFAULT)\n" \ | |
"\t\t- '3' for 1 amplitude HIGHER\n" \ | |
"-->",} | |
usevibrato = get_user_input(prompts["usevibrato"]) | |
if not usevibrato: return None, None, None, None, None | |
tempo = get_float_input(prompts["tempo"], default=120.0, min=1.0, max=60000000.0) | |
wavechoice = get_integer_input(prompts["wave"], default=1, min=1, max=4) | |
oscillate = ("sine", "triangle", "sawtooth", "square")[wavechoice-1] | |
vibrato_rate = get_float_input(prompts["vibato_rate"], default=120.0) | |
vibrato_depth = get_float_input(prompts["vibrato_depth"], default=20.0) | |
period = 192 * ( (vibrato_rate/1000)/( 1/(tempo/60)) ) | |
amplitude = ((vibrato_depth/100) * (8191.5/pitchwheel_range))/2 | |
phase_start_choice = get_integer_input(prompts["phase_start"], default=2, min=1, max=3) | |
phase_shift = [(-1*(period/4)), 0, (period/4)][phase_start_choice-1] | |
vertical_start_choice = get_integer_input(prompts["vertical_shift"], default=2, min=1, max=3) | |
vertical_shift = [(amplitude*-1), 0, amplitude][vertical_start_choice-1] | |
return oscillate, amplitude, period, phase_shift, vertical_shift | |
def insert_msg_to_tickpos(track, msg, tick, skip=None): | |
if skip: | |
i, current_tickpos = skip | |
else: | |
i, current_tickpos = 0, 0 | |
index = None | |
while True: | |
if (len(track) - 1) < i: break | |
previous_tickpos = current_tickpos | |
current_tickpos += track[i].time | |
if current_tickpos > tick: | |
track.insert(i, msg) | |
track[i + 1].time = current_tickpos - tick | |
track[i].time = int(tick) - previous_tickpos | |
index = i | |
break | |
elif current_tickpos == tick: | |
track.insert(i, msg) | |
index = i | |
break | |
i += 1 | |
if index == None: | |
track.append(msg) | |
track[-1].time = int(tick) - current_tickpos | |
index = len(track) - 1 | |
skip = index, int(tick) | |
def create_pitch_bend_events(track, channel, pitchwheel_range, start_tick, end_tick, start_pitch, end_pitch, easing_function, increment, oscillate, amplitude, period, phase_shift, vertical_shift): | |
if type(start_tick) == tuple: | |
print(start_tick) | |
for i in range(len(start_tick)): | |
print("A" + str(i)) | |
create_pitch_bend_events( | |
track, channel, pitchwheel_range, start_tick[i], end_tick[i], start_pitch, end_pitch, easing_function, increment, | |
oscillate, amplitude, period, phase_shift, vertical_shift) | |
return | |
start_pitch *= 8191.5/pitchwheel_range | |
end_pitch *= 8191.5/pitchwheel_range | |
if increment: | |
ticks = [] | |
for x in range(increment): | |
ticks.append(((end_tick-start_tick)/increment) * x) | |
else: | |
ticks = range(1, (end_tick-start_tick) + 1) | |
skip = None | |
for tick in ticks: | |
function_ease = start_pitch | |
if start_pitch != end_pitch: | |
function_ease = easing_function(start=start_pitch, end=end_pitch, duration=end_tick-start_tick)(tick) | |
function_oscillate = 0 | |
if oscillate: | |
function_oscillate = oscillate_function(oscillate, tick, amplitude, period, phase_shift, vertical_shift) | |
pitch = round(function_ease+function_oscillate) | |
insert_msg_to_tickpos(track, mido.Message("pitchwheel", channel=channel, pitch=pitch), int(start_tick+tick), skip) | |
def main(): | |
prompts = { | |
"midifile": "\nEnter the MIDI file location\n-->", | |
"track_num": "\nEnter the track numnber\n-->", | |
"channel": "\nEnter the channel\n(Enter value between 0 - 15)\n-->", | |
"end_tick": "\nEnter the end tick", | |
"pitchwheel_range": "\nEnter the pitch bend range of the channel at the time of the start tick\n", | |
"start_pitch": "\nEnter the START pitch value in SEMITONES\n-->", | |
"end_pitch": "\nEnter the END pitch vlaue in SEMITONES\n-->", | |
} | |
inconstant_prompts = {} | |
retaineddefs_pitchwheel_range = [ | |
None, None, None, None, None, | |
None, None, None, None, None, | |
None, None, None, None, None] | |
midifile = input(prompts["midifile"]) | |
events_towrite = [] | |
while True: | |
track_num = get_integer_input(prompts["track_num"], default="DEFAULT", min=0) | |
if track_num == "DEFAULT": | |
break | |
print(f"\n- Writing pitch events to track {track_num}") | |
count = 0 | |
while True: | |
count += 1 | |
print(f"[{count}]") | |
channel = get_integer_input(prompts["channel"], default="DEFAULT", min=0, max=15) | |
if channel == "DEFAULT": break | |
pitchwheel_range_finalprompt = prompts["pitchwheel_range"] | |
if retaineddefs_pitchwheel_range[channel] is not None: | |
pitchwheel_range_finalprompt += " (RETAINED FROM PREVIOUS INPUT)" | |
pitchwheel_range_finaldefault = retaineddefs_pitchwheel_range[channel] | |
else: | |
pitchwheel_range_finaldefault = 2 | |
pitchwheel_range_finalprompt += f"(DEFAULT = 2)" | |
pitchwheel_range_finalprompt += "\n-->" | |
start_tick = get_start_tick_input() | |
if type(start_tick) == int: | |
end_tick = get_integer_input(prompts["end_tick"] + ".\n-->", min=0) | |
else: | |
end_tick = get_end_tick_input() | |
print(f"{start_tick}, {end_tick}") | |
pitchwheel_range = get_integer_input(pitchwheel_range_finalprompt, default=pitchwheel_range_finaldefault, min=1, max=24) | |
start_pitch = get_float_input(prompts["start_pitch"], min=(-1*pitchwheel_range), max=pitchwheel_range) | |
end_pitch = get_float_input(prompts["end_pitch"], min = pitchwheel_range * -1, max = pitchwheel_range) | |
function, increment = None, None | |
if start_pitch != end_pitch: function, increment = get_function_input(), get_increment_input() | |
oscillate, amplitude, period, phase_shift, vertical_shift = get_oscillate_input(pitchwheel_range) | |
events_towrite.append( (track_num, | |
channel, pitchwheel_range, start_tick, end_tick, start_pitch, end_pitch, function, increment, | |
oscillate, amplitude, period, phase_shift, vertical_shift) ) | |
with mido.MidiFile(midifile) as mid: | |
for event in events_towrite: | |
create_pitch_bend_events(mid.tracks[event[0]], | |
event[1], event[2], event[3], event[4], event[5], event[6], | |
event[7], event[8], event[9], event[10], event[11], event[12], event[13]) | |
mid.save(input("Enter the new MIDI file location: ")) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment