defmodule SuperCollider.SynthDef.ScFile do
@moduledoc """
A struct representing a .scsyndef file in Elixir and a module for parsing (decoding) and encoding (to binary) SuperCollider synthdef files.
Currently only version 2 files are supported.
As a struct, `%ScFile{}` contains the following:
- `type_id`: a string (`SCgf`) representing the SuperCollider file format
- `file_version`: currently set to 2 (version 2 is the only file format currently supported)
- `synth_defs_count`: an integer count of the number of synthdefs within the file.`nil` if empty
- `synth_defs`: a list of synth definitions. These will use the `%SynthDef{}` struct.
Key functions in this module include:
- `parse/1`: for parsing a .scsyndef file. This will read the file from disc and call the `decode/1` function.
- `decode/1`: for deconding a scsyndef binary into an `%ScFile{}` struct.
- `encode/1`: for encoding one or more `%SynthDef{}` into the scsyndef binary format.
## Example
```
alias SuperCollider.SynthDef.ScFile
# Parse the scsyndef file
sc_file = ScFile.parse("/supercollider/ambient.scsyndef")
# returns the parsed file as a `%ScFile{}` struct
```
See below for further examples.
"""
alias SuperCollider.SynthDef
alias SuperCollider.SynthDef.ScFile
@type_id "SCgf"
@file_version_2 2
defstruct type_id: @type_id, file_version: @file_version_2, synth_defs_count: nil, synth_defs: nil
@doc"""
Takes a a filename as as single parameter, which is a the filename (and path) of the .scsyndef file to parse. This currently parses SuperCollider **version 2** file format only.
Returns the populated `%ScFile{}` struct.
You can access the individual synth definitions via the `:synth_def` key on the struct.
## Example
```
alias SuperCollider.SynthDef.ScFile
# Parse the scsyndef file
sc_file = ScFile.parse("/supercollider/ambient.scsyndef")
```
This returns the parsed file as a struct:
```
%SuperCollider.SynthDef.ScFile{
type_id: "SCgf",
file_version: 2,
synth_defs_count: 1,
synth_defs: [
# ... truncated, see below for example contents of the synth_defs key
],
varient_count: 0,
varient_specs_list: []
}
]
}
```
You can access the list of synth definitions using the synth_def key:
```
sc_file.synth_defs
```
Which will return
```
[
%SuperCollider.SynthDef{
name: "ambient",
constant_values_list: [0.2],
parameter_values_list: [0.0],
parameter_names_list: [%{parameter_index: 0, parameter_name: "out"}],
ugen_specs_list: [
%SuperCollider.SynthDef.UGen{
class_name: "Control",
calculation_rate: 1,
special_index: 0,
input_specs_list: [],
output_specs_list: [%{_enum_count: 0, calculation_rate: 1}]
},
%SuperCollider.SynthDef.UGen{
class_name: "BrownNoise",
calculation_rate: 2,
special_index: 0,
input_specs_list: [],
output_specs_list: [%{_enum_count: 0, calculation_rate: 2}]
},
%SuperCollider.SynthDef.UGen{
class_name: "BinaryOpUGen",
calculation_rate: 2,
special_index: 2,
input_specs_list: [
%{index: 1, output_index: 0, type: :ugen},
%{index: 0, type: :constant}
],
output_specs_list: [%{_enum_count: 0, calculation_rate: 2}]
},
%SuperCollider.SynthDef.UGen{
class_name: "Out",
calculation_rate: 2,
special_index: 0,
input_specs_list: [
%{index: 0, output_index: 0, type: :ugen},
%{index: 2, output_index: 0, type: :ugen}
],
output_specs_list: []
}
],
varient_specs_list: []
}
]
```
"""
# file = "/Users/haubie/Development/supercollider_livebook/ambient.scsyndef"
# file = "/Users/haubie/Development/supercollider_livebook/pink-ambient.scsyndef"
# file = "/Users/haubie/Development/supercollider_livebook/hoover.scsyndef"
# file = "/Users/haubie/Development/supercollider_livebook/closedhat.scsyndef"
def parse(filename) do
# Parse file header
File.read!(filename) |> decode()
end
@doc """
Decodes a scsyndef binary into an `%ScFile{}` struct.
## Example
Read a .scsyndef file from disc and decode it:
```
alias SuperCollider.SynthDef.ScFile
filename = "/supercollider/closedhat.scsyndef"
sc_file =
File.read!(filename)
|> ScFile.decode()
```
Note: If decoding directly from a file, you can use the `ScFile.parse(filename)` instead.
"""
def decode(binary) do
# Parse file header
case binary |> parse_header() do
{:error, _reason}=error -> error
{sc_file_struct, binary_data} ->
# Parse each synthdef
synth_defs = parse_synthdef(binary_data, [], sc_file_struct.synth_defs_count)
# Return the populated ScFile struct
%ScFile{sc_file_struct | synth_defs: synth_defs}
end
end
@doc """
Encodes an `%ScFile{}` or `%SynthDef{}` structs into SuperCollider's scsyndef binary format.
This can be used to either send as binary data to to scynth or supernova via the `:d_recv` command, or write as a file to disc .scsyndef file.
Takes either of the following as the first parameter:
- an `%ScFile{}` and encodes it into a new scsyndef binary
- single `%SynthDef{}` and encodes it into a new scsyndef binary (converting it to a `%ScFile{}` first)
- list of `%SynthDef{}` and encodes them into a new scsyndef binary (converting it to a `%ScFile{}` first).
"""
def encode(synthdefs) when is_struct(synthdefs, SuperCollider.SynthDef.ScFile), do: encode(synthdefs.synth_defs)
def encode(synthdefs) when is_list(synthdefs) do
num_synth_defs = length(synthdefs)
encode_header(num_synth_defs) <> SynthDef.encode(synthdefs)
end
def encode(synthdef) do
encode_header(1) <> SynthDef.encode(synthdef)
end
# The header consists of:
# * 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).
defp parse_header(bin_data) do
<<
file_type_id::binary-size(4),
file_version::big-signed-32,
num_synth_defs::big-signed-16,
rest::binary
>> = bin_data
if file_version == @file_version_2 do
{
%ScFile{type_id: file_type_id, file_version: file_version, synth_defs_count: num_synth_defs},
rest
}
else
{:error, "Incompatible file version. Only synthdef v2 files are supported."}
end
end
defp encode_header(num_synth_defs) do
<<
@type_id::binary,
@file_version_2::big-signed-32,
num_synth_defs::big-signed-16
>>
end
# The synthdef is the main data structure of the scsyndef file.
# It consists of:
# * name
# * list of:
# * constants
# * parameter values
# * parameter names
# * UGen specs
# * varient specs
# See the `SuperCollider.SynthDef` module for details.
defp parse_synthdef(_binary_data, acc, 0) do
acc
|> Enum.reverse()
end
defp parse_synthdef(binary_data, acc, num) when num > 0 do
{synthdef_struct, data} = SynthDef.decode(binary_data)
parse_synthdef(data, [synthdef_struct] ++ acc, num-1)
end
end