TuningFork.Voice (TuningFork v0.1.11)

Copy Markdown View Source

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)

Summary

Functions

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

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

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

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

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

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

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

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

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

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

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.

Types

t()

@type t() :: %TuningFork.Voice{
  coarse: number() | nil,
  crush: float() | nil,
  curves: %{optional(:freq | :cutoff | :gain) => 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
}

Functions

coarsened(sample, every, extra)

@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(sample, bits)

@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(sample, amount)

@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(voice)

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

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

modulation(voice)

@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(opts \\ [])

@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.Curves, 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(voice, rate)

@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(voice, rate, channels)

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

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

saturate(sample)

@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()

@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(sample, amount)

@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