rosu_map/section/hit_objects/
hit_samples.rs1use std::{
2 cmp,
3 fmt::{Display, Formatter, Result as FmtResult},
4 num::{NonZeroU32, ParseIntError},
5 ops::{BitAnd, BitAndAssign},
6 str::{FromStr, Split},
7};
8
9use crate::util::{ParseNumber, ParseNumberError, StrExt};
10
11#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct HitSampleInfo {
16 pub name: HitSampleInfoName,
17 pub bank: SampleBank,
18 pub suffix: Option<NonZeroU32>,
19 pub volume: i32,
20 pub custom_sample_bank: i32,
21 pub bank_specified: bool,
22 pub is_layered: bool,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum HitSampleInfoName {
28 Default(HitSampleDefaultName),
29 File(String),
30}
31
32#[derive(Copy, Clone, Debug, PartialEq, Eq)]
34pub enum HitSampleDefaultName {
35 Normal,
36 Whistle,
37 Finish,
38 Clap,
39}
40
41impl HitSampleDefaultName {
42 pub const fn to_lowercase_str(self) -> &'static str {
43 match self {
44 Self::Normal => "hitnormal",
45 Self::Whistle => "hitwhistle",
46 Self::Finish => "hitfinish",
47 Self::Clap => "hitclap",
48 }
49 }
50}
51
52impl Display for HitSampleDefaultName {
53 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
54 f.write_str(self.to_lowercase_str())
55 }
56}
57
58impl HitSampleInfo {
59 pub const HIT_NORMAL: HitSampleInfoName =
60 HitSampleInfoName::Default(HitSampleDefaultName::Normal);
61 pub const HIT_WHISTLE: HitSampleInfoName =
62 HitSampleInfoName::Default(HitSampleDefaultName::Whistle);
63 pub const HIT_FINISH: HitSampleInfoName =
64 HitSampleInfoName::Default(HitSampleDefaultName::Finish);
65 pub const HIT_CLAP: HitSampleInfoName = HitSampleInfoName::Default(HitSampleDefaultName::Clap);
66
67 pub fn new(
69 name: HitSampleInfoName,
70 bank: Option<SampleBank>,
71 custom_sample_bank: i32,
72 volume: i32,
73 ) -> Self {
74 Self {
75 name,
76 bank: bank.unwrap_or(SampleBank::Normal),
77 suffix: (custom_sample_bank >= 2)
78 .then(|| unsafe { NonZeroU32::new_unchecked(custom_sample_bank as u32) }),
80 volume,
81 custom_sample_bank,
82 bank_specified: bank.is_some(),
83 is_layered: false,
84 }
85 }
86
87 pub const fn lookup_name(&self) -> LookupName<'_> {
89 LookupName(self)
90 }
91}
92
93pub struct LookupName<'a>(&'a HitSampleInfo);
96
97impl Display for LookupName<'_> {
98 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
99 match self.0.name {
100 HitSampleInfoName::Default(name) => match self.0.suffix {
101 Some(ref suffix) => write!(f, "Gameplay/{}-{name}{suffix}", self.0.bank),
102 None => write!(f, "Gameplay/{}-{name}", self.0.bank),
103 },
104 HitSampleInfoName::File(ref filename) => f.write_str(filename),
105 }
106 }
107}
108
109#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
111pub enum SampleBank {
112 #[default]
113 None,
114 Normal,
115 Soft,
116 Drum,
117}
118
119impl Display for SampleBank {
120 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
121 f.write_str(self.to_lowercase_str())
122 }
123}
124
125impl SampleBank {
126 pub const fn to_lowercase_str(self) -> &'static str {
127 match self {
128 Self::None => "none",
129 Self::Normal => "normal",
130 Self::Soft => "soft",
131 Self::Drum => "drum",
132 }
133 }
134
135 pub fn from_lowercase(s: &str) -> Self {
136 match s {
137 "normal" => Self::Normal,
138 "soft" => Self::Soft,
139 "drum" => Self::Drum,
140 _ => Self::None,
141 }
142 }
143}
144
145impl FromStr for SampleBank {
146 type Err = ParseSampleBankError;
147
148 fn from_str(s: &str) -> Result<Self, Self::Err> {
149 match s {
150 "0" | "None" => Ok(Self::None),
151 "1" | "Normal" => Ok(Self::Normal),
152 "2" | "Soft" => Ok(Self::Soft),
153 "3" | "Drum" => Ok(Self::Drum),
154 _ => Err(ParseSampleBankError),
155 }
156 }
157}
158
159impl TryFrom<i32> for SampleBank {
160 type Error = ParseSampleBankError;
161
162 fn try_from(bank: i32) -> Result<Self, Self::Error> {
163 match bank {
164 0 => Ok(Self::None),
165 1 => Ok(Self::Normal),
166 2 => Ok(Self::Soft),
167 3 => Ok(Self::Drum),
168 _ => Err(ParseSampleBankError),
169 }
170 }
171}
172
173thiserror! {
174 #[error("invalid sample bank value")]
175 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
177 pub struct ParseSampleBankError;
178}
179
180#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
182pub struct HitSoundType(u8);
183
184impl HitSoundType {
185 pub const NONE: u8 = 0;
186 pub const NORMAL: u8 = 1;
187 pub const WHISTLE: u8 = 2;
188 pub const FINISH: u8 = 4;
189 pub const CLAP: u8 = 8;
190
191 pub const fn has_flag(self, flag: u8) -> bool {
193 (self.0 & flag) != 0
194 }
195}
196
197impl From<&[HitSampleInfo]> for HitSoundType {
198 fn from(samples: &[HitSampleInfo]) -> Self {
199 let mut kind = Self::NONE;
200
201 for sample in samples.iter() {
202 match sample.name {
203 HitSampleInfo::HIT_WHISTLE => kind |= Self::WHISTLE,
204 HitSampleInfo::HIT_FINISH => kind |= Self::FINISH,
205 HitSampleInfo::HIT_CLAP => kind |= Self::CLAP,
206 HitSampleInfo::HIT_NORMAL | HitSampleInfoName::File(_) => {}
207 }
208 }
209
210 Self(kind)
211 }
212}
213
214impl From<HitSoundType> for u8 {
215 fn from(kind: HitSoundType) -> Self {
216 kind.0
217 }
218}
219
220impl From<u8> for HitSoundType {
221 fn from(hit_sound_type: u8) -> Self {
222 Self(hit_sound_type)
223 }
224}
225
226impl FromStr for HitSoundType {
227 type Err = ParseHitSoundTypeError;
228
229 fn from_str(s: &str) -> Result<Self, Self::Err> {
230 s.parse::<i32>()
231 .map(|n| Self((n & 0b1111_1111) as u8))
232 .map_err(ParseHitSoundTypeError)
233 }
234}
235
236thiserror! {
237 #[error("invalid hit sound type")]
238 #[derive(Clone, Debug, PartialEq, Eq)]
240 pub struct ParseHitSoundTypeError(ParseIntError);
241}
242
243impl PartialEq<u8> for HitSoundType {
244 fn eq(&self, other: &u8) -> bool {
245 self.0.eq(other)
246 }
247}
248
249impl BitAnd<u8> for HitSoundType {
250 type Output = u8;
251
252 fn bitand(self, rhs: u8) -> Self::Output {
253 self.0 & rhs
254 }
255}
256
257impl BitAndAssign<u8> for HitSoundType {
258 fn bitand_assign(&mut self, rhs: u8) {
259 self.0 &= rhs;
260 }
261}
262
263#[derive(Clone, Debug, Default, PartialEq, Eq)]
268pub struct SampleBankInfo {
269 pub filename: Option<String>,
270 pub bank_for_normal: Option<SampleBank>,
271 pub bank_for_addition: Option<SampleBank>,
272 pub volume: i32,
273 pub custom_sample_bank: i32,
274}
275
276impl SampleBankInfo {
277 pub fn read_custom_sample_banks(
279 &mut self,
280 mut split: Split<'_, char>,
281 banks_only: bool,
282 ) -> Result<(), ParseSampleBankInfoError> {
283 let Some(first) = split.next().filter(|s| !s.is_empty()) else {
284 return Ok(());
285 };
286
287 let bank = i32::parse(first)?.try_into().unwrap_or(SampleBank::Normal);
288
289 let add_bank = split
290 .next()
291 .ok_or(ParseSampleBankInfoError::MissingInfo)?
292 .parse_num::<i32>()?
293 .try_into()
294 .unwrap_or(SampleBank::Normal);
295
296 let normal_bank = (bank != SampleBank::None).then_some(bank);
297 let add_bank = (add_bank != SampleBank::None).then_some(add_bank);
298
299 self.bank_for_normal = normal_bank;
300 self.bank_for_addition = add_bank.or(normal_bank);
301
302 if banks_only {
303 return Ok(());
304 }
305
306 if let Some(next) = split.next() {
307 self.custom_sample_bank = next.parse_num()?;
308 }
309
310 if let Some(next) = split.next() {
311 self.volume = cmp::max(0, next.parse_num()?);
312 }
313
314 self.filename = split.next().map(str::to_owned);
315
316 Ok(())
317 }
318
319 pub fn convert_sound_type(self, sound_type: HitSoundType) -> Vec<HitSampleInfo> {
321 let mut sound_types = Vec::new();
322
323 if let Some(filename) = self.filename.filter(|filename| !filename.is_empty()) {
324 sound_types.push(HitSampleInfo::new(
325 HitSampleInfoName::File(filename),
326 None,
327 1,
328 self.volume,
329 ));
330 } else {
331 let mut sample = HitSampleInfo::new(
332 HitSampleInfo::HIT_NORMAL,
333 self.bank_for_normal,
334 self.custom_sample_bank,
335 self.volume,
336 );
337
338 sample.is_layered =
339 sound_type != HitSoundType::NONE && !sound_type.has_flag(HitSoundType::NORMAL);
340
341 sound_types.push(sample);
342 }
343
344 if sound_type.has_flag(HitSoundType::FINISH) {
345 sound_types.push(HitSampleInfo::new(
346 HitSampleInfo::HIT_FINISH,
347 self.bank_for_addition,
348 self.custom_sample_bank,
349 self.volume,
350 ));
351 }
352
353 if sound_type.has_flag(HitSoundType::WHISTLE) {
354 sound_types.push(HitSampleInfo::new(
355 HitSampleInfo::HIT_WHISTLE,
356 self.bank_for_addition,
357 self.custom_sample_bank,
358 self.volume,
359 ));
360 }
361
362 if sound_type.has_flag(HitSoundType::CLAP) {
363 sound_types.push(HitSampleInfo::new(
364 HitSampleInfo::HIT_CLAP,
365 self.bank_for_addition,
366 self.custom_sample_bank,
367 self.volume,
368 ));
369 }
370
371 sound_types
372 }
373}
374
375thiserror! {
376 #[derive(Debug)]
378 pub enum ParseSampleBankInfoError {
379 #[error("missing info")]
380 MissingInfo,
381 #[error("failed to parse number")]
382 Number(#[from] ParseNumberError),
383 }
384}