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). [![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](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