diff --git a/.cargo/config-windows.toml b/.cargo/config-windows.toml new file mode 100644 index 0000000..68cf6e0 --- /dev/null +++ b/.cargo/config-windows.toml @@ -0,0 +1,6 @@ +[target.'cfg(target_env = "msvc")'] +rustflags = ["-C", "target-feature=+crt-static"] + +[env] +LIBCLANG_PATH = "" +FFMPEG_DIR = "" diff --git a/.cargo/config.toml b/.cargo/config.toml index 040ecfa..54730c3 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,2 @@ -[target.'cfg(target_env = "msvc")'] -rustflags = ["-C", "target-feature=+crt-static"] +[alias] +build-windows = "build --config .cargo/config-windows.toml" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2261da9..9ff5afb 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,7 +2,7 @@ name: Build on: push: - branches: ["main"] + branches: [main] tags: ["*"] env: @@ -27,34 +27,39 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable - - name: Install nasm - uses: ilammy/setup-nasm@v1 + - name: Install FFmpeg + run: | + if [ "${{ runner.os }}" = "Linux" ]; then + sudo apt-get update + sudo apt-get install libavformat-dev libavcodec-dev libavutil-dev libswresample-dev + elif [ "${{ runner.os }}" = "macOS" ]; then + sudo brew install ffmpeg + elif [ "${{ runner.os }}" = "Windows" ]; then + curl --location https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.1-latest-win64-lgpl-shared-7.1.zip --output ffmpeg.zip + unzip ffmpeg.zip + fi + shell: bash - uses: Swatinem/rust-cache@v2 - name: Build - run: cargo build --release + run: | + export name=mltd-git-$(git describe --tags --dirty)-${{ runner.os }} - - name: Get version (Windows) - if: ${{ contains('Windows', runner.os) }} - id: version_windows - run: echo "version=$(git describe --tags --dirty)" >> $env:GITHUB_OUTPUT + if [ "${{ runner.os }}" = "Windows" ]; then + export FFMPEG_DIR=$PWD/ffmpeg-n7.1-latest-win64-lgpl-shared-7.1 + cargo build-windows --release + export name=$name.exe + else + cargo build --release + fi - - name: Get version - if: ${{ !contains('Windows', runner.os) }} - id: version - run: echo "version=$(git describe --tags --dirty)" >> $GITHUB_OUTPUT + echo "name=$name" >> $GITHUB_OUTPUT - - name: Publish artifact (Windows) - if: ${{ contains('Windows', runner.os) }} - uses: actions/upload-artifact@v4 - with: - name: mltd-git-${{ steps.version_windows.outputs.version }}-Windows.exe - path: target/release/mltd.exe + shell: bash - name: Publish artifact - if: ${{ !contains('Windows', runner.os) }} uses: actions/upload-artifact@v4 with: - name: mltd-git-${{ steps.version.outputs.version }}-${{ runner.os }} - path: target/release/mltd + name: ${{ steps.version.outputs.name }} + path: target/release/mltd-git-* diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 64f8bb2..8b71d1b 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -17,12 +17,14 @@ jobs: with: submodules: true + - name: Install FFmpeg + run: | + sudo apt-get update + sudo apt-get install libavformat-dev libavcodec-dev libavutil-dev libswresample-dev + - name: Install Rust uses: dtolnay/rust-toolchain@nightly - - name: Install nasm - uses: ilammy/setup-nasm@v1 - - uses: Swatinem/rust-cache@v2 - name: Install cargo-binstall diff --git a/.github/workflows/fmt.yaml b/.github/workflows/fmt.yaml index d2dbb4a..12b5f6e 100644 --- a/.github/workflows/fmt.yaml +++ b/.github/workflows/fmt.yaml @@ -22,9 +22,6 @@ jobs: with: components: rustfmt - - name: Install nasm - uses: ilammy/setup-nasm@v1 - - uses: Swatinem/rust-cache@v2 - name: Fmt diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index bb1b44d..a8b16d4 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -17,14 +17,16 @@ jobs: with: submodules: true + - name: Install FFmpeg + run: | + sudo apt-get update + sudo apt-get install libavformat-dev libavcodec-dev libavutil-dev libswresample-dev + - name: Install Rust uses: dtolnay/rust-toolchain@stable with: components: clippy - - name: Install nasm - uses: ilammy/setup-nasm@v1 - - uses: Swatinem/rust-cache@v2 - name: Lint diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8707bb4..28750a1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -17,12 +17,14 @@ jobs: with: submodules: true + - name: Install FFmpeg + run: | + sudo apt-get update + sudo apt-get install libavformat-dev libavcodec-dev libavutil-dev libswresample-dev + - name: Install Rust uses: dtolnay/rust-toolchain@stable - - name: Install nasm - uses: ilammy/setup-nasm@v1 - - uses: Swatinem/rust-cache@v2 - name: Test diff --git a/.gitignore b/.gitignore index 80df833..c321af6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ output/ build/ .vscode/ + +.cargo/config-windows.toml diff --git a/Cargo.lock b/Cargo.lock index b7b66a5..d4ba1d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,6 +113,9 @@ name = "anyhow" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +dependencies = [ + "backtrace", +] [[package]] name = "async-compression" @@ -148,7 +151,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -184,6 +187,24 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.8.0", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.98", +] + [[package]] name = "bindgen" version = "0.71.1" @@ -199,9 +220,9 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 2.1.0", "shlex", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -248,9 +269,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" @@ -319,9 +340,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.9" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" dependencies = [ "jobserver", "libc", @@ -366,9 +387,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.26" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", "clap_derive", @@ -386,9 +407,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.26" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -408,7 +429,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -419,9 +440,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cmake" -version = "0.1.52" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" +checksum = "e24a03c8b52922d68a1589ad61032f2c1aa5a8158d2aa0d93c6e9534944bbad6" dependencies = [ "cc", ] @@ -462,9 +483,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -535,7 +556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -559,7 +580,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -570,7 +591,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -600,7 +621,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -610,7 +631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -621,7 +642,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -696,6 +717,31 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ffmpeg-next" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da02698288e0275e442a47fc12ca26d50daf0d48b15398ba5906f20ac2e2a9f9" +dependencies = [ + "bitflags 2.8.0", + "ffmpeg-sys-next", + "libc", +] + +[[package]] +name = "ffmpeg-sys-next" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc3234d0a4b2f7d083699d0860c6c9dd83713908771b60f94a96f8704adfe45" +dependencies = [ + "bindgen 0.70.1", + "cc", + "libc", + "num_cpus", + "pkg-config", + "vcpkg", +] + [[package]] name = "flate2" version = "1.0.35" @@ -792,7 +838,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -843,7 +889,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", ] [[package]] @@ -931,9 +989,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" [[package]] name = "human_bytes" @@ -949,9 +1007,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", @@ -1134,7 +1192,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1178,9 +1236,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown", @@ -1188,9 +1246,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.17.9" +version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" dependencies = [ "console", "number_prefix", @@ -1210,9 +1268,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is_terminal_polyfill" @@ -1274,7 +1332,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1400,7 +1458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1416,6 +1474,7 @@ dependencies = [ "clap-verbosity-flag", "ctor", "env_logger", + "ffmpeg-next", "futures", "human_bytes", "image", @@ -1434,6 +1493,7 @@ dependencies = [ "reqwest", "rmp-serde", "serde", + "tempfile", "texture2ddecoder", "thiserror", "tokio", @@ -1446,9 +1506,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" dependencies = [ "libc", "log", @@ -1485,7 +1545,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1560,9 +1620,9 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl" -version = "0.10.68" +version = "0.10.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" dependencies = [ "bitflags 2.8.0", "cfg-if", @@ -1581,14 +1641,14 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" @@ -1631,7 +1691,7 @@ checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1683,7 +1743,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -1693,7 +1753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" dependencies = [ "proc-macro2", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1750,20 +1810,20 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ - "libc", "rand_chacha", "rand_core", + "zerocopy 0.8.14", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", @@ -1771,18 +1831,19 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" dependencies = [ - "getrandom", + "getrandom 0.3.1", + "zerocopy 0.8.14", ] [[package]] name = "rand_xoshiro" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" dependencies = [ "rand_core", ] @@ -1891,7 +1952,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -1932,6 +1993,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.0" @@ -1949,9 +2016,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.43" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.8.0", "errno", @@ -1962,9 +2029,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" dependencies = [ "once_cell", "rustls-pki-types", @@ -1984,9 +2051,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] name = "rustls-webpki" @@ -2007,9 +2074,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "schannel" @@ -2045,9 +2112,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" dependencies = [ "serde", ] @@ -2069,14 +2136,14 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -2189,9 +2256,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -2215,7 +2282,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2241,13 +2308,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.15.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", - "getrandom", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2289,7 +2356,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2359,7 +2426,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2513,9 +2580,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-width" @@ -2620,6 +2687,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" name = "vgmstream" version = "0.1.0" dependencies = [ + "bitflags 2.8.0", "num-derive", "num-traits", "thiserror", @@ -2630,7 +2698,7 @@ dependencies = [ name = "vgmstream-sys" version = "0.1.0-vgmstream-r1980-16-g1f3a6b36" dependencies = [ - "bindgen", + "bindgen 0.71.1", "cmake", ] @@ -2649,6 +2717,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -2671,7 +2748,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "wasm-bindgen-shared", ] @@ -2706,7 +2783,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2874,6 +2951,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -2906,7 +2992,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "synstructure", ] @@ -2917,7 +3003,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +dependencies = [ + "zerocopy-derive 0.8.14", ] [[package]] @@ -2928,7 +3023,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", ] [[package]] @@ -2948,7 +3054,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "synstructure", ] @@ -2977,7 +3083,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9b33919..b74b3f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,19 +24,20 @@ version = "2.0.0" [dependencies] aes = { optional = true, version = "0.8.4" } -anyhow = "1.0.95" +anyhow = { features = ["backtrace"], version = "1.0.95" } byteorder = { optional = true, version = "1.5.0" } cbc = { optional = true, version = "0.1.2" } -clap = { features = ["color", "deprecated", "derive", "unicode", "wrap_help"], version = "4.5.26" } +clap = { features = ["color", "deprecated", "derive", "unicode", "wrap_help"], version = "4.5.27" } clap-verbosity-flag = "3.0.2" env_logger = { default-features = false, features = ["humantime"], version = "0.11.6" } +ffmpeg-next = { default-features = false, features = ["codec", "format", "software-resampling"], optional = true, version = "7.1.0" } futures = "0.3.31" human_bytes = { default-features = false, optional = true, version = "0.4.3" } image = { default-features = false, features = ["png"], optional = true, version = "0.25.5" } -indicatif = { default-features = false, version = "0.17.9" } +indicatif = { default-features = false, version = "0.17.11" } lazy-regex = { default-features = false, optional = true, features = ["std", "perf"], version = "3.4.1" } linked-hash-map = { features = ["serde_impl"], version = "0.5.6" } -log = "0.4.22" +log = "0.4.25" num_cpus = { optional = true, version = "1.16.0" } num-derive = { optional = true, version = "0.4.2" } num-traits = { optional = true, version = "0.2.19" } @@ -46,8 +47,9 @@ rayon = { optional = true, version = "1.10.0" } reqwest = { features = ["deflate", "gzip", "json", "stream", "zstd"], version = "0.12.12" } rmp-serde = "1.3.0" serde = { features = ["derive"], version = "1.0.217" } +tempfile = { optional = true, version = "3.16.0" } texture2ddecoder = { optional = true, version = "0.1.1" } -thiserror = "2.0.10" +thiserror = "2.0.11" tokio = { features = ["macros", "rt-multi-thread"], version = "1.43.0" } tokio-util = { features = ["compat"], version = "0.7.13" } vgmstream = { optional = true, path = "crates/vgmstream" } @@ -58,8 +60,8 @@ vergen-gitcl = { version = "1.0.5" } [dev-dependencies] ctor = "0.2.9" -rand = "0.8.5" -rand_xoshiro = "0.6.0" +rand = "0.9.0" +rand_xoshiro = "0.7.0" tokio-test = "0.4.4" [lib] @@ -89,6 +91,7 @@ extract = [ "dep:aes", "dep:byteorder", "dep:cbc", + "dep:ffmpeg-next", "dep:image", "dep:lazy-regex", "dep:num_cpus", @@ -96,5 +99,6 @@ extract = [ "dep:num-traits", "dep:rabex", "dep:rayon", + "dep:tempfile", "dep:texture2ddecoder" ] diff --git a/README.md b/README.md index caad847..c1e668d 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,6 @@ Game asset downloader for THE iDOLM@STER® MILLION LIVE! THEATER DAYS (MLTD), wr ## Usage -> [!NOTE] -> ffmpeg executable is required to be in `$PATH` for asset conversion. - ```console $ ./mltd --help A CLI made for assets in THE iDOLM@STER® MILLION LIVE! THEATER DAYS (MLTD) @@ -33,10 +30,16 @@ Options: -V, --version Print version ``` -## Download +## Get Started -* [Github latest release](https://github.com/nicks96432/mltd-asset-downloader/releases/latest) -* [main branch CI build](https://nightly.link/nicks96432/mltd-asset-downloader/workflows/build.yaml/main) +1. Download executable + * [Github latest release](https://github.com/nicks96432/mltd-asset-downloader/releases/latest) + * [main branch CI build](https://nightly.link/nicks96432/mltd-asset-downloader/workflows/build.yaml/main) +2. Install FFmpeg shared library >= 7.1 version + * [FFmpeg official link](https://www.ffmpeg.org/download.html) + * Make sure FFmpeg shared library is in your `PATH` + * On Windows, you can use `winget` to install FFmpeg so that it will be added to your `PATH` automatically. + For example, `winget install BtbN.FFmpeg.LGPL.Shared.7.1`. See `winget search ffmpeg` for more options. ## Build From Source @@ -46,22 +49,40 @@ The following tools are required: * A rust toolchain ([installation guide](https://www.rust-lang.org/tools/install)) * cmake >= 3.6 (for vgmstream) * clang (for bindgen) ([installation guide](https://rust-lang.github.io/rust-bindgen/requirements.html)) + * remember to set `LIBCLANG_PATH` environment variable on Windows +* pkg-config (Linux/MacOS) +* FFmpeg >= 7.1 shared library in your `PATH` (Windows) or `LD_LIBRARY_PATH` (Linux/MacOS) + * On Windows, `FFMPEG_DIR` should set to the folder where FFmpeg is installed. For example, setting it to `winget` installed + package would be like: + + ```powershell + FFMPEG_DIR = 'C:\Users\username\AppData\Local\Microsoft\WinGet\Packages\BtbN.FFmpeg.LGPL.Shared.7.1_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-n7.1-62-gb168ed9b14-win64-lgpl-shared-7.1' + ``` + +Alternatively, you can set the environment variables in `.cargo/config-windows.toml`. + +To compile (Windows): ```shell -cargo build --release +cargo build-windows --release ``` -The executable will be in the `target/release` directory. +MacOS/Linux: -## Disclaimer +```shell +cargo build --release +``` -None of the repo, the tool, nor the repo owner is affiliated with, or sponsored or authorized by -Bandai Namco Entertainment and Unity Technologies, nor their affiliates or subsidiaries. +The executable will be in the `target/release` directory. ## License Licensed under [MIT](LICENSE). +This software uses code of [FFmpeg](https://ffmpeg.org) licensed under the +[LGPLv2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) and its source can be downloaded +[here](https://github.com/FFmpeg/FFmpeg). + This program is an independent project and is not affiliated with, sponsored by, or endorsed by Bandai Namco Entertainment. The copyright of any output generated by this program belongs to its author. Use or distribution of the program's output must comply with the laws and regulations of the user's jurisdiction. diff --git a/README.zh-TW.md b/README.zh-TW.md index d260dc6..7aac269 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -11,9 +11,6 @@ ## 用法 -> [!NOTE] -> 若要轉換資源格式,ffmpeg必須要在`$PATH`中。 - ```console $ ./mltd --help 偶像大師 百萬人演唱會! 劇場時光 (MLTD) 遊戲資源下載器 @@ -35,8 +32,14 @@ Commands: ## 下載 -* [Github最新版本](https://github.com/nicks96432/mltd-asset-downloader/releases/latest) -* [main自動編譯版](https://nightly.link/nicks96432/mltd-asset-downloader/workflows/build.yaml/main) +1. 下載主程式 + * [Github最新版本](https://github.com/nicks96432/mltd-asset-downloader/releases/latest) + * [main自動編譯版](https://nightly.link/nicks96432/mltd-asset-downloader/workflows/build.yaml/main) +2. 安裝FFmpeg >= 7.1 共享函式庫版 + * [FFmpeg官網連結](https://www.ffmpeg.org/download.html) + * 確保FFmpeg共享函式庫在你的 `PATH` 裡面 + * Windows上可以用 `winget` 來安裝FFmpeg,這樣它就會被自動加進`PATH`裡,例如 + `winget install BtbN.FFmpeg.LGPL.Shared.7.1` 。用 `winget search ffmpeg` 可以查看更多選擇 ## 編譯 @@ -46,22 +49,39 @@ Commands: * rust 編譯工具 ([安裝教學](https://www.rust-lang.org/tools/install)) * cmake >= 3.6 (vgmstream要用到) * clang (bindgen要用到) ([安裝教學](https://rust-lang.github.io/rust-bindgen/requirements.html)) + * Windows上要記得設定 `LIBCLANG_PATH` 環境變數 +* pkg-config (Linux/MacOS) +* 在你的 `PATH` (Windows) 或 `LD_LIBRARY_PATH` (Linux/MacOS) 中有FFmpeg >= 7.1 共享函式庫版 + * Windows上要額外設定 `FFMPEG_DIR` 環境變數為FFmpeg安裝資料夾,例如在Powershell中設定 `winget` 安裝的FFmpeg套件: + + ```powershell + $env:FFMPEG_DIR='C:\Users\username\AppData\Local\Microsoft\WinGet\Packages\BtbN.FFmpeg.LGPL.Shared.7.1_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-n7.1-62-gb168ed9b14-win64-lgpl-shared-7.1' + ``` + +環境變數也可以在 `.cargo/config-windows.toml` 中設定。 + +編譯 (Windows): ```shell -cargo build --release +cargo build-windows --release ``` -執行檔會出現在`target/release`資料夾裡。 +Linux/MacOS: -## 免責聲明 +```shell +cargo build --release +``` -本軟體、工具、以及本軟體的作者與本軟體的repo與萬代南夢宮娛樂、Unity Technologies、以及他們的子公司 -沒有任何關係,也沒有任何贊助或授權關係。 +執行檔會出現在 `target/release` 資料夾裡。 ## 授權條款 本軟體遵守[MIT](LICENSE)授權條款。 +本軟體在[LGPLv2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)授權條款下使用 +[FFmpeg](https://ffmpeg.org)的程式,它的原始碼可以在[這裡](https://github.com/FFmpeg/FFmpeg) +下載。 + 本程式為個人專案,與萬代南夢宮娛樂無關,亦未受其贊助或認可。本程式的任何輸出內容之著作權均屬其作者所有。 使用或散布本程式的輸出內容,須遵循使用者所在地的相關法律規範。 diff --git a/crates/vgmstream/Cargo.toml b/crates/vgmstream/Cargo.toml index ddcc593..ca3350a 100644 --- a/crates/vgmstream/Cargo.toml +++ b/crates/vgmstream/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true repository.workspace = true [dependencies] +bitflags = "2.8.0" num-derive = "0.4.2" num-traits = "0.2.19" thiserror = "2.0.11" diff --git a/crates/vgmstream/src/error.rs b/crates/vgmstream/src/error.rs index dc1eb00..2319383 100644 --- a/crates/vgmstream/src/error.rs +++ b/crates/vgmstream/src/error.rs @@ -1,5 +1,3 @@ -use std::ffi::c_int; - use thiserror::Error; #[derive(Debug, Error)] @@ -7,8 +5,8 @@ pub enum Error { #[error("vgmstream initialization failed")] InitializationFailed, - #[error("invalid sample type: {0}")] - InvalidSampleType(c_int), + #[error("invalid channel mapping: {0}")] + InvalidChannelMapping(u32), #[error("vgmstream generic error")] Generic, diff --git a/crates/vgmstream/src/lib.rs b/crates/vgmstream/src/lib.rs index aa2d15d..1109e0b 100644 --- a/crates/vgmstream/src/lib.rs +++ b/crates/vgmstream/src/lib.rs @@ -3,6 +3,7 @@ mod sf; use std::ffi::CStr; +use bitflags::bitflags; use num_derive::FromPrimitive; use num_traits::FromPrimitive; @@ -24,6 +25,69 @@ impl From for SampleType { } } +bitflags! { + /// The `speaker_t` type in vgmstream. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct Speaker: u32 { + /// front left + const FL = (1 << 0); + /// front right + const FR = (1 << 1); + /// front center + const FC = (1 << 2); + /// low frequency effects + const LFE = (1 << 3); + /// back left + const BL = (1 << 4); + /// back right + const BR = (1 << 5); + /// front left center + const FLC = (1 << 6); + /// front right center + const FRC = (1 << 7); + /// back center + const BC = (1 << 8); + /// side left + const SL = (1 << 9); + /// side right + const SR = (1 << 10); + + /// top center + const TC = (1 << 11); + /// top front left + const TFL = (1 << 12); + /// top front center + const TFC = (1 << 13); + /// top front right + const TFR = (1 << 14); + /// top back left + const TBL = (1 << 15); + /// top back center + const TBC = (1 << 16); + /// top back left + const TBR = (1 << 17); + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ChannelMapping: u32 { + const MONO = Speaker::FC.bits(); + const STEREO = Speaker::FL.bits() | Speaker::FR.bits(); + const _2POINT1 = Speaker::FL.bits() | Speaker::FR.bits() | Speaker::LFE.bits(); + const _2POINT1_XIPH = Speaker::FL.bits() | Speaker::FR.bits() | Speaker::FC.bits(); + const QUAD = Speaker::FL.bits() | Speaker::FR.bits() | Speaker::BL.bits() | Speaker::BR.bits(); + const QUAD_SURROUND = Speaker::FL.bits() | Speaker::FR.bits() | Speaker::FC.bits() | Speaker::BC.bits(); + const QUAD_SIDE = Speaker::FL.bits() | Speaker::FR.bits() | Speaker::SL.bits() | Speaker::SR.bits(); + const _5POINT0 = Speaker::FL.bits() | Speaker::FR.bits() | Speaker::LFE.bits() | Speaker::BL.bits() | Speaker::BR.bits(); + const _5POINT0_XIPH = Speaker::FL.bits() | Speaker::FR.bits() | Speaker::FC.bits() | Speaker::BL.bits() | Speaker::BR.bits(); + const _5POINT0_SURROUND = Speaker::FL.bits() | Speaker::FR.bits() | Speaker::FC.bits() | Speaker::SL.bits() | Speaker::SR.bits(); + const _5POINT1 = Speaker::FL.bits() | Speaker::FR.bits() | Speaker::FC.bits() | Speaker::LFE.bits() | Speaker::BL.bits() | Speaker::BR.bits(); + const _5POINT1_SURROUND = Speaker::FL.bits() | Speaker::FR.bits() | Speaker::FC.bits() | Speaker::LFE.bits() | Speaker::SL.bits() | Speaker::SR.bits(); + const _7POINT0 = Speaker::FL.bits() | Speaker::FR.bits() | Speaker::FC.bits() | Speaker::LFE.bits() | Speaker::BC.bits() | Speaker::FLC.bits() | Speaker::FRC.bits(); + const _7POINT1 = Speaker::FL.bits() | Speaker::FR.bits() | Speaker::FC.bits() | Speaker::LFE.bits() | Speaker::BL.bits() | Speaker::BR.bits() | Speaker::FLC.bits() | Speaker::FRC.bits(); + const _7POINT1_SURROUND = Speaker::FL.bits() | Speaker::FR.bits() | Speaker::FC.bits() | Speaker::LFE.bits() | Speaker::BL.bits() | Speaker::BR.bits() | Speaker::SL.bits() | Speaker::SR.bits(); + } +} + pub struct VgmStream { pub(crate) inner: *mut vgmstream_sys::libvgmstream_t, } @@ -103,7 +167,7 @@ pub struct Format { /* extra info (may be 0 if not known or not relevant) */ /// standard WAVE bitflags - pub channel_layout: u32, + pub channel_layout: ChannelMapping, /// 0 = none, N = loaded subsong N (1=first) pub subsong_index: i32, @@ -261,7 +325,8 @@ impl VgmStream { sample_rate: format.sample_rate, sample_type, sample_size: format.sample_size, - channel_layout: format.channel_layout, + channel_layout: ChannelMapping::from_bits(format.channel_layout) + .ok_or(Error::InvalidChannelMapping(format.channel_layout))?, subsong_index: format.subsong_index, subsong_count: format.subsong_count, input_channels: format.input_channels, diff --git a/crates/vgmstream/src/sf.rs b/crates/vgmstream/src/sf.rs index 2c0290f..7892079 100644 --- a/crates/vgmstream/src/sf.rs +++ b/crates/vgmstream/src/sf.rs @@ -27,7 +27,10 @@ impl StreamFile<'_> { /// let vgmstream = VgmStream::new().unwrap(); /// let stream = StreamFile::open(&vgmstream, "path/to/file").unwrap(); /// ``` - pub fn open>(_: &VgmStream, path: P) -> Result { + pub fn open

(_: &VgmStream, path: &P) -> Result + where + P: AsRef + ?Sized, + { let path = CString::new(path.as_ref().to_string_lossy().as_ref()).unwrap(); let inner = unsafe { vgmstream_sys::libstreamfile_open_from_stdio(path.as_ptr()) }; diff --git a/deny.toml b/deny.toml index 247e61f..84f72b4 100644 --- a/deny.toml +++ b/deny.toml @@ -24,6 +24,7 @@ allow = [ "MIT", "Unicode-3.0", "Zlib", + "WTFPL", ] confidence-threshold = 0.95 diff --git a/src/mltd/error.rs b/src/mltd/error.rs index 2e866f1..1bfe6c2 100644 --- a/src/mltd/error.rs +++ b/src/mltd/error.rs @@ -15,6 +15,14 @@ pub enum Error { #[error("manifest serialization failed: {0}")] ManifestSerialize(#[from] rmp_serde::encode::Error), + /// VGMStream error. + #[error("vgmstream error: {0}")] + VGMStream(#[from] vgmstream::Error), + + /// FFmpeg Error. + #[error("ffmpeg error: {0}")] + FFmpeg(#[from] ffmpeg_next::Error), + /// Reqwest response serialization failed. #[error("response deserialization failed: {0}")] ResponseDeserialize(reqwest::Error), @@ -42,4 +50,8 @@ pub enum Error { /// Thread join failed. #[error("failed to join thread: {0}")] ThreadJoin(#[from] JoinError), + + /// Generic error. + #[error("{0}")] + Generic(String), } diff --git a/src/mltd/extract/class/text_asset.rs b/src/mltd/extract/class/text_asset.rs index 0761c5c..77c225e 100644 --- a/src/mltd/extract/class/text_asset.rs +++ b/src/mltd/extract/class/text_asset.rs @@ -10,15 +10,18 @@ use aes::cipher::block_padding::Pkcs7; use aes::cipher::inout::InOutBufReserved; use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; use aes::Aes192; +use anyhow::Result; use byteorder::{BigEndian, ByteOrder, LittleEndian}; use cbc::{Decryptor, Encryptor}; use rabex::files::SerializedFile; use rabex::objects::classes::TextAsset; use rabex::read_ext::ReadUrexExt; -use vgmstream::{Options, StreamFile, VgmStream}; +use tempfile::tempdir; +use crate::extract::utils::audio::Encoder; use crate::extract::utils::ReadAlignedExt; use crate::extract::version::*; +use crate::extract::ExtractorArgs; pub(super) fn _construct_text_asset( data: &[u8], @@ -63,36 +66,33 @@ pub fn construct_text_asset( pub fn extract_acb

( data: &[u8], output_dir: P, + args: &ExtractorArgs, serialized_file: &SerializedFile, ) -> Result<(), Box> where P: AsRef, { let text_asset = construct_text_asset(data, serialized_file)?; - let output_path = output_dir.as_ref().join(&text_asset.m_Name).with_extension("acb"); - let mut output_file = File::create(&output_path)?; - output_file.write_all(text_asset.m_Script.as_bytes())?; - - let mut vgmstream = VgmStream::new()?; - let sf = StreamFile::open(&vgmstream, &output_path)?; - vgmstream.open_song(&mut Options { - libsf: &sf, - format_id: 0, - stereo_track: 0, - subsong_index: 0, - })?; - - let output_path = output_dir.as_ref().join(&text_asset.m_Name).with_extension("pcm"); - let mut output_file = File::create(&output_path)?; - log::info!("writing audio to: {}", output_path.display()); - while let Ok(buf) = vgmstream.render() { - if buf.is_empty() { - break; - } - - output_file.write_all(buf.as_slice())?; + let temp_dir = tempdir()?; + let acb_path = temp_dir.path().join(&text_asset.m_Name).with_extension("acb"); + let mut acb_file = File::create(&acb_path)?; + acb_file.write_all(text_asset.m_Script.as_bytes())?; + + let output_path = + output_dir.as_ref().join(&text_asset.m_Name).with_extension(&args.audio_format); + + let mut options = ffmpeg_next::Dictionary::new(); + for (key, value) in &args.audio_args { + options.set(key, value); + } + if !args.image_args.is_empty() { + log::debug!("audio options: {:#?}", options); } + let mut encoder = Encoder::open(&acb_path, &output_path, &args.audio_codec, Some(options))?; + + encoder.encode()?; + Ok(()) } diff --git a/src/mltd/extract/mod.rs b/src/mltd/extract/mod.rs index 1ab0f33..fea28de 100644 --- a/src/mltd/extract/mod.rs +++ b/src/mltd/extract/mod.rs @@ -37,15 +37,22 @@ pub struct ExtractorArgs { #[arg(default_value_os_t = [".", "output"].iter().collect())] output: PathBuf, - /// Extension for audio output - #[arg(long, value_name = "EXT", display_order = 2)] + /// Audio output format extension + #[arg(long, value_name = "FORMAT", display_order = 2)] #[arg(default_value_t = String::from("wav"))] - audio_ext: String, + audio_format: String, - /// Arguments to pass to ffmpeg for audio output - #[arg(long, value_name = "ARGS", display_order = 2, hide_default_value = true)] - #[arg(default_value_t = String::from(""))] - audio_args: String, + /// Audio output codec + #[arg(long, value_name = "CODEC", display_order = 2)] + #[arg(default_value_t = String::from("pcm_s16le"))] + audio_codec: String, + + /// Arguments to pass to ffmpeg encoder for audio output + /// + /// Value should be a list of -arg=value pairs separated by commas + #[arg(long, value_name = "ARGS", display_order = 2)] + #[arg(value_parser = parse_key_val::, allow_hyphen_values = true)] + audio_args: Vec<(String, String)>, /// Extension for image output #[arg(long, value_name = "EXT", display_order = 2)] @@ -65,6 +72,22 @@ pub struct ExtractorArgs { // TODO: Add option to extract only specific files } +/// Parse a single key-value pair +fn parse_key_val(s: &str) -> Result<(T, U), Box> +where + T: std::str::FromStr, + T::Err: Error + Send + Sync + 'static, + U: std::str::FromStr, + U::Err: Error + Send + Sync + 'static, +{ + if !s.starts_with('-') { + return Err(format!("invalid -KEY=value: no `-` found in `{s}`").into()); + } + let pos = s.find('=').ok_or_else(|| format!("invalid -KEY=value: no `=` found in `{s}`"))?; + + Ok((s[1..pos].parse()?, s[pos + 1..].parse()?)) +} + pub fn extract_media(args: &ExtractorArgs) -> Result<(), Box> { #[cfg(not(feature = "debug"))] create_dir_all(&args.output)?; @@ -222,8 +245,8 @@ fn extract_object( match object_info.m_ClassID { map::TextAsset => { let text_asset = construct_text_asset(data, serialized_file)?; - match text_asset.m_Name.contains("acb") { - true => extract_acb(data, &output_dir, serialized_file)?, + match text_asset.m_Name.contains(".acb") { + true => extract_acb(data, &output_dir, args, serialized_file).unwrap(), false => { let output_path = output_dir.join(text_asset.m_Name).with_extension("txt"); log::info!("writing text to {}", output_path.display()); diff --git a/src/mltd/extract/utils/audio.rs b/src/mltd/extract/utils/audio.rs new file mode 100644 index 0000000..22c3414 --- /dev/null +++ b/src/mltd/extract/utils/audio.rs @@ -0,0 +1,381 @@ +use std::collections::VecDeque; +use std::ffi::{c_int, c_uint}; +use std::path::Path; + +use ffmpeg_next::packet::Ref; +use ffmpeg_next::Rescale; +use vgmstream::{Options, StreamFile, VgmStream}; + +use crate::Error; + +pub struct Encoder<'a> { + pub options: Option>, + + pub vgmstream: VgmStream, + pub from_channel_layout: ffmpeg_next::ChannelLayout, + pub from_sample_format: ffmpeg_next::format::Sample, + pub from_sample_rate: i32, + + pub encoder: ffmpeg_next::codec::encoder::audio::Encoder, + + pub output: ffmpeg_next::format::context::Output, + + pub resampler: ffmpeg_next::software::resampling::Context, + + pub frame: ffmpeg_next::frame::Audio, + + pub sample_count: i64, + pub next_pts: i64, + + queue: VecDeque, +} + +impl<'a> Encoder<'a> { + pub const DEFAULT_FRAME_SIZE: u32 = 4096; + + pub fn open

( + input: &P, + output: &P, + codec: &str, + options: Option>, + ) -> Result + where + P: AsRef + ?Sized, + { + let mut vgmstream = VgmStream::new()?; + let sf = StreamFile::open(&vgmstream, input)?; + vgmstream.open_song(&mut Options { + libsf: &sf, + format_id: 0, + stereo_track: 0, + subsong_index: 0, + })?; + + let acb_fmt = vgmstream.format()?; + + #[cfg(debug_assertions)] + ffmpeg_next::log::set_level(ffmpeg_next::log::Level::Debug); + + let mut output = match options { + Some(ref o) => ffmpeg_next::format::output_with(output, o.clone()), + None => ffmpeg_next::format::output(output.as_ref()), + }?; + + let codec = ffmpeg_next::encoder::find_by_name(codec) + .ok_or(Error::Generic("Failed to find encoder".to_owned()))?; + + let mut encoder = ffmpeg_next::codec::Context::new_with_codec(codec).encoder().audio()?; + + let supported_formats = get_supported_formats(&encoder)?; + log::debug!("supported formats: {:?}", supported_formats); + + let from_sample_format = to_ffmpeg_sample_format(acb_fmt.sample_type)?; + let from_channel_layout = to_ffmpeg_channel_layout(acb_fmt.channel_layout)?; + + encoder.set_format(choose_format(&supported_formats, from_sample_format)); + encoder.set_bit_rate(320000); + encoder.set_compression(Some(12)); + encoder.set_rate(acb_fmt.sample_rate); + encoder.set_channel_layout(from_channel_layout); + + if output.format().flags().contains(ffmpeg_next::format::Flags::GLOBAL_HEADER) { + let flag = ffmpeg_next::codec::Flags::from_bits( + unsafe { encoder.as_mut_ptr().as_ref() }.unwrap().flags as c_uint, + ) + .unwrap(); + encoder.set_flags(flag | ffmpeg_next::codec::Flags::GLOBAL_HEADER); + } + + let encoder = match options { + Some(ref o) => encoder.open_with(o.clone()), + None => encoder.open(), + }?; + + let _ = output.add_stream_with(&encoder.0 .0 .0)?; + + let frame_size = match encoder + .codec() + .unwrap() + .capabilities() + .intersects(ffmpeg_next::codec::Capabilities::VARIABLE_FRAME_SIZE) + { + true => { + log::debug!("variable frame size detected, using default frame size"); + Self::DEFAULT_FRAME_SIZE + } + false => encoder.frame_size(), + } as usize; + + let mut frame = + ffmpeg_next::frame::Audio::new(encoder.format(), frame_size, encoder.channel_layout()); + frame.set_pts(Some(0)); + frame.set_rate(encoder.rate()); + + let resampler = ffmpeg_next::software::resampler( + (from_sample_format, from_channel_layout, acb_fmt.sample_rate as u32), + (encoder.format(), encoder.channel_layout(), encoder.rate()), + )?; + + Ok(Self { + options, + vgmstream, + from_channel_layout, + from_sample_format, + from_sample_rate: acb_fmt.sample_rate, + encoder, + output, + resampler, + frame, + + sample_count: 0, + next_pts: 0, + + queue: VecDeque::new(), + }) + } + + fn write_frame(&mut self, eof: bool) -> Result { + match eof { + false => self.encoder.send_frame(&self.frame), + true => self.encoder.send_eof(), + }?; + + loop { + let mut packet = ffmpeg_next::Packet::empty(); + if let Err(e) = self.encoder.receive_packet(&mut packet) { + let errno = c_int::from(e); + if errno == ffmpeg_next::ffi::AVERROR(ffmpeg_next::ffi::EAGAIN) { + return Ok(true); + } + if errno == ffmpeg_next::ffi::AVERROR_EOF { + return Ok(false); + } + + return Err(Error::FFmpeg(e)); + } + + packet.rescale_ts( + self.encoder.time_base(), + self.output.stream_mut(0).unwrap().time_base(), + ); + packet.set_stream(0); + + // XXX: packet.write() and packet.write_interleved() checks that the packet + // is not empty, but empty packet with side data is valid. + match unsafe { + ffmpeg_next::ffi::av_interleaved_write_frame( + self.output.as_mut_ptr(), + packet.as_ptr() as *mut _, + ) + } { + 0 => Ok(()), + e => Err(ffmpeg_next::Error::from(e)), + }?; + } + } + + /// Gets the next audio frame from vgmstream decoder. + /// + /// Returns `None` if there is no more audio data. + fn get_audio_frame(&mut self) -> Option { + let needed_len = self.frame.samples() + * self.from_channel_layout.channels() as usize + * self.from_sample_format.bytes(); + + while let Ok(buf) = self.vgmstream.render() { + if buf.is_empty() { + break; + } + + self.queue.extend(buf); + if self.queue.len() >= needed_len { + break; + } + } + + let samples = match self.queue.len() { + 0 => return None, + len if len < needed_len => std::mem::take(&mut self.queue).into(), + _ => { + let mut rest = self.queue.split_off(needed_len); + std::mem::swap(&mut rest, &mut self.queue); + + Vec::from(rest) + } + }; + let frame_size = samples.len() + / (self.from_sample_format.bytes() * self.from_channel_layout.channels() as usize); + + let mut frame = ffmpeg_next::frame::Audio::new( + self.from_sample_format, + frame_size, + self.from_channel_layout, + ); + + frame.data_mut(0)[..samples.len()].copy_from_slice(&samples); + frame.data_mut(0)[samples.len()..].fill(0); + + frame.set_rate(self.from_sample_rate as u32); + frame.set_pts(Some(self.next_pts)); + self.next_pts += frame_size as i64; + + Some(frame) + } + + /// Encodes the next audio frame. + fn write_audio_frame(&mut self) -> Result { + if let Some(frame) = self.get_audio_frame() { + assert_eq!(self.resampler.delay(), None, "there should be no delay"); + + self.resampler.run(&frame, &mut self.frame)?; + + let pts = self.sample_count.rescale( + ffmpeg_next::Rational::new(1, self.encoder.rate() as i32), + self.encoder.time_base(), + ); + self.frame.set_pts(Some(pts)); + self.frame.set_samples(frame.samples()); + self.sample_count += self.frame.samples() as i64; + + return self.write_frame(false); + } + + loop { + if let Ok(None) = self.resampler.flush(&mut self.frame) { + break; + } + + let pts = self.frame.pts().unwrap().rescale( + ffmpeg_next::Rational::new(1, self.encoder.rate() as i32), + self.encoder.time_base(), + ); + self.frame.set_pts(Some(pts)); + self.sample_count += self.frame.samples() as i64; + + log::debug!("flushed {} samples", self.frame.samples()); + self.write_frame(false)?; + } + + self.write_frame(true) + } + + pub fn encode(&mut self) -> Result<(), Error> { + match self.options { + Some(ref o) => { + let _ = self.output.write_header_with(o.clone())?; + } + None => self.output.write_header()?, + }; + + while self.write_audio_frame()? {} + + self.output.write_trailer()?; + + Ok(()) + } +} + +fn to_ffmpeg_sample_format( + value: vgmstream::SampleType, +) -> Result { + match value { + vgmstream::SampleType::Pcm16 => { + Ok(ffmpeg_next::format::Sample::I16(ffmpeg_next::format::sample::Type::Packed)) + } + vgmstream::SampleType::Pcm32 => { + Ok(ffmpeg_next::format::Sample::I32(ffmpeg_next::format::sample::Type::Packed)) + } + vgmstream::SampleType::Float => { + Ok(ffmpeg_next::format::Sample::F32(ffmpeg_next::format::sample::Type::Packed)) + } + _ => Err(Error::Generic(format!("Unsupported sample type: {:?}", value))), + } +} + +fn to_ffmpeg_channel_layout( + value: vgmstream::ChannelMapping, +) -> Result { + match value { + vgmstream::ChannelMapping::MONO => Ok(ffmpeg_next::ChannelLayout::MONO), + vgmstream::ChannelMapping::STEREO => Ok(ffmpeg_next::ChannelLayout::STEREO), + vgmstream::ChannelMapping::_2POINT1 => Ok(ffmpeg_next::ChannelLayout::_2POINT1), + vgmstream::ChannelMapping::_2POINT1_XIPH => Ok(ffmpeg_next::ChannelLayout::SURROUND), + vgmstream::ChannelMapping::QUAD => Ok(ffmpeg_next::ChannelLayout::QUAD), + vgmstream::ChannelMapping::QUAD_SURROUND => Ok(ffmpeg_next::ChannelLayout::_4POINT0), + vgmstream::ChannelMapping::QUAD_SIDE => Ok(ffmpeg_next::ChannelLayout::_2_2), + vgmstream::ChannelMapping::_5POINT0_XIPH => Ok(ffmpeg_next::ChannelLayout::_5POINT0_BACK), + vgmstream::ChannelMapping::_5POINT0_SURROUND => Ok(ffmpeg_next::ChannelLayout::_5POINT0), + vgmstream::ChannelMapping::_5POINT1 => Ok(ffmpeg_next::ChannelLayout::_5POINT1_BACK), + vgmstream::ChannelMapping::_5POINT1_SURROUND => Ok(ffmpeg_next::ChannelLayout::_5POINT1), + vgmstream::ChannelMapping::_7POINT1 => Ok(ffmpeg_next::ChannelLayout::_7POINT1_WIDE_BACK), + vgmstream::ChannelMapping::_7POINT1_SURROUND => { + Ok(ffmpeg_next::ChannelLayout::_7POINT1_WIDE) + } + _ => Err(Error::Generic(format!("Unsupported channel layout: {:?}", value))), + } +} + +/// Returns a list of supported audio formats. +/// +/// XXX: In the next version of FFmpeg, this function will be removed. Use +/// `get_supported_formats_new` below. +fn get_supported_formats( + encoder: &ffmpeg_next::codec::encoder::Encoder, +) -> Result, Error> { + match encoder.codec().unwrap().audio()?.formats() { + Some(f) => Ok(f.collect()), + None => Err(Error::Generic("no supported audio formats found".to_owned())), + } +} + +/* +fn get_supported_formats_new( + encoder: &ffmpeg_next::codec::encoder::Encoder, +) -> Result, Error> { + let mut supported_formats = std::ptr::null(); + let mut num_formats = 0; + unsafe { + ffmpeg_next::ffi::avcodec_get_supported_config( + encoder.as_ptr(), + std::ptr::null(), + ffmpeg_next::ffi::AVCodecConfig::AV_CODEC_CONFIG_SAMPLE_FORMAT, + 0, + &mut supported_formats, + &mut num_formats, + ) + }; + if supported_formats.is_null() { + return Err(Error::Generic("Failed to get supported configs".to_owned())); + } + + Ok(unsafe { + std::slice::from_raw_parts( + supported_formats as *const ffmpeg_next::ffi::AVSampleFormat, + num_formats as usize, + ) + } + .iter() + .map(|fmt| (*fmt).into()) + .collect()) +} +*/ + +fn choose_format( + supported_formats: &[ffmpeg_next::format::Sample], + wanted_format: ffmpeg_next::format::Sample, +) -> ffmpeg_next::format::Sample { + if supported_formats.contains(&wanted_format) { + return wanted_format; + } + + // Try to find the closest supported format + let closest = supported_formats + .iter() + .map(|fmt| ((wanted_format.bytes() as i32 - fmt.bytes() as i32).abs(), fmt)) + .min_by_key(|(diff, _)| *diff) + .unwrap(); + + log::debug!("original sample format not supported, using closest: {:?}", closest.1); + + *closest.1 +} diff --git a/src/mltd/extract/utils/mod.rs b/src/mltd/extract/utils/mod.rs index 1e84ad1..e02fc41 100644 --- a/src/mltd/extract/utils/mod.rs +++ b/src/mltd/extract/utils/mod.rs @@ -1,3 +1,4 @@ +pub mod audio; mod puzzle; mod read_ext; diff --git a/src/mltd/util.rs b/src/mltd/util.rs index 1a6c883..d48a903 100644 --- a/src/mltd/util.rs +++ b/src/mltd/util.rs @@ -95,15 +95,15 @@ pub(crate) use init_test_logger; pub(crate) mod test_util { use std::io::Cursor; - use rand::distributions::uniform::{SampleRange, SampleUniform}; - use rand::{thread_rng, Rng, SeedableRng}; + use rand::distr::uniform::{SampleRange, SampleUniform}; + use rand::{rng, Rng, SeedableRng}; use rand_xoshiro::Xoshiro256PlusPlus as MyRng; pub fn rand_ascii_string(len: usize) -> Cursor> { - let mut rng = MyRng::from_rng(thread_rng()).unwrap(); + let mut rng = MyRng::from_rng(&mut rng()); let mut buf = vec![0u8; len]; for byte in buf.iter_mut().take(len) { - *byte = u8::try_from(rng.gen_range(0x33..0x7f)).unwrap(); // printable ascii + *byte = u8::try_from(rng.random_range(0x33..0x7f)).unwrap(); // printable ascii } buf.push(0u8); @@ -115,8 +115,8 @@ pub(crate) mod test_util { T: SampleUniform, R: SampleRange, { - let mut rng = MyRng::from_rng(thread_rng()).unwrap(); + let mut rng = MyRng::from_rng(&mut rng()); - rng.gen_range(range) + rng.random_range(range) } }