use serde::{Deserialize, Serialize};
use super::{
arrange::AccordionBassMode,
score::{Score, transpose_checked, transpose_chord_symbol_at},
standard_bass_arrangement::{
StandardBassArrangementDiagnostic, StandardBassArrangementOptions,
StandardBassArrangementResult, arrange_standard_bass,
},
standard_bass_pitch_selection::SelectedPitchArrangementResult,
standard_bass_transition::{
StandardBassTransition, StandardBassTransitionPolicy, screen_existing_standard_bass,
},
};
pub const MAX_TRANSPOSITION_CANDIDATES: usize = 25;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TransposedStandardBassOptions {
pub arrangement: StandardBassArrangementOptions,
pub semitones: i8,
#[serde(default)]
pub transition_policy: Option<StandardBassTransitionPolicy>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransposedStandardBassResult {
pub semitones: i8,
pub arrangement: StandardBassArrangementResult,
pub transitions: Vec<StandardBassTransition>,
pub source_options: StandardBassArrangementOptions,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum TranspositionStatus {
Arranged,
OutOfRange,
NoButtonCombination,
TransitionRejected,
Unsupported,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranspositionEvaluation {
pub semitones: i8,
pub status: TranspositionStatus,
pub diagnostics: Vec<StandardBassArrangementDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StandardBassTranspositionSearch {
pub arrangement: StandardBassArrangementOptions,
pub min_semitones: i8,
pub max_semitones: i8,
#[serde(default)]
pub transition_policy: Option<StandardBassTransitionPolicy>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StandardBassTranspositionSearchResult {
pub evaluations: Vec<TranspositionEvaluation>,
pub diagnostics: Vec<StandardBassArrangementDiagnostic>,
}
fn rejected(
options: &TransposedStandardBassOptions,
code: &str,
message: impl Into<String>,
measure_index: Option<usize>,
) -> TransposedStandardBassResult {
TransposedStandardBassResult {
semitones: options.semitones,
arrangement: StandardBassArrangementResult {
score: None,
source_pitches: vec![],
actions: vec![],
diagnostics: vec![StandardBassArrangementDiagnostic {
code: code.into(),
message: message.into(),
measure_index,
}],
requires_performer_review: true,
required_bass_mode: AccordionBassMode::StandardBass,
resolved_options: None,
edition_kinds: vec![
super::standard_bass_arrangement::StandardBassEditionKind::NoCandidate,
],
harmony_source: options.arrangement.harmony_source,
},
transitions: vec![],
source_options: options.arrangement.clone(),
}
}
pub fn arrange_standard_bass_transposed(
score: &Score,
options: &TransposedStandardBassOptions,
) -> TransposedStandardBassResult {
let source = &options.arrangement;
let Some(staff) = score
.parts
.get(source.melody_part)
.and_then(|part| part.staves.get(source.melody_staff))
else {
return rejected(
options,
"melody-selection",
"selected staff does not exist",
None,
);
};
let semitones = i16::from(options.semitones);
for (measure_index, measure) in score
.parts
.iter()
.flat_map(|part| &part.staves)
.flat_map(|staff| staff.measures.iter().enumerate())
{
let outside = measure.voices.iter().flatten().any(|note| {
!note.is_rest
&& !note.is_unpitched
&& note
.pitches
.iter()
.any(|pitch| !(0..=127).contains(&(pitch.to_midi() + semitones)))
});
if outside {
return rejected(
options,
"transposition-out-of-range",
"a transposed source pitch leaves the MIDI range",
Some(measure_index),
);
}
}
let transposed = match transpose_checked(score, options.semitones) {
Ok(transposed) => transposed,
Err(error) => {
return rejected(
options,
"transposition-unsupported",
format!("the source cannot be transposed: {error}"),
None,
);
}
};
let mut target = source.clone();
for positioned in &mut target.chords {
let Some(chord) = transpose_chord_symbol_at(
score,
staff,
positioned.measure_index,
&positioned.chord,
options.semitones,
) else {
return rejected(
options,
"unsupported-chord-spelling",
"chord root or slash bass spelling cannot be transposed",
Some(positioned.measure_index),
);
};
positioned.chord = chord;
}
let arrangement = arrange_standard_bass(&transposed, &target);
let (arrangement, transitions) = match &options.transition_policy {
Some(policy) => {
let screened = screen_existing_standard_bass(
SelectedPitchArrangementResult {
selections: vec![],
arrangement,
},
&target,
policy,
);
(screened.arrangement.arrangement, screened.transitions)
}
None => (arrangement, vec![]),
};
TransposedStandardBassResult {
semitones: options.semitones,
arrangement,
transitions,
source_options: source.clone(),
}
}
pub fn evaluate_standard_bass_transpositions(
score: &Score,
search: &StandardBassTranspositionSearch,
) -> StandardBassTranspositionSearchResult {
let span = i16::from(search.max_semitones) - i16::from(search.min_semitones) + 1;
if span < 1 || span as usize > MAX_TRANSPOSITION_CANDIDATES {
return StandardBassTranspositionSearchResult {
evaluations: vec![],
diagnostics: vec![StandardBassArrangementDiagnostic {
code: "transposition-search-limit".into(),
message: format!(
"evaluate 1 to {MAX_TRANSPOSITION_CANDIDATES} ascending semitone values"
),
measure_index: None,
}],
};
}
let evaluations = (search.min_semitones..=search.max_semitones)
.map(|semitones| {
let result = arrange_standard_bass_transposed(
score,
&TransposedStandardBassOptions {
arrangement: search.arrangement.clone(),
semitones,
transition_policy: search.transition_policy.clone(),
},
);
let diagnostics = result.arrangement.diagnostics;
let status = match diagnostics.first().map(|d| d.code.as_str()) {
_ if result.arrangement.score.is_some() => TranspositionStatus::Arranged,
Some("transposition-out-of-range" | "out-of-range") => {
TranspositionStatus::OutOfRange
}
Some("no-standard-bass-candidate") => TranspositionStatus::NoButtonCombination,
Some(code) if code.starts_with("transition-") => {
TranspositionStatus::TransitionRejected
}
_ => TranspositionStatus::Unsupported,
};
TranspositionEvaluation {
semitones,
status,
diagnostics,
}
})
.collect();
StandardBassTranspositionSearchResult {
evaluations,
diagnostics: vec![],
}
}