Add batch conversion mode
This commit is contained in:
parent
d8af20a703
commit
e7c84aed9c
50
Cargo.lock
generated
50
Cargo.lock
generated
|
@ -55,6 +55,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"num-derive",
|
"num-derive",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"pbr",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"quickcheck",
|
"quickcheck",
|
||||||
|
@ -118,6 +119,27 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
"maybe-uninit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"cfg-if",
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
@ -216,6 +238,12 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "maybe-uninit"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.3.3"
|
version = "2.3.3"
|
||||||
|
@ -257,6 +285,18 @@ version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06de47b848347d8c4c94219ad8ecd35eb90231704b067e67e6ae2e36ee023510"
|
checksum = "06de47b848347d8c4c94219ad8ecd35eb90231704b067e67e6ae2e36ee023510"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbr"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "74333e3d1d8bced07fd0b8599304825684bcdb4a1fcc6fa6a470e6e08cefd254"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"libc",
|
||||||
|
"time",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "podio"
|
name = "podio"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
|
@ -525,6 +565,16 @@ dependencies = [
|
||||||
"lazy_static",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
|
|
@ -12,6 +12,7 @@ konami-lz77 = { git = "https://github.com/sbruder/konami-lz77" }
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
num-derive = "0.3"
|
num-derive = "0.3"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
|
pbr = "1.0.3"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
quick-xml = { version = "0.18", features = [ "serialize" ] }
|
quick-xml = { version = "0.18", features = [ "serialize" ] }
|
||||||
serde = { version = "1.0", features = [ "derive" ] }
|
serde = { version = "1.0", features = [ "derive" ] }
|
||||||
|
|
252
src/main.rs
252
src/main.rs
|
@ -1,3 +1,4 @@
|
||||||
|
use std::convert::TryInto;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
@ -5,7 +6,8 @@ use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use clap::Clap;
|
use clap::Clap;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
|
use pbr::ProgressBar;
|
||||||
use tabwriter::TabWriter;
|
use tabwriter::TabWriter;
|
||||||
|
|
||||||
use brd::converter;
|
use brd::converter;
|
||||||
|
@ -46,6 +48,12 @@ enum SubCommand {
|
||||||
display_order = 1
|
display_order = 1
|
||||||
)]
|
)]
|
||||||
DDR2osu(Box<DDR2osu>),
|
DDR2osu(Box<DDR2osu>),
|
||||||
|
#[clap(
|
||||||
|
name = "ddr2osu-batch",
|
||||||
|
about = "Batch version of ddr2osu",
|
||||||
|
display_order = 1
|
||||||
|
)]
|
||||||
|
BatchDDR2osu(BatchDDR2osu),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clap)]
|
#[derive(Clap)]
|
||||||
|
@ -110,11 +118,50 @@ struct DDR2osu {
|
||||||
musicdb_file: Option<PathBuf>,
|
musicdb_file: Option<PathBuf>,
|
||||||
#[clap(
|
#[clap(
|
||||||
short = "n",
|
short = "n",
|
||||||
name = "sound name",
|
name = "basename",
|
||||||
about = "Sound in wave bank, otherwise inferred from SSQ filename",
|
about = "Sound in wave bank, otherwise inferred from SSQ filename",
|
||||||
display_order = 2
|
display_order = 2
|
||||||
)]
|
)]
|
||||||
sound_name: Option<String>,
|
basename: Option<String>,
|
||||||
|
#[clap(flatten)]
|
||||||
|
convert: converter::ddr2osu::Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clap)]
|
||||||
|
|
||||||
|
struct BatchDDR2osu {
|
||||||
|
#[clap(
|
||||||
|
short = "s",
|
||||||
|
long = "ssq",
|
||||||
|
name = "ssq_dir",
|
||||||
|
about = "directory with DDR step chart files",
|
||||||
|
display_order = 1
|
||||||
|
)]
|
||||||
|
ssq_dir: PathBuf,
|
||||||
|
#[clap(
|
||||||
|
short = "x",
|
||||||
|
long = "xwb",
|
||||||
|
name = "xwb_dir",
|
||||||
|
about = "directory with XAC3 wave bank files",
|
||||||
|
display_order = 1
|
||||||
|
)]
|
||||||
|
xwb_dir: PathBuf,
|
||||||
|
#[clap(
|
||||||
|
short = "o",
|
||||||
|
long = "out",
|
||||||
|
name = "out_dir",
|
||||||
|
about = "output directory",
|
||||||
|
display_order = 1
|
||||||
|
)]
|
||||||
|
out_dir: PathBuf,
|
||||||
|
#[clap(
|
||||||
|
short = "m",
|
||||||
|
long = "musicdb",
|
||||||
|
name = "musicdb.xml|startup.arc",
|
||||||
|
about = "musicdb.xml or startup.arc for metadata",
|
||||||
|
display_order = 1
|
||||||
|
)]
|
||||||
|
musicdb_file: PathBuf,
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
convert: converter::ddr2osu::Config,
|
convert: converter::ddr2osu::Config,
|
||||||
}
|
}
|
||||||
|
@ -150,6 +197,75 @@ fn read_musicdb(path: &PathBuf) -> Result<musicdb::MusicDB> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ddr2osu(
|
||||||
|
ssq_file: PathBuf,
|
||||||
|
xwb_file: PathBuf,
|
||||||
|
out_file: PathBuf,
|
||||||
|
basename: String,
|
||||||
|
convert_options: converter::ddr2osu::Config,
|
||||||
|
) -> Result<()> {
|
||||||
|
debug!(
|
||||||
|
"Converting {} and sound {} from {} to {}",
|
||||||
|
ssq_file.display(),
|
||||||
|
basename,
|
||||||
|
xwb_file.display(),
|
||||||
|
out_file.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let ssq_data = fs::read(&ssq_file)
|
||||||
|
.with_context(|| format!("failed to read SSQ file {}", &ssq_file.display()))?;
|
||||||
|
let ssq = SSQ::parse(&ssq_data).context("failed to parse SSQ file")?;
|
||||||
|
|
||||||
|
let beatmaps = ssq
|
||||||
|
.to_beatmaps(&convert_options)
|
||||||
|
.context("failed to convert DDR step chart to osu!mania beatmap")?;
|
||||||
|
|
||||||
|
let xwb_data = fs::read(&xwb_file)
|
||||||
|
.with_context(|| format!("failed to read XWB file {}", &xwb_file.clone().display()))?;
|
||||||
|
let wave_bank = WaveBank::parse(&xwb_data).context("failed to parse XWB file")?;
|
||||||
|
|
||||||
|
let audio_data = if wave_bank.sounds.contains_key(&basename) {
|
||||||
|
wave_bank
|
||||||
|
.sounds
|
||||||
|
.get(&basename)
|
||||||
|
.unwrap()
|
||||||
|
.to_wav()
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to convert wave bank sound entry “{}” to WAV",
|
||||||
|
basename
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
} else if wave_bank.sounds.len() == 2 {
|
||||||
|
warn!(
|
||||||
|
"Sound {} not found in wave bank, but it has two entries; assuming these are preview and full song",
|
||||||
|
basename
|
||||||
|
);
|
||||||
|
let mut sounds = wave_bank.sounds.values().collect::<Vec<&XWBSound>>();
|
||||||
|
sounds.sort_unstable_by(|a, b| b.size.cmp(&a.size));
|
||||||
|
sounds[0].to_wav().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to convert wave bank sound entry “{}” to WAV",
|
||||||
|
basename
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Could not find matching sound in wave bank (searched for {})",
|
||||||
|
basename,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let osz = osu::osz::Archive {
|
||||||
|
beatmaps,
|
||||||
|
assets: vec![("audio.wav", &audio_data)],
|
||||||
|
};
|
||||||
|
osz.write(&out_file)
|
||||||
|
.with_context(|| format!("failed to write OSZ file to {}", out_file.display()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
|
@ -255,37 +371,23 @@ fn main() -> Result<()> {
|
||||||
tw.flush()?;
|
tw.flush()?;
|
||||||
}
|
}
|
||||||
SubCommand::DDR2osu(opts) => {
|
SubCommand::DDR2osu(opts) => {
|
||||||
let sound_name =
|
let basename = opts.basename.clone().unwrap_or(
|
||||||
&opts
|
get_basename(&opts.ssq_file)
|
||||||
.sound_name
|
.map(|basename| basename.to_string())
|
||||||
.clone()
|
.ok_or_else(|| {
|
||||||
.unwrap_or(match get_basename(&opts.ssq_file) {
|
anyhow!(
|
||||||
Some(basename) => basename.to_string(),
|
"Could not extract chart id from file name. Please specify it manually."
|
||||||
None => {
|
)
|
||||||
return Err(anyhow!(
|
})?,
|
||||||
"Could not extract chart id from file name. Please specify it manually."))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Converting {} and sound {} from {} to {}",
|
|
||||||
opts.ssq_file.display(),
|
|
||||||
sound_name,
|
|
||||||
opts.xwb_file.display(),
|
|
||||||
opts.out_file.display()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let ssq_data = fs::read(&opts.ssq_file)
|
let mut convert_options = opts.convert;
|
||||||
.with_context(|| format!("failed to read SSQ file {}", &opts.ssq_file.display()))?;
|
|
||||||
let ssq = SSQ::parse(&ssq_data).context("failed to parse SSQ file")?;
|
|
||||||
|
|
||||||
let mut convert_options = opts.convert.clone();
|
|
||||||
|
|
||||||
if let Some(musicdb_file) = &opts.musicdb_file {
|
if let Some(musicdb_file) = &opts.musicdb_file {
|
||||||
debug!("Reading metadata from {}", musicdb_file.display());
|
debug!("Reading metadata from {}", musicdb_file.display());
|
||||||
let musicdb = read_musicdb(&musicdb_file)?;
|
let musicdb = read_musicdb(&musicdb_file)?;
|
||||||
let musicdb_entry = musicdb
|
let musicdb_entry = musicdb
|
||||||
.get_entry_from_basename(sound_name)
|
.get_entry_from_basename(&basename)
|
||||||
.ok_or_else(|| anyhow!("Entry not found in musicdb"))?;
|
.ok_or_else(|| anyhow!("Entry not found in musicdb"))?;
|
||||||
if convert_options.metadata.title.is_none() {
|
if convert_options.metadata.title.is_none() {
|
||||||
info!("Using title from musicdb: “{}”", musicdb_entry.title);
|
info!("Using title from musicdb: “{}”", musicdb_entry.title);
|
||||||
|
@ -297,60 +399,58 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
convert_options.metadata.levels = Some(musicdb_entry.diff_lv.clone());
|
convert_options.metadata.levels = Some(musicdb_entry.diff_lv.clone());
|
||||||
} else if convert_options.metadata.title.is_none() {
|
} else if convert_options.metadata.title.is_none() {
|
||||||
convert_options.metadata.title = Some(sound_name.to_string());
|
convert_options.metadata.title = Some(basename.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let beatmaps = ssq
|
ddr2osu(
|
||||||
.to_beatmaps(&convert_options)
|
opts.ssq_file,
|
||||||
.context("failed to convert DDR step chart to osu!mania beatmap")?;
|
opts.xwb_file,
|
||||||
|
opts.out_file,
|
||||||
|
basename,
|
||||||
|
convert_options,
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
SubCommand::BatchDDR2osu(opts) => {
|
||||||
|
let musicdb = read_musicdb(&opts.musicdb_file)?;
|
||||||
|
|
||||||
let xwb_data = fs::read(&opts.xwb_file).with_context(|| {
|
fs::create_dir_all(&opts.out_dir)?;
|
||||||
format!(
|
|
||||||
"failed to read XWB file {}",
|
|
||||||
&opts.xwb_file.clone().display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let wave_bank = WaveBank::parse(&xwb_data).context("failed to parse XWB file")?;
|
|
||||||
|
|
||||||
let audio_data = if wave_bank.sounds.contains_key(sound_name) {
|
let mut pb = ProgressBar::new(musicdb.music.len().try_into()?);
|
||||||
wave_bank
|
for entry in musicdb.music {
|
||||||
.sounds
|
pb.message(&format!("{} ", entry.basename));
|
||||||
.get(sound_name)
|
pb.tick();
|
||||||
.unwrap()
|
|
||||||
.to_wav()
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to convert wave bank sound entry “{}” to WAV",
|
|
||||||
sound_name
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
} else if wave_bank.sounds.len() == 2 {
|
|
||||||
warn!(
|
|
||||||
"Sound {} not found in wave bank, but it has two entries; assuming these are preview and full song",
|
|
||||||
sound_name
|
|
||||||
);
|
|
||||||
let mut sounds = wave_bank.sounds.values().collect::<Vec<&XWBSound>>();
|
|
||||||
sounds.sort_unstable_by(|a, b| b.size.cmp(&a.size));
|
|
||||||
sounds[0].to_wav().with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to convert wave bank sound entry “{}” to WAV",
|
|
||||||
sound_name
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
} else {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"Could not find matching sound in wave bank (searched for {})",
|
|
||||||
sound_name,
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
let osz = osu::osz::Archive {
|
let mut ssq_file = opts.ssq_dir.clone();
|
||||||
beatmaps,
|
ssq_file.push(&entry.basename);
|
||||||
assets: vec![("audio.wav", &audio_data)],
|
ssq_file.set_extension("ssq");
|
||||||
};
|
let mut xwb_file = opts.xwb_dir.clone();
|
||||||
osz.write(&opts.out_file).with_context(|| {
|
xwb_file.push(&entry.basename);
|
||||||
format!("failed to write OSZ file to {}", opts.out_file.display())
|
xwb_file.set_extension("xwb");
|
||||||
})?;
|
let mut out_file = opts.out_dir.clone();
|
||||||
|
out_file.push(format!("{} - {}.osz", entry.artist, entry.title));
|
||||||
|
|
||||||
|
let mut convert_options = opts.convert.clone();
|
||||||
|
|
||||||
|
convert_options.metadata.title = Some(entry.title.clone());
|
||||||
|
convert_options.metadata.artist = Some(entry.artist.clone());
|
||||||
|
convert_options.metadata.levels = Some(entry.diff_lv.clone());
|
||||||
|
|
||||||
|
ddr2osu(
|
||||||
|
ssq_file,
|
||||||
|
xwb_file,
|
||||||
|
out_file,
|
||||||
|
entry.basename.clone(),
|
||||||
|
convert_options,
|
||||||
|
)
|
||||||
|
.unwrap_or_else(move |err| {
|
||||||
|
error!(
|
||||||
|
"Could not convert {} ({}), continuing anyway",
|
||||||
|
entry.basename, err
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
pb.inc();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
Reference in a new issue