-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #108 from card-io-ecg/filt
Design filters in compile-time
- Loading branch information
Showing
6 changed files
with
175 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
use proc_macro2::{Span, TokenStream}; | ||
use quote::quote; | ||
use sci_rs::signal::filter::design::{ | ||
iirfilter_dyn, BaFormatFilter, DigitalFilter, FilterBandType, FilterType, | ||
}; | ||
use std::collections::HashMap; | ||
use syn::{ | ||
parse::{Parse, ParseBuffer}, | ||
Lit, LitStr, Token, | ||
}; | ||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
enum FilterKind { | ||
HighPassIir, | ||
} | ||
|
||
impl Parse for FilterKind { | ||
fn parse(input: &ParseBuffer) -> syn::Result<Self> { | ||
let kind = input.parse::<LitStr>()?; | ||
|
||
let filter_kind = kind.value().to_ascii_lowercase(); | ||
let filter_kind = match filter_kind.as_str() { | ||
"highpassiir" => FilterKind::HighPassIir, | ||
|
||
_ => { | ||
return Err(syn::Error::new( | ||
kind.span(), | ||
format!("unknown filter type: {filter_kind}"), | ||
)); | ||
} | ||
}; | ||
|
||
Ok(filter_kind) | ||
} | ||
} | ||
|
||
pub struct FilterSpec { | ||
filter_kind: FilterKind, | ||
span: Span, | ||
options: HashMap<String, f32>, | ||
} | ||
|
||
impl Parse for FilterSpec { | ||
fn parse(input: &ParseBuffer) -> syn::Result<Self> { | ||
let filter_kind = input.parse::<FilterKind>()?; | ||
let mut options = HashMap::new(); | ||
|
||
while !input.is_empty() { | ||
input.parse::<Token![,]>()?; | ||
let key = input.parse::<LitStr>()?; | ||
|
||
let keystr = validate_filter_option(filter_kind, &key)?; | ||
|
||
input.parse::<Token![,]>()?; | ||
let value = input.parse::<Lit>()?; | ||
|
||
let value = match value { | ||
Lit::Int(lit) => lit.base10_parse().unwrap(), | ||
Lit::Float(lit) => lit.base10_parse().unwrap(), | ||
_ => return Err(syn::Error::new_spanned(value, "expected a number")), | ||
}; | ||
|
||
if options.insert(keystr, value).is_some() { | ||
return Err(syn::Error::new( | ||
key.span(), | ||
format!("duplicate option: {}", key.value()), | ||
)); | ||
} | ||
} | ||
|
||
Ok(Self { | ||
filter_kind, | ||
span: input.span(), | ||
options, | ||
}) | ||
} | ||
} | ||
|
||
fn validate_filter_option(filter_kind: FilterKind, key: &LitStr) -> syn::Result<String> { | ||
let expected = match filter_kind { | ||
FilterKind::HighPassIir => &[ | ||
"filterorder", | ||
"passbandfrequency", | ||
"halfpowerfrequency", | ||
"passbandripple", | ||
"samplerate", | ||
], | ||
}; | ||
|
||
let value = key.value().to_ascii_lowercase(); | ||
if !expected.contains(&value.as_str()) { | ||
return Err(syn::Error::new( | ||
key.span(), | ||
format!("'{value}' is not expected for '{filter_kind:?}'"), | ||
)); | ||
} | ||
|
||
Ok(value) | ||
} | ||
|
||
pub fn run(args: FilterSpec) -> TokenStream { | ||
let module = quote! {crate::filter}; | ||
|
||
match args.filter_kind { | ||
FilterKind::HighPassIir => { | ||
let Some(&order) = args.options.get("filterorder") else { | ||
return syn::Error::new(args.span, "missing required option 'FilterOrder'") | ||
.to_compile_error(); | ||
}; | ||
|
||
let filter = iirfilter_dyn( | ||
order as usize, | ||
vec![args.options.get("halfpowerfrequency").copied().unwrap()], | ||
None, | ||
None, | ||
Some(FilterBandType::Highpass), | ||
Some(FilterType::Butterworth), | ||
Some(false), | ||
None, | ||
args.options.get("samplerate").copied(), | ||
); | ||
|
||
let DigitalFilter::Ba(BaFormatFilter { mut b, mut a }) = filter else { | ||
unreachable!() | ||
}; | ||
|
||
let zeros_a = a.iter().rev().take_while(|&&x| x == 0.0).count(); | ||
let zeros_b = b.iter().rev().take_while(|&&x| x == 0.0).count(); | ||
|
||
let remove = zeros_a.min(zeros_b); | ||
|
||
a.truncate(a.len() - remove); | ||
b.truncate(b.len() - remove); | ||
|
||
assert!(a.swap_remove(0) == 1.0); | ||
|
||
a.reverse(); | ||
|
||
quote! { | ||
#module::iir::Iir::new(&[#(#b,)*], &[#(#a,)*]) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters