{- | 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. -} module Synthesizer.Dimensional.Overview where