bms_rs/bms/model/
notes.rs

1use std::{
2    collections::{BTreeMap, HashMap},
3    marker::PhantomData,
4    ops::Bound,
5    path::PathBuf,
6};
7
8use itertools::Itertools;
9
10use crate::{
11    bms::prelude::*,
12    parse::{Result, prompt::ChannelDuplication},
13};
14
15#[derive(Debug, Default, Clone, PartialEq, Eq)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17struct WavObjArena(Vec<WavObj>);
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct WavObjArenaIndex(usize);
22
23/// The playable objects set for querying by lane or time.
24#[derive(Debug, Clone, PartialEq, Eq)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct Notes<T> {
27    /// The path to override the base path of the WAV file path.
28    /// This allows WAV files to be referenced relative to a different directory.
29    pub wav_path_root: Option<PathBuf>,
30    /// The WAV file paths corresponding to the id of the note object.
31    pub wav_files: HashMap<ObjId, PathBuf>,
32    /// Arena of `WavObj`, contains the master data of sound objects. `#XXXYY:ZZ...` (note placement)
33    arena: WavObjArena,
34    /// Note objects index for each wav sound of [`ObjId`].
35    idx_by_wav_id: HashMap<ObjId, Vec<WavObjArenaIndex>>,
36    /// Note objects index for each channel from the mapping `T`.
37    idx_by_channel: HashMap<NoteChannelId, Vec<WavObjArenaIndex>>,
38    /// Note objects index sorted by its time.
39    idx_by_time: BTreeMap<ObjTime, Vec<WavObjArenaIndex>>,
40    /// The path of MIDI file, which is played as BGM while playing the score.
41    #[cfg(feature = "minor-command")]
42    pub midi_file: Option<PathBuf>,
43    /// Material WAV file paths. #MATERIALSWAV
44    #[cfg(feature = "minor-command")]
45    pub materials_wav: Vec<PathBuf>,
46    /// BGM volume change events, indexed by time. #97
47    pub bgm_volume_changes: BTreeMap<ObjTime, BgmVolumeObj>,
48    /// KEY volume change events, indexed by time. #98
49    pub key_volume_changes: BTreeMap<ObjTime, KeyVolumeObj>,
50    /// Seek events, indexed by time. #05
51    #[cfg(feature = "minor-command")]
52    pub seek_events: BTreeMap<ObjTime, SeekObj>,
53    /// Text events, indexed by time. #99
54    pub text_events: BTreeMap<ObjTime, TextObj>,
55    /// Judge events, indexed by time. #A0
56    pub judge_events: BTreeMap<ObjTime, JudgeObj>,
57    /// BGA keybound events, indexed by time. #A5
58    #[cfg(feature = "minor-command")]
59    pub bga_keybound_events: BTreeMap<ObjTime, BgaKeyboundObj>,
60    /// Option events, indexed by time. #A6
61    #[cfg(feature = "minor-command")]
62    pub option_events: BTreeMap<ObjTime, OptionObj>,
63    _marker: PhantomData<fn() -> T>,
64}
65
66impl<T> Default for Notes<T> {
67    fn default() -> Self {
68        Self {
69            wav_path_root: Default::default(),
70            wav_files: Default::default(),
71            arena: Default::default(),
72            idx_by_wav_id: Default::default(),
73            idx_by_channel: Default::default(),
74            idx_by_time: Default::default(),
75            #[cfg(feature = "minor-command")]
76            midi_file: Default::default(),
77            #[cfg(feature = "minor-command")]
78            materials_wav: Default::default(),
79            bgm_volume_changes: Default::default(),
80            key_volume_changes: Default::default(),
81            #[cfg(feature = "minor-command")]
82            seek_events: Default::default(),
83            text_events: Default::default(),
84            judge_events: Default::default(),
85            #[cfg(feature = "minor-command")]
86            bga_keybound_events: Default::default(),
87            #[cfg(feature = "minor-command")]
88            option_events: Default::default(),
89            _marker: PhantomData,
90        }
91    }
92}
93
94// query methods
95impl<T> Notes<T> {
96    /// Checks whether there is no valid notes.
97    #[must_use]
98    pub fn is_empty(&self) -> bool {
99        self.all_notes().all(|obj| obj.wav_id.is_null())
100    }
101
102    /// Converts into the notes.
103    #[must_use]
104    pub fn into_all_notes(self) -> Vec<WavObj> {
105        self.arena.0
106    }
107
108    /// Returns the iterator having all of the notes sorted by time.
109    pub fn all_notes(&self) -> impl Iterator<Item = &WavObj> {
110        self.arena.0.iter().sorted()
111    }
112
113    /// Returns the iterator having all of the notes and its index sorted by time.
114    pub fn all_entries(&self) -> impl Iterator<Item = (WavObjArenaIndex, &WavObj)> {
115        self.arena
116            .0
117            .iter()
118            .enumerate()
119            .sorted_by_key(|obj| obj.1)
120            .map(|(idx, obj)| (WavObjArenaIndex(idx), obj))
121    }
122
123    /// Returns all the playable notes in the score.
124    pub fn playables(&self) -> impl Iterator<Item = &WavObj>
125    where
126        T: KeyLayoutMapper,
127    {
128        self.arena.0.iter().sorted().filter(|obj| {
129            obj.channel_id
130                .try_into_map::<T>()
131                .is_some_and(|map| map.kind().is_playable())
132        })
133    }
134
135    /// Returns all the displayable notes in the score.
136    pub fn displayables(&self) -> impl Iterator<Item = &WavObj>
137    where
138        T: KeyLayoutMapper,
139    {
140        self.arena.0.iter().sorted().filter(|obj| {
141            obj.channel_id
142                .try_into_map::<T>()
143                .is_some_and(|map| map.kind().is_displayable())
144        })
145    }
146
147    /// Returns all the bgms in the score.
148    pub fn bgms(&self) -> impl Iterator<Item = &WavObj>
149    where
150        T: KeyLayoutMapper,
151    {
152        self.arena.0.iter().sorted().filter(|obj| {
153            obj.channel_id
154                .try_into_map::<T>()
155                .is_none_or(|map| !map.kind().is_displayable())
156        })
157    }
158
159    /// Retrieves notes on the specified channel id by the key mapping `T`.
160    pub fn notes_on(
161        &self,
162        channel_id: NoteChannelId,
163    ) -> impl Iterator<Item = (WavObjArenaIndex, &WavObj)>
164    where
165        T: KeyLayoutMapper,
166    {
167        self.idx_by_channel
168            .get(&channel_id)
169            .into_iter()
170            .flatten()
171            .map(|&arena_index| (arena_index, &self.arena.0[arena_index.0]))
172    }
173
174    /// Retrieves notes in the specified time span.
175    pub fn notes_in<R: std::ops::RangeBounds<ObjTime>>(
176        &self,
177        time_span: R,
178    ) -> impl DoubleEndedIterator<Item = (WavObjArenaIndex, &WavObj)> {
179        self.idx_by_time
180            .range(time_span)
181            .flat_map(|(_, indexes)| indexes)
182            .map(|&arena_index| (arena_index, &self.arena.0[arena_index.0]))
183    }
184
185    /// Finds next object on the key `Key` from the time `ObjTime`.
186    #[must_use]
187    pub fn next_obj_by_key(&self, channel_id: NoteChannelId, time: ObjTime) -> Option<&WavObj> {
188        self.notes_in((Bound::Excluded(time), Bound::Unbounded))
189            .map(|(_, obj)| obj)
190            .find(|obj| obj.channel_id == channel_id)
191    }
192
193    /// Gets the latest starting time of all notes.
194    #[must_use]
195    pub fn last_obj_time(&self) -> Option<ObjTime> {
196        let (&time, _) = self.idx_by_time.last_key_value()?;
197        Some(time)
198    }
199
200    /// Gets the time of last playable object.
201    #[must_use]
202    pub fn last_playable_time(&self) -> Option<ObjTime>
203    where
204        T: KeyLayoutMapper,
205    {
206        self.notes_in(..)
207            .map(|(_, obj)| obj)
208            .rev()
209            .find(|obj| {
210                obj.channel_id
211                    .try_into_map::<T>()
212                    .is_some_and(|map| map.kind().is_displayable())
213            })
214            .map(|obj| obj.offset)
215    }
216
217    /// Gets the time of last BGM object.
218    ///
219    /// You can't use this to find the length of music. Because this doesn't consider that the length of sound. And visible notes may ring after all BGMs.
220    #[must_use]
221    pub fn last_bgm_time(&self) -> Option<ObjTime>
222    where
223        T: KeyLayoutMapper,
224    {
225        self.notes_in(..)
226            .map(|(_, obj)| obj)
227            .rev()
228            .find(|obj| {
229                obj.channel_id
230                    .try_into_map::<T>()
231                    .is_none_or(|map| !map.kind().is_displayable())
232            })
233            .map(|obj| obj.offset)
234    }
235}
236
237// push and remove methods
238impl<T> Notes<T> {
239    /// Adds the new note object to the notes.
240    pub fn push_note(&mut self, note: WavObj) {
241        let new_index = WavObjArenaIndex(self.arena.0.len());
242        self.idx_by_wav_id
243            .entry(note.wav_id)
244            .or_default()
245            .push(new_index);
246        self.idx_by_channel
247            .entry(note.channel_id)
248            .or_default()
249            .push(new_index);
250        self.idx_by_time
251            .entry(note.offset)
252            .or_default()
253            .push(new_index);
254        self.arena.0.push(note);
255    }
256
257    fn remove_index(&mut self, idx: usize, removing: &WavObj) {
258        let channel_id = removing.channel_id;
259        if let Some(ids_by_channel_idx) = self.idx_by_channel[&channel_id]
260            .iter()
261            .position(|id| id.0 == idx)
262        {
263            self.idx_by_channel
264                .get_mut(&channel_id)
265                .unwrap()
266                .swap_remove(ids_by_channel_idx);
267        }
268        if let Some(ids_by_time_idx) = self.idx_by_time[&removing.offset]
269            .iter()
270            .position(|id| id.0 == idx)
271        {
272            self.idx_by_time
273                .get_mut(&removing.offset)
274                .unwrap()
275                .swap_remove(ids_by_time_idx);
276        }
277    }
278
279    /// Removes the latest note from the notes.
280    pub fn pop_note(&mut self) -> Option<WavObj> {
281        let last_idx = self.arena.0.len();
282        let last = self.arena.0.pop()?;
283        if let Some(ids_by_wav_id_idx) = self.idx_by_wav_id[&last.wav_id]
284            .iter()
285            .position(|id| id.0 == last_idx)
286        {
287            self.idx_by_wav_id
288                .get_mut(&last.wav_id)?
289                .swap_remove(ids_by_wav_id_idx);
290        }
291        let channel_id = last.channel_id;
292        if let Some(ids_by_channel_idx) = self.idx_by_channel[&channel_id]
293            .iter()
294            .position(|id| id.0 == last_idx)
295        {
296            self.idx_by_channel
297                .get_mut(&channel_id)?
298                .swap_remove(ids_by_channel_idx);
299        }
300        if let Some(ids_by_time_idx) = self.idx_by_time[&last.offset]
301            .iter()
302            .position(|id| id.0 == last_idx)
303        {
304            self.idx_by_time
305                .get_mut(&last.offset)?
306                .swap_remove(ids_by_time_idx);
307        }
308        Some(last)
309    }
310
311    /// Removes notes belonging to the wav id.
312    pub fn remove_note(&mut self, wav_id: ObjId) -> Vec<WavObj>
313    where
314        T: KeyLayoutMapper,
315    {
316        let Some(indexes) = self.idx_by_wav_id.remove(&wav_id) else {
317            return vec![];
318        };
319        let mut objs = Vec::with_capacity(indexes.len());
320        for WavObjArenaIndex(idx) in indexes {
321            let removing = std::mem::replace(&mut self.arena.0[idx], WavObj::dangling());
322            self.remove_index(idx, &removing);
323            objs.push(removing);
324        }
325        objs
326    }
327
328    /// Removes a note of the specified index `idx`.
329    pub fn pop_by_idx(&mut self, idx: WavObjArenaIndex) -> Option<WavObj> {
330        let removing = std::mem::replace(self.arena.0.get_mut(idx.0)?, WavObj::dangling());
331        self.remove_index(idx.0, &removing);
332        Some(removing)
333    }
334
335    /// Removes the latest note using the wav of `wav_id`.
336    pub fn pop_latest_of(&mut self, wav_id: ObjId) -> Option<WavObj>
337    where
338        T: KeyLayoutMapper,
339    {
340        let &WavObjArenaIndex(to_pop) = self.idx_by_wav_id.get(&wav_id)?.last()?;
341        let removing = std::mem::replace(&mut self.arena.0[to_pop], WavObj::dangling());
342        self.remove_index(to_pop, &removing);
343        Some(removing)
344    }
345
346    /// Adds the BGM (auto-played) note of `wav_id` at `time`.
347    pub fn push_bgm(&mut self, time: ObjTime, wav_id: ObjId)
348    where
349        T: KeyLayoutMapper,
350    {
351        self.push_note(WavObj {
352            offset: time,
353            channel_id: NoteChannelId::bgm(),
354            wav_id,
355        });
356    }
357
358    /// Links the wav file `path` to the object id `wav_id`. Then returns the old path if existed.
359    pub fn push_wav<P: Into<PathBuf>>(&mut self, wav_id: ObjId, path: P) -> Option<PathBuf> {
360        self.wav_files.insert(wav_id, path.into())
361    }
362
363    /// Unlinks the wav file path from the object id `wav_id`, and return the path if existed.
364    pub fn remove_wav(&mut self, wav_id: &ObjId) -> Option<PathBuf> {
365        self.wav_files.remove(wav_id)
366    }
367
368    /// Retains note objects with the condition `cond`. It keeps only the [`WavObj`]s which `cond` returned `true`.
369    pub fn retain_notes<F: FnMut(&WavObj) -> bool>(&mut self, mut cond: F)
370    where
371        T: KeyLayoutMapper,
372    {
373        let removing_indexes: Vec<_> = self
374            .arena
375            .0
376            .iter()
377            .enumerate()
378            .filter(|&(_, obj)| cond(obj))
379            .map(|(i, _)| i)
380            .collect();
381        for removing_idx in removing_indexes {
382            let removing = std::mem::replace(&mut self.arena.0[removing_idx], WavObj::dangling());
383            self.remove_index(removing_idx, &removing);
384        }
385    }
386
387    /// Duplicates the object with id `src` at the time `at` into the channel of id `dst`.
388    pub fn dup_note_into(&mut self, src: ObjId, at: ObjTime, dst: NoteChannelId) {
389        let Some(src_obj) = self
390            .idx_by_wav_id
391            .get(&src)
392            .into_iter()
393            .flatten()
394            .map(|idx| &self.arena.0[idx.0])
395            .find(|obj| obj.offset == at)
396        else {
397            return;
398        };
399        let new = WavObj {
400            channel_id: dst,
401            ..*src_obj
402        };
403        self.push_note(new);
404    }
405
406    /// Adds a new BGM volume change object to the notes.
407    pub fn push_bgm_volume_change(
408        &mut self,
409        volume_obj: BgmVolumeObj,
410        prompt_handler: &mut impl PromptHandler,
411    ) -> Result<()> {
412        match self.bgm_volume_changes.entry(volume_obj.time) {
413            std::collections::btree_map::Entry::Vacant(entry) => {
414                entry.insert(volume_obj);
415                Ok(())
416            }
417            std::collections::btree_map::Entry::Occupied(mut entry) => {
418                let existing = entry.get();
419
420                prompt_handler
421                    .handle_channel_duplication(ChannelDuplication::BgmVolumeChangeEvent {
422                        time: volume_obj.time,
423                        older: existing,
424                        newer: &volume_obj,
425                    })
426                    .apply_channel(
427                        entry.get_mut(),
428                        volume_obj.clone(),
429                        volume_obj.time,
430                        Channel::BgmVolume,
431                    )
432            }
433        }
434    }
435
436    /// Adds a new KEY volume change object to the notes.
437    pub fn push_key_volume_change(
438        &mut self,
439        volume_obj: KeyVolumeObj,
440        prompt_handler: &mut impl PromptHandler,
441    ) -> Result<()> {
442        match self.key_volume_changes.entry(volume_obj.time) {
443            std::collections::btree_map::Entry::Vacant(entry) => {
444                entry.insert(volume_obj);
445                Ok(())
446            }
447            std::collections::btree_map::Entry::Occupied(mut entry) => {
448                let existing = entry.get();
449
450                prompt_handler
451                    .handle_channel_duplication(ChannelDuplication::KeyVolumeChangeEvent {
452                        time: volume_obj.time,
453                        older: existing,
454                        newer: &volume_obj,
455                    })
456                    .apply_channel(
457                        entry.get_mut(),
458                        volume_obj.clone(),
459                        volume_obj.time,
460                        Channel::KeyVolume,
461                    )
462            }
463        }
464    }
465
466    /// Adds a new seek object to the notes.
467    #[cfg(feature = "minor-command")]
468    pub fn push_seek_event(
469        &mut self,
470        seek_obj: SeekObj,
471        prompt_handler: &mut impl PromptHandler,
472    ) -> Result<()> {
473        match self.seek_events.entry(seek_obj.time) {
474            std::collections::btree_map::Entry::Vacant(entry) => {
475                entry.insert(seek_obj);
476                Ok(())
477            }
478            std::collections::btree_map::Entry::Occupied(mut entry) => {
479                let existing = entry.get();
480
481                prompt_handler
482                    .handle_channel_duplication(ChannelDuplication::SeekMessageEvent {
483                        time: seek_obj.time,
484                        older: existing,
485                        newer: &seek_obj,
486                    })
487                    .apply_channel(
488                        entry.get_mut(),
489                        seek_obj.clone(),
490                        seek_obj.time,
491                        Channel::Seek,
492                    )
493            }
494        }
495    }
496
497    /// Adds a new text object to the notes.
498    pub fn push_text_event(
499        &mut self,
500        text_obj: TextObj,
501        prompt_handler: &mut impl PromptHandler,
502    ) -> Result<()> {
503        match self.text_events.entry(text_obj.time) {
504            std::collections::btree_map::Entry::Vacant(entry) => {
505                entry.insert(text_obj);
506                Ok(())
507            }
508            std::collections::btree_map::Entry::Occupied(mut entry) => {
509                let existing = entry.get();
510
511                prompt_handler
512                    .handle_channel_duplication(ChannelDuplication::TextEvent {
513                        time: text_obj.time,
514                        older: existing,
515                        newer: &text_obj,
516                    })
517                    .apply_channel(
518                        entry.get_mut(),
519                        text_obj.clone(),
520                        text_obj.time,
521                        Channel::Text,
522                    )
523            }
524        }
525    }
526
527    /// Adds a new judge object to the notes.
528    pub fn push_judge_event(
529        &mut self,
530        judge_obj: JudgeObj,
531        prompt_handler: &mut impl PromptHandler,
532    ) -> Result<()> {
533        match self.judge_events.entry(judge_obj.time) {
534            std::collections::btree_map::Entry::Vacant(entry) => {
535                entry.insert(judge_obj);
536                Ok(())
537            }
538            std::collections::btree_map::Entry::Occupied(mut entry) => {
539                let existing = entry.get();
540
541                prompt_handler
542                    .handle_channel_duplication(ChannelDuplication::JudgeEvent {
543                        time: judge_obj.time,
544                        older: existing,
545                        newer: &judge_obj,
546                    })
547                    .apply_channel(
548                        entry.get_mut(),
549                        judge_obj.clone(),
550                        judge_obj.time,
551                        Channel::Judge,
552                    )
553            }
554        }
555    }
556
557    /// Adds a new BGA keybound object to the notes.
558    #[cfg(feature = "minor-command")]
559    pub fn push_bga_keybound_event(
560        &mut self,
561        keybound_obj: BgaKeyboundObj,
562        prompt_handler: &mut impl PromptHandler,
563    ) -> Result<()> {
564        match self.bga_keybound_events.entry(keybound_obj.time) {
565            std::collections::btree_map::Entry::Vacant(entry) => {
566                entry.insert(keybound_obj);
567                Ok(())
568            }
569            std::collections::btree_map::Entry::Occupied(mut entry) => {
570                let existing = entry.get();
571
572                prompt_handler
573                    .handle_channel_duplication(ChannelDuplication::BgaKeyboundEvent {
574                        time: keybound_obj.time,
575                        older: existing,
576                        newer: &keybound_obj,
577                    })
578                    .apply_channel(
579                        entry.get_mut(),
580                        keybound_obj.clone(),
581                        keybound_obj.time,
582                        Channel::BgaKeybound,
583                    )
584            }
585        }
586    }
587
588    /// Adds a new option object to the notes.
589    #[cfg(feature = "minor-command")]
590    pub fn push_option_event(
591        &mut self,
592        option_obj: OptionObj,
593        prompt_handler: &mut impl PromptHandler,
594    ) -> Result<()> {
595        match self.option_events.entry(option_obj.time) {
596            std::collections::btree_map::Entry::Vacant(entry) => {
597                entry.insert(option_obj);
598                Ok(())
599            }
600            std::collections::btree_map::Entry::Occupied(mut entry) => {
601                let existing = entry.get();
602
603                prompt_handler
604                    .handle_channel_duplication(ChannelDuplication::OptionEvent {
605                        time: option_obj.time,
606                        older: existing,
607                        newer: &option_obj,
608                    })
609                    .apply_channel(
610                        entry.get_mut(),
611                        option_obj.clone(),
612                        option_obj.time,
613                        Channel::Option,
614                    )
615            }
616        }
617    }
618}
619
620// modify methods
621impl<T> Notes<T> {
622    /// Changes the channel of notes `target` in `time_span` into another channel `dst`.
623    pub fn change_note_channel<I>(&mut self, targets: I, dst: NoteChannelId)
624    where
625        T: KeyLayoutMapper,
626        I: IntoIterator<Item = WavObjArenaIndex>,
627    {
628        for target in targets {
629            let Some(obj) = self.arena.0.get_mut(target.0) else {
630                continue;
631            };
632
633            // Drain all ids from ids_by_channel where channel id matches
634            let src = obj.channel_id;
635            if let Some(idx_by_channel_idx) = self.idx_by_channel[&src]
636                .iter()
637                .position(|&idx| idx == target)
638            {
639                self.idx_by_channel
640                    .get_mut(&src)
641                    .unwrap()
642                    .swap_remove(idx_by_channel_idx);
643            }
644            self.idx_by_channel.entry(dst).or_default().push(target);
645
646            // Modify entry
647            obj.channel_id = dst;
648        }
649    }
650
651    /// Changes the specified object `target`'s offset time into `new_time`.
652    pub fn change_note_time(
653        &mut self,
654        target: WavObjArenaIndex,
655        new_time: ObjTime,
656    ) -> Option<ObjTime> {
657        let to_change = self.arena.0.get_mut(target.0)?;
658        let old_time = to_change.offset;
659        if old_time == new_time {
660            return Some(new_time);
661        }
662
663        let idx_by_time = self.idx_by_time[&old_time]
664            .iter()
665            .position(|&idx| idx == target)?;
666        self.idx_by_time
667            .get_mut(&to_change.offset)?
668            .swap_remove(idx_by_time);
669        self.idx_by_time.entry(new_time).or_default().push(target);
670        to_change.offset = new_time;
671        Some(old_time)
672    }
673}
674
675#[cfg(test)]
676mod tests {
677    use super::Notes;
678    use crate::bms::prelude::*;
679
680    #[test]
681    fn push_and_pop() {
682        let mut notes = Notes::<KeyLayoutBeat>::default();
683        let note = WavObj {
684            offset: ObjTime::new(1, 2, 4),
685            channel_id: NoteChannelId::bgm(),
686            wav_id: "01".try_into().unwrap(),
687        };
688
689        assert!(notes.pop_note().is_none());
690
691        notes.push_note(note.clone());
692        let removed = notes.pop_note();
693        assert_eq!(Some(note), removed);
694
695        assert!(notes.pop_note().is_none());
696    }
697
698    #[test]
699    fn change_note_channel() {
700        let mut notes = Notes::<KeyLayoutBeat>::default();
701        let note = WavObj {
702            offset: ObjTime::new(1, 2, 4),
703            channel_id: NoteChannelId::bgm(),
704            wav_id: "01".try_into().unwrap(),
705        };
706
707        assert!(notes.pop_note().is_none());
708
709        notes.push_note(note.clone());
710        let (idx, _) = notes.all_entries().next().unwrap();
711        notes.change_note_channel(
712            [idx],
713            KeyLayoutBeat::new(PlayerSide::Player1, NoteKind::Visible, Key::Key(1)).to_channel_id(),
714        );
715
716        assert_eq!(
717            notes.all_notes().next(),
718            Some(&WavObj {
719                offset: ObjTime::new(1, 2, 4),
720                channel_id: KeyLayoutBeat::new(PlayerSide::Player1, NoteKind::Visible, Key::Key(1))
721                    .to_channel_id(),
722                wav_id: "01".try_into().unwrap(),
723            })
724        );
725    }
726
727    #[test]
728    fn change_note_time() {
729        let mut notes = Notes::<KeyLayoutBeat>::default();
730        let note = WavObj {
731            offset: ObjTime::new(1, 2, 4),
732            channel_id: NoteChannelId::bgm(),
733            wav_id: "01".try_into().unwrap(),
734        };
735
736        assert!(notes.pop_note().is_none());
737
738        notes.push_note(note.clone());
739        let (idx, _) = notes.all_entries().next().unwrap();
740        notes.change_note_time(idx, ObjTime::new(1, 1, 4));
741
742        assert_eq!(
743            notes.all_notes().next(),
744            Some(&WavObj {
745                offset: ObjTime::new(1, 1, 4),
746                channel_id: NoteChannelId::bgm(),
747                wav_id: "01".try_into().unwrap(),
748            })
749        );
750    }
751}