Format the code
This commit is contained in:
parent
de5f6d38f5
commit
9af51d54c4
@ -1,3 +1,3 @@
|
||||
from .song import Song
|
||||
|
||||
__version__ = '0.1.0'
|
||||
__version__ = "0.1.0"
|
||||
|
@ -3,6 +3,7 @@ from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
|
||||
@click.command()
|
||||
def convert():
|
||||
...
|
||||
|
@ -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 typing import Any, Callable, Iterable, Tuple, IO
|
||||
from typing import Callable, Dict, IO
|
||||
|
||||
from jubeatools.song import Song
|
||||
from .memon import *
|
||||
from ._filekind import FileKind
|
||||
from .memon import (
|
||||
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 = {
|
||||
"memon": "memon:v0.2.0",
|
||||
}
|
||||
|
||||
# Loaders take in a folder containing the files to be converted
|
||||
# and return a Song object
|
||||
LOADERS: Mapping[str, Callable[[Path], Song]] = {
|
||||
# Loaders deserialize a folder or a file to a Song object
|
||||
LOADERS: Dict[str, Callable[[Path], Song]] = {
|
||||
"memon:legacy": load_memon_legacy,
|
||||
"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: Mapping[str, Callable[[Song], Iterable[Tuple[Any, IO]]]] = {
|
||||
# Dumpers serialize a Song object into a (filename -> file) mapping
|
||||
DUMPERS: Dict[str, Callable[[Song], Dict[str, IO]]] = {
|
||||
"memon:legacy": dump_memon_legacy,
|
||||
"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,
|
||||
}
|
||||
|
@ -14,9 +14,17 @@ from itertools import chain
|
||||
|
||||
from path import Path
|
||||
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
|
||||
|
||||
# v0.x.x long note value :
|
||||
@ -24,7 +32,7 @@ from jubeatools.utils import lcm
|
||||
# 8
|
||||
# 4
|
||||
# 0
|
||||
# 11 7 3 . 1 5 9
|
||||
# 11 7 3 . 1 5 9
|
||||
# 2
|
||||
# 6
|
||||
# 10
|
||||
@ -41,10 +49,10 @@ X_Y_OFFSET_TO_P_VALUE = {
|
||||
(3, 0): 9,
|
||||
(-1, 0): 3,
|
||||
(-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):
|
||||
@ -64,7 +72,7 @@ class MemonNote(StrictSchema):
|
||||
x = data["n"] % 4
|
||||
y = data["n"] // 4
|
||||
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}")
|
||||
|
||||
|
||||
@ -83,7 +91,9 @@ class MemonMetadata_legacy(StrictSchema):
|
||||
artist = fields.String(required=True)
|
||||
audio = fields.String(required=True, data_key="music 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)
|
||||
|
||||
|
||||
@ -93,7 +103,9 @@ class MemonMetadata_0_1_0(MemonMetadata_legacy):
|
||||
|
||||
class MemonPreview(StrictSchema):
|
||||
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):
|
||||
@ -118,10 +130,10 @@ class Memon_0_2_0(StrictSchema):
|
||||
|
||||
|
||||
def _search_and_load(file_or_folder: Path) -> Any:
|
||||
|
||||
|
||||
"""If given a folder, search for a single .memon file then json.load it
|
||||
If given a file, just json.load it"""
|
||||
|
||||
|
||||
if file_or_folder.isdir():
|
||||
memon_files = file_or_folder.files("*.memon")
|
||||
if len(memon_files) > 1:
|
||||
@ -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]:
|
||||
position = NotePosition.from_index(note["n"])
|
||||
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)
|
||||
tail_tip = NotePosition(*P_VALUE_TO_X_Y_OFFSET[note["p"]])
|
||||
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)
|
||||
|
||||
|
||||
|
||||
def load_memon_legacy(file_or_folder: Path) -> Song:
|
||||
raw_memon = _search_and_load(file_or_folder)
|
||||
schema = Memon_legacy()
|
||||
memon = schema.load(raw_memon)
|
||||
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(
|
||||
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()
|
||||
for memon_chart in memon["data"]:
|
||||
@ -170,17 +178,11 @@ def load_memon_legacy(file_or_folder: Path) -> Song:
|
||||
notes=[
|
||||
_load_memon_note_v0(note, memon_chart["resolution"])
|
||||
for note in memon_chart["notes"]
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
return Song(
|
||||
metadata=metadata,
|
||||
charts=charts,
|
||||
global_timing=global_timing
|
||||
)
|
||||
|
||||
|
||||
return Song(metadata=metadata, charts=charts, global_timing=global_timing)
|
||||
|
||||
|
||||
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()
|
||||
memon = schema.load(raw_memon)
|
||||
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(
|
||||
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()
|
||||
for difficulty, memon_chart in memon["data"]:
|
||||
@ -206,15 +205,11 @@ def load_memon_0_1_0(file_or_folder: Path) -> Song:
|
||||
notes=[
|
||||
_load_memon_note_v0(note, memon_chart["resolution"])
|
||||
for note in memon_chart["notes"]
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
return Song(
|
||||
metadata=metadata,
|
||||
charts=charts,
|
||||
global_timing=global_timing
|
||||
)
|
||||
|
||||
return Song(metadata=metadata, charts=charts, global_timing=global_timing)
|
||||
|
||||
|
||||
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()
|
||||
memon = schema.load(raw_memon)
|
||||
metadata_dict = {
|
||||
key: memon["metadata"][key]
|
||||
for key in ["title", "artist", "audio", "cover"]
|
||||
key: memon["metadata"][key] for key in ["title", "artist", "audio", "cover"]
|
||||
}
|
||||
if "preview" in memon["metadata"]:
|
||||
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)
|
||||
global_timing = Timing(
|
||||
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()
|
||||
for difficulty, memon_chart in memon["data"]:
|
||||
@ -243,15 +237,11 @@ def load_memon_0_2_0(file_or_folder: Path) -> Song:
|
||||
notes=[
|
||||
_load_memon_note_v0(note, memon_chart["resolution"])
|
||||
for note in memon_chart["notes"]
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
return Song(
|
||||
metadata=metadata,
|
||||
charts=charts,
|
||||
global_timing=global_timing
|
||||
)
|
||||
return Song(metadata=metadata, charts=charts, global_timing=global_timing)
|
||||
|
||||
|
||||
def _long_note_tail_value_v0(note: LongNote) -> int:
|
||||
@ -260,40 +250,43 @@ def _long_note_tail_value_v0(note: LongNote) -> int:
|
||||
try:
|
||||
return X_Y_OFFSET_TO_P_VALUE[dx, dy]
|
||||
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:
|
||||
|
||||
|
||||
"""Raises an exception if the Song object is ill-formed or contains information
|
||||
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()):
|
||||
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:
|
||||
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)
|
||||
if number_of_timing_events != 1:
|
||||
if number_of_timing_events == 0:
|
||||
raise ValueError("The song has no BPM")
|
||||
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]
|
||||
if not isinstance(event, BPMEvent):
|
||||
raise ValueError("The song file has no BPM")
|
||||
|
||||
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:
|
||||
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():
|
||||
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:
|
||||
@ -306,28 +299,36 @@ def _compute_resolution(notes: List[Union[TapNote, LongNote]]) -> int:
|
||||
return lcm(
|
||||
*chain(
|
||||
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"""
|
||||
memon_note = {
|
||||
"n": note.index,
|
||||
"t": note.time.numerator * (resolution // note.time.denominator),
|
||||
"l": 0,
|
||||
"p": 0
|
||||
"p": 0,
|
||||
}
|
||||
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)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
memon = {
|
||||
@ -337,27 +338,31 @@ def dump_memon_legacy(song: Song) -> Iterable[Tuple[Any, IO]]:
|
||||
"music path": str(song.metadata.audio),
|
||||
"jacket path": str(song.metadata.cover),
|
||||
"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():
|
||||
resolution = _compute_resolution(chart.notes)
|
||||
memon["data"].append({
|
||||
"dif_name": difficulty,
|
||||
"level": chart.level,
|
||||
"resolution": resolution,
|
||||
"notes": [
|
||||
_dump_memon_note_v0(note, resolution)
|
||||
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
|
||||
]
|
||||
})
|
||||
memon["data"].append(
|
||||
{
|
||||
"dif_name": difficulty,
|
||||
"level": chart.level,
|
||||
"resolution": resolution,
|
||||
"notes": [
|
||||
_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))]
|
||||
|
||||
|
||||
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")
|
||||
|
||||
memon = {
|
||||
@ -368,9 +373,9 @@ def dump_memon_0_1_0(song: Song, folder: Path) -> None:
|
||||
"music path": str(song.metadata.audio),
|
||||
"album cover path": str(song.metadata.cover),
|
||||
"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():
|
||||
resolution = _compute_resolution(chart.notes)
|
||||
@ -380,14 +385,14 @@ def dump_memon_0_1_0(song: Song, folder: Path) -> None:
|
||||
"notes": [
|
||||
_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))]
|
||||
|
||||
|
||||
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")
|
||||
|
||||
memon = {
|
||||
@ -400,7 +405,7 @@ def dump_memon_0_2_0(song: Song, folder: Path) -> None:
|
||||
"BPM": song.global_timing.events[0].BPM,
|
||||
"offset": -song.global_timing.beat_zero_offset,
|
||||
},
|
||||
"data": {}
|
||||
"data": {},
|
||||
}
|
||||
|
||||
if song.metadata.preview_length != 0:
|
||||
@ -417,7 +422,7 @@ def dump_memon_0_2_0(song: Song, folder: Path) -> None:
|
||||
"notes": [
|
||||
_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))]
|
||||
|
@ -19,7 +19,7 @@ from multidict import MultiDict
|
||||
|
||||
class BeatsTime(Fraction):
|
||||
@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:
|
||||
raise ValueError(f"resolution cannot be negative : {resolution}")
|
||||
return cls(ticks, resolution)
|
||||
@ -36,14 +36,14 @@ class NotePosition:
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
return self.x + 4*self.y
|
||||
|
||||
return self.x + 4 * self.y
|
||||
|
||||
@classmethod
|
||||
def from_index(cls: Type[NotePosition], index: int) -> 'NotePosition':
|
||||
def from_index(cls: Type[NotePosition], index: int) -> "NotePosition":
|
||||
if not (0 <= index < 16):
|
||||
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
|
||||
@ -69,15 +69,9 @@ class BPMEvent:
|
||||
BPM: Decimal
|
||||
|
||||
|
||||
@dataclass
|
||||
class StopEvent:
|
||||
time: BeatsTime
|
||||
duration: BeatsTime
|
||||
|
||||
|
||||
@dataclass
|
||||
class Timing:
|
||||
events: List[Union[BPMEvent, StopEvent]]
|
||||
events: List[BPMEvent]
|
||||
beat_zero_offset: SecondsTime
|
||||
|
||||
|
||||
@ -100,10 +94,10 @@ class Metadata:
|
||||
|
||||
@dataclass
|
||||
class Song:
|
||||
|
||||
|
||||
"""The abstract representation format for all jubeat chart sets.
|
||||
A Song is a set of charts with associated metadata"""
|
||||
|
||||
|
||||
metadata: Metadata
|
||||
charts: Mapping[str, Chart] = field(default_factory=MultiDict)
|
||||
global_timing: Optional[Timing] = None
|
||||
global_timing: Optional[Timing] = None
|
||||
|
@ -1,10 +1,12 @@
|
||||
from functools import reduce
|
||||
from math import gcd
|
||||
|
||||
|
||||
def single_lcm(a: int, b: int):
|
||||
"""Return lowest common multiple of two numbers"""
|
||||
return a * b // gcd(a, b)
|
||||
|
||||
|
||||
def lcm(*args):
|
||||
"""Return lcm of args."""
|
||||
return reduce(single_lcm, args)
|
||||
"""Return lcm of args."""
|
||||
return reduce(single_lcm, args)
|
||||
|
@ -2,4 +2,4 @@ from jubeatools import __version__
|
||||
|
||||
|
||||
def test_version():
|
||||
assert __version__ == '0.1.0'
|
||||
assert __version__ == "0.1.0"
|
||||
|
Loading…
Reference in New Issue
Block a user