lib/supercollider/synthdef/ugen.ex

defmodule SuperCollider.SynthDef.UGen do
  @moduledoc """
  UGens represent calculations with signals. They are the basic building blocks of synth definitions on the server, and are used to generate or process both audio and control signals.

  They're made up of:
  * class_name - Examples include `SinOsc`, `Out`, and `Control`.
  * rate - The rate at which the UGen computes values. There are three rates numbered 0, 1, 2.
  * special_index - This value is used by some unit generators for a special purpose. For example, UnaryOpUGen and BinaryOpUGen use it to indicate which operator to perform. If not used it should be set to zero.
  * inputs - The inputs to this unit generator
  * outputs - The list of outputs of this unit generator. Each element in the list is the `rate` of the output, using the same number as the `rate` field of this struct.
  """

  alias SuperCollider.SynthDef
  alias SuperCollider.SynthDef.UGen
  alias SuperCollider.SynthDef.Parser
  alias SuperCollider.SynthDef.Encoder

  defstruct ~w[
    class_name

    calculation_rate
    special_index

    inputs_count input_specs_list

    outputs_count output_specs_list
  ]a

  @doc"""
  The parse function is used as part deconstructing UGen binary data in SuperCollider scsyndef v2 files.

  It is not usually accessed directly, but is called via `SuperCollider.SynthDef.ScFile.parse(filename)`.
  """
  def parse({synth_def_struct, binary_data}) do
    {num_ugens, rest_bin_data} = Parser.parse_integer_32(binary_data)
    {ugen_specs_list, rem_binary} = parse_ugens(rest_bin_data, num_ugens)

    {
      %SynthDef{synth_def_struct | ugen_count: num_ugens, ugen_specs_list: ugen_specs_list},
      rem_binary
    }
  end


  @doc """
  Encodes UGens into SuperCollider's binary format.

  This function is not usually called directly, but is automatically called as part of `SuperCollider.SynthDef.ScFile.encode(synthdef)`.
  """
  def encode(ugen_count, ugen_specs_list) do

    specs =
      ugen_specs_list
      |> Enum.map(fn ugen ->

        ugen_header =
          <<
            String.length(ugen.class_name)::big-integer-8,
            ugen.class_name::binary,
            ugen.calculation_rate::big-integer-8,
            ugen.inputs_count::big-integer-32,
            ugen.outputs_count::big-integer-32,
            ugen.special_index::big-integer-16
          >>

        ugen_input_specs =
          ugen.input_specs_list
          |> Enum.map(fn spec -> encode_input_spec(spec) end)
          |> Enum.join(<<>>)

        ugen_output_specs =
          ugen.output_specs_list
          |> Enum.map(fn spec ->
            <<spec.calculation_rate::big-integer-8>>
          end)
          |> Enum.join(<<>>)

        ugen_header <> ugen_input_specs <> ugen_output_specs
      end)
      |> List.flatten()
      |> Enum.join(<<>>)

      Encoder.write_32(ugen_count) <> specs
  end

  defp encode_input_spec(%{type: :constant, index: index_of_ugen_OR_index_of_output_gen}) do
    <<
      (-1)::signed-big-integer-32,
      index_of_ugen_OR_index_of_output_gen::big-integer-32
    >>
  end

  defp encode_input_spec(%{type: :ugen, index: ugen_index_or_constant_flag, output_index: index_of_ugen_OR_index_of_output_gen}) do
    <<
      ugen_index_or_constant_flag::signed-big-integer-32,
      index_of_ugen_OR_index_of_output_gen::big-integer-32
    >>
  end

  defp parse_ugens(binary, number) do
    parse_ugens(binary, number, 0, [])
  end

  defp parse_ugens(binary, number, count, acc) when count < number do
    <<
      ugen_class_name_length::big-integer-8,
      ugen_class_name::binary-size(ugen_class_name_length),
      calculation_rate::big-integer-8,
      num_inputs::big-integer-32,
      num_outputs::big-integer-32,
      special_index::big-integer-16,
      binary_ugen_rest::binary
    >> = binary

    {input_specs, binary_input_specs} = parse_input_spec(binary_ugen_rest, num_inputs)
    {output_specs, binary_output_specs} = parse_output_spec(binary_input_specs, num_outputs)

    ugen = [
      %UGen{
        class_name: ugen_class_name,
        calculation_rate: calculation_rate,
        inputs_count: num_inputs,
        outputs_count: num_outputs,
        special_index: special_index,
        input_specs_list: input_specs,
        output_specs_list: output_specs
      }
    ]

    parse_ugens(binary_output_specs, number, count + 1, ugen ++ acc)
  end

  defp parse_ugens(binary, _number, _count, acc) do
    {acc |> Enum.reverse(), binary}
  end


  defp parse_input_spec(binary, number) do
    parse_input_spec(binary, number, 0, [])
  end

  defp parse_input_spec(binary, number, count, acc) when count < number do
    <<
      ugen_index_or_constant_flag::signed-big-integer-32,
      index_of_ugen_OR_index_of_output_gen::big-integer-32,
      rest::binary
    >> = binary

    input_spec =
      case ugen_index_or_constant_flag do
        -1 ->
          # CONSTANT IF -1
            %{
              _enum_count: count,
              type: :constant,
              index: index_of_ugen_OR_index_of_output_gen,
            }

        _ -> # OTHERWISE ITS A UGEN
            %{
              _enum_count: count,
              type: :ugen,
              index: ugen_index_or_constant_flag,
              output_index: index_of_ugen_OR_index_of_output_gen
            }
      end

    parse_input_spec(rest, number, count + 1, [input_spec] ++ acc)
  end

  defp parse_input_spec(binary, _number, _count, acc) do
    {acc |> Enum.reverse(), binary}
  end

  defp parse_output_spec(binary, number) do
    parse_output_spec(binary, number, 0, [])
  end

  defp parse_output_spec(binary, number, count, acc) when count < number do
    <<output_calc_rate::big-integer-8, rest::binary>> = binary
    output_spec = [%{_enum_count: count, calculation_rate: output_calc_rate}]
    parse_output_spec(rest, number, count + 1, output_spec ++ acc)
  end

  defp parse_output_spec(binary, _number, _count, acc) do
    {acc |> Enum.reverse(), binary}
  end

end