"""Ultrastar writer module""" import re import langcodes from packaging import version from modules.console_colors import ULTRASINGER_HEAD from modules.Ultrastar.ultrastar_converter import ( real_bpm_to_ultrastar_bpm, second_to_beat, beat_to_second, ) from modules.Ultrastar.ultrastar_txt import UltrastarTxtValue, UltrastarTxtTag, UltrastarTxtNoteTypeTag, \ FILE_ENCODING from modules.Speech_Recognition.TranscribedData import TranscribedData from modules.Ultrastar.ultrastar_score_calculator import Score def get_thirtytwo_note_second(real_bpm: float): """Converts a beat to a 1/32 note in second""" return 60 / real_bpm / 8 def get_sixteenth_note_second(real_bpm: float): """Converts a beat to a 1/16 note in second""" return 60 / real_bpm / 4 def get_eighth_note_second(real_bpm: float): """Converts a beat to a 1/8 note in second""" return 60 / real_bpm / 2 def get_quarter_note_second(real_bpm: float): """Converts a beat to a 1/4 note in second""" return 60 / real_bpm def get_half_note_second(real_bpm: float): """Converts a beat to a 1/2 note in second""" return 60 / real_bpm * 2 def get_whole_note_second(real_bpm: float): """Converts a beat to a 1/1 note in second""" return 60 / real_bpm * 4 def get_multiplier(real_bpm: float) -> int: """Calculates the multiplier for the BPM""" if real_bpm == 0: raise Exception("BPM is 0") multiplier = 1 result = 0 while result < 400: result = real_bpm * multiplier multiplier += 1 return multiplier - 2 def get_language_name(language: str) -> str: """Creates the language name from the language code""" return langcodes.Language.make(language=language).display_name() def create_ultrastar_txt_from_automation( transcribed_data: list[TranscribedData], note_numbers: list[int], ultrastar_file_output: str, ultrastar_class: UltrastarTxtValue, real_bpm=120, ) -> None: """Creates an Ultrastar txt file from the automation data""" print( f"{ULTRASINGER_HEAD} Creating {ultrastar_file_output} from transcription." ) ultrastar_bpm = real_bpm_to_ultrastar_bpm(real_bpm) multiplication = get_multiplier(ultrastar_bpm) ultrastar_bpm = ultrastar_bpm * get_multiplier(ultrastar_bpm) silence_split_duration = calculate_silent_beat_length(transcribed_data) with open(ultrastar_file_output, "w", encoding=FILE_ENCODING) as file: gap = transcribed_data[0].start if version.parse(ultrastar_class.version) >= version.parse("1.0.0"): file.write(f"#{UltrastarTxtTag.VERSION}:{ultrastar_class.version}\n"), file.write(f"#{UltrastarTxtTag.ARTIST}:{ultrastar_class.artist}\n") file.write(f"#{UltrastarTxtTag.TITLE}:{ultrastar_class.title}\n") if ultrastar_class.year is not None: file.write(f"#{UltrastarTxtTag.YEAR}:{ultrastar_class.year}\n") if ultrastar_class.language is not None: file.write(f"#{UltrastarTxtTag.LANGUAGE}:{get_language_name(ultrastar_class.language)}\n") if ultrastar_class.genre: file.write(f"#{UltrastarTxtTag.GENRE}:{ultrastar_class.genre}\n") if ultrastar_class.cover is not None: file.write(f"#{UltrastarTxtTag.COVER}:{ultrastar_class.cover}\n") file.write(f"#{UltrastarTxtTag.MP3}:{ultrastar_class.mp3}\n") if version.parse(ultrastar_class.version) >= version.parse("1.1.0"): file.write(f"#{UltrastarTxtTag.AUDIO}:{ultrastar_class.audio}\n") if ultrastar_class.vocals is not None: file.write(f"#{UltrastarTxtTag.VOCALS}:{ultrastar_class.vocals}\n") if ultrastar_class.instrumental is not None: file.write(f"#{UltrastarTxtTag.INSTRUMENTAL}:{ultrastar_class.instrumental}\n") if ultrastar_class.tags is not None: file.write(f"#{UltrastarTxtTag.TAGS}:{ultrastar_class.tags}\n") file.write(f"#{UltrastarTxtTag.VIDEO}:{ultrastar_class.video}\n") file.write(f"#{UltrastarTxtTag.BPM}:{round(ultrastar_bpm, 2)}\n") # not the real BPM! file.write(f"#{UltrastarTxtTag.GAP}:{int(gap * 1000)}\n") file.write(f"#{UltrastarTxtTag.CREATOR}:{ultrastar_class.creator}\n") file.write(f"#{UltrastarTxtTag.COMMENT}:{ultrastar_class.comment}\n") # Write the singing part previous_end_beat = 0 separated_word_silence = [] # This is a workaround for separated words that get his ends to far away for i, data in enumerate(transcribed_data): start_time = (data.start - gap) * multiplication end_time = ( data.end - data.start ) * multiplication start_beat = round(second_to_beat(start_time, real_bpm)) duration = round(second_to_beat(end_time, real_bpm)) # Fix the round issue, so the beats don’t overlap start_beat = max(start_beat, previous_end_beat) previous_end_beat = start_beat + duration # Calculate the silence between the words if i < len(transcribed_data) - 1: silence = (transcribed_data[i + 1].start - data.end) else: silence = 0 # : 10 10 10 w # ':' start midi part # 'n1' start at real beat # 'n2' duration at real beat # 'n3' pitch where 0 == C4 # 'w' lyric line = f"{UltrastarTxtNoteTypeTag.NORMAL} " \ f"{str(start_beat)} " \ f"{str(duration)} " \ f"{str(note_numbers[i])} " \ f"{data.word}\n" file.write(line) # detect silence between words if not transcribed_data[i].is_word_end: separated_word_silence.append(silence) continue if silence_split_duration != 0 and silence > silence_split_duration or any( s > silence_split_duration for s in separated_word_silence) and i != len(transcribed_data) - 1: # - 10 # '-' end of current sing part # 'n1' show next at time in real beat show_next = ( second_to_beat(data.end - gap, real_bpm) * multiplication ) linebreak = f"{UltrastarTxtTag.LINEBREAK} " \ f"{str(round(show_next))}\n" file.write(linebreak) separated_word_silence = [] file.write(f"{UltrastarTxtTag.FILE_END}") def deviation(silence_parts): """Calculates the deviation of the silence parts""" if len(silence_parts) < 5: return 0 # Remove the longest part so the deviation is not that high sorted_parts = sorted(silence_parts) filtered_parts = [part for part in sorted_parts if part < sorted_parts[-1]] sum_parts = sum(filtered_parts) if sum_parts == 0: return 0 mean = sum_parts / len(filtered_parts) return mean def calculate_silent_beat_length(transcribed_data: list[TranscribedData]): print(f"{ULTRASINGER_HEAD} Calculating silence parts for linebreaks.") silent_parts = [] for i, data in enumerate(transcribed_data): if i < len(transcribed_data) - 1: silent_parts.append(transcribed_data[i + 1].start - data.end) return deviation(silent_parts) def create_repitched_txt_from_ultrastar_data( input_file: str, note_numbers: list[int], output_repitched_ultrastar: str ) -> None: """Creates a repitched ultrastar txt file from the original one""" # todo: just add '_repitched' to input_file print( "{PRINT_ULTRASTAR} Creating repitched ultrastar txt -> {input_file}_repitch.txt" ) # todo: to reader with open(input_file, "r", encoding=FILE_ENCODING) as file: txt = file.readlines() i = 0 # todo: just add '_repitched' to input_file with open(output_repitched_ultrastar, "w", encoding=FILE_ENCODING) as file: for line in txt: if line.startswith(f"{UltrastarTxtNoteTypeTag.NORMAL} "): parts = re.findall(r"\S+|\s+", line) # between are whitespaces # [0] : # [2] start beat # [4] duration # [6] pitch # [8] word parts[6] = str(note_numbers[i]) delimiter = "" file.write(delimiter.join(parts)) i += 1 else: file.write(line) def add_score_to_ultrastar_txt(ultrastar_file_output: str, score: Score) -> None: """Adds the score to the ultrastar txt file""" with open(ultrastar_file_output, "r", encoding=FILE_ENCODING) as file: text = file.read() text = text.split("\n") for i, line in enumerate(text): if line.startswith(f"#{UltrastarTxtTag.COMMENT}:"): text[ i ] = f"{line} | Score: total: {score.score}, notes: {score.notes} line: {score.line_bonus}, golden: {score.golden}" break if line.startswith(( f"{UltrastarTxtNoteTypeTag.FREESTYLE} ", f"{UltrastarTxtNoteTypeTag.NORMAL} ", f"{UltrastarTxtNoteTypeTag.GOLDEN} ", f"{UltrastarTxtNoteTypeTag.RAP} ", f"{UltrastarTxtNoteTypeTag.RAP_GOLDEN} ")): text.insert( i, f"#{UltrastarTxtTag.COMMENT}: UltraSinger [GitHub] | Score: total: {score.score}, notes: {score.notes} line: {score.line_bonus}, golden: {score.golden}", ) break text = "\n".join(text) with open(ultrastar_file_output, "w", encoding=FILE_ENCODING) as file: file.write(text) class UltraStarWriter: """Docstring"""