Initial commit

This commit is contained in:
Simon Bruder 2020-06-22 20:33:12 +02:00
commit 8319ee42d9
No known key found for this signature in database
GPG key ID: 6F03E0000CC5B62F
19 changed files with 2813 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/testfiles

568
Cargo.lock generated Normal file
View file

@ -0,0 +1,568 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "adler32"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2"
[[package]]
name = "aho-corasick"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f"
[[package]]
name = "arrayvec"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9"
dependencies = [
"nodrop",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "brd"
version = "0.1.0"
dependencies = [
"anyhow",
"byteorder",
"clap",
"log",
"nom",
"num-derive",
"num-traits",
"pretty_env_logger",
"zip",
]
[[package]]
name = "byteorder"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]]
name = "bzip2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.9+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad3b39a260062fca31f7b0b12f207e8f2590a67d32ec7d59c20484b07ea7285e"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "cc"
version = "1.0.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bbb73db36c1246e9034e307d0fba23f9a2e251faa47ade70c1bd252220c8311"
[[package]]
name = "cfg-if"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33"
[[package]]
name = "clap"
version = "3.0.0-beta.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "860643c53f980f0d38a5e25dfab6c3c93b2cb3aa1fe192643d17a293c6c41936"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim",
"termcolor",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "clap_derive"
version = "3.0.0-beta.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb51c9e75b94452505acd21d929323f5a5c6c4735a852adbd39ef5fb1b014f30"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "crc32fast"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
dependencies = [
"cfg-if",
]
[[package]]
name = "env_logger"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
dependencies = [
"atty",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]]
name = "flate2"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42"
dependencies = [
"cfg-if",
"crc32fast",
"libc",
"miniz_oxide",
]
[[package]]
name = "heck"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "hermit-abi"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909"
dependencies = [
"libc",
]
[[package]]
name = "humantime"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
dependencies = [
"quick-error",
]
[[package]]
name = "indexmap"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe"
dependencies = [
"autocfg",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lexical-core"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86d66d380c9c5a685aaac7a11818bdfa1f733198dfd9ec09c70b762cd12ad6f"
dependencies = [
"arrayvec",
"bitflags",
"cfg-if",
"rustc_version",
"ryu",
"static_assertions",
]
[[package]]
name = "libc"
version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
[[package]]
name = "log"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
dependencies = [
"cfg-if",
]
[[package]]
name = "memchr"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
[[package]]
name = "miniz_oxide"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
dependencies = [
"adler32",
]
[[package]]
name = "nodrop"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "5.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6"
dependencies = [
"lexical-core",
"memchr",
"version_check",
]
[[package]]
name = "num-derive"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c8b15b261814f992e33760b1fca9fe8b693d8a65299f20c9901688636cfb746"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "num-traits"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
dependencies = [
"autocfg",
]
[[package]]
name = "os_str_bytes"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06de47b848347d8c4c94219ad8ecd35eb90231704b067e67e6ae2e36ee023510"
[[package]]
name = "pkg-config"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
[[package]]
name = "podio"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b18befed8bc2b61abc79a457295e7e838417326da1586050b919414073977f19"
[[package]]
name = "pretty_env_logger"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d"
dependencies = [
"env_logger",
"log",
]
[[package]]
name = "proc-macro-error"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn-mid",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"thread_local",
]
[[package]]
name = "regex-syntax"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8"
[[package]]
name = "rustc_version"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "static_assertions"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5304cfdf27365b7585c25d4af91b35016ed21ef88f17ced89c7093b43dba8b6"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "syn-mid"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "termcolor"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "thread_local"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
dependencies = [
"lazy_static",
]
[[package]]
name = "time"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "unicode-segmentation"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
[[package]]
name = "unicode-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
[[package]]
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
[[package]]
name = "winapi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "zip"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6df134e83b8f0f8153a094c7b0fd79dfebe437f1d76e7715afa18ed95ebe2fd7"
dependencies = [
"bzip2",
"crc32fast",
"flate2",
"podio",
"time",
]

16
Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "brd"
version = "0.1.0"
authors = ["Simon Bruder <simon@sbruder.de>"]
edition = "2018"
[dependencies]
anyhow = "1.0.31"
byteorder = "1.3.4"
log = "0.4.8"
nom = "5.1.1"
num-derive = "0.3"
num-traits = "0.2"
pretty_env_logger = "0.4"
zip = "0.5.5"
clap = "3.0.0-beta.1"

15
LICENSE Normal file
View file

@ -0,0 +1,15 @@
ISC License (ISC)
Copyright 2020 Simon Bruder
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

87
README.md Normal file
View file

@ -0,0 +1,87 @@
# BRD
BRD is a tool for working with [*Dance Dance Revolution*][ddr] step charts and
wave banks. It currently supports conversion to [*osu!mania*][osu!mania]
beatmaps and extraction of sounds from wave banks.
## Installation
Currently this is not published as a crate so you either have to clone the
repository manually and run `cargo build --release` or you can use `cargo
install --git https://github.com/sbruder/brd` to install the binary without
cloning.
## Modes
### ddr2osu
This converts DDR step charts (.ssq files) and the corresponding audio (from
.xwb files) to osu beatmaps (in an .osz container).
Basic usage:
brd ddr2osu -s file.ssq -x file.xwb -o file.osz --title "Song Title" --artist "Song Artist"
To learn more about supported options run `brd ddr2osu --help`
Batch conversion is possible with the included shell script `batch_convert.sh`
(usage guide at the top of the script).
#### Known Problems
* Since *osu!mania* does not support shock arrows, it either ignores them or
(by default) replaces them with a two-key combination (↑↓ or ←→); you can
change this with the (`--shock-action` option)
* Freezes do not work (I do not know how to get the start time yet) and
therefore are disabled in code (`ddr::ssq::FREEZE`)
### unxwb
This can list and extract sounds from XWB wave banks. It currently only
supports [ADPCM][ADPCM] sounds.
Basic Usage:
brd unxwb file.xwb
brd unxwb -l file.xwb
#### Known Problems
If you want to extract sounds that are stored in other formats, you can use
[Luigi Auriemmas unxwb][unxwb] (<kbd>Ctrl</kbd>+<kbd>F</kbd> unxwb).
## About this project
This is my first rust project. Dont expect too much from the code in terms of
quality, robustness or idiomacity (especially regarding error handling). There
currently are no tests.
Large portions of this tool would not have been possible without the following
resources:
* [SaxxonPike][SaxxonPike]s [scharfrichter][scharfrichter] which implements
[SSQ][scharfrichter-ssq] and XWB ([1][scharfrichter-xwb1],
[2][scharfrichter-xwb2]) and their [documentation about SSQ][ssq-doc]
* The [official osu! file format documentation][osu-doc]
* [MonoGame][MonoGame]s [XWB implementation][MonoGame-xwb]
* [Luigi Auriemma][aluigi]s [unxwb][unxwb] (especially the ADPCM header part)
## License
[ISC License](LICENSE)
This project is not affiliated with ppy or Konami.
[ADPCM]: https://en.wikipedia.org/wiki/Adaptive_differential_pulse-code_modulation
[MonoGame-xwb]: https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Audio/Xact/WaveBank.cs
[MonoGame]: https://github.com/MonoGame/MonoGame
[SaxxonPike]: https://github.com/SaxxonPike
[aluigi]: http://aluigi.altervista.org/
[ddr]: https://en.wikipedia.org/wiki/Dance_Dance_Revolution
[osu!mania]: https://osu.ppy.sh/help/wiki/Game_Modes/osu%21mania
[osu-doc]: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)
[scharfrichter-ssq]: https://github.com/SaxxonPike/scharfrichter/blob/master/Scharfrichter/Archives/BemaniSSQ.cs
[scharfrichter-xwb1]: https://github.com/SaxxonPike/scharfrichter/blob/master/Scharfrichter/Archives/MicrosoftXWB.cs
[scharfrichter-xwb2]: https://github.com/SaxxonPike/scharfrichter/blob/master/Scharfrichter/XACT3/Xact3WaveBank.cs
[scharfrichter]: https://github.com/SaxxonPike/scharfrichter
[ssq-doc]: https://github.com/SaxxonPike/rhythm-game-formats/blob/master/ddr/ssq.md
[unxwb]: http://aluigi.altervista.org/papers.htm

59
batch_convert.sh Executable file
View file

@ -0,0 +1,59 @@
#!/bin/bash
# Batch conversion helper script
#
# Create a new directory with two child directories (ssq and xwb). Put the step
# charts (*.ssq) into ssq and the wave banks (*.xwb) into xwb. If the directory
# is a child of this project, you can directly run the script, otherwise you
# have to execute it with the environment variable RUN_COMMAND that points to
# the executable. If you want to specify additional flags to pass to brd you
# can just pass them to this script. The converted beatmaps will be placed in
# the osz directory.
# If you want your beatmaps to have the right metadata (title and artist),
# create a file “metadata.csv” in the directory where you run this script with
# the following structure: name,Title,Artist (name is the name of the ssq file
# without the extension)
#
# Example:
# $ RUN_COMMAND=/path/to/target/release/brd /path/to/batch_covert.sh --source "Dance Dance Revolution x3"
set -e
mkdir -p osz
RUN_COMMAND=${RUN_COMMAND:-"cargo run --release --"}
echo "Extracting wave bank sound names"
for i in xwb/*.xwb; do
outfile="xwb/$(basename $i .xwb).xwb.sounds"
if ! [ -f "$outfile" ]; then
$RUN_COMMAND unxwb -l $i > "$outfile"
fi
done
for ssq_file in ssq/*.ssq; do
name=$(basename $ssq_file .ssq)
if [ -f "xwb/${name}.xwb" ]; then
xwb_file="xwb/${name}.xwb"
else
xwb_file="$(grep -lE "^${name}$" xwb/*.xwb.sounds|head -n 1)"
# strip .sounds
xwb_file="${xwb_file%.*}"
if [ -z "$xwb_file" ]; then
echo "ERR: Could not find wave bank for $name" >&2
continue
fi
fi
metadata=$(grep -sE "^${name}," metadata.csv|head -n 1)
if ! [ -z "$metadata" ]; then
title="$(cut -d, -f2 <<< $metadata)"
artist="$(cut -d, -f3 <<< $metadata)"
else
title="$name"
artist="unknown artist"
fi
echo "Converting $name"
$RUN_COMMAND ddr2osu -s "$ssq_file" -x "$xwb_file" -o "osz/${name}.osz" --title "$title" --artist "$artist" "$@"
done

1
src/converter.rs Normal file
View file

@ -0,0 +1 @@
pub mod ddr2osu;

421
src/converter/ddr2osu.rs Normal file
View file

@ -0,0 +1,421 @@
use std::fmt;
use std::str::FromStr;
use anyhow::{anyhow, Result};
use clap::Clap;
use log::{debug, info, trace};
use crate::ddr::ssq;
use crate::osu::beatmap;
#[derive(Debug)]
pub struct ConfigRange(f32, f32);
impl ConfigRange {
/// Map value from 0 to 1 onto the range
fn map_from(&self, value: f32) -> f32 {
(value * (self.1 - self.0)) + self.0
}
}
impl fmt::Display for ConfigRange {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}:{}", self.0, self.1)
}
}
impl FromStr for ConfigRange {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self> {
match string.split(':').collect::<Vec<&str>>()[..] {
[start, end] => Ok(ConfigRange(start.parse::<f32>()?, end.parse::<f32>()?)),
_ => Err(anyhow!("Invalid range format (expected start:end)")),
}
}
}
#[derive(Debug, Clap)]
pub struct Config {
#[clap(skip = "audio.wav")]
pub audio_filename: String,
#[clap(
long,
default_value = "180",
about = "Offset in milliseconds",
display_order = 5
)]
pub offset: i32,
#[clap(
long = "no-stops",
about = "Disable stops",
parse(from_flag = std::ops::Not::not),
display_order = 5
)]
pub stops: bool,
#[clap(
arg_enum,
long,
default_value = "step",
about = "What to do with shocks",
display_order = 5
)]
pub shock_action: ShockAction,
#[clap(
long = "hp",
about = "Range of HP drain (beginner:challenge)",
default_value = "4:8"
)]
pub hp_drain: ConfigRange,
#[clap(
long = "acc",
about = "Range of Accuracy (beginner:challenge)",
default_value = "7:8"
)]
pub accuracy: ConfigRange,
#[clap(flatten)]
pub metadata: ConfigMetadata,
}
#[derive(Clap, Debug)]
pub struct ConfigMetadata {
#[clap(long, about = "Song title to use in beatmap", display_order = 6)]
pub title: String,
#[clap(long, about = "Artist name to use in beatmap", display_order = 6)]
pub artist: String,
#[clap(
long,
default_value = "Dance Dance Revolution",
about = "Source to use in beatmap",
display_order = 6
)]
pub source: String,
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"ddr2osu (+{}ms{} shock→{:?} hp{} acc{})",
self.offset,
if self.stops { " stops" } else { "" },
self.shock_action,
self.hp_drain,
self.accuracy
)
}
}
#[derive(Clap, Clone, Debug)]
pub enum ShockAction {
Ignore,
Step,
//Static(Vec<u8>),
}
struct ShockStepGenerator {
last: u8,
columns: u8,
mode: ShockAction,
}
impl Iterator for ShockStepGenerator {
type Item = Vec<u8>;
fn next(&mut self) -> Option<Vec<u8>> {
match &self.mode {
ShockAction::Ignore => None,
ShockAction::Step => {
let columns = match self.last {
0 | 3 => vec![0, 3],
1 | 2 => vec![1, 2],
4 | 7 => vec![4, 7],
5 | 6 => vec![5, 6],
_ => vec![],
};
self.last = (self.last + 1) % self.columns;
Some(columns)
} //ShockAction::Static(columns) => Some(columns.clone()),
}
}
}
impl ShockStepGenerator {
fn new(columns: u8, mode: ShockAction) -> Self {
Self {
last: 0,
columns,
mode,
}
}
}
fn get_time_from_beats(beats: f32, tempo_changes: &[ssq::TempoChange]) -> Result<i32> {
for tempo_change in tempo_changes {
// For TempoChanges that are infinitely short but exactly cover that beat, use the start
// time of that TempoChange
if (beats - tempo_change.start_beats).abs() < 0.001
&& (beats - tempo_change.end_beats).abs() < 0.001
{
return Ok(tempo_change.start_ms);
}
if beats < tempo_change.end_beats {
return Ok(tempo_change.start_ms
+ ((beats - tempo_change.start_beats) * tempo_change.beat_length) as i32);
}
}
Err(anyhow!(
"Conversion of Step to HitObject failed: Beat lies outside of TimingPoints range"
))
}
impl From<ssq::TempoChange> for beatmap::TimingPoint {
fn from(tempo_change: ssq::TempoChange) -> Self {
beatmap::TimingPoint {
time: tempo_change.start_ms,
beat_length: if tempo_change.beat_length == f32::INFINITY {
10000.0
} else {
tempo_change.beat_length
},
meter: 4,
sample_set: beatmap::SampleSet::BeatmapDefault,
sample_index: 0,
volume: 100,
uninherited: true,
effects: beatmap::TimingPointEffects {
kiai_time: false,
omit_first_barline: false,
},
}
}
}
impl ssq::Step {
fn to_hit_objects(
&self,
num_columns: u8,
tempo_changes: &ssq::TempoChanges,
shock_step_generator: &mut ShockStepGenerator,
) -> Result<Vec<beatmap::HitObject>> {
let mut hit_objects = Vec::new();
match self {
ssq::Step::Step { beats, row } => {
let time = get_time_from_beats(*beats, &tempo_changes.0)?;
let columns: Vec<bool> = row.clone().into();
for (column, active) in columns.iter().enumerate() {
if *active {
hit_objects.push(beatmap::HitObject::HitCircle {
x: beatmap::column_to_x(column as u8, num_columns),
y: 192,
time,
hit_sound: beatmap::HitSound {
normal: true,
whistle: false,
finish: false,
clap: false,
},
new_combo: false,
skip_combo_colours: 0,
hit_sample: beatmap::HitSample {
normal_set: 0,
addition_set: 0,
index: 0,
volume: 0,
filename: "".to_string(),
},
})
}
}
}
ssq::Step::Freeze { start, end, row } => {
let time = get_time_from_beats(*start, &tempo_changes.0)?;
let end_time = get_time_from_beats(*end, &tempo_changes.0)?;
let columns: Vec<bool> = row.clone().into();
for (column, active) in columns.iter().enumerate() {
if *active {
hit_objects.push(beatmap::HitObject::Hold {
column: column as u8,
columns: num_columns,
time,
end_time,
hit_sound: beatmap::HitSound {
normal: true,
whistle: false,
finish: true,
clap: false,
},
new_combo: false,
skip_combo_colours: 0,
hit_sample: beatmap::HitSample {
normal_set: 0,
addition_set: 0,
index: 0,
volume: 0,
filename: "".to_string(),
},
})
}
}
}
ssq::Step::Shock { beats } => {
let columns = match shock_step_generator.next() {
Some(columns) => columns,
None => vec![],
};
for column in columns {
hit_objects.push(beatmap::HitObject::HitCircle {
x: beatmap::column_to_x(column as u8, num_columns),
y: 192,
time: get_time_from_beats(*beats, &tempo_changes.0)?,
hit_sound: beatmap::HitSound {
normal: true,
whistle: false,
finish: false,
clap: false,
},
new_combo: false,
skip_combo_colours: 0,
hit_sample: beatmap::HitSample {
normal_set: 0,
addition_set: 0,
index: 0,
volume: 0,
filename: "".to_string(),
},
})
}
}
}
Ok(hit_objects)
}
}
struct ConvertedChart {
difficulty: ssq::Difficulty,
hit_objects: beatmap::HitObjects,
timing_points: beatmap::TimingPoints,
}
impl ConvertedChart {
fn to_beatmap(&self, config: &Config) -> beatmap::Beatmap {
beatmap::Beatmap {
version: 14,
general: beatmap::General {
audio_filename: config.audio_filename.clone(),
audio_lead_in: 0,
preview_time: 0,
countdown: beatmap::Countdown::No,
sample_set: beatmap::SampleSet::Soft,
mode: beatmap::Mode::Mania,
},
editor: beatmap::Editor {},
metadata: beatmap::Metadata {
title: config.metadata.title.clone(),
artist: config.metadata.artist.clone(),
creator: format!("{}", config),
version: format!("{}", self.difficulty),
source: config.metadata.source.clone(),
tags: vec![],
},
difficulty: beatmap::Difficulty {
hp_drain_rate: config.hp_drain.map_from(self.difficulty.clone().into()),
circle_size: self.difficulty.players as f32 * 4.0,
overall_difficulty: config.accuracy.map_from(self.difficulty.clone().into()),
approach_rate: 8.0,
slider_multiplier: 0.64,
slider_tick_rate: 1.0,
},
events: beatmap::Events(vec![]),
timing_points: self.timing_points.clone(),
colours: beatmap::Colours(vec![]),
hit_objects: self.hit_objects.clone(),
}
}
}
impl ssq::SSQ {
pub fn to_beatmaps(&self, config: &Config) -> Result<Vec<beatmap::Beatmap>> {
debug!("Configuration: {:?}", config);
let mut timing_points = Vec::new();
timing_points.push(beatmap::TimingPoint {
time: 0,
beat_length: config.offset as f32,
meter: 4,
sample_set: beatmap::SampleSet::BeatmapDefault,
sample_index: 0,
volume: 0,
uninherited: true,
effects: beatmap::TimingPointEffects {
kiai_time: false,
omit_first_barline: false,
},
});
for entry in &self.tempo_changes.0 {
if config.stops || entry.beat_length != f32::INFINITY {
trace!("Converting {:?} to to timing point", entry);
let timing_point: beatmap::TimingPoint = entry.clone().into();
timing_points.push(timing_point);
}
}
debug!(
"Converted {} tempo changes to timing points",
self.tempo_changes.0.len()
);
let mut converted_charts = Vec::new();
for chart in &self.charts {
debug!("Converting chart {} to beatmap", chart.difficulty);
let mut hit_objects = beatmap::HitObjects(Vec::new());
let mut shock_step_generator =
ShockStepGenerator::new(chart.difficulty.players * 4, config.shock_action.clone());
for step in &chart.steps.0 {
trace!("Converting {:?} to hit object", step);
let mut step_hit_objects = step.to_hit_objects(
chart.difficulty.players * 4,
&self.tempo_changes,
&mut shock_step_generator,
)?;
hit_objects.0.append(&mut step_hit_objects);
}
let converted_chart = ConvertedChart {
difficulty: chart.difficulty.clone(),
hit_objects,
timing_points: beatmap::TimingPoints(timing_points.clone()),
};
debug!(
"Converted to beatmap with {} hit objects",
converted_chart.hit_objects.0.len(),
);
converted_charts.push(converted_chart);
}
let mut beatmaps = Vec::new();
for converted_chart in converted_charts {
let beatmap = converted_chart.to_beatmap(config);
beatmaps.push(beatmap);
}
info!("Converted {} step charts to beatmaps", beatmaps.len());
Ok(beatmaps)
}
}

1
src/ddr.rs Normal file
View file

@ -0,0 +1 @@
pub mod ssq;

420
src/ddr/ssq.rs Normal file
View file

@ -0,0 +1,420 @@
use std::convert::From;
use std::fmt;
use anyhow::{anyhow, Result};
use log::{debug, info, trace, warn};
use nom::bytes::complete::take;
use nom::multi::many0;
use nom::number::complete::{le_i16, le_i32, le_u16};
use nom::IResult;
use crate::utils;
use crate::utils::exec_nom_parser;
const MEASURE_LENGTH: i32 = 4096;
const FREEZE: bool = false;
// Convert time offset to beats
// time offset is the measure times MEASURE_LENGTH
fn measure_to_beats(metric: i32) -> f32 {
4.0 * metric as f32 / MEASURE_LENGTH as f32
}
fn parse_n_i32(n: usize, input: &[u8]) -> IResult<&[u8], Vec<i32>> {
let (input, bytes) = take(n as usize * 4)(input)?;
let (unprocessed_input, values) = many0(le_i32)(bytes)?;
assert_eq!(unprocessed_input.len(), 0);
Ok((input, values))
}
fn parse_usize(input: &[u8]) -> IResult<&[u8], usize> {
let (input, value) = le_i32(input)?;
Ok((input, value as usize))
}
#[derive(Clone, Debug, PartialEq)]
pub struct TempoChange {
pub start_ms: i32,
pub start_beats: f32,
pub end_beats: f32,
pub beat_length: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TempoChanges(pub Vec<TempoChange>);
impl TempoChanges {
fn parse(ticks_per_second: i32, input: &[u8]) -> IResult<&[u8], Self> {
let (input, count) = parse_usize(input)?;
let (input, measure) = parse_n_i32(count, input)?;
let (input, tempo_data) = parse_n_i32(count, input)?;
let mut entries = Vec::new();
let mut elapsed_ms = 0;
let mut elapsed_beats = 0.0;
for i in 1..count {
let delta_measure = measure[i] - measure[i - 1];
let delta_ticks = tempo_data[i] - tempo_data[i - 1];
let length_ms = 1000 * delta_ticks / ticks_per_second;
let length_beats = measure_to_beats(delta_measure);
let beat_length = length_ms as f32 / length_beats;
let entry = TempoChange {
start_ms: elapsed_ms,
start_beats: elapsed_beats,
end_beats: elapsed_beats + length_beats,
beat_length,
};
entries.push(entry);
elapsed_ms += length_ms;
elapsed_beats += length_beats;
}
Ok((input, Self(entries)))
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Step {
Step { beats: f32, row: Row },
Freeze { start: f32, end: f32, row: Row },
Shock { beats: f32 },
}
#[derive(Clone, Debug, PartialEq)]
pub struct Steps(pub Vec<Step>);
impl Steps {
fn parse(input: &[u8], players: u8) -> IResult<&[u8], Self> {
let (input, count) = parse_usize(input)?;
let (input, measure) = parse_n_i32(count, input)?;
let (input, steps) = take(count)(input)?;
// freeze data can be padded with zeroes
let (input, freeze_data) = take(input.len())(input)?;
let mut freeze = freeze_data.iter().skip_while(|x| **x == 0).copied();
let mut parsed_steps = Vec::new();
for i in 0..count {
let beats = measure_to_beats(measure[i]);
// check if either all eight bits are set (shock for double) or the first four (shock for
// single)
if steps[i] == 0xff || steps[i] == 0xf {
// shock
trace!("Shock arrow at {}", beats);
parsed_steps.push(Step::Shock { beats });
} else if steps[i] == 0x00 {
// extra data
let columns = freeze.next().unwrap();
let extra_type = freeze.next().unwrap();
if extra_type == 1 {
// freeze end (start is the last normal step in that column)
trace!("Freeze arrow at {}", beats);
let row = Row::new(columns, players);
if row.count_active() != 1 {
warn!("Found freeze with not exactly one column, which is not implemented, skipping");
continue;
}
let last_step = match Self::find_last(Self(parsed_steps.clone()), &row) {
Ok(last_step) => last_step,
Err(err) => {
warn!("Could not add freeze arrow: {}; adding normal step", err);
parsed_steps.push(Step::Step { beats, row });
continue;
}
};
if FREEZE {
parsed_steps.push(Step::Freeze {
start: if let Step::Step { beats, .. } = parsed_steps[last_step] {
beats
} else {
unreachable!()
},
end: beats,
row,
});
parsed_steps.remove(last_step);
} else {
trace!("Freeze disabled, adding normal step");
parsed_steps.push(Step::Step { beats, row });
}
} else {
debug!(
"Encountered unknown extra step with type {}, ignoring",
extra_type
);
}
} else {
// normal step
trace!("Normal step at {}", beats);
parsed_steps.push(Step::Step {
beats,
row: Row::new(steps[i], players),
});
}
}
debug!("Parsed {} steps", parsed_steps.len());
Ok((input, Self(parsed_steps)))
}
fn find_last(steps: Self, row: &Row) -> Result<usize> {
for i in (0..steps.0.len()).rev() {
if let Step::Step { row: step_row, .. } = &steps.0[i] {
if step_row.clone().intersects(row.clone()) {
return Ok(i);
}
}
}
Err(anyhow!("No previous step found on that column"))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Difficulty {
pub players: u8,
difficulty: u8,
}
impl From<u16> for Difficulty {
fn from(parameter: u16) -> Self {
Self {
difficulty: ((parameter & 0xFF00) >> 8) as u8,
players: (parameter & 0xF) as u8 / 4,
}
}
}
impl Into<f32> for Difficulty {
fn into(self) -> f32 {
match self.difficulty {
1 => 0.25,
2 => 0.5,
3 => 1.0,
4 => 0.0,
6 => 0.75,
_ => 1.0,
}
}
}
impl fmt::Display for Difficulty {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let players = match self.players {
1 => "Single",
2 => "Double",
_ => "Unknown Number of Players",
};
let difficulty = match self.difficulty {
1 => "Basic",
2 => "Difficult",
3 => "Challenge",
4 => "Beginner",
6 => "Expert",
_ => "Unknown Difficulty",
};
write!(f, "{} {}", players, difficulty)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Chart {
pub difficulty: Difficulty,
pub steps: Steps,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SSQ {
pub tempo_changes: TempoChanges,
pub charts: Vec<Chart>,
}
impl From<Chunks> for SSQ {
fn from(chunks: Chunks) -> Self {
let mut ssq = Self {
tempo_changes: TempoChanges(Vec::new()),
charts: Vec::new(),
};
for chunk in chunks.0 {
match chunk {
Chunk::TempoChanges(mut tempo_changes) => {
ssq.tempo_changes.0.append(&mut tempo_changes.0)
}
Chunk::Chart(chart) => ssq.charts.push(chart),
Chunk::Extra(..) => {}
}
}
info!("Parsed {} charts", ssq.charts.len());
ssq
}
}
impl SSQ {
pub fn parse(data: &[u8]) -> Result<Self> {
debug!(
"Configuration: measure length: {}, use freezes: {}",
MEASURE_LENGTH, FREEZE
);
let chunks = exec_nom_parser(Chunks::parse, data)?;
Ok(Self::from(chunks))
}
}
#[derive(Clone, Debug, PartialEq)]
enum Chunk {
Chart(Chart),
TempoChanges(TempoChanges),
Extra(Vec<u8>),
}
impl Chunk {
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (input, length) = le_i32(input)?;
let (input, chunk_type) = le_i16(input)?;
let (input, parameter) = le_u16(input)?;
// length without i32 and 2 × i16
let (input, data) = take(length as usize - 8)(input)?;
let chunk = match chunk_type {
1 => {
debug!("Parsing tempo changes (ticks/s: {})", parameter);
let (_, TempoChanges(tempo_changes)) = TempoChanges::parse(parameter as i32, data)?;
Self::TempoChanges(TempoChanges(tempo_changes))
}
3 => {
let difficulty = Difficulty::from(parameter);
debug!("Parsing step chunk ({})", difficulty);
let (_, steps) = Steps::parse(data, difficulty.players)?;
Self::Chart(Chart { difficulty, steps })
}
_ => {
debug!("Found extra chunk (length {})", data.len());
Self::Extra(data.to_vec())
}
};
Ok((input, chunk))
}
}
pub struct Chunks(Vec<Chunk>);
impl Chunks {
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (input, chunks) = many0(Chunk::parse)(input)?;
Ok((input, Self(chunks)))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlayerRow {
pub left: bool,
pub down: bool,
pub up: bool,
pub right: bool,
}
impl From<u8> for PlayerRow {
fn from(byte: u8) -> Self {
let columns = utils::byte_to_bitarray(byte);
PlayerRow {
left: columns[0],
down: columns[1],
up: columns[2],
right: columns[3],
}
}
}
impl Into<Vec<bool>> for PlayerRow {
fn into(self) -> Vec<bool> {
vec![self.left, self.down, self.up, self.right]
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Row {
Single(PlayerRow),
Double(PlayerRow, PlayerRow),
}
impl Into<Vec<bool>> for Row {
fn into(self) -> Vec<bool> {
match self {
Self::Single(row) => row.into(),
Self::Double(row1, row2) => {
let mut row: Vec<bool> = Vec::new();
row.append(&mut row1.into());
row.append(&mut row2.into());
row
}
}
}
}
impl Row {
fn new(byte: u8, players: u8) -> Self {
match players {
1 => Self::Single(PlayerRow::from(byte)),
2 => Self::Double(PlayerRow::from(byte), PlayerRow::from(byte >> 4)),
_ => unreachable!(),
}
}
fn count_active(&self) -> u8 {
let mut rows = Vec::<bool>::new();