klavier_core/
note.rs

1
2use std::rc::Rc;
3
4use once_cell::unsync::Lazy;
5
6use crate::channel::Channel;
7use crate::clipper::Clipper;
8use crate::clipper;
9use crate::can_apply::CanApply;
10use crate::duration::Duration;
11use crate::trimmer::RateTrimmer;
12use crate::pitch::Pitch;
13use super::duration::{Numerator, Dots, Denominator};
14use super::have_start_tick::{HaveBaseStartTick, HaveStartTick};
15use super::percent::PercentU16;
16use super::pitch::PitchError;
17use super::trimmer::Trimmer;
18use super::velocity::{Velocity, self};
19use derive_builder::Builder;
20
21/// Error type for tick-related operations.
22///
23/// This error occurs when a tick calculation results in a negative value,
24/// which is not allowed in the MIDI timing system.
25#[derive(Clone, Copy, PartialEq, Eq, Debug)]
26pub enum TickError {
27    /// Indicates that a tick calculation resulted in a negative value.
28    Minus,
29}
30
31/// Error type for invalid dot count in note duration.
32///
33/// This error occurs when attempting to set a dot count that is outside
34/// the valid range (0 to `Duration::MAX_DOT`).
35#[derive(Clone, Copy, PartialEq, Eq, Debug)]
36pub struct InvalidDot(i32);
37
38/// Represents a musical note with timing, pitch, duration, and velocity information.
39///
40/// A `Note` is the fundamental building block of musical composition in this library.
41/// It contains all the information needed to play a single note, including:
42/// - Timing information (start tick)
43/// - Pitch (musical note and octave)
44/// - Duration (note length)
45/// - Velocity (how hard the note is played)
46/// - Tie information (for connecting notes)
47/// - Trimmers for fine-tuning timing, duration, and velocity
48///
49/// # Examples
50///
51/// ```
52/// use klavier_core::note::{Note, NoteBuilder};
53/// use klavier_core::pitch::Pitch;
54/// use klavier_core::solfa::Solfa;
55/// use klavier_core::octave::Octave;
56/// use klavier_core::sharp_flat::SharpFlat;
57/// use klavier_core::duration::{Duration, Numerator, Denominator, Dots};
58/// use klavier_core::velocity::Velocity;
59///
60/// // Create a note using the builder pattern
61/// let note = NoteBuilder::default()
62///     .base_start_tick(0)
63///     .pitch(Pitch::new(Solfa::C, Octave::Oct4, SharpFlat::Null))
64///     .duration(Duration::new(Numerator::Whole, Denominator::from_value(4).unwrap(), Dots::ZERO))
65///     .base_velocity(Velocity::new(100))
66///     .build()
67///     .unwrap();
68/// ```
69#[derive(serde::Deserialize, serde::Serialize, Default)]
70#[derive(Debug, PartialEq, Eq, Clone, Builder)]
71#[builder(default)]
72pub struct Note {
73    /// The base start tick position of the note (before any trimmer adjustments).
74    pub base_start_tick: u32,
75    
76    /// The pitch of the note (musical note, octave, and accidental).
77    pub pitch: Pitch,
78    
79    /// The duration of the note (note length).
80    pub duration: Duration,
81    
82    /// Whether this note is tied to the next note (tie start).
83    pub tie: bool,
84    
85    /// Whether this note is tied from the previous note (tie end).
86    pub tied: bool,
87    
88    /// The base velocity of the note (before any trimmer adjustments).
89    pub base_velocity: Velocity,
90    
91    /// Trimmer for adjusting the start tick position.
92    pub start_tick_trimmer: Trimmer,
93    
94    /// Trimmer for adjusting the duration as a rate multiplier.
95    pub duration_trimmer: RateTrimmer,
96    
97    /// Trimmer for adjusting the velocity.
98    pub velocity_trimmer: Trimmer,
99    
100    /// The MIDI channel for this note.
101    pub channel: Channel,
102}
103
104impl Note {
105    /// Returns the actual start tick of the note after applying the start tick trimmer.
106    ///
107    /// This method calculates the final start tick by adding the base start tick
108    /// and the trimmer adjustment. If the result would be negative, it returns 0.
109    ///
110    /// # Returns
111    ///
112    /// The actual start tick position (always >= 0).
113    #[inline]
114    pub fn start_tick(&self) -> u32 {
115        let tick = self.base_start_tick as i64 + self.start_tick_trimmer.sum() as i64;
116        if tick < 0 { 0 } else { tick as u32 }
117    }
118
119    /// Returns the actual tick length of the note after applying the duration trimmer.
120    ///
121    /// This method calculates the final duration by applying the rate trimmer
122    /// to the base duration's tick length.
123    ///
124    /// # Returns
125    ///
126    /// The actual tick length of the note.
127    #[inline]
128    pub fn tick_len(&self) -> u32 {
129        self.duration_trimmer.apply(self.duration.tick_length())
130    }
131
132    /// Creates a new note with the pitch raised by one semitone.
133    ///
134    /// # Returns
135    ///
136    /// - `Ok(Note)` - A new note with the pitch raised by one semitone.
137    /// - `Err(PitchError)` - If the pitch is already at the maximum value.
138    ///
139    /// # Examples
140    ///
141    /// ```
142    /// # use klavier_core::note::Note;
143    /// # use klavier_core::pitch::Pitch;
144    /// # use klavier_core::solfa::Solfa;
145    /// # use klavier_core::octave::Octave;
146    /// # use klavier_core::sharp_flat::SharpFlat;
147    /// let note = Note {
148    ///     pitch: Pitch::new(Solfa::C, Octave::Oct4, SharpFlat::Null),
149    ///     ..Default::default()
150    /// };
151    /// let higher_note = note.up_score_offset().unwrap();
152    /// assert_eq!(higher_note.pitch, Pitch::new(Solfa::D, Octave::Oct4, SharpFlat::Null));
153    /// ```
154    pub fn up_score_offset(&self) -> Result<Self, PitchError> {
155        self.pitch.up().map(|p| {
156            Self {
157                pitch: p,
158                ..*self
159            }
160        })
161    }
162    
163    /// Creates a new note with the pitch lowered by one semitone.
164    ///
165    /// # Returns
166    ///
167    /// - `Ok(Note)` - A new note with the pitch lowered by one semitone.
168    /// - `Err(PitchError)` - If the pitch is already at the minimum value.
169    pub fn down_score_offset(&self) -> Result<Self, PitchError> {
170        self.pitch.down().map(|p| {
171            Self {
172                pitch: p,
173                ..*self
174            }
175        })
176    }
177    
178    /// Creates a new note with the specified duration.
179    ///
180    /// # Arguments
181    ///
182    /// * `d` - The new duration for the note.
183    ///
184    /// # Returns
185    ///
186    /// A new note with the specified duration.
187    pub fn with_duration(&self, d: Duration) -> Self {
188        Self {
189            duration: d,
190            ..*self
191        }
192    }
193    
194    /// Creates a new note with the specified duration numerator.
195    ///
196    /// This method only updates the duration if the numerator is different
197    /// from the current one, optimizing for cases where no change is needed.
198    ///
199    /// # Arguments
200    ///
201    /// * `numerator` - The new numerator for the note's duration.
202    ///
203    /// # Returns
204    ///
205    /// A new note with the specified duration numerator.
206    pub fn with_duration_numerator(&self, numerator: Numerator) -> Self {
207        Self {
208            duration:
209                if self.duration.numerator != numerator {
210                    self.duration.with_numerator(numerator)
211                } else {
212                    self.duration
213                },
214            ..*self
215        }
216    }
217    
218    /// Creates a new note with the start tick adjusted by the specified delta.
219    ///
220    /// # Arguments
221    ///
222    /// * `tick_delta` - The amount to add to the start tick (can be negative).
223    /// * `is_trim` - If `true`, adjusts the trimmer; if `false`, adjusts the base start tick.
224    ///
225    /// # Returns
226    ///
227    /// - `Ok(Note)` - A new note with the adjusted start tick.
228    /// - `Err(TickError::Minus)` - If the resulting tick would be negative.
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// # use klavier_core::note::Note;
234    /// let note = Note {
235    ///     base_start_tick: 100,
236    ///     ..Default::default()
237    /// };
238    /// let later_note = note.with_tick_added(50, false).unwrap();
239    /// assert_eq!(later_note.base_start_tick, 150);
240    /// ```
241    pub fn with_tick_added(&self, tick_delta: i32, is_trim: bool) -> Result<Self, TickError> {
242        let tick = self.base_start_tick as i64 + tick_delta as i64;
243        if tick < 0 {
244            Err(TickError::Minus)
245        } else if is_trim {
246            let mut copied = self.clone();
247            copied.start_tick_trimmer = self.start_tick_trimmer.added(tick_delta);
248            Ok(copied)
249        } else {
250            Ok(
251                Self {
252                    base_start_tick: tick as u32,
253                    ..*self
254                }
255            )
256        }
257    }
258    
259    /// Creates a new note with both timing and pitch adjusted (for drag operations).
260    ///
261    /// This method is typically used for interactive editing where a note is
262    /// being dragged both horizontally (time) and vertically (pitch).
263    ///
264    /// # Arguments
265    ///
266    /// * `tick_delta` - The amount to add to the start tick.
267    /// * `score_offset_delta` - The amount to adjust the pitch (in semitones).
268    ///
269    /// # Returns
270    ///
271    /// A new note with adjusted timing and pitch.
272    ///
273    /// # Panics
274    ///
275    /// Panics if the pitch adjustment would result in an invalid pitch.
276    pub fn drag(&self, tick_delta: i32, score_offset_delta: i32) -> Self {
277        let tick = self.base_start_tick as i64 + tick_delta as i64;
278        let pitch = self.pitch.with_score_offset_delta(score_offset_delta).unwrap();
279        Self {
280            base_start_tick: tick as u32,
281            pitch,
282            ..*self
283        }
284    }
285
286    /// Creates a new note with additional dots added to the duration.
287    ///
288    /// Dots extend the duration of a note. Each dot adds half the value of the
289    /// previous duration component.
290    ///
291    /// # Arguments
292    ///
293    /// * `dots_to_add` - The number of dots to add (can be negative to remove dots).
294    ///
295    /// # Returns
296    ///
297    /// - `Ok(Note)` - A new note with the adjusted dot count.
298    /// - `Err(InvalidDot)` - If the resulting dot count is outside the valid range.
299    ///
300    /// # Examples
301    ///
302    /// ```
303    /// # use klavier_core::note::Note;
304    /// # use klavier_core::duration::{Duration, Numerator, Denominator, Dots};
305    /// let note = Note {
306    ///     duration: Duration::new(Numerator::Whole, Denominator::from_value(4).unwrap(), Dots::ZERO),
307    ///     ..Default::default()
308    /// };
309    /// let dotted_note = note.add_dots(1).unwrap();
310    /// assert_eq!(dotted_note.duration.dots, Dots::ONE);
311    /// ```
312    pub fn add_dots(&self, dots_to_add: i32) -> Result<Self, InvalidDot> {
313        let new_dots = self.duration.dots.value() as i32 + dots_to_add;
314        if new_dots < 0 || (Duration::MAX_DOT as i32) < new_dots {
315            Err(InvalidDot(new_dots))
316        } else {
317            Ok(
318                Self {
319                    duration: self.duration.with_dots(Dots::from_value(new_dots as u8).unwrap()),
320                    ..*self
321                }
322            )
323        }
324    }
325
326    /// Creates a new note with the sharp accidental toggled.
327    ///
328    /// If the note has no accidental, it becomes sharp.
329    /// If the note is already sharp, it becomes natural.
330    ///
331    /// # Returns
332    ///
333    /// - `Ok(Note)` - A new note with the sharp toggled.
334    /// - `Err(PitchError)` - If the operation would result in an invalid pitch.
335    pub fn toggle_sharp(&self) -> Result<Self, PitchError> {
336        self.pitch.toggle_sharp().map(|pitch| {
337            Self {
338                pitch,
339                ..*self
340            }
341        })
342    }
343
344    /// Creates a new note with the flat accidental toggled.
345    ///
346    /// If the note has no accidental, it becomes flat.
347    /// If the note is already flat, it becomes natural.
348    ///
349    /// # Returns
350    ///
351    /// - `Ok(Note)` - A new note with the flat toggled.
352    /// - `Err(PitchError)` - If the operation would result in an invalid pitch.
353    pub fn toggle_flat(&self) -> Result<Self, PitchError> {
354        self.pitch.toggle_flat().map(|pitch| {
355            Self {
356                pitch,
357                ..*self
358            }
359        })
360    }
361
362    /// Creates a new note with the natural accidental toggled.
363    ///
364    /// If the note has an accidental (sharp or flat), it becomes natural.
365    /// If the note is already natural, this may have no effect depending on the key signature.
366    ///
367    /// # Returns
368    ///
369    /// - `Ok(Note)` - A new note with the natural toggled.
370    /// - `Err(PitchError)` - If the operation would result in an invalid pitch.
371    pub fn toggle_natural(&self) -> Result<Self, PitchError> {
372        self.pitch.toggle_natural().map(|pitch| {
373            Self {
374                pitch,
375                ..*self
376            }
377        })
378    }
379
380    /// Creates a new note with the tie state toggled through its cycle.
381    ///
382    /// The tie state cycles through four states:
383    /// 1. No tie: `tie=false, tied=false`
384    /// 2. Tie start: `tie=true, tied=false`
385    /// 3. Tie end: `tie=false, tied=true`
386    /// 4. Tie middle: `tie=true, tied=true`
387    ///
388    /// # Returns
389    ///
390    /// A new note with the tie state advanced to the next state in the cycle.
391    pub fn toggle_tie(&self) -> Note {
392        let mut tie = self.tie;
393        let mut tied = self.tied;
394
395        if ! tie && ! tied {
396            tie = true;
397            tied = false;
398        } else if tie && ! tied {
399            tie = false;
400            tied = true;
401        } else if ! tie && tied {
402            tie = true;
403            tied = true;
404        } else {
405            tie = false;
406            tied = false;
407        }
408
409        Self {
410            tie, tied,
411            ..*self
412        }
413    }
414
415    /// Returns the base velocity of the note (before trimmer adjustments).
416    ///
417    /// # Returns
418    ///
419    /// The base velocity value.
420    #[inline]
421    pub fn base_velocity(&self) -> Velocity {
422        self.base_velocity
423    }
424
425    /// Returns the actual velocity of the note after applying the velocity trimmer.
426    ///
427    /// The velocity is clamped to the valid MIDI range (0-127).
428    ///
429    /// # Returns
430    ///
431    /// The actual velocity value (0-127).
432    pub fn velocity(&self) -> Velocity {
433        let mut v = self.base_velocity.as_u8() as i32;
434        v += self.velocity_trimmer.sum();
435        if v < 0 { velocity::MIN }
436        else if 127 < v { velocity::MAX }
437        else { Velocity::new(v as u8) }
438    }
439}
440
441impl Note {
442    /// The minimum tick value (always 0).
443    pub const MIN_TICK: i32 = 0;
444    
445    /// The maximum score offset value (76 semitones, covering the full MIDI range).
446    pub const MAX_SCORE_OFFSET: i32 = 76;
447    
448    /// Clipper for tick values, ensuring they stay within valid range.
449    pub const TICK_CLIPPER: Clipper<i32> = clipper::for_i32(0, i32::MAX);
450    
451    /// Clipper for velocity values, ensuring they stay within MIDI range (0-127).
452    pub const VELOCITY_CLIPPER: Clipper<i16> = clipper::for_i16(0, 127);
453    
454    /// The longest possible tick length for a note.
455    ///
456    /// This is calculated as a whole note with 7 dots at the slowest denominator,
457    /// multiplied by the maximum duration trimmer rate.
458    #[allow(clippy::declare_interior_mutable_const)]
459    pub const LONGEST_TICK_LEN: Lazy<u32> = Lazy::new(||
460        Duration::new(Numerator::Whole, Denominator::from_value(2).unwrap(), Dots::SEVEN).tick_length() * (PercentU16::MAX.to_f32() as u32)
461    );
462}
463
464/// The maximum tick length for a note (8 measures at standard resolution).
465pub const MAX_TICK_LEN: i32 = Duration::TICK_RESOLUTION * 8;
466
467impl HaveBaseStartTick for Note {
468    fn base_start_tick(&self) -> u32 {
469        self.base_start_tick
470    }
471}
472
473impl HaveStartTick for Note {
474    fn start_tick(&self) -> u32 {
475        self.start_tick()
476    }
477}
478
479impl HaveBaseStartTick for Rc<Note> {
480    fn base_start_tick(&self) -> u32 {
481        self.base_start_tick
482    }
483}
484
485impl HaveStartTick for Rc<Note> {
486    fn start_tick(&self) -> u32 {
487        <Note>::start_tick(self)
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use crate::{note::Note, pitch::{Pitch, self}, solfa::Solfa, octave::Octave, sharp_flat::SharpFlat, duration::{Duration, Numerator, Denominator, Dots}, trimmer::RateTrimmer, velocity::Velocity};
494
495    use super::NoteBuilder;
496    
497    #[test]
498    fn tick_len() {
499        let note = Note {
500            base_start_tick: 123,
501            pitch: Pitch::new(Solfa::A, Octave::Oct1, SharpFlat::Null),
502            duration: Duration::new(Numerator::Half, Denominator::from_value(2).unwrap(), Dots::ZERO),
503            base_velocity: Velocity::new(10),
504            duration_trimmer: RateTrimmer::new(1.0, 0.5, 2.0, 1.5), // duration_trimmer
505            ..Default::default()
506        };
507        assert_eq!(note.tick_len(), 720);
508    }
509    
510    #[test]
511    fn up_score_offset() {
512        let note = Note {
513            base_start_tick: 123,
514            pitch: pitch::MAX,
515            duration: Duration::new(Numerator::Half, Denominator::from_value(2).unwrap(), Dots::ZERO),
516            base_velocity: Velocity::new(10),
517            duration_trimmer: RateTrimmer::new(1.0, 0.5, 2.0, 1.5),
518            ..Default::default()
519        };
520        assert!(note.up_score_offset().is_err());
521        
522        let note = Note {
523            base_start_tick: 123,
524            pitch: Pitch::new(Solfa::A, Octave::Oct1, SharpFlat::Null),
525            duration: Duration::new(Numerator::Half, Denominator::from_value(2).unwrap(), Dots::ZERO),
526            base_velocity: Velocity::new(10),
527            duration_trimmer: RateTrimmer::new(1.0, 0.5, 2.0, 1.5),
528            ..Default::default()
529        };
530        assert_eq!(note.up_score_offset().unwrap().pitch, Pitch::new(Solfa::B, Octave::Oct1, SharpFlat::Null));
531    }
532    
533    #[test]
534    fn with_tick_added() {
535        let note = Note {
536            base_start_tick: 123,
537            pitch: pitch::MAX,
538            duration: Duration::new(Numerator::Half, Denominator::from_value(2).unwrap(), Dots::ZERO),
539            base_velocity: Velocity::new(10),
540            duration_trimmer: RateTrimmer::new(1.0, 0.5, 2.0, 1.5),
541            ..Default::default()
542        };
543        assert_eq!(note.with_tick_added(10, true).unwrap().start_tick(), 133);
544        assert_eq!(note.with_tick_added(-122, true).unwrap().start_tick(), 1);
545        assert_eq!(note.with_tick_added(-123, true).unwrap().start_tick(), 0);
546        assert!(note.with_tick_added(-124, true).is_err());
547    }
548    
549    #[test]
550    fn builder() {
551        let note_builder: NoteBuilder = NoteBuilder::default()
552          .base_start_tick(12u32)
553          .base_velocity(Velocity::new(99))
554          .clone();
555        
556        let note0 = note_builder.clone().base_start_tick(123u32).build().unwrap();
557        let note1 = note_builder.clone().build().unwrap();
558        
559        assert_eq!(note0.base_start_tick, 123);
560        assert_eq!(note1.base_start_tick, 12);
561    }
562}