use thiserror::Error;
use crate::ByMeasureIdx;
use crate::ByPulse;
use crate::Chart;
use crate::GraphSectionPoint;
use crate::Interval;
use crate::LaserSection;
use crate::TimeSignature;
#[derive(Debug, Error)]
pub enum VoxReadError {
#[error("Unknown Track Identifier: '{0}'")]
UnknownTrackId(String),
#[error("Failed to parse value: '{0}'")]
ParseError(#[from] std::string::ParseError),
#[error("Failed to parse value: '{0}'")]
ParseFloatError(#[from] std::num::ParseFloatError),
#[error("Failed to parse value: '{0}'")]
ParseIntError(#[from] std::num::ParseIntError),
#[error("Failed to parse line values: '{0}'")]
LineParseError(String),
#[error("Failed to parse VOX time: '{0}'")]
TimeParseError(String),
#[error("Unsupported VOX version: {0}")]
UnsupportedVersionError(u32),
#[error("Unknown laser node type: {0}")]
UnknownLaserNodeError(i32),
}
#[derive(Debug, Error)]
pub enum VoxWriteError {}
pub trait Vox {
fn from_vox(data: &str) -> Result<crate::Chart, VoxReadError>;
fn to_vox<W>(&self, out: W) -> Result<(), VoxWriteError>
where
W: std::io::Write;
}
#[inline]
fn is_not_end(line: &&str) -> bool {
*line != "#END"
}
#[inline]
fn is_not_comment(line: &&str) -> bool {
!line.starts_with("//")
}
#[inline]
fn split_data_line(line: &str) -> Vec<&str> {
let comment_idx = line.find("//").unwrap_or(line.len());
let uncommented = &line[0..comment_idx];
uncommented.split('\t').filter(|s| !s.is_empty()).collect()
}
#[inline]
fn time_sig_accumulator(
mut accu: ByMeasureIdx<TimeSignature>,
line_data: Vec<&str>,
) -> Result<ByMeasureIdx<TimeSignature>, VoxReadError> {
let measure = line_data
.first()
.and_then(|v| v.split(',').next().map(|i| i.parse::<u32>()));
if let Some(Ok(m)) = measure {
accu.push((
m - 1,
TimeSignature(
line_data.get(1).unwrap_or(&"").parse()?,
line_data.get(2).unwrap_or(&"").parse()?,
),
));
Ok(accu)
} else {
Err(VoxReadError::LineParseError(line_data.join(", ")))
}
}
#[inline]
fn tick_from_vox(vox_time: &str, chart: &Chart) -> Result<u32, VoxReadError> {
let mut time_parts = vox_time.split(',');
let (measure, beat, ticks): (u32, u32, u32) =
match (time_parts.next(), time_parts.next(), time_parts.next()) {
(Some(m), Some(b), Some(t)) => (m.parse()?, b.parse()?, t.parse()?),
_ => return Err(VoxReadError::TimeParseError(vox_time.to_string())),
};
let current_sig = match chart
.beat
.time_sig
.binary_search_by_key(&(measure - 1), |t| t.0)
{
Ok(i) => chart.beat.time_sig[i],
Err(i) => chart.beat.time_sig[i - 1],
};
let tick_per_beat = 192 / current_sig.1 .1;
Ok(chart.measure_to_tick(measure - 1) + tick_per_beat * (beat - 1) + ticks)
}
impl Vox for crate::Chart {
fn from_vox(data: &str) -> Result<crate::Chart, VoxReadError> {
let mut data = data.lines();
let mut chart = crate::Chart::new();
let mut tracks: [Vec<Vec<&str>>; 8] = Default::default();
let mut vox_version = 0;
let mut bpm_info = Vec::new();
while let Some(line) = data.next() {
match line {
"#FORMAT VERSION" => vox_version = data.by_ref().next().unwrap_or("0").parse()?,
"#BEAT INFO" => {
chart.beat.time_sig = data
.by_ref()
.take_while(is_not_end)
.filter(is_not_comment)
.map(split_data_line)
.try_fold(Vec::new(), time_sig_accumulator)?;
}
"#BPM INFO" => {
bpm_info = data
.by_ref()
.take_while(is_not_end)
.filter(is_not_comment)
.map(split_data_line)
.collect()
}
"#TAB EFFECT INFO" => {}
"#FXBUTTON EFFECT INFO" => {}
"#TAB PARAM ASSIGN INFO" => {}
"#SPCONTROLER" => {} "#TRACK AUTO TAB" | "#TRACK ORIGINAL L" | "#TRACK ORIGINAL R" => {}
track if track.starts_with("#TRACK") => {
let tracknum = match track.chars().filter_map(|c| c.to_digit(10)).next() {
Some(c) => c,
None => return Err(VoxReadError::UnknownTrackId(track.to_string())),
};
if (1..=8).contains(&tracknum) {
tracks[tracknum as usize - 1] = data
.by_ref()
.take_while(is_not_end)
.filter(is_not_comment)
.map(split_data_line)
.collect();
} else {
return Err(VoxReadError::UnknownTrackId(tracknum.to_string()));
}
}
_ => (),
}
}
chart.beat.bpm = bpm_info.iter().try_fold(
Vec::new(),
|mut bpm, line| -> Result<ByPulse<f64>, VoxReadError> {
let tick = tick_from_vox(line[0], &chart)?;
bpm.push((tick, line[1].trim().parse()?));
Ok(bpm)
},
)?;
for (track_idx, track) in tracks.iter().enumerate() {
if track_idx == 0 || track_idx == 7 {
let (lasers, _, _) = track.iter().try_fold(
(Vec::new(), LaserSection (0, Vec::new(), 0), 300),
|(mut lasers, mut current_section, mut last_vox_v),
line|
-> Result<(Vec<LaserSection>, LaserSection, i32), VoxReadError> {
let y = tick_from_vox(line[0], &chart)?;
let v = if vox_version < 12 {
let vox_v: i32 = line[1].parse()?;
let vox_v = if (vox_v - last_vox_v).abs() == 1 {last_vox_v} else {vox_v};
last_vox_v = vox_v;
vox_v as f64 / 127.0}
else {
line[1].parse()?
};
let node_type: i32 = line[2].parse()?;
let wide: u8 = if line.len() > 5 {
line[5].parse()?
}
else { 1
};
match node_type {
1 => { current_section = LaserSection(
y,
vec![GraphSectionPoint {
ry: 0,
v,
a: 0.5,
b: 0.5,
vf: None,
}],
wide
)
}
0 | 2 => { let ry = y - current_section.0;
if let Some(last) = current_section.1.last_mut() {
if last.ry == ry {
last.vf = Some(v);
}
else
{
current_section.1.push(GraphSectionPoint {
ry,
v ,
a: 0.5,
b: 0.5,
vf: None,
});
}
}
else {
unreachable!();
}
}
_ => return Err(VoxReadError::UnknownLaserNodeError(node_type))
}
if node_type == 2 {
let finished_section = std::mem::replace(&mut current_section, LaserSection(y, vec![GraphSectionPoint {ry: 0,v: 0.0,a: 0.5,b: 0.5,vf: None,
}],
wide
));
lasers.push(finished_section);
}
Ok((lasers, current_section, last_vox_v))
},
)?;
chart.note.laser[track_idx / 7] = lasers;
} else {
let notes = track.iter().try_fold(
Vec::new(),
|mut notes, line| -> Result<Vec<Interval>, VoxReadError> {
let y = tick_from_vox(line[0], &chart)?;
let l = line[1].parse()?;
notes.push(Interval { y, l });
Ok(notes)
},
)?;
match track_idx {
1 | 6 => chart.note.fx[track_idx / 6] = notes,
2..=5 => chart.note.bt[track_idx - 2] = notes,
_ => unreachable!(),
}
}
}
Ok(chart)
}
fn to_vox<W>(&self, _out: W) -> Result<(), VoxWriteError>
where
W: std::io::Write,
{
todo!()
}
}