use crate::errors::{InvalidValueError, ProjectParseError};
use crate::generics::Slots;
use crate::projects::{parse_hashmap_string_value, SlotAttributes, DEFAULT_GAIN, DEFAULT_TEMPO};
use crate::settings::{LoopMode, SlotType, TimeStretchMode, TrigQuantizationMode};
use itertools::Itertools;
use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
pub fn parse_id(hmap: &HashMap<String, String>) -> Result<u8, ProjectParseError> {
let x = parse_hashmap_string_value::<u8>(hmap, "slot", None)?;
Ok(x)
}
pub fn parse_loop_mode(hmap: &HashMap<String, String>) -> Result<LoopMode, InvalidValueError> {
let default = LoopMode::default() as u8;
let default_str = format!["{default}"];
let x = parse_hashmap_string_value::<u8>(hmap, "loopmode", Some(default_str.as_str()))
.unwrap_or(default);
LoopMode::try_from(&x)
}
pub fn parse_tstrech_mode(
hmap: &HashMap<String, String>,
) -> Result<TimeStretchMode, InvalidValueError> {
let default = TimeStretchMode::default() as u8;
let default_str = format!["{default}"];
let x = parse_hashmap_string_value::<u8>(hmap, "tsmode", Some(default_str.as_str()))
.unwrap_or(default);
TimeStretchMode::try_from(&x)
}
pub fn parse_trig_quantize_mode(
hmap: &HashMap<String, String>,
) -> Result<TrigQuantizationMode, InvalidValueError> {
let default = TrigQuantizationMode::default() as u8;
let default_str = format!["{default}"];
let x = parse_hashmap_string_value::<u8>(hmap, "trigquantization", Some(default_str.as_str()))
.unwrap_or(default);
TrigQuantizationMode::try_from(x)
}
pub fn parse_gain(hmap: &HashMap<String, String>) -> Result<u8, ProjectParseError> {
let x =
parse_hashmap_string_value::<u8>(hmap, "gain", Some(format!["{DEFAULT_GAIN}"].as_str()))
.unwrap_or(DEFAULT_GAIN);
Ok(x)
}
pub fn parse_tempo(hmap: &HashMap<String, String>) -> Result<u16, ProjectParseError> {
let x = parse_hashmap_string_value::<u16>(
hmap,
"bpmx24",
Some(format!["{DEFAULT_TEMPO}"].as_str()),
)
.unwrap_or(DEFAULT_TEMPO);
Ok(x)
}
pub fn parse_path(hmap: &HashMap<String, String>) -> Result<PathBuf, ProjectParseError> {
let path_str = hmap.get("path").ok_or(ProjectParseError::HashMap)?;
let path = PathBuf::from_str(path_str).map_err(|_| ProjectParseError::String)?;
Ok(path)
}
impl TryFrom<&HashMap<String, String>> for SlotAttributes {
type Error = ProjectParseError;
fn try_from(value: &HashMap<String, String>) -> Result<Self, Self::Error> {
let slot_id = parse_id(value)?;
let sample_slot_type = value
.get("type")
.ok_or(ProjectParseError::HashMap)?
.to_string();
let slot_type = SlotType::try_from(sample_slot_type)?;
let path = parse_path(value)?;
let loop_mode = parse_loop_mode(value)?;
let timestrech_mode = parse_tstrech_mode(value)?;
let trig_quantization_mode = parse_trig_quantize_mode(value)?;
let gain = parse_gain(value)?;
let bpm = parse_tempo(value)?;
let sample_struct = Self {
slot_type,
slot_id,
path: if path.as_os_str() != "" {
Some(path)
} else {
None
},
timestrech_mode,
loop_mode,
trig_quantization_mode,
gain,
bpm,
};
Ok(sample_struct)
}
}
impl FromStr for SlotAttributes {
type Err = ProjectParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let k_v: Vec<Vec<&str>> = s
.strip_prefix("\r\n\r\n[SAMPLE]\r\n")
.ok_or(ProjectParseError::HashMap)?
.strip_suffix("\r\n")
.ok_or(ProjectParseError::HashMap)?
.split("\r\n")
.map(|x: &str| x.split('=').collect_vec())
.filter(|x: &Vec<&str>| x.len() == 2)
.collect_vec();
let mut hmap: HashMap<String, String> = HashMap::new();
for key_value_pair in k_v {
hmap.insert(
key_value_pair[0].to_string().to_lowercase(),
key_value_pair[1].to_string(),
);
}
let sample_struct = SlotAttributes::try_from(&hmap)?;
Ok(sample_struct)
}
}
impl fmt::Display for SlotAttributes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let mut s = "[SAMPLE]\r\n".to_string();
s.push_str(&format!("TYPE={}", self.slot_type));
s.push_str("\r\n");
s.push_str(format!("SLOT={:0>3}", self.slot_id).as_str());
s.push_str("\r\n");
if let Some(path) = &self.path {
s.push_str(
format!("PATH={path:#?}")
.replace('"', "") .replace("\\", "") .as_str(),
);
} else {
s.push_str("PATH=");
}
s.push_str("\r\n");
s.push_str(format!("BPMx24={}", self.bpm).as_str());
s.push_str("\r\n");
s.push_str(format!("TSMODE={}", self.timestrech_mode as u8).as_str());
s.push_str("\r\n");
s.push_str(format!("LOOPMODE={}", self.loop_mode as u8).as_str());
s.push_str("\r\n");
s.push_str(format!("GAIN={}", self.gain).as_str());
s.push_str("\r\n");
s.push_str(format!("TRIGQUANTIZATION={}", self.trig_quantization_mode as u8).as_str());
s.push_str("\r\n[/SAMPLE]");
write!(f, "{s:#}")
}
}
impl fmt::Display for Slots<Option<SlotAttributes>> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let mut string_slots: String = "".to_string();
let slots = vec![
self.static_slots.to_vec(),
self.flex_slots.to_vec(),
self.recording_buffers.to_vec(),
];
let slots_concat = itertools::concat(slots).into_iter().flatten();
for slot in slots_concat {
string_slots.push_str(&slot.to_string());
string_slots.push_str("\r\n\r\n");
}
string_slots = string_slots
.strip_suffix("\r\n\r\n")
.ok_or(fmt::Error)?
.to_string();
write!(f, "{string_slots:#}")
}
}
#[cfg(test)]
mod test_slot_attr_display {
use crate::projects::slots::test_utils;
use crate::OtToolsIoError;
#[test]
fn valid_full() -> Result<(), OtToolsIoError> {
test_utils::new_slot_attr_full_args()?.to_string();
Ok(())
}
#[test]
fn valid_minimal() -> Result<(), OtToolsIoError> {
test_utils::new_slot_attr_minimal_args()?.to_string();
Ok(())
}
}
fn collect_slots_by_id_and_type(
slots: &mut Slots<Option<SlotAttributes>>,
string: &str,
) -> Result<(), ProjectParseError> {
let slot = SlotAttributes::from_str(string)?;
let zero_indexed_id = slot.slot_id as usize - 1;
match slot.slot_type {
SlotType::Static if slot.slot_id <= 128 => {
slots.static_slots[zero_indexed_id] = Some(slot);
Ok(())
}
SlotType::Flex if slot.slot_id <= 128 => {
slots.flex_slots[zero_indexed_id] = Some(slot);
Ok(())
}
SlotType::Flex if slot.slot_id > 128 && slot.slot_id <= 136 => {
let index = slot.slot_id as usize - 129;
slots.recording_buffers[index] = Some(slot);
Ok(())
}
_ => Err(ProjectParseError::String),
}
}
impl FromStr for Slots<Option<SlotAttributes>> {
type Err = ProjectParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let footer_stripped = s
.strip_suffix("\r\n\r\n############################\r\n\r\n")
.ok_or(ProjectParseError::Footer)?;
let data_window: Vec<&str> = footer_stripped
.split("############################\r\n# Samples\r\n############################")
.collect();
let mut samples_string: Vec<&str> = data_window[1].split("[/SAMPLE]").collect();
samples_string.pop();
let mut slots = Self::default();
for s in &samples_string {
collect_slots_by_id_and_type(&mut slots, s)?;
}
Ok(slots)
}
}
#[cfg(test)]
#[allow(unused_imports)]
mod test {
#[test]
fn parse_id_001_correct() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("slot".to_string(), "001".to_string());
let slot_id = crate::projects::slots::parsing::parse_id(&hmap);
assert_eq!(1, slot_id.unwrap());
}
#[test]
fn parse_id_1_correct() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("slot".to_string(), "1".to_string());
let slot_id = crate::projects::slots::parsing::parse_id(&hmap);
assert_eq!(1, slot_id.unwrap());
}
#[test]
fn parse_id_127_correct() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("slot".to_string(), "127".to_string());
let slot_id = crate::projects::slots::parsing::parse_id(&hmap);
assert_eq!(127, slot_id.unwrap());
}
#[test]
fn parse_id_099_correct() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("slot".to_string(), "099".to_string());
let slot_id = crate::projects::slots::parsing::parse_id(&hmap);
assert_eq!(99, slot_id.unwrap());
}
#[test]
fn parse_id_010_correct() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("slot".to_string(), "010".to_string());
let slot_id = crate::projects::slots::parsing::parse_id(&hmap);
assert_eq!(10, slot_id.unwrap());
}
#[test]
fn test_parse_id_err_bad_value_type_err() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("slot".to_string(), "AAAA".to_string());
let slot_id = crate::projects::slots::parsing::parse_id(&hmap);
assert!(slot_id.is_err());
}
#[test]
fn test_parse_tempo_correct_default() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("bpmx24".to_string(), "2880".to_string());
let r = crate::projects::slots::parsing::parse_tempo(&hmap);
assert_eq!(2880_u16, r.unwrap());
}
#[test]
fn test_parse_tempo_correct_min() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("bpmx24".to_string(), "720".to_string());
let r = crate::projects::slots::parsing::parse_tempo(&hmap);
assert_eq!(720_u16, r.unwrap());
}
#[test]
fn test_parse_tempo_correct_max() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("bpmx24".to_string(), "7200".to_string());
let r = crate::projects::slots::parsing::parse_tempo(&hmap);
assert_eq!(7200_u16, r.unwrap());
}
#[test]
fn test_parse_tempo_bad_value_type_default_return() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("bpmx24".to_string(), "AAAFSFSFSSFfssafAA".to_string());
let r = crate::projects::slots::parsing::parse_tempo(&hmap);
assert_eq!(r.unwrap(), 2880_u16);
}
#[test]
fn test_parse_gain_correct() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("gain".to_string(), "72".to_string());
let r = crate::projects::slots::parsing::parse_gain(&hmap);
assert_eq!(72, r.unwrap());
}
#[test]
fn test_parse_gain_bad_value_type_default_return() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("gain".to_string(), "AAAFSFSFSSFfssafAA".to_string());
let r = crate::projects::slots::parsing::parse_gain(&hmap);
assert_eq!(r.unwrap(), super::DEFAULT_GAIN); }
#[test]
fn test_parse_loop_mode_correct_off() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("loopmode".to_string(), "0".to_string());
let r = crate::projects::slots::parsing::parse_loop_mode(&hmap);
assert_eq!(r.unwrap(), crate::settings::LoopMode::Off);
}
#[test]
fn test_parse_loop_mode_correct_normal() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("loopmode".to_string(), "1".to_string());
let r = crate::projects::slots::parsing::parse_loop_mode(&hmap);
assert_eq!(r.unwrap(), crate::settings::LoopMode::Normal);
}
#[test]
fn test_parse_loop_mode_correct_pingpong() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("loopmode".to_string(), "2".to_string());
let r = crate::projects::slots::parsing::parse_loop_mode(&hmap);
assert_eq!(r.unwrap(), crate::settings::LoopMode::PingPong);
}
#[test]
fn test_parse_loop_mode_bad_value_type_default_return() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("loopmode".to_string(), "AAAFSFSFSSFfssafAA".to_string());
let r = crate::projects::slots::parsing::parse_loop_mode(&hmap);
assert_eq!(r.unwrap(), crate::settings::LoopMode::default());
}
#[test]
fn test_parse_tstretch_correct_off() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("tsmode".to_string(), "0".to_string());
let r = crate::projects::slots::parsing::parse_tstrech_mode(&hmap);
assert_eq!(crate::settings::TimeStretchMode::Off, r.unwrap());
}
#[test]
fn test_parse_tstretch_correct_normal() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("tsmode".to_string(), "2".to_string());
let r = crate::projects::slots::parsing::parse_tstrech_mode(&hmap);
assert_eq!(crate::settings::TimeStretchMode::Normal, r.unwrap());
}
#[test]
fn test_parse_tstretch_correct_beat() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("tsmode".to_string(), "3".to_string());
let r = crate::projects::slots::parsing::parse_tstrech_mode(&hmap);
assert_eq!(crate::settings::TimeStretchMode::Beat, r.unwrap());
}
#[test]
fn test_parse_tstretch_bad_value_type_default_return() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("tsmode".to_string(), "AAAFSFSFSSFfssafAA".to_string());
let r = crate::projects::slots::parsing::parse_tstrech_mode(&hmap);
assert_eq!(r.unwrap(), crate::settings::TimeStretchMode::default());
}
#[test]
fn test_parse_tquantize_correct_off() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("trigquantization".to_string(), "255".to_string());
let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
assert_eq!(crate::settings::TrigQuantizationMode::Direct, r.unwrap());
}
#[test]
fn test_parse_tquantize_correct_direct() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("trigquantization".to_string(), "0".to_string());
let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
assert_eq!(
crate::settings::TrigQuantizationMode::PatternLength,
r.unwrap()
);
}
#[test]
fn test_parse_tquantize_correct_onestep() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("trigquantization".to_string(), "1".to_string());
let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
assert_eq!(crate::settings::TrigQuantizationMode::OneStep, r.unwrap());
}
#[test]
fn test_parse_tquantize_correct_twostep() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("trigquantization".to_string(), "2".to_string());
let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
assert_eq!(crate::settings::TrigQuantizationMode::TwoSteps, r.unwrap());
}
#[test]
fn test_parse_tquantize_correct_threestep() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("trigquantization".to_string(), "3".to_string());
let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
assert_eq!(
crate::settings::TrigQuantizationMode::ThreeSteps,
r.unwrap()
);
}
#[test]
fn test_parse_tquantize_correct_fourstep() {
let mut hmap = std::collections::HashMap::new();
hmap.insert("trigquantization".to_string(), "4".to_string());
let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
assert_eq!(crate::settings::TrigQuantizationMode::FourSteps, r.unwrap());
}
#[test]
fn test_parse_tquantize_bad_value_type_default_return() {
let mut hmap = std::collections::HashMap::new();
hmap.insert(
"trigquantization".to_string(),
"AAAFSFSFSSFfssafAA".to_string(),
);
let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
assert_eq!(r.unwrap(), crate::settings::TrigQuantizationMode::default());
}
use std::path::PathBuf;
#[test]
fn test_parse_path_good_utf8_value() {
let test_path = "../AUDIO/some/file.wav";
let mut hmap = std::collections::HashMap::new();
hmap.insert("path".to_string(), test_path.to_string());
let r = crate::projects::slots::parsing::parse_path(&hmap);
assert_eq!(r.unwrap(), PathBuf::from(test_path));
}
#[test]
fn test_parse_empty_path() {
let test_path = "";
let mut hmap = std::collections::HashMap::new();
hmap.insert("path".to_string(), test_path.to_string());
let r = crate::projects::slots::parsing::parse_path(&hmap);
assert_eq!(r.unwrap(), PathBuf::from(test_path));
}
#[test]
fn test_parse_non_utf8_path() {
let test_path = "../AUDIO/🇯🇲something.wav";
let mut hmap = std::collections::HashMap::new();
hmap.insert("path".to_string(), test_path.to_string());
let r = crate::projects::slots::parsing::parse_path(&hmap);
assert_eq!(r.unwrap(), PathBuf::from(test_path));
}
}