mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2025-12-22 00:02:04 +00:00
Keeps old one and adds new one as another option
This commit is contained in:
308
tools/py-midi2piano/midi2piano.py
Normal file
308
tools/py-midi2piano/midi2piano.py
Normal file
@@ -0,0 +1,308 @@
|
||||
"""
|
||||
This module allows user to convert MIDI melodies to SS13 sheet music ready
|
||||
for copy-and-paste
|
||||
"""
|
||||
from functools import reduce
|
||||
import midi as mi
|
||||
import easygui as egui
|
||||
import pyperclip as pclip
|
||||
|
||||
LINE_LENGTH_LIM = 50
|
||||
LINES_LIMIT = 200
|
||||
TICK_LAG = 0.5
|
||||
OVERALL_IMPORT_LIM = 2*LINE_LENGTH_LIM*LINES_LIMIT
|
||||
END_OF_LINE_CHAR = """
|
||||
""" # BYOND can't parse \n and I am forced to define my own NEWLINE char
|
||||
|
||||
OCTAVE_TRANSPOSE = 0 # Change here to transpose melodies by octaves
|
||||
FLOAT_PRECISION = 2 # Change here to allow more or less numbers after dot in floats
|
||||
|
||||
OCTAVE_KEYS = 12
|
||||
HIGHEST_OCTAVE = 8
|
||||
|
||||
time_quanta = 100 * TICK_LAG
|
||||
"""
|
||||
class Meta():
|
||||
version = 1.0
|
||||
integer = 1
|
||||
anti_integer = -1
|
||||
maximum = 1000
|
||||
epsilon = 0.51
|
||||
delta_epsilon = -0.1
|
||||
integral = []
|
||||
tensor = [[],[],[]]
|
||||
o_complexity = epsilon**2
|
||||
random_variance = 0.01
|
||||
"""
|
||||
|
||||
# UTILITY FUNCTIONS
|
||||
def condition(event):
|
||||
"""
|
||||
This function check if given MIDI event is meaningful
|
||||
"""
|
||||
if event[0] == 'track_name' and event[2] == 'Drums': # Percussion
|
||||
return False
|
||||
if event[0] == 'note': # Only thing that matters
|
||||
return True
|
||||
return False
|
||||
|
||||
def notenum2string(num, accidentals, octaves):
|
||||
"""
|
||||
This function converts given notenum to SS13 note according to previous
|
||||
runs expressed using _accidentals_ and _octaves_
|
||||
"""
|
||||
names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
||||
convert_table = {1:0, 3:1, 6:2, 8:3, 10:4}
|
||||
inclusion_table = {0:0, 2:1, 5:2, 7:3, 9:4}
|
||||
|
||||
num += OCTAVE_KEYS * OCTAVE_TRANSPOSE
|
||||
octave = int(num / OCTAVE_KEYS)
|
||||
if octave < 1 or octave > HIGHEST_OCTAVE:
|
||||
return ["", accidentals, octaves]
|
||||
|
||||
accidentals = accidentals.copy()
|
||||
octaves = octaves.copy()
|
||||
|
||||
output_octaves = list(octaves)
|
||||
name_indx = num % OCTAVE_KEYS
|
||||
|
||||
accidental = (len(names[name_indx]) == 2)
|
||||
output_octaves[name_indx] = octave
|
||||
add_n = False
|
||||
|
||||
if accidental:
|
||||
accidentals[convert_table[name_indx]] = True
|
||||
else:
|
||||
if name_indx in inclusion_table:
|
||||
add_n = accidentals[inclusion_table[name_indx]]
|
||||
accidentals[inclusion_table[name_indx]] = False
|
||||
|
||||
return [
|
||||
(
|
||||
names[name_indx]+
|
||||
("n" if add_n else "")+
|
||||
str((octave if octave != octaves[name_indx] else ""))
|
||||
),
|
||||
accidentals,
|
||||
output_octaves
|
||||
]
|
||||
|
||||
def dur2mod(dur, bpm_mod=1.0):
|
||||
"""
|
||||
This functions returns float representation of duration ready to be
|
||||
added to the note after /
|
||||
"""
|
||||
mod = bpm_mod / dur
|
||||
mod = round(mod, FLOAT_PRECISION)
|
||||
return str(mod).rstrip('0').rstrip('.')
|
||||
# END OF UTILITY FUNCTIONS
|
||||
|
||||
# CONVERSION FUNCTIONS
|
||||
def obtain_midi_file():
|
||||
"""
|
||||
Asks user to select MIDI and returns this file opened in binary mode for reading
|
||||
"""
|
||||
file = egui.fileopenbox(msg='Choose MIDI file to convert',
|
||||
title='MIDI file selection',
|
||||
filetypes=[['*.mid', 'MID files']])
|
||||
if not file:
|
||||
return None
|
||||
file = open(file, mode='rb').read()
|
||||
return file
|
||||
|
||||
def midi2score_without_ticks(midi_file):
|
||||
"""
|
||||
Transforms aforementioned file into a score, truncates it and returns it
|
||||
"""
|
||||
opus = mi.midi2opus(midi_file)
|
||||
opus = mi.to_millisecs(opus)
|
||||
score = mi.opus2score(opus)
|
||||
return score[1:] # Ticks don't matter anymore, it is always 1000
|
||||
|
||||
def filter_events_from_score(score):
|
||||
"""
|
||||
Filters out irrevelant events and returns new score
|
||||
"""
|
||||
return list(map( # For each score track
|
||||
lambda score_track: list(filter( # Filter irrevelant events
|
||||
condition,
|
||||
score_track
|
||||
)),
|
||||
score
|
||||
))
|
||||
|
||||
def filter_empty_tracks(score):
|
||||
"""
|
||||
Filters out empty tracks and returns new score
|
||||
"""
|
||||
return list(filter(
|
||||
lambda score_track: score_track,
|
||||
score))
|
||||
|
||||
|
||||
def filter_start_time_and_note_num(score):
|
||||
"""
|
||||
Recreates score with only note numbers and start time of each note and returns new score
|
||||
"""
|
||||
return list(map(
|
||||
lambda score_track: list(map(
|
||||
lambda event: [event[1], event[4]],
|
||||
score_track)),
|
||||
score))
|
||||
|
||||
def merge_events(score):
|
||||
"""Merges all tracks together and returns new score"""
|
||||
return list(reduce(
|
||||
lambda lst1, lst2: lst1+lst2,
|
||||
score))
|
||||
|
||||
def sort_score_by_event_times(score):
|
||||
"""Sorts events by start time and returns new score"""
|
||||
return list(map(
|
||||
lambda index: score[index],
|
||||
sorted(
|
||||
list(range(len(score))),
|
||||
key=lambda indx: score[indx][0])
|
||||
))
|
||||
|
||||
def convert_into_delta_times(score):
|
||||
"""
|
||||
Transform start_time into delta_time and returns new score
|
||||
"""
|
||||
return list(map(
|
||||
lambda super_event: (
|
||||
[
|
||||
super_event[1][0]-super_event[0][0],
|
||||
super_event[0][1]
|
||||
]), # [ [1, 2], [3, 4] ] -> [ [2, 2] ]
|
||||
zip(score[:-1], score[1:]) # Shifted association. [1, 2, 3] -> [ (1, 2), (2, 3) ]
|
||||
))+[[1000, score[-1][1]]] # Add 1 second note to the end
|
||||
|
||||
def perform_roundation(score):
|
||||
"""
|
||||
Rounds delta times to the nearest multiple of time quanta as BYOND can't
|
||||
process duration less than that and returns new score
|
||||
"""
|
||||
return list(map(
|
||||
lambda event: [time_quanta*round(event[0]/time_quanta), event[1]],
|
||||
score))
|
||||
|
||||
def obtain_common_duration(score):
|
||||
"""
|
||||
Returns the most frequent duration throughout the whole melody
|
||||
"""
|
||||
# Parse durations and filter out 0s
|
||||
durs = list(filter(lambda x: x, list(map(lambda event: event[0], score))))
|
||||
unique_durs = []
|
||||
for dur in durs:
|
||||
if dur not in unique_durs:
|
||||
unique_durs.append(dur)
|
||||
# How many such durations occur throughout the melody?
|
||||
counter = [durs.count(dur) for dur in unique_durs]
|
||||
highest_counter = max(counter) # Highest counter
|
||||
dur_n_count = list(zip(durs, counter))
|
||||
dur_n_count = list(filter(lambda e: e[1] == highest_counter, dur_n_count))
|
||||
return dur_n_count[0][0] # Will be there
|
||||
|
||||
def reduce_score_to_chords(score):
|
||||
"""
|
||||
Reforms score into a chord-duration list:
|
||||
[[chord_notes], duration_of_chord]
|
||||
and returns it
|
||||
"""
|
||||
new_score = []
|
||||
new_chord = [[], 0]
|
||||
# [ [chord notes], duration of chord ]
|
||||
for event in score:
|
||||
new_chord[0].append(event[1]) # Append new note to the chord
|
||||
if event[0] == 0:
|
||||
continue # Add new notes to the chord until non-zero duration is hit
|
||||
new_chord[1] = event[0] # This is the duration of chord
|
||||
new_score.append(new_chord) # Append chord to the list
|
||||
new_chord = [[], 0] # Reset the chord
|
||||
return new_score
|
||||
|
||||
def obtain_sheet_music(score, most_frequent_dur):
|
||||
"""
|
||||
Returns unformated sheet music from score
|
||||
"""
|
||||
result = ""
|
||||
|
||||
octaves = [3 for i in range(12)]
|
||||
accidentals = [False for i in range(7)]
|
||||
for event in score:
|
||||
for note_indx in range(len(event[0])):
|
||||
data = notenum2string(event[0][note_indx], accidentals, octaves)
|
||||
result += data[0]
|
||||
accidentals = data[1]
|
||||
octaves = data[2]
|
||||
if note_indx != len(event[0])-1:
|
||||
result += '-'
|
||||
|
||||
if event[1] != most_frequent_dur: # Quarters are default
|
||||
result += '/'
|
||||
result += dur2mod(event[1], most_frequent_dur)
|
||||
result += ','
|
||||
|
||||
return result
|
||||
|
||||
def explode_sheet_music(sheet_music):
|
||||
"""
|
||||
Splits unformatted sheet music into formated lines of LINE_LEN_LIM
|
||||
and such and returns a list of such lines
|
||||
"""
|
||||
split_music = sheet_music.split(',')
|
||||
split_music = list(map(lambda note: note+',', split_music))
|
||||
split_list = []
|
||||
counter = 0
|
||||
line_counter = 1
|
||||
for note in split_music:
|
||||
if line_counter > LINES_LIMIT-1:
|
||||
break
|
||||
if counter+len(note) > LINE_LENGTH_LIM-2:
|
||||
split_list[-1] = split_list[-1].rstrip(',')
|
||||
split_list[-1] += END_OF_LINE_CHAR
|
||||
counter = 0
|
||||
line_counter += 1
|
||||
split_list.append(note)
|
||||
counter += len(note)
|
||||
|
||||
return split_list
|
||||
|
||||
def finalize_sheet_music(split_music, most_frequent_dur):
|
||||
"""
|
||||
Recreates sheet music from exploded sheet music, truncates it and returns it
|
||||
"""
|
||||
sheet_music = ""
|
||||
for note in split_music:
|
||||
sheet_music += note
|
||||
sheet_music = sheet_music.rstrip(',') # Trim the last ,
|
||||
sheet_music = "BPM: " + str(int(60000 / most_frequent_dur)) + END_OF_LINE_CHAR + sheet_music
|
||||
return sheet_music[:min(len(sheet_music), OVERALL_IMPORT_LIM)]
|
||||
# END OF CONVERSION FUNCTIONS
|
||||
|
||||
def main_cycle():
|
||||
"""
|
||||
Activate the script
|
||||
"""
|
||||
while True:
|
||||
midi_file = obtain_midi_file()
|
||||
if not midi_file:
|
||||
return # Cancel
|
||||
score = midi2score_without_ticks(midi_file)
|
||||
score = filter_events_from_score(score)
|
||||
score = filter_start_time_and_note_num(score)
|
||||
score = filter_empty_tracks(score)
|
||||
score = merge_events(score)
|
||||
score = sort_score_by_event_times(score)
|
||||
score = convert_into_delta_times(score)
|
||||
score = perform_roundation(score)
|
||||
most_frequent_dur = obtain_common_duration(score)
|
||||
score = reduce_score_to_chords(score)
|
||||
sheet_music = obtain_sheet_music(score, most_frequent_dur)
|
||||
split_music = explode_sheet_music(sheet_music)
|
||||
sheet_music = finalize_sheet_music(split_music, most_frequent_dur)
|
||||
|
||||
pclip.copy(sheet_music)
|
||||
|
||||
main_cycle()
|
||||
Reference in New Issue
Block a user