Initial commit

master
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();
match self {
Self::Single(row) => {
rows.append(&mut row.clone().into());
}
Self::Double(player1, player2) => {
rows.append(&mut player1.clone().into());
rows.append(&mut player2.clone().into());
}
}
rows.iter().map(|x| *x as u8).sum()
}
fn intersects(self, other: Self) -> bool {
let rows: Vec<(Vec<bool>, Vec<bool>)> = match (self, other) {
(Self::Single(self_row), Self::Single(other_row)) => {
vec![(self_row.into(), other_row.into())]
}
(Self::Double(self_row1, self_row2), Self::Double(other_row1, other_row2)) => vec![
(self_row1.into(), other_row1.into()),
(self_row2.into(), other_row2.into()),
],
_ => vec![],
};
for (self_row, other_row) in rows {
for (self_col, other_col) in self_row.iter().zip(other_row.iter()) {
if *self_col && self_col == other_col {
return true;
}
}
}
false
}
}

5
src/lib.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod converter;
pub mod ddr;
pub mod osu;
mod utils;
pub mod xact3;

179
src/main.rs Normal file
View File

@ -0,0 +1,179 @@
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
use anyhow::{anyhow, Result};
use clap::Clap;
use log::{debug, error, info};
use brd::converter;
use brd::ddr::ssq::SSQ;
use brd::osu;
use brd::xact3::xwb::WaveBank;
#[derive(Clap)]
#[clap()]
struct Opts {
#[clap(subcommand)]
subcmd: SubCommand,
}
#[derive(Clap)]
enum SubCommand {
#[clap(
name = "unxwb",
about = "Extracts sounds from XWB wave banks",
display_order = 1
)]
UnXWB(UnXWB),
#[clap(
about = "Converts DDR step charts to osu!mania beatmaps",
display_order = 1
)]
DDR2osu(DDR2osu),
}
#[derive(Clap)]
struct UnXWB {
#[clap(short, long, about = "List available sounds and exit")]
list_entries: bool,
#[clap(short = "e", long, about = "Only extract this entry")]
single_entry: Option<String>,
#[clap(name = "file")]
file: PathBuf,
}
#[derive(Clap)]
struct DDR2osu {
#[clap(
short = "s",
long = "ssq",
name = "file.ssq",
about = "DDR step chart file",
display_order = 1
)]
ssq_file: PathBuf,
#[clap(
short = "x",
long = "xwb",
name = "file.xwb",
about = "XAC3 wave bank file",
display_order = 2
)]
xwb_file: PathBuf,
#[clap(
short = "o",
long = "out",
name = "file.osz",
about = "osu! beatmap archive",
display_order = 3
)]
out_file: PathBuf,
#[clap(
short = "n",
name = "sound name",
about = "Sound in wave bank, otherwise inferred from SSQ filename",
display_order = 4
)]
sound_name: Option<String>,
#[clap(flatten)]
convert: converter::ddr2osu::Config,
}
fn error(message: String) -> Result<()> {
error!("{}", message);
Err(anyhow!(message))
}
fn read_file(name: &PathBuf) -> Result<Vec<u8>> {
let mut file = File::open(name)?;
let mut data = vec![];
file.read_to_end(&mut data)?;
Ok(data)
}
fn get_basename(path: &PathBuf) -> Option<&str> {
match path.file_stem() {
Some(stem) => stem.to_str(),
None => None,
}
}
fn main() -> Result<()> {
pretty_env_logger::init();
let opts: Opts = Opts::parse();
match opts.subcmd {
SubCommand::UnXWB(opts) => {
let xwb_data = read_file(&opts.file)?;
let wave_bank = WaveBank::parse(&xwb_data)?;
info!("Opened wave bank “{}” from {:?}", wave_bank.name, opts.file);
match opts.single_entry {
Some(name) => {
let sound = match wave_bank.sounds.get(&name) {
Some(sound) => sound,
None => return error(format!("Entry {} not found in wave bank", name)),
};
let out_file = format!("{}.wav", name);
let mut wav_file = File::create(out_file)?;
wav_file.write_all(&sound.to_wav()?)?;
}
None => {
for (name, sound) in wave_bank.sounds {
if opts.list_entries {
println!("{}", name);
continue;
}
info!("Extracting {}", name);
let out_file = format!("{}.wav", name);
let mut wav_file = File::create(out_file)?;
wav_file.write_all(&sound.to_wav()?)?;
}
}
}
}
SubCommand::DDR2osu(opts) => {
let sound_name = &opts
.sound_name
.unwrap_or(match get_basename(&opts.ssq_file) {
Some(basename) => basename.to_string(),
None => return error(
"Could not extract chart id from file name. Please specify it manually."
.to_string(),
),
});
debug!(
"Converting {:?} and sound {} from {:?} to {:?}",
opts.ssq_file, sound_name, opts.xwb_file, opts.out_file
);
let ssq_data = read_file(&opts.ssq_file)?;
let ssq = SSQ::parse(&ssq_data)?;
let convert_config = opts.convert;
let beatmaps = ssq.to_beatmaps(&convert_config)?;
let xwb_data = read_file(&opts.xwb_file)?;
let wave_bank = WaveBank::parse(&xwb_data)?;
let audio_data = if wave_bank.sounds.contains_key(sound_name) {
wave_bank.sounds.get(sound_name).unwrap().to_wav()?
} else {
return error(format!(
"Could not find sound with chart name {} in wave bank",
sound_name,
));
};
let osz = osu::osz::Archive {
beatmaps,
assets: vec![("audio.wav", &audio_data)],
};
osz.write(&opts.out_file)?;
}
}
Ok(())
}

7
src/osu.rs Normal file
View File

@ -0,0 +1,7 @@
/*
* For documentation on the file format see
* https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)
*/
pub mod beatmap;
pub mod osz;

595
src/osu/beatmap.rs Normal file
View File

@ -0,0 +1,595 @@
use num_derive::ToPrimitive;
use num_traits::ToPrimitive;
use std::fmt;
// Generic Type Aliases
pub type OsuPixel = i16;
pub type DecimalOsuPixel = f32;
pub type SampleIndex = u16;
pub type Time = i32;
// Helper functions
fn bitflags(flags: [bool; 8]) -> u8 {
let mut value = 0u8;
for (i, flag) in flags.iter().enumerate() {
value += ((0b1 as u8) << i) * (*flag as u8) as u8;
}
value
}
fn join_display_values<T: fmt::Display>(iterable: Vec<T>, separator: &'_ str) -> String {
iterable
.iter()
.map(|val| val.to_string())
.collect::<Vec<_>>()
.join(&separator)
}
fn assemble_hit_object_type(hit_object_type: u8, new_combo: bool, skip_combo_colours: U3) -> u8 {
let hit_object_type = 1u8 << hit_object_type;
let new_combo = if new_combo { 0b0000_0010_u8 } else { 0u8 };
let skip_combo_colours = (skip_combo_colours & 0b_0000_0111u8) << 1;
hit_object_type + new_combo + skip_combo_colours
}
pub fn column_to_x(column: u8, columns: u8) -> OsuPixel {
((512 * column as OsuPixel + 256) / columns as OsuPixel) as OsuPixel
}
#[derive(ToPrimitive, Clone)]
pub enum Countdown {
No = 0,
Normal = 1,
Half = 2,
Double = 3,
}
#[derive(ToPrimitive, Clone)]
pub enum Mode {
Normal = 0,
Taiko = 1,
Catch = 2,
Mania = 3,
}
#[derive(ToPrimitive, Debug, Clone)]
pub enum SampleSet {
BeatmapDefault = 0,
Normal = 1,
Soft = 2,
Drum = 3,
}
#[derive(Clone)]
pub struct General {
pub audio_filename: String,
pub audio_lead_in: Time,
pub preview_time: Time,
pub countdown: Countdown,
pub sample_set: SampleSet,
pub mode: Mode,
}
impl fmt::Display for General {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[General]\n\
AudioFilename: {}\n\
AudioLeadIn: {}\n\
PreviewTime: {}\n\
Countdown: {}\n\
SampleSet: {:?}\n\
Mode: {}\n\
",
self.audio_filename,
self.audio_lead_in,
self.preview_time,
ToPrimitive::to_u8(&self.countdown).unwrap(),
self.sample_set,
ToPrimitive::to_u8(&self.mode).unwrap()
)
}
}
#[derive(Clone)]
pub struct Editor {/* stub */}
impl fmt::Display for Editor {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "[Editor]")
}
}
#[derive(Clone)]
pub struct Metadata {
pub title: String,
pub artist: String,
pub creator: String,
pub version: String,
pub source: String,
pub tags: Vec<String>,
}
impl fmt::Display for Metadata {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[Metadata]\n\
Title:{}\n\
Artist:{}\n\
Creator:{}\n\
Version:{}\n\
Source:{}\n\
Tags:{}\n\
",
self.title,
self.artist,
self.creator,
self.version,
self.source,
self.tags.join(" ")
)
}
}
#[derive(Clone)]
pub struct Difficulty {
pub hp_drain_rate: f32,
/// Also is the number of keys in mania
pub circle_size: f32,
pub overall_difficulty: f32,
pub approach_rate: f32,
pub slider_multiplier: f32,
pub slider_tick_rate: f32,
}
impl fmt::Display for Difficulty {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[Difficulty]\n\
HPDrainRate:{}\n\
CircleSize:{}\n\
OverallDifficulty:{}\n\
ApproachRate:{}\n\
SliderMultiplier:{}\n\
SliderTickRate:{}\n\
",
self.hp_drain_rate,
self.circle_size,
self.overall_difficulty,
self.approach_rate,
self.slider_multiplier,
self.slider_tick_rate
)
}
}
#[derive(Clone)]
pub struct Events(pub Vec<Event>);
impl fmt::Display for Events {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[Events]\n\
{}\n\
",
join_display_values(self.0.clone(), "\n")
)
}
}
#[derive(Clone)]
pub enum Event {
Background {
filename: String,
x_offset: OsuPixel,
y_offset: OsuPixel,
},
Video {
start_time: Time,
filename: String,
x_offset: OsuPixel,
y_offset: OsuPixel,
},
Break {
start_time: Time,
end_time: Time,
},
}
impl fmt::Display for Event {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Event::Background {
filename,
x_offset,
y_offset,
} => write!(f, "0,0,{},{},{}", filename, x_offset, y_offset),
Event::Video {
start_time,
filename,
x_offset,
y_offset,
} => write!(
f,
"Video,{},{},{},{}",
start_time, filename, x_offset, y_offset
),
Event::Break {
start_time,
end_time,
} => write!(f, "Break,{},{}", start_time, end_time),
}
}
}
#[derive(Clone)]
pub struct TimingPoints(pub Vec<TimingPoint>);
impl fmt::Display for TimingPoints {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[TimingPoints]\n\
{}\n\
",
join_display_values(self.0.clone(), "\n")
)
}
}
#[derive(Clone)]
pub struct TimingPointEffects {
pub kiai_time: bool,
pub omit_first_barline: bool,
}
impl fmt::Display for TimingPointEffects {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
bitflags([
self.kiai_time,
false,
false,
self.omit_first_barline,
false,
false,
false,
false
])
)
}
}
#[derive(Clone)]
pub struct TimingPoint {
pub time: Time,
pub beat_length: f32,
pub meter: u8,
pub sample_set: SampleSet,
pub sample_index: SampleIndex,
pub volume: u8,
pub uninherited: bool,
pub effects: TimingPointEffects,
}
impl fmt::Display for TimingPoint {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{},{},{},{},{},{},{},{}",
self.time,
self.beat_length,
self.meter,
ToPrimitive::to_u8(&self.sample_set).unwrap(),
self.sample_index,
self.volume,
self.uninherited as u8,
self.effects
)
}
}
#[derive(Clone)]
pub struct Colours(pub Vec<Colour>);
impl fmt::Display for Colours {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[Colours]\n\
{}\n\
",
join_display_values(self.0.clone(), "\n")
)
}
}
#[derive(Clone, Debug)]
pub enum ColourScope {
Combo(u16),
SliderTrackOverride,
SliderBorder,
}
impl fmt::Display for ColourScope {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ColourScope::Combo(i) => write!(f, "Combo{}", i),
_ => write!(f, "{:?}", self),
}
}
}
#[derive(Clone)]
pub struct Colour {
pub scope: ColourScope,
pub colour: [u8; 3],
}
impl fmt::Display for Colour {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{} : {}",
self.scope,
join_display_values(self.colour.to_vec(), ",")
)
}
}
#[derive(Clone)]
pub struct HitSound {
pub normal: bool,
pub whistle: bool,
pub finish: bool,
pub clap: bool,
}
impl fmt::Display for HitSound {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
bitflags([
self.normal,
self.whistle,
self.finish,
self.clap,
false,
false,
false,
false
])
)
}
}
#[derive(Clone)]
pub struct HitSample {
pub normal_set: SampleIndex,
pub addition_set: SampleIndex,
pub index: SampleIndex,
pub volume: u8,
pub filename: String,
}
impl fmt::Display for HitSample {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}:{}:{}:{}:{}",
self.normal_set, self.addition_set, self.index, self.volume, self.filename
)
}
}
// Three bit integer
pub type U3 = u8;
#[derive(Clone)]
pub enum HitObject {
HitCircle {
x: OsuPixel,
y: OsuPixel,
time: Time,
hit_sound: HitSound,
new_combo: bool,
skip_combo_colours: U3,
hit_sample: HitSample,
},
Slider {
x: OsuPixel,
y: OsuPixel,
time: Time,
hit_sound: HitSound,
new_combo: bool,
skip_combo_colours: U3,
curve_type: char,
curve_points: Vec<(DecimalOsuPixel, DecimalOsuPixel)>,
slides: u8,
length: DecimalOsuPixel,
edge_sounds: Vec<SampleIndex>,
edge_sets: Vec<(SampleSet, SampleSet)>,
hit_sample: HitSample,
},
Spinner {
time: Time,
hit_sound: HitSound,
new_combo: bool,
skip_combo_colours: U3,
end_time: Time,
hit_sample: HitSample,
},
Hold {
column: u8,
columns: u8,
time: Time,
hit_sound: HitSound,
new_combo: bool,
skip_combo_colours: U3,
end_time: Time,
hit_sample: HitSample,
},
}
impl fmt::Display for HitObject {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
HitObject::HitCircle {
x,
y,
time,
hit_sound,
new_combo,
skip_combo_colours,
hit_sample,
} => write!(
f,
"{},{},{},{},{},{}",
x,
y,
time,
assemble_hit_object_type(0, *new_combo, *skip_combo_colours),
hit_sound,
hit_sample
),
HitObject::Slider {
x,
y,
time,
hit_sound,
new_combo,
skip_combo_colours,
curve_type,
curve_points,
slides,
length,
edge_sounds,
edge_sets,
hit_sample,
} => write!(
f,
"{},{},{},{},{},{}|{},{},{},{},{},{}",
x,
y,
time,
assemble_hit_object_type(1, *new_combo, *skip_combo_colours),
hit_sound,
curve_type,
curve_points
.iter()
.map(|point| format!("{}:{}", point.0, point.1))
.collect::<Vec<_>>()
.join("|"),
slides,
length,
join_display_values(edge_sounds.clone(), "|"),
edge_sets
.iter()
.map(|set| format!(
"{}:{}",
ToPrimitive::to_u16(&set.0).unwrap(),
ToPrimitive::to_u16(&set.1).unwrap()
))
.collect::<Vec<_>>()
.join("|"),
hit_sample
),
HitObject::Spinner {
time,
hit_sound,
new_combo,
skip_combo_colours,
end_time,
hit_sample,
} => write!(
f,
"256,192,{},{},{},{},{}",
time,
assemble_hit_object_type(3, *new_combo, *skip_combo_colours),
hit_sound,
end_time,
hit_sample
),
HitObject::Hold {
column,
columns,
time,
hit_sound,
new_combo,
skip_combo_colours,
end_time,
hit_sample,
} => write!(
f,
"{},192,{},{},{},{}:{}",
column_to_x(*column, *columns),
time,
assemble_hit_object_type(7, *new_combo, *skip_combo_colours),
hit_sound,
end_time,
hit_sample
),
}
}
}
#[derive(Clone)]
pub struct HitObjects(pub Vec<HitObject>);
impl fmt::Display for HitObjects {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[HitObjects]\n\
{}\n\
",
join_display_values(self.0.clone(), "\n")
)
}
}
pub struct Beatmap {
pub version: u8,
pub general: General,
pub editor: Editor,
pub metadata: Metadata,
pub difficulty: Difficulty,
pub events: Events,
pub timing_points: TimingPoints,
pub colours: Colours,
pub hit_objects: HitObjects,
}
impl fmt::Display for Beatmap {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"osu file format v{}\n\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n",
self.version,
self.general.clone(),
self.editor.clone(),
self.metadata.clone(),
self.difficulty.clone(),
self.events.clone(),
self.timing_points.clone(),
self.colours.clone(),
self.hit_objects.clone()
)
}
}

43
src/osu/osz.rs Normal file
View File

@ -0,0 +1,43 @@
use std::fs::File;
use std::io::{Result, Write};
use std::path::PathBuf;
use zip::write::{FileOptions, ZipWriter};
use crate::osu::beatmap;
pub struct Archive<'a> {
pub beatmaps: Vec<beatmap::Beatmap>,
pub assets: Vec<(&'a str, &'a [u8])>,
}
impl Archive<'_> {
pub fn write(&self, filename: &PathBuf) -> Result<()> {
let file = File::create(filename)?;
let mut zip = ZipWriter::new(file);
for beatmap in &self.beatmaps {
let filename = format!(
"{} - {} ({}) [{}].osu",
beatmap.metadata.artist,
beatmap.metadata.title,
beatmap.metadata.creator,
beatmap.metadata.version
);
let options = FileOptions::default();
zip.start_file(filename, options)?;
zip.write_all(format!("{}", beatmap).as_bytes())?;
}
for asset in &self.assets {
// Assets mostly are already compressed (e.g. JPEG, MP3)
let options = FileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file(asset.0, options)?;
zip.write_all(asset.1)?;
}
zip.finish()?;
Ok(())
}
}

39
src/utils.rs Normal file
View File

@ -0,0 +1,39 @@
use anyhow::{anyhow, Result};
use log::debug;
pub fn get_nth_bit(byte: u8, n: u8) -> bool {
((byte & (0b1 << n)) >> n) != 0
}
pub fn byte_to_bitarray(byte: u8) -> [bool; 8] {
let mut bitarray = [false; 8];
for (i, bit) in bitarray.iter_mut().enumerate() {
*bit = get_nth_bit(byte, i as u8);
}
bitarray
}
pub fn offset_length_to_start_end(offset: usize, length: usize) -> (usize, usize) {
(offset, offset + length)
}
// This probably isnt the right way to do this, but after countless attempts to implement
// error conversion (IResult to anyhow::Result) it was the only thing I could come up with.
pub fn exec_nom_parser<'a, F, R>(func: F, input: &'a [u8]) -> Result<R>
where
F: Fn(&'a [u8]) -> nom::IResult<&[u8], R>,
{
match func(input) {
Ok((unprocessed, result)) => {
if !unprocessed.is_empty() {
debug!(
"Parser returned {} bytes of unprocessed input: {:?}",
unprocessed.len(),
unprocessed
);
}
Ok(result)
}
Err(error) => Err(anyhow!("Nom returned error: {}", error)),
}
}

2
src/xact3.rs Normal file
View File

@ -0,0 +1,2 @@
mod adpcm;
pub mod xwb;

118
src/xact3/adpcm.rs Normal file
View File

@ -0,0 +1,118 @@
use std::io::{Cursor, Write};
use anyhow::Result;
use byteorder::{LittleEndian, WriteBytesExt};
use log::{debug, trace};
#[rustfmt::skip]
const COEFFS: &[CoefSet] = &[
(256, 0),
(512, -256),
( 0, 0),
(192, 64),
(240, 0),
(460, -208),
(392, -232),
];
trait WaveChunk {
fn to_chunk(&self) -> Vec<u8>;
}
type CoefSet = (i16, i16);
pub struct WaveFormat {
// w_format_tag = 2
pub n_channels: u16,
pub n_samples_per_sec: u32,
pub n_avg_bytes_per_sec: u32,
pub n_block_align: u16,
// w_bits_per_sample = 4
// cb_size = 32
pub n_samples_per_block: u16,
// w_num_coeff = 7
// a_coeff = COEFFS
}
impl WaveChunk for WaveFormat {
fn to_chunk(&self) -> Vec<u8> {
let mut buf = Cursor::new(Vec::new());
write!(buf, "fmt ").unwrap();
buf.write_u32::<LittleEndian>(2 + 2 + 4 + 4 + 2 + 2 + 2 + 2 + 2 + 4 * COEFFS.len() as u32)
.unwrap();
buf.write_u16::<LittleEndian>(2).unwrap(); // WAVE_FORMAT_ADPCM
buf.write_u16::<LittleEndian>(self.n_channels).unwrap();
buf.write_u32::<LittleEndian>(self.n_samples_per_sec)
.unwrap();
buf.write_u32::<LittleEndian>(self.n_avg_bytes_per_sec)
.unwrap();
buf.write_u16::<LittleEndian>(self.n_block_align).unwrap();
buf.write_u16::<LittleEndian>(4).unwrap(); // wBitsPerSample
buf.write_u16::<LittleEndian>(32).unwrap(); // cbSize
buf.write_u16::<LittleEndian>(self.n_samples_per_block)
.unwrap();
buf.write_u16::<LittleEndian>(7).unwrap(); // wNumCoeff
for coef_set in COEFFS {
buf.write_i16::<LittleEndian>(coef_set.0).unwrap();
buf.write_i16::<LittleEndian>(coef_set.1).unwrap();
}
buf.into_inner()
}
}
struct WaveFact {
length_samples: u32,
}
impl WaveChunk for WaveFact {
fn to_chunk(&self) -> Vec<u8> {
let mut buf = Cursor::new(Vec::new());
write!(buf, "fact").unwrap();
buf.write_u32::<LittleEndian>(4).unwrap();
buf.write_u32::<LittleEndian>(self.length_samples).unwrap();
buf.into_inner()
}
}
struct RIFFHeader {
file_size: u32,
}
impl WaveChunk for RIFFHeader {
fn to_chunk(&self) -> Vec<u8> {
let mut buf = Cursor::new(Vec::new());
write!(buf, "RIFF").unwrap();
buf.write_u32::<LittleEndian>(self.file_size).unwrap();
write!(buf, "WAVE").unwrap();
buf.into_inner()
}
}
pub fn build_wav(format: WaveFormat, data: &[u8]) -> Result<Vec<u8>> {
debug!("Building file");
let riff_header = RIFFHeader {
file_size: 82 + data.len() as u32,
};
let fact = WaveFact {
length_samples: ((data.len() as u32 / format.n_block_align as u32)
* ((format.n_block_align - (7 * format.n_channels)) * 8) as u32
/ 4)
/ format.n_channels as u32,
};
let mut buf = Cursor::new(Vec::new());
trace!("Building RIFF header");
buf.write_all(&riff_header.to_chunk())?;
trace!("Building fmt chunk");
buf.write_all(&format.to_chunk())?;
trace!("Building fact chunk");
buf.write_all(&fact.to_chunk())?;
write!(buf, "data")?;
buf.write_u32::<LittleEndian>(data.len() as u32).unwrap();
buf.write_all(data)?;
Ok(buf.into_inner())
}

235
src/xact3/xwb.rs Normal file
View File

@ -0,0 +1,235 @@
use std::collections::HashMap;
use std::convert::TryInto;
use anyhow::{anyhow, Result};
use log::{debug, info, warn};
use nom::bytes::complete::tag;
use nom::error::ParseError;
use nom::multi::count;
use nom::number::complete::{le_i32, le_u32};
use nom::{take_str, IResult};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use crate::utils;
use crate::utils::exec_nom_parser;
use crate::xact3::adpcm;
#[derive(Clone, FromPrimitive, Debug, PartialEq)]
enum FormatTag {
PCM = 0,
XMA = 1,
ADPCM = 2,
WMA = 3,
}
#[derive(Clone, Debug)]
struct Format {
tag: FormatTag,
channels: u16,
sample_rate: u32,
alignment: u8,
}
impl From<u32> for Format {
fn from(format: u32) -> Self {
Self {
tag: FormatTag::from_u32(format & ((1 << 2) - 1)).unwrap(),
channels: ((format >> 2) & ((1 << 3) - 1)) as u16,
sample_rate: (format >> 5) & ((1 << 18) - 1),
alignment: ((format >> 23) & ((1 << 8) - 1)) as u8,
}
}
}
impl TryInto<adpcm::WaveFormat> for Format {
type Error = anyhow::Error;
fn try_into(self) -> Result<adpcm::WaveFormat> {
if self.tag != FormatTag::ADPCM {
return Err(anyhow!("Format is not ADPCM"));
}
let n_block_align = (self.alignment as u16 + 22) * self.channels;
let n_samples_per_block =
(((n_block_align - (7 * self.channels)) * 8) / (4 * self.channels)) + 2;
let n_avg_bytes_per_sec =
(self.sample_rate / n_samples_per_block as u32) * n_block_align as u32;
Ok(adpcm::WaveFormat {
n_channels: self.channels,
n_samples_per_sec: self.sample_rate,
n_avg_bytes_per_sec,
n_block_align,
n_samples_per_block,
})
}
}
#[derive(Debug)]
struct SegmentPosition {
start: usize,
end: usize,
}
impl SegmentPosition {
fn get_from<'a>(&self, data: &'a [u8]) -> &'a [u8] {
&data[self.start..self.end]
}
fn parse<'a, E: ParseError<&'a [u8]>>(input: &'a [u8]) -> IResult<&[u8], Self, E> {
let (input, offset) = le_u32(input)?;
let (input, length) = le_u32(input)?;
let (start, end) = utils::offset_length_to_start_end(offset as usize, length as usize);
Ok((input, Self { start, end }))
}
}
struct Header {
segment_positions: Vec<SegmentPosition>,
}
impl Header {
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (input, _magic) = tag("WBND")(input)?;
let (input, version) = le_u32(input)?;
debug!("Recognised file (version {})", version);
if version != 43 {
warn!("The provided file has an unsupported version ({})", version);
}
let (input, _header_version) = le_u32(input)?;
let (_input, segment_positions) = count(SegmentPosition::parse, 5)(input)?;
Ok((
// difference between first segment and parsed bytes of header
&input[8 * 5 + 12..segment_positions[0].start],
Self { segment_positions },
))
}
}
#[derive(Debug)]
struct Info {
entry_count: usize,
name: String,
}
impl Info {
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (input, _flags) = le_u32(input)?;
let (input, entry_count) = le_u32(input)?;
let (input, name) = take_str64(input)?;
let (input, _entry_meta_data_element_size) = le_u32(input)?;
let (input, _entry_name_element_size) = le_u32(input)?;
let (input, _alignment) = le_u32(input)?;
let (input, _compact_format) = le_i32(input)?;
let (input, _build_time) = le_u32(input)?;
Ok((
input,
Self {
entry_count: entry_count as usize,
name,
},
))
}
}
#[derive(Debug)]
struct Entry {
name: String,
format: Format,
data_offset: usize,
data_length: usize,
}
impl Entry {
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (input, _flags_and_duration) = le_u32(input)?;
let (input, format) = le_u32(input)?;
let (input, data_offset) = le_u32(input)?;
let (input, data_length) = le_u32(input)?;
let (input, _loop_start) = le_u32(input)?;
let (input, _loop_length) = le_u32(input)?;
Ok((
input,
Self {
name: "".to_string(),
format: format.into(),
data_offset: data_offset as usize,
data_length: data_length as usize,
},
))
}
}
fn take_str(input: &[u8], len: usize) -> IResult<&[u8], String> {
let (input, parsed) = take_str!(input, len)?;
Ok((input, parsed.replace("\0", "")))
}
fn take_str64(input: &[u8]) -> IResult<&[u8], String> {
take_str(input, 64)
}
#[derive(Debug, Clone)]
pub struct WaveBank<'a> {
pub name: String,
pub sounds: HashMap<String, Sound<'a>>,
}
impl WaveBank<'_> {
pub fn parse(data: &'_ [u8]) -> Result<WaveBank> {
debug!("Parsing header");
let header = exec_nom_parser(Header::parse, data)?;
debug!("Getting segments from file");
let segments: Vec<&'_ [u8]> = header
.segment_positions
.iter()
.map(|x| x.get_from(data))
.collect();
debug!("Parsing info");
let info = exec_nom_parser(Info::parse, segments[0])?;
debug!("Parsing entries");
let entries = exec_nom_parser(count(Entry::parse, info.entry_count as usize), segments[1])?;
debug!("Parsing entry names");
let entry_names =
exec_nom_parser(count(take_str64, info.entry_count as usize), segments[3])?;
let mut wave_bank = WaveBank {
name: info.name,
sounds: HashMap::new(),
};
for (entry, name) in entries.iter().zip(entry_names.iter()) {
let (start, end) =
utils::offset_length_to_start_end(entry.data_offset, entry.data_length);
wave_bank.sounds.insert(
name.replace("\0", "").to_string(),
Sound {
format: entry.format.clone(),
data: &segments[4][start..end],
},
);
}
info!("Parsed WaveBank with {} sounds", wave_bank.sounds.len());
Ok(wave_bank)
}
}
#[derive(Clone, Debug)]
pub struct Sound<'a> {
format: Format,
data: &'a [u8],
}
impl Sound<'_> {
pub fn to_wav(&self) -> Result<Vec<u8>> {
adpcm::build_wav(self.format.clone().try_into()?, self.data)
}
}