Arrays (Arr)
April 22, 2018 ยท View on GitHub
We can create arrays of values. The data type of array is parametrized with index and value. This typing scheme prevents us from reading or writing the wrong values to the arrays although it doesn't prevents us from out of bounds errors.
data Arr ix a
Notice that the data types for indexes and values can be tuples. It let's us easily create multidimensional array. If we want say 2D array we can use pairs as indexes.
Creation of arrays
Arrays can be global and local. Local arrays are accessible only within the body of single Csound instrument where they are created. The scope translated to Haskell is somewhat obscure. The global arrays are accessible at any point of the code.
We can create arrays with functions:
newLocalArr :: Tuple a => [D] -> [a] -> SE (Arr ix a)
newGlobalArr :: Tuple a => [D] -> [a] -> SE (Arr ix a)
They accept the list of dimensions and list of initial values. Also arrays can contain audio or control rate signals. The aforementioned functions create audio-rate signals. If we want to create control rate signals we should use the functions:
newLocalCtrlArr :: Tuple a => [D] -> [a] -> SE (Arr ix a)
newGlobalCtrlArr :: Tuple a => [D] -> [a] -> SE (Arr ix a)
Read and write operations
To read and write the values from array we have two functions:
writeArr :: (Tuple ix, Tuple a) => Arr ix a -> ix -> a -> SE ()
readArr :: (Tuple a, Tuple ix) => Arr ix a -> ix -> SE a
We can modify the value in the arry with function:
modifyArr :: (Tuple a, Tuple ix) => Arr ix a -> ix -> (a -> a) -> SE ()
Type synonyms for often used array data-types
To save some typing there are some aliases defined for most frequntly used array data types:
type Arr1 a = Arr Sig a
type DArr1 a = Arr D a
type Arr2 a = Arr (Sig, Sig) a
type DArr2 a = Arr (D, D) a
type Arr3 a = Arr (Sig, Sig, Sig) a
type DArr3 a = Arr (D, D, D) a
Arrays that are parametrized with constant index (like DArr1) can be manipulated only at a single moment.
We can only read and write constants to it.
If an array is parametrized with signal index (like Arr1) it can be manipulated continuously. We can read and write signals to it.
Also to help the type inference we can use the functions that do nothing with the values (just pass them through) but they have strict data type so that type inference can derive the desired data type:
arr1 :: SE (Arr Sig a) -> SE (Arr Sig a)
darr1 :: SE (Arr D a) -> SE (Arr D a)
arr2 :: SE (Arr (Sig, Sig) a) -> SE (Arr (Sig, Sig) a)
darr2 :: SE (Arr (D, D) a) -> SE (Arr (D, D) a)
arr3 :: SE (Arr (Sig, Sig, Sig) a) -> SE (Arr (Sig, Sig, Sig) a)
darr3 :: SE (Arr (D, D, D) a) -> SE (Arr (D, D, D) a)
Csound opcodes
In Csound there are plenty of opcodes to work with arrays. Almost all of them are supported.
We can find out the complete list at the documentation for the module Csound.Types (see section for Arrays).
Many functions are dedicated to manipulate spectral data.
Copy vs Allocation
Some peculiarity of transition form Csound to Haskell way of thinking lies in array functions. In the Csound almost all array functions can perform two different operations. They are overloaded. If we write:
kOut[] array_operation kWin
It can do two distinct operations:
-
It can create new array if the value
kOutwas not previously initialized -
It can copy the data of the result of operation to the array
kOutif it was already allocated.
In Haskell we often find two operations coresponding to the single Csound operation.
Take for example the function fft. It performs fast Fourier transform.
In Haskell we have two operations:
type SpecArr = Arr Sig Sig
fftNew :: SpecArr -> SE SpecArr
fftCopy :: SpecArr -> SpecArr -> SE ()
The function fftNew allocates new array. But fftCopy just copies the data to existing array.
Notice how the roles of the functions are signified with the signatures.
Functional traversals and folds
There are special functions that make traversal and folding very easy.
Traverse
We can traverse all elements in the array with function:
foreachArr :: (Tuple ix, Tuple a) => Arr ix a -> ((ix, a) -> SE ()) -> SE ()
foreachArr array proc
It takes an array and procedure that is defined on pairs of index and the value. These procedure is applied to all elments in the array. Notice that it can be applied to arrays of any sizes. All of them are going to be processed in the uniform way.
There are two useful special cases for 2D arrays:
forRowArr, forColumnArr :: Tuple a => Sig -> Arr Sig2 a -> ((Sig, a) -> SE ()) -> SE ()
forRowArr rowId array proc
They traverse only specific rows or columns. The index of the row (column) is the first argument of the function.
Fold
We can fold the array with the function. The process of folding is a traversal with value accumulation.
foldArr :: (Tuple ix, Tuple a, Tuple b) =>
((ix, a) -> b -> SE b) -> b -> Arr ix a -> SE b
The foldArr function takes a procedure that updates the result of type b
based on the current index and value and the value of accumulator from the previous step.
Also it takes initial value for accumulator and array.
There are specific foldfunctions to fold on rows and columns of 2D matrix:
foldRowArr, foldColumnArr
:: (Tuple a, Tuple b) =>
((Sig, a) -> b -> SE b) -> b -> Sig -> Arr Sig2 a -> SE b
Init vs control rate traversals
In Csound there is a distinction between initial pass of the instrument. When everything gets initialized
and control rate. When the audio goes on and we can control it. The aforementioned traversal functions
work at control rate. But it's useful to be able to run them at init pass. To do it we have to use
special variants of them with suffix D. The D is a synonym for constant number in the library
that gets initialized and never changed at the control rate. So we have the functions:
foreachArrD forRowArrD forColumnArrD
foldArrD foldRowArrD foldColumnArrD
-
<= Spectrums
-
=> Csound API