File reorg.
This commit is contained in:
parent
0724f4a4e1
commit
818deeae56
@ -8,7 +8,7 @@ from path import Path
|
||||
|
||||
from jubeatools.song import Song
|
||||
|
||||
from .memo.mono_column import dump_mono_column, load_mono_column
|
||||
from .jubeat_analyser.mono_column import dump_mono_column, load_mono_column
|
||||
from .memon import (
|
||||
dump_memon_0_1_0,
|
||||
dump_memon_0_2_0,
|
||||
|
17
jubeatools/formats/jubeat_analyser/__init__.py
Normal file
17
jubeatools/formats/jubeat_analyser/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""
|
||||
Formats read by jubeat analyser
|
||||
|
||||
memo is a vague term refering to several legacy formats.
|
||||
They were originally derived from the (somewhat) human-readable format choosen
|
||||
by websites storing official jubeat charts in text form as a memory aid.
|
||||
|
||||
The machine-readable variants are partially documented (in japanese)
|
||||
on these pages :
|
||||
- http://yosh52.web.fc2.com/jubeat/fumenformat.html
|
||||
- http://yosh52.web.fc2.com/jubeat/holdmarker.html
|
||||
"""
|
||||
|
||||
from .mono_column.dump import dump_mono_column
|
||||
from .mono_column.load import load_mono_column
|
||||
from .memo.dump import dump_memo
|
||||
from .memo.load import load_memo
|
@ -1,6 +1,29 @@
|
||||
"""
|
||||
Useful things to parse and dump the header of analyser-like formats
|
||||
Useful things to parse and dump the jubeat analyser command format
|
||||
|
||||
Known simple commands :
|
||||
- b=<decimal> : beats per measure (4 by default)
|
||||
- m="<path>" : music file path
|
||||
- o=<int> : offset in ms (100 by default)
|
||||
- r=<int> : ? increase the offset ? (in ms) (not supported, couldn't find any examples, wtf is it for ?)
|
||||
- t=<decimal> : tempo
|
||||
|
||||
Known hash commands :
|
||||
- #memo # youbeat-like format but a bar division always means a 1/4 note (amongst other quirks)
|
||||
- #memo1 # youbeat-like but without bpm changes in the bar
|
||||
- #memo2 # youbeat-like with memo symbols
|
||||
- #boogie # youbeat-like
|
||||
- #pw=<int> # number of panels horizontally (4 by default)
|
||||
- #ph=<int> # number of panels vertically (4 by default)
|
||||
- #lev=<int> # chart level (typically 1 to 10)
|
||||
- #dif={1, 2, 3} # 1: BSC, 2: ADV, 3: EXT
|
||||
- #title="<str>" # music title
|
||||
- #artist="<str>" # artist's name
|
||||
- #jacket="<path>" # music cover art path
|
||||
- #prevpos=<int> # preview start (in ms)
|
||||
- #bpp # bytes per panel (2 by default)
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
from numbers import Number
|
||||
from typing import Any, Iterable, List, Optional, Tuple, Union
|
20
jubeatools/formats/jubeat_analyser/files.py
Normal file
20
jubeatools/formats/jubeat_analyser/files.py
Normal file
@ -0,0 +1,20 @@
|
||||
from path import Path
|
||||
from typing import Dict, List
|
||||
|
||||
def load_files(path: Path) -> Dict[Path, List[str]]:
|
||||
# The vast majority of memo files you will encounter will be propely
|
||||
# decoded using shift_jis_2004. Get ready for endless fun with the small
|
||||
# portion of files that won't
|
||||
files = {}
|
||||
if path.isdir():
|
||||
for f in path.files("*.txt"):
|
||||
_load_file(f, files)
|
||||
elif path.isfile():
|
||||
_load_file(path, files)
|
||||
return files
|
||||
|
||||
def _load_file(path: Path, files: Dict[Path, List[str]]):
|
||||
try:
|
||||
files[path] = path.lines('shift_jis_2004')
|
||||
except UnicodeDecodeError:
|
||||
pass
|
11
jubeatools/formats/jubeat_analyser/memo/__init__.py
Normal file
11
jubeatools/formats/jubeat_analyser/memo/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""
|
||||
memo
|
||||
|
||||
#memo is the first (and probably oldest) youbeat-like format jubeat analyser
|
||||
supports, the japanese docs give a good overview of what it looks like :
|
||||
|
||||
http://yosh52.web.fc2.com/jubeat/fumenformat.html
|
||||
"""
|
||||
|
||||
from .dump import dump_memo
|
||||
from .load import load_memo
|
2
jubeatools/formats/jubeat_analyser/memo/dump.py
Normal file
2
jubeatools/formats/jubeat_analyser/memo/dump.py
Normal file
@ -0,0 +1,2 @@
|
||||
def dump_memo(song):
|
||||
...
|
43
jubeatools/formats/jubeat_analyser/memo/load.py
Normal file
43
jubeatools/formats/jubeat_analyser/memo/load.py
Normal file
@ -0,0 +1,43 @@
|
||||
from jubeatools.song import Song
|
||||
from path import Path
|
||||
from ..parser import JubeatAnalyserParser
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
|
||||
|
||||
class MemoParser(JubeatAnalyserParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.sections: List[MemoLoadedSection] = []
|
||||
|
||||
def do_memo(self):
|
||||
...
|
||||
|
||||
def do_memo1(self):
|
||||
raise ValueError("This is not a memo file")
|
||||
|
||||
do_boogie = do_memo2 = do_memo1
|
||||
|
||||
def do_bpp(self, value):
|
||||
if self.sections:
|
||||
raise ValueError(
|
||||
"jubeatools does not handle changing the bytes per panel value halfway"
|
||||
)
|
||||
else:
|
||||
super().do_bpp(value)
|
||||
|
||||
|
||||
def append_chart_line(self, position: str):
|
||||
if self.bytes_per_panel == 1 and len(line) != 4:
|
||||
raise SyntaxError(f"Invalid chart line for #bpp=1 : {line}")
|
||||
elif self.bytes_per_panel == 2 and len(line.encode("shift_jis_2004")) != 8:
|
||||
raise SyntaxError(f"Invalid chart line for #bpp=2 : {line}")
|
||||
self.current_chart_lines.append(line)
|
||||
|
||||
def load_memo(path: Path) -> Song:
|
||||
# The vast majority of memo files you will encounter will be propely
|
||||
# decoded using shift_jis_2004. Get ready for endless fun with the small
|
||||
# portion of files that won't
|
||||
with open(path, encoding="shift_jis_2004") as f:
|
||||
lines = f.readlines()
|
26
jubeatools/formats/jubeat_analyser/mono_column/__init__.py
Normal file
26
jubeatools/formats/jubeat_analyser/mono_column/__init__.py
Normal file
@ -0,0 +1,26 @@
|
||||
"""
|
||||
Mono column (not an official name)
|
||||
|
||||
It's the format jubeat analyser expects when otherwise no format command has
|
||||
been found (like #memo #memo1 #memo2 or #boogie)
|
||||
|
||||
Mono-column files are usually properly decoded using `shift_jis_2004`
|
||||
|
||||
Mono-column files are made up of several sections.
|
||||
Each section is made up of a series of command lines and chart lines.
|
||||
The section end is marked by a line starting with "--"
|
||||
|
||||
Command lines follow the usual jubeat analyser command pattern
|
||||
|
||||
Chart lines come in groups of 4 in each section.
|
||||
A group of 4 chart lines makes up a frame, which contains the note symbols.
|
||||
|
||||
Note symbols encode both the position and the time. The position is determined
|
||||
"visually" by the position the note symbol occupies in the frame.
|
||||
The time within the frame is determined by the precise symbol used.
|
||||
|
||||
The default note symbols are the circled numbers from ① to ⑯, they represent
|
||||
"""
|
||||
|
||||
from .dump import dump_mono_column
|
||||
from .load import load_mono_column
|
@ -23,7 +23,7 @@ from jubeatools.song import (
|
||||
)
|
||||
|
||||
from ..command import dump_command
|
||||
from .commons import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS
|
||||
from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS
|
||||
|
||||
COMMAND_ORDER = [
|
||||
"b",
|
@ -7,6 +7,7 @@ from decimal import Decimal
|
||||
from enum import Enum
|
||||
from itertools import product
|
||||
from typing import Dict, Iterator, List, Set, Tuple
|
||||
from functools import reduce
|
||||
|
||||
import constraint
|
||||
from parsimonious import Grammar, NodeVisitor, ParseError
|
||||
@ -27,8 +28,10 @@ from jubeatools.song import (
|
||||
)
|
||||
|
||||
from ..command import is_command, parse_command
|
||||
from ..symbol import is_symbol_definition, parse_symbol_definition
|
||||
from .commons import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS
|
||||
from ..files import load_files
|
||||
from ..parser import JubeatAnalyserParser
|
||||
from ..symbol_definition import is_symbol_definition, parse_symbol_definition
|
||||
from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS
|
||||
|
||||
mono_column_chart_line_grammar = Grammar(
|
||||
r"""
|
||||
@ -211,127 +214,23 @@ def decimal_to_beats(current_beat: Decimal, symbol_timing: Decimal) -> BeatsTime
|
||||
return BeatsTime(decimal_time).limit_denominator(240)
|
||||
|
||||
|
||||
class MonoColumnParser:
|
||||
class MonoColumnParser(JubeatAnalyserParser):
|
||||
def __init__(self):
|
||||
self.music = None
|
||||
self.symbols = deepcopy(SYMBOL_TO_DECIMAL_TIME)
|
||||
self.current_beat = Decimal("0")
|
||||
self.current_tempo = None
|
||||
self.current_chart_lines = []
|
||||
self.timing_events = []
|
||||
self.offset = 0
|
||||
self.beats_per_section = 4
|
||||
self.bytes_per_panel = 2
|
||||
self.level = 1
|
||||
self.difficulty = None
|
||||
self.title = None
|
||||
self.artist = None
|
||||
self.jacket = None
|
||||
self.preview_start = None
|
||||
self.hold_by_arrow = False
|
||||
self.circle_free = False
|
||||
super().__init__()
|
||||
self.sections: List[MonoColumnLoadedSection] = []
|
||||
|
||||
def handle_command(self, command, value=None):
|
||||
try:
|
||||
method = getattr(self, f"do_{command}")
|
||||
except AttributeError:
|
||||
raise SyntaxError(f"Unknown analyser command : {command}") from None
|
||||
|
||||
if value is not None:
|
||||
method(value)
|
||||
else:
|
||||
method()
|
||||
|
||||
def do_m(self, value):
|
||||
self.music = value
|
||||
|
||||
def do_t(self, value):
|
||||
self.current_tempo = Decimal(value)
|
||||
self.timing_events.append(BPMEvent(self.current_beat, BPM=self.current_tempo))
|
||||
|
||||
def do_o(self, value):
|
||||
self.offset = int(value)
|
||||
|
||||
def do_b(self, value):
|
||||
self.beats_per_section = Decimal(value)
|
||||
|
||||
def do_memo(self):
|
||||
raise ValueError("This is not a mono-column file")
|
||||
|
||||
do_boogie = do_memo2 = do_memo1 = do_memo
|
||||
|
||||
def do_pw(self, value):
|
||||
if int(value) != 4:
|
||||
raise ValueError("jubeatools only supports 4×4 charts")
|
||||
|
||||
do_ph = do_pw
|
||||
|
||||
def do_lev(self, value):
|
||||
self.level = int(value)
|
||||
|
||||
def do_dif(self, value):
|
||||
dif = int(value)
|
||||
if dif <= 0:
|
||||
raise ValueError(f"Unknown chart difficulty : {dif}")
|
||||
if dif < 4:
|
||||
self.difficulty = DIFFICULTIES[dif]
|
||||
else:
|
||||
self.difficulty = f"EDIT-{dif-3}"
|
||||
|
||||
def do_title(self, value):
|
||||
self.title = value
|
||||
|
||||
def do_artist(self, value):
|
||||
self.artist = value
|
||||
|
||||
def do_jacket(self, value):
|
||||
self.jacket = value
|
||||
|
||||
def do_prevpos(self, value):
|
||||
self.preview_start = int(value)
|
||||
|
||||
def do_bpp(self, value):
|
||||
bpp = int(value)
|
||||
if self.sections:
|
||||
raise ValueError(
|
||||
"jubeatools does not handle changing the bytes per panel value halfway"
|
||||
)
|
||||
elif bpp not in (1, 2):
|
||||
raise ValueError(f"Unexcpected bpp value : {value}")
|
||||
elif self.circle_free and bpp == 1:
|
||||
raise ValueError("#bpp can only be 2 when #circlefree is activated")
|
||||
else:
|
||||
self.bytes_per_panel = int(value)
|
||||
|
||||
def do_holdbyarrow(self, value):
|
||||
self.hold_by_arrow = int(value) == 1
|
||||
|
||||
def do_holdbytilde(self, value):
|
||||
if int(value):
|
||||
raise ValueError("jubeatools does not support #holdbytilde")
|
||||
|
||||
def do_circlefree(self, raw_value):
|
||||
activate = bool(int(raw_value))
|
||||
if activate and self.bytes_per_panel != 2:
|
||||
raise ValueError("#circlefree can only be activated when #bpp=2")
|
||||
self.circle_free = activate
|
||||
|
||||
def define_symbol(self, symbol: str, timing: Decimal):
|
||||
bpp = self.bytes_per_panel
|
||||
length_as_shift_jis = len(symbol.encode("shift_jis_2004"))
|
||||
if length_as_shift_jis != bpp:
|
||||
raise ValueError(
|
||||
f"Invalid symbol definition. Since #bpp={bpp}, timing symbols "
|
||||
f"should be {bpp} bytes long but '{symbol}' is {length_as_shift_jis}"
|
||||
)
|
||||
if timing > self.beats_per_section:
|
||||
message = (
|
||||
"Invalid symbol definition conscidering the number of beats per section :\n"
|
||||
f"*{symbol}:{timing}"
|
||||
)
|
||||
raise ValueError(message)
|
||||
self.symbols[symbol] = timing
|
||||
super().do_bpp(value)
|
||||
|
||||
def move_to_next_section(self):
|
||||
if len(self.current_chart_lines) % 4 != 0:
|
||||
@ -369,7 +268,7 @@ class MonoColumnParser:
|
||||
elif is_separator(line):
|
||||
self.move_to_next_section()
|
||||
elif not is_empty_line(line):
|
||||
raise SyntaxError(f"not a valid #memo line : {line}")
|
||||
raise SyntaxError(f"not a valid mono-column file line : {line}")
|
||||
|
||||
def notes(self) -> Iterator[Union[TapNote, LongNote]]:
|
||||
if self.hold_by_arrow:
|
||||
@ -500,12 +399,15 @@ class MonoColumnParser:
|
||||
|
||||
|
||||
def load_mono_column(path: Path) -> Song:
|
||||
# The vast majority of memo files you will encounter will be propely
|
||||
# decoded using shift_jis_2004. Get ready for endless fun with the small
|
||||
# portion of files that won't
|
||||
with open(path, encoding="shift_jis_2004") as f:
|
||||
lines = f.readlines()
|
||||
files = load_files(path)
|
||||
charts = [
|
||||
_load_mono_column_file(lines)
|
||||
for _, lines in files.items()
|
||||
]
|
||||
return reduce(lambda a, b: a.merge(b), charts)
|
||||
|
||||
|
||||
def _load_mono_column_file(lines: List[str]) -> Song:
|
||||
state = MonoColumnParser()
|
||||
for i, raw_line in enumerate(lines):
|
||||
try:
|
124
jubeatools/formats/jubeat_analyser/parser.py
Normal file
124
jubeatools/formats/jubeat_analyser/parser.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""Base class and tools for the different parsers"""
|
||||
from copy import deepcopy
|
||||
from .symbols import NOTE_SYMBOLS
|
||||
from decimal import Decimal
|
||||
from jubeatools.song import BPMEvent
|
||||
|
||||
DIFFICULTIES = {1: "BSC", 2: "ADV", 3: "EXT"}
|
||||
|
||||
SYMBOL_TO_DECIMAL_TIME = {
|
||||
symbol: Decimal("0.25") * index for index, symbol in enumerate(NOTE_SYMBOLS)
|
||||
}
|
||||
|
||||
|
||||
class JubeatAnalyserParser:
|
||||
def __init__(self):
|
||||
self.music = None
|
||||
self.symbols = deepcopy(SYMBOL_TO_DECIMAL_TIME)
|
||||
self.current_beat = Decimal("0")
|
||||
self.current_tempo = None
|
||||
self.current_chart_lines = []
|
||||
self.timing_events = []
|
||||
self.offset = 0
|
||||
self.beats_per_section = 4
|
||||
self.bytes_per_panel = 2
|
||||
self.level = 1
|
||||
self.difficulty = None
|
||||
self.title = None
|
||||
self.artist = None
|
||||
self.jacket = None
|
||||
self.preview_start = None
|
||||
self.hold_by_arrow = False
|
||||
self.circle_free = False
|
||||
|
||||
def handle_command(self, command, value=None):
|
||||
try:
|
||||
method = getattr(self, f"do_{command}")
|
||||
except AttributeError:
|
||||
raise SyntaxError(f"Unknown analyser command : {command}") from None
|
||||
|
||||
if value is not None:
|
||||
method(value)
|
||||
else:
|
||||
method()
|
||||
|
||||
def do_m(self, value):
|
||||
self.music = value
|
||||
|
||||
def do_t(self, value):
|
||||
self.current_tempo = Decimal(value)
|
||||
self.timing_events.append(BPMEvent(self.current_beat, BPM=self.current_tempo))
|
||||
|
||||
def do_o(self, value):
|
||||
self.offset = int(value)
|
||||
|
||||
def do_b(self, value):
|
||||
self.beats_per_section = Decimal(value)
|
||||
|
||||
def do_pw(self, value):
|
||||
if int(value) != 4:
|
||||
raise ValueError("jubeatools only supports 4×4 charts")
|
||||
|
||||
do_ph = do_pw
|
||||
|
||||
def do_lev(self, value):
|
||||
self.level = int(value)
|
||||
|
||||
def do_dif(self, value):
|
||||
dif = int(value)
|
||||
if dif <= 0:
|
||||
raise ValueError(f"Unknown chart difficulty : {dif}")
|
||||
if dif < 4:
|
||||
self.difficulty = DIFFICULTIES[dif]
|
||||
else:
|
||||
self.difficulty = f"EDIT-{dif-3}"
|
||||
|
||||
def do_title(self, value):
|
||||
self.title = value
|
||||
|
||||
def do_artist(self, value):
|
||||
self.artist = value
|
||||
|
||||
def do_jacket(self, value):
|
||||
self.jacket = value
|
||||
|
||||
def do_prevpos(self, value):
|
||||
self.preview_start = int(value)
|
||||
|
||||
def do_bpp(self, value):
|
||||
bpp = int(value)
|
||||
if bpp not in (1, 2):
|
||||
raise ValueError(f"Unexcpected bpp value : {value}")
|
||||
elif self.circle_free and bpp == 1:
|
||||
raise ValueError("#bpp can only be 2 when #circlefree is activated")
|
||||
else:
|
||||
self.bytes_per_panel = int(value)
|
||||
|
||||
def do_holdbyarrow(self, value):
|
||||
self.hold_by_arrow = int(value) == 1
|
||||
|
||||
def do_holdbytilde(self, value):
|
||||
if int(value):
|
||||
raise ValueError("jubeatools does not support #holdbytilde")
|
||||
|
||||
def do_circlefree(self, raw_value):
|
||||
activate = bool(int(raw_value))
|
||||
if activate and self.bytes_per_panel != 2:
|
||||
raise ValueError("#circlefree can only be activated when #bpp=2")
|
||||
self.circle_free = activate
|
||||
|
||||
def define_symbol(self, symbol: str, timing: Decimal):
|
||||
bpp = self.bytes_per_panel
|
||||
length_as_shift_jis = len(symbol.encode("shift_jis_2004"))
|
||||
if length_as_shift_jis != bpp:
|
||||
raise ValueError(
|
||||
f"Invalid symbol definition. Since #bpp={bpp}, timing symbols "
|
||||
f"should be {bpp} bytes long but '{symbol}' is {length_as_shift_jis}"
|
||||
)
|
||||
if timing > self.beats_per_section:
|
||||
message = (
|
||||
"Invalid symbol definition conscidering the number of beats per section :\n"
|
||||
f"*{symbol}:{timing}"
|
||||
)
|
||||
raise ValueError(message)
|
||||
self.symbols[symbol] = timing
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
Beat symbol definition
|
||||
Note symbol definition
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from typing import Optional, Tuple
|
@ -1,3 +1,5 @@
|
||||
"""Usual symbols for memo files"""
|
||||
|
||||
NOTE_SYMBOLS = [
|
||||
"①",
|
||||
"②",
|
15
jubeatools/formats/jubeat_analyser/tests/test_memo.py
Normal file
15
jubeatools/formats/jubeat_analyser/tests/test_memo.py
Normal file
@ -0,0 +1,15 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from hypothesis import given
|
||||
|
||||
from jubeatools.song import BeatsTime, BPMEvent, Chart, Metadata, SecondsTime, Timing
|
||||
from jubeatools.testutils.strategies import NoteOption
|
||||
from jubeatools.testutils.strategies import notes as notes_strat
|
||||
|
||||
from ..mono_column.dump import _dump_mono_column_chart
|
||||
from ..mono_column.load import MonoColumnParser
|
||||
|
||||
|
||||
@given(notes_strat(NoteOption.LONGS))
|
||||
def test_many_notes(notes):
|
||||
...
|
@ -4,7 +4,9 @@ import hypothesis.strategies as st
|
||||
from hypothesis import given
|
||||
|
||||
from jubeatools.song import BeatsTime, BPMEvent, Chart, Metadata, SecondsTime, Timing
|
||||
from jubeatools.testutils.strategies import long_note, tap_note
|
||||
from jubeatools.testutils.strategies import NoteOption, long_note
|
||||
from jubeatools.testutils.strategies import notes as notes_strat
|
||||
from jubeatools.testutils.strategies import tap_note
|
||||
|
||||
from ..mono_column.dump import _dump_mono_column_chart
|
||||
from ..mono_column.load import MonoColumnParser
|
||||
@ -42,3 +44,21 @@ def test_single_long_note(note):
|
||||
parser.load_line(line)
|
||||
actual = set(parser.notes())
|
||||
assert set([note]) == actual
|
||||
|
||||
|
||||
@given(notes_strat(NoteOption.LONGS))
|
||||
def test_many_notes(notes):
|
||||
timing = Timing(
|
||||
events=[BPMEvent(BeatsTime(0), Decimal(120))], beat_zero_offset=SecondsTime(0)
|
||||
)
|
||||
chart = Chart(
|
||||
level=0, timing=timing, notes=sorted(notes, key=lambda n: (n.time, n.position))
|
||||
)
|
||||
metadata = Metadata("", "", "", "")
|
||||
string_io = _dump_mono_column_chart("", chart, metadata, timing)
|
||||
chart = string_io.getvalue()
|
||||
parser = MonoColumnParser()
|
||||
for line in chart.split("\n"):
|
||||
parser.load_line(line)
|
||||
actual = set(parser.notes())
|
||||
assert notes == actual
|
@ -1,32 +0,0 @@
|
||||
"""
|
||||
memo is a vague term refering to several legacy formats.
|
||||
They were originally derived from the (somewhat) human-readable format choosen
|
||||
by websites storing official jubeat charts in text form as a memory aid.
|
||||
|
||||
The machine-readable variants are partially documented (in japanese)
|
||||
on these pages :
|
||||
- http://yosh52.web.fc2.com/jubeat/fumenformat.html
|
||||
- http://yosh52.web.fc2.com/jubeat/holdmarker.html
|
||||
|
||||
Known simple commands :
|
||||
- b=<decimal> : beats per measure (4 by default)
|
||||
- m="<path>" : music file path
|
||||
- o=<int> : offset in ms (100 by default)
|
||||
- r=<int> : increase the offset (in ms)
|
||||
- t=<decimal> : tempo
|
||||
|
||||
Known hash commands :
|
||||
- #memo # mono-column format
|
||||
- #memo1 # youbeat-like but missing a lot of the youbeat features
|
||||
- #memo2 # youbeat-like memo
|
||||
- #boogie # youbeat
|
||||
- #pw=<int> # number of panels horizontally (4 by default)
|
||||
- #ph=<int> # number of panels vertically (4 by default)
|
||||
- #lev=<int> # chart level (typically 1 to 10)
|
||||
- #dif={1, 2, 3} # 1: BSC, 2: ADV, 3: EXT
|
||||
- #title="<str>" # music title
|
||||
- #artist="<str>" # artist's name
|
||||
- #jacket="<path>" # music cover art path
|
||||
- #prevpos=<int> # preview start (in ms)
|
||||
- #bpp # bytes per panel (2 by default)
|
||||
"""
|
@ -1,2 +0,0 @@
|
||||
from .dump import dump_mono_column
|
||||
from .load import load_mono_column
|
@ -166,3 +166,23 @@ class Song:
|
||||
metadata: Metadata
|
||||
charts: Mapping[str, Chart] = field(default_factory=MultiDict)
|
||||
global_timing: Optional[Timing] = None
|
||||
|
||||
def merge(self, other: Song) -> Song:
|
||||
if self.metadata != other.metadata:
|
||||
raise ValueError(
|
||||
"Merge conflit in song metadata :\n"
|
||||
f"{self.metadata}\n"
|
||||
f"{other.metadata}"
|
||||
)
|
||||
charts = MultiDict()
|
||||
charts.extend(self.charts)
|
||||
charts.extend(other.charts)
|
||||
if (
|
||||
self.global_timing is not None
|
||||
and other.global_timing is not None
|
||||
and self.global_timing != other.global_timing
|
||||
):
|
||||
raise ValueError("Can't merge songs with differing global timings")
|
||||
global_timing = self.global_timing or other.global_timing
|
||||
return Song(self.metadata, charts, global_timing)
|
||||
|
||||
|
@ -94,7 +94,7 @@ def bad_notes(draw, longs: bool):
|
||||
note_strat = tap_note()
|
||||
if longs:
|
||||
note_strat = st.one_of(note_strat, long_note())
|
||||
return draw(st.sets(note_strat))
|
||||
return draw(st.sets(note_strat, max_size=50))
|
||||
|
||||
|
||||
@st.composite
|
||||
@ -105,7 +105,7 @@ def notes(draw, options: NoteOption):
|
||||
note_strat = tap_note()
|
||||
if NoteOption.LONGS in options:
|
||||
note_strat = st.one_of(note_strat, long_note())
|
||||
raw_notes = draw(st.sets(note_strat))
|
||||
raw_notes = draw(st.sets(note_strat, max_size=50))
|
||||
|
||||
if NoteOption.COLLISIONS in options:
|
||||
return raw_notes
|
||||
|
82
poetry.lock
generated
82
poetry.lock
generated
@ -72,7 +72,7 @@ description = "A library for property-based testing"
|
||||
name = "hypothesis"
|
||||
optional = false
|
||||
python-versions = ">=3.5.2"
|
||||
version = "5.16.3"
|
||||
version = "5.19.0"
|
||||
|
||||
[package.dependencies]
|
||||
attrs = ">=19.2.0"
|
||||
@ -109,12 +109,12 @@ description = "A lightweight library for converting complex datatypes to and fro
|
||||
name = "marshmallow"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "3.6.0"
|
||||
version = "3.6.1"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pytest", "pytz", "simplejson", "mypy (0.770)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)", "tox"]
|
||||
docs = ["sphinx (3.0.3)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)"]
|
||||
lint = ["mypy (0.770)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)"]
|
||||
dev = ["pytest", "pytz", "simplejson", "mypy (0.770)", "flake8 (3.8.2)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"]
|
||||
docs = ["sphinx (3.0.4)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)", "autodocsumm (0.1.13)"]
|
||||
lint = ["mypy (0.770)", "flake8 (3.8.2)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)"]
|
||||
tests = ["pytest", "pytz", "simplejson"]
|
||||
|
||||
[[package]]
|
||||
@ -217,7 +217,7 @@ description = "library with cross-python path, ini-parsing, io, code, log facili
|
||||
name = "py"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "1.8.1"
|
||||
version = "1.9.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
@ -233,7 +233,7 @@ description = "pytest: simple powerful testing with Python"
|
||||
name = "pytest"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "5.4.2"
|
||||
version = "5.4.3"
|
||||
|
||||
[package.dependencies]
|
||||
atomicwrites = ">=1.0"
|
||||
@ -267,7 +267,7 @@ description = "Alternative regular expression module, to replace re."
|
||||
name = "regex"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2020.5.14"
|
||||
version = "2020.6.8"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
@ -330,11 +330,11 @@ version = "3.7.4.2"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Measures number of Terminal column cells of wide-character codes"
|
||||
description = "Measures the displayed width of unicode strings in a terminal"
|
||||
name = "wcwidth"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.1.9"
|
||||
version = "0.2.5"
|
||||
|
||||
[metadata]
|
||||
content-hash = "ea32264fbb53166d279acff6d1adda6bbf3892b58d1c1ccebe0efabd8b661959"
|
||||
@ -366,16 +366,16 @@ colorama = [
|
||||
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
|
||||
]
|
||||
hypothesis = [
|
||||
{file = "hypothesis-5.16.3-py3-none-any.whl", hash = "sha256:7e8e0b7d412cb758c662dcbdcdc93cb8dccc38a9c67f2cd4bf885ba9f714f8d4"},
|
||||
{file = "hypothesis-5.16.3.tar.gz", hash = "sha256:81f9a033900ea73c7b594173dbce7b9eb39222c04fc6278a6f33cc39c49b144a"},
|
||||
{file = "hypothesis-5.19.0-py3-none-any.whl", hash = "sha256:dd21b1be951fefc9022047824c262f4e88d95dd24141b837b92e235c63baabb7"},
|
||||
{file = "hypothesis-5.19.0.tar.gz", hash = "sha256:ba7c92006716aaee4684f7876c116adedcfb88b19fcb55d21c47b28f03f933bf"},
|
||||
]
|
||||
isort = [
|
||||
{file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"},
|
||||
{file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"},
|
||||
]
|
||||
marshmallow = [
|
||||
{file = "marshmallow-3.6.0-py2.py3-none-any.whl", hash = "sha256:f88fe96434b1f0f476d54224d59333eba8ca1a203a2695683c1855675c4049a7"},
|
||||
{file = "marshmallow-3.6.0.tar.gz", hash = "sha256:c2673233aa21dde264b84349dc2fd1dce5f30ed724a0a00e75426734de5b84ab"},
|
||||
{file = "marshmallow-3.6.1-py2.py3-none-any.whl", hash = "sha256:9aa20f9b71c992b4782dad07c51d92884fd0f7c5cb9d3c737bea17ec1bad765f"},
|
||||
{file = "marshmallow-3.6.1.tar.gz", hash = "sha256:35ee2fb188f0bd9fc1cf9ac35e45fd394bd1c153cee430745a465ea435514bd5"},
|
||||
]
|
||||
more-itertools = [
|
||||
{file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"},
|
||||
@ -440,42 +440,42 @@ pluggy = [
|
||||
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"},
|
||||
{file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"},
|
||||
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
|
||||
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
|
||||
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-5.4.2-py3-none-any.whl", hash = "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3"},
|
||||
{file = "pytest-5.4.2.tar.gz", hash = "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"},
|
||||
{file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
|
||||
{file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
|
||||
]
|
||||
python-constraint = [
|
||||
{file = "python-constraint-1.4.0.tar.bz2", hash = "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e"},
|
||||
]
|
||||
regex = [
|
||||
{file = "regex-2020.5.14-cp27-cp27m-win32.whl", hash = "sha256:e565569fc28e3ba3e475ec344d87ed3cd8ba2d575335359749298a0899fe122e"},
|
||||
{file = "regex-2020.5.14-cp27-cp27m-win_amd64.whl", hash = "sha256:d466967ac8e45244b9dfe302bbe5e3337f8dc4dec8d7d10f5e950d83b140d33a"},
|
||||
{file = "regex-2020.5.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:27ff7325b297fb6e5ebb70d10437592433601c423f5acf86e5bc1ee2919b9561"},
|
||||
{file = "regex-2020.5.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ea55b80eb0d1c3f1d8d784264a6764f931e172480a2f1868f2536444c5f01e01"},
|
||||
{file = "regex-2020.5.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c9bce6e006fbe771a02bda468ec40ffccbf954803b470a0345ad39c603402577"},
|
||||
{file = "regex-2020.5.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:d881c2e657c51d89f02ae4c21d9adbef76b8325fe4d5cf0e9ad62f850f3a98fd"},
|
||||
{file = "regex-2020.5.14-cp36-cp36m-win32.whl", hash = "sha256:99568f00f7bf820c620f01721485cad230f3fb28f57d8fbf4a7967ec2e446994"},
|
||||
{file = "regex-2020.5.14-cp36-cp36m-win_amd64.whl", hash = "sha256:70c14743320a68c5dac7fc5a0f685be63bc2024b062fe2aaccc4acc3d01b14a1"},
|
||||
{file = "regex-2020.5.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a7c37f048ec3920783abab99f8f4036561a174f1314302ccfa4e9ad31cb00eb4"},
|
||||
{file = "regex-2020.5.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:89d76ce33d3266173f5be80bd4efcbd5196cafc34100fdab814f9b228dee0fa4"},
|
||||
{file = "regex-2020.5.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:51f17abbe973c7673a61863516bdc9c0ef467407a940f39501e786a07406699c"},
|
||||
{file = "regex-2020.5.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ce5cc53aa9fbbf6712e92c7cf268274eaff30f6bd12a0754e8133d85a8fb0f5f"},
|
||||
{file = "regex-2020.5.14-cp37-cp37m-win32.whl", hash = "sha256:8044d1c085d49673aadb3d7dc20ef5cb5b030c7a4fa253a593dda2eab3059929"},
|
||||
{file = "regex-2020.5.14-cp37-cp37m-win_amd64.whl", hash = "sha256:c2062c7d470751b648f1cacc3f54460aebfc261285f14bc6da49c6943bd48bdd"},
|
||||
{file = "regex-2020.5.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:329ba35d711e3428db6b45a53b1b13a0a8ba07cbbcf10bbed291a7da45f106c3"},
|
||||
{file = "regex-2020.5.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:579ea215c81d18da550b62ff97ee187b99f1b135fd894a13451e00986a080cad"},
|
||||
{file = "regex-2020.5.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:3a9394197664e35566242686d84dfd264c07b20f93514e2e09d3c2b3ffdf78fe"},
|
||||
{file = "regex-2020.5.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ce367d21f33e23a84fb83a641b3834dd7dd8e9318ad8ff677fbfae5915a239f7"},
|
||||
{file = "regex-2020.5.14-cp38-cp38-win32.whl", hash = "sha256:1386e75c9d1574f6aa2e4eb5355374c8e55f9aac97e224a8a5a6abded0f9c927"},
|
||||
{file = "regex-2020.5.14-cp38-cp38-win_amd64.whl", hash = "sha256:7e61be8a2900897803c293247ef87366d5df86bf701083b6c43119c7c6c99108"},
|
||||
{file = "regex-2020.5.14.tar.gz", hash = "sha256:ce450ffbfec93821ab1fea94779a8440e10cf63819be6e176eb1973a6017aff5"},
|
||||
{file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"},
|
||||
{file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"},
|
||||
{file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"},
|
||||
]
|
||||
rope = [
|
||||
{file = "rope-0.17.0.tar.gz", hash = "sha256:658ad6705f43dcf3d6df379da9486529cf30e02d9ea14c5682aa80eb33b649e1"},
|
||||
@ -551,6 +551,6 @@ typing-extensions = [
|
||||
{file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"},
|
||||
]
|
||||
wcwidth = [
|
||||
{file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"},
|
||||
{file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"},
|
||||
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
|
||||
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
|
||||
]
|
||||
|
@ -28,6 +28,14 @@ isort = "^4.3.21"
|
||||
[tool.poetry.scripts]
|
||||
jubeatools = 'jubeatools.cli:convert'
|
||||
|
||||
[tool.isort]
|
||||
multi_line_output = 3
|
||||
include_trailing_comma = true
|
||||
force_grid_wrap = 0
|
||||
use_parentheses = true
|
||||
ensure_newline_before_comments = true
|
||||
line_length = 88
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
||||
|
Loading…
x
Reference in New Issue
Block a user