33
44use crate :: dsp:: AudioBlock ;
55use crate :: util:: audio:: { self , Channel , DEFAULT_SAMPLE_RATE } ;
6- use realfft:: { RealFftPlanner , RealToComplex } ;
6+ use realfft:: { ComplexToReal , RealFftPlanner , RealToComplex } ;
77use rustfft:: num_complex:: Complex ;
88use serde:: { Deserialize , Serialize } ;
99use 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 ) ]
74104struct 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
87111impl 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
270277fn 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+
564574pub 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
576584impl 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 {
743739fn 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