Initial commit
This commit is contained in:
commit
8319ee42d9
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/testfiles
|
568
Cargo.lock
generated
Normal file
568
Cargo.lock
generated
Normal 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
16
Cargo.toml
Normal 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
15
LICENSE
Normal 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
87
README.md
Normal 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 Auriemma’s unxwb][unxwb] (<kbd>Ctrl</kbd>+<kbd>F</kbd> unxwb).
|
||||
|
||||
## About this project
|
||||
|
||||
This is my first rust project. Don’t 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
59
batch_convert.sh
Executable 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
1
src/converter.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod ddr2osu;
|
421
src/converter/ddr2osu.rs
Normal file
421
src/converter/ddr2osu.rs
Normal 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
1
src/ddr.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod ssq;
|
420
src/ddr/ssq.rs
Normal file
420
src/ddr/ssq.rs
Normal 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
5
src/lib.rs
Normal 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
179
src/main.rs
Normal 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
7
src/osu.rs
Normal 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
595
src/osu/beatmap.rs
Normal 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
43
src/osu/osz.rs
Normal 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
39
src/utils.rs
Normal 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 isn’t 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
2
src/xact3.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
mod adpcm;
|
||||
pub mod xwb;
|
118
src/xact3/adpcm.rs
Normal file
118
src/xact3/adpcm.rs
Normal 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
235
src/xact3/xwb.rs
Normal 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)
|
||||
}
|
||||
}
|
Reference in a new issue