From 183a467a3ed57f01dc0277d77c3461b7df226e71 Mon Sep 17 00:00:00 2001 From: Ivan L Date: Tue, 6 Feb 2024 21:32:52 +0400 Subject: [PATCH] Performance --- Cargo.toml | 3 + src/lib.rs | 2 + src/music/interval.rs | 8 + src/music/mod.rs | 41 +++- src/music/performance.rs | 487 +++++++++++++++++++++++++++++++++++++++ src/music/phrases.rs | 111 +++++++++ 6 files changed, 641 insertions(+), 11 deletions(-) create mode 100644 src/music/performance.rs create mode 100644 src/music/phrases.rs diff --git a/Cargo.toml b/Cargo.toml index 6f9e8a6..aad5f2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,5 +14,8 @@ repository = "https://github.com/tsionyx/musik" [dependencies] num-rational = "0.4" num-integer = "0.1" +num-traits = "0.2" enum-map ="2.6" enum-iterator = "1.4" +ordered-float = "4.2" +itertools = "0.12" diff --git a/src/lib.rs b/src/lib.rs index 2b35767..c7fc023 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,8 @@ pub use music::{ adapters::TrillOptions, duration::Dur, interval::{AbsPitch, Interval, Octave}, + performance::Performance, + phrases::PhraseAttribute, pitch::{Pitch, PitchClass}, rests, Music, }; diff --git a/src/music/interval.rs b/src/music/interval.rs index 989b927..09ef86b 100644 --- a/src/music/interval.rs +++ b/src/music/interval.rs @@ -191,6 +191,14 @@ impl From for AbsPitch { } } +impl Add for AbsPitch { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + impl Add for AbsPitch { type Output = Self; diff --git a/src/music/mod.rs b/src/music/mod.rs index f0a4079..292fdcf 100644 --- a/src/music/mod.rs +++ b/src/music/mod.rs @@ -7,11 +7,15 @@ use super::instruments::InstrumentName; pub(crate) mod adapters; pub(crate) mod duration; pub(crate) mod interval; +pub(crate) mod performance; +pub(crate) mod phrases; pub(crate) mod pitch; use self::{ duration::Dur, - interval::{AbsPitch, Octave}, + interval::{Interval, Octave}, + performance::NoteAttribute, + phrases::PhraseAttribute, pitch::{Pitch, PitchClass}, }; @@ -33,8 +37,7 @@ impl

Primitive

{ } } -#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] -pub struct PlayerName(String); +pub type PlayerName = String; #[derive(Debug, PartialEq, Eq, Copy, Clone, PartialOrd, Ord)] pub enum Mode { @@ -45,9 +48,9 @@ pub enum Mode { #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub enum Control { Tempo(Ratio), // scale the tempo - Transpose(AbsPitch), + Transpose(Interval), Instrument(InstrumentName), - //TODO: Phrase(Vec), + Phrase(Vec), Player(PlayerName), KeySig(PitchClass, Mode), // key signature and mode } @@ -128,17 +131,17 @@ impl

Music

{ self.with(Control::Tempo(tempo.into())) } - pub fn with_transpose(self, abs_pitch: AbsPitch) -> Self { - self.with(Control::Transpose(abs_pitch)) + pub fn with_transpose(self, delta: Interval) -> Self { + self.with(Control::Transpose(delta)) } pub fn with_instrument(self, name: impl Into) -> Self { self.with(Control::Instrument(name.into())) } - //fn with_phrase(self, attributes: Vec) -> Self { - // self.with(Control::Phrase(attributes)) - //} + pub fn with_phrase(self, attributes: Vec) -> Self { + self.with(Control::Phrase(attributes)) + } pub fn with_player(self, name: PlayerName) -> Self { self.with(Control::Player(name)) @@ -203,7 +206,7 @@ impl

Music

{ } } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct Volume(pub u8); impl Volume { @@ -222,6 +225,22 @@ impl Music { } } +pub type AttrNote = (Pitch, Vec); + +pub type MusicAttr = Music; + +impl From for MusicAttr { + fn from(value: Music) -> Self { + value.map(|pitch| (pitch, vec![])) + } +} + +impl From> for MusicAttr { + fn from(value: Music<(Pitch, Volume)>) -> Self { + value.map(|(pitch, vol)| (pitch, vec![NoteAttribute::Volume(vol)])) + } +} + impl Music { def_note_constructor![Aff, Af, A, As, Ass]; def_note_constructor![Bff, Bf, B, Bs, Bss]; diff --git a/src/music/performance.rs b/src/music/performance.rs new file mode 100644 index 0000000..f878aa7 --- /dev/null +++ b/src/music/performance.rs @@ -0,0 +1,487 @@ +use std::{collections::HashMap, fmt}; + +use itertools::Itertools as _; +use num_rational::Ratio; +use ordered_float::OrderedFloat; + +use crate::instruments::InstrumentName; + +use super::{ + duration::Dur, interval::AbsPitch, phrases::PhraseAttribute, pitch::PitchClass, Control, Mode, + Music, PlayerName, Primitive, Volume, +}; + +#[derive(Debug)] +/// [`Performance`] is a time-ordered sequence +/// of musical [`events`][Event]. +pub struct Performance { + repr: Vec, +} + +impl

Music

{ + pub fn perform<'p>(&self, players: &'p PlayerMap

, ctx: Context<'p, P>) -> Performance { + self.perf(players, ctx).0 + } + + fn perf<'p>( + &self, + players: &'p PlayerMap

, + mut ctx: Context<'p, P>, + ) -> (Performance, Duration) { + match self { + Self::Prim(Primitive::Note(d, p)) => { + let dur = d.into_ratio() * ctx.whole_note; + ((ctx.player.play_note)(ctx, *d, p), dur) + } + Self::Prim(Primitive::Rest(d)) => ( + Performance { repr: vec![] }, + d.into_ratio() * ctx.whole_note, + ), + Self::Sequential(m1, m2) => { + let (mut p1, d1) = m1.perf(players, ctx.clone()); + ctx.start_time += d1; + let (p2, d2) = m2.perf(players, ctx); + p1.repr.extend(p2.repr); + (p1, d1 + d2) + } + Self::Parallel(m1, m2) => { + let (p1, d1) = m1.perf(players, ctx.clone()); + let (p2, d2) = m2.perf(players, ctx); + ( + Performance { + repr: p1 + .repr + .into_iter() + // use simple `.merge()` for perfectly commutative `Self::Parallel` + .merge_by(p2.repr, |x, y| x.start_time < y.start_time) + .collect(), + }, + d1.max(d2), + ) + } + Self::Modify(Control::Tempo(t), m) => { + ctx.whole_note /= convert_ratio(*t); + m.perf(players, ctx) + } + Self::Modify(Control::Transpose(p), m) => { + ctx.pitch = ctx.pitch + *p; + m.perf(players, ctx) + } + Self::Modify(Control::Instrument(i), m) => { + ctx.instrument = i.clone(); + m.perf(players, ctx) + } + Self::Modify(Control::Phrase(phrases), m) => { + (ctx.player.interpret_phrase)(m, players, ctx, phrases) + } + Self::Modify(Control::Player(p), m) => { + // TODO + let player = players.get(p).expect("not found player"); + ctx.player = player; + m.perf(players, ctx) + } + Self::Modify(Control::KeySig(pc, mode), m) => { + ctx.key = (*pc, *mode); + m.perf(players, ctx) + } + } + } +} + +fn convert_ratio(x: Ratio) -> Ratio +where + U: From + Clone + num_integer::Integer, +{ + let (num, denom) = x.into(); + Ratio::new(U::from(num), U::from(denom)) +} + +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] +/// The playing of one individual note. +pub struct Event { + start_time: TimePoint, + instrument: InstrumentName, + pitch: AbsPitch, + duration: Duration, + volume: Volume, + /// Used for instruments [other than MIDI][InstrumentName::Custom]. + params: Vec>, +} + +/// Measured in seconds both. +pub type TimePoint = Ratio; +pub type Duration = Ratio; + +#[derive(Debug)] +/// The state of the [`Performance`] that changes +/// as we go through the interpretation. +pub struct Context<'p, P> { + start_time: TimePoint, + player: &'p Player

, + instrument: InstrumentName, + whole_note: Duration, + pitch: AbsPitch, + volume: Volume, + key: (PitchClass, Mode), +} + +impl<'p, P> Clone for Context<'p, P> { + fn clone(&self) -> Self { + let Self { + start_time, + player, + instrument, + whole_note, + pitch, + volume, + key, + } = self; + Self { + start_time: *start_time, + player: *player, + instrument: instrument.clone(), + whole_note: *whole_note, + pitch: *pitch, + volume: *volume, + key: *key, + } + } +} + +/// Defines a tempo of X notes per minute +fn metro(setting: u32, note_dur: Dur) -> Duration { + Ratio::from_integer(60) / (Ratio::from_integer(setting) * note_dur.into_ratio()) +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum NoteAttribute { + Volume(Volume), + Fingering(u32), + Dynamics(String), + Params(Vec>), +} + +type PlayerMap

= HashMap>; + +pub struct Player

{ + pub name: String, + pub play_note: NoteFun

, + pub interpret_phrase: PhraseFun

, + notate_player: NotateFun

, +} + +impl

fmt::Debug for Player

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Player {}", self.name) + } +} + +type NoteFun

= Box, Dur, &P) -> Performance>; +type PhraseFun

= Box< + dyn Fn(&Music

, &PlayerMap

, Context

, &[PhraseAttribute]) -> (Performance, Duration), +>; +// TODO: producing a properly notated score is not defined yet +type NotateFun

= std::marker::PhantomData

; + +mod defaults { + use num_traits::{ops::checked::CheckedSub as _, One as _}; + + use crate::instruments::StandardMidiInstrument; + + use super::{ + super::{ + phrases::{Articulation, Dynamic, Tempo}, + pitch::Pitch, + AttrNote, + }, + *, + }; + + pub fn default_play_note( + attr_modifier: NoteWithAttributeHandler, + ) -> NoteFun<(Pitch, Vec)> + where + Attr: 'static, + { + Box::new(move |ctx, dur, (note_pitch, attrs)| { + let Context { + start_time, + player: _ignore_player, + instrument, + whole_note, + pitch, + volume, + key: _ignore_key, + } = ctx.clone(); + let init = Event { + start_time, + instrument, + pitch: note_pitch.abs() + pitch, + duration: dur.into_ratio() * whole_note, + volume, + params: vec![], + }; + + let event = attrs + .iter() + .fold(init, |acc, attr| attr_modifier(&ctx, attr, acc)); + Performance { repr: vec![event] } + }) + } + + pub fn default_note_attribute_handler

() -> NoteWithAttributeHandler { + Box::new(|_ignore_context, attr, event| match attr { + NoteAttribute::Volume(vol) => Event { + volume: *vol, + ..event + }, + NoteAttribute::Params(params) => Event { + params: params.clone(), + ..event + }, + NoteAttribute::Fingering(_) | NoteAttribute::Dynamics(_) => event, + }) + } + + /// Transform the event according to [`Context`] and Attribute. + type NoteWithAttributeHandler = + Box)>, &Attr, Event) -> Event>; + + // Transform the whole performance according to [`Context`] and [`PhraseAttribute`]. + // type PhraseAttributeHandler = Box Performance>; + + pub fn default_interpret_phrase(attr_modifier: PhraseF) -> PhraseFun

+ where + Player

: Default, + PhraseF: Fn(Performance, &PhraseAttribute) -> Performance + 'static, + { + Box::new(move |music, players, ctx, attrs| { + let (perf, dur) = music.perf(players, ctx); + let perf = attrs.iter().fold(perf, &attr_modifier); + (perf, dur) + }) + } + + pub fn default_phrase_attribute_handler( + perf: Performance, + attr: &PhraseAttribute, + ) -> Performance { + match attr { + PhraseAttribute::Dyn(Dynamic::Accent(x)) => perf.map(|event| Event { + volume: Volume((x * Ratio::from_integer(event.volume.0)).to_integer()), + ..event + }), + PhraseAttribute::Art(Articulation::Staccato(x)) => perf.map(|event| Event { + duration: x * event.duration, + ..event + }), + PhraseAttribute::Art(Articulation::Legato(x)) => perf.map(|event| Event { + duration: x * event.duration, + ..event + }), + + PhraseAttribute::Dyn(_) + | PhraseAttribute::Tmp(_) + | PhraseAttribute::Art(_) + | PhraseAttribute::Orn(_) => perf, + } + } + + impl Performance { + fn map(self, f: F) -> Self + where + F: FnMut(Event) -> Event, + { + Self { + repr: self.repr.into_iter().map(f).collect(), + } + } + } + + impl Default for Player { + fn default() -> Self { + Self { + name: "Default".to_string(), + play_note: default_play_note(default_note_attribute_handler()), + interpret_phrase: default_interpret_phrase(default_phrase_attribute_handler), + notate_player: Default::default(), + } + } + } + + pub fn fancy_interpret_phrase

( + music: &Music

, + players: &PlayerMap

, + mut ctx: Context

, + attrs: &[PhraseAttribute], + ) -> (Performance, Duration) + where + Player

: Default, + { + let last_volume_phrase = attrs.iter().fold(None, |found, pa| match pa { + // ignore the previous volume if found new one + PhraseAttribute::Dyn(Dynamic::StdLoudness(std_loud)) => Some(std_loud.get_volume()), + PhraseAttribute::Dyn(Dynamic::Loudness(vol)) => Some(*vol), + _ => found, + }); + + if let Some(volume) = last_volume_phrase { + ctx.volume = volume; + } + + let (perf, dur) = + default_interpret_phrase(fancy_phrase_attribute_handler)(music, players, ctx, attrs); + + let t0 = match perf.repr.first().map(|e| e.start_time) { + Some(t) => t, + None => { + return (perf, dur); + } + }; + + let inflate = |event: Event, coef: Ratio, sign: bool| { + let r = coef / dur; + let dt = event.start_time - t0; + let coef_event = dt * r; + let shift = if sign { + Ratio::one() + coef_event + } else { + // for `sign=false`, the `coef` should belong + // to the range `[0 (no changes)..1 (fade out to zero)]` + Ratio::one().checked_sub(&coef_event).unwrap_or_default() + }; + + let new_volume = Ratio::from(u32::from(event.volume.0)) * shift; + Event { + volume: Volume(new_volume.to_integer() as u8), + ..event + } + }; + + let stretch = |event: Event, coef: Ratio, sign: bool| { + let r = coef / dur; + let dt = event.start_time - t0; + let time_coef_event = dt * r; + let dur_coef_event = (Ratio::from(2) * dt + event.duration) * r; + + let (time_shift, dur_shift) = if sign { + ( + Ratio::one() + time_coef_event, + Ratio::one() + dur_coef_event, + ) + } else { + ( + // for `sign=false`, the `coef` should belong + // to the range `[0 (no changes)..1 (shrink to point)]` + Ratio::one() + .checked_sub(&time_coef_event) + .unwrap_or_default(), + // for `sign=false`, the `coef` should belong + // to the range `[0 (no changes)..0.5 (shrink to point)]` + Ratio::one() + .checked_sub(&dur_coef_event) + .unwrap_or_default(), + ) + }; + + Event { + start_time: time_shift * dt + t0, + duration: dur_shift * event.duration, + ..event + } + }; + + attrs + .iter() + .fold((perf, dur), |(perf, dur), attr| match attr { + PhraseAttribute::Dyn(Dynamic::Crescendo(x)) => { + let perf = perf.map(|e| inflate(e, *x, true)); + (perf, dur) + } + PhraseAttribute::Dyn(Dynamic::Diminuendo(x)) => { + let perf = perf.map(|e| inflate(e, *x, false)); + (perf, dur) + } + PhraseAttribute::Tmp(Tempo::Ritardando(x)) => { + let perf = perf.map(|e| stretch(e, *x, true)); + let dur = (Ratio::one() + *x) * dur; + (perf, dur) + } + PhraseAttribute::Tmp(Tempo::Accelerando(x)) => { + let perf = perf.map(|e| stretch(e, *x, false)); + let dur = Ratio::one().checked_sub(x).unwrap_or_default() * dur; + (perf, dur) + } + _ => (perf, dur), + }) + } + + fn fancy_phrase_attribute_handler(perf: Performance, attr: &PhraseAttribute) -> Performance { + match attr { + PhraseAttribute::Dyn(Dynamic::Accent(x)) => perf.map(|event| Event { + volume: Volume((x * Ratio::from_integer(event.volume.0)).to_integer()), + ..event + }), + PhraseAttribute::Dyn(_) | PhraseAttribute::Tmp(_) => { + // already handled in the fancy_interpret_phrase + perf + } + PhraseAttribute::Art(Articulation::Staccato(x)) => perf.map(|event| Event { + duration: x * event.duration, + ..event + }), + PhraseAttribute::Art(Articulation::Legato(x)) => perf.map(|event| Event { + duration: x * event.duration, + ..event + }), + PhraseAttribute::Art(Articulation::Slurred(x)) => { + // the same as Legato, but do not extend the duration of the last note(s) + let last_start_time = perf.repr.iter().map(|e| e.start_time).max(); + if let Some(last_start_time) = last_start_time { + perf.map(|event| { + if event.start_time < last_start_time { + Event { + duration: x * event.duration, + ..event + } + } else { + event + } + }) + } else { + perf + } + } + PhraseAttribute::Art(_) | PhraseAttribute::Orn(_) => perf, + } + } + + impl

Player

+ where + P: 'static, + Self: Default, + { + /// All like the [default][Self::default] one but + /// with changed interpretations of the [phrases][PhraseAttribute]. + pub fn fancy() -> Self { + Self { + interpret_phrase: Box::new(fancy_interpret_phrase), + ..Self::default() + } + } + } + + impl<'p, P> Context<'p, P> { + pub fn with_player(player: &'p Player

) -> Self { + Self { + start_time: TimePoint::from_integer(0), + player, + instrument: StandardMidiInstrument::AcousticGrandPiano.into(), + whole_note: metro(120, Dur::QN), + pitch: AbsPitch::from(0), + volume: Volume::loudest(), + key: (PitchClass::C, Mode::Major), + } + } + } +} diff --git a/src/music/phrases.rs b/src/music/phrases.rs new file mode 100644 index 0000000..a638ab3 --- /dev/null +++ b/src/music/phrases.rs @@ -0,0 +1,111 @@ +use enum_iterator::Sequence; +use enum_map::Enum; + +use crate::music::Volume; + +type Rational = num_rational::Ratio; + +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum PhraseAttribute { + Dyn(Dynamic), + Tmp(Tempo), + Art(Articulation), + Orn(Ornament), +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum Dynamic { + Accent(num_rational::Ratio), + Crescendo(Rational), + Diminuendo(Rational), + StdLoudness(StdLoudness), + Loudness(Volume), +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Enum, Sequence)] +pub enum StdLoudness { + PianoPianissimo, + Pianissimo, + Piano, + MezzoPiano, + Sforzato, + MezzoForte, + Nf, + Fortissimo, + ForteFortissimo, +} + +impl StdLoudness { + pub const fn get_volume(self) -> Volume { + let vol = match self { + Self::PianoPianissimo => 40, + Self::Pianissimo => 50, + Self::Piano => 60, + Self::MezzoPiano => 70, + Self::Sforzato => 80, + Self::MezzoForte => 90, + Self::Nf => 100, + Self::Fortissimo => 110, + Self::ForteFortissimo => 120, + }; + Volume(vol) + } +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum Tempo { + Ritardando(Rational), + Accelerando(Rational), +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum Articulation { + Staccato(Rational), + Legato(Rational), + Slurred(Rational), + Tenuto, + Marcato, + Pedal, + Fermata, + FermataDown, + Breath, + DownBow, + UpBow, + Harmonic, + Pizzicato, + LeftPizz, + BartokPizz, + Swell, + Wedge, + Thumb, + Stopped, +} + +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum Ornament { + Trill, + Mordent, + InvMordent, + DoubleMordent, + Turn, + TrilledTurn, + ShortTrill, + Arpeggio, + ArpeggioUp, + ArpeggioDown, + Instruction(String), + Head(NoteHead), + DiatonicTrans(u8), +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum NoteHead { + DiamondHead, + SquareHead, + XHead, + TriangleHead, + TremoloHead, + SlashHead, + ArtHarmonic, + NoHead, +}