----------------------------------------------------------------------------- -- | -- Module : DSP.Source.Oscillator -- Copyright : (c) Matthew Donadio 1998,2003 -- License : GPL -- -- Maintainer : m.p.donadio@ieee.org -- Stability : experimental -- Portability : portable -- -- NCO and NCOM functions -- ----------------------------------------------------------------------------- module DSP.Source.Oscillator (nco, ncom, quadrature_nco, complex_ncom, quadrature_ncom, agc) where import Data.Complex -- | 'nco' creates a sine wave with normalized frequency wn (numerically -- controlled oscillator, or NCO) using the recurrence relation y[n] = -- 2cos(wn)*y[n-1] - y[n-2]. Eventually, cumulative errors will creep -- into the data. This is unavoidable since performing AGC on this type -- of real data is hard. The good news is that the error is small with -- floating point data. nco :: RealFloat a => a -- ^ w -> a -- ^ phi -> [a] -- ^ y nco wn phi = y where a0 = 2 * cos wn y1 = -(sin (wn + phi)) : y y2 = -(sin (2 * wn + phi)) : y1 y = zipWith (-) (map (a0 *) y1) y2 -- | 'ncom' mixes (multiplies) x by a real sine wave with normalized -- frequency wn. This is usually called an NCOM: Numerically Controlled -- Oscillator and Modulator. ncom :: RealFloat a => a -- ^ w -> a -- ^ phi -> [a] -- ^ x -> [a] -- ^ y ncom wn phi x = zipWith (*) x (nco wn phi) -- agc is used in quadrature_nco (below) to scale a complex phasor to -- have length as close to 1 as possible, ie perform some automatic gain -- control. Since we aren't computing sin and cos for each sample, not -- using AGC would results in cumulative errors (small one with floating -- point data). The Complex class includes the signum function which -- will do what we want, but we will use the approximation 1/sqrt(x) ~= -- (3-x)/2 for x ~= 1 to eliminate doing a sqrt for every point. agc :: RealFloat a => Complex a -> Complex a agc (x:+y) = x * r :+ y * r where r = (3 - x * x - y * y) / 2 -- | 'quadrature_nco' returns an infinite list representing a complex phasor -- with a phase step of wn radians, ie a quadrature nco with normalized -- frequency wn radians\/sample. Since Haskell uses lazy evaluation, -- rotate will only be computed once, so this NCO uses only one sin and -- one cos for the entire list, at the expense of 4 mults, 1 add, and 1 -- subtract per point. quadrature_nco :: RealFloat a => a -- ^ w -> a -- ^ phi -> [ Complex a ] -- ^ y quadrature_nco wn phi = (cis phi) : map ((*) (cis wn)) (quadrature_nco wn phi) -- | 'complex_ncom' mixes the complex input x with a quardatue nco with -- normalized frequency wn radians\/sample using complex multiplies -- (perform a complex spectral shift) complex_ncom :: RealFloat a => a -- ^ w -> a -- ^ phi -> [ Complex a ] -- ^ x -> [ Complex a ] -- ^ y complex_ncom _ _ [] = [] complex_ncom wn phi x = zipWith (*) (quadrature_nco wn phi) x -- quadrature_mults returns the sum of the real parts and the imagimary -- parts of two complex numbers (dot product) quadrature_mult :: RealFloat a => Complex a -> Complex a -> a quadrature_mult (x1:+y1) (x2:+y2) = x1 * x2 + y1 * y2 -- | 'quadrature_ncom' mixes the complex input x with a quadrature nco with -- normalized frequency wn radians\/sample in quadrature (I\/Q modulation) quadrature_ncom :: RealFloat a => a -- ^ w -> a -- ^ phi -> [Complex a] -- ^ x -> [a] -- ^ y quadrature_ncom wn phi x = zipWith quadrature_mult x (quadrature_nco wn phi)