use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use super::notation::ChordSymbol;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StandardBassNoteButton {
pub id: String,
pub position: i16,
pub midi: u8,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StandardBassChordButton {
pub id: String,
pub position: i16,
pub midi: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StandardBassProfile {
pub bass_buttons: Vec<StandardBassNoteButton>,
pub chord_buttons: Vec<StandardBassChordButton>,
pub max_distance: u16,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StandardBassRequest {
pub pitch_classes: Vec<u8>,
pub bass_pitch_class: u8,
pub allow_omissions: bool,
pub allow_additions: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StandardBassCandidate {
pub bass_button_id: String,
pub chord_button_id: String,
pub bass_midi: u8,
pub chord_midi: Vec<u8>,
pub distance: u16,
pub retained_pitch_classes: Vec<u8>,
pub omitted_pitch_classes: Vec<u8>,
pub added_pitch_classes: Vec<u8>,
pub requires_performer_review: bool,
}
pub fn standard_bass_request_for_chord(
chord: &ChordSymbol,
allow_omissions: bool,
allow_additions: bool,
) -> Result<StandardBassRequest, String> {
fn pitch_class(name: &str) -> Option<u8> {
let mut chars = name.chars();
let base: i16 = match chars.next()? {
'C' => 0,
'D' => 2,
'E' => 4,
'F' => 5,
'G' => 7,
'A' => 9,
'B' => 11,
_ => return None,
};
let offset = match chars.next() {
None => 0,
Some('#') => 1,
Some('b') => -1,
_ => return None,
};
if chars.next().is_some() {
return None;
}
Some((base + offset).rem_euclid(12) as u8)
}
if !chord.degrees.is_empty() {
return Err("chord degrees are not supported by this candidate search".into());
}
let root = pitch_class(&chord.root).ok_or("unsupported chord root spelling")?;
let bass = match &chord.bass {
Some(name) => pitch_class(name).ok_or("unsupported chord bass spelling")?,
None => root,
};
let intervals: &[u8] = match chord.kind.as_str() {
"major" => &[0, 4, 7],
"minor" => &[0, 3, 7],
"diminished" => &[0, 3, 6],
"augmented" => &[0, 4, 8],
"dominant" => &[0, 4, 7, 10],
"major-seventh" => &[0, 4, 7, 11],
"minor-seventh" => &[0, 3, 7, 10],
_ => return Err("unsupported chord kind for this candidate search".into()),
};
let pitch_classes = intervals
.iter()
.map(|interval| (root + interval) % 12)
.chain(std::iter::once(bass))
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
Ok(StandardBassRequest {
pitch_classes,
bass_pitch_class: bass,
allow_omissions,
allow_additions,
})
}
pub fn standard_bass_candidates(
profile: &StandardBassProfile,
request: &StandardBassRequest,
) -> Result<Vec<StandardBassCandidate>, String> {
if profile.bass_buttons.len() > 128 || profile.chord_buttons.len() > 128 {
return Err("a profile may contain at most 128 buttons per row".into());
}
if request.pitch_classes.is_empty()
|| request.pitch_classes.len() > 12
|| request.pitch_classes.iter().any(|&pitch| pitch >= 12)
|| request.bass_pitch_class >= 12
{
return Err(
"pitch classes must be between 0 and 11 and the target must not be empty".into(),
);
}
let target: BTreeSet<u8> = request.pitch_classes.iter().copied().collect();
let mut ids = BTreeSet::new();
for button in &profile.bass_buttons {
if button.id.is_empty()
|| button.id.len() > 128
|| !ids.insert(&button.id)
|| button.midi >= 128
{
return Err(
"bass button IDs must be unique and pitches must be valid MIDI notes".into(),
);
}
}
for button in &profile.chord_buttons {
if button.id.is_empty()
|| button.id.len() > 128
|| !ids.insert(&button.id)
|| button.midi.is_empty()
|| button.midi.len() > 16
|| button.midi.iter().any(|&pitch| pitch >= 128)
{
return Err(
"chord button IDs must be unique and contain 1 to 16 valid MIDI notes".into(),
);
}
}
let mut candidates = Vec::new();
for bass in &profile.bass_buttons {
if bass.midi % 12 != request.bass_pitch_class {
continue;
}
for chord in &profile.chord_buttons {
let distance = (i32::from(bass.position) - i32::from(chord.position)).unsigned_abs();
if distance > u32::from(profile.max_distance)
|| chord.midi.iter().any(|&pitch| pitch < bass.midi)
{
continue;
}
let sounding: BTreeSet<u8> = std::iter::once(bass.midi % 12)
.chain(chord.midi.iter().map(|pitch| pitch % 12))
.collect();
let omitted: Vec<_> = target.difference(&sounding).copied().collect();
let added: Vec<_> = sounding.difference(&target).copied().collect();
if (!request.allow_omissions && !omitted.is_empty())
|| (!request.allow_additions && !added.is_empty())
{
continue;
}
candidates.push(StandardBassCandidate {
bass_button_id: bass.id.clone(),
chord_button_id: chord.id.clone(),
bass_midi: bass.midi,
chord_midi: chord.midi.clone(),
distance: distance as u16,
retained_pitch_classes: target.intersection(&sounding).copied().collect(),
omitted_pitch_classes: omitted,
added_pitch_classes: added,
requires_performer_review: true,
});
}
}
candidates.sort_by(|a, b| {
(
a.omitted_pitch_classes.len(),
a.added_pitch_classes.len(),
a.distance,
&a.bass_button_id,
&a.chord_button_id,
)
.cmp(&(
b.omitted_pitch_classes.len(),
b.added_pitch_classes.len(),
b.distance,
&b.bass_button_id,
&b.chord_button_id,
))
});
Ok(candidates)
}