diff --git a/Cargo.lock b/Cargo.lock index 6d9029a..4dddba9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,6 +55,7 @@ dependencies = [ "log", "num-derive", "num-traits", + "pbr", "pretty_env_logger", "quick-xml", "quickcheck", @@ -118,6 +119,27 @@ dependencies = [ "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]] name = "env_logger" version = "0.7.1" @@ -216,6 +238,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "memchr" version = "2.3.3" @@ -257,6 +285,18 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "podio" version = "0.1.7" @@ -525,6 +565,16 @@ 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" diff --git a/Cargo.toml b/Cargo.toml index a911344..db6154e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ konami-lz77 = { git = "https://github.com/sbruder/konami-lz77" } log = "0.4.8" num-derive = "0.3" num-traits = "0.2" +pbr = "1.0.3" pretty_env_logger = "0.4" quick-xml = { version = "0.18", features = [ "serialize" ] } serde = { version = "1.0", features = [ "derive" ] } diff --git a/src/main.rs b/src/main.rs index d42520a..ae4f58b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use std::convert::TryInto; use std::fs; use std::io; use std::io::Write; @@ -5,7 +6,8 @@ use std::path::PathBuf; use anyhow::{anyhow, Context, Result}; use clap::Clap; -use log::{debug, info, warn}; +use log::{debug, error, info, warn}; +use pbr::ProgressBar; use tabwriter::TabWriter; use brd::converter; @@ -46,6 +48,12 @@ enum SubCommand { display_order = 1 )] DDR2osu(Box), + #[clap( + name = "ddr2osu-batch", + about = "Batch version of ddr2osu", + display_order = 1 + )] + BatchDDR2osu(BatchDDR2osu), } #[derive(Clap)] @@ -110,11 +118,50 @@ struct DDR2osu { musicdb_file: Option, #[clap( short = "n", - name = "sound name", + name = "basename", about = "Sound in wave bank, otherwise inferred from SSQ filename", display_order = 2 )] - sound_name: Option, + basename: Option, + #[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)] convert: converter::ddr2osu::Config, } @@ -150,6 +197,75 @@ fn read_musicdb(path: &PathBuf) -> Result { } } +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::>(); + 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<()> { pretty_env_logger::init(); @@ -255,37 +371,23 @@ fn main() -> Result<()> { tw.flush()?; } SubCommand::DDR2osu(opts) => { - let sound_name = - &opts - .sound_name - .clone() - .unwrap_or(match get_basename(&opts.ssq_file) { - Some(basename) => basename.to_string(), - 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 basename = opts.basename.clone().unwrap_or( + get_basename(&opts.ssq_file) + .map(|basename| basename.to_string()) + .ok_or_else(|| { + anyhow!( + "Could not extract chart id from file name. Please specify it manually." + ) + })?, ); - let ssq_data = fs::read(&opts.ssq_file) - .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(); + let mut convert_options = opts.convert; if let Some(musicdb_file) = &opts.musicdb_file { debug!("Reading metadata from {}", musicdb_file.display()); let musicdb = read_musicdb(&musicdb_file)?; let musicdb_entry = musicdb - .get_entry_from_basename(sound_name) + .get_entry_from_basename(&basename) .ok_or_else(|| anyhow!("Entry not found in musicdb"))?; if convert_options.metadata.title.is_none() { 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()); } 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 - .to_beatmaps(&convert_options) - .context("failed to convert DDR step chart to osu!mania beatmap")?; + ddr2osu( + opts.ssq_file, + 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(|| { - format!( - "failed to read XWB file {}", - &opts.xwb_file.clone().display() + fs::create_dir_all(&opts.out_dir)?; + + let mut pb = ProgressBar::new(musicdb.music.len().try_into()?); + 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, ) - })?; - let wave_bank = WaveBank::parse(&xwb_data).context("failed to parse XWB file")?; - - let audio_data = if wave_bank.sounds.contains_key(sound_name) { - 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::>(); - 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 + .unwrap_or_else(move |err| { + error!( + "Could not convert {} ({}), continuing anyway", + entry.basename, err ) - })? - } else { - return Err(anyhow!( - "Could not find matching sound in wave bank (searched for {})", - sound_name, - )); - }; + }); - let osz = osu::osz::Archive { - 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()) - })?; + pb.inc(); + } } } Ok(())