Skip to content

Commit f67c4e7

Browse files
refactor(oscilloscope): periodfft struct simpllification
Signed-off-by: Maika Namuo <httpworldview@gmail.com>
1 parent fb15d46 commit f67c4e7

1 file changed

Lines changed: 87 additions & 98 deletions

File tree

src/visuals/oscilloscope/processor.rs

Lines changed: 87 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use crate::dsp::AudioBlock;
55
use crate::util::audio::{self, Channel, DEFAULT_SAMPLE_RATE};
6-
use realfft::{RealFftPlanner, RealToComplex};
6+
use realfft::{ComplexToReal, RealFftPlanner, RealToComplex};
77
use rustfft::num_complex::Complex;
88
use serde::{Deserialize, Serialize};
99
use std::collections::VecDeque;
@@ -70,39 +70,49 @@ struct PeriodEstimate {
7070
confidence: f32,
7171
}
7272

73+
struct PeriodFft {
74+
forward: Arc<dyn RealToComplex<f32>>,
75+
inverse: Arc<dyn ComplexToReal<f32>>,
76+
input: Vec<f32>,
77+
spectrum: Vec<Complex<f32>>,
78+
output: Vec<f32>,
79+
scratch: Vec<Complex<f32>>,
80+
}
81+
82+
impl PeriodFft {
83+
fn new(size: usize) -> Self {
84+
let mut planner = RealFftPlanner::new();
85+
let forward = planner.plan_fft_forward(size);
86+
let inverse = planner.plan_fft_inverse(size);
87+
let mut scratch = forward.make_scratch_vec();
88+
let inv_scratch = inverse.make_scratch_vec();
89+
if inv_scratch.len() > scratch.len() {
90+
scratch = inv_scratch;
91+
}
92+
Self {
93+
input: forward.make_input_vec(),
94+
spectrum: forward.make_output_vec(),
95+
output: inverse.make_output_vec(),
96+
scratch,
97+
forward,
98+
inverse,
99+
}
100+
}
101+
}
102+
73103
#[derive(Default)]
74104
struct PeriodEstimator {
75105
periodicity: Vec<f32>,
76106
energy_prefix: Vec<f32>,
77107
last_peak: f32,
78-
fft_size: usize,
79-
fft_forward: Option<Arc<dyn RealToComplex<f32>>>,
80-
fft_inverse: Option<Arc<dyn realfft::ComplexToReal<f32>>>,
81-
fft_input: Vec<f32>,
82-
fft_spectrum: Vec<Complex<f32>>,
83-
fft_output: Vec<f32>,
84-
fft_scratch: Vec<Complex<f32>>,
108+
fft: Option<PeriodFft>,
85109
}
86110

87111
impl PeriodEstimator {
88112
fn rebuild_fft(&mut self, size: usize) {
89-
if self.fft_size == size && self.fft_forward.is_some() {
90-
return;
91-
}
92-
self.fft_size = size;
93-
let mut planner = RealFftPlanner::new();
94-
let forward = planner.plan_fft_forward(size);
95-
let inverse = planner.plan_fft_inverse(size);
96-
self.fft_input = forward.make_input_vec();
97-
self.fft_spectrum = forward.make_output_vec();
98-
self.fft_output = inverse.make_output_vec();
99-
self.fft_scratch = forward.make_scratch_vec();
100-
let inv_scratch = inverse.make_scratch_vec();
101-
if inv_scratch.len() > self.fft_scratch.len() {
102-
self.fft_scratch = inv_scratch;
113+
if self.fft.as_ref().is_none_or(|fft| fft.input.len() != size) {
114+
self.fft = Some(PeriodFft::new(size));
103115
}
104-
self.fft_forward = Some(forward);
105-
self.fft_inverse = Some(inverse);
106116
}
107117

108118
fn estimate_period(&mut self, samples: &[f32], rate: f32) -> Option<PeriodEstimate> {
@@ -123,7 +133,7 @@ impl PeriodEstimator {
123133

124134
let nsdf = &self.periodicity[..=max_period];
125135
let zero_crossing = (1..=max_period).find(|&tau| nsdf[tau] <= 0.0)?;
126-
let first_tau = min_period.max(zero_crossing).max(1);
136+
let first_tau = min_period.max(zero_crossing);
127137
if first_tau >= max_period { return None; }
128138

129139
let is_candidate = |tau: &usize| {
@@ -148,47 +158,44 @@ impl PeriodEstimator {
148158
fn compute_periodicity(&mut self, samples: &[f32], mean: f32, max_lag: usize) -> Option<()> {
149159
let fft_size = (samples.len() * 2).next_power_of_two();
150160
self.rebuild_fft(fft_size);
161+
let Self { periodicity, energy_prefix, fft, .. } = self;
162+
let fft = fft.as_mut()?;
151163

152-
let (Some(forward), Some(inverse)) = (self.fft_forward.as_ref(), self.fft_inverse.as_ref())
153-
else {
154-
return None;
155-
};
156-
157-
self.energy_prefix.resize(samples.len() + 1, 0.0);
158-
self.energy_prefix[0] = 0.0;
159-
for (i, (dst, &sample)) in self.fft_input[..samples.len()]
164+
energy_prefix.resize(samples.len() + 1, 0.0);
165+
energy_prefix[0] = 0.0;
166+
for (i, (dst, &sample)) in fft.input[..samples.len()]
160167
.iter_mut()
161168
.zip(samples)
162169
.enumerate()
163170
{
164171
let centered = sample - mean;
165172
*dst = centered;
166-
self.energy_prefix[i + 1] = centered.mul_add(centered, self.energy_prefix[i]);
173+
energy_prefix[i + 1] = centered.mul_add(centered, energy_prefix[i]);
167174
}
168-
self.fft_input[samples.len()..].fill(0.0);
175+
fft.input[samples.len()..].fill(0.0);
169176

170-
forward
171-
.process_with_scratch(&mut self.fft_input, &mut self.fft_spectrum, &mut self.fft_scratch)
177+
fft.forward
178+
.process_with_scratch(&mut fft.input, &mut fft.spectrum, &mut fft.scratch)
172179
.ok()?;
173180

174-
for bin in &mut self.fft_spectrum {
181+
for bin in &mut fft.spectrum {
175182
*bin = Complex::new(bin.norm_sqr(), 0.0);
176183
}
177184

178-
inverse
179-
.process_with_scratch(&mut self.fft_spectrum, &mut self.fft_output, &mut self.fft_scratch)
185+
fft.inverse
186+
.process_with_scratch(&mut fft.spectrum, &mut fft.output, &mut fft.scratch)
180187
.ok()?;
181188

182189
let norm = 1.0 / fft_size as f32;
183-
self.periodicity.resize(max_lag + 1, 0.0);
184-
let total_energy = self.energy_prefix[samples.len()];
190+
periodicity.resize(max_lag + 1, 0.0);
191+
let total_energy = energy_prefix[samples.len()];
185192
if total_energy <= f32::EPSILON { return None; }
186193
for tau in 0..=max_lag {
187-
let left_energy = self.energy_prefix[samples.len() - tau];
188-
let right_energy = total_energy - self.energy_prefix[tau];
194+
let left_energy = energy_prefix[samples.len() - tau];
195+
let right_energy = total_energy - energy_prefix[tau];
189196
let denom = left_energy + right_energy;
190-
self.periodicity[tau] = if denom > f32::EPSILON {
191-
2.0 * self.fft_output[tau] * norm / denom
197+
periodicity[tau] = if denom > f32::EPSILON {
198+
2.0 * fft.output[tau] * norm / denom
192199
} else {
193200
0.0
194201
};
@@ -269,7 +276,7 @@ fn sample_linear_zero(data: &[f32], pos: f32) -> f32 {
269276

270277
fn retune_reference(reference: &[f32], old_period: f32, new_period: f32, len: usize) -> Vec<f32> {
271278
let ratio = new_period / old_period;
272-
if len == 0 || !ratio.is_finite() || ratio <= f32::EPSILON {
279+
if !ratio.is_finite() || ratio <= f32::EPSILON {
273280
return vec![0.0; len];
274281
}
275282

@@ -309,9 +316,6 @@ impl StableTrigger {
309316
self.missed_periods = 0;
310317
self.reference.clear();
311318
self.reference_period = None;
312-
self.kernel.clear();
313-
self.work.clear();
314-
self.candidate.clear();
315319
self.mean = 0.0;
316320
}
317321

@@ -561,16 +565,20 @@ pub struct OscilloscopeSnapshot {
561565
pub samples_per_channel: usize,
562566
}
563567

568+
#[derive(Default)]
569+
struct TraceState {
570+
buffer: Vec<f32>,
571+
trigger: StableTrigger,
572+
}
573+
564574
pub struct OscilloscopeProcessor {
565575
config: OscilloscopeConfig,
566576
snapshot: OscilloscopeSnapshot,
567577
epoch: u64,
568578
history: VecDeque<f32>,
569579
history_channels: Option<usize>,
570-
trace_buffers: [Vec<f32>; TRACE_COUNT],
571-
trigger_buffer: Vec<f32>,
572-
stable_triggers: [StableTrigger; TRACE_COUNT],
573-
source_trigger: StableTrigger,
580+
traces: [TraceState; TRACE_COUNT],
581+
source: TraceState,
574582
}
575583

576584
impl OscilloscopeProcessor {
@@ -581,26 +589,21 @@ impl OscilloscopeProcessor {
581589
epoch: 0,
582590
history: VecDeque::new(),
583591
history_channels: None,
584-
trace_buffers: std::array::from_fn(|_| Vec::new()),
585-
trigger_buffer: Vec::new(),
586-
stable_triggers: std::array::from_fn(|_| StableTrigger::default()),
587-
source_trigger: StableTrigger::default(),
592+
traces: std::array::from_fn(|_| TraceState::default()),
593+
source: TraceState::default(),
588594
}
589595
}
590596

591597
pub fn config(&self) -> OscilloscopeConfig {
592598
self.config
593599
}
594600

595-
fn trace_channels(&self) -> [Channel; TRACE_COUNT] {
596-
[self.config.channel_1, self.config.channel_2]
597-
}
598-
599601
#[cfg(test)]
600602
fn last_cycle_rate(&self) -> Option<f32> {
601-
self.source_trigger
603+
self.source
604+
.trigger
602605
.period
603-
.or_else(|| self.stable_triggers.iter().find_map(|trigger| trigger.period))
606+
.or_else(|| self.traces.iter().find_map(|trace| trace.trigger.period))
604607
.map(|period| self.config.sample_rate / period)
605608
}
606609

@@ -634,7 +637,7 @@ impl OscilloscopeProcessor {
634637
stable_history_frames(max_period, num_cycles, self.config.sample_rate)
635638
}
636639
};
637-
let trace_channels = self.trace_channels();
640+
let trace_channels = [self.config.channel_1, self.config.channel_2];
638641
let trigger_source = self.config.trigger_source;
639642
let samples = &block.samples[..block.frame_count() * channel_count];
640643
audio::extend_interleaved_history(
@@ -648,29 +651,27 @@ impl OscilloscopeProcessor {
648651
let mode = self.config.trigger_mode;
649652
let sample_rate = self.config.sample_rate;
650653
let capture = |trace: &[f32], trigger: &mut StableTrigger| match mode {
651-
TriggerMode::ZeroCrossing => {
652-
zero_crossing_capture(trace, available, base_frames, max_period)
653-
}
654-
TriggerMode::Stable { num_cycles } => (available >= base_frames).then(|| {
654+
TriggerMode::ZeroCrossing => zero_crossing_capture(trace, base_frames, max_period),
655+
TriggerMode::Stable { num_cycles } => (trace.len() >= base_frames).then(|| {
655656
trigger.capture(trace, sample_rate, probe_frames, base_frames, num_cycles)
656657
}),
657658
};
658659
let linked_capture = if audio::project_interleaved_channel_into(
659-
&mut self.trigger_buffer,
660+
&mut self.source.buffer,
660661
data,
661662
channel_count,
662663
available,
663664
trigger_source,
664665
) {
665-
capture(&self.trigger_buffer, &mut self.source_trigger)
666+
capture(&self.source.buffer, &mut self.source.trigger)
666667
} else {
667668
None
668669
};
669670
let mut captures = [None; TRACE_COUNT];
670671

671-
for (slot, channel) in trace_channels.into_iter().enumerate() {
672+
for (slot, (trace, channel)) in self.traces.iter_mut().zip(trace_channels).enumerate() {
672673
if !audio::project_interleaved_channel_into(
673-
&mut self.trace_buffers[slot],
674+
&mut trace.buffer,
674675
data,
675676
channel_count,
676677
available,
@@ -679,12 +680,7 @@ impl OscilloscopeProcessor {
679680
continue;
680681
}
681682

682-
captures[slot] = linked_capture.or_else(|| {
683-
capture(
684-
&self.trace_buffers[slot],
685-
&mut self.stable_triggers[slot],
686-
)
687-
});
683+
captures[slot] = linked_capture.or_else(|| capture(&trace.buffer, &mut trace.trigger));
688684
}
689685

690686
if captures.iter().all(Option::is_none) { return None; }
@@ -696,12 +692,12 @@ impl OscilloscopeProcessor {
696692
fn clear_history(&mut self) {
697693
self.epoch = self.epoch.wrapping_add(1);
698694
self.history.clear();
699-
self.trace_buffers.iter_mut().for_each(Vec::clear);
700-
self.trigger_buffer.clear();
701-
self.stable_triggers
702-
.iter_mut()
703-
.for_each(StableTrigger::unlock);
704-
self.source_trigger.unlock();
695+
self.traces.iter_mut().for_each(|trace| {
696+
trace.buffer.clear();
697+
trace.trigger.unlock();
698+
});
699+
self.source.buffer.clear();
700+
self.source.trigger.unlock();
705701
}
706702

707703
fn write_snapshot(&mut self, captures: &[Option<Capture>; TRACE_COUNT]) {
@@ -721,7 +717,7 @@ impl OscilloscopeProcessor {
721717
let Some(capture) = capture else { continue };
722718
if downsample_trace(
723719
&mut self.snapshot.samples,
724-
&self.trace_buffers[slot],
720+
&self.traces[slot].buffer,
725721
capture,
726722
target,
727723
) {
@@ -743,21 +739,16 @@ impl OscilloscopeProcessor {
743739
fn stable_history_frames(max_period: usize, cycles: usize, sample_rate: f32) -> usize {
744740
let max_period_f = max_period as f32;
745741
let max_kernel = trigger_kernel_len(max_period_f, sample_rate);
746-
let max_tail = (max_period * cycles.max(1) + 1).max(max_kernel - max_kernel / 2);
742+
let max_tail = (max_period * cycles.max(1) + 1).max(max_kernel.div_ceil(2));
747743
let max_search = (max_period_f * StableTuning::SEARCH_PERIODS).ceil() as usize;
748744
max_kernel / 2 + max_tail + max_search + 2
749745
}
750746

751-
fn zero_crossing_capture(
752-
samples: &[f32],
753-
available: usize,
754-
frames: usize,
755-
search_range: usize,
756-
) -> Option<Capture> {
757-
let frames = frames.min(available);
747+
fn zero_crossing_capture(samples: &[f32], frames: usize, search_range: usize) -> Option<Capture> {
748+
let frames = frames.min(samples.len());
758749
if frames == 0 { return None; }
759750

760-
let end = available.saturating_sub(1);
751+
let end = samples.len().saturating_sub(1);
761752
let right_lo = end.saturating_sub(search_range);
762753
let right = find_rising_zero_crossing(samples, (right_lo..=end).rev()).unwrap_or(end);
763754

@@ -785,9 +776,7 @@ fn downsample_trace(output: &mut Vec<f32>, data: &[f32], capture: Capture, targe
785776
if !span.is_finite() || span <= 0.0 { return false; }
786777

787778
let step = span / (target - 1) as f32;
788-
for i in 0..target {
789-
output.push(sample_linear_zero(data, start_offset + i as f32 * step));
790-
}
779+
output.extend((0..target).map(|i| sample_linear_zero(data, start_offset + i as f32 * step)));
791780
true
792781
}
793782

0 commit comments

Comments
 (0)