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}