# `TuningFork.Voice`
[🔗](https://github.com/jaman/tuning_fork/blob/v0.1.11/tuning_fork/lib/tuning_fork/voice.ex#L1)

One sound described as a waveform, pitch, envelope and filter, rendered to 16-bit PCM.

    TuningFork.Voice.new(shape: :saw, freq: 220.0) |> TuningFork.Voice.render(44_100)

# `t`

```elixir
@type t() :: %TuningFork.Voice{
  coarse: number() | nil,
  crush: float() | nil,
  curves: %{optional(:freq | :cutoff | :gain) =&gt; TuningFork.Curve.t()},
  cutoff: float() | nil,
  distort: float() | nil,
  envelope: TuningFork.Envelope.t(),
  filter: TuningFork.Filter.t() | nil,
  fm: float() | nil,
  fmattack: float() | nil,
  fmh: float() | nil,
  freq: float(),
  gain: float(),
  highpass: float() | nil,
  pan: float(),
  phaser: float() | nil,
  phaserdepth: float() | nil,
  sample: TuningFork.Sample.t() | nil,
  seed: TuningFork.Wave.seed(),
  shape: TuningFork.Wave.shape() | :noise,
  sweep: float(),
  vib: float() | nil,
  vibmod: float() | nil,
  vowel: atom() | nil,
  waveshape: float() | nil
}
```

# `coarsened`

```elixir
@spec coarsened(float(), number() | nil, map()) :: {float(), map()}
```

Hold each sample for `every` samples, carrying the hold in `extra`. `nil` or 1 changes
nothing.

# `crush`

```elixir
@spec crush(float(), number() | nil) :: float()
```

Round the sample to `bits` of resolution. `nil` or 16 and up leaves it alone; `bits` may be
fractional.

    iex> TuningFork.Voice.crush(0.3, nil)
    0.3
    iex> TuningFork.Voice.crush(0.3, 1) in [-1.0, 0.0, 1.0]
    true

# `distort`

```elixir
@spec distort(float(), number() | nil) :: float()
```

Drive the sample through a soft clipper, `amount` deciding how hard. `nil` or 0.0 leaves
it alone; the gain is normalised out afterwards.

    iex> TuningFork.Voice.distort(0.5, 0.0)
    0.5
    iex> TuningFork.Voice.distort(0.5, 4.0) > 0.5
    true

# `duration`

```elixir
@spec duration(t()) :: float()
```

How long the voice sounds for, in seconds: the length of its envelope.

# `modulation`

```elixir
@spec modulation(t()) :: %{
  freq: TuningFork.Curve.t() | nil,
  cutoff: TuningFork.Curve.t() | nil,
  gain: TuningFork.Curve.t() | nil
}
```

The curves actually moving on this voice, as `%{freq:, cutoff:, gain:}`.

A field whose curve holds one value is `nil` here, and so is `:freq` where `:sweep` is 1.0
and no freq curve is given.

# `new`

```elixir
@spec new(keyword()) :: t()
```

A voice, with anything unset left at its default. An unknown key raises `KeyError`.

## Options

  * `:shape` — `:sine`, `:square`, `:saw`, `:triangle` or `:noise`, default `:sine`
  * `:freq` — starting pitch in Hz, default 440.0. Ignored by `:noise`
  * `:sweep` — pitch multiplier reached by the end of the envelope, default 1.0. A `:freq`
    curve supersedes it, even a flat one
  * `:envelope` — a `TuningFork.Envelope`. When `nil`, a voice with a `:sample` gets one
    spanning the recording at the voice's pitch with a 2 ms attack and 4 ms release, and
    any other voice gets `TuningFork.Envelope.new/1`'s defaults
  * `:gain` — 0.0 to 1.0, default 0.8
  * `:pan` — `-1.0` hard left to `1.0` hard right, default `0.0`
  * `:filter` — a `TuningFork.Filter`, or `nil` for none
  * `:cutoff` — one-pole lowpass coefficient, 0.0 to 1.0; `nil` for none
  * `:highpass` — one-pole highpass coefficient, 0.0 to 1.0; `nil` for none
  * `:crush` — round every sample to this many bits; `nil` or 16 for none
  * `:distort` — drive into a soft clipper, 0.0 upwards; `nil` for none
  * `:waveshape` — Strudel's `shape` waveshaper, 0.0 to below 1.0; `nil` for none
  * `:vowel` — `:a`, `:e`, `:i`, `:o` or `:u`; `nil` for none
  * `:fm`, `:fmh`, `:fmattack` — modulation index of a second oscillator, its frequency as
    a multiple of `:freq`, and the fraction of the note over which the index arrives
  * `:vib`, `:vibmod` — vibrato rate in hertz and depth in semitones
  * `:coarse` — hold every sample for this many; `nil` or 1 for none
  * `:phaser`, `:phaserdepth` — sweep rate in hertz and depth 0.0 to 1.0
  * `:curves` — `%{freq: curve, cutoff: curve, gain: curve}` of `TuningFork.Curve`s, each a
    multiplier over its field across the note
  * `:sample` — a `TuningFork.Sample` to play instead of an oscillator, or `nil`
  * `:seed` — the noise seed; the same seed renders the same samples

# `render`

```elixir
@spec render(t(), pos_integer()) :: binary()
```

Render to mono signed 16-bit little-endian PCM at `rate` samples per second.

The result is `duration/1` seconds long, and at least one frame. `:pan` is not applied.

# `render`

```elixir
@spec render(t(), pos_integer(), pos_integer()) :: binary()
```

Render for `channels` channels: 2 applies the voice's `:pan`, 1 is `render/2` unchanged.

# `saturate`

```elixir
@spec saturate(float()) :: float()
```

Bend a sample over 0.7 softly towards full scale instead of clipping it. Samples within
-0.7 to 0.7 are unchanged.

    iex> TuningFork.Voice.saturate(0.5)
    0.5
    iex> TuningFork.Voice.saturate(4.0) < 1.0
    true

# `vowels`

```elixir
@spec vowels() :: keyword([{float(), float()}])
```

The vowels `:vowel` takes, each with its three formants as `{hz, level}` pairs.

    iex> TuningFork.Voice.vowels() |> Keyword.keys()
    [:a, :e, :i, :o, :u]

# `waveshape`

```elixir
@spec waveshape(float(), number() | nil) :: float()
```

Bend the sample by Strudel's `shape` curve, `amount` from 0.0 (none) towards 1.0 (hard).
`nil` or 0.0 leaves it alone; 1.0 and above is treated as just under 1.0.

    iex> TuningFork.Voice.waveshape(0.5, 0.0)
    0.5

---

*Consult [api-reference.md](api-reference.md) for complete listing*
