From 82e6d51f558e64ed6134fbaee764a9fa5fbfd02a Mon Sep 17 00:00:00 2001 From: Vixalie Date: Wed, 5 Mar 2025 09:29:17 +0800 Subject: [PATCH] migrate pattern structures from previous projects. --- src-tauri/Cargo.toml | 1 + src-tauri/src/canvas_model.rs | 13 ++ src-tauri/src/fraction.rs | 88 +++++++++ src-tauri/src/lib.rs | 4 + src-tauri/src/pattern/algorithms.rs | 73 +++++++ src-tauri/src/pattern/frequency_shifting.rs | 201 ++++++++++++++++++++ src-tauri/src/pattern/mod.rs | 66 +++++++ src-tauri/src/pattern/pulse.rs | 127 +++++++++++++ src-tauri/src/protocols/mod.rs | 37 ++++ 9 files changed, 610 insertions(+) create mode 100644 src-tauri/src/canvas_model.rs create mode 100644 src-tauri/src/fraction.rs create mode 100644 src-tauri/src/pattern/algorithms.rs create mode 100644 src-tauri/src/pattern/frequency_shifting.rs create mode 100644 src-tauri/src/pattern/mod.rs create mode 100644 src-tauri/src/pattern/pulse.rs create mode 100644 src-tauri/src/protocols/mod.rs diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8b55fec..eb1ad7b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -32,6 +32,7 @@ bincode = "1.3.3" uuid = { version = "1.10.0", features = ["serde"] } tauri-plugin-dialog = "2" futures = "0.3.31" +rand = "0.8.5" tauri-plugin-devtools = "2.0.0" tauri-plugin-os = "2" tauri-plugin-notification = "2" diff --git a/src-tauri/src/canvas_model.rs b/src-tauri/src/canvas_model.rs new file mode 100644 index 0000000..03da36d --- /dev/null +++ b/src-tauri/src/canvas_model.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct CanvasPoint { + pub x: f64, + pub y: f64, +} + +impl CanvasPoint { + pub fn new(x: f64, y: f64) -> Self { + CanvasPoint { x, y } + } +} diff --git a/src-tauri/src/fraction.rs b/src-tauri/src/fraction.rs new file mode 100644 index 0000000..0b6399a --- /dev/null +++ b/src-tauri/src/fraction.rs @@ -0,0 +1,88 @@ +use std::{ + cmp::Ordering, + fmt::{self, Display, Formatter}, +}; + +use serde::Serialize; + +#[derive(Debug, Serialize)] +pub struct Fraction { + pub numerator: i64, + pub denominator: i64, +} + +impl Fraction { + pub fn new_from_float(numerator: f64, denominator: f64) -> Self { + if denominator == 0.0 { + panic!("denomitator cannot be zero"); + } + + // 计算转换因子,以消除浮点数的小数部分 + let scale_factor = 10i64 + .pow(Self::decimal_places(numerator).max(Self::decimal_places(denominator)) as u32); + + let int_numerator = (numerator * scale_factor as f64).round() as i64; + let int_denominator = (denominator * scale_factor as f64).round() as i64; + + let gcd = Self::gcd(int_numerator, int_denominator); + Fraction { + numerator: int_numerator / gcd, + denominator: int_denominator / gcd, + } + } + + pub fn new(numerator: i64, denominator: i64) -> Self { + if denominator == 0 { + panic!("denominator cannot be zero"); + } + + let gcd = Self::gcd(numerator, denominator); + Fraction { + numerator: numerator / gcd, + denominator: denominator / gcd, + } + } + + fn decimal_places(value: f64) -> u32 { + let mut decimal_places = 0; + let mut scaled_value = value; + while scaled_value.fract() != 0.0 && decimal_places < 15 { + scaled_value *= 10.0; + decimal_places += 1; + } + decimal_places + } + + fn gcd(a: i64, b: i64) -> i64 { + let mut a = a.abs(); + let mut b = b.abs(); + while b != 0 { + let temp = b; + b = a % b; + a = temp; + } + a + } + + fn cmp(&self, other: &Fraction) -> Ordering { + (self.numerator * other.denominator).cmp(&(other.numerator * self.denominator)) + } +} + +impl PartialEq for Fraction { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +impl PartialOrd for Fraction { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Display for Fraction { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}/{}", self.numerator, self.denominator) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 852759a..75c22c2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,10 +10,14 @@ use tauri::{ use tauri_plugin_dialog::DialogExt; mod bluetooth; +mod canvas_model; mod cmd; mod config_db; mod errors; +mod fraction; +mod pattern; mod playlist; +mod protocols; mod state; #[cfg_attr(mobile, tauri::mobile_entry_point)] diff --git a/src-tauri/src/pattern/algorithms.rs b/src-tauri/src/pattern/algorithms.rs new file mode 100644 index 0000000..885e55a --- /dev/null +++ b/src-tauri/src/pattern/algorithms.rs @@ -0,0 +1,73 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[repr(u8)] +pub enum PulseInterpolation { + Constant = 1, + JumpChange = 2, + Linear = 3, + Bilinear = 4, + Cubic = 5, + Bezier = 6, +} + +impl PulseInterpolation { + fn as_u8(&self) -> u8 { + self.clone() as u8 + } + + fn as_str(&self) -> String { + self.as_u8().to_string() + } +} + +impl TryFrom<&str> for PulseInterpolation { + type Error = String; + + fn try_from(value: &str) -> Result { + match value.parse::() { + Ok(1) => Ok(PulseInterpolation::Constant), + Ok(2) => Ok(PulseInterpolation::JumpChange), + Ok(3) => Ok(PulseInterpolation::Linear), + Ok(4) => Ok(PulseInterpolation::Bilinear), + Ok(5) => Ok(PulseInterpolation::Cubic), + Ok(6) => Ok(PulseInterpolation::Bezier), + _ => Err(format!("Invalid Pulse Interpolation value: {}", value)), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[repr(u8)] +pub enum FrequencyChange { + Constant = 1, + Linear = 2, + EaseIn = 3, + EaseOut = 4, + Randomize = 5, +} + +impl FrequencyChange { + fn as_u8(&self) -> u8 { + self.clone() as u8 + } + + fn as_str(&self) -> String { + self.as_u8().to_string() + } +} + +impl TryFrom<&str> for FrequencyChange { + type Error = String; + + fn try_from(value: &str) -> Result { + match value.parse::() { + Ok(1) => Ok(FrequencyChange::Constant), + Ok(2) => Ok(FrequencyChange::Linear), + Ok(3) => Ok(FrequencyChange::EaseIn), + Ok(4) => Ok(FrequencyChange::EaseOut), + Ok(5) => Ok(FrequencyChange::Randomize), + _ => Err(format!("Invalid Frequency Change value: {}", value)), + } + } +} diff --git a/src-tauri/src/pattern/frequency_shifting.rs b/src-tauri/src/pattern/frequency_shifting.rs new file mode 100644 index 0000000..5fb5151 --- /dev/null +++ b/src-tauri/src/pattern/frequency_shifting.rs @@ -0,0 +1,201 @@ +use rand::Rng; +use serde::{Deserialize, Serialize}; + +use crate::fraction::Fraction; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum FrequencyShifting { + Linear, + Quadratic, + Cubic, + Ease, + Pulsating, + Spiking, + Randomize, + Maniac, + Synchronized, +} + +impl TryFrom for FrequencyShifting { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(FrequencyShifting::Linear), + 1 => Ok(FrequencyShifting::Quadratic), + 2 => Ok(FrequencyShifting::Cubic), + 3 => Ok(FrequencyShifting::Ease), + 4 => Ok(FrequencyShifting::Pulsating), + 5 => Ok(FrequencyShifting::Spiking), + 6 => Ok(FrequencyShifting::Randomize), + 7 => Ok(FrequencyShifting::Maniac), + 8 => Ok(FrequencyShifting::Synchronized), + _ => Err(String::from("Unregconized shifting mode")), + } + } +} + +pub fn deserialize_frequency_shifting<'de, D>( + deserializer: D, +) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = u32::deserialize(deserializer)?; + FrequencyShifting::try_from(value).map_err(serde::de::Error::custom) +} + +pub fn shifting_frequency( + sample_tick: u32, + start_timing: u32, + start_pulse: &crate::pattern::Pulse, + end_pulse: &crate::pattern::Pulse, +) -> (u16, u16, u8) { + match end_pulse.frequency_shifting { + FrequencyShifting::Linear => bezier_shifting( + sample_tick, + start_timing, + start_pulse.frequency, + start_timing + start_pulse.duration, + end_pulse.frequency, + (0.0, 0.0), + (1.0, 1.0), + ), + FrequencyShifting::Quadratic => bezier_shifting( + sample_tick, + start_timing, + start_pulse.frequency, + start_timing + start_pulse.duration, + end_pulse.frequency, + (1.0, 0.0), + (1.0, 1.0), + ), + FrequencyShifting::Cubic => bezier_shifting( + sample_tick, + start_timing, + start_pulse.frequency, + start_timing + start_pulse.duration, + end_pulse.frequency, + (0.9, 0.1), + (1.0, 0.2), + ), + FrequencyShifting::Ease => bezier_shifting( + sample_tick, + start_timing, + start_pulse.frequency, + start_timing + start_pulse.duration, + end_pulse.frequency, + (0.33, 0.0), + (0.67, 1.0), + ), + FrequencyShifting::Pulsating => periodic_shifting( + sample_tick, + start_timing, + start_timing + start_pulse.duration, + start_pulse.frequency.min(end_pulse.frequency), + end_pulse.frequency.max(start_pulse.frequency), + ), + FrequencyShifting::Spiking => periodic_shifting( + sample_tick, + start_timing, + start_timing + start_pulse.duration, + start_pulse.frequency.min(end_pulse.frequency), + start_pulse.frequency.max(end_pulse.frequency) * 2.0, + ), + FrequencyShifting::Randomize => randomize_shifting( + start_pulse.frequency.min(end_pulse.frequency), + start_pulse.frequency.max(end_pulse.frequency), + ), + FrequencyShifting::Maniac => randomize_shifting(1.0, 1000.0), + FrequencyShifting::Synchronized => bezier_shifting( + sample_tick, + start_timing, + start_pulse.frequency, + start_timing + start_pulse.duration, + end_pulse.frequency, + end_pulse.control_point_1.into(), + end_pulse.control_point_2.into(), + ), + } +} + +fn estimate_frequency_level(frequency: f64) -> u8 { + match frequency { + 0.0..=150.0 => 1, + 150.0..=300.0 => 2, + 300.0..=600.0 => 3, + 600.0..=800.0 => 4, + 800.0.. => 5, + _ => 0, + } +} + +fn resolve_frequency(frequency: f64) -> (u16, u16) { + let waveform_frequency = frequency * 1000_f64; + let x = (waveform_frequency.sqrt() * 15_f64).floor() as i64; + let y = (waveform_frequency - x as f64).floor() as i64; + let f = Fraction::new(x, y); + let output = u16::try_from(f.numerator).unwrap_or(u16::MAX); + let idle = u16::try_from(f.denominator).unwrap_or(u16::MAX); + (output, idle) +} + +fn bezier_shifting( + sample_tick: u32, + start_time: u32, + start_frequency: f64, + end_time: u32, + end_frequency: f64, + control_1: (f64, f64), + control_2: (f64, f64), +) -> (u16, u16, u8) { + let t = (sample_tick - start_time) as f64 / (end_time - start_time) as f64; + let u = 1.0 - t; + let tt = t * t; + let uu = u * u; + let uuu = uu * u; + let ttt = tt * t; + + let p0 = (start_time as f64, start_frequency); + let p1 = control_1; + let p2 = control_2; + let p3 = (end_time as f64, end_frequency); + + let frequency = uuu * p0.1 + 3.0 * uu * t * p1.1 + 3.0 * u * tt * p2.1 + ttt * p3.1; + + let (x, y) = resolve_frequency(frequency); + let estimate_level = estimate_frequency_level(frequency); + (x, y, estimate_level) +} + +fn periodic_shifting( + sample_tick: u32, + start_time: u32, + _end_time: u32, + min_frequency: f64, + max_frequency: f64, +) -> (u16, u16, u8) { + let period_length = { + let mut l = sample_tick - start_time; + while l > 25 { + l -= 25; + } + l + }; + let frequency = if (sample_tick - start_time) / period_length % 2 == 0 { + max_frequency + } else { + min_frequency + }; + let (x, y) = resolve_frequency(frequency); + let estimate_level = estimate_frequency_level(frequency); + (x, y, estimate_level) +} + +fn randomize_shifting(min_frequency: f64, max_frequency: f64) -> (u16, u16, u8) { + let mut rng = rand::thread_rng(); + let frequency = rng.gen_range(min_frequency..=max_frequency); + let (x, y) = resolve_frequency(frequency); + let estimate_level = estimate_frequency_level(frequency); + (x, y, estimate_level) +} diff --git a/src-tauri/src/pattern/mod.rs b/src-tauri/src/pattern/mod.rs new file mode 100644 index 0000000..ce2055c --- /dev/null +++ b/src-tauri/src/pattern/mod.rs @@ -0,0 +1,66 @@ +mod algorithms; +mod frequency_shifting; +mod pulse; + +use std::collections::VecDeque; + +pub use pulse::Pulse; +use pulse::{generate_pulse_sequence, RawPulse}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Pattern { + pub id: Uuid, + pub name: String, + pub smooth_repeat: bool, + pub pulses: VecDeque, +} + +impl Pattern { + pub fn add_pulse(&mut self, pulse: Pulse) { + self.pulses.push_back(pulse); + } + + pub fn delete_pulse(&mut self, pulse_id: &Uuid) { + if let Some(index) = self.pulses.iter().position(|p| &p.id == pulse_id) { + self.pulses.remove(index); + } + } + + pub fn move_forward(&mut self, pulse_id: &Uuid) { + if let Some(index) = self.pulses.iter().position(|p| &p.id == pulse_id) { + if index > 0 && index <= self.pulses.len() - 1 { + self.pulses.swap(index, index - 1); + } + } + } + + pub fn move_backward(&mut self, pulse_id: &Uuid) { + if let Some(index) = self.pulses.iter().position(|p| &p.id == pulse_id) { + if index < self.pulses.len() - 1 { + self.pulses.swap(index, index + 1); + } + } + } + + pub fn set_smooth_repeat(&mut self, repeat: bool) { + self.smooth_repeat = repeat; + } + + pub fn output_pulses(&self) -> Box> { + generate_pulse_sequence(self.pulses.clone(), self.smooth_repeat) + } + + pub fn total_duration(&self) -> u32 { + let mut duration = 0; + for (index, pulse) in self.pulses.iter().enumerate() { + if (index == self.pulses.len() - 1 && self.smooth_repeat) + || index < self.pulses.len() - 1 + { + duration += pulse.duration; + } + } + duration + } +} diff --git a/src-tauri/src/pattern/pulse.rs b/src-tauri/src/pattern/pulse.rs new file mode 100644 index 0000000..c7472cc --- /dev/null +++ b/src-tauri/src/pattern/pulse.rs @@ -0,0 +1,127 @@ +use std::collections::VecDeque; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::frequency_shifting::{self, deserialize_frequency_shifting, FrequencyShifting}; +use crate::fraction::Fraction; + +pub fn calculate_output_ratio(waveform_frequency: f64) -> Fraction { + let x = waveform_frequency.sqrt() * 15f64; + let y = waveform_frequency * 1000f64 - x; + Fraction::new_from_float(x, y) +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct CanvasPoint { + pub x: f64, + pub y: f64, +} + +impl Into<(f64, f64)> for CanvasPoint { + fn into(self) -> (f64, f64) { + (self.x, self.y) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Pulse { + pub order: u32, + pub id: Uuid, + #[serde(default)] + pub duration: u32, + pub width: u32, + pub maniac: bool, + pub frequency: f64, + #[serde(deserialize_with = "deserialize_frequency_shifting")] + pub frequency_shifting: FrequencyShifting, + pub control_point_1: CanvasPoint, + pub control_point_2: CanvasPoint, +} + +#[derive(Debug, Clone, Copy, Serialize)] +pub struct RawPulse { + pub tick_order: u32, + pub z: u16, + pub x: u16, + pub y: u16, + pub frequency_level: u8, +} + +fn bezier( + t: f64, + p0: CanvasPoint, + p1: CanvasPoint, + cp1: CanvasPoint, + cp2: CanvasPoint, +) -> CanvasPoint { + let mut p = CanvasPoint { x: t, y: 0.0 }; + + let t = (t - p0.x) / (p1.x - p0.x); + let u = 1.0 - t; // 1-t + let tt = t * t; // t^2 + let uu = u * u; // (1-t)^2 + let uuu = uu * u; // (1-t)^3 + let ttt = tt * t; // t^3 + + p.y = uuu * p0.y; // (1-t)^3 * p0.y + p.y += 3.0 * uu * t * cp1.y; // 3 * (1-t)^2 * t * cp1.y + p.y += 3.0 * u * tt * cp2.y; // 3 * (1-t) * t^2 * cp2.y + p.y += ttt * p1.y; // t^3 * p1.y + + p +} + +pub fn generate_pulse_sequence>( + pulse: P, + smooth_repeat: bool, +) -> Box> { + let sorted_pulses = pulse.into_iter().collect::>(); + + let mut sample_tick = 0; + let mut timing = 0; + let mut sample_queue: VecDeque<(u32, u32, Pulse, Pulse)> = VecDeque::new(); + let head_pulse = sorted_pulses.front(); + let mut peekable_pulses = sorted_pulses.iter().peekable(); + while let Some(p) = peekable_pulses.next() { + if let Some(next_pulse) = peekable_pulses.peek() { + while sample_tick < (timing + p.duration) { + sample_queue.push_back((sample_tick, timing, p.clone(), (*next_pulse).clone())); + sample_tick += 25; + } + timing += p.duration; + } else if smooth_repeat && head_pulse.is_some() { + sample_queue.push_back((sample_tick, timing, p.clone(), head_pulse.unwrap().clone())); + } else { + break; + } + } + + Box::new(std::iter::from_fn(move || { + if let Some((tick, timing, prev, next)) = sample_queue.pop_front() { + let bezier_point = bezier( + tick as f64, + CanvasPoint { + x: timing as f64, + y: prev.frequency, + }, + CanvasPoint { + x: (timing + prev.duration) as f64, + y: next.frequency, + }, + prev.control_point_1, + prev.control_point_2, + ); + let (x, y, frequency_level) = + frequency_shifting::shifting_frequency(tick, timing, &prev, &next); + return Some(RawPulse { + tick_order: tick, + z: bezier_point.y as u16, + x, + y, + frequency_level, + }); + } + None + })) +} diff --git a/src-tauri/src/protocols/mod.rs b/src-tauri/src/protocols/mod.rs new file mode 100644 index 0000000..1d116bb --- /dev/null +++ b/src-tauri/src/protocols/mod.rs @@ -0,0 +1,37 @@ +use btleplug::platform::Peripheral; +use thiserror::Error; + +#[derive(Debug, Clone, Error)] +pub enum CoyoteProtocolError { + #[error("Failed to connect to peripheral")] + FailedToConnect, +} + +#[derive(Debug, Clone, Copy)] +pub enum CoyoteChannel { + A, + B, +} + +pub trait CoyoteProtocol { + fn identify(peripheral: &Peripheral) -> bool; + fn connected(&mut self, peripheral: &Peripheral) -> Result<(), CoyoteProtocolError>; + fn disconnect(&mut self) -> Result<(), CoyoteProtocolError>; + fn notify_battery_level(&self, callback: F) + where + F: FnOnce(Option); + fn notify_strength_change(&self, callback: F) + where + F: FnOnce((Option, Option)); + fn set_output_strength( + &self, + channel: CoyoteChannel, + strength: i16, + ) -> Result<(), CoyoteProtocolError>; + fn output( + &self, + channel: CoyoteChannel, + data: Vec, + maintain: bool, + ) -> Result<(), CoyoteProtocolError>; +}