Skip to main content

audiobox/synth/
score.rs

1use super::{Waveform, wave::Duty};
2use crate::{Error, Result};
3
4/// The most channels a score can have.
5pub const MAX_CHANNELS: usize = 8;
6/// The deepest nesting of repeats and patterns that [`ScoreLimits::max_nesting`] can allow.
7pub const MAX_NESTING: usize = 256;
8const MAX_MS: u32 = 60_000;
9
10/// Bounds on a score, checked while it is parsed or built.
11#[derive(Clone, Debug, PartialEq)]
12pub struct ScoreLimits {
13    /// Length of score text in bytes.
14    pub max_text_bytes: usize,
15    /// Depth of nested repeats and pattern references, at most [`MAX_NESTING`].
16    pub max_nesting: usize,
17    /// Number of channels, at most [`MAX_CHANNELS`].
18    pub max_channels: usize,
19    /// Notes in all channels after repeats and patterns are expanded.
20    pub max_notes: usize,
21    /// Commands executed while expanding repeats and patterns. Bounds parsing work
22    /// even for repeats of commands that produce no notes.
23    pub max_events: usize,
24    /// Length of the song once through, before loops.
25    pub max_duration_seconds: f64,
26}
27impl Default for ScoreLimits {
28    fn default() -> Self {
29        Self {
30            max_text_bytes: 1024 * 1024,
31            max_nesting: 16,
32            max_channels: MAX_CHANNELS,
33            max_notes: 200_000,
34            max_events: 1_000_000,
35            max_duration_seconds: 1200.0,
36        }
37    }
38}
39
40/// A MIDI note number, 0 through 127. Middle C (`o4 c`) is 60 and A4 (440 Hz) is 69.
41#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
42pub struct Pitch(u8);
43impl Pitch {
44    pub const A4: Pitch = Pitch(69);
45    pub const C4: Pitch = Pitch(60);
46    pub fn new(midi: u8) -> Result<Self> {
47        if midi > 127 {
48            return Err(Error::invalid("Pitch must be a MIDI note 0..=127"));
49        }
50        Ok(Self(midi))
51    }
52    /// A note letter `a`..`g`, a number of semitones to raise (`1` for sharp, `-1`
53    /// for flat) and an octave, where octave 4 starts at middle C.
54    pub fn from_name(letter: char, accidental: i8, octave: i8) -> Result<Self> {
55        let semitone = match letter.to_ascii_lowercase() {
56            'c' => 0,
57            'd' => 2,
58            'e' => 4,
59            'f' => 5,
60            'g' => 7,
61            'a' => 9,
62            'b' => 11,
63            _ => return Err(Error::invalid(format!("Unknown note letter {letter}"))),
64        };
65        let midi = (octave as i32 + 1) * 12 + semitone + accidental as i32;
66        if !(0..=127).contains(&midi) {
67            return Err(Error::invalid("Note is outside MIDI range 0..=127"));
68        }
69        Ok(Self(midi as u8))
70    }
71    pub fn midi(self) -> u8 {
72        self.0
73    }
74    /// Equal-tempered frequency with A4 at 440 Hz.
75    pub fn frequency(self) -> f64 {
76        crate::math::cents_to_hz((self.0 as f64 - 69.0) * 100.0)
77    }
78}
79
80/// A note length: a fraction of a whole note with dots, or an exact number of ticks.
81#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
82pub enum Length {
83    /// `denominator` 4 is a quarter note. Each dot adds half of the previous value.
84    Note {
85        denominator: u16,
86        dots: u8,
87    },
88    Ticks(u32),
89}
90impl Length {
91    pub const WHOLE: Length = Length::note(1);
92    pub const HALF: Length = Length::note(2);
93    pub const QUARTER: Length = Length::note(4);
94    pub const EIGHTH: Length = Length::note(8);
95    pub const SIXTEENTH: Length = Length::note(16);
96    pub const fn note(denominator: u16) -> Self {
97        Self::Note {
98            denominator,
99            dots: 0,
100        }
101    }
102    /// Adds a dot. A tick length is returned unchanged.
103    pub const fn dotted(self) -> Self {
104        match self {
105            Self::Note { denominator, dots } => Self::Note {
106                denominator,
107                dots: dots.saturating_add(1),
108            },
109            ticks => ticks,
110        }
111    }
112    /// Length in ticks. Fails unless it is a positive whole number of ticks.
113    pub fn ticks(self, ticks_per_quarter: u16) -> Result<u64> {
114        let (denominator, dots) = match self {
115            Self::Ticks(0) => return Err(Error::invalid("Length must be positive")),
116            Self::Ticks(t) => return Ok(t as u64),
117            Self::Note { denominator, dots } => (denominator as u64, dots),
118        };
119        let whole = 4 * ticks_per_quarter as u64;
120        let unrepresentable = || {
121            Error::invalid(format!(
122                "Length 1/{denominator}{} is not a whole number of ticks at {ticks_per_quarter} ticks per quarter",
123                ".".repeat(dots as usize)
124            ))
125        };
126        if denominator == 0 || whole % denominator != 0 {
127            return Err(unrepresentable());
128        }
129        let mut part = whole / denominator;
130        let mut total = part;
131        for _ in 0..dots {
132            if part % 2 != 0 {
133                return Err(unrepresentable());
134            }
135            part /= 2;
136            total += part;
137        }
138        Ok(total)
139    }
140}
141
142/// Linear attack, decay and release in milliseconds, and a sustain level in percent.
143/// A note's attack starts from the channel's current level, so retriggering does not click.
144#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
145pub struct Envelope {
146    pub attack_ms: u32,
147    pub decay_ms: u32,
148    /// 0 through 100.
149    pub sustain: u8,
150    pub release_ms: u32,
151}
152impl Default for Envelope {
153    fn default() -> Self {
154        Self {
155            attack_ms: 0,
156            decay_ms: 0,
157            sustain: 100,
158            release_ms: 0,
159        }
160    }
161}
162
163/// Sinusoidal pitch modulation that starts `delay_ms` after the note starts.
164#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
165pub struct Vibrato {
166    /// Peak deviation, up to 1200 cents.
167    pub depth_cents: u16,
168    pub period_ms: u32,
169    pub delay_ms: u32,
170}
171
172/// Semitone offsets cycled every `step_ms` from the start of each note.
173#[derive(Clone, Debug, Eq, Hash, PartialEq)]
174pub struct Arpeggio {
175    /// One to 16 offsets of -48..=48 semitones.
176    pub offsets: Vec<i8>,
177    pub step_ms: u32,
178}
179
180/// Duties that replace a pulse's duty one after another every `step_ms` from the
181/// start of each note. The last one holds.
182#[derive(Clone, Debug, Eq, Hash, PartialEq)]
183pub struct DutySweep {
184    /// One to 16 duties.
185    pub duties: Vec<Duty>,
186    pub step_ms: u32,
187}
188
189/// The sound of a channel: a waveform, an envelope, a volume and pitch effects.
190#[derive(Clone, Debug, Eq, Hash, PartialEq)]
191pub struct Instrument {
192    /// Optional polyphonic unison/filter/LFO path. `None` retains legacy samples.
193    pub synthwave: Option<super::Synthwave>,
194    /// Opt-in tracker sources, macros and quantization; `None` keeps legacy samples.
195    pub chip: Option<super::ChipInstrument>,
196    /// A finite synthesized one-shot instead of a melodic oscillator/ADSR.
197    pub drum: Option<super::Drum>,
198    pub waveform: Waveform,
199    pub envelope: Envelope,
200    /// 0 through 15. The default is 15.
201    pub volume: u8,
202    /// Constant pitch slide from the start of each note, in cents per second.
203    pub slide: i32,
204    pub vibrato: Option<Vibrato>,
205    pub arpeggio: Option<Arpeggio>,
206    /// Only for [`Waveform::Pulse`].
207    pub duty_sweep: Option<DutySweep>,
208}
209impl Default for Instrument {
210    fn default() -> Self {
211        Self::new(Waveform::default())
212    }
213}
214impl Instrument {
215    /// The waveform at full volume with the default envelope and no effects.
216    pub fn new(waveform: Waveform) -> Self {
217        Self {
218            synthwave: None,
219            chip: None,
220            drum: None,
221            waveform,
222            envelope: Envelope::default(),
223            volume: 15,
224            slide: 0,
225            vibrato: None,
226            arpeggio: None,
227            duty_sweep: None,
228        }
229    }
230    /// A drum at full instrument volume. Configure its envelopes in [`super::Drum`].
231    pub fn drum(settings: super::Drum) -> Self {
232        Self {
233            drum: Some(settings),
234            ..Self::default()
235        }
236    }
237    pub fn validate(&self) -> Result<()> {
238        if let Some(wide) = self.synthwave {
239            wide.validate()?;
240            // A wavetable source may take a voice pool, opting in to per-note voices;
241            // the rest of synthwave renders its own oscillators and stays exclusive.
242            let wavetable_voices = matches!(
243                self.chip.as_ref().map(|c| &c.source),
244                Some(super::ChipSource::Wavetable(_))
245            ) && wide
246                == super::Synthwave {
247                    voices: wide.voices,
248                    ..Default::default()
249                };
250            if self.chip.is_some() && !wavetable_voices {
251                return Err(Error::invalid(
252                    "A chip source takes only voices= from synthwave, and only with a wavetable",
253                ));
254            }
255            if self.drum.is_some() || matches!(self.waveform, Waveform::Noise(_)) {
256                return Err(Error::invalid(
257                    "Synthwave requires a melodic waveform without chip/drum sources",
258                ));
259            }
260        }
261        if let Some(chip) = &self.chip {
262            chip.validate(self.waveform)?;
263            if self.drum.is_some()
264                || (self.arpeggio.is_some() && chip.arpeggio.is_some())
265                || (self.duty_sweep.is_some() && chip.duty.is_some())
266            {
267                return Err(Error::invalid(
268                    "Conflicting drum/tracker or duplicate modulation controls",
269                ));
270            }
271            if !matches!(chip.source, super::ChipSource::Waveform) && self.duty_sweep.is_some() {
272                return Err(Error::invalid("Duty sweeps need a waveform source"));
273            }
274            if matches!(chip.source, super::ChipSource::Noise { .. })
275                && (self.slide != 0 || self.vibrato.is_some() || self.arpeggio.is_some())
276            {
277                return Err(Error::invalid(
278                    "Fixed-period noise does not take melodic pitch controls",
279                ));
280            }
281        }
282        if let Some(drum) = self.drum {
283            drum.validate()?;
284            if self.waveform != Waveform::default()
285                || self.envelope != Envelope::default()
286                || self.slide != 0
287                || self.vibrato.is_some()
288                || self.arpeggio.is_some()
289                || self.duty_sweep.is_some()
290            {
291                return Err(Error::invalid(
292                    "Drums use their own envelopes and oscillators, without melodic modulation",
293                ));
294            }
295        }
296        let e = &self.envelope;
297        if self.volume > 15 {
298            return Err(Error::invalid("Instrument volume must be 0..=15"));
299        }
300        if e.sustain > 100 {
301            return Err(Error::invalid("Sustain must be 0..=100 percent"));
302        }
303        if [e.attack_ms, e.decay_ms, e.release_ms]
304            .iter()
305            .any(|&ms| ms > MAX_MS)
306        {
307            return Err(Error::invalid("Envelope times must be at most 60000 ms"));
308        }
309        if !(-1_200_000..=1_200_000).contains(&self.slide) {
310            return Err(Error::invalid(
311                "Slide must be within ±1200000 cents per second",
312            ));
313        }
314        if let Some(v) = self.vibrato {
315            if v.depth_cents > 1200
316                || v.period_ms == 0
317                || v.period_ms > MAX_MS
318                || v.delay_ms > MAX_MS
319            {
320                return Err(Error::invalid(
321                    "Vibrato needs a depth of at most 1200 cents and a period and delay of at most 60000 ms, with a positive period",
322                ));
323            }
324        }
325        if let Some(a) = &self.arpeggio {
326            if a.offsets.is_empty()
327                || a.offsets.len() > 16
328                || a.offsets.iter().any(|o| !(-48..=48).contains(o))
329                || a.step_ms == 0
330                || a.step_ms > MAX_MS
331            {
332                return Err(Error::invalid(
333                    "Arpeggio needs 1 to 16 offsets of -48..=48 semitones and a step of 1..=60000 ms",
334                ));
335            }
336        }
337        if let Some(s) = &self.duty_sweep {
338            if !matches!(self.waveform, Waveform::Pulse(_)) {
339                return Err(Error::invalid("A duty sweep needs a pulse waveform"));
340            }
341            if s.duties.is_empty() || s.duties.len() > 16 || s.step_ms == 0 || s.step_ms > MAX_MS {
342                return Err(Error::invalid(
343                    "Duty sweep needs 1 to 16 duties and a step of 1..=60000 ms",
344                ));
345            }
346        }
347        Ok(())
348    }
349}
350
351/// A note with its position and length in ticks and the channel settings it was played with.
352#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
353pub struct Note {
354    /// Attack velocity, 0..=127. Existing `note` calls and unsuffixed text use 127.
355    pub velocity: u8,
356    pub start: u64,
357    pub length: u64,
358    pub pitch: Pitch,
359    /// Index into [`Score::instruments`].
360    pub instrument: usize,
361    /// Channel volume, 0 through 15.
362    pub volume: u8,
363    /// Fraction of the length in eighths, 1 through 8, before the release starts.
364    pub gate: u8,
365}
366
367#[derive(Clone, Debug)]
368pub struct Channel {
369    name: String,
370    notes: Vec<Note>,
371    length: u64,
372    instrument: Option<usize>,
373    volume: u8,
374    gate: u8,
375    last_group: usize,
376}
377impl Channel {
378    pub fn name(&self) -> &str {
379        &self.name
380    }
381    /// Notes in start order. Chord members share the same start and length.
382    pub fn notes(&self) -> &[Note] {
383        &self.notes
384    }
385    /// Length in ticks including trailing rests.
386    pub fn length(&self) -> u64 {
387        self.length
388    }
389}
390impl PartialEq for Channel {
391    fn eq(&self, other: &Self) -> bool {
392        self.name == other.name && self.notes == other.notes && self.length == other.length
393    }
394}
395
396/// A song of up to eight channels of notes, at one tempo.
397///
398/// Build it in code with [`Score::channel`], or parse the text format described in
399/// the [module documentation](super) with [`Score::parse`]. Both give equal scores.
400#[derive(Clone, Debug)]
401pub struct Score {
402    tempo: u16,
403    ticks_per_quarter: u16,
404    instruments: Vec<(String, Instrument)>,
405    channels: Vec<Channel>,
406    loop_tick: Option<u64>,
407    notes: usize,
408    limits: ScoreLimits,
409}
410impl PartialEq for Score {
411    fn eq(&self, other: &Self) -> bool {
412        self.tempo == other.tempo
413            && self.ticks_per_quarter == other.ticks_per_quarter
414            && self.instruments == other.instruments
415            && self.channels == other.channels
416            && self.loop_tick == other.loop_tick
417    }
418}
419impl Score {
420    /// `tempo` in quarter notes per minute, 1..=999, and a time base of 1..=960 ticks per quarter.
421    pub fn new(tempo: u16, ticks_per_quarter: u16) -> Result<Self> {
422        Self::with_limits(tempo, ticks_per_quarter, ScoreLimits::default())
423    }
424    pub fn with_limits(tempo: u16, ticks_per_quarter: u16, limits: ScoreLimits) -> Result<Self> {
425        if !(1..=999).contains(&tempo) {
426            return Err(Error::invalid("Tempo must be 1..=999"));
427        }
428        if !(1..=960).contains(&ticks_per_quarter) {
429            return Err(Error::invalid("Ticks per quarter must be 1..=960"));
430        }
431        if limits.max_channels > MAX_CHANNELS
432            || limits.max_nesting > MAX_NESTING
433            || !limits.max_duration_seconds.is_finite()
434            || limits.max_duration_seconds < 0.0
435        {
436            return Err(Error::invalid(
437                "Score limits allow at most 8 channels, a nesting of 256 and a finite, non-negative duration",
438            ));
439        }
440        Ok(Self {
441            tempo,
442            ticks_per_quarter,
443            instruments: Vec::new(),
444            channels: Vec::new(),
445            loop_tick: None,
446            notes: 0,
447            limits,
448        })
449    }
450    pub fn tempo(&self) -> u16 {
451        self.tempo
452    }
453    pub fn ticks_per_quarter(&self) -> u16 {
454        self.ticks_per_quarter
455    }
456    pub fn limits(&self) -> &ScoreLimits {
457        &self.limits
458    }
459    pub fn instruments(&self) -> &[(String, Instrument)] {
460        &self.instruments
461    }
462    pub fn instrument_index(&self, name: &str) -> Option<usize> {
463        self.instruments.iter().position(|(n, _)| n == name)
464    }
465    pub fn channels(&self) -> &[Channel] {
466        &self.channels
467    }
468    /// The tick where the song restarts after its end. Without one, it restarts at 0.
469    pub fn loop_tick(&self) -> Option<u64> {
470        self.loop_tick
471    }
472    /// The song's length in ticks: the longest channel.
473    pub fn length_ticks(&self) -> u64 {
474        self.channels.iter().map(|c| c.length).max().unwrap_or(0)
475    }
476    pub fn duration_seconds(&self) -> f64 {
477        self.ticks_to_seconds(self.length_ticks())
478    }
479    fn ticks_to_seconds(&self, ticks: u64) -> f64 {
480        ticks as f64 * 60.0 / (self.tempo as f64 * self.ticks_per_quarter as f64)
481    }
482    /// Adds a named instrument and returns its index.
483    pub fn add_instrument(
484        &mut self,
485        name: impl Into<String>,
486        instrument: Instrument,
487    ) -> Result<usize> {
488        let name = name.into();
489        if self.instrument_index(&name).is_some() {
490            return Err(Error::invalid(format!(
491                "Instrument {name} is already defined"
492            )));
493        }
494        if self.instruments.len() >= 256 {
495            return Err(Error::limit("A score has at most 256 instruments"));
496        }
497        instrument.validate()?;
498        self.instruments.push((name, instrument));
499        Ok(self.instruments.len() - 1)
500    }
501    /// Writes to the named channel, creating it after the existing ones. Writing to
502    /// it again continues where it ended, with the same instrument, volume and gate.
503    pub fn channel(&mut self, name: &str) -> Result<ChannelWriter<'_>> {
504        let index = match self.channels.iter().position(|c| c.name == name) {
505            Some(i) => i,
506            None => {
507                if self.channels.len() >= self.limits.max_channels {
508                    return Err(Error::limit(format!(
509                        "A score has at most {} channels",
510                        self.limits.max_channels
511                    )));
512                }
513                self.channels.push(Channel {
514                    name: name.to_owned(),
515                    notes: Vec::new(),
516                    length: 0,
517                    instrument: None,
518                    volume: 15,
519                    gate: 8,
520                    last_group: 0,
521                });
522                self.channels.len() - 1
523            }
524        };
525        Ok(ChannelWriter { score: self, index })
526    }
527}
528
529/// Appends notes and rests to one channel of a [`Score`].
530pub struct ChannelWriter<'a> {
531    score: &'a mut Score,
532    index: usize,
533}
534impl ChannelWriter<'_> {
535    pub(super) fn score(&self) -> &Score {
536        self.score
537    }
538    fn state(&mut self) -> &mut Channel {
539        &mut self.score.channels[self.index]
540    }
541    /// The position in ticks where the next note or rest starts.
542    pub fn position(&self) -> u64 {
543        self.score.channels[self.index].length
544    }
545    /// Selects the instrument for the following notes, by its index.
546    pub fn instrument(&mut self, index: usize) -> Result<&mut Self> {
547        if index >= self.score.instruments.len() {
548            return Err(Error::invalid("Instrument index out of range"));
549        }
550        self.state().instrument = Some(index);
551        Ok(self)
552    }
553    /// Channel volume for the following notes, 0 through 15. The default is 15.
554    pub fn volume(&mut self, volume: u8) -> Result<&mut Self> {
555        if volume > 15 {
556            return Err(Error::invalid("Volume must be 0..=15"));
557        }
558        self.state().volume = volume;
559        Ok(self)
560    }
561    /// The part of each following note, in eighths, played before its release. The default is 8.
562    pub fn gate(&mut self, eighths: u8) -> Result<&mut Self> {
563        if !(1..=8).contains(&eighths) {
564            return Err(Error::invalid("Gate must be 1..=8 eighths"));
565        }
566        self.state().gate = eighths;
567        Ok(self)
568    }
569    pub fn note(&mut self, pitch: Pitch, length: Length) -> Result<&mut Self> {
570        self.note_with_velocity(pitch, length, 127)
571    }
572    /// Appends a note with velocity 0..=127, independently of channel volume.
573    pub fn note_with_velocity(
574        &mut self,
575        pitch: Pitch,
576        length: Length,
577        velocity: u8,
578    ) -> Result<&mut Self> {
579        self.append_group(&[pitch], length, velocity, false)
580    }
581    /// Appends 1..=16 distinct simultaneous pitches, advancing once. Requires a
582    /// synthwave instrument and no more members than its configured voice limit.
583    pub fn chord(&mut self, pitches: &[Pitch], length: Length) -> Result<&mut Self> {
584        self.chord_with_velocity(pitches, length, 127)
585    }
586    pub fn chord_with_velocity(
587        &mut self,
588        pitches: &[Pitch],
589        length: Length,
590        velocity: u8,
591    ) -> Result<&mut Self> {
592        self.append_group(pitches, length, velocity, true)
593    }
594    fn append_group(
595        &mut self,
596        pitches: &[Pitch],
597        length: Length,
598        velocity: u8,
599        chord: bool,
600    ) -> Result<&mut Self> {
601        if velocity > 127 {
602            return Err(Error::invalid("Velocity must be 0..=127"));
603        }
604        let ticks = length.ticks(self.score.ticks_per_quarter)?;
605        let channel = &self.score.channels[self.index];
606        let instrument = channel
607            .instrument
608            .ok_or_else(|| Error::invalid("Select an instrument before the first note"))?;
609        if pitches.is_empty()
610            || pitches.len() > 16
611            || pitches
612                .iter()
613                .enumerate()
614                .any(|(i, p)| pitches[..i].contains(p))
615        {
616            return Err(Error::invalid("A chord needs 1..=16 distinct pitches"));
617        }
618        if chord
619            && self.score.instruments[instrument]
620                .1
621                .synthwave
622                .is_none_or(|s| pitches.len() > s.voices as usize)
623        {
624            return Err(Error::invalid(
625                "Chord needs a synthwave instrument with enough voices",
626            ));
627        }
628        if pitches.len() > self.score.limits.max_notes.saturating_sub(self.score.notes) {
629            return Err(Error::limit("Score exceeds its note limit"));
630        }
631        let note = Note {
632            velocity,
633            start: channel.length,
634            length: ticks,
635            pitch: pitches[0],
636            instrument,
637            volume: channel.volume,
638            gate: channel.gate,
639        };
640        self.advance(ticks)?;
641        self.score.notes += pitches.len();
642        let channel = self.state();
643        for &pitch in pitches {
644            channel.notes.push(Note { pitch, ..note });
645        }
646        channel.last_group = pitches.len();
647        Ok(self)
648    }
649    pub fn rest(&mut self, length: Length) -> Result<&mut Self> {
650        let ticks = length.ticks(self.score.ticks_per_quarter)?;
651        self.advance(ticks)?;
652        self.state().last_group = 0;
653        Ok(self)
654    }
655    /// Lengthens the previous note, or the previous rest, by `length`.
656    pub fn tie(&mut self, length: Length) -> Result<&mut Self> {
657        let ticks = length.ticks(self.score.ticks_per_quarter)?;
658        if self.position() == 0 {
659            return Err(Error::invalid("A tie needs a preceding note or rest"));
660        }
661        self.advance(ticks)?;
662        let channel = self.state();
663        for note in channel.notes.iter_mut().rev().take(channel.last_group) {
664            note.length += ticks;
665        }
666        Ok(self)
667    }
668    /// Marks the current position as the song's loop point. All channels that mark
669    /// one must mark the same tick.
670    pub fn loop_point(&mut self) -> Result<&mut Self> {
671        let tick = self.position();
672        match self.score.loop_tick {
673            Some(t) if t != tick => {
674                return Err(Error::invalid(format!(
675                    "Loop point at tick {tick} differs from the loop point at tick {t}"
676                )));
677            }
678            _ => self.score.loop_tick = Some(tick),
679        }
680        Ok(self)
681    }
682    fn advance(&mut self, ticks: u64) -> Result<()> {
683        let end = self
684            .position()
685            .checked_add(ticks)
686            .filter(|&end| {
687                self.score.ticks_to_seconds(end) <= self.score.limits.max_duration_seconds
688            })
689            .ok_or_else(|| Error::limit("Score exceeds its duration limit"))?;
690        self.state().length = end;
691        Ok(())
692    }
693}