rhythm_open_exchange/codec/formats/sm/
decoder.rs1#![allow(clippy::doc_markdown)]
2use crate::codec::Decoder;
5use crate::error::RoxResult;
6use crate::model::{Metadata, Note, RoxChart, TimingPoint};
7
8use super::parser;
9use super::types::{SmChart, SmFile, SmNoteType};
10
11pub struct SmDecoder;
13
14impl SmDecoder {
15 #[must_use]
20 pub fn from_file(sm: &SmFile) -> Option<RoxChart> {
21 sm.charts.first().map(|chart| Self::from_chart(sm, chart))
22 }
23
24 #[must_use]
26 pub fn from_chart(sm: &SmFile, chart: &SmChart) -> RoxChart {
27 let mut rox = RoxChart::new(chart.column_count);
28
29 rox.metadata = Metadata {
31 key_count: chart.column_count,
32 title: sm.metadata.title.clone().into(),
33 artist: sm.metadata.artist.clone().into(),
34 creator: sm.metadata.credit.clone().into(),
35 difficulty_name: chart.difficulty.clone().into(),
36 #[allow(clippy::cast_precision_loss)]
37 difficulty_value: Some(chart.meter as f32),
38 audio_file: sm.metadata.music.clone().into(),
39 background_file: if sm.metadata.background.is_empty() {
40 None
41 } else {
42 Some(sm.metadata.background.clone().into())
43 },
44 audio_offset_us: -sm.offset_us,
45 #[allow(clippy::cast_possible_truncation)]
46 preview_time_us: (sm.metadata.sample_start * 1_000_000.0) as i64,
47 #[allow(clippy::cast_possible_truncation)]
48 preview_duration_us: (sm.metadata.sample_length * 1_000_000.0) as i64,
49 source: Some(sm.metadata.banner.clone().into()),
50 genre: None,
51 language: None,
52 tags: Vec::new(),
53 is_coop: false,
54 ..Default::default()
55 };
56
57 for (time_us, bpm) in &sm.bpms {
59 rox.timing_points.push(TimingPoint::bpm(*time_us, *bpm));
60 }
61
62 let mut pending_holds: Vec<(i64, u8)> = Vec::new(); let mut pending_rolls: Vec<(i64, u8)> = Vec::new(); let mut sorted_notes = chart.notes.clone();
69 sorted_notes.sort_by(|a, b| a.time_us.cmp(&b.time_us).then(a.column.cmp(&b.column)));
70
71 for note in &sorted_notes {
72 match note.note_type {
73 SmNoteType::Tap => {
74 rox.notes.push(Note::tap(note.time_us, note.column));
75 }
76 SmNoteType::HoldHead => {
77 pending_holds.push((note.time_us, note.column));
79 }
80 SmNoteType::RollHead => {
81 pending_rolls.push((note.time_us, note.column));
83 }
84 SmNoteType::Tail => {
85 if let Some(idx) = pending_holds
87 .iter()
88 .position(|(_, col)| *col == note.column)
89 {
90 let (start_time, column) = pending_holds.remove(idx);
91 let duration = note.time_us - start_time;
92 rox.notes.push(Note::hold(start_time, duration, column));
93 } else if let Some(idx) = pending_rolls
94 .iter()
95 .position(|(_, col)| *col == note.column)
96 {
97 let (start_time, column) = pending_rolls.remove(idx);
98 let duration = note.time_us - start_time;
99 rox.notes.push(Note::burst(start_time, duration, column));
100 }
101 }
103 SmNoteType::Mine => {
104 rox.notes.push(Note::mine(note.time_us, note.column));
105 }
106 SmNoteType::Lift => {
107 rox.notes.push(Note::tap(note.time_us, note.column));
109 }
110 SmNoteType::Empty | SmNoteType::Fake => {
111 }
113 }
114 }
115
116 rox.notes.sort_by_key(|n| n.time_us);
118
119 rox
120 }
121
122 #[must_use]
124 pub fn decode_all(sm: &SmFile) -> Vec<RoxChart> {
125 sm.charts
126 .iter()
127 .map(|chart| Self::from_chart(sm, chart))
128 .collect()
129 }
130}
131
132impl Decoder for SmDecoder {
133 fn decode(data: &[u8]) -> RoxResult<RoxChart> {
134 let sm = parser::parse(data)?;
135 sm.charts
136 .first()
137 .map(|chart| Self::from_chart(&sm, chart))
138 .ok_or_else(|| {
139 crate::error::RoxError::InvalidFormat("No charts found in SM file".into())
140 })
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::codec::Decoder;
148
149 const BASIC_SM: &str = r#"
151#TITLE:Test Song;
152#ARTIST:Test Artist;
153#CREDIT:Test Mapper;
154#MUSIC:song.ogg;
155#OFFSET:0;
156#BPMS:0=120;
157#STOPS:;
158
159#NOTES:
160 dance-single:
161 :
162 Beginner:
163 1:
164 0,0,0,0,0:
1650000
1661000
1670100
1680010
169,
1700001
1710000
1720000
1730000
174;
175"#;
176
177 #[test]
178 fn test_decode_basic_sm() {
179 let chart = <SmDecoder as Decoder>::decode(BASIC_SM.as_bytes()).expect("Failed to decode");
180
181 assert_eq!(chart.key_count(), 4);
182 assert_eq!(chart.metadata.title, "Test Song");
183 assert_eq!(chart.metadata.artist, "Test Artist");
184 assert_eq!(chart.metadata.creator, "Test Mapper");
185 assert_eq!(chart.metadata.difficulty_name, "Beginner");
186 assert!(!chart.notes.is_empty());
187 }
188
189 #[test]
190 fn test_sm_note_count() {
191 let chart = <SmDecoder as Decoder>::decode(BASIC_SM.as_bytes()).expect("Failed to decode");
192
193 assert_eq!(chart.notes.len(), 4);
195 }
196
197 #[test]
198 fn test_sm_timing_points() {
199 let chart = <SmDecoder as Decoder>::decode(BASIC_SM.as_bytes()).expect("Failed to decode");
200
201 assert!(!chart.timing_points.is_empty());
203 assert_eq!(chart.timing_points[0].bpm, 120.0);
204 }
205
206 #[test]
207 fn test_decode_asset_4k() {
208 let data = crate::test_utils::get_test_asset("stepmania/4k.sm");
210 let chart = <SmDecoder as Decoder>::decode(&data).expect("Failed to decode 4k.sm");
211
212 assert_eq!(chart.key_count(), 4);
214 }
217}