We use a hierarchy of signal wrappers in order to capture all features of a signal.
At the bottom there is the signal storage as described in Synthesizer.Storage. With the element type of the storage you decide whether you want mono or stereo signal (Synthesizer.Frame.Stereo), and you decide on the precision, fixed point vs. floating point and so on. However, due to Haskell's flexible type system you don't need to decide finally on a type for your signal processing routines. E.g. mono and stereo signals can be handled together using the Algebra.Module class from the numeric-prelude package. (An algebraic module is a vector space without requiring availability of a division of scalars).
You can use the storage types directly using the functions from Synthesizer.Plain.Signal and Synthesizer.Generic.Signal and its cousins. This is in a way simple, since you do not have bother with units and complicated types, but you miss type safety and re-usability. E.g. you have to give frequencies in ratios of the sampling rate. If you later decide to change the sampling rate, you must rewrite all time and frequency values. If you anticipate changes in sampling rate, you may write those values as ratios of a global sampling rate right from the start. But you might want different sample rates for some parts of the computation, or you may want sample rates that have no time dimension, but say, length dimension. The advanced system described below handles all these cases for you.
Ok, we said that at the bottom, there is the signal storage. The next level is the decision whether the raw data is interpreted as straight or as cyclic signal. Most of the signals you are using, will be Synthesizer.Dimensional.Straight.Signal. Currently, Synthesizer.Dimensional.Cyclic.Signal is only needed for Fourier transform and as input to oscillators. To get a straight signal out of storablevector data, you will write
import qualified Synthesizer.Storable.Signal as Store import qualified Synthesizer.Dimensional.Straight.Signal as Straight type MySignal y = Straight.T Store.T y
Note that Straight.T has the type constructor Store.T as first argument, not the entire storage type Store.T. This way compositions of such wrappers are automatically Functors. However, I'm not completely certain, that this is good, since fmap allows to do unintended things (e.g. switch from a numeric to a non-numeric element type).
The next level copes with amplitudes and their units. An amplitude and its unit are provided per signal, not per sample. We think that it is the most natural way, and it is also an efficient one. Since the signal might be a stereo signal, the numeric type of the amplitude can differ from the storage element type. Usually, the first and the latter one are related by an Algebra.Module constraint. You get a signal with amplitude by
import qualified Synthesizer.Dimensional.Amplitude.Signal as Amp type MySignal v y yv = Amp.T v y (Straight.T Store.T) yv
where v is the dimension of the amplitude of type y. The storage element type, a vector with respect to y, is of type yv.
In some cases, an amplitude with a physical dimension just makes no sense. Imagine a control signal consisting of Bool elements like a gate signal, or a signal containing elements of an enumeration for switching between signals depending on the time.
For some control signals the amplitude unit is one. We call these signals flat. In this case you can choose whether you use an explicit amplitude with Scalar dimension or you use no amplitude wrapper at all. Most signal processors handle both kinds of flat signals by the corresponding type class in Synthesizer.Dimensional.Abstraction.Flat.
There is a special signal type for Dirac impulses, that does not fit to that scheme, that is, it cannot be equipped with an amplitude. See Synthesizer.Dimensional.Rate.Dirac.
Last but not least we want to look on how to handle sample rates. Our goal is to write signal processes that do not depend on the sample rate. E.g. we want to have an exponential decay with a half-life of one second. A second means 44100 samples at 44100 Hz sample rate, or 22050 samples at 22050 Hz sample rate. We want to abstract from the particular number of samples in order to be able generate a signal at any sample rate (i.e. quality) we like. The ideal representation of a signal is be a real function, and we try to come close to it. (Not quite, because a Dirac impulse is not a real function, but we need it as identity element of the convolution.)
To this end we can equip a discrete signal with a sample rate, see Synthesizer.Dimensional.RateWrapper. This alone however, leads to several problems:
Naturally, when you want to get a signal with rate 44100 Hz sample rate, you perform all signal processes at this rate. Even if want to use oversampling, then you will perform all signal processes at the higher rate and downsample only once at the end. Thus we introduce a way to run a set of signal processes in the context of a sample rate. It is still sensible and possible to escape from this context or enter it again.
The sample rate context is provided by Synthesizer.Dimensional.Process. It is a Reader monad, but we only need applicative functor methods for signal processing. This context is equipped with the type parameter s, just as we know it from the Control.Monad.ST.ST monad. It also serves the same purpose: We tag both signals and the sample rate context with the type parameter s. The forall s constraint for runProcess ensures, that a signal with such a tag remains in the context. You can only escape the sample rate context by rendering the signal and attach the sample rate to the rendered signal.
The sample rate tag type is provided by Synthesizer.Dimensional.RatePhantom. Haskell's type system does not allow to restrict the types that it can wrap. So in principle you can abuse it to wrap many things. However we do not provide such functions and this way the wrappable types ar restricted anyway.
|Produced by Haddock version 2.4.2|