use std::cmp::Ordering;
use std::sync::Arc;
use crate::util;
pub fn v2s_f32_rounded(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
let rounding_multiplier = 10u32.pow(digits as u32) as f32;
Arc::new(move |value| {
if (value * rounding_multiplier).round() / rounding_multiplier == 0.0 {
format!("{:.digits$}", 0.0)
} else {
format!("{value:.digits$}")
}
})
}
pub fn v2s_f32_percentage(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
Arc::new(move |value| format!("{:.digits$}", value * 100.0))
}
pub fn s2v_f32_percentage() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
Arc::new(|string| {
string
.trim_end_matches([' ', '%'])
.parse()
.ok()
.map(|x: f32| x / 100.0)
})
}
pub fn v2s_compression_ratio(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
Arc::new(move |value| {
if value >= 1.0 {
format!("{value:.digits$}:1")
} else {
format!("1:{:.digits$}", value.recip())
}
})
}
pub fn s2v_compression_ratio() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
Arc::new(|string| {
let string = string.trim();
string
.trim()
.split_once(':')
.and_then(|(numerator, denominator)| {
let numerator: f32 = numerator.trim().parse().ok()?;
let denominator: f32 = denominator.trim().parse().ok()?;
Some(numerator / denominator)
})
.or_else(|| string.parse().ok())
})
}
pub fn v2s_f32_gain_to_db(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
let rounding_multiplier = 10u32.pow(digits as u32) as f32;
Arc::new(move |value| {
if value < util::MINUS_INFINITY_GAIN {
String::from("-inf")
} else {
let value_db = util::gain_to_db(value);
if (value_db * rounding_multiplier).round() / rounding_multiplier == 0.0 {
format!("{:.digits$}", 0.0)
} else {
format!("{value_db:.digits$}")
}
}
})
}
pub fn s2v_f32_gain_to_db() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
Arc::new(|string| {
let string = string.trim_end_matches([' ', 'd', 'D', 'b', 'B', 'f', 'F', 's', 'S']);
if string.eq_ignore_ascii_case("-in") {
Some(0.0)
} else {
string.parse().ok().map(util::db_to_gain)
}
})
}
pub fn v2s_f32_panning() -> Arc<dyn Fn(f32) -> String + Send + Sync> {
Arc::new(move |value| match value.partial_cmp(&0.0) {
Some(Ordering::Less) => format!("{:.0}L", value * -100.0),
Some(Ordering::Equal) => String::from("C"),
Some(Ordering::Greater) => format!("{:.0}R", value * 100.0),
None => String::from("NaN"),
})
}
pub fn s2v_f32_panning() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
Arc::new(|string| {
let string = string.trim();
let cleaned_string = string
.trim_end_matches([' ', 'l', 'L', 'c', 'C', 'r', 'R'])
.parse()
.ok();
match string.chars().last()?.to_uppercase().next()? {
'L' => cleaned_string.map(|x: f32| x / -100.0),
'C' => Some(0.0),
'R' => cleaned_string.map(|x: f32| x / 100.0),
_ => None,
}
})
}
pub fn v2s_f32_hz_then_khz(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
Arc::new(move |value| {
if value < 1000.0 {
format!("{value:.digits$} Hz")
} else {
format!("{:.digits$} kHz", value / 1000.0, digits = digits.max(1))
}
})
}
pub fn v2s_f32_hz_then_khz_with_note_name(
digits: usize,
include_cents: bool,
) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
Arc::new(move |value| {
if value.abs() < 1.0 {
return format!("{value:.digits$} Hz");
}
let fractional_note = util::freq_to_midi_note(value);
let note = fractional_note.round();
let cents = ((fractional_note - note) * 100.0).round() as i32;
let note_name = util::NOTES[(note as i32).rem_euclid(12) as usize];
let octave = (note / 12.0).floor() as i32 - 1;
let note_str = if cents == 0 || !include_cents {
format!("{note_name}{octave}")
} else {
format!("{note_name}{octave}, {cents:+} ct.")
};
if value < 1000.0 {
format!("{value:.digits$} Hz, {note_str}")
} else {
format!(
"{:.digits$} kHz, {}",
value / 1000.0,
note_str,
digits = digits.max(1)
)
}
})
}
pub fn s2v_f32_hz_then_khz() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
let note_formatter = s2v_i32_note_formatter();
Arc::new(move |string| {
let string = string.trim();
let mut segments = string.split(',');
let segments = (segments.next(), segments.next(), segments.next());
if let (_, Some(midi_note_number_str), Some(cents_str))
| (Some(midi_note_number_str), Some(cents_str), None) = segments
{
let cents_str = cents_str
.trim_start_matches([' ', '+'])
.trim_end_matches([' ', 'C', 'c', 'E', 'e', 'N', 'n', 'T', 't', 'S', 's', '.']);
if let (Some(midi_note_number), Ok(cents)) = (
note_formatter(midi_note_number_str),
cents_str.parse::<i32>(),
) {
let plain_note_freq = util::f32_midi_note_to_freq(midi_note_number as f32);
let cents_multiplier = 2.0f32.powf(cents as f32 / 100.0 / 12.0);
return Some(plain_note_freq * cents_multiplier);
}
}
if let (_, Some(midi_note_number_str), _) | (Some(midi_note_number_str), None, None) =
segments
{
if let Some(midi_note_number) = note_formatter(midi_note_number_str) {
return Some(util::f32_midi_note_to_freq(midi_note_number as f32));
}
}
let frequency_segment = segments.0?;
let cleaned_string = frequency_segment
.trim_end_matches([' ', 'k', 'K', 'h', 'H', 'z', 'Z'])
.parse()
.ok();
match frequency_segment.get(frequency_segment.len().saturating_sub(3)..) {
Some(unit) if unit.eq_ignore_ascii_case("khz") => cleaned_string.map(|x| x * 1000.0),
_ => cleaned_string,
}
})
}
pub fn v2s_i32_power_of_two() -> Arc<dyn Fn(i32) -> String + Send + Sync> {
Arc::new(|value| format!("{}", 1 << value))
}
pub fn s2v_i32_power_of_two() -> Arc<dyn Fn(&str) -> Option<i32> + Send + Sync> {
Arc::new(|string| string.parse().ok().map(|n: i32| (n as f32).log2() as i32))
}
pub fn v2s_i32_note_formatter() -> Arc<dyn Fn(i32) -> String + Send + Sync> {
Arc::new(move |value| {
let note_name = util::NOTES[value.rem_euclid(12) as usize];
let octave = (value / 12) - 1;
format!("{note_name}{octave}")
})
}
pub fn s2v_i32_note_formatter() -> Arc<dyn Fn(&str) -> Option<i32> + Send + Sync> {
Arc::new(|string| {
let string = string.trim();
if string.len() < 2 {
return None;
}
let (note_name, octave) = string
.split_once(|c: char| c.is_whitespace())
.unwrap_or_else(|| {
if string.len() > 2 && &string[1..2] == "#" {
(&string[..2], &string[2..])
} else {
(&string[..1], &string[1..])
}
});
let note_id = util::NOTES
.iter()
.position(|&candidate| note_name.eq_ignore_ascii_case(candidate))?
as i32;
let octave: i32 = octave.trim().parse().ok()?;
Some(note_id + (12 * (octave + 1)))
})
}
pub fn v2s_bool_bypass() -> Arc<dyn Fn(bool) -> String + Send + Sync> {
Arc::new(move |value| {
if value {
String::from("Bypassed")
} else {
String::from("Not Bypassed")
}
})
}
pub fn s2v_bool_bypass() -> Arc<dyn Fn(&str) -> Option<bool> + Send + Sync> {
Arc::new(|string| {
let string = string.trim();
if string.eq_ignore_ascii_case("bypassed") {
Some(true)
} else if string.eq_ignore_ascii_case("not bypassed") {
Some(false)
} else {
None
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v2s_f32_rounded_negative_zero() {
let v2s = v2s_f32_rounded(2);
assert_eq!("0.00", v2s(-0.001));
assert_eq!("-0.01", v2s(-0.009));
assert_eq!("0.01", v2s(0.009));
}
#[test]
fn f32_hz_then_khz_with_note_name_roundtrip() {
let v2s = v2s_f32_hz_then_khz_with_note_name(1, true);
let s2v = s2v_f32_hz_then_khz();
for freq in [0.0, 5.0, 7.18, 8.18, 69.420, 18181.8, 133333.7] {
let string = v2s(freq);
let roundtrip_freq = s2v(&string).unwrap();
let roundtrip_string = v2s(roundtrip_freq);
assert_eq!(
string, roundtrip_string,
"Unexpected: {string} -> {roundtrip_freq} -> {roundtrip_string}"
);
}
}
}