rhythm_open_exchange/codec/formats/qua/
encoder.rs1use crate::codec::Encoder;
4use crate::error::RoxResult;
5use crate::model::RoxChart;
6
7use super::types::{QuaChart, QuaHitObject, QuaSliderVelocity, QuaTimingPoint};
8
9pub struct QuaEncoder;
11
12impl Encoder for QuaEncoder {
13 fn encode(chart: &RoxChart) -> RoxResult<Vec<u8>> {
14
15use compact_str::CompactString;
16
17 let mut qua = QuaChart {
18 audio_file: chart.metadata.audio_file.to_string(),
19 #[allow(clippy::cast_possible_truncation)]
21 preview_time: (chart.metadata.preview_time_us / 1000) as i32,
22 background_file: Some(chart
23 .metadata
24 .background_file
25 .as_ref()
26 .unwrap_or(&CompactString::new(""))
27 .to_string()),
28 map_id: if let Some(id) = chart.metadata.chart_id {
29 id as i32
30 } else {
31 -1
32 },
33 title: chart.metadata.title.to_string(),
34 artist: chart.metadata.artist.to_string(),
35 creator: chart.metadata.creator.to_string(),
36 difficulty_name: chart.metadata.difficulty_name.to_string(),
37 source: Some(chart.metadata.source.clone().unwrap_or_default().to_string()),
38 tags: Some(chart
39 .metadata
40 .tags
41 .iter()
42 .map(|s| s.as_str())
43 .collect::<Vec<_>>()
44 .join(" ")),
45 description: None,
46 initial_scroll_velocity: 1.0,
47 bpm_does_not_affect_sv: true,
48 ..Default::default()
49 };
50
51 for tp in &chart.timing_points {
53 #[allow(clippy::cast_precision_loss)]
55 let start_time = tp.time_us as f64 / 1000.0;
56
57 if tp.is_inherited {
58 qua.slider_velocities.push(QuaSliderVelocity {
60 start_time,
61 multiplier: f64::from(tp.scroll_speed),
62 });
63 } else {
64 qua.timing_points.push(QuaTimingPoint {
66 start_time,
67 bpm: tp.bpm,
68 signature: None,
69 });
70 }
71 }
72
73 for note in &chart.notes {
75 #[allow(clippy::cast_precision_loss)]
76 let start_time = note.time_us as f64 / 1000.0;
77 let lane = note.column + 1;
79
80 let end_time = match ¬e.note_type {
81 crate::model::NoteType::Hold { duration_us } => {
82 #[allow(clippy::cast_precision_loss)]
83 let end = (note.time_us + duration_us) as f64 / 1000.0;
84 Some(end)
85 }
86 _ => None,
87 };
88
89 qua.hit_objects.push(QuaHitObject {
90 start_time,
91 lane,
92 end_time,
93 key_sounds: Vec::new(),
94 });
95 }
96
97 let yaml = serde_yaml::to_string(&qua).map_err(|e| {
99 crate::error::RoxError::InvalidFormat(format!("YAML encoding error: {e}"))
100 })?;
101
102 Ok(yaml.into_bytes())
103 }
104}
105
106#[cfg(test)]
107mod tests {
108
109 #[test]
110 fn test_roundtrip() {
111 use super::*;
112 use crate::codec::Decoder;
113 use crate::codec::formats::qua::QuaDecoder;
114 let data = crate::test_utils::get_test_asset("quaver/4K.qua");
115 let chart1 = QuaDecoder::decode(&data).unwrap();
116 let encoded = QuaEncoder::encode(&chart1).unwrap();
117 let chart2 = QuaDecoder::decode(&encoded).unwrap();
118
119 assert_eq!(chart1.key_count(), chart2.key_count());
120
121 assert_eq!(chart1.notes.len(), chart2.notes.len());
123 for (n1, n2) in chart1.notes.iter().zip(chart2.notes.iter()) {
124 assert_eq!(n1.column, n2.column);
125 assert!(
126 (n1.time_us - n2.time_us).abs() <= 1000,
127 "Note time mismatch"
128 );
129 }
130
131 assert_eq!(chart1.timing_points.len(), chart2.timing_points.len());
132 for (tp1, tp2) in chart1.timing_points.iter().zip(chart2.timing_points.iter()) {
133 assert!(
134 (tp1.time_us - tp2.time_us).abs() <= 1000,
135 "Timing point time mismatch"
136 );
137 if !tp1.is_inherited {
138 assert!((tp1.bpm - tp2.bpm).abs() < 0.01, "BPM mismatch");
139 }
140 }
141 }
142}