use std::collections::{BTreeMap, HashMap};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub enum ElementKind {
Note,
Chord,
Rest,
Measure,
}
pub type ClassifiedElements = HashMap<String, ElementKind>;
#[derive(Debug, Clone, PartialEq)]
pub struct MidiOptions {
pub tempo_adjustment: f64,
pub omit_cue_notes: bool,
}
impl Default for MidiOptions {
fn default() -> Self {
Self {
tempo_adjustment: 1.0,
omit_cue_notes: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BBox {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub page: u32,
}
impl BBox {
pub fn contains(&self, x: f64, y: f64) -> bool {
x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ScoreMetadata {
pub title: Option<String>,
pub composer: Option<String>,
pub lyricist: Option<String>,
pub arranger: Option<String>,
pub copyright: Option<String>,
pub instruments: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MeasureInfo {
pub id: String,
pub start_ms: f64,
pub end_ms: f64,
pub start_qfrac: [i64; 2],
pub end_qfrac: [i64; 2],
}
impl MidiOptions {
pub(crate) fn to_json(&self) -> String {
format!(
r#"{{"midiTempoAdjustment": {}, "midiNoCue": {}}}"#,
self.tempo_adjustment, self.omit_cue_notes
)
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct SvgOptions {
pub css: String,
pub show_bounding_boxes: bool,
pub show_content_bounding_boxes: bool,
pub format_raw: bool,
pub html5: bool,
pub remove_xlink: bool,
}
impl SvgOptions {
pub(crate) fn to_json(&self) -> String {
let css_escaped = self
.css
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n");
format!(
r#"{{"svgCss": "{}", "svgBoundingBoxes": {}, "svgContentBoundingBoxes": {}, "svgFormatRaw": {}, "svgHtml5": {}, "svgRemoveXlink": {}}}"#,
css_escaped,
self.show_bounding_boxes,
self.show_content_bounding_boxes,
self.format_raw,
self.html5,
self.remove_xlink,
)
}
}
pub type ExpansionMap = BTreeMap<String, Vec<String>>;
#[derive(Debug, Clone, PartialEq)]
pub struct TempoChange {
pub at_qstamp: f64,
pub bpm: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TempoMap {
pub changes: Vec<TempoChange>,
}
impl TempoMap {
pub fn from_timemap(timemap: &[TimemapEvent]) -> Option<Self> {
let first = timemap.first()?;
let initial_bpm = first.tempo?;
let mut changes = vec![TempoChange {
at_qstamp: first.qstamp,
bpm: initial_bpm,
}];
let mut last_bpm = initial_bpm;
for ev in timemap.iter().skip(1) {
if let Some(bpm) = ev.tempo {
if (bpm - last_bpm).abs() > f64::EPSILON {
changes.push(TempoChange {
at_qstamp: ev.qstamp,
bpm,
});
last_bpm = bpm;
}
}
}
Some(Self { changes })
}
pub fn new(changes: Vec<TempoChange>) -> Self {
Self { changes }
}
pub fn qstamp_to_ms(&self, qstamp: f64) -> f64 {
if qstamp <= 0.0 || self.changes.is_empty() {
return 0.0;
}
let mut ms = 0.0;
for i in 0..self.changes.len() {
let q_start = self.changes[i].at_qstamp;
let bpm = self.changes[i].bpm;
let q_end = self
.changes
.get(i + 1)
.map(|c| c.at_qstamp)
.unwrap_or(f64::INFINITY);
let segment_end = q_end.min(qstamp);
if segment_end > q_start {
ms += (segment_end - q_start) * 60_000.0 / bpm;
}
if segment_end >= qstamp {
return ms;
}
}
ms
}
pub fn scaled(&self, factor: f64) -> Self {
let f = factor.max(1e-9);
let changes = self
.changes
.iter()
.map(|c| TempoChange {
at_qstamp: c.at_qstamp,
bpm: c.bpm * f,
})
.collect();
Self { changes }
}
pub fn bpm_at_qstamp(&self, q: f64) -> Option<f64> {
let mut current: Option<f64> = None;
for change in &self.changes {
if change.at_qstamp <= q {
current = Some(change.bpm);
} else {
break;
}
}
current
}
pub fn bpm_at_ms(&self, ms: f64) -> Option<f64> {
if self.changes.is_empty() {
return None;
}
let mut accumulated_ms = 0.0;
for i in 0..self.changes.len() {
let q_start = self.changes[i].at_qstamp;
let bpm = self.changes[i].bpm;
let q_end = self
.changes
.get(i + 1)
.map(|c| c.at_qstamp)
.unwrap_or(f64::INFINITY);
let segment_ms_duration = (q_end - q_start) * 60_000.0 / bpm;
if ms < accumulated_ms + segment_ms_duration {
return Some(bpm);
}
accumulated_ms += segment_ms_duration;
}
Some(self.changes.last()?.bpm)
}
pub fn ms_to_qstamp(&self, ms: f64) -> f64 {
if ms <= 0.0 || self.changes.is_empty() {
return 0.0;
}
let mut accumulated_ms = 0.0;
let mut last_q = self.changes[0].at_qstamp;
for i in 0..self.changes.len() {
let q_start = self.changes[i].at_qstamp;
let bpm = self.changes[i].bpm;
let q_end = self
.changes
.get(i + 1)
.map(|c| c.at_qstamp)
.unwrap_or(f64::INFINITY);
let segment_q_duration = q_end - q_start;
let segment_ms_duration = segment_q_duration * 60_000.0 / bpm;
if accumulated_ms + segment_ms_duration >= ms {
let elapsed_in_segment_ms = ms - accumulated_ms;
return q_start + elapsed_in_segment_ms / 60_000.0 * bpm;
}
accumulated_ms += segment_ms_duration;
last_q = q_end;
}
last_q
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct TimemapEvent {
pub tstamp: f64,
pub qstamp: f64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub on: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub off: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tempo: Option<f64>,
}
pub type Timemap = Vec<TimemapEvent>;
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct ElementsAtTime {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub chords: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub measure: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub notes: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page: Option<u32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rests: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct TimemapEventExact {
pub qfrac: [i64; 2],
pub tstamp: f64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub on: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub off: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty", rename = "restsOn")]
pub rests_on: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty", rename = "restsOff")]
pub rests_off: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "measureOn")]
pub measure_on: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tempo: Option<f64>,
}
impl TimemapEventExact {
pub fn quarter_beats(&self) -> (i64, i64) {
(self.qfrac[0], self.qfrac[1])
}
pub fn tstamp_ms_at_tempo(&self, bpm: f64) -> f64 {
(self.qfrac[0] as f64 / self.qfrac[1] as f64) * 60_000.0 / bpm
}
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct MidiValues {
pub time: i32,
pub pitch: i32,
pub duration: i32,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct ElementTimes {
#[serde(default, rename = "qfracOn")]
pub qfrac_on: Vec<[i64; 2]>,
#[serde(default, rename = "qfracOff")]
pub qfrac_off: Vec<[i64; 2]>,
#[serde(default, rename = "qfracDuration")]
pub qfrac_duration: Vec<[i64; 2]>,
#[serde(default, rename = "qfracTiedDuration")]
pub qfrac_tied_duration: Vec<[i64; 2]>,
#[serde(default, rename = "tstampOn")]
pub tstamp_on: Vec<f64>,
#[serde(default, rename = "tstampOff")]
pub tstamp_off: Vec<f64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MeiOptions {
pub page_no: Option<u32>,
pub score_based: bool,
pub basic: bool,
pub remove_ids: bool,
}
impl Default for MeiOptions {
fn default() -> Self {
Self {
page_no: None,
score_based: true,
basic: false,
remove_ids: false,
}
}
}
impl MeiOptions {
pub(crate) fn to_json(&self) -> String {
let page_no = self.page_no.unwrap_or(0);
format!(
r#"{{"pageNo": {page_no}, "scoreBased": {sb}, "basic": {b}, "removeIds": {ri}}}"#,
sb = self.score_based,
b = self.basic,
ri = self.remove_ids,
)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct LayoutOptions {
pub font: Option<String>,
pub scale: Option<u32>,
pub page_width: Option<u32>,
pub page_height: Option<u32>,
pub breaks: Option<String>,
pub landscape: Option<bool>,
pub measure_from: Option<String>,
pub measure_to: Option<String>,
}
impl LayoutOptions {
pub(crate) fn to_json(&self) -> String {
let mut fields: Vec<String> = Vec::new();
if let Some(font) = &self.font {
let escaped = font.replace('\\', "\\\\").replace('"', "\\\"");
fields.push(format!(r#""font": "{escaped}""#));
}
if let Some(scale) = self.scale {
fields.push(format!(r#""scale": {scale}"#));
}
if let Some(w) = self.page_width {
fields.push(format!(r#""pageWidth": {w}"#));
}
if let Some(h) = self.page_height {
fields.push(format!(r#""pageHeight": {h}"#));
}
if let Some(breaks) = &self.breaks {
let escaped = breaks.replace('\\', "\\\\").replace('"', "\\\"");
fields.push(format!(r#""breaks": "{escaped}""#));
}
if let Some(landscape) = self.landscape {
fields.push(format!(r#""landscape": {landscape}"#));
}
if let Some(from) = &self.measure_from {
let escaped = from.replace('\\', "\\\\").replace('"', "\\\"");
fields.push(format!(r#""measureFrom": "{escaped}""#));
}
if let Some(to) = &self.measure_to {
let escaped = to.replace('\\', "\\\\").replace('"', "\\\"");
fields.push(format!(r#""measureTo": "{escaped}""#));
}
format!("{{{}}}", fields.join(", "))
}
}