///|
pub fn moving_average(
  signal : ArrayView[Double],
  width : Int,
) -> Array[Double] raise SpectrumError {
  if signal.length() == 0 {
    raise EmptySignal
  }
  if width <= 0 {
    raise InvalidArgument(message="width must be positive")
  }
  let out : Array[Double] = []
  for i in 0.. Array[Double] raise SpectrumError {
  convolve(signal, taps)
}

///|
pub fn lowpass_taps(
  length~ : Int,
  cutoff_hz~ : Double,
  sample_rate~ : Double,
) -> Array[Double] raise SpectrumError {
  validate_filter_args(length, cutoff_hz, sample_rate)
  let fc = cutoff_hz / sample_rate
  let mid = (length - 1) / 2
  let win = window(Hamming, length)
  let taps : Array[Double] = []
  for n in 0.. Array[Double] raise SpectrumError {
  let low = lowpass_taps(length~, cutoff_hz~, sample_rate~)
  let mid = (length - 1) / 2
  let out : Array[Double] = []
  for i in 0.. Unit raise SpectrumError {
  if length <= 0 {
    raise EmptySignal
  }
  if length % 2 == 0 {
    raise InvalidArgument(message="tap length must be odd")
  }
  if cutoff_hz <= 0.0 || sample_rate <= 0.0 || cutoff_hz >= sample_rate / 2.0 {
    raise InvalidArgument(message="cutoff must be between 0 and Nyquist")
  }
}

///|
fn normalize_taps(taps : Array[Double]) -> Array[Double] raise SpectrumError {
  let sum = taps.fold(init=0.0, (acc, item) => acc + item)
  if sum == 0.0 {
    raise InvalidArgument(message="tap sum is zero")
  }
  taps.map(item => item / sum)
}

///|
pub fn bandpass_fir_taps(
  length~ : Int,
  low_cutoff_hz~ : Double,
  high_cutoff_hz~ : Double,
  sample_rate~ : Double,
) -> Array[Double] raise SpectrumError {
  if low_cutoff_hz >= high_cutoff_hz {
    raise InvalidArgument(message="low_cutoff must be less than high_cutoff")
  }
  let low1 = lowpass_taps(length~, cutoff_hz=low_cutoff_hz, sample_rate~)
  let low2 = lowpass_taps(length~, cutoff_hz=high_cutoff_hz, sample_rate~)
  let out : Array[Double] = []
  for i in 0.. Array[Double] raise SpectrumError {
  if low_cutoff_hz >= high_cutoff_hz {
    raise InvalidArgument(message="low_cutoff must be less than high_cutoff")
  }
  let low1 = lowpass_taps(length~, cutoff_hz=low_cutoff_hz, sample_rate~)
  let low2 = lowpass_taps(length~, cutoff_hz=high_cutoff_hz, sample_rate~)
  let mid = (length - 1) / 2
  let out : Array[Double] = []
  for i in 0..