1use super::{Waveform, wave::Duty};
2use crate::{Error, Result};
3
4pub const MAX_CHANNELS: usize = 8;
6pub const MAX_NESTING: usize = 256;
8const MAX_MS: u32 = 60_000;
9
10#[derive(Clone, Debug, PartialEq)]
12pub struct ScoreLimits {
13 pub max_text_bytes: usize,
15 pub max_nesting: usize,
17 pub max_channels: usize,
19 pub max_notes: usize,
21 pub max_events: usize,
24 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#[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 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 pub fn frequency(self) -> f64 {
76 crate::math::cents_to_hz((self.0 as f64 - 69.0) * 100.0)
77 }
78}
79
80#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
82pub enum Length {
83 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 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 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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
145pub struct Envelope {
146 pub attack_ms: u32,
147 pub decay_ms: u32,
148 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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
165pub struct Vibrato {
166 pub depth_cents: u16,
168 pub period_ms: u32,
169 pub delay_ms: u32,
170}
171
172#[derive(Clone, Debug, Eq, Hash, PartialEq)]
174pub struct Arpeggio {
175 pub offsets: Vec<i8>,
177 pub step_ms: u32,
178}
179
180#[derive(Clone, Debug, Eq, Hash, PartialEq)]
183pub struct DutySweep {
184 pub duties: Vec<Duty>,
186 pub step_ms: u32,
187}
188
189#[derive(Clone, Debug, Eq, Hash, PartialEq)]
191pub struct Instrument {
192 pub synthwave: Option<super::Synthwave>,
194 pub chip: Option<super::ChipInstrument>,
196 pub drum: Option<super::Drum>,
198 pub waveform: Waveform,
199 pub envelope: Envelope,
200 pub volume: u8,
202 pub slide: i32,
204 pub vibrato: Option<Vibrato>,
205 pub arpeggio: Option<Arpeggio>,
206 pub duty_sweep: Option<DutySweep>,
208}
209impl Default for Instrument {
210 fn default() -> Self {
211 Self::new(Waveform::default())
212 }
213}
214impl Instrument {
215 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 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 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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
353pub struct Note {
354 pub velocity: u8,
356 pub start: u64,
357 pub length: u64,
358 pub pitch: Pitch,
359 pub instrument: usize,
361 pub volume: u8,
363 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 pub fn notes(&self) -> &[Note] {
383 &self.notes
384 }
385 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#[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 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 pub fn loop_tick(&self) -> Option<u64> {
470 self.loop_tick
471 }
472 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 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 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
529pub 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 pub fn position(&self) -> u64 {
543 self.score.channels[self.index].length
544 }
545 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 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 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 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 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 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 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}