defmodule SuperCollider do
@moduledoc """
This is the main `SuperCollider` module.
With this module, you can:
* start a `SuperCollider.SoundServer`, which is a GenServer used to communicate with scynth and recieve messages from it.
* issue commands
* access SuperColliders state.
This module aims to minimise the need to pass a pid around representing the active `SoundServer` by storing it's PID as a persisitient term under `:supercollider_soundserver`. Calling `SuperCollider.start()` will automatically store the pid of the SoundServer as a persistient term.
## Example
```
alias SuperCollider, as :SC
# Start the `SuperCollider.SoundServer` GenServer
SC.start() # this will return the PID of the SoundServer, however, you don't have to assign this to a variable as its stored as a persistient term used by the other functions in this module.
# Issue the verion command and get the response from SoundServer's state
SC.command(:version) # send the version commant to SuperCollider
SC.response(:version) # retrieves the version response from the SoundServer's state
# Play a sine wave UGen on node 600, with a frequency of 300
SC.command(:s_new, ["sine", 600, 1, 0, ["freq", 300]])
# Stop the sine wave by freeing node 600
SC.command(:n_free, 600)
```
## LiveBook tour
You can explore this library further in the [LiveBook demo](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fhaubie%2Fsupercollider%2Fblob%2Fmain%2Flivebook%2Fsupercollider_tour.livemd).
[](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fhaubie%2Fsupercollider%2Fblob%2Fmain%2Flivebook%2Fsupercollider_tour.livemd)
"""
alias SuperCollider.SoundServer
@doc """
Starts the `SuperCollider.SoundServer` GenServer and returns it's pid.
The pid for the `SuperCollider.SoundServer` is also stored as a persisitient term under `:supercollider_soundserver`. This is used by the `SuperCollider.state/0`, `SuperCollider.response/0`, `SuperCollider.response/1`, `SuperCollider.command/1` and `SuperCollider.command/2` functions so that they're passed the global SoundServer pid without having to specify it.
If you want to directly interact with a SoundServer through it's pid, see the equivalent functions under `SuperCollider.SoundServer`, such as `{:ok, pid} = SuperCollider.SoundServer.start_link()` or through `{:ok, pid} = GenServer.start_link(SoundServer, SoundServer.new())`.
## Options
Additionally, this function can take options:
- ip: the IP address of scserver. This defaults to '127.0.0.1'
- hostname: the hostname of the server. This defaults to 'localhost'.
- port: the port used to communicate with scserver. This defaults to 57110.
- socket: the UDP socket used to communicate with scserver, once the connection is open.
- type: the server type being used, accepts :scsynth (default) or :supernova (multicore)
Note if the hostname is set to `nil` it will try the IP address at `ip`.
## Examples
Start SuperCollider with defaults
```
SuperCollider.start()
```
Start SuperCollider with a supernova server
```
SuperCollider.start(type: :supernova)
```
This function starts the `SuperCollider.SoundServer` GenServer which will check if SuperCollider has booted on your system. If not, it will currently attempt to start scynth or supernova at the following locations:
- Mac: /Applications/SuperCollider.app/Contents/Resources/
- Linux: /usr/local/
- Windows: \\Program Files\\SuperCollider\\
"""
def start(opts \\ []) do
{:ok, pid} = GenServer.start_link(SoundServer, SoundServer.new(opts))
:persistent_term.put(:supercollider_soundserver, pid)
pid
end
@doc """
Get's the state of the global SoundServer (whos PID is stored as a persisitient term under `:supercollider_soundserver`).
## Example
```
SuperCollider.state()
## Returns the populated SoundServer struct
# %SuperCollider.SoundServer{
# ip: '127.0.0.1',
# hostname: 'localhost',
# port: 57110,
# socket: #Port<0.12>,
# type: :supernova,
# responses: %{}
# }
```
"""
def state() do
case :persistent_term.get(:supercollider_soundserver, nil) do
nil -> {:error, "Global SuperCollider.SoundServer pid not stored as a persistient term under :supercollider_soundserver"}
pid ->
SoundServer.state(pid)
end
end
@doc """
Get's any OSC responses stored in the state of the `SoundServer`.
Responses are stored in a Map. If there are no responses stored, an empty map is returned, e.g. `%{}`.
"""
def response() do
case :persistent_term.get(:supercollider_soundserver, nil) do
nil -> {:error, "Global SuperCollider.SoundServer pid not stored as a persistient term under :supercollider_soundserver"}
pid ->
SoundServer.state(pid)
|> Map.get(:responses)
end
end
@doc """
Get's a specific OSC response stored in the state of the server, by it's key. If no response is available for the specific key, `nil` is returned.
The following are currently stored responses:
* :version - the version information from a `SuperCollider.command(:version)` call
* :status - the last status message stored from a `SuperCollider.command(:status)` call
* :fail - the last fail message from a `SuperCollider.command(...)` call
## Example
For example, if a version request was made:
```
SuperCollider.command(:version)
```
To get the response held by the `SuperCollider.SoundServer` you'd call:
```
SuperCollider.response(:version)
```
This would return the version response in the following format:
```
[
{"Program name. May be \"scsynth\" or \"supernova\".", "scsynth"},
{"Major version number. Equivalent to sclang's Main.scVersionMajor.", 3},
{"Minor version number. Equivalent to sclang's Main.scVersionMinor.", 13},
{"Patch version name. Equivalent to the sclang code \".\" ++ Main.scVersionPatch ++ Main.scVersionTweak.",
".0"},
{"Git branch name.", "Version-3.13.0"},
{"First seven hex digits of the commit hash.", "3188503"}
]
```
"""
def response(key) do
case :persistent_term.get(:supercollider_soundserver, nil) do
nil -> {:error, "Global SuperCollider.SoundServer pid not stored as a persistient term under :supercollider_soundserver"}
pid ->
SoundServer.state(pid)
|> Map.get(:responses)
|> Map.get(key, nil)
end
end
@doc """
Returns the PID of the SoundServer stored as a persistent_term as :supercollider_soundserver.
"""
def pid, do: :persistent_term.get(:supercollider_soundserver, nil)
@doc """
Send a command to the default SoundServer.
This function accepts the following parameters:
* command, in a form of an atom representing SuperCollider commands (See: `SuperCollider.SoundServer.Command` for details)
## Examples
```
# Get the server's status
SuperCollider.command(:status) # Make the call
SuperCollider.response(:status) # Get the status response data
```
"""
def command(command_name) do
case :persistent_term.get(:supercollider_soundserver, nil) do
nil -> {:error, "Global SuperCollider.SoundServer pid not stored as a persistient term under :supercollider_soundserver"}
pid -> SoundServer.command(pid, command_name)
end
end
@doc """
Send a command to the default SoundServer with arguments.
This function accepts the following parameters:
* command, in a form of an atom representing SuperCollider commands (See: `SuperCollider.SoundServer.Command` for details)
* args, commands which take them. Multiple options can be provided as a list.
## Examples
```
# Send a command to play a basic 300Hz sinusoidal sound on node 100
SuperCollider.command(:s_new, ["sine", 100, 1, 0, ["freq", 300]])
# Stop the sound by freeing node 100
SuperCollider.command(:n_free, 100)
```
"""
def command(command_name, args) do
case :persistent_term.get(:supercollider_soundserver, nil) do
nil -> {:error, "Global SuperCollider.SoundServer pid not stored as a persistient term under :supercollider_soundserver"}
pid -> SoundServer.command(pid, command_name, args)
end
end
end