1
0
mirror of synced 2024-11-13 18:10:48 +01:00

More docs

This commit is contained in:
Stepland 2022-09-06 18:51:37 +02:00
parent b43f404526
commit 9b88f732de
24 changed files with 1070 additions and 50 deletions

Binary file not shown.

View File

@ -0,0 +1,12 @@
@font-face {
font-family: "Noto-Memo";
src: url("../NotoMemoSubset.woff");
}
:root {
--font-mono-jp: "Noto-Memo", monospace;
}
.japanese-monospaced pre {
font-family: var(--font-mono-jp);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1 +0,0 @@
# API

View File

@ -0,0 +1,8 @@
# Chart file formats
```{eval-rst}
.. autofunction:: jubeatools.formats.guess::guess_format
.. autoclass:: jubeatools.formats.format_names::Format
.. autoclass:: jubeatools.formats.typing::Loader
.. autoclass:: jubeatools.formats.typing::Dumper
```

10
docs/source/api/index.md Normal file
View File

@ -0,0 +1,10 @@
# API
Look in here if you need information about a specific class, function or object
```{toctree}
:maxdepth: 1
song
loaders and dumpers
chart file formats
```

View File

@ -0,0 +1,139 @@
# Loaders and dumpers
## Collections
```{eval-rst}
.. autodata:: jubeatools.formats.loaders_and_dumpers.LOADERS
:no-value:
.. autodata:: jubeatools.formats.loaders_and_dumpers.DUMPERS
:no-value:
```
## Reference
### jubeat analyzer
```{eval-rst}
.. autofunction:: jubeatools.formats.jubeat_analyser.memo.load::load_memo
.. autofunction:: jubeatools.formats.jubeat_analyser.memo.dump.dump_memo
Default Filename Template
``{title} {difficulty_number}.txt``
:kwarg bool circle_free: Use circle-free symbols for long note ends
.. autofunction:: jubeatools.formats.jubeat_analyser.memo1.load::load_memo1
.. autofunction:: jubeatools.formats.jubeat_analyser.memo1.dump::dump_memo1
Default Filename Template
``{title} {difficulty_number}.txt``
:keyword bool circle_free: Use circle-free symbols for long note ends
.. autofunction:: jubeatools.formats.jubeat_analyser.memo2.load::load_memo2
.. autofunction:: jubeatools.formats.jubeat_analyser.memo2.dump::dump_memo2
Default Filename Template
``{title} {difficulty_number}.txt``
:keyword bool circle_free: Use circle-free symbols for long note ends
.. autofunction:: jubeatools.formats.jubeat_analyser.mono_column.load::load_mono_column
.. autofunction:: jubeatools.formats.jubeat_analyser.mono_column.dump::dump_mono_column
Default Filename Template
``{title} {difficulty_number}.txt``
:keyword bool circle_free: Use circle-free symbols for long note ends
```
### konami
```{eval-rst}
.. autofunction:: jubeatools.formats.konami.eve.load::load_eve
:kwarg int beat_snap: Snap all events to nearest 1/``beat_snap`` beat
.. autofunction:: jubeatools.formats.konami.eve.dump::dump_eve
Default Filename Template
``{difficulty:l}.eve``
.. autofunction:: jubeatools.formats.konami.jbsq.load::load_jbsq
:kwarg int beat_snap: Snap all events to nearest 1/``beat_snap`` beat
.. autofunction:: jubeatools.formats.konami.jbsq.dump::dump_jbsq
Default Filename Template
``seq_{difficulty:l}.jbsq``
```
### malody
```{eval-rst}
.. autofunction:: jubeatools.formats.malody.load::load_malody
.. autofunction:: jubeatools.formats.malody.dump::dump_malody
Default Filename Template
``{difficulty:l}.mc``
```
### memon
```{eval-rst}
.. autofunction:: jubeatools.formats.memon.v0.load::load_memon_legacy
:kwarg bool merge: When called on a folder, try to merge all the .memon
files found into a single :py:class:`Song <jubeatools.song.Song>` object
.. autofunction:: jubeatools.formats.memon.v0.dump::dump_memon_legacy
Default Filename Template
``{title}.memon``
.. autofunction:: jubeatools.formats.memon.v0.load::load_memon_0_1_0
:kwarg bool merge: When called on a folder, try to merge all the .memon
files found into a single :py:class:`Song <jubeatools.song.Song>` object
.. autofunction:: jubeatools.formats.memon.v0.dump::dump_memon_0_1_0
Default Filename Template
``{title}.memon``
.. autofunction:: jubeatools.formats.memon.v0.load::load_memon_0_2_0
:kwarg bool merge: When called on a folder, try to merge all the .memon
files found into a single :py:class:`Song <jubeatools.song.Song>` object
.. autofunction:: jubeatools.formats.memon.v0.dump::dump_memon_0_2_0
Default Filename Template
``{title}.memon``
.. autofunction:: jubeatools.formats.memon.v0.load::load_memon_0_3_0
:kwarg bool merge: When called on a folder, try to merge all the .memon
files found into a single :py:class:`Song <jubeatools.song.Song>` object
.. autofunction:: jubeatools.formats.memon.v0.dump::dump_memon_0_3_0
Default Filename Template
``{title}.memon``
.. autofunction:: jubeatools.formats.memon.v1.load::load_memon_1_0_0
:kwarg bool merge: When called on a folder, try to merge all the .memon
files found into a single :py:class:`Song <jubeatools.song.Song>` object
.. autofunction:: jubeatools.formats.memon.v1.dump::dump_memon_1_0_0
Default Filename Template
``{title}.memon``
```

7
docs/source/api/song.md Normal file
View File

@ -0,0 +1,7 @@
# Song and chart data model
## The `song` module
```{eval-rst}
.. automodule:: jubeatools.song
```

2
docs/source/cli.md Normal file
View File

@ -0,0 +1,2 @@
# Command-line Interface

View File

@ -3,28 +3,82 @@
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'jubeatools'
copyright = '2022, Stepland'
author = 'Stepland'
release = '1.4.0'
project = "jubeatools"
copyright = "2022, Stepland"
author = "Stepland"
release = "1.4.0"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = ["myst_parser"]
extensions = [
"myst_parser",
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx"
]
templates_path = ['_templates']
templates_path = ["_templates"]
exclude_patterns = []
nitpicky = True
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None)
}
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'furo'
html_static_path = ['_static']
html_theme = "furo"
html_static_path = ["_static"]
myst_enable_extensions = ["colon_fence"]
# These paths are either relative to html_static_path
# or fully qualified paths (eg. https://...)
html_css_files = [
'css/custom.css',
]
myst_enable_extensions = [
# "amsmath",
"colon_fence",
# "deflist",
# "dollarmath",
"fieldlist",
# "html_admonition",
# "html_image",
# "linkify",
# "replacements",
# "smartquotes",
# "strikethrough",
# "substitution",
# "tasklist",
]
# Autodoc options
# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_mock_imports
autodoc_mock_imports = [
"more_itertools",
"sortedcontainers",
"parsimonious",
"constraint",
"construct",
"construct_typed",
"simplejson",
"marshmallow",
"marshmallow_dataclass"
]
autodoc_default_options = {
"members": None, # document all members
"member-order": "bysource", # respect source declaration order
"undoc-members": True, # add an entry for members that lack docstrings
}

View File

@ -1,12 +0,0 @@
# Converting Charts
Once jubeatools is [installed](how to install jubeatools.md), you can use its command-line interface.
The general usage is as follows :
```console
$ jubeatools (input) (output) -f (output format) (options)
```
## Input

View File

@ -0,0 +1,134 @@
# Converting charts with jubeatools
This page explains how to use jubeatools via command line for people who are
unfamiliar with the terminal.
Make sure you [installed Python and jubeatools](<how to install jubeatools.md>)
before reading this page.
**jubeatools has no graphical user interface**, to use it, you have to type
commands in a terminal. If you are completely new to the terminal, this next
section was made for you, otherwise you can skip it.
## Primer on the terminal
The terminal is a text interface to your computer, you type in a command,
it does something in response, and maybe displays some text to tell you what
it just did.
### It's basically File Explorer but in text
At any given time while using the terminal you will be "in" a folder, just like
when using File Explorer on Windows.
On Windows, in both `cmd.exe` and PowerShell, the current directory
(this folder you are *in*) is displayed on the left as part of the *prompt*
```{figure-md}
:class: myclass
![](_static/current_directory_in_cmd.exe's_prompt.png)
Here as you can see I'm inside `C:\Users\Stepland`
```
On Linux and macOS, the current directory might not be displayed in the prompt.
You can use the `pwd` command to display it (`pwd` as in **P**rint **W**orking
**D**irectory)
### Navigating with `dir`/`ls` and `cd`
Now that you know how to see where you are in, let's learn how to move around
and see what things are there.
To see what's in the current folder, use :
- `dir` on Windows (**dir**ectory)
- `ls` on macOS/Linux (**l**i**s**t)
To move to a different folder, use `cd` (**c**hange **d**irectory) (same on all
3 OSes).
`cd` accepts two kinds of paths :
- absolute paths (paths that start with `C:\` on Windows or `/` on macOS/Linux)
- relative paths (relative to the current folder) : if there is a sub-folder
named `Photos` in your home folder *and* you are already in you home folder
on the terminal, you can just do `cd Photos` instead of giving the absolute
path to the `Photos` folder
On all 3 OSes, there's a special "fake" folder called `..` (double dot). It's
there to help you move *out* of the current folder. You can do `cd ..` from
anywhere and it will take you one folder up the hirerarchy.
### Tab triggers autocomplete
Typing long paths is tedious, luckily the terminal can help you. Start typing
your command (`cd ...something...`) then hit Tab before finishing, the terminal will
try to the fill in the rest of the folder or file name you were trying to type,
this can be chained multiple times in a row to type a longer path :
- Type the first few characters of the folder
- Tab (maybe more than once if several folders match)
- Type the first few characters of the sub-folder
- Tab again
etc etc ...
## Using jubeatools in a terminal
Now that you know the commands to move around, let's learn how to use
jubeatools itself as a command.
jubeatools expects arguments like this :
```console
$ jubeatools (input) (output) -f (output format) (options)
```
Let's break this down :
- `$` is a common way to represent a terminal *prompt* in computer litterature,
it is not part of the actual command, you don't have to type it. It's just
some sort of punctuation mark to remind you that whatever comes after it is a
command, and can be typed in a terminal.
- `jubeatools` is the command
- `(input)` is the path of the chart file you want to convert
- `(output)` is the path of the *converted* chart file you want to create
- `-f (output format)` is the output format you want jubeatools to use
- `(options)` are the extra options you might want to use (see [](cli.md))
### Example
Say you have a memo file called `sigsig.txt` and you want to convert it to
memon 1.0.0, then you would open up a terminal, navigate to the folder where
`sigsig.txt` is, then type the following command :
```console
$ jubeatools sigsig.txt sigsig.memon -f memon:v1.0.0
```
This will create a file called `sigsig.memon` in the same folder.
### Formats
Each format jubeatools supports has a precise name you need to use for the
`-f` option :
| | | name |
|-----------------|----------------------|----------------|
| memon | v1.0.0 | `memon:v1.0.0` |
| | v0.3.0 | `memon:v0.3.0` |
| | v0.2.0 | `memon:v0.2.0` |
| | v0.1.0 | `memon:v0.1.0` |
| | legacy | `memon:legacy` |
| jubeat analyser | #memo2 | `memo2` |
| | #memo1 | `memo1` |
| | #memo | `memo` |
| | mono-column (1列形式) | `mono-column` |
| jubeat (arcade) | .eve | `eve` |
| jubeat plus | .jbsq | `jbsq` |
| malody | .mc (Pad Mode) | `malody` |
### Options
Options are documented here : [](cli.md)

View File

@ -18,13 +18,13 @@ Open the installer
On the first page be sure the tick the box that says `Add Python 3.(number) to PATH`
:::{figure-md}
```{figure-md}
:class: myclass
![](_static/Add_Python_3.10_to_PATH.png)
Why isn't this on by default ?
:::
```
Click `Install Now`
@ -32,18 +32,117 @@ Once it's done, let's check that everything went fine.
Open up any terminal (for instance you can search "cmd" in the start menu)
Once at the prompt type in `py --version` then hit enter.
Once at the prompt type in `py --version` then hit Enter.
If everything went right it should answer back with the version number you just
installed.
:::{figure-md}
```{figure-md}
:class: myclass
![](_static/py_--version.png)
Here's what it's supposed to look like in `cmd.exe`
:::
```
### macOS
Use [brew](https://brew.sh/) to install python (at least 3.9)
### Linux
Python most-likely already came installed on you computer, but depending on your
chosen distribution the Python version your system ships with may be too old,
jubeatools requires Python 3.9
You can check which python version you have by opening up a terminal and typing
```console
$ python --version
```
or, if that doesn't work
```console
$ python3 --version
```
To install a more recent version on python on Debian and its variants you
should be able to do
```console
$ sudo apt install python3.9
```
If you use Fedora, Arch, or anything else, check your distro's documentation
to get the precise package name, the available versions, and the actual
command you need to execute.
## Installing jubeatools
Now that Python is on your machine, we are going to use
[`pip`](https://en.wikipedia.org/wiki/Pip_(package_manager)), Python's own
package manager, to download and install jubeatools.
Open up a terminal or Command Prompt, then type :
- **For Windows** : `py -m pip install jubeatools`
- **For Linux/macOS** : `pip install jubeatools`
```{figure-md}
:class: myclass
![](_static/py_-m_pip_install_jubeatools.png)
Here's how it would look like on Windows with `cmd.exe`
```
Hit Enter
`pip` is then going to blurt out the name and version of every
package and sub-package it's installing. You don't really need to pay much
attention to that. Just have a quick glance at the last few lines to make sure
`pip` says it successfully installed some packages and not that some error made
it stop.
## Checking that jubeatools works
While in a terminal or command prompt, type `jubeatools --help`, then hit Enter.
jubeatools should answer with something like :
```none
Usage: jubeatools [OPTIONS] SRC DST
Convert SRC to DST using the format specified by -f
Options:
--input-format [eve|jbsq|malody|memon:legacy|memon:v0.1.0|memon:v0.2.0|memon:v0.3.0|memon:v1.0.0|mono-column|memo|memo1|memo2]
Force jubeatools to read the input
file/folder as the given format.If this
option is not used jubeatools will try to
guess the format
-f, --format [eve|jbsq|malody|memon:legacy|memon:v0.1.0|memon:v0.2.0|memon:v0.3.0|memon:v1.0.0|mono-column|memo|memo1|memo2]
Output file format [required]
--circlefree Use #circlefree=1 for jubeat analyser
formats
--beat-snap INTEGER RANGE For compatible input formats, snap all notes
and bpm changes to the nearest 1/beat_snap
beat [x>=1]
--merge For memon, if called on a folder, merge all
the .memon files found
--help Show this message and exit.
```
If you see this help text, jubeatools is installed !
Congrats !

View File

@ -2,21 +2,20 @@
A toolbox for jubeat file formats
## For Charters
```{toctree}
---
maxdepth: 2
---
:caption: For Charters
:maxdepth: 1
:hidden:
how to install jubeatools
converting_charts
how to convert charts
```
## For Developpers
```{toctree}
---
maxdepth: 2
---
api
:caption: For Developers
:maxdepth: 1
:hidden:
library/index
cli
api/index
```

View File

@ -0,0 +1,10 @@
# Using jubeatools as a library
Look in here if you want to use jubeatools as a library in your own python code
```{toctree}
:maxdepth: 1
reading chart files
song object
writing chart files
```

View File

@ -0,0 +1,92 @@
# Reading charts
Reading a chart file is done by using a *loader*, it's a function that takes in
a path and returns a [`Song`](Song) object.
Let's go over how you can import then use one.
## Guessing the format
If you need to read chart files whose format you don't know in advance, you can
use the [`guess_format()`](guess_format) function. It's the same function the
CLI uses under the hood when you don't specify the input format yourself.
```python
>>> from pathlib import Path
>>> from jubeatools.formats.guess import guess_format
>>> guess_format(Path("sigsig.txt"))
<Format.MEMO_2: 'memo2'>
```
```{warning}
[`guess_format()`](guess_format) makes an honest attempt at guessing but it
doesn't work for all files 100% of the time. If you *know* that you will only
ever read a single format, I **strongly** recommend you import the correct
loader directly
```
## Importing a loader
Loaders are all defined somewhere in the `jubeatools.formats` module. The
precise import path of each loader is documented here :
[](<../api/loaders and dumpers.md>)
For example you can import the loader for `#memo2` files like this :
```python
from jubeatools.formats.jubeat_analyser import load_memo2
```
:::{tip}
If you don't want to have to write down the full import path or if you don't
know which format you will have to read in advance, you can import
[`LOADERS`](LOADERS) from `jubeatools.formats` to query the correct loader
based on the format. It's a dict that maps [`Format`](Format) enum members to
their associated loader.
```python
>>> from jubeatools.formats import LOADERS, Format
>>> load_memo2 = LOADERS[Format.MEMO_2]
```
:::
## Using a loader
Once you've imported or queried your loader, you can use it as follows :
```python
from pathlib import Path
path = Path("my_file.txt")
song = load_thing(path)
```
:::{note}
Some loaders accept extra options as keyword arguments :
```python
song = load_thing(path, splines="reticulate")
```
These extra options are specific to each loader and are documented in
[](<../api/loaders and dumpers.md>).
:::
Check out [](<song object.md>) to see how the chart data is organized in a
[`Song`](Song) object.
## The Loader Protocol
Loaders have a *uniform* interface, in other words they are all compatible with
the same function signature. It's the Loader *Protocol* :
```{py:function} load(path: pathlib.Path, **kwargs: Any) -> Song
Read what's in `path` and turn it into a `Song` object.
Possibly takes in some options via the kwargs.
:param pathlib.Path path: path to a file or folder to be read
:param Any **kwargs: Format-specific options
:return: the Song instance read from the file(s)
:rtype: Song
```

View File

@ -0,0 +1,346 @@
# `Song` objects
The [`Song`](Song) object is the main data model of jubeatools, it holds all
the data jubeatools can make sense of in a chart file : metadata,
timing information, and a set of charts.
## Reading properties
Loaders all return a [`Song`](Song) object, but how is the information stored
inside of it ?
### Metadata
The song metadata is accessible via the [`Song.metadata`](Song.metadata)
attribute
```python
>>> sigsig = load_memo2(Path("sigsig.txt"))
>>> sigsig.metadata.title
'SigSig'
>>> sigsig.metadata.artist
'kors k'
```
See {py:class}`jubeatools.song.Metadata` for a complete list of the existing
fields
### Charts
Charts are stored in the [`Song.charts`](Song.charts) attribute.
It's a dict that maps difficulty names (like `"ADV"` or `"EXT"`) to
[`Chart`](Chart) objects
```python
>>> sigsig.charts
{
'EXT': Chart(
level=Decimal('9.1'),
timing=Timing(
events=(
BPMEvent(time=Fraction(0), BPM=Decimal('179'))
),
beat_zero_offset=Decimal('2.32')
),
hakus=None,
notes=[...]
),
}
```
### Timing
Timing information is split between a common timing object and per-chart timing
objects. The common timing object acts as a fallback in case a chart doesn't
have its own timing object.
The common timing object is stored in the [`common_timing`](Song.common_timing)
attribute of [`Song`](Song) objects, while the chart timing is stored in the
[`timing`](Chart.timing) attribute of [`Chart`](Chart) objects.
```python
>>> sigsig.chart.timing
Timing(
events=BPMEvent(time=Fraction(0), BPM=Decimal('179')),
beat_zero_offset=Decimal('2.32')
)
```
## Constructing `Song` objects
If you want to programatically create [`Song`](Song) objects directly in
python code you have to construct a lot of different sub-objects.
Let's start with the most basic ones and work our way up the a full
[`Song`](Song) object.
All of the classes in the following sections are defined in the
`jubeatools.song` module. You should import them from this module.
### Beats
All musical time points and durations in jubeatools are stored as a fractional
amount of beats in a [`BeatsTime`](BeatsTime) object.
[`BeatsTime`](BeatsTime) is just a renamed copy of the
[`Fraction`](fractions.Fraction) class from python's standard library.
```python
beat_zero = BeatsTime(0)
half_a_beat = BeatsTime(1, 2)
beat_three_and_a_quarter = BeatsTime(3) + BeatsTime(1, 4)
```
jubeatools counts beats from *zero*, not one. You can think of the beat number
like the *duration* in beats from the start.
### Buttons
jubeatools identifies the controller buttons with 0-based x and y coordinates
with this orientation :
```
x →
0 1 2 3
y 0 □ □ □ □
↓ 1 □ □ □ □
2 □ □ □ □
3 □ □ □ □
```
x goes right and y goes down, both counting from 0 to 3.
A button is stored as a [`NotePosition`](NotePosition) object
If we label the buttons this way :
```
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
```
Button 5 would be stored this way :
```python
button_5 = NotePosition(x=0, y=1)
```
And button 12 would be stored this way :
```python
button_12 = NotePosition(x=3, y=2)
```
### Regular Notes
Regular notes are stored as [`TapNote`](TapNote) objects, these are just the
combination of a time in beats, stored in a [`BeatsTime`](BeatsTime) object,
and a button, stored in a [`NotePosition`](NotePosition)
For instance, the following [`TapNote`](TapNote) object
```python
note = TapNote(
time=BeatsTime(5, 4),
position=NotePosition(x=1, y=2)
)
```
would be represented this way in a `#memo2` file
```{code}
:class: japanese-monospaced
□□□□ ||
□□□□ |-①--|
□①□□ ||
□□□□ ||
```
`NotePosition(x=1, y=2)` means the note appears on button 10
`BeatsTime(5, 4)` means the note happens at beat {math}`\frac{5}{4}`.
Remember that jubeatools counts beats starting at *zero*, not one.
Since {math}`\frac{5}{4} = 1 + \frac{1}{4}`, this means that ① happens on the
*second* quarter note of the *second* beat.
### Long notes
Long notes are stored as [`LongNote`](LongNote) objects.
In addition to storing their starting time and position (just like regular
notes), long notes store their duration expressed in beats, as well as the
*starting* position of their tail, represented as a
[`NotePosition`](NotePosition) object.
For instance, the following long note
```python
long_note = LongNote(
time=BeatsTime(1, 2),
position=NotePosition(x=0, y=1),
duration=BeatsTime(1),
tail_tip=NotePosition(x=3, y=1)
)
```
would be written this way in a `#memo2` file
```{code}
:class: japanese-monospaced
□□□□ |--①-|
①――< |--②-|
□□□□ ||
□□□□ ||
□□□□
2□□□
□□□□
□□□□
```
<small>(assuming the file uses `#circlefree=1`)</small>
### BPM Changes
A BPM Change is represented as a [`BPMEvent`](BPMEvent) object. It defines the
BPM at a given time in beats.
For instance this [`BPMEvent`](BPMEvent) object
```python
bpm_event = BPMEvent(time=BeatsTime(0), BPM=Decimal(120))
```
Defines that the BPM at beat 0 is 120
:::{warning}
Use a **string**, not a float, when storing a non-interger BPM in a
[`Decimal`](decimal.Decimal) object
```python
>>> Decimal("120.1")
Decimal('120.1')
>>> Decimal(120.1)
Decimal('120.099999999999994315658113919198513031005859375')
```
:::
### Timing
[`Timing`](Timing) objects store all the info necessary to convert between
beats and seconds. That information boils down to two things :
- a list of BPM changes
- an initial offset
The initial offset is the "beat zero" offset, it's the time in *seconds* at
which beat 0 occurs in the audio file.
```{attention}
If you are used to the Stepmania notion of an "offset", this is the *opposite*
value
```
The beat zero offset is stored in a [`SecondsTime`](SecondsTime), which is
just a renamed copy of the [`Decimal`](decimal.Decimal) class from the standard
library
Here's a simple example of a [`Timing`](Timing) object
```python
timing = Timing(
events=[BPMEvent(time=BeatsTime(0), BPM=Decimal("180.5"))],
beat_zero_offset=SecondsTime("0.25")
)
```
This object means that the song's initial beat (beat *zero*) happens at time
00:00.25 in the audio file, and that the song has a constant BPM of 180.5
throughout
```{warning}
Be sure to set the first BPM at beat 0, some parts of jubeatools won't be able
to handle a [`Timing`](Timing) object nicely if its first BPM change isn't at
beat 0
```
### Charts
[`Chart`](Chart) objects store a [`Decimal`](decimal.Decimal)
level along with a list of mixed [`TapNote`](TapNote) and
[`LongNote`](LongNote) objects .
Here's a small example :
```python
basic = Chart(
level=Decimal("1.0"),
notes=[
TapNote(time=BeatsTime(0), position=NotePosition(x=0, y=0)),
LongNote(
time=BeatsTime(0),
position=NotePosition(x=0, y=1),
duration=BeatsTime(1),
tail_tip=NotePosition(x=3, y=1)
),
]
)
```
### Metadata
The [`Metadata`](Metadata) object stores all the song information that's not
specific to any single chart.
Currently this includes :
- Song title
- Artist
- Path to the audio file
- Path to the jacket file
- Song preview segment
- Path to a separate audio preview file (akin to BMS preview files)
Here's an example :
```python
metadata = Metadata(
title="My great song",
artist="Myself",
audio=Path("my_great_song.ogg"),
cover=Path("my_great_song.png"),
preview=Preview(start=SecondsTime("10.5"), length=("5"))
preview_file=Path("preview.ogg")
)
```
### Song
Finally, the [`Song`](Song) object combines all the previous elements.
It holds :
- a [`Metadata`](Metadata) object
- a `dict` that maps difficulty names to [`Chart`](Chart) objects
- a [`Timing`](Timing) object that applies to all charts
Here's an example :
```python
song = Song(
metadata=Metadata(
title="My great song",
artist="Myself",
audio=Path("my_great_song.ogg"),
cover=Path("my_great_song.png"),
),
charts={
"BSC": basic_chart,
"ADV": advanced_chart,
"EXT": extreme_chart
},
common_timing=timing_for_all_charts
)
```

View File

@ -0,0 +1,105 @@
# Writing charts
Similar to how you can read a chart file with a loader, writing a chart file
is done by using a *dumper*, it's a function that takes in a [`Song`](Song)
object and an output path template and return a dict that maps path names to a
[`bytes`](bytes) objects with the file contents.
Let's go over how you can import then use one.
## Importing a dumper
Much like loaders, dumpers are all defined somewhere in the `jubeatools.formats`
module.
The precise import path of each dumper is documented here :
[](<../api/loaders and dumpers.md>)
For example you can import the dumper for `#memo2` files like this :
```python
from jubeatools.formats.jubeat_analyser import dump_memo2
```
:::{tip}
Just like for loaders, if you don't want to have to write down the full import
path or if you don't know which formats you will have to write to in advance,
you can import [`DUMPERS`](DUMPERS) from `jubeatools.formats` to query the
correct dumper based on the format. It's a dict that maps [`Format`](Format)
enum members to their associated dumper.
```python
>>> from jubeatools.formats import DUMPERS, Format
>>> dump_memo2 = DUMPERS[Format.MEMO_2]
```
:::
## Using a dumper
Dumpers take in a [`Song`](Song) object and an output path template
```python
song = Song(...)
path = Path("new_file")
files = dumper(song, path)
```
Some dumpers also accept extra options as keyword arguments
```python
files = dumper(song, Path("new_file"), make_it_pretty=True)
```
These extra options are specific to each dumper and are documented
in [](<../api/loaders and dumpers.md>).
## The output path template
Dumpers all take in an output path template. If it doesn't point to an existing
*folder*, the path is used as a [format string](https://docs.python.org/3/library/string.html#format-string-syntax).
The following parameters are available, they are all passed as `str` :
| name | description |
|---------------------|----------------------------------------------------|
| `title` | song title |
| `difficulty` | uppercase BSC ADV EXT |
| `difficulty_index` | 0-based difficulty index, (BSC: 0, ADV: 1, EXT: 2) |
| `difficulty_number` | 1-based |
| `dedup` | dedup string ("-1", "-2" etc ...) |
For example `"{title} {difficulty}.txt"` would generate filenames like
`"SigSig BSC.txt"`, `"SigSig ADV.txt"`, or `"SigSig EXT.txt"`
jubeatools adds support for suffix `u` and `l` in the format specification
string for uppercase and lowercase respectively. This means that for example
`"{difficulty:l}.eve"` would generate filenames like `"bsc.eve"`, `"adv.eve"`
or `"ext.eve"`.
If a generated filename points to a file that already exists, a deduplicator
will be added right before the extension : `"SigSig EXT-1.txt"`,
`"SigSig EXT-2.txt"`, `"SigSig EXT-3.txt"` etc ...
If the output path template points to an existing folder, the dumper will
generate paths to files inside that folder. The filenames will follow a
template preset that tries to mimick what's usual for files in the format the
dumper outputs. These preset templates are documented in
[](<../api/loaders and dumpers.md>).
## The Dumper Protocol
Dumpers have a *uniform* interface, in other words they are all compatible with
the same function signature. It's the Dumper *Protocol* :
```{py:function} dump(song: Song, path: pathlib.Path, **kwargs: Any) -> Dict[pathlib.Path, bytes]
Convert the contents of `song` to files with associated name suggestions.
Possibly takes in some options via the kwargs.
:param Song song: Song object to be exported
:param pathlib.Path path: output path template
:param Any **kwargs: Format-specific options
:return: A dict that maps filenames to file contents as bytes
:rtype: Dict[pathlib.Path, bytes]
```

View File

@ -140,8 +140,7 @@ def double_braces(s: str) -> str:
class BetterStringFormatter(string.Formatter):
"""Enables the use of 'u' and 'l' suffixes in string format specifiers to
convert the string to uppercase or lowercase
Thanks stackoverflow ! https://stackoverflow.com/a/57570269/10768117
"""
Thanks stackoverflow ! https://stackoverflow.com/a/57570269/10768117"""
def format_field(self, value: Any, format_spec: str) -> str:
if isinstance(value, str):

View File

@ -6,6 +6,8 @@ from .format_names import Format
def guess_format(path: Path) -> Format:
"""Try to guess the format of the given file, raise an exception if the
format is unknown"""
if path.is_dir():
raise ValueError("Can't guess chart format for a folder")

View File

@ -4,6 +4,7 @@ from . import jubeat_analyser, konami, malody, memon
from .format_names import Format
from .typing import Dumper, Loader
#: Maps each Format enum member to its associated loader
LOADERS: Dict[Format, Loader] = {
Format.EVE: konami.load_eve,
Format.JBSQ: konami.load_jbsq,
@ -19,6 +20,7 @@ LOADERS: Dict[Format, Loader] = {
Format.MEMO_2: jubeat_analyser.load_memo2,
}
#: Maps each Format enum member to its associated dumper
DUMPERS: Dict[Format, Dumper] = {
Format.EVE: konami.dump_eve,
Format.JBSQ: konami.dump_jbsq,

View File

@ -1,6 +1,8 @@
"""Provides the Song class, the central model for chartsets
Every input format is converted to a Song instance
Every output format is created from a Song instance
"""
Provides the :py:obj:`Song` class, the central model for chartsets
- Every input format is converted to a :py:obj:`Song` instance
- Every output format is created from a :py:obj:`Song` instance
Most timing-related info is stored as beat fractions, otherwise a decimal
number of seconds is used"""
@ -30,7 +32,9 @@ from typing import (
from jubeatools.utils import none_or
#: A time measured in beats
BeatsTime = Fraction
#: A time measured in seconds
SecondsTime = Decimal
@ -84,15 +88,18 @@ class Position:
@dataclass(frozen=True, order=True)
class NotePosition(Position):
"""A specific square on the controller. (0, 0) is the top-left button, x
"""
A specific square on the controller. (0, 0) is the top-left button, x
goes right, y goes down.
::
x
0 1 2 3
y 0
1
2
3
x
0 1 2 3
y 0
1
2
3
The main difference with Position is that x and y MUST be between 0 and 3
"""
@ -279,9 +286,15 @@ class Song:
"""The abstract representation format for all jubeat chart sets.
A Song is a set of charts with associated metadata"""
#: Miscellaneous information about the song
metadata: Metadata
#: A regular dict that maps a difficulty name to a :py:obj:`Chart`.
#: The names for the usual jubeat difficulties should be ``"BSC"``, ``"ADV"``,
#: and ``"EXT"``
charts: Dict[str, Chart] = field(default_factory=dict)
#: The optional shared timing object that applies to all charts by default
common_timing: Optional[Timing] = None
#: The optional shared set of HAKUs that apply to all charts by default
common_hakus: Optional[Set[BeatsTime]] = None
@classmethod