Skip to content

Instantly share code, notes, and snippets.

@do0m-nametaken
Created August 23, 2024 04:20
Show Gist options
  • Save do0m-nametaken/2bc285e3aa3164545ed5d3a85ca47c2c to your computer and use it in GitHub Desktop.
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
# 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