use crate::graph::{NodeId, Patch};
use crate::port::{PortSpec, SignalKind};
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct DotStyle {
pub rankdir: String,
pub node_shape: String,
pub font_name: String,
pub font_size: u32,
pub show_port_names: bool,
pub color_by_signal: bool,
pub bg_color: String,
pub node_color: String,
pub edge_color: String,
}
impl Default for DotStyle {
fn default() -> Self {
Self {
rankdir: "LR".to_string(),
node_shape: "box".to_string(),
font_name: "Helvetica".to_string(),
font_size: 12,
show_port_names: true,
color_by_signal: true,
bg_color: "#1a1a2e".to_string(),
node_color: "#16213e".to_string(),
edge_color: "#e94560".to_string(),
}
}
}
impl DotStyle {
pub fn light() -> Self {
Self {
bg_color: "#ffffff".to_string(),
node_color: "#f0f0f0".to_string(),
edge_color: "#333333".to_string(),
..Default::default()
}
}
pub fn minimal() -> Self {
Self {
show_port_names: false,
color_by_signal: false,
node_shape: "ellipse".to_string(),
..Default::default()
}
}
pub fn with_rankdir(mut self, dir: impl Into<String>) -> Self {
self.rankdir = dir.into();
self
}
pub fn with_node_shape(mut self, shape: impl Into<String>) -> Self {
self.node_shape = shape.into();
self
}
}
pub struct DotExporter;
impl DotExporter {
pub fn export(patch: &Patch, style: &DotStyle) -> String {
let mut dot = String::new();
dot.push_str("digraph patch {\n");
dot.push_str(&format!(" rankdir={};\n", style.rankdir));
dot.push_str(&format!(" bgcolor=\"{}\";\n", style.bg_color));
dot.push_str(&format!(
" node [shape={}, style=filled, fillcolor=\"{}\", fontname=\"{}\", fontsize={}];\n",
style.node_shape, style.node_color, style.font_name, style.font_size
));
dot.push_str(&format!(
" edge [color=\"{}\", fontname=\"{}\", fontsize={}];\n",
style.edge_color,
style.font_name,
style.font_size - 2
));
dot.push('\n');
let mut node_map: std::collections::HashMap<NodeId, String> =
std::collections::HashMap::new();
for (id, name, module) in patch.nodes() {
node_map.insert(id, name.to_string());
let spec = module.port_spec();
let label = Self::create_node_label(name, module.type_id(), spec);
dot.push_str(&format!(" \"{}\" [label=<{}>];\n", name, label));
}
dot.push('\n');
for cable in patch.cables() {
let from_name = node_map
.get(&cable.from.node)
.map(|s| s.as_str())
.unwrap_or("?");
let to_name = node_map
.get(&cable.to.node)
.map(|s| s.as_str())
.unwrap_or("?");
let from_port = Self::get_port_name(patch, cable.from.node, cable.from.port, false);
let to_port = Self::get_port_name(patch, cable.to.node, cable.to.port, true);
let mut edge_attrs = Vec::new();
if style.show_port_names {
let label = format!("{}→{}", from_port, to_port);
edge_attrs.push(format!("label=\"{}\"", label));
}
if style.color_by_signal {
if let Some(color) = Self::get_signal_color(patch, cable.from.node, cable.from.port)
{
edge_attrs.push(format!("color=\"{}\"", color));
}
}
if let Some(att) = cable.attenuation {
if (att - 1.0).abs() > 0.01 {
edge_attrs.push("style=dashed".to_string());
if !style.show_port_names {
edge_attrs.push(format!("label=\"×{:.2}\"", att));
}
}
}
let attrs = if edge_attrs.is_empty() {
String::new()
} else {
format!(" [{}]", edge_attrs.join(", "))
};
dot.push_str(&format!(
" \"{}\" -> \"{}\"{};\n",
from_name, to_name, attrs
));
}
dot.push_str("}\n");
dot
}
pub fn export_default(patch: &Patch) -> String {
Self::export(patch, &DotStyle::default())
}
fn create_node_label(name: &str, type_id: &str, spec: &PortSpec) -> String {
let mut label = String::new();
label.push_str("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">");
label.push_str(&format!(
"<TR><TD COLSPAN=\"2\"><B>{}</B><BR/><FONT POINT-SIZE=\"10\">{}</FONT></TD></TR>",
name, type_id
));
if !spec.inputs.is_empty() || !spec.outputs.is_empty() {
label.push_str("<TR>");
label.push_str("<TD ALIGN=\"LEFT\">");
for input in &spec.inputs {
label.push_str(&format!("→ {}<BR/>", input.name));
}
if spec.inputs.is_empty() {
label.push(' ');
}
label.push_str("</TD>");
label.push_str("<TD ALIGN=\"RIGHT\">");
for output in &spec.outputs {
label.push_str(&format!("{} →<BR/>", output.name));
}
if spec.outputs.is_empty() {
label.push(' ');
}
label.push_str("</TD>");
label.push_str("</TR>");
}
label.push_str("</TABLE>");
label
}
fn get_port_name(patch: &Patch, node: NodeId, port_id: u32, is_input: bool) -> String {
for (id, _, module) in patch.nodes() {
if id == node {
let spec = module.port_spec();
let ports = if is_input {
&spec.inputs
} else {
&spec.outputs
};
for p in ports {
if p.id == port_id {
return p.name.clone();
}
}
break;
}
}
format!("port_{}", port_id)
}
fn get_signal_color(patch: &Patch, node: NodeId, port_id: u32) -> Option<String> {
for (id, _, module) in patch.nodes() {
if id == node {
let spec = module.port_spec();
for p in &spec.outputs {
if p.id == port_id {
return Some(Self::signal_kind_color(&p.kind));
}
}
break;
}
}
None
}
fn signal_kind_color(kind: &SignalKind) -> String {
match kind {
SignalKind::Audio => "#e94560".to_string(), SignalKind::CvBipolar => "#0f3460".to_string(), SignalKind::CvUnipolar => "#00b4d8".to_string(), SignalKind::VoltPerOctave => "#90be6d".to_string(), SignalKind::Gate => "#f9c74f".to_string(), SignalKind::Trigger => "#f8961e".to_string(), SignalKind::Clock => "#9d4edd".to_string(), }
}
}
#[derive(Debug, Clone, Copy)]
pub struct AutomationPoint {
pub time: u64,
pub value: f64,
}
#[derive(Debug, Clone)]
pub struct AutomationTrack {
pub param_id: String,
pub points: Vec<AutomationPoint>,
pub sample_rate: f64,
}
impl AutomationTrack {
pub fn new(param_id: impl Into<String>, sample_rate: f64) -> Self {
Self {
param_id: param_id.into(),
points: Vec::new(),
sample_rate,
}
}
pub fn record(&mut self, time: u64, value: f64) {
self.points.push(AutomationPoint { time, value });
}
pub fn value_at(&self, time: u64) -> Option<f64> {
if self.points.is_empty() {
return None;
}
let mut before: Option<&AutomationPoint> = None;
let mut after: Option<&AutomationPoint> = None;
for point in &self.points {
if point.time <= time {
before = Some(point);
}
if point.time >= time && after.is_none() {
after = Some(point);
}
}
match (before, after) {
(Some(b), Some(a)) if b.time == a.time => Some(b.value),
(Some(b), Some(a)) => {
let t = (time - b.time) as f64 / (a.time - b.time) as f64;
Some(b.value + t * (a.value - b.value))
}
(Some(b), None) => Some(b.value),
(None, Some(a)) => Some(a.value),
(None, None) => None,
}
}
pub fn duration(&self) -> u64 {
self.points.last().map(|p| p.time).unwrap_or(0)
}
pub fn duration_seconds(&self) -> f64 {
self.duration() as f64 / self.sample_rate
}
pub fn simplify(&mut self, tolerance: f64) {
if self.points.len() < 3 {
return;
}
let mut simplified = vec![self.points[0]];
for i in 1..self.points.len() - 1 {
let prev = simplified.last().unwrap();
let curr = &self.points[i];
let next = &self.points[i + 1];
let expected = prev.value
+ (next.value - prev.value) * ((curr.time - prev.time) as f64)
/ ((next.time - prev.time) as f64);
if (curr.value - expected).abs() > tolerance {
simplified.push(*curr);
}
}
simplified.push(*self.points.last().unwrap());
self.points = simplified;
}
}
#[derive(Debug)]
pub struct AutomationRecorder {
tracks: Vec<AutomationTrack>,
current_time: u64,
sample_rate: f64,
recording: bool,
record_interval: u64,
sample_counter: u64,
}
impl AutomationRecorder {
pub fn new(sample_rate: f64) -> Self {
Self {
tracks: Vec::new(),
current_time: 0,
sample_rate,
recording: false,
record_interval: 1,
sample_counter: 0,
}
}
pub fn set_interval(&mut self, interval: u64) {
self.record_interval = interval.max(1);
}
pub fn start(&mut self) {
self.recording = true;
self.current_time = 0;
self.sample_counter = 0;
}
pub fn stop(&mut self) {
self.recording = false;
}
pub fn is_recording(&self) -> bool {
self.recording
}
pub fn add_track(&mut self, param_id: impl Into<String>) {
let id = param_id.into();
if !self.tracks.iter().any(|t| t.param_id == id) {
self.tracks.push(AutomationTrack::new(id, self.sample_rate));
}
}
pub fn remove_track(&mut self, param_id: &str) {
self.tracks.retain(|t| t.param_id != param_id);
}
pub fn tick(&mut self, mut get_value: impl FnMut(&str) -> Option<f64>) {
if !self.recording {
return;
}
self.sample_counter += 1;
if self.sample_counter >= self.record_interval {
self.sample_counter = 0;
for track in &mut self.tracks {
if let Some(value) = get_value(&track.param_id) {
track.record(self.current_time, value);
}
}
}
self.current_time += 1;
}
pub fn tracks(&self) -> &[AutomationTrack] {
&self.tracks
}
pub fn get_track(&self, param_id: &str) -> Option<&AutomationTrack> {
self.tracks.iter().find(|t| t.param_id == param_id)
}
pub fn clear(&mut self) {
for track in &mut self.tracks {
track.points.clear();
}
self.current_time = 0;
}
pub fn simplify_all(&mut self, tolerance: f64) {
for track in &mut self.tracks {
track.simplify(tolerance);
}
}
pub fn export(&self) -> AutomationData {
AutomationData {
sample_rate: self.sample_rate,
duration: self.current_time,
tracks: self.tracks.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct AutomationData {
pub sample_rate: f64,
pub duration: u64,
pub tracks: Vec<AutomationTrack>,
}
#[derive(Debug)]
pub struct Scope {
buffer_size: usize,
buffer: VecDeque<f64>,
trigger_level: f64,
trigger_mode: TriggerMode,
triggered: bool,
samples_since_trigger: usize,
prev_sample: f64,
time_div: usize,
volt_div: f64,
frozen_buffer: Option<Vec<f64>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TriggerMode {
Free,
RisingEdge,
FallingEdge,
AnyEdge,
Single,
}
impl Scope {
pub fn new(buffer_size: usize) -> Self {
Self {
buffer_size,
buffer: VecDeque::with_capacity(buffer_size),
trigger_level: 0.0,
trigger_mode: TriggerMode::Free,
triggered: false,
samples_since_trigger: 0,
prev_sample: 0.0,
time_div: buffer_size / 10,
volt_div: 1.0,
frozen_buffer: None,
}
}
pub fn set_trigger_level(&mut self, level: f64) {
self.trigger_level = level;
}
pub fn set_trigger_mode(&mut self, mode: TriggerMode) {
self.trigger_mode = mode;
self.triggered = false;
self.frozen_buffer = None;
}
pub fn set_time_div(&mut self, samples: usize) {
self.time_div = samples.max(1);
}
pub fn set_volt_div(&mut self, volts: f64) {
self.volt_div = volts.max(0.001);
}
pub fn tick(&mut self, sample: f64) {
let trigger_detected = match self.trigger_mode {
TriggerMode::Free => true,
TriggerMode::RisingEdge => {
self.prev_sample < self.trigger_level && sample >= self.trigger_level
}
TriggerMode::FallingEdge => {
self.prev_sample > self.trigger_level && sample <= self.trigger_level
}
TriggerMode::AnyEdge => {
(self.prev_sample < self.trigger_level && sample >= self.trigger_level)
|| (self.prev_sample > self.trigger_level && sample <= self.trigger_level)
}
TriggerMode::Single => {
if self.frozen_buffer.is_some() {
false
} else {
self.prev_sample < self.trigger_level && sample >= self.trigger_level
}
}
};
if trigger_detected && !self.triggered {
self.triggered = true;
self.samples_since_trigger = 0;
self.buffer.clear();
}
if self.triggered || self.trigger_mode == TriggerMode::Free {
self.buffer.push_back(sample);
if self.buffer.len() > self.buffer_size {
self.buffer.pop_front();
}
self.samples_since_trigger += 1;
if self.samples_since_trigger >= self.buffer_size {
if self.trigger_mode == TriggerMode::Single {
self.frozen_buffer = Some(self.buffer.iter().copied().collect());
}
self.triggered = false;
}
}
self.prev_sample = sample;
}
pub fn get_buffer(&self) -> &[f64] {
self.frozen_buffer.as_deref().unwrap_or_default()
}
pub fn buffer_vec(&self) -> Vec<f64> {
if let Some(ref frozen) = self.frozen_buffer {
frozen.clone()
} else {
self.buffer.iter().copied().collect()
}
}
pub fn get_display_data(&self) -> Vec<(f64, f64)> {
let samples = self.buffer_vec();
let len = samples.len();
if len == 0 {
return vec![];
}
samples
.iter()
.enumerate()
.map(|(i, &v)| (i as f64 / len as f64, v))
.collect()
}
pub fn reset(&mut self) {
self.buffer.clear();
self.triggered = false;
self.samples_since_trigger = 0;
self.prev_sample = 0.0;
self.frozen_buffer = None;
}
}
#[derive(Debug)]
pub struct SpectrumAnalyzer {
fft_size: usize,
buffer: Vec<f64>,
write_pos: usize,
sample_rate: f64,
spectrum: Vec<f64>,
smoothing: f64,
}
impl SpectrumAnalyzer {
pub fn new(fft_size: usize, sample_rate: f64) -> Self {
let fft_size = fft_size.next_power_of_two();
Self {
fft_size,
buffer: vec![0.0; fft_size],
write_pos: 0,
sample_rate,
spectrum: vec![-100.0; fft_size / 2],
smoothing: 0.8,
}
}
pub fn set_smoothing(&mut self, smoothing: f64) {
self.smoothing = smoothing.clamp(0.0, 0.99);
}
pub fn tick(&mut self, sample: f64) {
self.buffer[self.write_pos] = sample;
self.write_pos = (self.write_pos + 1) % self.fft_size;
if self.write_pos == 0 {
self.compute_spectrum();
}
}
fn compute_spectrum(&mut self) {
let n = self.fft_size;
let half = n / 2;
let mut re: Vec<f64> = Vec::with_capacity(n);
let mut im: Vec<f64> = vec![0.0; n];
for (i, &sample) in self.buffer.iter().enumerate() {
let window =
0.5 * (1.0 - (2.0 * std::f64::consts::PI * i as f64 / (n - 1) as f64).cos());
re.push(sample * window);
}
crate::observer::fft_radix2(&mut re, &mut im);
for k in 0..half {
let magnitude = (re[k] * re[k] + im[k] * im[k]).sqrt() / (n as f64);
let db = 20.0 * (magnitude + 1e-10).log10();
self.spectrum[k] = self.smoothing * self.spectrum[k] + (1.0 - self.smoothing) * db;
}
}
pub fn get_spectrum(&self) -> Vec<(f64, f64)> {
let freq_resolution = self.sample_rate / self.fft_size as f64;
self.spectrum
.iter()
.enumerate()
.map(|(i, &db)| (i as f64 * freq_resolution, db))
.collect()
}
pub fn magnitude_at(&self, freq: f64) -> f64 {
let bin = (freq * self.fft_size as f64 / self.sample_rate) as usize;
if bin < self.spectrum.len() {
self.spectrum[bin]
} else {
-100.0
}
}
pub fn peak_frequency(&self) -> f64 {
let freq_resolution = self.sample_rate / self.fft_size as f64;
let (peak_bin, _) = self
.spectrum
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| {
let a = if a.is_nan() { f64::NEG_INFINITY } else { **a };
let b = if b.is_nan() { f64::NEG_INFINITY } else { **b };
a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or((0, &-100.0));
peak_bin as f64 * freq_resolution
}
pub fn reset(&mut self) {
self.buffer.fill(0.0);
self.spectrum.fill(-100.0);
self.write_pos = 0;
}
}
#[derive(Debug)]
pub struct LevelMeter {
rms_db: f64,
peak_db: f64,
peak_hold_db: f64,
peak_hold_counter: u64,
peak_hold_samples: u64,
rms_window: VecDeque<f64>,
window_size: usize,
attack_coeff: f64,
release_coeff: f64,
}
impl LevelMeter {
pub fn new(sample_rate: f64) -> Self {
let window_size = (sample_rate * 0.05) as usize; Self {
rms_db: -100.0,
peak_db: -100.0,
peak_hold_db: -100.0,
peak_hold_counter: 0,
peak_hold_samples: (sample_rate * 1.5) as u64, rms_window: VecDeque::with_capacity(window_size),
window_size,
attack_coeff: (-1.0 / (sample_rate * 0.001)).exp(), release_coeff: (-1.0 / (sample_rate * 0.300)).exp(), }
}
pub fn set_peak_hold_time(&mut self, seconds: f64, sample_rate: f64) {
self.peak_hold_samples = (sample_rate * seconds) as u64;
}
pub fn tick(&mut self, sample: f64) {
let abs_sample = sample.abs();
self.rms_window.push_back(sample * sample);
if self.rms_window.len() > self.window_size {
self.rms_window.pop_front();
}
let rms = (self.rms_window.iter().sum::<f64>() / self.rms_window.len() as f64).sqrt();
let target_rms_db = 20.0 * (rms + 1e-10).log10();
let coeff = if target_rms_db > self.rms_db {
self.attack_coeff
} else {
self.release_coeff
};
self.rms_db = coeff * self.rms_db + (1.0 - coeff) * target_rms_db;
let sample_db = 20.0 * (abs_sample + 1e-10).log10();
if sample_db > self.peak_db {
self.peak_db = sample_db;
} else {
self.peak_db =
self.release_coeff * self.peak_db + (1.0 - self.release_coeff) * sample_db;
}
if sample_db >= self.peak_hold_db {
self.peak_hold_db = sample_db;
self.peak_hold_counter = 0;
} else {
self.peak_hold_counter += 1;
if self.peak_hold_counter >= self.peak_hold_samples {
self.peak_hold_db = self.peak_db;
}
}
}
pub fn rms(&self) -> f64 {
self.rms_db
}
pub fn peak(&self) -> f64 {
self.peak_db
}
pub fn peak_hold(&self) -> f64 {
self.peak_hold_db
}
pub fn is_clipping(&self) -> bool {
self.peak_db > 0.0
}
pub fn reset(&mut self) {
self.rms_db = -100.0;
self.peak_db = -100.0;
self.peak_hold_db = -100.0;
self.peak_hold_counter = 0;
self.rms_window.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dot_style_default() {
let style = DotStyle::default();
assert_eq!(style.rankdir, "LR");
assert!(style.show_port_names);
assert!(style.color_by_signal);
}
#[test]
fn test_dot_style_light() {
let style = DotStyle::light();
assert_eq!(style.bg_color, "#ffffff");
}
#[test]
fn test_dot_style_minimal() {
let style = DotStyle::minimal();
assert!(!style.show_port_names);
assert!(!style.color_by_signal);
}
#[test]
fn test_automation_track() {
let mut track = AutomationTrack::new("test.param", 44100.0);
track.record(0, 0.0);
track.record(44100, 1.0);
assert_eq!(track.value_at(0), Some(0.0));
assert_eq!(track.value_at(44100), Some(1.0));
let mid = track.value_at(22050).unwrap();
assert!((mid - 0.5).abs() < 0.01);
}
#[test]
fn test_automation_recorder() {
let mut recorder = AutomationRecorder::new(44100.0);
recorder.add_track("osc.freq");
recorder.start();
let mut value = 0.0;
for _ in 0..100 {
recorder.tick(|_| {
value += 0.01;
Some(value)
});
}
recorder.stop();
let track = recorder.get_track("osc.freq").unwrap();
assert_eq!(track.points.len(), 100);
}
#[test]
fn test_automation_simplify() {
let mut track = AutomationTrack::new("test", 44100.0);
for i in 0..100 {
track.record(i * 100, i as f64);
}
let original_len = track.points.len();
track.simplify(0.1);
assert!(track.points.len() < original_len);
}
#[test]
fn test_scope_free_running() {
let mut scope = Scope::new(100);
scope.set_trigger_mode(TriggerMode::Free);
for i in 0..200 {
scope.tick((i as f64 * 0.1).sin());
}
let data = scope.get_display_data();
assert_eq!(data.len(), 100);
}
#[test]
fn test_scope_trigger() {
let mut scope = Scope::new(100);
scope.set_trigger_mode(TriggerMode::RisingEdge);
scope.set_trigger_level(0.0);
for _ in 0..50 {
scope.tick(-1.0);
}
for i in 0..150 {
scope.tick(i as f64 * 0.1);
}
let data = scope.get_display_data();
assert!(!data.is_empty());
}
#[test]
fn test_spectrum_analyzer() {
let mut analyzer = SpectrumAnalyzer::new(256, 44100.0);
for i in 0..512 {
let sample = (2.0 * std::f64::consts::PI * 440.0 * i as f64 / 44100.0).sin();
analyzer.tick(sample);
}
let peak = analyzer.peak_frequency();
assert!((peak - 440.0).abs() < 200.0);
}
#[test]
fn test_spectrum_peak_frequency_nan_safe() {
let mut analyzer = SpectrumAnalyzer::new(256, 44100.0);
let freq_resolution = 44100.0 / analyzer.fft_size as f64;
analyzer.spectrum[5] = 0.0; analyzer.spectrum[10] = f64::NAN;
let peak = analyzer.peak_frequency();
assert!(peak.is_finite());
assert!((peak - 5.0 * freq_resolution).abs() < 1e-6);
for bin in analyzer.spectrum.iter_mut() {
*bin = f64::NAN;
}
assert!(analyzer.peak_frequency().is_finite());
}
#[test]
fn test_level_meter() {
let mut meter = LevelMeter::new(44100.0);
for i in 0..44100 {
let sample = (2.0 * std::f64::consts::PI * 440.0 * i as f64 / 44100.0).sin();
meter.tick(sample);
}
let rms = meter.rms();
assert!(rms > -6.0 && rms < 0.0);
}
#[test]
fn test_level_meter_clipping() {
let mut meter = LevelMeter::new(44100.0);
for _ in 0..1000 {
meter.tick(2.0);
}
assert!(meter.is_clipping());
}
}