Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files
src/modules/Ultrastar/ultrastar_parser.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ultrastar txt parser"""
|
| 2 |
+
|
| 3 |
+
from modules.console_colors import ULTRASINGER_HEAD
|
| 4 |
+
from modules.Ultrastar.ultrastar_converter import (
|
| 5 |
+
get_end_time_from_ultrastar,
|
| 6 |
+
get_start_time_from_ultrastar,
|
| 7 |
+
)
|
| 8 |
+
from modules.Ultrastar.ultrastar_txt import UltrastarTxtValue, UltrastarTxtTag, UltrastarTxtNoteTypeTag, FILE_ENCODING
|
| 9 |
+
|
| 10 |
+
def parse_ultrastar_txt(input_file: str) -> UltrastarTxtValue:
|
| 11 |
+
"""Parse ultrastar txt file to UltrastarTxt class"""
|
| 12 |
+
print(f"{ULTRASINGER_HEAD} Parse ultrastar txt -> {input_file}")
|
| 13 |
+
|
| 14 |
+
with open(input_file, "r", encoding=FILE_ENCODING) as file:
|
| 15 |
+
txt = file.readlines()
|
| 16 |
+
|
| 17 |
+
ultrastar_class = UltrastarTxtValue()
|
| 18 |
+
count = 0
|
| 19 |
+
|
| 20 |
+
# Strips the newline character
|
| 21 |
+
for line in txt:
|
| 22 |
+
count += 1
|
| 23 |
+
if line.startswith("#"):
|
| 24 |
+
if line.startswith(f"#{UltrastarTxtTag.ARTIST}"):
|
| 25 |
+
ultrastar_class.artist = line.split(":")[1].replace("\n", "")
|
| 26 |
+
elif line.startswith(f"#{UltrastarTxtTag.TITLE}"):
|
| 27 |
+
ultrastar_class.title = line.split(":")[1].replace("\n", "")
|
| 28 |
+
elif line.startswith(f"#{UltrastarTxtTag.MP3}"):
|
| 29 |
+
ultrastar_class.mp3 = line.split(":")[1].replace("\n", "")
|
| 30 |
+
elif line.startswith(f"#{UltrastarTxtTag.AUDIO}"):
|
| 31 |
+
ultrastar_class.audio = line.split(":")[1].replace("\n", "")
|
| 32 |
+
elif line.startswith(f"#{UltrastarTxtTag.VIDEO}"):
|
| 33 |
+
ultrastar_class.video = line.split(":")[1].replace("\n", "")
|
| 34 |
+
elif line.startswith(f"#{UltrastarTxtTag.GAP}"):
|
| 35 |
+
ultrastar_class.gap = line.split(":")[1].replace("\n", "")
|
| 36 |
+
elif line.startswith(f"#{UltrastarTxtTag.BPM}"):
|
| 37 |
+
ultrastar_class.bpm = line.split(":")[1].replace("\n", "")
|
| 38 |
+
elif line.startswith((
|
| 39 |
+
f"{UltrastarTxtNoteTypeTag.FREESTYLE} ",
|
| 40 |
+
f"{UltrastarTxtNoteTypeTag.NORMAL} ",
|
| 41 |
+
f"{UltrastarTxtNoteTypeTag.GOLDEN} ",
|
| 42 |
+
f"{UltrastarTxtNoteTypeTag.RAP} ",
|
| 43 |
+
f"{UltrastarTxtNoteTypeTag.RAP_GOLDEN} ")):
|
| 44 |
+
parts = line.split()
|
| 45 |
+
# [0] F : * R G
|
| 46 |
+
# [1] start beat
|
| 47 |
+
# [2] duration
|
| 48 |
+
# [3] pitch
|
| 49 |
+
# [4] word
|
| 50 |
+
|
| 51 |
+
ultrastar_class.noteType.append(parts[0])
|
| 52 |
+
ultrastar_class.startBeat.append(parts[1])
|
| 53 |
+
ultrastar_class.durations.append(parts[2])
|
| 54 |
+
ultrastar_class.pitches.append(parts[3])
|
| 55 |
+
ultrastar_class.words.append(parts[4] if len(parts) > 4 else "")
|
| 56 |
+
|
| 57 |
+
# do always as last
|
| 58 |
+
pos = len(ultrastar_class.startBeat) - 1
|
| 59 |
+
ultrastar_class.startTimes.append(
|
| 60 |
+
get_start_time_from_ultrastar(ultrastar_class, pos)
|
| 61 |
+
)
|
| 62 |
+
ultrastar_class.endTimes.append(
|
| 63 |
+
get_end_time_from_ultrastar(ultrastar_class, pos)
|
| 64 |
+
)
|
| 65 |
+
# todo: Progress?
|
| 66 |
+
|
| 67 |
+
return ultrastar_class
|
src/modules/Ultrastar/ultrastar_score_calculator.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ultrastar score calculator."""
|
| 2 |
+
|
| 3 |
+
import librosa
|
| 4 |
+
|
| 5 |
+
from modules.console_colors import (
|
| 6 |
+
ULTRASINGER_HEAD,
|
| 7 |
+
blue_highlighted,
|
| 8 |
+
cyan_highlighted,
|
| 9 |
+
gold_highlighted,
|
| 10 |
+
light_blue_highlighted,
|
| 11 |
+
underlined,
|
| 12 |
+
)
|
| 13 |
+
from modules.Midi.midi_creator import create_midi_note_from_pitched_data
|
| 14 |
+
from modules.Ultrastar.ultrastar_converter import (
|
| 15 |
+
get_end_time_from_ultrastar,
|
| 16 |
+
get_start_time_from_ultrastar,
|
| 17 |
+
ultrastar_note_to_midi_note,
|
| 18 |
+
)
|
| 19 |
+
from modules.Ultrastar.ultrastar_txt import UltrastarTxtValue
|
| 20 |
+
from modules.Pitcher.pitched_data import PitchedData
|
| 21 |
+
|
| 22 |
+
MAX_SONG_SCORE = 10000
|
| 23 |
+
MAX_SONG_LINE_BONUS = 1000
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class Points:
|
| 27 |
+
"""Docstring"""
|
| 28 |
+
|
| 29 |
+
notes = 0
|
| 30 |
+
golden_notes = 0
|
| 31 |
+
rap = 0
|
| 32 |
+
golden_rap = 0
|
| 33 |
+
line_bonus = 0
|
| 34 |
+
parts = 0
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def add_point(note_type: str, points: Points) -> Points:
|
| 38 |
+
"""Add calculated points to the points object."""
|
| 39 |
+
|
| 40 |
+
if note_type == ":":
|
| 41 |
+
points.notes += 1
|
| 42 |
+
elif note_type == "*":
|
| 43 |
+
points.golden_notes += 2
|
| 44 |
+
elif note_type == "R":
|
| 45 |
+
points.rap += 1
|
| 46 |
+
elif note_type == "G":
|
| 47 |
+
points.golden_rap += 2
|
| 48 |
+
return points
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class Score:
|
| 52 |
+
"""Docstring"""
|
| 53 |
+
|
| 54 |
+
max_score = 0
|
| 55 |
+
notes = 0
|
| 56 |
+
golden = 0
|
| 57 |
+
line_bonus = 0
|
| 58 |
+
score = 0
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def get_score(points: Points) -> Score:
|
| 62 |
+
"""Score calculation."""
|
| 63 |
+
|
| 64 |
+
score = Score()
|
| 65 |
+
score.max_score = (
|
| 66 |
+
MAX_SONG_SCORE
|
| 67 |
+
if points.line_bonus == 0
|
| 68 |
+
else MAX_SONG_SCORE - MAX_SONG_LINE_BONUS
|
| 69 |
+
)
|
| 70 |
+
score.notes = round(
|
| 71 |
+
score.max_score * (points.notes + points.rap) / points.parts
|
| 72 |
+
)
|
| 73 |
+
score.golden = round(points.golden_notes + points.golden_rap)
|
| 74 |
+
score.score = round(score.notes + points.line_bonus + score.golden)
|
| 75 |
+
score.line_bonus = round(points.line_bonus)
|
| 76 |
+
return score
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def print_score(score: Score) -> None:
|
| 80 |
+
"""Print score."""
|
| 81 |
+
|
| 82 |
+
print(
|
| 83 |
+
f"{ULTRASINGER_HEAD} Total: {cyan_highlighted(str(score.score))}, notes: {blue_highlighted(str(score.notes))}, line bonus: {light_blue_highlighted(str(score.line_bonus))}, golden notes: {gold_highlighted(str(score.golden))}"
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def calculate_score(pitched_data: PitchedData, ultrastar_class: UltrastarTxtValue) -> (Score, Score):
|
| 88 |
+
"""Calculate score."""
|
| 89 |
+
|
| 90 |
+
print(ULTRASINGER_HEAD + " Calculating Ultrastar Points")
|
| 91 |
+
|
| 92 |
+
simple_points = Points()
|
| 93 |
+
accurate_points = Points()
|
| 94 |
+
|
| 95 |
+
reachable_line_bonus_per_word = MAX_SONG_LINE_BONUS / len(
|
| 96 |
+
ultrastar_class.words
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
for i in enumerate(ultrastar_class.words):
|
| 100 |
+
pos = i[0]
|
| 101 |
+
if ultrastar_class.words == "":
|
| 102 |
+
continue
|
| 103 |
+
|
| 104 |
+
if ultrastar_class.noteType[pos] == "F":
|
| 105 |
+
continue
|
| 106 |
+
|
| 107 |
+
start_time = get_start_time_from_ultrastar(ultrastar_class, pos)
|
| 108 |
+
end_time = get_end_time_from_ultrastar(ultrastar_class, pos)
|
| 109 |
+
duration = end_time - start_time
|
| 110 |
+
step_size = 0.09 # Todo: Whats is the step size of the game? Its not 1/bps -> one beat in seconds s = 60/bpm
|
| 111 |
+
parts = int(duration / step_size)
|
| 112 |
+
parts = 1 if parts == 0 else parts
|
| 113 |
+
|
| 114 |
+
accurate_part_line_bonus_points = 0
|
| 115 |
+
simple_part_line_bonus_points = 0
|
| 116 |
+
|
| 117 |
+
ultrastar_midi_note = ultrastar_note_to_midi_note(
|
| 118 |
+
int(ultrastar_class.pitches[pos])
|
| 119 |
+
)
|
| 120 |
+
ultrastar_note = librosa.midi_to_note(ultrastar_midi_note)
|
| 121 |
+
|
| 122 |
+
for part in range(parts):
|
| 123 |
+
start = start_time + step_size * part
|
| 124 |
+
end = start + step_size
|
| 125 |
+
if end_time < end or part == parts - 1:
|
| 126 |
+
end = end_time
|
| 127 |
+
pitch_note = create_midi_note_from_pitched_data(
|
| 128 |
+
start, end, pitched_data
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
if pitch_note[:-1] == ultrastar_note[:-1]:
|
| 132 |
+
# Ignore octave high
|
| 133 |
+
simple_points = add_point(
|
| 134 |
+
ultrastar_class.noteType[pos], simple_points
|
| 135 |
+
)
|
| 136 |
+
simple_part_line_bonus_points += 1
|
| 137 |
+
|
| 138 |
+
if pitch_note == ultrastar_note:
|
| 139 |
+
# Octave high must be the same
|
| 140 |
+
accurate_points = add_point(
|
| 141 |
+
ultrastar_class.noteType[pos], accurate_points
|
| 142 |
+
)
|
| 143 |
+
accurate_part_line_bonus_points += 1
|
| 144 |
+
|
| 145 |
+
accurate_points.parts += 1
|
| 146 |
+
simple_points.parts += 1
|
| 147 |
+
|
| 148 |
+
if accurate_part_line_bonus_points >= parts:
|
| 149 |
+
accurate_points.line_bonus += reachable_line_bonus_per_word
|
| 150 |
+
|
| 151 |
+
if simple_part_line_bonus_points >= parts:
|
| 152 |
+
simple_points.line_bonus += reachable_line_bonus_per_word
|
| 153 |
+
|
| 154 |
+
return get_score(simple_points), get_score(accurate_points)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def print_score_calculation(simple_points: Score, accurate_points: Score) -> None:
|
| 158 |
+
"""Print score calculation."""
|
| 159 |
+
|
| 160 |
+
print(
|
| 161 |
+
f"{ULTRASINGER_HEAD} {underlined('Simple (octave high ignored)')} points"
|
| 162 |
+
)
|
| 163 |
+
print_score(simple_points)
|
| 164 |
+
|
| 165 |
+
print(
|
| 166 |
+
f"{ULTRASINGER_HEAD} {underlined('Accurate (octave high matches)')} points:"
|
| 167 |
+
)
|
| 168 |
+
print_score(accurate_points)
|
src/modules/Ultrastar/ultrastar_txt.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ultrastar TXT"""
|
| 2 |
+
|
| 3 |
+
from enum import Enum
|
| 4 |
+
|
| 5 |
+
FILE_ENCODING = "utf-8"
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class UltrastarTxtTag(str, Enum):
|
| 9 |
+
"""Tags for Ultrastar TXT files."""
|
| 10 |
+
|
| 11 |
+
# 0.2.0
|
| 12 |
+
VERSION = 'VERSION' # Version of the file format: See https://usdx.eu/format/
|
| 13 |
+
ARTIST = 'ARTIST'
|
| 14 |
+
TITLE = 'TITLE'
|
| 15 |
+
MP3 = 'MP3' # Removed in v2.0.0
|
| 16 |
+
GAP = 'GAP'
|
| 17 |
+
BPM = 'BPM'
|
| 18 |
+
LANGUAGE = 'LANGUAGE' # Multi-language support since v1.1.0
|
| 19 |
+
GENRE = 'GENRE' # Multi-language support since v1.1.0
|
| 20 |
+
YEAR = 'YEAR' # Multi-language support since v1.1.0
|
| 21 |
+
COVER = 'COVER' # Path to cover. Should end with `*[CO].jpg`
|
| 22 |
+
CREATOR = 'CREATOR' # Multi-language support since v1.1.0
|
| 23 |
+
COMMENT = 'COMMENT'
|
| 24 |
+
VIDEO = 'VIDEO'
|
| 25 |
+
FILE_END = 'E'
|
| 26 |
+
LINEBREAK = '-'
|
| 27 |
+
|
| 28 |
+
# 1.1.0
|
| 29 |
+
AUDIO = 'AUDIO' # Its instead of MP3. Just renamed
|
| 30 |
+
VOCALS = 'VOCALS' # Vocals only audio
|
| 31 |
+
INSTRUMENTAL = 'INSTRUMENTAL' # Instrumental only audio
|
| 32 |
+
TAGS = 'TAGS' # Tags for the song. Can be used for filtering
|
| 33 |
+
|
| 34 |
+
# Unused 0.2.0
|
| 35 |
+
BACKGROUND = 'BACKGROUND' # Path to background. Is shown when there is no video. Should end with `*[BG].jpg`
|
| 36 |
+
VIDEOGAP = 'VIDEOGAP'
|
| 37 |
+
EDITION = 'EDITION' # Multi-language support since v1.1.0
|
| 38 |
+
START = 'START'
|
| 39 |
+
END = 'END'
|
| 40 |
+
PREVIEWSTART = 'PREVIEWSTART'
|
| 41 |
+
MEDLEYSTARTBEAT = 'MEDLEYSTARTBEAT' # Removed in 2.0.0
|
| 42 |
+
MEDLEYENDBEAT = 'MEDLEYENDBEAT' # Removed in v2.0.0
|
| 43 |
+
CALCMEDLEY = 'CALCMEDLEY'
|
| 44 |
+
P1 = 'P1' # Only for UltraStar Deluxe
|
| 45 |
+
P2 = 'P2' # Only for UltraStar Deluxe
|
| 46 |
+
DUETSINGERP1 = 'DUETSINGERP1' # Removed in 1.0.0 (Used by UltraStar WorldParty)
|
| 47 |
+
DUETSINGERP2 = 'DUETSINGERP2' # Removed in 1.0.0 (Used by UltraStar WorldParty)
|
| 48 |
+
RESOLUTION = 'RESOLUTION' # Changes the grid resolution of the editor. Only for the editor and nothing for singing. # Removed in 1.0.0
|
| 49 |
+
NOTESGAP = 'NOTESGAP' # Removed in 1.0.0
|
| 50 |
+
RELATIVE = 'RELATIVE' # Removed in 1.0.0
|
| 51 |
+
ENCODING = 'ENCODING' # Removed in 1.0.0
|
| 52 |
+
|
| 53 |
+
# (Unused) 1.1.0
|
| 54 |
+
PROVIDEDBY = 'PROVIDEDBY' # Should the URL from hoster server
|
| 55 |
+
|
| 56 |
+
# (Unused) New in (unreleased) 1.2.0
|
| 57 |
+
AUDIOURL = 'AUDIOURL' # URL to the audio file
|
| 58 |
+
COVERURL = 'COVERURL' # URL to the cover file
|
| 59 |
+
BACKGROUNDURL = 'BACKGROUNDURL' # URL to the background file
|
| 60 |
+
VIDEOURL = 'VIDEOURL' # URL to the video file
|
| 61 |
+
|
| 62 |
+
# (Unused) New in (unreleased) 2.0.0
|
| 63 |
+
MEDLEYSTART = 'MEDLEYSTART' # Rename of MEDLEYSTARTBEAT
|
| 64 |
+
MEDLEYEND = 'MEDLEYEND' # Renmame of MEDLEYENDBEAT
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class UltrastarTxtNoteTypeTag(str, Enum):
|
| 68 |
+
"""Note types for Ultrastar TXT files."""
|
| 69 |
+
NORMAL = ':'
|
| 70 |
+
RAP = 'R'
|
| 71 |
+
RAP_GOLDEN = 'G'
|
| 72 |
+
FREESTYLE = 'F'
|
| 73 |
+
GOLDEN = '*'
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class UltrastarTxtValue:
|
| 77 |
+
"""Vaules for Ultrastar TXT files."""
|
| 78 |
+
|
| 79 |
+
version = "1.0.0"
|
| 80 |
+
artist = ""
|
| 81 |
+
title = ""
|
| 82 |
+
year = None
|
| 83 |
+
genre = ""
|
| 84 |
+
mp3 = ""
|
| 85 |
+
audio = ""
|
| 86 |
+
video = None
|
| 87 |
+
gap = ""
|
| 88 |
+
bpm = ""
|
| 89 |
+
language = None
|
| 90 |
+
cover = None
|
| 91 |
+
vocals = None
|
| 92 |
+
instrumental = None
|
| 93 |
+
tags = None
|
| 94 |
+
creator = "UltraSinger [GitHub]"
|
| 95 |
+
comment = "UltraSinger [GitHub]"
|
| 96 |
+
startBeat = []
|
| 97 |
+
startTimes = []
|
| 98 |
+
endTimes = []
|
| 99 |
+
durations = []
|
| 100 |
+
pitches = []
|
| 101 |
+
words = []
|
| 102 |
+
noteType = [] # F, R, G, *, :
|
src/modules/Ultrastar/ultrastar_writer.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ultrastar writer module"""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
import langcodes
|
| 5 |
+
from packaging import version
|
| 6 |
+
|
| 7 |
+
from modules.console_colors import ULTRASINGER_HEAD
|
| 8 |
+
from modules.Ultrastar.ultrastar_converter import (
|
| 9 |
+
real_bpm_to_ultrastar_bpm,
|
| 10 |
+
second_to_beat,
|
| 11 |
+
beat_to_second,
|
| 12 |
+
)
|
| 13 |
+
from modules.Ultrastar.ultrastar_txt import UltrastarTxtValue, UltrastarTxtTag, UltrastarTxtNoteTypeTag, \
|
| 14 |
+
FILE_ENCODING
|
| 15 |
+
from modules.Speech_Recognition.TranscribedData import TranscribedData
|
| 16 |
+
from modules.Ultrastar.ultrastar_score_calculator import Score
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_thirtytwo_note_second(real_bpm: float):
|
| 20 |
+
"""Converts a beat to a 1/32 note in second"""
|
| 21 |
+
return 60 / real_bpm / 8
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def get_sixteenth_note_second(real_bpm: float):
|
| 25 |
+
"""Converts a beat to a 1/16 note in second"""
|
| 26 |
+
return 60 / real_bpm / 4
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def get_eighth_note_second(real_bpm: float):
|
| 30 |
+
"""Converts a beat to a 1/8 note in second"""
|
| 31 |
+
return 60 / real_bpm / 2
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def get_quarter_note_second(real_bpm: float):
|
| 35 |
+
"""Converts a beat to a 1/4 note in second"""
|
| 36 |
+
return 60 / real_bpm
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def get_half_note_second(real_bpm: float):
|
| 40 |
+
"""Converts a beat to a 1/2 note in second"""
|
| 41 |
+
return 60 / real_bpm * 2
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def get_whole_note_second(real_bpm: float):
|
| 45 |
+
"""Converts a beat to a 1/1 note in second"""
|
| 46 |
+
return 60 / real_bpm * 4
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def get_multiplier(real_bpm: float) -> int:
|
| 50 |
+
"""Calculates the multiplier for the BPM"""
|
| 51 |
+
|
| 52 |
+
if real_bpm == 0:
|
| 53 |
+
raise Exception("BPM is 0")
|
| 54 |
+
|
| 55 |
+
multiplier = 1
|
| 56 |
+
result = 0
|
| 57 |
+
while result < 400:
|
| 58 |
+
result = real_bpm * multiplier
|
| 59 |
+
multiplier += 1
|
| 60 |
+
return multiplier - 2
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def get_language_name(language: str) -> str:
|
| 64 |
+
"""Creates the language name from the language code"""
|
| 65 |
+
|
| 66 |
+
return langcodes.Language.make(language=language).display_name()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def create_ultrastar_txt_from_automation(
|
| 70 |
+
transcribed_data: list[TranscribedData],
|
| 71 |
+
note_numbers: list[int],
|
| 72 |
+
ultrastar_file_output: str,
|
| 73 |
+
ultrastar_class: UltrastarTxtValue,
|
| 74 |
+
real_bpm=120,
|
| 75 |
+
) -> None:
|
| 76 |
+
"""Creates an Ultrastar txt file from the automation data"""
|
| 77 |
+
|
| 78 |
+
print(
|
| 79 |
+
f"{ULTRASINGER_HEAD} Creating {ultrastar_file_output} from transcription."
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
ultrastar_bpm = real_bpm_to_ultrastar_bpm(real_bpm)
|
| 83 |
+
multiplication = get_multiplier(ultrastar_bpm)
|
| 84 |
+
ultrastar_bpm = ultrastar_bpm * get_multiplier(ultrastar_bpm)
|
| 85 |
+
silence_split_duration = calculate_silent_beat_length(transcribed_data)
|
| 86 |
+
|
| 87 |
+
with open(ultrastar_file_output, "w", encoding=FILE_ENCODING) as file:
|
| 88 |
+
gap = transcribed_data[0].start
|
| 89 |
+
|
| 90 |
+
if version.parse(ultrastar_class.version) >= version.parse("1.0.0"):
|
| 91 |
+
file.write(f"#{UltrastarTxtTag.VERSION}:{ultrastar_class.version}\n"),
|
| 92 |
+
file.write(f"#{UltrastarTxtTag.ARTIST}:{ultrastar_class.artist}\n")
|
| 93 |
+
file.write(f"#{UltrastarTxtTag.TITLE}:{ultrastar_class.title}\n")
|
| 94 |
+
if ultrastar_class.year is not None:
|
| 95 |
+
file.write(f"#{UltrastarTxtTag.YEAR}:{ultrastar_class.year}\n")
|
| 96 |
+
if ultrastar_class.language is not None:
|
| 97 |
+
file.write(f"#{UltrastarTxtTag.LANGUAGE}:{get_language_name(ultrastar_class.language)}\n")
|
| 98 |
+
if ultrastar_class.genre:
|
| 99 |
+
file.write(f"#{UltrastarTxtTag.GENRE}:{ultrastar_class.genre}\n")
|
| 100 |
+
if ultrastar_class.cover is not None:
|
| 101 |
+
file.write(f"#{UltrastarTxtTag.COVER}:{ultrastar_class.cover}\n")
|
| 102 |
+
file.write(f"#{UltrastarTxtTag.MP3}:{ultrastar_class.mp3}\n")
|
| 103 |
+
if version.parse(ultrastar_class.version) >= version.parse("1.1.0"):
|
| 104 |
+
file.write(f"#{UltrastarTxtTag.AUDIO}:{ultrastar_class.audio}\n")
|
| 105 |
+
if ultrastar_class.vocals is not None:
|
| 106 |
+
file.write(f"#{UltrastarTxtTag.VOCALS}:{ultrastar_class.vocals}\n")
|
| 107 |
+
if ultrastar_class.instrumental is not None:
|
| 108 |
+
file.write(f"#{UltrastarTxtTag.INSTRUMENTAL}:{ultrastar_class.instrumental}\n")
|
| 109 |
+
if ultrastar_class.tags is not None:
|
| 110 |
+
file.write(f"#{UltrastarTxtTag.TAGS}:{ultrastar_class.tags}\n")
|
| 111 |
+
file.write(f"#{UltrastarTxtTag.VIDEO}:{ultrastar_class.video}\n")
|
| 112 |
+
file.write(f"#{UltrastarTxtTag.BPM}:{round(ultrastar_bpm, 2)}\n") # not the real BPM!
|
| 113 |
+
file.write(f"#{UltrastarTxtTag.GAP}:{int(gap * 1000)}\n")
|
| 114 |
+
file.write(f"#{UltrastarTxtTag.CREATOR}:{ultrastar_class.creator}\n")
|
| 115 |
+
file.write(f"#{UltrastarTxtTag.COMMENT}:{ultrastar_class.comment}\n")
|
| 116 |
+
|
| 117 |
+
# Write the singing part
|
| 118 |
+
previous_end_beat = 0
|
| 119 |
+
separated_word_silence = [] # This is a workaround for separated words that get his ends to far away
|
| 120 |
+
|
| 121 |
+
for i, data in enumerate(transcribed_data):
|
| 122 |
+
start_time = (data.start - gap) * multiplication
|
| 123 |
+
end_time = (
|
| 124 |
+
data.end - data.start
|
| 125 |
+
) * multiplication
|
| 126 |
+
start_beat = round(second_to_beat(start_time, real_bpm))
|
| 127 |
+
duration = round(second_to_beat(end_time, real_bpm))
|
| 128 |
+
|
| 129 |
+
# Fix the round issue, so the beats don’t overlap
|
| 130 |
+
start_beat = max(start_beat, previous_end_beat)
|
| 131 |
+
previous_end_beat = start_beat + duration
|
| 132 |
+
|
| 133 |
+
# Calculate the silence between the words
|
| 134 |
+
if i < len(transcribed_data) - 1:
|
| 135 |
+
silence = (transcribed_data[i + 1].start - data.end)
|
| 136 |
+
else:
|
| 137 |
+
silence = 0
|
| 138 |
+
|
| 139 |
+
# : 10 10 10 w
|
| 140 |
+
# ':' start midi part
|
| 141 |
+
# 'n1' start at real beat
|
| 142 |
+
# 'n2' duration at real beat
|
| 143 |
+
# 'n3' pitch where 0 == C4
|
| 144 |
+
# 'w' lyric
|
| 145 |
+
line = f"{UltrastarTxtNoteTypeTag.NORMAL} " \
|
| 146 |
+
f"{str(start_beat)} " \
|
| 147 |
+
f"{str(duration)} " \
|
| 148 |
+
f"{str(note_numbers[i])} " \
|
| 149 |
+
f"{data.word}\n"
|
| 150 |
+
|
| 151 |
+
file.write(line)
|
| 152 |
+
|
| 153 |
+
# detect silence between words
|
| 154 |
+
if not transcribed_data[i].is_word_end:
|
| 155 |
+
separated_word_silence.append(silence)
|
| 156 |
+
continue
|
| 157 |
+
|
| 158 |
+
if silence_split_duration != 0 and silence > silence_split_duration or any(
|
| 159 |
+
s > silence_split_duration for s in separated_word_silence) and i != len(transcribed_data) - 1:
|
| 160 |
+
# - 10
|
| 161 |
+
# '-' end of current sing part
|
| 162 |
+
# 'n1' show next at time in real beat
|
| 163 |
+
show_next = (
|
| 164 |
+
second_to_beat(data.end - gap, real_bpm)
|
| 165 |
+
* multiplication
|
| 166 |
+
)
|
| 167 |
+
linebreak = f"{UltrastarTxtTag.LINEBREAK} " \
|
| 168 |
+
f"{str(round(show_next))}\n"
|
| 169 |
+
file.write(linebreak)
|
| 170 |
+
separated_word_silence = []
|
| 171 |
+
file.write(f"{UltrastarTxtTag.FILE_END}")
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def deviation(silence_parts):
|
| 175 |
+
"""Calculates the deviation of the silence parts"""
|
| 176 |
+
|
| 177 |
+
if len(silence_parts) < 5:
|
| 178 |
+
return 0
|
| 179 |
+
|
| 180 |
+
# Remove the longest part so the deviation is not that high
|
| 181 |
+
sorted_parts = sorted(silence_parts)
|
| 182 |
+
filtered_parts = [part for part in sorted_parts if part < sorted_parts[-1]]
|
| 183 |
+
|
| 184 |
+
sum_parts = sum(filtered_parts)
|
| 185 |
+
if sum_parts == 0:
|
| 186 |
+
return 0
|
| 187 |
+
|
| 188 |
+
mean = sum_parts / len(filtered_parts)
|
| 189 |
+
return mean
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def calculate_silent_beat_length(transcribed_data: list[TranscribedData]):
|
| 193 |
+
print(f"{ULTRASINGER_HEAD} Calculating silence parts for linebreaks.")
|
| 194 |
+
|
| 195 |
+
silent_parts = []
|
| 196 |
+
for i, data in enumerate(transcribed_data):
|
| 197 |
+
if i < len(transcribed_data) - 1:
|
| 198 |
+
silent_parts.append(transcribed_data[i + 1].start - data.end)
|
| 199 |
+
|
| 200 |
+
return deviation(silent_parts)
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def create_repitched_txt_from_ultrastar_data(
|
| 204 |
+
input_file: str, note_numbers: list[int], output_repitched_ultrastar: str
|
| 205 |
+
) -> None:
|
| 206 |
+
"""Creates a repitched ultrastar txt file from the original one"""
|
| 207 |
+
# todo: just add '_repitched' to input_file
|
| 208 |
+
print(
|
| 209 |
+
"{PRINT_ULTRASTAR} Creating repitched ultrastar txt -> {input_file}_repitch.txt"
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
# todo: to reader
|
| 213 |
+
with open(input_file, "r", encoding=FILE_ENCODING) as file:
|
| 214 |
+
txt = file.readlines()
|
| 215 |
+
|
| 216 |
+
i = 0
|
| 217 |
+
# todo: just add '_repitched' to input_file
|
| 218 |
+
with open(output_repitched_ultrastar, "w", encoding=FILE_ENCODING) as file:
|
| 219 |
+
for line in txt:
|
| 220 |
+
if line.startswith(f"{UltrastarTxtNoteTypeTag.NORMAL} "):
|
| 221 |
+
parts = re.findall(r"\S+|\s+", line)
|
| 222 |
+
# between are whitespaces
|
| 223 |
+
# [0] :
|
| 224 |
+
# [2] start beat
|
| 225 |
+
# [4] duration
|
| 226 |
+
# [6] pitch
|
| 227 |
+
# [8] word
|
| 228 |
+
parts[6] = str(note_numbers[i])
|
| 229 |
+
delimiter = ""
|
| 230 |
+
file.write(delimiter.join(parts))
|
| 231 |
+
i += 1
|
| 232 |
+
else:
|
| 233 |
+
file.write(line)
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def add_score_to_ultrastar_txt(ultrastar_file_output: str, score: Score) -> None:
|
| 237 |
+
"""Adds the score to the ultrastar txt file"""
|
| 238 |
+
with open(ultrastar_file_output, "r", encoding=FILE_ENCODING) as file:
|
| 239 |
+
text = file.read()
|
| 240 |
+
text = text.split("\n")
|
| 241 |
+
|
| 242 |
+
for i, line in enumerate(text):
|
| 243 |
+
if line.startswith(f"#{UltrastarTxtTag.COMMENT}:"):
|
| 244 |
+
text[
|
| 245 |
+
i
|
| 246 |
+
] = f"{line} | Score: total: {score.score}, notes: {score.notes} line: {score.line_bonus}, golden: {score.golden}"
|
| 247 |
+
break
|
| 248 |
+
|
| 249 |
+
if line.startswith((
|
| 250 |
+
f"{UltrastarTxtNoteTypeTag.FREESTYLE} ",
|
| 251 |
+
f"{UltrastarTxtNoteTypeTag.NORMAL} ",
|
| 252 |
+
f"{UltrastarTxtNoteTypeTag.GOLDEN} ",
|
| 253 |
+
f"{UltrastarTxtNoteTypeTag.RAP} ",
|
| 254 |
+
f"{UltrastarTxtNoteTypeTag.RAP_GOLDEN} ")):
|
| 255 |
+
text.insert(
|
| 256 |
+
i,
|
| 257 |
+
f"#{UltrastarTxtTag.COMMENT}: UltraSinger [GitHub] | Score: total: {score.score}, notes: {score.notes} line: {score.line_bonus}, golden: {score.golden}",
|
| 258 |
+
)
|
| 259 |
+
break
|
| 260 |
+
|
| 261 |
+
text = "\n".join(text)
|
| 262 |
+
|
| 263 |
+
with open(ultrastar_file_output, "w", encoding=FILE_ENCODING) as file:
|
| 264 |
+
file.write(text)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
class UltraStarWriter:
|
| 268 |
+
"""Docstring"""
|