Add batch conversion mode

This commit is contained in:
Simon Bruder 2020-06-30 11:58:08 +02:00
parent d8af20a703
commit e7c84aed9c
No known key found for this signature in database
GPG key ID: 6F03E0000CC5B62F
3 changed files with 226 additions and 75 deletions

50
Cargo.lock generated
View file

@ -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"

View file

@ -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" ] }

View file

@ -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 {}", let mut pb = ProgressBar::new(musicdb.music.len().try_into()?);
&opts.xwb_file.clone().display() for entry in musicdb.music {
pb.message(&format!("{} ", entry.basename));
pb.tick();
let mut ssq_file = opts.ssq_dir.clone();
ssq_file.push(&entry.basename);
ssq_file.set_extension("ssq");
let mut xwb_file = opts.xwb_dir.clone();
xwb_file.push(&entry.basename);
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| {
let wave_bank = WaveBank::parse(&xwb_data).context("failed to parse XWB file")?; error!(
"Could not convert {} ({}), continuing anyway",
let audio_data = if wave_bank.sounds.contains_key(sound_name) { entry.basename, err
wave_bank
.sounds
.get(sound_name)
.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 { pb.inc();
beatmaps, }
assets: vec![("audio.wav", &audio_data)],
};
osz.write(&opts.out_file).with_context(|| {
format!("failed to write OSZ file to {}", opts.out_file.display())
})?;
} }
} }
Ok(()) Ok(())