1
0
mirror of synced 2025-03-03 08:35:48 +01:00

Format the code

This commit is contained in:
Stepland 2020-06-06 12:07:44 +02:00
parent de5f6d38f5
commit 9af51d54c4
7 changed files with 132 additions and 125 deletions

View File

@ -1,3 +1,3 @@
from .song import Song from .song import Song
__version__ = '0.1.0' __version__ = "0.1.0"

View File

@ -3,6 +3,7 @@ from typing import Optional
import click import click
@click.command() @click.command()
def convert(): def convert():
... ...

View File

@ -1,28 +1,33 @@
""" """
Base class for all file formats Module containing all the load/dump code for all file formats
""" """
from path import Path from path import Path
from typing import Any, Callable, Iterable, Tuple, IO from typing import Callable, Dict, IO
from jubeatools.song import Song from jubeatools.song import Song
from .memon import * from .memon import (
from ._filekind import FileKind dump_memon_legacy,
dump_memon_0_1_0,
dump_memon_0_2_0,
load_memon_legacy,
load_memon_0_1_0,
load_memon_0_2_0,
)
ALIASES = { ALIASES = {
"memon": "memon:v0.2.0", "memon": "memon:v0.2.0",
} }
# Loaders take in a folder containing the files to be converted # Loaders deserialize a folder or a file to a Song object
# and return a Song object LOADERS: Dict[str, Callable[[Path], Song]] = {
LOADERS: Mapping[str, Callable[[Path], Song]] = {
"memon:legacy": load_memon_legacy, "memon:legacy": load_memon_legacy,
"memon:v0.1.0": load_memon_0_1_0, "memon:v0.1.0": load_memon_0_1_0,
"memon:v0.2.0": load_memon_0_2_0 "memon:v0.2.0": load_memon_0_2_0,
} }
# Dumpers take in the song object and return a list of tuples # Dumpers serialize a Song object into a (filename -> file) mapping
DUMPERS: Mapping[str, Callable[[Song], Iterable[Tuple[Any, IO]]]] = { DUMPERS: Dict[str, Callable[[Song], Dict[str, IO]]] = {
"memon:legacy": dump_memon_legacy, "memon:legacy": dump_memon_legacy,
"memon:v0.1.0": dump_memon_0_1_0, "memon:v0.1.0": dump_memon_0_1_0,
"memon:v0.2.0": dump_memon_0_2_0 "memon:v0.2.0": dump_memon_0_2_0,
} }

View File

@ -14,7 +14,15 @@ from itertools import chain
from path import Path from path import Path
import simplejson as json import simplejson as json
from marshmallow import Schema, fields, RAISE, validate, validates_schema, ValidationError, post_load from marshmallow import (
Schema,
fields,
RAISE,
validate,
validates_schema,
ValidationError,
post_load,
)
from jubeatools.song import * from jubeatools.song import *
from jubeatools.utils import lcm from jubeatools.utils import lcm
@ -41,10 +49,10 @@ X_Y_OFFSET_TO_P_VALUE = {
(3, 0): 9, (3, 0): 9,
(-1, 0): 3, (-1, 0): 3,
(-2, 0): 7, (-2, 0): 7,
(-3, 0): 11 (-3, 0): 11,
} }
P_VALUE_TO_X_Y_OFFSET = { v: k for k, v in X_Y_OFFSET_TO_P_VALUE.items() } P_VALUE_TO_X_Y_OFFSET = {v: k for k, v in X_Y_OFFSET_TO_P_VALUE.items()}
class StrictSchema(Schema): class StrictSchema(Schema):
@ -64,7 +72,7 @@ class MemonNote(StrictSchema):
x = data["n"] % 4 x = data["n"] % 4
y = data["n"] // 4 y = data["n"] // 4
dx, dy = P_VALUE_TO_X_Y_OFFSET[data["p"]] dx, dy = P_VALUE_TO_X_Y_OFFSET[data["p"]]
if (not (0 <= x + dx < 4 and 0 <= y + dy < 4)): if not (0 <= x + dx < 4 and 0 <= y + dy < 4):
raise ValidationError("Invalid tail position : {data}") raise ValidationError("Invalid tail position : {data}")
@ -83,7 +91,9 @@ class MemonMetadata_legacy(StrictSchema):
artist = fields.String(required=True) artist = fields.String(required=True)
audio = fields.String(required=True, data_key="music path") audio = fields.String(required=True, data_key="music path")
cover = fields.String(required=True, data_key="jacket path") cover = fields.String(required=True, data_key="jacket path")
BPM = fields.Decimal(required=True, validate=validate.Range(min=0, min_inclusive=False)) BPM = fields.Decimal(
required=True, validate=validate.Range(min=0, min_inclusive=False)
)
offset = fields.Decimal(required=True) offset = fields.Decimal(required=True)
@ -93,7 +103,9 @@ class MemonMetadata_0_1_0(MemonMetadata_legacy):
class MemonPreview(StrictSchema): class MemonPreview(StrictSchema):
position = fields.Decimal(required=True, validate=validate.Range(min=0)) position = fields.Decimal(required=True, validate=validate.Range(min=0))
length = fields.Decimal(required=True, validate=validate.Range(min=0, min_inclusive=False)) length = fields.Decimal(
required=True, validate=validate.Range(min=0, min_inclusive=False)
)
class MemonMetadata_0_2_0(MemonMetadata_0_1_0): class MemonMetadata_0_2_0(MemonMetadata_0_1_0):
@ -138,7 +150,7 @@ def _search_and_load(file_or_folder: Path) -> Any:
def _load_memon_note_v0(note: dict, resolution: int) -> Union[TapNote, LongNote]: def _load_memon_note_v0(note: dict, resolution: int) -> Union[TapNote, LongNote]:
position = NotePosition.from_index(note["n"]) position = NotePosition.from_index(note["n"])
time = BeatsTime.from_ticks(ticks=note["t"], resolution=resolution) time = BeatsTime.from_ticks(ticks=note["t"], resolution=resolution)
if (note["l"] > 0): if note["l"] > 0:
duration = BeatsTime.from_ticks(ticks=note["l"], resolution=resolution) duration = BeatsTime.from_ticks(ticks=note["l"], resolution=resolution)
tail_tip = NotePosition(*P_VALUE_TO_X_Y_OFFSET[note["p"]]) tail_tip = NotePosition(*P_VALUE_TO_X_Y_OFFSET[note["p"]])
return LongNote(time, position, duration, tail_tip) return LongNote(time, position, duration, tail_tip)
@ -146,20 +158,16 @@ def _load_memon_note_v0(note: dict, resolution: int) -> Union[TapNote, LongNote]
return TapNote(time, position) return TapNote(time, position)
def load_memon_legacy(file_or_folder: Path) -> Song: def load_memon_legacy(file_or_folder: Path) -> Song:
raw_memon = _search_and_load(file_or_folder) raw_memon = _search_and_load(file_or_folder)
schema = Memon_legacy() schema = Memon_legacy()
memon = schema.load(raw_memon) memon = schema.load(raw_memon)
metadata = Metadata( metadata = Metadata(
**{ **{key: memon["metadata"][key] for key in ["title", "artist", "audio", "cover"]}
key: memon["metadata"][key]
for key in ["title", "artist", "audio", "cover"]
}
) )
global_timing = Timing( global_timing = Timing(
events=[BPMEvent(time=0, BPM=memon["metadata"]["BPM"])], events=[BPMEvent(time=0, BPM=memon["metadata"]["BPM"])],
beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]) beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]),
) )
charts: Mapping[str, Chart] = MultiDict() charts: Mapping[str, Chart] = MultiDict()
for memon_chart in memon["data"]: for memon_chart in memon["data"]:
@ -170,17 +178,11 @@ def load_memon_legacy(file_or_folder: Path) -> Song:
notes=[ notes=[
_load_memon_note_v0(note, memon_chart["resolution"]) _load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"] for note in memon_chart["notes"]
] ],
) ),
) )
return Song( return Song(metadata=metadata, charts=charts, global_timing=global_timing)
metadata=metadata,
charts=charts,
global_timing=global_timing
)
def load_memon_0_1_0(file_or_folder: Path) -> Song: def load_memon_0_1_0(file_or_folder: Path) -> Song:
@ -188,14 +190,11 @@ def load_memon_0_1_0(file_or_folder: Path) -> Song:
schema = Memon_0_1_0() schema = Memon_0_1_0()
memon = schema.load(raw_memon) memon = schema.load(raw_memon)
metadata = Metadata( metadata = Metadata(
**{ **{key: memon["metadata"][key] for key in ["title", "artist", "audio", "cover"]}
key: memon["metadata"][key]
for key in ["title", "artist", "audio", "cover"]
}
) )
global_timing = Timing( global_timing = Timing(
events=[BPMEvent(time=0, BPM=memon["metadata"]["BPM"])], events=[BPMEvent(time=0, BPM=memon["metadata"]["BPM"])],
beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]) beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]),
) )
charts: Mapping[str, Chart] = MultiDict() charts: Mapping[str, Chart] = MultiDict()
for difficulty, memon_chart in memon["data"]: for difficulty, memon_chart in memon["data"]:
@ -206,15 +205,11 @@ def load_memon_0_1_0(file_or_folder: Path) -> Song:
notes=[ notes=[
_load_memon_note_v0(note, memon_chart["resolution"]) _load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"] for note in memon_chart["notes"]
] ],
) ),
) )
return Song( return Song(metadata=metadata, charts=charts, global_timing=global_timing)
metadata=metadata,
charts=charts,
global_timing=global_timing
)
def load_memon_0_2_0(file_or_folder: Path) -> Song: def load_memon_0_2_0(file_or_folder: Path) -> Song:
@ -222,8 +217,7 @@ def load_memon_0_2_0(file_or_folder: Path) -> Song:
schema = Memon_0_2_0() schema = Memon_0_2_0()
memon = schema.load(raw_memon) memon = schema.load(raw_memon)
metadata_dict = { metadata_dict = {
key: memon["metadata"][key] key: memon["metadata"][key] for key in ["title", "artist", "audio", "cover"]
for key in ["title", "artist", "audio", "cover"]
} }
if "preview" in memon["metadata"]: if "preview" in memon["metadata"]:
metadata_dict["preview_start"] = memon["metadata"]["preview"]["position"] metadata_dict["preview_start"] = memon["metadata"]["preview"]["position"]
@ -232,7 +226,7 @@ def load_memon_0_2_0(file_or_folder: Path) -> Song:
metadata = Metadata(**metadata_dict) metadata = Metadata(**metadata_dict)
global_timing = Timing( global_timing = Timing(
events=[BPMEvent(time=0, BPM=memon["metadata"]["BPM"])], events=[BPMEvent(time=0, BPM=memon["metadata"]["BPM"])],
beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]) beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]),
) )
charts: Mapping[str, Chart] = MultiDict() charts: Mapping[str, Chart] = MultiDict()
for difficulty, memon_chart in memon["data"]: for difficulty, memon_chart in memon["data"]:
@ -243,15 +237,11 @@ def load_memon_0_2_0(file_or_folder: Path) -> Song:
notes=[ notes=[
_load_memon_note_v0(note, memon_chart["resolution"]) _load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"] for note in memon_chart["notes"]
] ],
) ),
) )
return Song( return Song(metadata=metadata, charts=charts, global_timing=global_timing)
metadata=metadata,
charts=charts,
global_timing=global_timing
)
def _long_note_tail_value_v0(note: LongNote) -> int: def _long_note_tail_value_v0(note: LongNote) -> int:
@ -260,7 +250,9 @@ def _long_note_tail_value_v0(note: LongNote) -> int:
try: try:
return X_Y_OFFSET_TO_P_VALUE[dx, dy] return X_Y_OFFSET_TO_P_VALUE[dx, dy]
except KeyError: except KeyError:
raise ValueError(f"memon cannot represent a long note with its tail starting ({dx}, {dy}) away from the note") from None raise ValueError(
f"memon cannot represent a long note with its tail starting ({dx}, {dy}) away from the note"
) from None
def check_representable_in_v0(song: Song, version: str) -> None: def check_representable_in_v0(song: Song, version: str) -> None:
@ -269,31 +261,32 @@ def check_representable_in_v0(song: Song, version: str) -> None:
that cannot be represented in a memon v0.x.x file (includes legacy)""" that cannot be represented in a memon v0.x.x file (includes legacy)"""
if any(chart.timing is not None for chart in song.charts.values()): if any(chart.timing is not None for chart in song.charts.values()):
raise ValueError(f"memon:{version} cannot represent a song with per-chart timing") raise ValueError(
f"memon:{version} cannot represent a song with per-chart timing"
)
if song.global_timing is None: if song.global_timing is None:
raise ValueError("The song has no timing information") raise ValueError("The song has no global timing information")
number_of_timing_events = len(song.global_timing.events) number_of_timing_events = len(song.global_timing.events)
if number_of_timing_events != 1: if number_of_timing_events != 1:
if number_of_timing_events == 0: if number_of_timing_events == 0:
raise ValueError("The song has no BPM") raise ValueError("The song has no BPM")
else: else:
raise ValueError(f"memon:{version} does not handle Stops or BPM changes") raise ValueError(f"memon:{version} does not handle BPM changes")
event = song.global_timing.events[0] event = song.global_timing.events[0]
if not isinstance(event, BPMEvent):
raise ValueError("The song file has no BPM")
if event.BPM <= 0: if event.BPM <= 0:
raise ValueError("memon:legacy only accepts strictly positive BPMs") raise ValueError("memon:{version} only accepts strictly positive BPMs")
if event.time != 0: if event.time != 0:
raise ValueError("memon:legacy only accepts a BPM on the first beat") raise ValueError(f"memon:{version} only accepts a BPM on the first beat")
for difficulty, chart in song.charts.items(): for difficulty, chart in song.charts.items():
if len(set(chart.notes)) != len(chart.notes): if len(set(chart.notes)) != len(chart.notes):
raise ValueError(f"{difficulty} chart has duplicate notes, these cannot be represented") raise ValueError(
f"{difficulty} chart has duplicate notes, these cannot be represented"
)
def _dump_to_json(memon: dict) -> IO: def _dump_to_json(memon: dict) -> IO:
@ -306,27 +299,35 @@ def _compute_resolution(notes: List[Union[TapNote, LongNote]]) -> int:
return lcm( return lcm(
*chain( *chain(
iter(note.time.denominator for note in notes), iter(note.time.denominator for note in notes),
iter(note.duration.denominator for note in notes if isinstance(note, LongNote)) iter(
note.duration.denominator
for note in notes
if isinstance(note, LongNote)
),
) )
) )
def _dump_memon_note_v0(note: Union[TapNote, LongNote], resolution: int) -> Dict[str, int]: def _dump_memon_note_v0(
note: Union[TapNote, LongNote], resolution: int
) -> Dict[str, int]:
"""converts a note into the {n, t, l, p} form""" """converts a note into the {n, t, l, p} form"""
memon_note = { memon_note = {
"n": note.index, "n": note.index,
"t": note.time.numerator * (resolution // note.time.denominator), "t": note.time.numerator * (resolution // note.time.denominator),
"l": 0, "l": 0,
"p": 0 "p": 0,
} }
if isinstance(note, LongNote): if isinstance(note, LongNote):
memon_note["l"] = note.duration.numerator * (resolution // note.duration.denominator) memon_note["l"] = note.duration.numerator * (
resolution // note.duration.denominator
)
memon_note["p"] = _long_note_tail_value_v0(note) memon_note["p"] = _long_note_tail_value_v0(note)
return memon_note return memon_note
def dump_memon_legacy(song: Song) -> Iterable[Tuple[Any, IO]]: def dump_memon_legacy(song: Song) -> Dict[str, IO]:
check_representable_in_v0(song, "legacy") check_representable_in_v0(song, "legacy")
@ -337,26 +338,30 @@ def dump_memon_legacy(song: Song) -> Iterable[Tuple[Any, IO]]:
"music path": str(song.metadata.audio), "music path": str(song.metadata.audio),
"jacket path": str(song.metadata.cover), "jacket path": str(song.metadata.cover),
"BPM": song.global_timing.events[0].BPM, "BPM": song.global_timing.events[0].BPM,
"offset": -song.global_timing.beat_zero_offset "offset": -song.global_timing.beat_zero_offset,
}, },
"data": [] "data": [],
} }
for difficulty, chart in song.charts.items(): for difficulty, chart in song.charts.items():
resolution = _compute_resolution(chart.notes) resolution = _compute_resolution(chart.notes)
memon["data"].append({ memon["data"].append(
"dif_name": difficulty, {
"level": chart.level, "dif_name": difficulty,
"resolution": resolution, "level": chart.level,
"notes": [ "resolution": resolution,
_dump_memon_note_v0(note, resolution) "notes": [
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position)) _dump_memon_note_v0(note, resolution)
] for note in sorted(
}) set(chart.notes), key=lambda n: (n.time, n.position)
)
],
}
)
return [(song, _dump_to_json(memon))] return [(song, _dump_to_json(memon))]
def dump_memon_0_1_0(song: Song, folder: Path) -> None: def dump_memon_0_1_0(song: Song) -> Dict[str, IO]:
check_representable_in_v0(song, "legacy") check_representable_in_v0(song, "legacy")
@ -368,9 +373,9 @@ def dump_memon_0_1_0(song: Song, folder: Path) -> None:
"music path": str(song.metadata.audio), "music path": str(song.metadata.audio),
"album cover path": str(song.metadata.cover), "album cover path": str(song.metadata.cover),
"BPM": song.global_timing.events[0].BPM, "BPM": song.global_timing.events[0].BPM,
"offset": -song.global_timing.beat_zero_offset "offset": -song.global_timing.beat_zero_offset,
}, },
"data": {} "data": {},
} }
for difficulty, chart in song.charts.items(): for difficulty, chart in song.charts.items():
resolution = _compute_resolution(chart.notes) resolution = _compute_resolution(chart.notes)
@ -380,13 +385,13 @@ def dump_memon_0_1_0(song: Song, folder: Path) -> None:
"notes": [ "notes": [
_dump_memon_note_v0(note, resolution) _dump_memon_note_v0(note, resolution)
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position)) for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
] ],
} }
return [(song, _dump_to_json(memon))] return [(song, _dump_to_json(memon))]
def dump_memon_0_2_0(song: Song, folder: Path) -> None: def dump_memon_0_2_0(song: Song) -> Dict[str, IO]:
check_representable_in_v0(song, "legacy") check_representable_in_v0(song, "legacy")
@ -400,7 +405,7 @@ def dump_memon_0_2_0(song: Song, folder: Path) -> None:
"BPM": song.global_timing.events[0].BPM, "BPM": song.global_timing.events[0].BPM,
"offset": -song.global_timing.beat_zero_offset, "offset": -song.global_timing.beat_zero_offset,
}, },
"data": {} "data": {},
} }
if song.metadata.preview_length != 0: if song.metadata.preview_length != 0:
@ -417,7 +422,7 @@ def dump_memon_0_2_0(song: Song, folder: Path) -> None:
"notes": [ "notes": [
_dump_memon_note_v0(note, resolution) _dump_memon_note_v0(note, resolution)
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position)) for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
] ],
} }
return [(song, _dump_to_json(memon))] return [(song, _dump_to_json(memon))]

View File

@ -19,7 +19,7 @@ from multidict import MultiDict
class BeatsTime(Fraction): class BeatsTime(Fraction):
@classmethod @classmethod
def from_ticks(cls: Type[Fraction], ticks: int, resolution: int) -> 'BeatsTime': def from_ticks(cls: Type[Fraction], ticks: int, resolution: int) -> "BeatsTime":
if resolution < 1: if resolution < 1:
raise ValueError(f"resolution cannot be negative : {resolution}") raise ValueError(f"resolution cannot be negative : {resolution}")
return cls(ticks, resolution) return cls(ticks, resolution)
@ -36,14 +36,14 @@ class NotePosition:
@property @property
def index(self): def index(self):
return self.x + 4*self.y return self.x + 4 * self.y
@classmethod @classmethod
def from_index(cls: Type[NotePosition], index: int) -> 'NotePosition': def from_index(cls: Type[NotePosition], index: int) -> "NotePosition":
if not (0 <= index < 16): if not (0 <= index < 16):
raise ValueError(f"Note position index out of range : {index}") raise ValueError(f"Note position index out of range : {index}")
return cls(x = index%4, y = index//4) return cls(x=index % 4, y=index // 4)
@dataclass @dataclass
@ -69,15 +69,9 @@ class BPMEvent:
BPM: Decimal BPM: Decimal
@dataclass
class StopEvent:
time: BeatsTime
duration: BeatsTime
@dataclass @dataclass
class Timing: class Timing:
events: List[Union[BPMEvent, StopEvent]] events: List[BPMEvent]
beat_zero_offset: SecondsTime beat_zero_offset: SecondsTime

View File

@ -1,10 +1,12 @@
from functools import reduce from functools import reduce
from math import gcd from math import gcd
def single_lcm(a: int, b: int): def single_lcm(a: int, b: int):
"""Return lowest common multiple of two numbers""" """Return lowest common multiple of two numbers"""
return a * b // gcd(a, b) return a * b // gcd(a, b)
def lcm(*args): def lcm(*args):
"""Return lcm of args.""" """Return lcm of args."""
return reduce(single_lcm, args) return reduce(single_lcm, args)

View File

@ -2,4 +2,4 @@ from jubeatools import __version__
def test_version(): def test_version():
assert __version__ == '0.1.0' assert __version__ == "0.1.0"