1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
//! This module introduces struct [`BpmObjects`], which manages definition and events of BPM change on playing.
use std::collections::{BTreeMap, HashMap, HashSet, btree_map::Entry};
use strict_num_extended::PositiveF64;
use crate::bms::{
command::StringValue,
parse::{Result, prompt::ChannelDuplication},
prelude::*,
};
const DEFAULT_BPM: PositiveF64 = PositiveF64::new_const(120.0);
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// This aggregate manages definition and events of BPM change on playing.
pub struct BpmObjects {
/// The initial BPM of the score.
pub bpm: Option<StringValue<PositiveF64>>,
/// BPM change definitions, indexed by [`ObjId`]. `#BPM[01-ZZ]`
pub bpm_defs: HashMap<ObjId, StringValue<PositiveF64>>,
/// `#BASEBPM` for LR. Replaced by bpm match in LR2.
pub base_bpm: Option<StringValue<PositiveF64>>,
/// The BPMs corresponding to the id of the BPM change object.
/// BPM change events, indexed by time. `#BPM[01-ZZ]` in message
pub bpm_changes: BTreeMap<ObjTime, BpmChangeObj>,
/// BPM change events on its channel [`Channel::BpmChangeU8`], indexed by time.
pub bpm_changes_u8: BTreeMap<ObjTime, u8>,
/// Record of used BPM change ids from `#BPMxx` messages, for validity checks.
pub bpm_change_ids_used: HashSet<ObjId>,
}
impl BpmObjects {
/// Gets the time of the last BPM change object.
#[must_use]
pub fn last_obj_time(&self) -> Option<ObjTime> {
self.bpm_changes.last_key_value().map(|(&time, _)| time)
}
/// Calculates a required resolution to convert the notes time into pulses, which split one quarter note evenly.
#[must_use]
pub fn resolution_for_pulses(&self) -> u64 {
use num::Integer;
let mut hyp_resolution = 1;
for bpm_change in self.bpm_changes.values() {
hyp_resolution = hyp_resolution.lcm(&bpm_change.time.denominator().get());
}
hyp_resolution
}
}
impl BpmObjects {
/// Adds a new BPM change object to the notes.
///
/// # Errors
///
/// Returns [`ParseWarning`] if a conflict is found and the
/// provided [`Prompter`] decides to treat it as an error.
pub fn push_bpm_change(
&mut self,
bpm_change: BpmChangeObj,
prompt_handler: &impl Prompter,
) -> Result<()> {
match self.bpm_changes.entry(bpm_change.time) {
Entry::Vacant(entry) => {
entry.insert(bpm_change);
Ok(())
}
Entry::Occupied(mut entry) => {
let existing = entry.get();
prompt_handler
.handle_channel_duplication(ChannelDuplication::BpmChangeEvent {
time: bpm_change.time,
older: existing,
newer: &bpm_change,
})
.apply_channel(
entry.get_mut(),
bpm_change.clone(),
bpm_change.time,
Channel::BpmChange,
)
}
}
}
/// Adds a new BPM change (on [`Channel::BpmChangeU8`] channel) object to the notes.
///
/// # Errors
///
/// Returns [`ParseWarning`] if a conflict is found and the
/// provided [`Prompter`] decides to treat it as an error.
pub fn push_bpm_change_u8(
&mut self,
time: ObjTime,
bpm_change: u8,
prompt_handler: &impl Prompter,
) -> Result<()> {
match self.bpm_changes_u8.entry(time) {
Entry::Vacant(entry) => {
entry.insert(bpm_change);
Ok(())
}
Entry::Occupied(mut entry) => {
let existing = entry.get();
let older = BpmChangeObj {
time,
bpm: PositiveF64::new(*existing as f64).unwrap_or(DEFAULT_BPM),
};
let newer = BpmChangeObj {
time,
bpm: PositiveF64::new(bpm_change as f64).unwrap_or(DEFAULT_BPM),
};
prompt_handler
.handle_channel_duplication(ChannelDuplication::BpmChangeEvent {
time,
older: &older,
newer: &newer,
})
.apply_channel(entry.get_mut(), bpm_change, time, Channel::BpmChangeU8)
}
}
}
}