vivid-0.1.0.0: Vivid/SynthDef/Literally.hs
-- | __You probably don't need to use this directly__ -- use "Vivid.SynthDef" instead
--
-- This is a representation of how SynthDefs are sent over the wire, as described in the
-- < http://doc.sccode.org/Reference/Synth-Definition-File-Format.html Synth Definition File Format >
-- helpfile.
--
{-# OPTIONS_HADDOCK show-extensions #-}
{-# LANGUAGE NoRebindableSyntax #-}
{-# LANGUAGE ScopedTypeVariables, OverloadedStrings #-}
module Vivid.SynthDef.Literally (
LiteralSynthDef(..)
, encodeSynthDefFile
, decodeSynthDefFile
, UGenSpec(..)
, InputSpec(..)
, ParamName(..)
, SynthDefFile(..)
, OutputSpec(..)
) where
import Vivid.SynthDef.Types
import Vivid.OSC.Util (floatToWord, wordToFloat)
import Control.Arrow (first)
import Control.Monad (when)
import Data.Binary (decode, encode)
import qualified Data.ByteString as B
import qualified Data.ByteString as BS
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as BSL
import Data.Int
import Data.List.Split (chunksOf)
import Data.Monoid
data LiteralSynthDef
= LiteralSynthDef {
_synthDefName :: ByteString -- pstring: "a pascal format string: a byte giving the length followed by that many bytes"
,_synthDefConstants :: [Float]
,_synthDefParameters :: [Float] -- Initial values
,_synthDefParamNames :: [ParamName]
,_synthDefUGens :: [UGenSpec]
,_synthDefVariants :: [VariantSpec]
}
deriving (Show)
data SynthDefFile = SynthDefFile [LiteralSynthDef]
deriving (Show)
decodeSynthDefFile :: ByteString -> IO SynthDefFile
decodeSynthDefFile blob = do
let (top, rest) = BS.splitAt 4 blob
let (fileVersion :: Int32, rest2) =
first (decode . BSL.fromStrict) $ BS.splitAt 4 rest
when (top /= "SCgf" || fileVersion /= 2) $
error $ "screwed up synthdef file " <> show top <> show fileVersion
let (numberOfSynthDefs :: Int16, rest3) =
first (decode . BSL.fromStrict) $ BS.splitAt 2 rest2
let (synthDefs, rest4) = getNWith numberOfSynthDefs decodeSynthDef rest3
if rest4 /= ""
then error $ "leftover data: " <> show rest4
else return ()
return $ SynthDefFile synthDefs
{-
a synth-definition-file is :
int32 - four byte file type id containing the ASCII characters: "SCgf"
int32 - file version, currently 2.
int16 - number of synth definitions in this file (D).
[ synth-definition ] * D
-}
encodeSynthDefFile :: SynthDefFile -> ByteString
encodeSynthDefFile (SynthDefFile synthDefs) = mconcat [
"SCgf"
, BSL.toStrict $ encode (2 :: Int32)
, BSL.toStrict $ encode (toEnum (length synthDefs) :: Int16)
, mconcat $ map encodeSynthDef synthDefs
]
-- Yes, the 'restN's are ugly, yes i could have used a state monad. Don't judge me.
decodeSynthDef :: ByteString -> (LiteralSynthDef, {- rest: -} ByteString)
decodeSynthDef blob =
let (name :: ByteString, rest) = getPString blob
(numConstants :: Int32, rest2) = getInt32 rest
(constants :: [Float], rest3) =
first (map (wordToFloat . decode . BSL.fromStrict)) $ getN4ByteBlocks numConstants rest2
(numParams :: Int32, rest4) = getInt32 rest3
(params :: [Float], rest5) =
first (map (wordToFloat . decode . BSL.fromStrict)) $ getN4ByteBlocks numParams rest4
(numParamNames :: Int32, rest6) = getInt32 rest5
(paramNames :: [ParamName], rest7) = getNWith numParamNames getParamName rest6
(numUGens :: Int32, rest8) = getInt32 rest7
(uGens, rest9) = getNWith numUGens getUGenSpec rest8
(numVariants, rest10) = getInt16 rest9
(variantSpecs, rest11) = getNWith numVariants (getVariantSpec numParams) rest10
in (LiteralSynthDef name constants params paramNames uGens variantSpecs, rest11)
{-
a synth-definition is :
pstring - the name of the synth definition
int32 - number of constants (K)
[float32] * K - constant values
int32 - number of parameters (P)
[float32] * P - initial parameter values
int32 - number of parameter names (N)
[ param-name ] * N
int32 - number of unit generators (U)
[ ugen-spec ] * U
int16 - number of variants (V)
[ variant-spec ] * V
-}
encodeSynthDef :: LiteralSynthDef -> ByteString
encodeSynthDef (LiteralSynthDef name constants params paramNames uGenSpecs variants) = mconcat [
encodePString name
, BSL.toStrict $ encode (toEnum (length constants) :: Int32)
, mconcat $ map (BSL.toStrict . encode . floatToWord) constants
, BSL.toStrict $ encode (toEnum (length params) :: Int32)
, mconcat $ map (BSL.toStrict . encode . floatToWord) params
, BSL.toStrict $ encode (toEnum (length paramNames) :: Int32)
, mconcat $ map encodeParamName paramNames
, BSL.toStrict $ encode (toEnum (length uGenSpecs) :: Int32)
, mconcat $ map encodeUGenSpec uGenSpecs
, BSL.toStrict $ encode (toEnum (length variants) :: Int16)
, mconcat $ map encodeVariantSpec variants
]
data ParamName = ParamName {
_paramName_name :: ByteString
,_paramName_indexInParamArray :: Int32
}
deriving (Show)
{-
a param-name is :
pstring - the name of the parameter
int32 - its index in the parameter array
-}
getParamName :: ByteString -> (ParamName, ByteString)
getParamName blob =
let (name, rest) = getPString blob
(index, rest2) = getInt32 rest
in (ParamName name index, rest2)
encodeParamName :: ParamName -> ByteString
encodeParamName (ParamName name index) =
encodePString name <> BSL.toStrict (encode index)
data UGenSpec = UGenSpec {
_uGenSpec_name :: ByteString
,_uGenSpec_calcRate :: CalculationRate
,_uGenSpec_inputs :: [InputSpec]
,_uGenSpec_outputs :: [OutputSpec]
,_uGenSpec_specialIndex :: Int16
}
deriving (Show)
{-
a ugen-spec is :
pstring - the name of the SC unit generator class
int8 - calculation rate
int32 - number of inputs (I)
int32 - number of outputs (O)
int16 - special index
[ input-spec ] * I
[ output-spec ] * O
-}
getUGenSpec :: ByteString -> (UGenSpec, {- rest: -} ByteString)
getUGenSpec blob =
let (name, rest) = getPString blob
(calcRate :: CalculationRate, rest2) =
first ((toEnum) . (fromEnum :: Int8 -> Int) . decode . BSL.fromStrict) $
B.splitAt 1 rest
(numInputs :: Int32, rest3) = getInt32 rest2
(numOutputs :: Int32, rest4) = getInt32 rest3
(specialIndex, rest5) = getInt16 rest4
(inputSpecs, rest6) = getNWith numInputs getInputSpec rest5
(outputSpecs, rest7) = getNWith numOutputs getOutputSpec rest6
in (UGenSpec name calcRate inputSpecs outputSpecs specialIndex, rest7)
encodeUGenSpec :: UGenSpec -> ByteString
encodeUGenSpec (UGenSpec name calcRate inputSpecs outputSpecs specialIndex) = mconcat [
encodePString name
,BSL.toStrict $ encode $ (toEnum (fromEnum calcRate) :: Int8)
,BSL.toStrict $ encode $ (toEnum (length inputSpecs) :: Int32)
,BSL.toStrict $ encode $ (toEnum (length outputSpecs) :: Int32)
,BSL.toStrict $ encode specialIndex
,mconcat $ map encodeInputSpec inputSpecs
,mconcat $ map encodeOutputSpec outputSpecs
]
data InputSpec
= InputSpec_UGen {
_inputSpec_uGen_index :: Int32
,_inputSpec_uGen_outputIndex :: Int32
}
| InputSpec_Constant {
_inputSpec_constant_index :: Int32
}
deriving (Show, Read, Eq)
{-
an input-spec is :
int32 - index of unit generator or -1 for a constant
if (unit generator index == -1) :
int32 - index of constant
else :
int32 - index of unit generator output
-}
getInputSpec :: ByteString -> (InputSpec, ByteString)
getInputSpec blob =
let (stuffForThis, rest) = BS.splitAt 8 blob
(one :: Int32, two :: Int32) = (\(a,b) -> ((decode . BSL.fromStrict) a, (decode . BSL.fromStrict) b)) $ BS.splitAt 4 stuffForThis
spec = case one of
-1 -> InputSpec_Constant two
n | n > -1 -> InputSpec_UGen one two
_ -> error "bad number"
in (spec, rest)
encodeInputSpec :: InputSpec -> ByteString
encodeInputSpec inputSpec = mconcat $ map (BSL.toStrict . encode) $ encodeInputSpec' inputSpec
where
encodeInputSpec' :: InputSpec -> [Int32]
encodeInputSpec' (InputSpec_Constant i) = [ (-1), i ]
encodeInputSpec' (InputSpec_UGen i oI) = [ i, oI ]
data OutputSpec = OutputSpec { _outputSpec_calcRate :: CalculationRate }
deriving (Show, Read, Eq)
{-
an output-spec is :
int8 - calculation rate
-}
getOutputSpec :: ByteString -> (OutputSpec, ByteString)
getOutputSpec blob =
first (OutputSpec . toEnum . (fromEnum :: Int8 -> Int) . decode . BSL.fromStrict) $
BS.splitAt 1 blob
encodeOutputSpec :: OutputSpec -> ByteString
encodeOutputSpec (OutputSpec calcRate) =
BSL.toStrict $ encode $ (toEnum (fromEnum calcRate) :: Int8)
data VariantSpec
= VariantSpec {
_variantSpec_name :: ByteString
,_variantSpec_initialParamVals :: [Float] -- float32
}
deriving (Show)
{-
a variant-spec is :
pstring - the name of the variant
[float32] * P - variant initial parameter values
-}
getVariantSpec :: Int32 -> ByteString -> (VariantSpec, ByteString)
getVariantSpec numParams blob =
let (name, rest) = getPString blob
(initialParamVals :: [Float], rest2) = first (map (decode . BSL.fromStrict)) $ getN4ByteBlocks numParams rest
in (VariantSpec name initialParamVals, rest2)
encodeVariantSpec :: VariantSpec -> ByteString
encodeVariantSpec (VariantSpec name initialParamVals) =
encodePString name <> mconcat (map (BSL.toStrict . encode . floatToWord) initialParamVals)
--- helpers:
getPString :: ByteString -> (ByteString, {- rest: -} ByteString)
getPString blob = first (BS.drop 1) $
BS.splitAt (fromEnum (BS.head blob) + 1) blob
encodePString :: ByteString -> ByteString
encodePString s = toEnum (BS.length s) `BS.cons` s
getNWith :: (Integral i) => i -> (ByteString -> (a, ByteString)) -> ByteString -> ([a], ByteString)
getNWith 0 _ rest = ([], rest)
getNWith n f rest =
let (head1, rest2) = f rest
(head2, rest3) = getNWith (n - 1) f rest2
in (head1 : head2, rest3)
getInt32 :: ByteString -> (Int32, ByteString)
getInt32 blob = first (decode . BSL.fromStrict) $ B.splitAt 4 blob
getInt16 :: ByteString -> (Int16, ByteString)
getInt16 blob = first (decode . BSL.fromStrict) $ B.splitAt 2 blob
getN4ByteBlocks :: Int32 -> ByteString -> ([ByteString], ByteString)
getN4ByteBlocks numBlocks blob =
first (map (BS8.pack) . chunksOf 4 . BS8.unpack) $
B.splitAt (4 * fromEnum numBlocks) blob