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"""
|