use std::f64::consts::{PI, TAU};
use crate::{
Normalization, SignConvention, SignalError,
fft::{Complex, fft},
interpolate::{forward_scale, reconstruction_scale},
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AnalyticSignalPlan {
pub normalization: Normalization,
pub sign: SignConvention,
pub max_len: usize,
pub max_work: u64,
}
impl Default for AnalyticSignalPlan {
fn default() -> Self {
Self {
normalization: Normalization::Inverse,
sign: SignConvention::NegativeForward,
max_len: 1_048_576,
max_work: 1_000_000_000,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AnalyticSignalReport {
pub len: usize,
pub normalization: Normalization,
pub sign: SignConvention,
pub retained_nyquist: bool,
pub work_units: u64,
pub work_limit: u64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AnalyticSignal {
pub samples: Vec<(f64, f64)>,
pub report: AnalyticSignalReport,
}
pub fn analytic_signal(
samples: &[f64],
plan: &AnalyticSignalPlan,
) -> Result<AnalyticSignal, SignalError> {
if samples.is_empty() || samples.len() > plan.max_len {
return Err(SignalError::InvalidLength {
len: samples.len(),
reason: "analytic signal length must be nonzero and within the plan limit",
});
}
for (index, value) in samples.iter().copied().enumerate() {
if !value.is_finite() {
return Err(SignalError::NonFinite {
index,
component: "value",
});
}
}
let work_units = analytic_work(samples.len())?;
if work_units > plan.max_work {
return Err(SignalError::WorkLimit {
required: work_units,
maximum: plan.max_work,
});
}
let mut bins = fft(
&samples
.iter()
.map(|value| Complex::new(*value, 0.0))
.collect::<Vec<_>>(),
plan.sign.angle_sign(crate::Direction::Forward),
)?;
let coefficient_scale = forward_scale(plan.normalization, samples.len());
for bin in &mut bins {
*bin = bin.scale(coefficient_scale);
}
let positive_end = samples.len().div_ceil(2);
for bin in bins.iter_mut().take(positive_end).skip(1) {
*bin = bin.scale(2.0);
}
for bin in bins.iter_mut().skip(samples.len() / 2 + 1) {
*bin = Complex::ZERO;
}
let output_scale = reconstruction_scale(plan.normalization, samples.len());
let samples = fft(&bins, plan.sign.angle_sign(crate::Direction::Inverse))?
.into_iter()
.enumerate()
.map(|(index, value)| {
let value = value.scale(output_scale);
if !value.re.is_finite() {
return Err(SignalError::NonFinite {
index,
component: "real",
});
}
if !value.im.is_finite() {
return Err(SignalError::NonFinite {
index,
component: "imag",
});
}
Ok(value.into())
})
.collect::<Result<Vec<_>, _>>()?;
let len = samples.len();
Ok(AnalyticSignal {
samples,
report: AnalyticSignalReport {
len,
normalization: plan.normalization,
sign: plan.sign,
retained_nyquist: len.is_multiple_of(2),
work_units,
work_limit: plan.max_work,
},
})
}
pub fn hilbert_transform(
samples: &[f64],
plan: &AnalyticSignalPlan,
) -> Result<Vec<f64>, SignalError> {
Ok(analytic_signal(samples, plan)?
.samples
.into_iter()
.map(|(_, imaginary)| imaginary)
.collect())
}
pub fn unwrap_phase(phases: &[f64], discontinuity: f64) -> Result<Vec<f64>, SignalError> {
if !discontinuity.is_finite() || !(PI..=TAU).contains(&discontinuity) {
return Err(SignalError::InvalidPolicy {
policy: "phase unwrap discontinuity",
reason: "the finite threshold must lie between pi and one turn",
});
}
for (index, phase) in phases.iter().copied().enumerate() {
if !phase.is_finite() {
return Err(SignalError::NonFinite {
index,
component: "phase",
});
}
}
let Some(first) = phases.first().copied() else {
return Ok(Vec::new());
};
let mut output = Vec::with_capacity(phases.len());
output.push(first);
let mut correction = 0.0;
for index in 1..phases.len() {
let delta = phases[index] - phases[index - 1];
if delta.abs() > discontinuity {
correction += (delta + PI).rem_euclid(TAU) - PI - delta;
}
output.push(phases[index] + correction);
}
Ok(output)
}
#[derive(Clone, Debug, PartialEq)]
pub struct InstantaneousFrequency {
pub time_seconds: Vec<f64>,
pub frequency_hz: Vec<f64>,
pub unwrapped_phase: Vec<f64>,
pub sample_rate_hz: f64,
}
pub fn instantaneous_frequency(
analytic: &[(f64, f64)],
sample_rate_hz: f64,
) -> Result<InstantaneousFrequency, SignalError> {
if analytic.len() < 2 {
return Err(SignalError::InvalidLength {
len: analytic.len(),
reason: "instantaneous frequency requires at least two analytic samples",
});
}
if !sample_rate_hz.is_finite() || sample_rate_hz <= 0.0 {
return Err(SignalError::InvalidPolicy {
policy: "instantaneous-frequency grid",
reason: "sample rate must be finite and positive",
});
}
let phases = analytic
.iter()
.copied()
.enumerate()
.map(|(index, (real, imag))| {
if !real.is_finite() || !imag.is_finite() {
Err(SignalError::NonFinite {
index,
component: "analytic sample",
})
} else {
Ok(imag.atan2(real))
}
})
.collect::<Result<Vec<_>, _>>()?;
let unwrapped_phase = unwrap_phase(&phases, PI)?;
let frequency_hz = unwrapped_phase
.windows(2)
.map(|pair| (pair[1] - pair[0]) * sample_rate_hz / TAU)
.collect::<Vec<_>>();
let time_seconds = (0..frequency_hz.len())
.map(|index| (index as f64 + 0.5) / sample_rate_hz)
.collect();
Ok(InstantaneousFrequency {
time_seconds,
frequency_hz,
unwrapped_phase,
sample_rate_hz,
})
}
pub fn analytic_envelope(analytic: &[(f64, f64)]) -> Result<Vec<f64>, SignalError> {
analytic
.iter()
.copied()
.enumerate()
.map(|(index, (real, imag))| {
let magnitude = real.hypot(imag);
if magnitude.is_finite() {
Ok(magnitude)
} else {
Err(SignalError::NonFinite {
index,
component: "analytic envelope",
})
}
})
.collect()
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct EnvelopeFollowerPlan {
pub sample_rate_hz: f64,
pub attack_seconds: f64,
pub release_seconds: f64,
pub initial_value: f64,
}
pub fn envelope_follow(
samples: &[f64],
plan: &EnvelopeFollowerPlan,
) -> Result<Vec<f64>, SignalError> {
if !plan.sample_rate_hz.is_finite()
|| plan.sample_rate_hz <= 0.0
|| !plan.attack_seconds.is_finite()
|| plan.attack_seconds < 0.0
|| !plan.release_seconds.is_finite()
|| plan.release_seconds < 0.0
|| !plan.initial_value.is_finite()
|| plan.initial_value < 0.0
{
return Err(SignalError::InvalidPolicy {
policy: "envelope follower",
reason: "positive sample rate and finite nonnegative time constants/state are required",
});
}
let attack = smoothing_coefficient(plan.attack_seconds, plan.sample_rate_hz);
let release = smoothing_coefficient(plan.release_seconds, plan.sample_rate_hz);
let mut state = plan.initial_value;
let mut output = Vec::with_capacity(samples.len());
for (index, sample) in samples.iter().copied().enumerate() {
if !sample.is_finite() {
return Err(SignalError::NonFinite {
index,
component: "value",
});
}
let target = sample.abs();
let coefficient = if target > state { attack } else { release };
state = coefficient * state + (1.0 - coefficient) * target;
output.push(state);
}
Ok(output)
}
fn smoothing_coefficient(seconds: f64, sample_rate_hz: f64) -> f64 {
if seconds == 0.0 {
0.0
} else {
(-1.0 / (seconds * sample_rate_hz)).exp()
}
}
fn analytic_work(len: usize) -> Result<u64, SignalError> {
let len = u64::try_from(len).map_err(|_| SignalError::InvalidPolicy {
policy: "analytic signal work",
reason: "length does not fit the work counter",
})?;
let stages = u64::from(usize::BITS - (len as usize).leading_zeros()).max(1);
len.checked_mul(stages)
.and_then(|value| value.checked_mul(4))
.ok_or(SignalError::InvalidPolicy {
policy: "analytic signal work",
reason: "work-unit arithmetic overflowed",
})
}