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,
#[serde(default)]
pub label: String,
#[serde(default)]
pub realization: StandardBassRealization,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StandardBassRealization {
#[default]
Direct,
Combined,
}
const NAMES: [&str; 12] = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];
fn chord_button_name(tones: &BTreeSet<u8>) -> String {
for &root in tones {
let has = |i: u8| tones.contains(&((root + i) % 12));
let suffix = match (tones.len(), has(4), has(3), has(7), has(6), has(8), has(10)) {
(3, true, _, true, ..) => "",
(3, _, true, true, ..) => "m",
(3, _, true, _, true, ..) => "dim",
(3, true, _, _, _, true, _) => "aug",
(3, true, _, _, _, _, true) => "7",
(4, true, _, true, _, _, true) => "7",
_ => continue,
};
return format!("{}{suffix}", NAMES[usize::from(root)]);
}
let names: Vec<_> = tones.iter().map(|&p| NAMES[usize::from(p)]).collect();
format!("({})", names.join(" "))
}
pub fn standard_bass_request_for_chord(
chord: &ChordSymbol,
allow_omissions: bool,
allow_additions: bool,
) -> Result<StandardBassRequest, String> {
use super::arrange::accordion_pitch_class as pitch_class;
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;
}
let chord_tones: BTreeSet<u8> = chord.midi.iter().map(|pitch| pitch % 12).collect();
candidates.push(StandardBassCandidate {
label: format!(
"{} bass + {} chord",
NAMES[usize::from(bass.midi % 12)],
chord_button_name(&chord_tones)
),
realization: if chord_tones.contains(&(bass.midi % 12)) {
StandardBassRealization::Direct
} else {
StandardBassRealization::Combined
},
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)
}
pub const MAX_DOUBLE_CHORD_CANDIDATES: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StandardBassDoubleChordCandidate {
pub bass_button_id: String,
pub chord_button_ids: [String; 2],
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 label: String,
}
pub fn standard_bass_double_chord_candidates(
profile: &StandardBassProfile,
request: &StandardBassRequest,
) -> Result<Vec<StandardBassDoubleChordCandidate>, String> {
standard_bass_candidates(profile, request)?;
let target: BTreeSet<u8> = request.pitch_classes.iter().copied().collect();
let tones =
|b: &StandardBassChordButton| -> BTreeSet<u8> { b.midi.iter().map(|p| p % 12).collect() };
let gap = |a: i16, b: i16| (i32::from(a) - i32::from(b)).unsigned_abs();
let mut found = Vec::new();
for bass in profile
.bass_buttons
.iter()
.filter(|b| b.midi % 12 == request.bass_pitch_class)
{
let bass_pc = bass.midi % 12;
for (i, first) in profile.chord_buttons.iter().enumerate() {
for second in &profile.chord_buttons[i + 1..] {
let (t1, t2) = (tones(first), tones(second));
let distance = gap(bass.position, first.position)
.max(gap(bass.position, second.position))
.max(gap(first.position, second.position));
if distance > u32::from(profile.max_distance)
|| first
.midi
.iter()
.chain(&second.midi)
.any(|&p| p < bass.midi)
{
continue;
}
let needed = |own: &BTreeSet<u8>, other: &BTreeSet<u8>| {
own.iter()
.any(|p| target.contains(p) && *p != bass_pc && !other.contains(p))
};
if !needed(&t1, &t2) || !needed(&t2, &t1) {
continue;
}
let sounding: BTreeSet<u8> = t1
.union(&t2)
.copied()
.chain(std::iter::once(bass_pc))
.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;
}
let chord_midi: BTreeSet<u8> =
first.midi.iter().chain(&second.midi).copied().collect();
found.push(StandardBassDoubleChordCandidate {
label: format!(
"{} bass + {} chord + {} chord",
NAMES[usize::from(bass_pc)],
chord_button_name(&t1),
chord_button_name(&t2)
),
bass_button_id: bass.id.clone(),
chord_button_ids: [first.id.clone(), second.id.clone()],
bass_midi: bass.midi,
chord_midi: chord_midi.into_iter().collect(),
distance: distance as u16,
retained_pitch_classes: target.intersection(&sounding).copied().collect(),
omitted_pitch_classes: omitted,
added_pitch_classes: added,
requires_performer_review: true,
});
}
}
}
found.sort_by(|a, b| {
(
a.omitted_pitch_classes.len(),
a.added_pitch_classes.len(),
a.distance,
&a.bass_button_id,
&a.chord_button_ids,
)
.cmp(&(
b.omitted_pitch_classes.len(),
b.added_pitch_classes.len(),
b.distance,
&b.bass_button_id,
&b.chord_button_ids,
))
});
found.truncate(MAX_DOUBLE_CHORD_CANDIDATES);
Ok(found)
}