synthesizer- Audio signal processing coded in HaskellSource codeContentsIndex

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:

  • When combining some signals, it is not clear how to cope with different sample rates. Say you want to mix signals a and b. Shall mix a b have the sample rate of a or that of b or a new one? How shall the signals convert to a new rate? Since an automatically chosen method can always be inappropriate (either too slow or too low quality), the caller have to explicitly give tell it to mix. This is not only inconvenient for the caller, but also requires a lot of boilerplate code in functions like mix.
  • An alternative solution to the problem above is, to check before mixing whether sample rates are equal, and abort with an error if they differ. This way no decisions on the sample rate and subsequent conversions are necessary. This still needs boilerplate in signal processors. It also does not tell the user by types, whether a processor can handle differing sample rates. Generally, dynamic checks are both inefficient and an inferior way to indicate programming errors, since they are only catched at run-time, if at all.
  • Both solutions suffer from the inconvenience to specify the sample rate in all leaves, e.g. mix (oscillator 44100 10) (oscillator 44100 11).

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.

  • You need to be to enter a sample rate context with a signal read from disk, with a sample rate that is not known at compile time. You might also intensionally compute a control signal at a low sample rate and convert it to a sample rate context for filtering. Generally the scheme of functions that allow different sample rates is: Use the sample rate of the output signal as context. Take all signals with independent sample rate as inputs outside the context. The according function is Synthesizer.Dimensional.Rate.Filter.frequencyModulationDecoupled.
  • When you want to play a sound or write it to disk, you must choose a sample rate and fix the computation to that rate. This conversion however means to run the whole computation within one sample rate context, since everything in that context depends on the sample rate. The according function is Synthesizer.Dimensional.RateWrapper.runProcess.

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