use std::any::Any;
use std::collections::BTreeMap;
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use sim_lib_music_core::{Articulation, Channel, Time};
use sim_lib_pitch_dissonance::ContextualSonanceOptions;
use sim_lib_pitch_scale::{PlayerScale, Scale};
use sim_lib_sound_tuning::Tuning;
use crate::{ErasedParameterBinding, SerialEventId, SerialPlan, SerialRealization, VoiceId};
fn validate_id(kind: &'static str, value: impl Into<String>) -> Result<String, String> {
let value = value.into();
if value.trim().is_empty() {
return Err(format!("{kind} cannot be empty"));
}
if value
.chars()
.any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '_' | '.')))
{
return Err(format!(
"{kind} must use ASCII letters, digits, /, -, _, or ."
));
}
Ok(value)
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RealizerId(String);
impl RealizerId {
pub fn new(value: impl Into<String>) -> Result<Self, String> {
Ok(Self(validate_id("realizer-id", value)?))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for RealizerId {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EventSound {
Notes,
Rest,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrictPitchLayout {
pub register: i8,
pub octave_displacements: Vec<i8>,
}
impl StrictPitchLayout {
pub fn in_register(register: i8) -> Self {
Self {
register,
octave_displacements: Vec::new(),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TiePolicy {
None,
IntoNext,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SimultaneousRenderPolicy {
PreserveOnset,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrictEventSpec {
pub sound: EventSound,
pub pitch_layout: StrictPitchLayout,
pub duration: Time,
pub velocity: u8,
pub channel: Channel,
pub articulation: Articulation,
pub tie: TiePolicy,
}
impl StrictEventSpec {
pub fn notes(
register: i8,
duration: Time,
velocity: u8,
channel: Channel,
articulation: Articulation,
) -> Self {
Self {
sound: EventSound::Notes,
pitch_layout: StrictPitchLayout::in_register(register),
duration,
velocity,
channel,
articulation,
tie: TiePolicy::None,
}
}
pub fn rest(duration: Time) -> Self {
Self {
sound: EventSound::Rest,
pitch_layout: StrictPitchLayout::in_register(4),
duration,
velocity: 0,
channel: Channel::new(0).expect("MIDI channel zero is valid"),
articulation: Articulation::Normal,
tie: TiePolicy::None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RegisterBounds {
pub lowest: i8,
pub highest: i8,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VoiceBounds {
pub max_notes_per_event: Option<usize>,
}
pub trait RealizationService: Any + Send + Sync {
fn as_any(&self) -> &dyn Any;
}
impl<T: Any + Send + Sync> RealizationService for T {
fn as_any(&self) -> &dyn Any {
self
}
}
#[derive(Clone, Default)]
pub struct RealizationServices {
entries: BTreeMap<String, Arc<dyn RealizationService>>,
}
impl RealizationServices {
pub fn new() -> Self {
Self::default()
}
pub fn insert(
&mut self,
name: impl Into<String>,
service: Arc<dyn RealizationService>,
) -> Option<Arc<dyn RealizationService>> {
self.entries.insert(name.into(), service)
}
pub fn get<T: Any + Send + Sync>(&self, name: &str) -> Option<&T> {
self.entries
.get(name)
.and_then(|service| service.as_ref().as_any().downcast_ref::<T>())
}
pub fn names(&self) -> Vec<&str> {
self.entries.keys().map(String::as_str).collect()
}
}
impl std::fmt::Debug for RealizationServices {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("RealizationServices")
.field("names", &self.names())
.finish()
}
}
impl PartialEq for RealizationServices {
fn eq(&self, other: &Self) -> bool {
self.names() == other.names()
}
}
impl Eq for RealizationServices {}
#[derive(Clone)]
pub struct RealizationContext {
pub specs: BTreeMap<SerialEventId, StrictEventSpec>,
pub simultaneous_policy: SimultaneousRenderPolicy,
pub scale: Option<Scale>,
pub modal_scale: Option<PlayerScale>,
pub tuning: Option<Arc<dyn Tuning>>,
pub contextual_sonance: Option<ContextualSonanceOptions>,
pub register_bounds: BTreeMap<VoiceId, RegisterBounds>,
pub voice_bounds: BTreeMap<VoiceId, VoiceBounds>,
pub parameter_tracks: BTreeMap<String, Arc<dyn ErasedParameterBinding>>,
pub services: RealizationServices,
}
impl RealizationContext {
pub fn new(specs: BTreeMap<SerialEventId, StrictEventSpec>) -> Self {
Self {
specs,
simultaneous_policy: SimultaneousRenderPolicy::PreserveOnset,
scale: None,
modal_scale: None,
tuning: None,
contextual_sonance: None,
register_bounds: BTreeMap::new(),
voice_bounds: BTreeMap::new(),
parameter_tracks: BTreeMap::new(),
services: RealizationServices::default(),
}
}
}
impl std::fmt::Debug for RealizationContext {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("RealizationContext")
.field("specs", &self.specs)
.field("simultaneous_policy", &self.simultaneous_policy)
.field("scale", &self.scale)
.field("modal_scale", &self.modal_scale)
.field("tuning", &self.tuning.as_ref().map(|tuning| tuning.name()))
.field("contextual_sonance", &self.contextual_sonance)
.field("register_bounds", &self.register_bounds)
.field("voice_bounds", &self.voice_bounds)
.field(
"parameter_tracks",
&self.parameter_tracks.keys().collect::<Vec<_>>(),
)
.field("services", &self.services)
.finish()
}
}
impl RealizationContext {
pub fn effective_modal_scale(&self) -> Option<PlayerScale> {
self.modal_scale
.clone()
.or_else(|| self.scale.map(PlayerScale::from_scale))
}
}
pub type StrictRealizationContext = RealizationContext;
pub trait SerialRealizer: Send + Sync {
fn id(&self) -> &RealizerId;
fn realize(
&self,
plan: &SerialPlan,
context: &RealizationContext,
) -> Result<SerialRealization, crate::StrictRealizationError>;
}