rosu_map/section/hit_objects/
hit_samples.rs

1use 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/// Info about a [`HitObject`]'s sample.
12///
13/// [`HitObject`]: crate::section::hit_objects::HitObject
14#[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/// The name of a [`HitSampleInfo`].
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum HitSampleInfoName {
28    Default(HitSampleDefaultName),
29    File(String),
30}
31
32/// The default names of a [`HitSampleInfo`].
33#[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    /// Initialize a new [`HitSampleInfo`] without a filename.
68    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                // SAFETY: The value is guaranteed to be >= 2
79                .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    /// The filename with the highest preference that can be used as a source.
88    pub const fn lookup_name(&self) -> LookupName<'_> {
89        LookupName(self)
90    }
91}
92
93/// The filename of [`HitSampleInfo`] with the highest preference that can be
94/// used as a source.
95pub 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/// The different types of samples.
110#[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    /// Error when failing to parse a [`SampleBank`].
176    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
177    pub struct ParseSampleBankError;
178}
179
180/// The type of a hit sample.
181#[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    /// Check whether any of the given bitflags are set.
192    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    /// Error when failing to parse a [`HitSoundType`].
239    #[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/// Sample info of a [`HitObject`] to convert [`HitSoundType`] into a [`Vec`]
264/// of [`HitSampleInfo`].
265///
266/// [`HitObject`]: crate::section::hit_objects::HitObject
267#[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    /// Read and store custom sample banks.
278    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    /// Convert a [`HitSoundType`] into a [`Vec`] of [`HitSampleInfo`].
320    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    /// All the ways that parsing into [`SampleBankInfo`] can fail.
377    #[derive(Debug)]
378    pub enum ParseSampleBankInfoError {
379        #[error("missing info")]
380        MissingInfo,
381        #[error("failed to parse number")]
382        Number(#[from] ParseNumberError),
383    }
384}