All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.sciss.synth.ugen.HelperElements.scala Maven / Gradle / Ivy

/*
 *  HelperElements.scala
 *  (ScalaCollider)
 *
 *  Copyright (c) 2008-2019 Hanns Holger Rutz. All rights reserved.
 *
 *  This software is published under the GNU Lesser General Public License v2.1+
 *
 *
 *  For further information, please contact Hanns Holger Rutz at
 *  [email protected]
 */

package de.sciss.synth
package ugen

import de.sciss.synth.Ops.stringToControl
import de.sciss.synth.UGenSource._

import scala.annotation.{switch, tailrec}

/** A graph element that flattens the channels from a nested multi-channel structure.
  *
  * @param elem the element to flatten
  */
final case class Flatten(elem: GE) extends GE.Lazy {
  def rate: MaybeRate = elem.rate

  override def toString = s"$elem.flatten"

  protected def makeUGens: UGenInLike = UGenInGroup(elem.expand.flatOutputs)
}

/** A graph element that mixes the channels of a signal together.
  * It works like the sclang counterpart.
  *
  * The `Mix` companion object contains various useful mixing idioms:
  *
  * - `Mix.tabulate(n: Int)(fun: Int => GE)`: corresponds to `Seq.tabulate`
  *   and to `Array.fill` in sclang.
  * - `Mix.fill(n: Int)(thunk: => GE)`: corresponds to `Seq.fill`.
  * - `Mix.seq(elems: GE*)`: A shortcut for `Mix(GESeq(elems: _*))`.
  *
  * A separate graph element is `Mix.mono`.
  * `Mix.mono(elem: GE)` flattens all channels of
  * the input element before summing them, guaranteeing that the result is monophonic.
  *
  * Finally, `Mix.fold` is an idiom that not actually adds elements,
  * but recursively folds them. Thus,
  * `Mix.fold(elem: GE, n: Int)(fun: GE => GE)` is equivalent to
  * {{{
  * (1 to n).foldLeft(elem) { (res, _) => fun(res) }
  * }}}
  *
  * `Mix.fold` is often used in the SuperCollider examples to apply a filtering
  * process such as reverberation several times. For cases where the iteration
  * index is needed, the full form as shown above can be used instead.
  *
  * ===Examples===
  *
  * {{{
  * // non-nested multi-channel signal reduced to mono (1)
  * play {
  *   Mix(SinOsc.ar(440 :: 660 :: Nil)) * 0.2 // --> SinOsc.ar(440) + SinOsc.ar(660)
  * }
  * }}}
  *
  * {{{
  * // non-nested multi-channel signal reduced to mono (2)
  * play {
  *   Mix(Pan2.ar(SinOsc.ar)) * 0.2 // --> left + right
  * }
  * }}}
  *
  * {{{
  * // mix inner channels
  * play {
  *   // --> [left(440) + left(660), right(440) + right(660)]
  *   Mix(Pan2.ar(SinOsc.ar(440 :: 660 :: Nil))) * 0.2
  * }
  * }}}
  *
  * {{{
  * // enforce monophonic mix
  * play {
  *   // --> left(440) + left(660) + right(440) + right(660)
  *   Mix.mono(Pan2.ar(SinOsc.ar(440 :: 660 :: Nil))) * 0.2
  * }
  * }}}
  *
  * {{{
  * // combine Mix(), Mix.fill(), Mix.fold()
  * // from original SC examples: reverberated sine percussion
  * play {
  *   val d = 6    // number of percolators
  *   val c = 5    // number of comb delays
  *   val a = 4    // number of allpass delays
  *
  *   // sine percolation sound :
  *   val s = Mix.fill(d) { Resonz.ar(Dust.ar(2.0 / d) * 50, Rand(200, 3200), 0.003) }
  *
  *   // reverb pre-delay time :
  *   val z = DelayN.ar(s, 0.048)
  *
  *   // 'c' length modulated comb delays in parallel :
  *   val y = Mix(CombL.ar(z, 0.1, LFNoise1.kr(Seq.fill(c)(Rand(0, 0.1))).madd(0.04, 0.05), 15))
  *
  *   // chain of 'a' allpass delays on each of two channels (2 times 'a' total) :
  *   val x = Mix.fold(y, a) { in =>
  *     AllpassN.ar(in, 0.050, Seq(Rand(0, 0.050), Rand(0, 0.050)), 1)
  *   }
  *
  *   // add original sound to reverb and play it :
  *   s + 0.2 * x
  * }
  * }}}
  *
  * {{{
  * // Mix.tabulate usage
  * // from original SC examples: harmonic swimming
  * play {
  *   val f = 50       // fundamental frequency
  *   val p = 20       // number of partials per channel
  *   val offset = Line.kr(0, -0.02, 60, doneAction = freeSelf) // causes sound to separate and fade
  *   Mix.tabulate(p) { i =>
  *     FSinOsc.ar(f * (i+1)) * // freq of partial
  *       LFNoise1.kr(Seq(Rand(2, 10), Rand(2, 10)))  // amplitude rate
  *       .madd(
  *         0.02,     // amplitude scale
  *         offset    // amplitude offset
  *       ).max(0)    // clip negative amplitudes to zero
  *   }
  * }
  * }}}
  *
  * @see [[de.sciss.synth.ugen.Reduce$ Reduce]]
  * @see [[de.sciss.synth.ugen.BinaryOpUGen$ BinaryOpUGen]]
  */
object Mix {
  /** A mixing idiom that corresponds to `Seq.tabulate` and to `Array.fill` in sclang. */
  def tabulate(n: Int)(fun: Int => GE): GE = Mix(GESeq(Vector.tabulate(n)(i => fun(i))))

  /** A mixing idiom that corresponds to `Seq.fill`. */
  def fill(n: Int)(thunk: => GE): GE = Mix(GESeq(Vector.fill(n)(thunk)))

  /** A shortcut for `Mix(GESeq(elems: _*))`. */
  def seq(elems: GE*): GE = Mix(GESeq(elems.toIndexedSeq))

  /** A special mix that flattens all channels of
    * the input element before summing them, guaranteeing that result is monophonic.
    */
  def mono(elem: GE): GE = Mono(elem)

  /** A mixing idiom that is not actually adding elements, but recursively folding them.
    *
    * Calling this method is equivalent to
    * {{{
    * (1 to n).foldLeft(elem) { (res, _) => fun(res) }
    * }}}
    *
    * It is often used in the SuperCollider examples to apply a filtering process such as reverberation
    * several times. For cases where the iteration index is needed, the full form as shown above
    * can be used instead.
    *
    * @param elem the input element
    * @param n    the number of iterations
    * @param fun  a function that is recursively applied to produce the output
    */
  def fold(elem: GE, n: Int)(fun: GE => GE): GE = (1 to n).foldLeft(elem) { (res, _) => fun(res) }

  final case class Mono(elem: GE) extends GE.Lazy {
    def numOutputs  = 1
    def rate: MaybeRate = elem.rate
    override def productPrefix = s"Mix$$Mono"

    override def toString = s"$productPrefix($elem)"

    protected def makeUGens: UGenInLike = {
      val flat = elem.expand.flatOutputs
      Mix.makeUGen(flat)
    }
  }

  @tailrec private def makeUGen(args: Vec[UGenIn]): UGenInLike = {
    val sz = args.size
    if (sz == 0) UGenInGroup.empty else if (sz <= 4) make1(args) else {
      val mod = sz % 4
      if (mod == 0) {
        makeUGen(args.grouped(4).map(make1).toIndexedSeq)
      } else {
        // we keep the 1 to 3 tail elements, because
        // then the nested `makeUGen` call may again
        // call `make1` with the `Sum4` instances included.
        // E.g., if args.size is 6, we get `Sum3(Sum4(_,_,_,_),_,_)`,
        // whereas we would get `Plus(Plus(Sum4(_,_,_,_),_),_)`
        // if we just used the mod-0 branch.
        val (init, tail) = args.splitAt(sz - mod)
        val quad = init.grouped(4).map(make1).toIndexedSeq
        makeUGen(quad ++ tail)
      }
    }
  }

  private def make1(args: Vec[UGenIn]): UGenIn = {
    import BinaryOpUGen.Plus
    val sz = args.size
    (sz: @switch) match {
      case 1 => args.head
      case 2 => Plus.make1(args(0), args(1))
      case 3 => Sum3.make1(args)
      case 4 => Sum4.make1(args)
    }
  }
}

/** A graph element that mixes the channels of a signal together.
  * It works like the sclang counterpart.
  *
  * The `Mix` companion object contains various useful mixing idioms:
  *
  * - `Mix.tabulate(n: Int)(fun: Int => GE)`: corresponds to `Seq.tabulate`
  *   and to `Array.fill` in sclang.
  * - `Mix.fill(n: Int)(thunk: => GE)`: corresponds to `Seq.fill`.
  * - `Mix.seq(elems: GE*)`: A shortcut for `Mix(GESeq(elems: _*))`.
  *
  * A separate graph element is `Mix.mono`.
  * `Mix.mono(elem: GE)` flattens all channels of
  * the input element before summing them, guaranteeing that the result is monophonic.
  *
  * Finally, `Mix.fold` is an idiom that not actually adds elements,
  * but recursively folds them. Thus,
  * `Mix.fold(elem: GE, n: Int)(fun: GE => GE)` is equivalent to
  * {{{
  * (1 to n).foldLeft(elem) { (res, _) => fun(res) }
  * }}}
  *
  * `Mix.fold` is often used in the SuperCollider examples to apply a filtering
  * process such as reverberation several times. For cases where the iteration
  * index is needed, the full form as shown above can be used instead.
  *
  * @param  elem  the graph element whose channels to mix together
  *
  * @see [[de.sciss.synth.ugen.Reduce$ Reduce]]
  * @see [[de.sciss.synth.ugen.BinaryOpUGen$ BinaryOpUGen]]
  */
final case class Mix(elem: GE) extends UGenSource.SingleOut {  // XXX TODO: should not be UGenSource

  def rate: MaybeRate = elem.rate

  protected def makeUGens: UGenInLike = unwrap(this, elem.expand.outputs)

  protected def makeUGen(args: Vec[UGenIn]): UGenInLike = Mix.makeUGen(args)

  override def toString: String = elem match {
    case GESeq(elems) => elems.mkString(s"$productPrefix.seq(", ", ", ")")
    case _            => s"$productPrefix($elem)"
  }
}

/** A graph element that interleaves a number of (multi-channel) input signals.
  * For example, if two stereo-signals `a` and `b` are zipped, the output will be a four-channel
  * signal corresponding to `[ a out 0, b out 0, a out 1, b out 1 ]`. If the input signals
  * have different numbers of channels, the minimum number of channels is used.
  *
  * ===Examples===
  *
  * {{{
  * // peak and RMS metering
  * val x = play {
  *   val sig   = PhysicalIn.ar(0 to 1)  // stereo input
  *   val tr    = Impulse.kr(5)
  *   val peak  = Peak.kr(sig, tr)
  *   val rms   = A2K.kr(Lag.ar(sig.squared, 0.1))
  *   SendReply.kr(tr, Zip(peak, rms), "/meter")
  * }
  *
  * val r = message.Responder.add(x.server) {
  *   case osc.Message("/meter", x.id, _, peakL: Float, rmsL: Float, peakR: Float, rmsR: Float) =>
  *     println(f"peak-left \$peakL%g, rms-left \$rmsL%g, peak-right \$peakR%g, rms-right \$rmsR%g")
  *
  * x.free(); r.remove()
  * }}}
  *
  * @param elems  the signals to interleave in a multi-channel output signal
  */
final case class Zip(elems: GE*) extends GE.Lazy {
  def rate: MaybeRate = MaybeRate.reduce(elems.map(_.rate): _*)

  protected def makeUGens: UGenInLike = {
    val exp   = elems.map(_.expand)
    val sz    = exp.map(_.outputs.size)
    val minSz = sz.min
    UGenInGroup((0 until minSz).flatMap(i => exp.map(_.unwrap(i))))
  }
}

object Reduce {
  import BinaryOpUGen.{BitAnd, BitOr, BitXor, Max, Min, Plus, Times}
  /** Same result as `Mix( _ )` */
  def +  (elem: GE): Reduce = apply(elem, Plus  )
  def *  (elem: GE): Reduce = apply(elem, Times )
  //   def all_sig_==( elem: GE ) = ...
  //   def all_sig_!=( elem: GE ) = ...
  def min(elem: GE): Reduce = apply(elem, Min   )
  def max(elem: GE): Reduce = apply(elem, Max   )
  def &  (elem: GE): Reduce = apply(elem, BitAnd)
  def |  (elem: GE): Reduce = apply(elem, BitOr )
  def ^  (elem: GE): Reduce = apply(elem, BitXor)
}

final case class Reduce(elem: GE, op: BinaryOpUGen.Op) extends UGenSource.SingleOut {
  // XXX TODO: should not be UGenSource

  def rate: MaybeRate = elem.rate

  protected def makeUGens: UGenInLike = unwrap(this, elem.expand.outputs)

  protected def makeUGen(args: Vec[UGenIn]): UGenInLike = args match {
    case head +: tail => tail.foldLeft(head)(op.make1)
    case _            => UGenInGroup.empty
  }
}

/** An element which writes an input signal to a bus, optionally applying a short fade-in.
  * This is automatically added when using the `play { ... }` syntax. If the fade time is
  * given, an envelope is added with a control named `"gate"` which can be used to release
  * the synth. The bus is given by a control named `"out"` and defaults to zero.
  */
object WrapOut {
  private def makeFadeEnv(fadeTime: Double): UGenIn = {
    val cFadeTime = "fadeTime".kr(fadeTime)
    val cGate     = "gate".kr(1f)
    val startVal  = cFadeTime <= 0f
    val env       = Env(startVal, List(Env.Segment(1, 1, Curve.parametric(-4)), Env.Segment(1, 0, Curve.sine)), 1)
    val res       = EnvGen.kr(env, gate = cGate, timeScale = cFadeTime, doneAction = freeSelf)
    res.expand.flatOutputs.head
  }
}

// XXX TODO: This should not be a UGenSource.ZeroOut but just a Lazy.Expander[Unit] !
/** An element which writes an input signal to a bus, optionally applying a short fade-in.
  * This is automatically added when using the `play { ... }` syntax. If the fade time is
  * given, an envelope is added with a control named `"gate"` which can be used to release
  * the synth. The bus is given by a control named `"out"` and defaults to zero.
  *
  * @param  in        the signal to play to the default output
  * @param  fadeTime  the fade in time; use a negative number for no fading
  */
final case class WrapOut(in: GE, fadeTime: Double = 0.02) extends UGenSource.ZeroOut with WritesBus {
  import WrapOut._

  protected def makeUGens: Unit = unwrap(this, in.expand.outputs)

  protected def makeUGen(ins: Vec[UGenIn]): Unit = {
    if (ins.isEmpty) return
    val rate = ins.map(_.rate).max
    if ((rate == audio) || (rate == control)) {
      val ins3 = if (fadeTime >= 0) {
        val env = makeFadeEnv(fadeTime)
        ins.map(BinaryOpUGen.Times.make1(_, env))
      } else {
        ins
      }
      val cOut = "out".kr(0f)
      Out.ar(cOut, ins3)
    }
  }
}

/** A graph element that spreads a sequence of input channels across a ring of output channels.
  * This works by feeding each input channel through a dedicated `PanAz` UGen, and mixing the
  * results together.
  *
  * The panning position of each input channel with index `ch` is calculated by the formula:
  * {{{
  * val pf = 2.0 / (num-in-channels - 1) * (num-out-channels - 1) / num-out-channels
  * ch * pf + center
  * }}}
  *
  * @see  [[de.sciss.synth.ugen.PanAz$ PanAz]]
  */
object SplayAz {
  /** @param numChannels  the number of output channels
    * @param in           the input signal
    * @param spread       the spacing between input channels with respect to the output panning
    * @param center       the position of the first channel (see `PanAz`)
    * @param level        a global gain factor (see `PanAz`)
    * @param width        the `width` parameter for each `PanAz`
    * @param orient       the `orient` parameter for each `PanAz`
    */
  def ar(numChannels: Int, in: GE, spread: GE = 1f, center: GE = 0f, level: GE = 1f,
         width: GE = 2f, orient: GE = 0f): SplayAz =
    apply(audio, numChannels, in, spread, center, level, width, orient)
}
/** A graph element that spreads a sequence of input channels across a ring of output channels.
  * This works by feeding each input channel through a dedicated `PanAz` UGen, and mixing the
  * results together.
  *
  * The panning position of each input channel with index `ch` is calculated by the formula:
  * {{{
  * val pf = 2.0 / (num-in-channels - 1) * (num-out-channels - 1) / num-out-channels
  * ch * pf + center
  * }}}
  *
  * @param numChannels  the number of output channels
  * @param in           the input signal
  * @param spread       the spacing between input channels with respect to the output panning
  * @param center       the position of the first channel (see `PanAz`)
  * @param level        a global gain factor (see `PanAz`)
  * @param width        the `width` parameter for each `PanAz`
  * @param orient       the `orient` parameter for each `PanAz`
  *
  * @see  [[de.sciss.synth.ugen.PanAz$ PanAz]]
  */
final case class SplayAz(rate: Rate, numChannels: Int, in: GE, spread: GE, center: GE, level: GE, width: GE, orient: GE)
  extends GE.Lazy {

  def numOutputs: Int = numChannels

  protected def makeUGens: UGenInLike = {
    val _in     = in.expand
    val numIn   = _in.outputs.size
    // last channel must have position (2 * i / (numIn - 1) * (numOut - 1) / numOut)
    val pf  = if (numIn < 2 || numOutputs == 0) 0.0 else 2.0 / (numIn - 1) * (numOutputs - 1) / numOutputs
    val pos = Seq.tabulate(numIn)(center + _ * pf)
    val mix = Mix(PanAz(rate, numChannels, _in, pos, level, width, orient))
    mix
  }
}

/** A graph element which maps a linear range to another linear range.
  * The equivalent formula is `(in - srcLo) / (srcHi - srcLo) * (dstHi - dstLo) + dstLo`.
  *
  * '''Note''': No clipping is performed. If the input signal exceeds the input range,
  * the output will also exceed its range.
  *
  * ===Examples===
  *
  * {{{
  * // oscillator to frequency range
  * play {
  *   val mod = SinOsc.kr(Line.kr(1, 10, 10))
  *   SinOsc.ar(LinLin(mod, -1, 1, 100, 900)) * 0.1
  * }
  * }}}
  *
  * @param in              The input signal to convert.
  * @param srcLo           The lower limit of input range.
  * @param srcHi           The upper limit of input range.
  * @param dstLo           The lower limit of output range.
  * @param dstHi           The upper limit of output range.
  *
  * @see [[de.sciss.synth.ugen.LinExp$ LinExp]]
  * @see [[de.sciss.synth.ugen.Clip$ Clip]]
  * @see [[de.sciss.synth.ugen.MulAdd MulAdd]]
  */
final case class LinLin(/* rate: MaybeRate, */ in: GE, srcLo: GE = 0f, srcHi: GE = 1f, dstLo: GE = 0f, dstHi: GE = 1f)
  extends GE.Lazy {

  def rate: MaybeRate = in.rate // XXX correct?

  protected def makeUGens: UGenInLike = {
    val scale  = (dstHi - dstLo) / (srcHi - srcLo)
    val offset = dstLo - (scale * srcLo)
    MulAdd(in, scale, offset)
  }
}

/** A graph element that produces a constant silent
  * (zero) audio-rate output signal.
  *
  * @see [[de.sciss.synth.ugen.DC$ DC]]
  */
object Silent {
  def ar: Silent = ar()

  def ar(numChannels: Int = 1): Silent = apply(numChannels)
}
/** A graph element that produces a constant silent
  * (zero) audio-rate output signal.
  *
  * @param numChannels  the number of output channels
  *
  * @see [[de.sciss.synth.ugen.DC$ DC]]
  */
final case class Silent(numChannels: Int) extends GE.Lazy with AudioRated {

  protected def makeUGens: UGenInLike = {
    val dc = DC.ar(0)
    val ge: GE = if (numChannels == 1) dc else Seq.fill(numChannels)(dc)
    ge
  }
}

/** A graph element which reads from a connected sound driver input. This is a convenience
  * element for accessing physical input signals, e.g. from a microphone connected to your
  * audio interface. It expands to a regular `In` UGen offset by `NumOutputBuses.ir`.
  *
  * For example, consider an audio interface with channels 1 to 8 being analog line inputs,
  * channels 9 and 10 being AES/EBU and channels 11 to 18 being ADAT inputs. To read a combination
  * of the analog and ADAT inputs, either of the following statement can be used:
  *
  * {{{
  * PhysicalIn(Seq(0, 8), Seq(8, 8))
  * PhysicalIn(Seq(0, 8), Seq(8))      // numChannels wraps!
  * }}}
  *
  * If SuperCollider runs with less physical inputs than requested by this UGen,
  * invalid channels are muted.
  */
object PhysicalIn {
  /** Short cut for reading a mono signal from the first physical input. */
  def ar: PhysicalIn = ar()

  /** @param indices       the physical index to read from (beginning at zero which corresponds to
    *                      the first channel of the audio interface or sound driver). It may be a
    *                      multichannel element to specify discrete indices.
    * @param numChannels   the number of consecutive channels to read. For discrete indices this
    *                      applies to each index!
    */
  def ar(indices: GE = 0, numChannels: Int = 1): PhysicalIn = apply(indices, Seq(numChannels))

  /** @param indices       the physical index to read from (beginning at zero which corresponds to
    *                      the first channel of the audio interface or sound driver). It may be a
    *                      multichannel element to specify discrete indices.
    * @param numChannels   the number of consecutive channels to read for each index. Wraps around
    *                      if the sequence has less elements than indices has channels.
    */
  def ar(indices: GE, numChannels: Seq[Int]): PhysicalIn = apply(indices, numChannels)

  //   def apply( index: GE, moreIndices: GE* ) : PhysicalIn = apply( (index +: moreIndices).map( (_, 1) ): _* )
}

/** A graph element which reads from a connected sound driver input. This is a convenience
  * element for accessing physical input signals, e.g. from a microphone connected to your
  * audio interface. It expands to a regular `In` UGen offset by `NumOutputBuses.ir`.
  *
  * For example, consider an audio interface with channels 1 to 8 being analog line inputs,
  * channels 9 and 10 being AES/EBU and channels 11 to 18 being ADAT inputs. To read a combination
  * of the analog and ADAT inputs, either of the following statement can be used:
  *
  * {{{
  * PhysicalIn(Seq(0, 8), Seq(8, 8))
  * PhysicalIn(Seq(0, 8), Seq(8))      // numChannels wraps!
  * }}}
  *
  * If SuperCollider runs with less physical inputs than requested by this UGen,
  * invalid channels are muted.
  *
  * @param indices       the physical index to read from (beginning at zero which corresponds to
  *                      the first channel of the audio interface or sound driver). It may be a
  *                      multichannel element to specify discrete indices.
  * @param numChannels   the number of consecutive channels to read for each index. Wraps around
  *                      if the sequence has less elements than indices has channels.
  */
final case class PhysicalIn(indices: GE, numChannels: Seq[Int]) extends GE.Lazy with AudioRated {

  protected def makeUGens: UGenInLike = {
    val numIn        = NumInputBuses .ir
    val numOut       = NumOutputBuses.ir

    val _indices     = indices.expand.outputs
    val iNumCh       = numChannels.toIndexedSeq
    val _numChannels = if (_indices.size <= iNumCh.size) iNumCh
    else {
      Vector.tabulate(_indices.size)(ch => iNumCh(ch % iNumCh.size))
    }

    val ins: GE = (_indices zip _numChannels).map {
      case (index, num) =>
        val in = In.ar(index + numOut, num)
        val ok = (0 until num).map(off => index + off < numIn)
        in * ok
    }

    // fix for #72
    val out = ins.expand.flatOutputs
    if (out.size == 1) out.head else out: GE
  }
}

/** A graph element which writes to a connected sound driver output. This is a convenience
  * element for `Out` with the ability to provide a set of discrete indices to which
  * corresponding channels of the input signal are mapped, whereas multichannel expansion
  * with respect to the index argument of `Out` typically do not achieve what you expect.
  *
  * If SuperCollider runs with less physical outputs than requested by this UGen,
  * the output is muted.
  *
  * ===Examples===
  *
  * {{{
  * // flip left and right when writing a stereo signal
  * play {
  *   val indices = Seq(1, 0)
  *   val in:GE   = Seq(SinOsc.ar * LFPulse.ar(4), WhiteNoise.ar)
  *   // sine appears on the right channel, and noise on the left
  *   PhysicalOut(indices, in * 0.2)
  * }
  * }}}
  */
object PhysicalOut {
  /** @param indices       the physical index to write to (beginning at zero which corresponds to
    *                      the first channel of the audio interface or sound driver). may be a
    *                      multichannel argument to specify discrete channels. In this case, any
    *                      remaining channels in `in` are associated with the last bus index offset.
    * @param in            the signal to write
    */
  def ar(indices: GE = 0, in: GE): PhysicalOut = apply(indices, in)
}

/** A graph element which writes to a connected sound driver output. This is a convenience
  * element for `Out` with the ability to provide a set of discrete indices to which
  * corresponding channels of the input signal are mapped, whereas multichannel expansion
  * with respect to the index argument of `Out` typically do not achieve what you expect.
  *
  * If SuperCollider runs with less physical outputs than requested by this UGen,
  * the output is muted.
  *
  * @param indices       the physical index to write to (beginning at zero which corresponds to
  *                      the first channel of the audio interface or sound driver). may be a
  *                      multichannel argument to specify discrete channels. In this case, any
  *                      remaining channels in `in` are associated with the last bus index offset.
  * @param in            the signal to write
  */
final case class PhysicalOut(indices: GE, in: GE) extends UGenSource.ZeroOut with AudioRated {
  // XXX TODO: should not be UGenSource

  protected def makeUGens: Unit = {
    val numOut    = NumOutputBuses.ir
    val _in       = in.expand.outputs
    val _indices  = indices.expand.outputs
    _indices.dropRight(1).zip(_in).foreach {
      case (index, sig) =>
        val ok = index + NumChannels(sig) <= numOut
        // Out.ar(index, sig)
        XOut.ar(index, in = sig, xfade = ok)
    }
    (_indices.lastOption, _in.drop(_indices.size - 1)) match {
      case (Some(index), sig) if sig.nonEmpty =>
        val ok = index + NumChannels(sig) <= numOut
        // Out.ar(index, sig)
        XOut.ar(index, in = sig, xfade = ok)
      case _ =>
    }
  }

  protected def makeUGen(args: Vec[UGenIn]): Unit = () // XXX not used, ugly
}

/** An auxiliary graph element that repeats
  * the channels of an input signal, allowing
  * for example for an exhaustive element-wise
  * combination with another signal.
  *
  * Normally, the way multi-channel expansion
  * works is that when two signals are combined,
  * the output signal has a number of channels
  * that is the ''maximum'' of the individual number
  * of channels, and channels will be automatically
  * wrapped around.
  *
  * For example, in `x * y` if `x` has three and
  * `y` has five channels, the result expands to
  *
  * {{{
  * Seq[GE](
  *   x.out(0) * y.out(0), x.out(1) * y.out(1),
  *   x.out(2) * y.out(2), x.out(0) * y.out(3),
  *   x.out(1) * y.out(4)
  * )
  * }}}
  *
  * Using this element, we can enforce the appearance
  * of all combinations of channels, resulting in a signal
  * whose number of channels is the ''sum'' of the individual
  * number of channels.
  *
  * For example, `RepeatChannels(x, 5)` expands to
  *
  * {{{
  * Seq[GE](
  *   x.out(0), x.out(0), x.out(0), x.out(0), x.out(0),
  *   x.out(1), x.out(1), x.out(1), x.out(1), x.out(1),
  *   x.out(2), x.out(2), x.out(2), x.out(2), x.out(2)
  * )
  * }}}
  *
  * And `RepeatChannels(x, 5) * y` accordingly expands to
  * the fifteen-channels signal
  *
  * {{{
  * Seq[GE](
  *   (x out 0) * (y out 0), (x out 0) * (y out 1), (x out 0) * (y out 2), (x out 0) * (y out 3), (x out 0) * (y out 4),
  *   (x out 1) * (y out 0), (x out 1) * (y out 1), (x out 1) * (y out 2), (x out 1) * (y out 3), (x out 1) * (y out 4),
  *   (x out 2) * (y out 0), (x out 2) * (y out 1), (x out 2) * (y out 2), (x out 2) * (y out 3), (x out 2) * (y out 4)
  * )
  * }}}
  *
  * @param  a   the signal whose channels to repeat
  * @param  num the number of repetitions for each input channel
  *
  * @see  [[ChannelRangeProxy]]
  */
final case class RepeatChannels(a: GE, num: Int) extends GE.Lazy {
  def rate: MaybeRate = a.rate

  protected def makeUGens: UGenInLike = {
    val out = a.expand.outputs
    val seq = Seq.fill(num)(out).transpose.flatten
    seq: GE
  }
}

/** A helper graph element that selects a particular range of
  * output channel of another element. The range is specified with
  * integers and thus cannot be determined at graph expansion time.
  * If this is desired, the `Select` UGen can be used.
  *
  * Usually the graph element operator `out` along with
  * a standard Scala `Range` argument can be used
  * instead of explicitly writing `ChannelRangeProxy`. Thus
  * `elem out (0 until 4)` selects the first four channels and is
  * equivalent to `ChannelRangeProxy(elem, from = 0, until = 4, step = 1)`.
  *
  * Behind the scene, `ChannelProxy` instances are created, thus
  * `ChannelRangeProxy(x, a, b)` is the same as
  * `(a until b).map(ChannelProxy(x, _)): GE`.
  *
  * Because ScalaCollider allows late-expanding
  * graph elements, we have no direct way to get some
  * array of a UGen's outputs.
  *
  * @param  elem  a multi-channel element from which to select channels.
  * @param  from  the first index (inclusive) of the channel range, counting from zero.
  * @param  until the end index (exclusive) of the channel range, counting from zero.
  * @param  step  the increment from index to index in the range. A value of one
  *               means all channels from `from` until `until` will be selected. A
  *               value of two means, every second channel will be skipped. A negative
  *               value can be used to count down from high to low indices.
  *
  * @see [[de.sciss.synth.ugen.NumChannels NumChannels]]
  * @see [[de.sciss.synth.ugen.Select$ Select]]
  * @see [[de.sciss.synth.ugen.ChannelProxy ChannelProxy]]
  * @see [[de.sciss.synth.ugen.RepeatChannels RepeatChannels]]
  */
final case class ChannelRangeProxy(elem: GE, from: Int, until: Int, step: Int) extends GE.Lazy {
  def rate: MaybeRate = elem.rate

  def range: Range = Range(from, until, step)

  override def toString: String =
    if (step == 1) s"$elem.out($from until $until)" else s"$elem.out($from until $until by $step)"

  def makeUGens: UGenInLike = {
    val r = range
    if (r.isEmpty) return UGenInGroup.empty

    //    val _elem = elem.expand
    //    UGenInGroup(r.map(_elem.unwrap))

    GESeq(range.map(index => ChannelProxy(elem, index).expand))
  }
}

/** A graph element that produces an integer sequence
  * from zero until the number-of-channels of the input element.
  *
  * ===Examples===
  *
  * {{{
  * // cross-faded select
  * play {
  *   val sines: GE = Seq.fill(4)(SinOsc.ar(ExpRand(200, 2000)))
  *   val index   = MouseX.kr(lo = 0, hi = NumChannels(sines) - 1)
  *   val indices = ChannelIndices(sines)
  *   indices.poll(0, "indices")
  *   val select  = 1 - (indices absdif index).min(1)
  *   val sig     = Mix(sines * select)
  *   sig * 0.2
  * }
  * }}}
  *
  * @param in the element whose indices to produce
  *
  * @see [[de.sciss.synth.ugen.NumChannels NumChannels]]
  */
final case class ChannelIndices(in: GE) extends UGenSource.SingleOut with ScalarRated {
  protected def makeUGens: UGenInLike = unwrap(this, in.expand.outputs)

  protected def makeUGen(args: Vec[UGenIn]): UGenInLike = args.indices: GE
}

/** A graph element that produces an integer with number-of-channels of the input element.
  *
  * Because ScalaCollider allows late-expanding
  * graph elements, we have no direct way to get an integer of some
  * array-size of a UGen's outputs. On the other hand, there may be
  * sound synthesis definitions that can abstract over the number of
  * channels at definition time.
  *
  * ===Examples===
  *
  * {{{
  * // amplitude compensation
  * play {
  *   val sines: GE = Seq.fill(8)(SinOsc.ar(ExpRand(200, 2000)))
  *   val norm = Mix(sines) / NumChannels(sines)   // guarantee that they don't clip
  *   norm * 0.2
  * }
  * }}}
  *
  * @param in the element whose number-of-channels to produce
  *
  * @see [[de.sciss.synth.ugen.ChannelIndices ChannelIndices]]
  */
final case class NumChannels(in: GE) extends UGenSource.SingleOut with ScalarRated {
  protected def makeUGens: UGenInLike = unwrap(this, in.expand.outputs)

  protected def makeUGen(args: Vec[UGenIn]): UGenInLike = Constant(args.size.toFloat)
}

/** A graph element that controls the multi-channel expansion of
  * its `in` argument to match the `to` argument by padding (extending
  * and wrapping) it.
  */
object Pad {
  /** Enforces multi-channel expansion for the input argument
    * even if it is passed into a vararg input of another UGen.
    * This is done by wrapping it in a `GESeq`.
    */
  def Split(in: GE): GE = GESeq(Vector(in))
}
/** A graph element that controls the multi-channel expansion of
  * its `in` argument to match the `to` argument by padding (extending
  * and wrapping) it.
  *
  * @param in the element to replicate
  * @param to the reference element that controls the multi-channel expansion.
  *           the signal itself is not used or output by `Pad`.
  */
final case class Pad(in: GE, to: GE) extends UGenSource.SingleOut {
  def rate: MaybeRate = in.rate

  protected def makeUGens: UGenInLike = unwrap(this, Vector(in.expand, to.expand))

  protected def makeUGen(args: Vec[UGenIn]): UGenInLike = args.head
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy