de.sciss.synth.Server.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of scalacollider_2.10.0-RC2 Show documentation
Show all versions of scalacollider_2.10.0-RC2 Show documentation
A sound synthesis library for the SuperCollider server
The newest version!
/*
* Server.scala
* (ScalaCollider)
*
* Copyright (c) 2008-2012 Hanns Holger Rutz. All rights reserved.
*
* This software is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either
* version 2, june 1991 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License (gpl.txt) along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*
* For further information, please contact Hanns Holger Rutz at
* [email protected]
*/
package de.sciss.synth
import io.{AudioFileType, SampleFormat}
import java.io.{BufferedReader, File, InputStreamReader, IOException}
import java.util.{Timer, TimerTask}
import actors.{Actor, Channel, DaemonActor, Future, OutputChannel, TIMEOUT}
import concurrent.SyncVar
import de.sciss.osc.{Dump, Client => OSCClient, Message, Packet, Transport, TCP, UDP}
import java.net.{DatagramSocket, InetAddress, InetSocketAddress, ServerSocket}
import collection.mutable.ListBuffer
object Server {
private val allSync = new AnyRef
// private var allVar = Set.empty[ Server ]
var default: Server = null
/**
* The default file path to `scsynth`. If the runtime (system) property `"SC_HOME"` is provided,
* this specifies the directory of `scsynth`. Otherwise, an environment (shell) variable named
* `"SC_HOME"` is checked. If neither exists, this returns `scsynth` in the current working directory.
*/
def defaultProgramPath = new File( sys.props.getOrElse( "SC_HOME", sys.env.getOrElse( "SC_HOME", "" )),
"scsynth" ).getAbsolutePath
/**
* The base trait for `Config` and `ConfigBuilder` describes the settings used to boot scsynth in
* realtime or non-realtime mode, as well as its server address and port.
*
* You obtain a `ConfigBuilder` by calling `Server.Config()`. This builder can then be mutated and
* will be implicitly converted to an immutable `Config` when required.
*
* See `ConfigBuilder` for its default values.
*
* @see [[de.sciss.synth.Server.ConfigBuilder]]
* @see [[de.sciss.synth.Server.Config]]
*/
trait ConfigLike {
/**
* The path to `scsynth`, used when booting a server. This can be either a relative path
* (relating to the JVM's working directory), or an absolute path.
*
* @see [[de.sciss.synth.Server#defaultProgramPath]]
*/
def programPath: String
/**
* The maximum number of control bus channels.
*/
def controlBusChannels: Int
/**
* The maximum number of audio bus channels. This includes the channels connected
* to hardware (`outputBusChannels`) as well as all channels for internal routing.
*/
def audioBusChannels: Int
/**
* The number of connected audio hardware output channels. This does not need to
* correspond to the actual number of channels your sound card provides, but can
* be lower or higher, although a higher value doesn't have any effect as channel
* indices above the number of channels of the sound card will be treated as
* internal channels.
*/
def outputBusChannels: Int
/**
* The calculation block size. That is, the number of audio samples calculated en-bloc.
* This corresponds with the control rate, such that
* `controlRate := audioRate / blockSize`. It should be a power of two.
*/
def blockSize: Int
/**
* The audio hardware sampling rate to use. A value of `0` indicates that scsynth
* should use the current sampling rate of the audio hardware. An explicit setting
* will make scsynth try to switch the sound card's sample rate if necessary.
*/
def sampleRate: Int
/**
* The maximum number of audio buffers (for the `Buffer` class).
*/
def audioBuffers: Int
/**
* The maximum number of concurrent nodes (synths and groups).
*/
def maxNodes: Int
/**
* The maximum number of synth defs.
*/
def maxSynthDefs: Int
/**
* The maximum number of pre-allocated realtime memory in bytes. This memory
* is used for many UGens such as `Limiter`, `DelayN` etc. It does not
* affect dynamically allocated memory such as audio buffers.
*/
def memorySize: Int
/**
* The maximum number of concurrent connections between UGens in a single synth.
* ScalaCollider performs a depth-first topological sorting of the synth defs,
* so you should not worry too much about this value. It can become important
* in very heavy channel expansions and mixdown.
*
* This value will be automatically increased if a more complex def is loaded
* at startup, but it cannot be increased thereafter without rebooting.
*/
def wireBuffers: Int
/**
* The number of individual random number generators allocated.
*/
def randomSeeds: Int
/**
* Whether scsynth should load synthdefs stored on the harddisk when booted.
*/
def loadSynthDefs: Boolean
/**
* ?
*/
def machPortName: Option[ (String, String) ]
/**
* The verbosity level of scsynth. The standard value is `0`, while
* `-1` suppresses informational messages, `-2` also suppresses many error messages.
*/
def verbosity: Int
/**
* An explicit list of paths where DSP plugins are found. Usually this is not
* specified, and scsynth looks for plugins in their default location.
*/
def plugInsPaths: List[ String ]
/**
* An option to restrict access to files (e.g. for loading and saving buffers) to
* a particular directory. This is a security measure, preventing malicious clients from
* accessing parts of the harddisk which they shouldn't.
*/
def restrictedPath: Option[ String ]
// def memoryLocking: Boolean
// // client only
// def clientID: Int
// def nodeIDOffset: Int
// ---- realtime only ----
/**
* (Realtime) Host address of scsynth, when trying to `connect` to an already running server on the net.
*/
def host: String
/**
* (Realtime) UDP or TCP port used by scsynth.
*/
def port: Int
/**
* (Realtime) Open Sound Control transport used by scsynth. (Either of `UDP` and `TCP`).
*/
def transport: Transport.Net
/**
* (Realtime) An option to enable particular input 'streams' or 'bundles' of a sound card.
* This is a 'binary' String made of `'0'` and `'1'` characters.
* If the string is `"01100"`, for example, then only the second and third input streams on
* the device will be enabled.
*/
def inputStreamsEnabled: Option[ String ]
/**
* (Realtime) An option to enable particular output 'streams' or 'bundles' of a sound card.
* This is a 'binary' String made of `'0'` and `'1'` characters.
* If the string is `"01100"`, for example, then only the second and third output streams on
* the device will be enabled.
*/
def outputStreamsEnabled: Option[ String ]
/**
* (Realtime) An option denoting the name of the sound card to use. On systems which distinguish
* input and output devices (OS X), this implies that both are the same. Otherwise, you can
* use the `deviceNames` method instead.
*
* @see deviceNames
*/
def deviceName: Option[ String ]
/**
* (Realtime) An option denoting the name of the input and output sound device to use. This is for
* systems which distinguish input and output devices (OS X). If you use a single device both for
* input and output (applies to most professional audio interfaces), you can simply use the
* single string method `deviceName`.
*
* @see deviceName
*/
def deviceNames: Option[ (String, String) ]
/**
* (Realtime) The number of connected audio hardware input channels. This does not need to
* correspond to the actual number of channels your sound card provides, but can
* be lower or higher, although a higher value doesn't have any effect as channel
* indices above the number of channels of the sound card will be treated as
* internal channels.
*/
def inputBusChannels: Int
/**
* (Realtime) A value to adjust the sound card's hardware block size. Typically you will leave
* this to `0` which means that the current block size is used. The block sizes supported depend
* on the particular sound card. Lower values decrease latency but may increase CPU load.
*/
def hardwareBlockSize: Int
/**
* (Realtime) Whether to announce scsynth's OSC service via zero conf. See
* [[http://en.wikipedia.org/wiki/Zero_configuration_networking Wikipedia]] for more details.
*/
def zeroConf: Boolean
/**
* (Realtime) The maximum number of client connections when using TCP transport.
*/
def maxLogins: Int
/**
* (Realtime) A requires session password when using TCP transport. When using TCP and the password option
* is set, each client must send the correct password as the first command to the server, otherwise it is
* rejected.
*/
def sessionPassword: Option[ String ]
// ---- nonrealtime only ----
/**
* (Non-Realtime) Path to the binary OSC file.
*/
def nrtCommandPath: String
/**
* (Non-Realtime) Path to the audio input file used as audio input bus supplement.
*/
def nrtInputPath: Option[ String ]
/**
* (Non-Realtime) Path to the audio output file used as audio output bus supplement.
*/
def nrtOutputPath: String
/**
* (Non-Realtime) Audio file format for writing the output.
*/
def nrtHeaderFormat: AudioFileType
/**
* (Non-Realtime) Audio sample format for writing the output.
*/
def nrtSampleFormat: SampleFormat
/**
* Produces a command line for booting scsynth in realtime mode.
*/
final def toRealtimeArgs : List[ String ] = Config.toRealtimeArgs( this )
/**
* Produces a command line for booting scsynth in non-realtime mode.
*/
final def toNonRealtimeArgs : List[ String ] = Config.toNonRealtimeArgs( this )
/**
* A utility method providing the audio bus offset for the start of
* the internal channels. (simply the sum of `outputBusChannels` and `inputBusChannels`).
*/
final def internalBusIndex: Int = outputBusChannels + inputBusChannels
}
object Config {
/**
* Creates a new configuration builder with default settings
*/
def apply() : ConfigBuilder = new ConfigBuilder()
/**
* Implicit conversion which allows you to use a `ConfigBuilder`
* wherever a `Config` is required.
*/
implicit def build( cb: ConfigBuilder ) : Config = cb.build
private[Server] def toNonRealtimeArgs( o: ConfigLike ): List[ String ] = {
val result = new ListBuffer[ String ]()
// -N <...other scsynth arguments>
result += o.programPath
result += "-N"
result += o.nrtCommandPath
result += o.nrtInputPath.getOrElse( "_" )
result += o.nrtOutputPath
result += o.sampleRate.toString
result += o.nrtHeaderFormat.id
result += o.nrtSampleFormat.id
addCommonArgs( o, result )
result.toList
}
private[Server] def toRealtimeArgs( o: ConfigLike ): List[ String ] = {
val result = new ListBuffer[ String ]()
result += o.programPath
o.transport match {
case TCP =>
result += "-t"
result += o.port.toString
case UDP =>
result += "-u"
result += o.port.toString
}
addCommonArgs( o, result )
if( o.hardwareBlockSize != 0 ) {
result += "-Z"
result += o.hardwareBlockSize.toString
}
if( o.sampleRate != 0 ) {
result += "-S"
result += o.sampleRate.toString
}
if( o.maxLogins != 64 ) {
result += "-l"
result += o.maxLogins.toString
}
o.sessionPassword.foreach { pwd =>
result += "-p"
result += pwd
}
o.inputStreamsEnabled.foreach { stream =>
result += "-I"
result += stream
}
o.outputStreamsEnabled.foreach { stream =>
result += "-O"
result += stream
}
if( !o.zeroConf ) {
result += "-R"
result += "0"
}
o.deviceNames.foreach { case (inDev, outDev) =>
result += "-H"
result += inDev
result += outDev
}
o.deviceName.foreach { n =>
result += "-H"
result += n
}
o.restrictedPath.foreach { path =>
result += "-P"
result += path
}
result.toList
}
private[Server] def addCommonArgs( o: ConfigLike, result: ListBuffer[ String ]) = {
if( o.controlBusChannels != 4096 ) {
result += "-c"
result += o.controlBusChannels.toString
}
if( o.audioBusChannels != 128 ) {
result += "-a"
result += o.audioBusChannels.toString
}
if( o.inputBusChannels != 8 ) {
result += "-i"
result += o.inputBusChannels.toString
}
if( o.outputBusChannels != 8 ) {
result += "-o"
result += o.outputBusChannels.toString
}
if( o.blockSize != 64 ) {
result += "-z"
result += o.blockSize.toString
}
if( o.audioBuffers != 1024 ) {
result += "-b"
result += o.audioBuffers.toString
}
if( o.maxNodes != 1024 ) {
result += "-n"
result += o.maxNodes.toString
}
if( o.maxSynthDefs != 1024 ) {
result += "-d"
result += o.maxSynthDefs.toString
}
if( o.memorySize != 8192 ) {
result += "-m"
result += o.memorySize.toString
}
if( o.wireBuffers != 64 ) {
result += "-w"
result += o.wireBuffers.toString
}
if( o.randomSeeds != 64 ) {
result += "-r"
result += o.randomSeeds.toString
}
if( !o.loadSynthDefs ) {
result += "-D"
result += "0"
}
o.machPortName.foreach { case (send, reply) =>
result += "-M"
result += send
result += reply
}
if( o.verbosity != 0 ) {
result += "-v"
result += o.verbosity.toString
}
if( o.plugInsPaths.nonEmpty ) {
result += "-U"
result += o.plugInsPaths.mkString( ":" )
}
// if( o.memoryLocking ) {
// result += "-L"
// }
}
}
/**
* @see [[de.sciss.synth.Server.ConfigBuilder]]
* @see [[de.sciss.synth.Server.ConfigLike]]
*/
final class Config private[Server]( val programPath: String, val controlBusChannels: Int, val audioBusChannels: Int,
val outputBusChannels: Int, val blockSize: Int, val sampleRate: Int, val audioBuffers: Int,
val maxNodes: Int, val maxSynthDefs: Int, val memorySize: Int, val wireBuffers: Int,
val randomSeeds: Int, val loadSynthDefs: Boolean, val machPortName: Option[ (String, String) ],
val verbosity: Int, val plugInsPaths: List[ String ], val restrictedPath: Option[ String ],
/* val memoryLocking: Boolean, */ val host: String, val port: Int, val transport: Transport.Net,
val inputStreamsEnabled: Option[ String ], val outputStreamsEnabled: Option[ String ],
val deviceNames: Option[ (String, String) ], val deviceName: Option[ String ],
val inputBusChannels: Int,
val hardwareBlockSize: Int, val zeroConf: Boolean, val maxLogins: Int,
val sessionPassword: Option[ String ], val nrtCommandPath: String,
val nrtInputPath: Option[ String ],
val nrtOutputPath: String, val nrtHeaderFormat: AudioFileType,
val nrtSampleFormat: SampleFormat )
extends ConfigLike {
override def toString = "ServerOptions"
}
/**
* @see [[de.sciss.synth.Server.Config]]
* @see [[de.sciss.synth.Server.ConfigLike]]
*/
final class ConfigBuilder private[Server] () extends ConfigLike {
/**
* The default `programPath` is read from `defaultProgramPath`
*
* @see [[de.sciss.synth.Server#defaultProgramPath]]
*/
var programPath: String = defaultProgramPath
/**
* The default number of control bus channels is `4096` (scsynth default)
*/
var controlBusChannels: Int = 4096
/**
* The default number of audio bus channels is `128` (scsynth default)
*/
var audioBusChannels: Int = 128
/**
* The default number of output bus channels is `8` (scsynth default)
*/
var outputBusChannels: Int = 8
/**
* The default calculation block size is `64` (scsynth default)
*/
var blockSize: Int = 64
/**
* The default sample rate is `0` (meaning that it is adjusted to
* the sound card's current rate; scsynth default)
*/
var sampleRate: Int = 0
/**
* The default number of audio buffers is `1024` (scsynth default)
*/
var audioBuffers: Int = 1024
/**
* The default maximum number of nodes is `1024` (scsynth default)
*/
var maxNodes: Int = 1024
/**
* The default maximum number of synth defs is `1024` (scsynth default)
*/
var maxSynthDefs: Int = 1024
/**
* The default memory size is `65536` (64 KB) (higher than scsynth's default of 8 KB)
*/
var memorySize: Int = 65536 // 8192
/**
* The default number of wire buffers is `256` (higher than scsynth's default of `64`).
*/
var wireBuffers: Int = 256 // 64
/**
* The default number of random number generators is `64` (scsynth default)
*/
var randomSeeds: Int = 64
/**
* The default setting for loading synth defs is `true` (scsynth default)
*/
var loadSynthDefs: Boolean = true
/**
* The default settings for mach port name is `None` (scsynth default)
*/
var machPortName: Option[ (String, String) ] = None
/**
* The default verbosity level is `0` (scsynth default)
*/
var verbosity: Int = 0
/**
* The default setting for plugin path redirection is `Nil`
* (use standard paths; scsynth default)
*/
var plugInsPaths: List[ String ] = Nil
/**
* The default setting for restricting file access is `None` (scsynth default)
*/
var restrictedPath: Option[ String ] = None
// var memoryLocking: Boolean = false
// // client only
// var clientID: Int = 0
// var nodeIDOffset: Int = 1000
// ---- realtime only ----
/**
* (Realtime) The default host name is `127.0.0.1`
*/
var host: String = "127.0.0.1"
/**
* (Realtime) The default port is `57110`.
*/
var port: Int = 57110
/**
* (Realtime) The default transport is `UDP`.
*/
var transport: Transport.Net = UDP
/**
* (Realtime) The default settings for enabled input streams is `None`
*/
var inputStreamsEnabled: Option[ String ] = None
/**
* (Realtime) The default settings for enabled output streams is `None`
*/
var outputStreamsEnabled: Option[ String ] = None
/**
* (Realtime) The default device name is `None` (scsynth default; it will
* use the system default sound card)
*/
private var deviceNameVar: Option[ String ] = None
/**
* (Realtime) The default input/output device names is `None` (scsynth default; it will
* use the system default sound card)
*/
private var deviceNamesVar: Option[ (String, String) ] = None
def deviceName: Option[ String ] = deviceNameVar
def deviceNames: Option[ (String, String) ] = deviceNamesVar
def deviceName_=( value: Option[ String ]) {
deviceNameVar = value
if( value.isDefined ) deviceNamesVar = None
}
def deviceNames_=( value: Option[ (String, String) ]) {
deviceNamesVar = value
if( value.isDefined ) deviceNameVar = None
}
/**
* (Realtime) The default number of input bus channels is `8` (scsynth default)
*/
var inputBusChannels: Int = 8
/**
* (Realtime) The default setting for hardware block size is `0` (meaning that
* scsynth uses the hardware's current block size; scsynth default)
*/
var hardwareBlockSize: Int = 0
/**
* (Realtime) The default setting for zero-conf is `false` (other than
* scsynth's default which is `true`)
*/
var zeroConf: Boolean = false // true
/**
* (Realtime) The maximum number of TCP clients is `64` (scsynth default)
*/
var maxLogins: Int = 64
/**
* (Realtime) The default TCP session password is `None`
*/
var sessionPassword: Option[ String ] = None
// ---- nonrealtime only ----
var nrtCommandPath: String = ""
var nrtInputPath: Option[ String ] = None
var nrtOutputPath: String = ""
var nrtHeaderFormat: AudioFileType = AudioFileType.AIFF
var nrtSampleFormat: SampleFormat = SampleFormat.Float
/**
* Picks and assigns a random free port for the server. This implies that
* the server will be running on the local machine.
*
* As a result, this method will change this config builder's `port` value.
* The caller must ensure that the `host` and `transport` fields have been
* decided on before calling this method. Later changes of either of these
* will render the result invalid.
*
* This method will fail with runtime exception if the host is not local.
*/
def pickPort() {
require( isLocal )
transport match {
case UDP =>
val tmp = new DatagramSocket()
port = tmp.getLocalPort
tmp.close()
case TCP =>
val tmp = new ServerSocket( 0 )
port = tmp.getLocalPort
tmp.close()
}
}
/**
* Checks if the currently set `host` is located on the local machine.
*/
def isLocal : Boolean = {
val hostAddr = InetAddress.getByName( host )
hostAddr.isLoopbackAddress || hostAddr.isSiteLocalAddress
}
def build : Config = new Config(
programPath, controlBusChannels, audioBusChannels, outputBusChannels, blockSize, sampleRate, audioBuffers,
maxNodes, maxSynthDefs, memorySize, wireBuffers, randomSeeds, loadSynthDefs, machPortName, verbosity,
plugInsPaths, restrictedPath, /* memoryLocking, */ host, port, transport, inputStreamsEnabled, outputStreamsEnabled,
deviceNames, deviceName, inputBusChannels, hardwareBlockSize, zeroConf, maxLogins, sessionPassword,
nrtCommandPath,
nrtInputPath, nrtOutputPath, nrtHeaderFormat, nrtSampleFormat )
}
@throws( classOf[ IOException ])
def boot: ServerConnection = boot()()
@throws( classOf[ IOException ])
def boot( name: String = "localhost", config: Config = Config().build,
clientConfig: Client.Config = Client.Config().build )
( listener: Model.Listener = Model.EmptyListener ) : ServerConnection = {
val sc = initBoot( name, config, clientConfig )
if( !(listener eq Model.EmptyListener) ) sc.addListener( listener )
sc.start()
sc
}
private def initBoot( name: String = "localhost", config: Config = Config().build,
clientConfig: Client.Config = Client.Config().build ) = {
val (addr, c) = prepareConnection( config, clientConfig )
//c.dump()
new BootingImpl( name, c, addr, config, clientConfig, true )
}
@throws( classOf[ IOException ])
def connect: ServerConnection = connect()()
@throws( classOf[ IOException ])
def connect( name: String = "localhost", config: Config = Config().build,
clientConfig: Client.Config = Client.Config().build )
( listener: Model.Listener = Model.EmptyListener ) : ServerConnection = {
val (addr, c) = prepareConnection( config, clientConfig )
val sc = new ConnectionImpl( name, c, addr, config, clientConfig, true )
if( !(listener eq Model.EmptyListener) ) sc.addListener( listener )
sc.start()
sc
}
def run( code: Server => Unit ) { run()( code )}
/**
* Utility method to test code quickly with a running server. This boots a
* server and executes the passed in code when the server is up. A shutdown
* hook is registered to make sure the server is destroyed when the VM exits.
*/
def run( config: Config = Config().build )( code: Server => Unit ) {
// val b = boot( config = config )
val sync = new AnyRef
var s : Server = null
val sc = initBoot( config = config )
sc.addListener {
case ServerConnection.Running( srv ) => sync.synchronized { s = srv }; code( srv )
}
Runtime.getRuntime.addShutdownHook( new Thread { override def run() { sync.synchronized {
if( s != null ) {
if( s.condition != Server.Offline ) s.quit()
} else sc.abort
}}})
sc.start()
}
// @throws( classOf[ IOException ])
// def dummy: Server = dummy()
/**
* Creates an unconnected server proxy. This may be useful for creating NRT command files.
* Any attempt to try to send messages to the server will fail.
*/
@throws( classOf[ IOException ])
def dummy( name: String = "dummy", config: Config = Config().build,
clientConfig: Client.Config = Client.Config().build ) : Server = {
val (addr, c) = prepareConnection( config, clientConfig )
new Server( name, c, addr, config, clientConfig )
}
@throws( classOf[ IOException ])
private def prepareConnection( config: Config, clientConfig: Client.Config ) : (InetSocketAddress, OSCClient) = {
val addr = new InetSocketAddress( config.host, config.port )
val clientAddr = clientConfig.addr getOrElse {
if( addr.getAddress.isLoopbackAddress )
new InetSocketAddress( "127.0.0.1", 0 ) else
new InetSocketAddress( InetAddress.getLocalHost, 0 )
}
val c = createClient( config.transport, addr, clientAddr )
(addr, c)
}
def allocPort( transport: Transport ) : Int = {
transport match {
case TCP => {
val ss = new ServerSocket( 0 )
try {
ss.getLocalPort
} finally {
ss.close()
}
}
case UDP => {
val ds = new DatagramSocket()
try {
ds.getLocalPort
} finally {
ds.close()
}
}
case x => sys.error( "Unsupported transport : " + x.name )
}
}
private def add( s: Server ) {
allSync.synchronized {
// allVar += s
if( default == null ) default = s
}
}
private def remove( s: Server ) {
allSync.synchronized {
// allVar -= s
if( default == s ) default = null
}
}
def printError( name: String, t: Throwable ) {
println( name + " : " )
t.printStackTrace()
}
implicit def defaultGroup( s: Server ) = s.defaultGroup
abstract sealed class Condition
case object Running extends Condition
// case object Booting extends Condition
case object Offline extends Condition
// private case object Terminating extends Condition
private case object NoPending extends Condition
case class Counts( c: osc.StatusReplyMessage )
private def createClient( transport: Transport.Net, serverAddr: InetSocketAddress,
clientAddr: InetSocketAddress ) : OSCClient = {
// val client = OSCClient( transport, 0, addr.getAddress.isLoopbackAddress, osc.ServerCodec )
//println( "transport = " + transport + " ; server = " + serverAddr + " ; client = " + clientAddr )
val client = transport match {
case UDP =>
val cfg = UDP.Config()
cfg.localSocketAddress = clientAddr
cfg.codec = osc.ServerCodec
cfg.bufferSize = 0x10000
UDP.Client( serverAddr, cfg )
case TCP =>
val cfg = TCP.Config()
cfg.codec = osc.ServerCodec
cfg.localSocketAddress = clientAddr
cfg.bufferSize = 0x10000
TCP.Client( serverAddr, cfg )
}
// client.connect()
client
}
// -------- internal class BootThread --------
private object ConnectionImplLike {
case object Ready
case object Abort
case object QueryServer
final case class AddListener( l: Model.Listener )
final case class RemoveListener( l: Model.Listener )
// case object Aborted
}
// XXX TODO : CLEAN UP THIS MESS
private trait ConnectionImplLike extends ServerConnection {
import ConnectionImplLike._
import ServerConnection.{ Running => SCRunning, _ }
val actor = new DaemonActor {
// var state: Condition = Connecting
def act() { react {
// dispatch( Connecting )
case Abort => abortHandler( None )
case Ready => loop {
if( connectionAlive ) {
// def retryConnect() {
// val tretry = System.currentTimeMillis + 500
// var looping = true
// loopWhile( looping ) { reactWithin( math.max( 0L, tretry - System.currentTimeMillis) ) {
// case TIMEOUT => looping = false
// case AddListener( l ) => actAddList( l )
// case RemoveListener( l )=> actRemoveList( l )
// case Abort => abortHandler( None )
// }}
// }
// try {
//println( "?? Connect")
// c.start
//println( "isConnected? " + c.isConnected + " ; isOpen? " + c.isOpen )
if( !c.isConnected ) c.connect()
//println( "!! Connect")
//c.dumpOSC()
c.action = p => this ! p
var tnotify = 0L
def snotify() {
tnotify = System.currentTimeMillis + 500
//println( ">>> NOT" )
//try {
c ! osc.ServerNotifyMessage( onOff = true )
//} catch {
// case n: PortUnreachableException => println( "caught : " + n )
//}
}
snotify()
loop { reactWithin( math.max( 0L, tnotify - System.currentTimeMillis) ) {
case TIMEOUT => snotify() // loop is retried
case AddListener( l ) => actAddList( l )
case RemoveListener( l )=> actRemoveList( l )
case Abort => abortHandler( None )
case Message( "/done", "/notify" ) => {
//println( "<<< NOT" )
var tstatus = 0L
def sstatus() {
tstatus = System.currentTimeMillis + 500
//println( ">>> STAT" )
//try {
c ! osc.StatusMessage
//} catch {
// case n: PortUnreachableException => println( "caught : " + n )
//}
}
sstatus()
loop { reactWithin( math.max( 0L, tstatus - System.currentTimeMillis) ) {
case TIMEOUT => sstatus() // loop is retried
case AddListener( l ) => actAddList( l )
case RemoveListener( l )=> actRemoveList( l )
case Abort => abortHandler( None )
case counts: osc.StatusReplyMessage => {
//println( "<<< STAT" )
val s = new Server( name, c, addr, config, clientConfig )
s.counts = counts
dispatch( Preparing( s ))
s.initTree()
dispatch( SCRunning( s ))
createAliveThread( s )
loop { react {
case QueryServer => reply( s )
case AddListener( l ) => actAddList( l ); actDispatch( l, SCRunning( s ))
case RemoveListener( l )=> actRemoveList( l )
case Abort => abortHandler( Some( s ))
case ServerConnection.Aborted => {
s.serverOffline()
dispatch( Aborted )
loop { react {
case AddListener( l ) => actAddList( l ); actDispatch( l, Aborted )
case RemoveListener( l )=> actRemoveList( l )
case Abort => reply ()
case QueryServer => reply( s )
}}
}
}}
}
}}
}
}}
// }
// catch {
// case _: ConnectException => retryConnect() // thrown when TCP server not available
// case _: PortUnreachableException => retryConnect() // thrown when server sets up UDP
// case e: ClosedChannelException =>
//println( "CAUGHT:" )
//e.printStackTrace( Console.out )
//println( "IS OPEN? " + c.isOpen() )
// retryConnect() // thrown when in TCP mode and socket not yet available
////println( "!= Connect")
// }
} else loop { react {
case Abort => reply ()
case _ =>
}}
}
}}
private def abortHandler( server: Option[ Server ]) {
handleAbort()
val from = sender
loop { react {
case ServerConnection.Aborted => {
server.foreach( _.serverOffline() )
dispatch( ServerConnection.Aborted )
from ! ()
}
case AddListener( l ) => actAddList( l )
case RemoveListener( l )=> actRemoveList( l )
case _ => // XXX ?
}}
}
}
private def actDispatch( l: Model.Listener, change: AnyRef ) {
try {
if( l.isDefinedAt( change )) l( change )
} catch {
case e: Throwable => e.printStackTrace() // catch, but print
}
}
private def actAddList( l: Model.Listener ) {
super.addListener( l )
}
private def actRemoveList( l: Model.Listener ) {
super.removeListener( l )
}
override def addListener( l: Model.Listener ) : Model.Listener = {
actor ! AddListener( l )
l
}
override def removeListener( l: Model.Listener ) : Model.Listener = {
actor ! RemoveListener( l )
l
}
// def start { actor ! Start }
lazy val server : Future[ Server ] = actor !! (QueryServer, { case s: Server => s })
lazy val abort : Future[ Unit ] = actor !! (Abort, { case _ => ()})
def handleAbort() : Unit
def connectionAlive : Boolean
def c : OSCClient
def clientConfig: Client.Config
def createAliveThread( s: Server ) : Unit
}
private class ConnectionImpl @throws( classOf[ IOException ])
( val name: String, val c: OSCClient, val addr: InetSocketAddress, val config: Config,
val clientConfig: Client.Config, aliveThread: Boolean )
extends ConnectionImplLike {
// import ConnectionImplLike._
def start() {
actor.start()
actor ! ConnectionImplLike.Ready
}
override def toString = "connect<" + name + ">"
def handleAbort() {}
def connectionAlive = true // XXX could add a timeout?
def createAliveThread( s: Server ) {
if( aliveThread ) s.startAliveThread( 1.0f, 0.25f, 40 ) // allow for a luxury 10 seconds absence
}
}
// private object BootingImpl {
// final case class Booted( open: Boolean )
// }
private class BootingImpl @throws( classOf[ IOException ])
( val name: String, val c: OSCClient, val addr: InetSocketAddress, val config: Config,
val clientConfig: Client.Config, aliveThread: Boolean )
extends ConnectionImplLike {
import ConnectionImplLike._
lazy val p = {
val processArgs = config.toRealtimeArgs
val directory = new File( config.programPath ).getParentFile
val pb = new ProcessBuilder( processArgs: _* )
.directory( directory )
.redirectErrorStream( true )
pb.start // throws IOException if command not found or not executable
}
lazy val processThread = new Thread {
override def run() { try {
p.waitFor()
} catch { case e: InterruptedException =>
p.destroy()
} finally {
println( "scsynth terminated (" + p.exitValue +")" )
actor ! ServerConnection.Aborted
}}
}
// @volatile private var isOpen = false
// @volatile private var isBooting = false
//
def start() {
val inReader = new BufferedReader( new InputStreamReader( p.getInputStream ))
val postThread = new Thread {
override def run() {
var isOpen = true
var isBooting = true
try {
while( isOpen && isBooting ) {
val line = inReader.readLine
isOpen = line != null
if( isOpen ) {
println( line )
// of course some sucker screwed it up and added another period in SC 3.4.4
// if( line == "SuperCollider 3 server ready." ) isBooting = false
// one more... this should allow for debug versions and supernova to be detected, too
if( line.startsWith( "Super" ) && line.contains( " ready" )) isBooting = false
}
}
} catch {
case e: Throwable => isOpen = false
}
actor ! (if( isOpen ) Ready else Abort)
while( isOpen ) {
val line = inReader.readLine
isOpen = line != null
if( isOpen ) println( line )
}
}
}
// ...and go
postThread.start()
processThread.start()
actor.start()
}
override def toString = "boot<" + name + ">"
def handleAbort() { processThread.interrupt() }
def connectionAlive = processThread.isAlive
def createAliveThread( s: Server ) {
// note that we optimistically assume that if we boot the server, it
// will not die (exhausting deathBounces). if it crashes, the boot
// thread's process will know anyway. this way we avoid stupid
// server offline notifications when using slow asynchronous commands
if( aliveThread ) s.startAliveThread( 1.0f, 0.25f, Int.MaxValue )
}
}
}
sealed trait ServerLike extends Model {
def name: String
def config: Server.Config
def addr: InetSocketAddress
}
object ServerConnection {
sealed abstract class Condition
// case object Connecting extends Condition
case class Preparing( server: Server ) extends Condition
case class Running( server: Server ) extends Condition
case object Aborted extends Condition
}
sealed trait ServerConnection extends ServerLike {
// def start : Unit
def server : Future[ Server ]
def abort : Future[ Unit ]
}
//abstract class Server extends Model {}
final class Server private( val name: String, c: OSCClient, val addr: InetSocketAddress, val config: Server.Config,
val clientConfig: Client.Config )
extends ServerLike {
server =>
import Server._
private var aliveThread: Option[StatusWatcher] = None
// private var bootThread: Option[BootThread] = None
private var countsVar = new osc.StatusReplyMessage( 0, 0, 0, 0, 0f, 0f, 0.0, 0.0 )
// private var collBootCompletion = Queue.empty[ (Server) => Unit ]
private val condSync = new AnyRef
private var conditionVar: Condition = Running // Offline
private var pendingCondition: Condition = NoPending
// private var bufferAllocatorVar: ContiguousBlockAllocator = null
// private val host = InetAddress.getByName( config.host.value )
// val addr = new InetSocketAddress( host, config.port.value )
val rootNode = new Group( this, 0 )
val defaultGroup = new Group( this, 1 )
val nodeMgr = new NodeManager( this )
val bufMgr = new BufferManager( this )
// var latency = 0.2f
// ---- constructor ----
OSCReceiverActor.start()
c.action = OSCReceiverActor.messageReceived
add( server )
def isLocal : Boolean = {
val host = addr.getAddress
host.isLoopbackAddress || host.isSiteLocalAddress
}
def isConnected = c.isConnected
def isRunning = condSync.synchronized { conditionVar == Running }
// def isBooting = condSync.synchronized { conditionVar == Booting }
def isOffline = condSync.synchronized { conditionVar == Offline }
// def bufferAllocator = bufferAllocatorVar
object nodes {
private val allocator = new NodeIDAllocator( clientConfig.clientID, clientConfig.nodeIDOffset )
def nextID() = allocator.alloc()
}
object busses {
private val controlAllocator = new ContiguousBlockAllocator( config.controlBusChannels )
private val audioAllocator = new ContiguousBlockAllocator( config.audioBusChannels, config.internalBusIndex )
def allocControl( numChannels: Int ) = controlAllocator.alloc( numChannels )
def allocAudio( numChannels: Int ) = audioAllocator.alloc( numChannels )
def freeControl( index: Int ) { controlAllocator.free( index )}
def freeAudio( index: Int ) { audioAllocator.free( index )}
}
object buffers {
private val allocator = new ContiguousBlockAllocator( config.audioBuffers )
def alloc( numChannels: Int ) = allocator.alloc( numChannels )
def free( index: Int ) { allocator.free( index )}
}
private object uniqueID {
private var id = 0
def nextID = this.synchronized { val res = id; id += 1; res }
}
def !( p: Packet ) { c ! p }
/**
* Sends out an OSC packet that generates some kind of reply, and
* returns immediately a `RevocableFuture` representing the parsed reply.
* This parsing is done by a handler which is registered.
* The handler is tested for each incoming OSC message (using its
* `isDefinedAt` method) and invoked and removed in case of a
* match. Note that the caller is responsible for timing out
* the handler after a reasonable time. To do this, the
* method `revoke` on the returned future must be called, which
* will silently unregister the handler.
*
* '''Warning''': It is crucial that the Future is awaited
* only within a dedicated actor thread. In particular you must
* be careful and aware of the fact that the handler is executed
* on the OSC receiver actor's body, and that you must not
* try to await the future from ''any'' handler function
* registered with OSC reception, because it would not be
* possible to pull the reply message of the OSC receiver's
* mailbox while the actor body blocks.
*
* @param p the packet to send out
* @param handler the handler to match against incoming messages
* or timeout
* @return the future representing the parsed reply, and providing
* a `revoke` method to issue a timeout.
*
* @see [[scala.actors.Futures]]
*/
def !![ A ]( p: Packet, handler: PartialFunction[ Message, A ]) : RevocableFuture[ A ] = {
val c = new Channel[ A ]( Actor.self )
val a = new FutureActor[ A ]( c ) {
val sync = new AnyRef
var revoked = false
var oh: Option[ osc.Handler ] = None
def body( res: SyncVar[ A ]) {
val futCh = new Channel[ A ]( Actor.self )
sync.synchronized { if( !revoked ) {
val h = new OSCInfHandler( handler, futCh )
oh = Some( h )
OSCReceiverActor.addHandler( h )
server ! p // only after addHandler!
}}
futCh.react { case r => res.set( r )}
}
def revoke() { sync.synchronized {
revoked = true
oh.foreach( OSCReceiverActor.removeHandler( _ ))
oh = None
}}
}
a.start()
// NOTE: race condition, addHandler might take longer than
// the /done, notify!
// this ! p
a
}
/**
* Sends out an OSC packet that generates some kind of reply, and
* returns immediately. It registers a handler to parse that reply.
* The handler is tested for each incoming OSC message (using its
* `isDefinedAt` method) and invoked and removed in case of a
* match. If the handler doesn't match in the given timeout period,
* it is invoked with message `TIMEOUT` and removed. If the handler
* wishes not to do anything particular in the case of a timeout,
* it simply should not add a case for `TIMEOUT`.
*
* @param timeOut the timeout in milliseconds
* @param p the packet to send out
* @param handler the handler to match against incoming messages
* or timeout
*
* @see [[scala.actors.TIMEOUT]]
*/
def !?( timeOut: Long, p: Packet, handler: PartialFunction[ Any, Unit ]) {
val a = new DaemonActor {
def act() {
val futCh = new Channel[ Any ]( Actor.self )
val oh = new OSCTimeOutHandler( handler, futCh )
OSCReceiverActor.addHandler( oh )
server ! p // only after addHandler!
futCh.reactWithin( timeOut ) {
case TIMEOUT => OSCReceiverActor.timeOutHandler( oh )
case r =>
}
}
}
a.start()
// NOTE: race condition, addHandler might take longer than
// the /done, notify!
// this ! p
a
}
def counts = countsVar
private[synth] def counts_=( newCounts: osc.StatusReplyMessage ) {
countsVar = newCounts
dispatch( Counts( newCounts ))
}
def sampleRate = counts.sampleRate
// def dumpTree { dumpTree( false )}
def dumpTree( controls: Boolean = false ) {
rootNode.dumpTree( controls )
}
def condition = condSync.synchronized { conditionVar }
private[synth] def condition_=( newCondition: Condition ) {
condSync.synchronized {
if( newCondition != conditionVar ) {
conditionVar = newCondition
if( newCondition == Offline ) {
pendingCondition = NoPending
serverLost()
}
// else if( newCondition == Running ) {
// if( pendingCondition == Booting ) {
// pendingCondition = NoPending
// collBootCompletion.foreach( action => try {
// action.apply( this )
// }
// catch { case e => e.printStackTrace() }
// )
// collBootCompletion = Queue.empty
// }
// }
dispatch( newCondition )
}
}
}
def startAliveThread( delay: Float = 0.25f, period: Float = 0.25f, deathBounces: Int = 25 ) {
condSync.synchronized {
if( aliveThread.isEmpty ) {
val statusWatcher = new StatusWatcher( delay, period, deathBounces )
aliveThread = Some( statusWatcher )
statusWatcher.start()
}
}
}
def stopAliveThread() {
condSync.synchronized {
aliveThread.foreach( _.stop() )
aliveThread = None
}
}
def queryCounts() {
this ! osc.StatusMessage
}
def syncMsg : osc.SyncMessage = syncMsg()
def syncMsg( id: Int = uniqueID.nextID ) = osc.SyncMessage( id )
def dumpOSC( mode: Dump = Dump.Text ) {
c.dumpIn( mode, filter = {
case m: osc.StatusReplyMessage => false
case _ => true
})
c.dumpOut( mode, filter = {
case osc.StatusMessage => false
case _ => true
})
}
private def serverLost() {
nodeMgr.clear()
bufMgr.clear()
OSCReceiverActor.clear()
}
private def serverOffline() {
condSync.synchronized {
// bootThread = None
stopAliveThread()
condition = Offline
}
}
def quit() {
this ! quitMsg
// cleanUpAfterQuit()
dispose()
}
def quitMsg = osc.ServerQuitMessage
// private def cleanUpAfterQuit() {
// try {
// condSync.synchronized {
// stopAliveThread()
// pendingCondition = Terminating
// }
// }
// catch { case e: IOException => printError( "Server.cleanUpAfterQuit", e )}
// }
private[synth] def addResponder( resp: osc.Responder ) {
OSCReceiverActor.addHandler( resp )
}
private[synth] def removeResponder( resp: osc.Responder ) {
OSCReceiverActor.removeHandler( resp )
}
private[synth] def initTree() {
nodeMgr.register( defaultGroup )
server ! defaultGroup.newMsg( rootNode, addToHead )
}
def dispose() {
condSync.synchronized {
serverOffline()
remove( this )
// c.dispose // = (msg: Message, sender: SocketAddress, time: Long) => ()
c.close()
OSCReceiverActor.dispose()
// c.dispose
}
}
override def toString = "<" + name + ">"
// -------- internal class StatusWatcher --------
private class StatusWatcher( delay: Float, period: Float, deathBounces: Int )
extends Runnable {
watcher =>
private var alive = deathBounces
private val delayMillis = (delay * 1000).toInt
private val periodMillis = (period * 1000).toInt
// private val timer = new SwingTimer( periodMillis, this )
private var timer: Option[ Timer ] = None
private var callServerContacted = true
private val sync = new AnyRef
// // ---- constructor ----
// timer.setInitialDelay( delayMillis )
def start() {
stop()
timer = {
val t = new Timer( "StatusWatcher", true )
t.schedule( new TimerTask {
def run() { watcher.run() } // invokeOnMainThread( watcher )
}, delayMillis, periodMillis )
Some( t )
}
}
def stop() {
// timer.stop
timer.foreach( t => {
t.cancel()
timer = None
})
}
def run() {
sync.synchronized {
alive -= 1
if( alive < 0 ) {
callServerContacted = true
condition = Offline
}
}
try {
queryCounts()
}
catch { case e: IOException => printError( "Server.status", e )}
}
def statusReply( msg: osc.StatusReplyMessage ) {
sync.synchronized {
alive = deathBounces
// note: put the counts before running
// because that way e.g. the sampleRate
// is instantly available
counts = msg
if( !isRunning && callServerContacted ) {
callServerContacted = false
// serverContacted
condition = Running
}
}
}
}
private object OSCReceiverActor extends DaemonActor {
private case object Clear
private case object Dispose
// private case class ReceivedMessage( msg: Message, sender: SocketAddress, time: Long )
private case class AddHandler( h: osc.Handler )
private case class RemoveHandler( h: osc.Handler )
private case class TimeOutHandler( h: OSCTimeOutHandler )
def clear() {
this ! Clear
}
def dispose() {
clear()
this ! Dispose
}
def addHandler( handler: osc.Handler ) {
this ! AddHandler( handler )
}
def removeHandler( handler: osc.Handler ) {
this ! RemoveHandler( handler )
}
def timeOutHandler( handler: OSCTimeOutHandler ) {
this ! TimeOutHandler( handler )
}
// ------------ OSCListener interface ------------
def messageReceived( p: Packet ) {
//if( msg.name == "/synced" ) println( "" + new java.aux.Date() + " : ! : " + msg )
this ! p
}
def act() {
var running = true
var handlers = Set.empty[ osc.Handler ]
// while( running )( receive { })
loopWhile( running )( react {
case msg: Message => debug( msg ) {
// case ReceivedMessage( msg, sender, time ) => debug( msg ) {
//if( msg.name == "/synced" ) println( "" + new java.aux.Date() + " : received : " + msg )
msg match {
case nodeMsg: osc.NodeChange => nodeMgr.nodeChange( nodeMsg )
case bufInfoMsg: osc.BufferInfoMessage => bufMgr.bufferInfo( bufInfoMsg )
case statusReplyMsg: osc.StatusReplyMessage => aliveThread.foreach( _.statusReply( statusReplyMsg ))
case _ =>
}
//if( msg.name == "/synced" ) println( "" + new java.aux.Date() + " : handlers" )
handlers.foreach( h => if( h.handle( msg )) handlers -= h )
}
case AddHandler( h ) => handlers += h
case RemoveHandler( h ) => if( handlers.contains( h )) { handlers -= h; h.removed() }
case TimeOutHandler( h )=> if( handlers.contains( h )) { handlers -= h; h.timedOut() }
case Clear => handlers.foreach( _.removed() ); handlers = Set.empty
case Dispose => running = false
case m => println( "Received illegal message " + m )
})
}
}
private def debug( msg: AnyRef )( code: => Unit ) {
val t1 = System.currentTimeMillis
try {
code
} catch {
case e: Throwable => println( "" + new java.util.Date() + " OOOPS : msg " + msg + " produced " + e )
}
val t2 = System.currentTimeMillis
if( (t2 - t1) > 2000 ) println( "" + new java.util.Date() + " WOW this took long (" + (t2-t1) + "): " + msg )
}
// -------- internal osc.Handler implementations --------
private class OSCInfHandler[ A ]( fun: PartialFunction[ Message, A ], ch: OutputChannel[ A ])
extends osc.Handler {
def handle( msg: Message ) : Boolean = {
val handled = fun.isDefinedAt( msg )
//if( msg.name == "/synced" ) println( "" + new java.aux.Date() + " : inf handled : " + msg + " ? " + handled )
if( handled ) try {
ch ! fun.apply( msg )
} catch { case e: Throwable => e.printStackTrace() }
handled
}
def removed() {}
}
private class OSCTimeOutHandler( fun: PartialFunction[ Any, Unit ], ch: OutputChannel[ Any ])
extends osc.Handler {
def handle( msg: Message ) : Boolean = {
val handled = fun.isDefinedAt( msg )
//if( msg.name == "/synced" ) println( "" + new java.aux.Date() + " : to handled : " + msg + " ? " + handled )
if( handled ) try {
ch ! fun.apply( msg )
} catch { case e: Throwable => e.printStackTrace() }
handled
}
def removed() {}
def timedOut() {
if( fun.isDefinedAt( TIMEOUT )) try {
fun.apply( TIMEOUT )
} catch { case e: Throwable => e.printStackTrace() }
}
}
}