{-# LANGUAGE TypeSynonymInstances #-} -- | This module includes utilities for spawning an external scsynth process, -- either for realtime or non-realtime execution. module Sound.SC3.Server.Process ( Verbosity(..), ServerOptions(..), defaultServerOptions, NRTOptions(..), defaultNRTOptions, RTOptions(..), defaultRTOptionsUDP, defaultRTOptionsTCP, EventHandler(..), defaultEventHandler, withSynth, withNRT ) where import Sound.OpenSoundControl (Transport, TCP, UDP, openTCP, openUDP) import Control.Concurrent (forkIO) import Control.Monad (unless) import Prelude hiding (catch) import Data.List (isPrefixOf) import Data.Maybe (fromMaybe) import System.Exit (ExitCode) import System.IO (Handle, hGetLine, hIsEOF, hPutStrLn, stderr, stdout) import System.Process (runInteractiveProcess, waitForProcess) -- ==================================================================== -- scsynth commandline options class CommandLine a where argumentList :: a -> [String] -- | Convert a value to an option string. class Show a => Option a where showOption :: a -> String showOption = show -- | String options need special handling (show introduces an additional level -- of quoting). instance Option (String) where showOption = id -- | Option instance for Int with default method implementations. instance Option (Int) mkOpt :: (Eq b, Option b, Show b) => String -> (a -> b) -> a -> a -> [String] mkOpt _ f d v | (f v) == (f d) = [] mkOpt o f _ v = [o, showOption (f v)] mkMaybeOpt :: (Option a, Show a) => String -> Maybe a -> [String] mkMaybeOpt o = maybe [] ((o:) . (:[]) . showOption) -- ==================================================================== -- * Server options -- | Used with the 'verbosity' field in 'ServerOptions'. data Verbosity = Silent | Quiet | Normal | Verbose | VeryVerbose | ExtremelyVerbose deriving (Eq, Show) -- 'Enum' instance for 'Verbosity' for conversion to a commandline option. instance Enum (Verbosity) where fromEnum Silent = -2 fromEnum Quiet = -1 fromEnum Normal = 0 fromEnum Verbose = 1 fromEnum VeryVerbose = 2 fromEnum ExtremelyVerbose = 4 toEnum (-1) = Quiet toEnum 0 = Normal toEnum 1 = Verbose toEnum 2 = VeryVerbose toEnum x | x >= 4 = ExtremelyVerbose toEnum _ = Silent -- | Specify general server options used b oth in realtime and non-realtime -- mode. data ServerOptions = ServerOptions { serverProgram :: FilePath, -- ^ Path to the @scsynth@ program numberOfControlBusChannels :: Int, -- ^ Number of allocated control bus channels numberOfAudioBusChannels :: Int, -- ^ Number of allocated audio bus channels numberOfInputBusChannels :: Int, -- ^ Number of physical input channels numberOfOutputBusChannels :: Int, -- ^ Number of physical output channels blockSize :: Int, -- ^ Synthesis block size numberOfSampleBuffers :: Int, -- ^ Number of allocated sample buffers maxNumberOfNodes :: Int, -- ^ Maximum number of synthesis nodes maxNumberOfSynthDefs :: Int, -- ^ Maximum number of synth definitions realTimeMemorySize :: Int, -- ^ Realtime memory size in bytes numberOfWireBuffers :: Int, -- ^ Number of unit generator connection buffers numberOfRandomSeeds :: Int, -- ^ Number of random number generator seeds loadSynthDefs :: Bool, -- ^ If 'True', load synth definitions from /synthdefs/ directory on startup verbosity :: Verbosity -- ^ 'Verbosity' level } deriving (Eq, Show) -- | Default server options. defaultServerOptions :: ServerOptions defaultServerOptions = ServerOptions { serverProgram = "scsynth", numberOfControlBusChannels = 4096, numberOfAudioBusChannels = 128, numberOfInputBusChannels = 8, numberOfOutputBusChannels = 8, blockSize = 64, numberOfSampleBuffers = 1024, maxNumberOfNodes = 1024, maxNumberOfSynthDefs = 1024, realTimeMemorySize = 8192, numberOfWireBuffers = 64, numberOfRandomSeeds = 64, loadSynthDefs = True, verbosity = Normal } instance CommandLine (ServerOptions) where argumentList v = serverProgram v : concat [ mkOpt "-c" numberOfControlBusChannels d v , mkOpt "-a" numberOfAudioBusChannels d v , mkOpt "-i" numberOfInputBusChannels d v , mkOpt "-o" numberOfOutputBusChannels d v , mkOpt "-z" blockSize d v , mkOpt "-b" numberOfSampleBuffers d v , mkOpt "-n" maxNumberOfNodes d v , mkOpt "-d" maxNumberOfSynthDefs d v , mkOpt "-w" numberOfWireBuffers d v , mkOpt "-r" numberOfRandomSeeds d v , mkOpt "-D" (fromEnum . loadSynthDefs) d v , mkOpt "-v" (fromEnum . verbosity) d v ] where d = defaultServerOptions -- ==================================================================== -- * Realtime options -- | Helper class for polymorphic opening of network connections. class OpenTransport t where openTransport :: RTOptions t -> String -> IO t instance OpenTransport (UDP) where openTransport rtOptions server = openUDP server (udpPortNumber rtOptions) instance OpenTransport (TCP) where openTransport rtOptions server = openTCP server (tcpPortNumber rtOptions) -- | Realtime server options, parameterized by the OpenSoundControl -- 'Transport' to be used. data RTOptions t = RTOptions { -- Network control udpPortNumber :: Int, -- ^ UDP port number (one of 'udpPortNumber' and 'tcpPortNumber' must be non-zero) tcpPortNumber :: Int, -- ^ TCP port number (one of 'udpPortNumber' and 'tcpPortNumber' must be non-zero) useZeroconf :: Bool, -- ^ If 'True', publish scsynth service through Zeroconf maxNumberOfLogins :: Int, -- ^ Max number of supported logins if 'sessionPassword' is set sessionPassword :: Maybe String, -- ^ Session password -- Audio device control hardwareDeviceName :: Maybe String, -- ^ Hardware device name (JACK client:server name on Linux) hardwareBufferSize :: Int, -- ^ Hardware buffer size (no effect with JACK) hardwareSampleRate :: Int, -- ^ Hardware buffer size (no effect with JACK) inputStreamsEnabled :: Maybe Int, -- ^ Enabled input streams (CoreAudio only) outputStreamsEnabled :: Maybe Int -- ^ Enabled output streams (CoreAudio only) } deriving (Eq, Show) -- | Default realtime server options. defaultRTOptions :: RTOptions t defaultRTOptions = RTOptions { -- Network control udpPortNumber = 0, tcpPortNumber = 0, useZeroconf = False, maxNumberOfLogins = 16, sessionPassword = Nothing, -- Audio device control hardwareDeviceName = Nothing, hardwareBufferSize = 0, hardwareSampleRate = 0, inputStreamsEnabled = Nothing, outputStreamsEnabled = Nothing } -- | Default realtime server options (UDP transport). defaultRTOptionsUDP :: RTOptions UDP defaultRTOptionsUDP = defaultRTOptions { udpPortNumber = 57110 } -- | Default realtime server options (TCP transport). defaultRTOptionsTCP :: RTOptions TCP defaultRTOptionsTCP = defaultRTOptions { tcpPortNumber = 57110 } instance CommandLine (RTOptions t) where argumentList v = concat [ mkOpt "-u" udpPortNumber d v , mkOpt "-t" tcpPortNumber d v , mkOpt "-R" (fromEnum . useZeroconf) d v , mkMaybeOpt "-H" $ hardwareDeviceName v , mkOpt "-Z" hardwareBufferSize d v , mkOpt "-S" hardwareSampleRate d v , mkOpt "-l" maxNumberOfLogins d v , mkMaybeOpt "-p" $ sessionPassword v , mkMaybeOpt "-I" $ inputStreamsEnabled v , mkMaybeOpt "-O" $ outputStreamsEnabled v ] where d = defaultRTOptions -- ==================================================================== -- * Non-Realtime options -- | Non-realtime server options. data NRTOptions = NRTOptions { commandFilePath :: Maybe FilePath, -- ^ Path to OSC command file ('Nothing' for stdin) inputFilePath :: Maybe FilePath, -- ^ Path to input sound file ('Nothing' for no audio input) outputFilePath :: FilePath, -- ^ Path to output sound file outputSampleRate :: Int, -- ^ Output sound file sample rate outputHeaderFormat :: String, -- ^ Output sound file header format outputSampleFormat :: String -- ^ Output sound file sample format } deriving (Eq, Show) -- | Default non-realtime server options. defaultNRTOptions :: NRTOptions defaultNRTOptions = NRTOptions { commandFilePath = Nothing, inputFilePath = Nothing, outputFilePath = "output.wav", outputSampleRate = 44100, outputHeaderFormat = "wav", outputSampleFormat = "int16" } instance CommandLine (NRTOptions) where argumentList x = "-N" : map ($x) [ fromMaybe "_" . commandFilePath , fromMaybe "_" . inputFilePath , outputFilePath , show . outputSampleRate , outputHeaderFormat , outputSampleFormat ] -- ==================================================================== -- * Event handler -- | Event handler for handling I/O with external @scsynth@ processes, -- parameterized by the I/O handle used for sending OSC commands to the -- server. data EventHandler t = EventHandler { onPutString :: String -> IO (), -- ^ Handle one line of normal output onPutError :: String -> IO (), -- ^ Handle one line of error output onBoot :: t -> IO () -- ^ Executed with the OSC handle after the server has booted } -- | Default event handler, writing to stdout and stderr, respectively. defaultEventHandler :: EventHandler t defaultEventHandler = EventHandler { onPutString = hPutStrLn stdout, onPutError = hPutStrLn stderr, onBoot = const (return ()) } -- ==================================================================== -- Process helpers pipeOutput :: (String -> IO ()) -> Handle -> IO () pipeOutput f h = hIsEOF h >>= flip unless (hGetLine h >>= f >> pipeOutput f h) -- ==================================================================== -- * Realtime scsynth execution -- | Execute a realtime instance of @scsynth@ with 'Transport' t and return -- 'ExitCode' when the process exists. withSynth :: (Transport t, OpenTransport t) => ServerOptions -> RTOptions t -> EventHandler t -> IO ExitCode withSynth serverOptions rtOptions handler = do (_, hOut, hErr, hProc) <- runInteractiveProcess exe args Nothing Nothing forkIO $ putStdout0 hOut forkIO $ putStderr hErr waitForProcess hProc where (exe:args) = argumentList serverOptions ++ argumentList rtOptions putStdout0 h = do eof <- hIsEOF h unless eof $ do l <- hGetLine h if isPrefixOf "SuperCollider 3 server ready.." l then do onPutString handler l fd <- openTransport rtOptions "127.0.0.1" forkIO $ onBoot handler fd -- Spawn more efficient output handler forkIO $ putStdout h return () else do onPutString handler l putStdout0 h -- recurse putStdout = pipeOutput (onPutString handler) putStderr = pipeOutput (onPutError handler) -- ==================================================================== -- * Non-Realtime scsynth execution -- | Execute a non-realtime instance of @scsynth@ and return 'ExitCode' when -- the process exists. withNRT :: ServerOptions -> NRTOptions -> EventHandler Handle -> IO ExitCode withNRT serverOptions nrtOptions handler = do (hIn, hOut, hErr, hProc) <- runInteractiveProcess exe args Nothing Nothing forkIO $ putStdout hOut forkIO $ putStderr hErr forkIO $ onBoot handler hIn waitForProcess hProc where (exe:args) = argumentList serverOptions ++ argumentList nrtOptions { commandFilePath = Nothing } putStdout = pipeOutput (onPutString handler) putStderr = pipeOutput (onPutError handler) -- EOF