TuningFork.Stage (TuningFork v0.1.11)

Copy Markdown View Source

A process that mixes sounding voices, loops, patterns and scores into one stream and writes it to a TuningFork.Sink.

{:ok, _pid} = TuningFork.Stage.start_link(sink: TuningFork.Sink.Speaker)
TuningFork.Stage.play(TuningFork.Voice.new(freq: 440.0))

Summary

Functions

Block until loop name next comes round.

Which beat the transport is on, or nil if no score is playing.

Loop pcm underneath everything else, replacing any bed already playing.

Set how loud the bed is, from 0.0 to 1.0, from the next chunk. Values outside that range are clamped.

The playhead of the layer set, in frames since it started, or nil with no set.

Returns a specification to start this module under a supervisor.

Stop the bed. Anything sounding over it keeps playing.

Cycles per minute from cycles per second.

Cycles per second from cycles per minute.

Where the pattern has reached, in cycles, or nil when none is playing.

Sound voice and keep it sounding under key until release/3, however long its envelope's hold is. A voice already held under key is released first. Ignored while muted.

Silence everything the pattern has already started, without stopping the pattern.

Set the gain of the named layers, 0.0 to 1.0, from the next chunk.

Loop a set of named layers underneath everything else, all read from one playhead.

The peak sample of the last chunk written to the sink, after effects and limiting, 0.0 to 1.0. 0.0 before anything has been written.

Every loop running, as %{name => %{beat: beat, rounds: rounds, pending?: boolean}}.

Set whether play/2 and play_pcm/2 are ignored. Muting does not silence what is already sounding or affect the bed.

Run the pattern at cpm cycles per minute, keeping its place.

Run the pattern at a different speed, in cycles per second, keeping its place.

Scale the pattern's whole output by gain, sounding notes included, from the next chunk; kept for patterns started later.

Sound a voice. Returns immediately; the voice is rendered in the stage. Ignored while muted.

Sound already-rendered PCM.

Let the voice held under key go, fading over seconds (default 0.05) from where it is. Nothing happens when no voice is held under key.

The last :scope frames of the left channel as written to the sink, aligned to a rising zero crossing and thinned to points samples from -1.0 to 1.0. [] before anything has been written.

How many voices are sounding right now, the transport's included.

Start a stage, linked to the caller.

Start a named loop, playing score round and round from its own beat zero.

Play a TuningFork.Pattern from cycle zero until stop_pattern/1, replacing any pattern already playing.

Play a score in time, note by note, replacing any score already playing.

Silence everything currently sounding. The bed and any transport keep going.

Stop one loop. What it has already started is left to finish sounding.

Stop every loop at once.

Stop the pattern. Anything it has already started is left to finish sounding.

Stop the transport. Anything it has already started is left to finish sounding.

Swap a loop's score without moving where it has got to.

Swap the playing pattern without moving the position.

Swap the playing score without moving the position.

Types

t()

@type t() :: GenServer.server()

Functions

after_round(stage \\ __MODULE__, name, timeout \\ 30000)

@spec after_round(t(), term(), timeout()) :: :ok | {:error, :no_such_loop | :timeout}

Block until loop name next comes round.

Returns :ok when it does, {:error, :no_such_loop} at once when nothing is running under that name, or {:error, :timeout} after timeout milliseconds. A loop that is not looping never comes round.

beat(stage \\ __MODULE__)

@spec beat(t()) :: float() | nil

Which beat the transport is on, or nil if no score is playing.

bed(stage \\ __MODULE__, pcm)

@spec bed(t(), binary()) :: :ok

Loop pcm underneath everything else, replacing any bed already playing.

pcm must be at the stage's rate and channel count. It is read by sample position and wraps where the buffer ends, however short it is. An empty binary clears the bed.

bed_gain(stage \\ __MODULE__, gain)

@spec bed_gain(t(), float()) :: :ok

Set how loud the bed is, from 0.0 to 1.0, from the next chunk. Values outside that range are clamped.

bed_position(stage \\ __MODULE__)

@spec bed_position(t()) :: non_neg_integer() | nil

The playhead of the layer set, in frames since it started, or nil with no set.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

clear_bed(stage \\ __MODULE__)

@spec clear_bed(t()) :: :ok

Stop the bed. Anything sounding over it keeps playing.

cpm_of(cps)

@spec cpm_of(number()) :: float()

Cycles per minute from cycles per second.

cps_of(cpm)

@spec cps_of(number()) :: float()

Cycles per second from cycles per minute.

iex> TuningFork.Stage.cps_of(120)
2.0

cycle(stage \\ __MODULE__)

@spec cycle(t()) :: float() | nil

Where the pattern has reached, in cycles, or nil when none is playing.

Whole numbers are cycle lines, so 3.75 is three quarters of the way through cycle three.

hold(stage \\ __MODULE__, key, voice)

@spec hold(t(), term(), TuningFork.Voice.t()) :: :ok

Sound voice and keep it sounding under key until release/3, however long its envelope's hold is. A voice already held under key is released first. Ignored while muted.

Stage.hold(stage, {1, 60}, Kit.voice(%{note: 60, s: "gm_piano"}, 1.0))
Stage.release(stage, {1, 60})

hush(stage \\ __MODULE__)

@spec hush(t()) :: :ok

Silence everything the pattern has already started, without stopping the pattern.

layer_gains(stage \\ __MODULE__, gains)

@spec layer_gains(t(), %{required(term()) => number()}) :: :ok

Set the gain of the named layers, 0.0 to 1.0, from the next chunk.

layers(stage \\ __MODULE__, layers, opts \\ [])

@spec layers(t(), %{required(term()) => binary()}, keyword()) :: :ok

Loop a set of named layers underneath everything else, all read from one playhead.

layers maps a name to PCM at the stage's rate and channel count; each wraps at its own length. The set replaces the one playing unless keep: true, which merges into it. bed/2 is the same as a set with one layer named :bed.

Options

  • :gains — a map from name to 0.0..1.0, default 1.0 for every layer
  • :keep — merge into the playing set instead of replacing it. Default false
  • :at{:bar, frames} delays the change until the playhead next reaches a multiple of frames. Default: at once
  • :fade_ms — cross-fade from the old set to the new over this many milliseconds. Default 0

level(stage \\ __MODULE__)

@spec level(t()) :: float()

The peak sample of the last chunk written to the sink, after effects and limiting, 0.0 to 1.0. 0.0 before anything has been written.

loops(stage \\ __MODULE__)

@spec loops(t()) :: %{
  required(term()) => %{
    beat: float(),
    rounds: non_neg_integer(),
    pending?: boolean()
  }
}

Every loop running, as %{name => %{beat: beat, rounds: rounds, pending?: boolean}}.

rounds is how many times that loop has been round; pending? is whether a swapped score is waiting for the next round.

mute(stage \\ __MODULE__, muted?)

@spec mute(t(), boolean()) :: :ok

Set whether play/2 and play_pcm/2 are ignored. Muting does not silence what is already sounding or affect the bed.

pattern_cpm(stage \\ __MODULE__, cpm)

@spec pattern_cpm(t(), number()) :: :ok

Run the pattern at cpm cycles per minute, keeping its place.

pattern_cps(stage \\ __MODULE__, cps)

@spec pattern_cps(t(), number()) :: :ok

Run the pattern at a different speed, in cycles per second, keeping its place.

pattern_gain(stage \\ __MODULE__, gain)

@spec pattern_gain(t(), number()) :: :ok

Scale the pattern's whole output by gain, sounding notes included, from the next chunk; kept for patterns started later.

play(stage \\ __MODULE__, voice)

@spec play(t(), TuningFork.Voice.t()) :: :ok

Sound a voice. Returns immediately; the voice is rendered in the stage. Ignored while muted.

play_pcm(stage \\ __MODULE__, pcm)

@spec play_pcm(t(), binary()) :: :ok

Sound already-rendered PCM.

It must be at the stage's rate and channel count. Ignored while muted.

release(stage \\ __MODULE__, key, seconds \\ 0.05)

@spec release(t(), term(), number()) :: :ok

Let the voice held under key go, fading over seconds (default 0.05) from where it is. Nothing happens when no voice is held under key.

scope(stage \\ __MODULE__, points \\ 128)

@spec scope(t(), pos_integer()) :: [float()]

The last :scope frames of the left channel as written to the sink, aligned to a rising zero crossing and thinned to points samples from -1.0 to 1.0. [] before anything has been written.

sounding(stage \\ __MODULE__)

@spec sounding(t()) :: non_neg_integer()

How many voices are sounding right now, the transport's included.

start_link(opts \\ [])

@spec start_link(keyword()) :: GenServer.on_start()

Start a stage, linked to the caller.

A sink that will not open is logged and replaced by TuningFork.Sink.Silent; the stage still keeps time.

Options

  • :name — the registered name, default TuningFork.Stage
  • :rate — samples per second, default 44100
  • :chunk — frames per write, default 512
  • :channels — 2 for stereo, the default, or 1 for mono. Passed to the sink as well
  • :sink — a TuningFork.Sink, default TuningFork.Sink.configured/0
  • :sink_opts — passed to the sink's open/1, with :rate and :channels filled in
  • :voices — most voices sounding at once, default 16. Past that the oldest are dropped
  • :fx — effects over everything the stage produces, as [reverb: [...]], taking what TuningFork.Fx.Live.new/3 takes
  • :limit — the threshold TuningFork.Mixer.soft_clip/2 rounds peaks off above, 0.0 to 1.0, default 0.7; nil for none
  • :scope — frames of recent audio kept for scope/2, default 4096

start_loop(name, score)

Start a named loop, playing score round and round from its own beat zero.

Stage.start_loop(stage, :bass, Score.from_parts([bass]))

Starting a loop under a name already running replaces it from the next chunk. opts are TuningFork.Transport.new/3's, with :loop defaulting to true, and one more:

  • :body — a zero-arity function returning {:ok, score} or {:error, reason}, run once for each round to give the next round's score. It is run in its own process during the round before; a body that raises, returns an error or does not finish in time leaves the loop playing what it played last

start_loop(name, score, opts)

start_loop(stage, name, score, opts)

@spec start_loop(t(), term(), TuningFork.Score.t(), keyword()) :: :ok

start_pattern(pattern)

Play a TuningFork.Pattern from cycle zero until stop_pattern/1, replacing any pattern already playing.

opts are TuningFork.Pattern.Player.new/3's: :cps, :voice, :voices and :parallel. The pattern is synthesised in a process of its own (TuningFork.Pattern.Ahead), a few chunks ahead of the stream, so a change to it — update_pattern/3, pattern_gain/2, pattern_cps/2, hush/1 — sounds within those chunks, and cycle/1 reports the place the synthesis has reached.

start_pattern(pattern, opts)

start_pattern(stage, pattern, opts)

@spec start_pattern(t(), TuningFork.Pattern.t(), keyword()) :: :ok

start_score(stage \\ __MODULE__, score, opts \\ [])

@spec start_score(t(), TuningFork.Score.t(), keyword()) :: :ok

Play a score in time, note by note, replacing any score already playing.

opts are TuningFork.Transport.new/3's: :loop and :playing. A transport that reaches the end without looping is dropped.

stop_all(stage \\ __MODULE__)

@spec stop_all(t()) :: :ok

Silence everything currently sounding. The bed and any transport keep going.

stop_loop(stage \\ __MODULE__, name)

@spec stop_loop(t(), term()) :: :ok

Stop one loop. What it has already started is left to finish sounding.

stop_loops(stage \\ __MODULE__)

@spec stop_loops(t()) :: :ok

Stop every loop at once.

stop_pattern(stage \\ __MODULE__)

@spec stop_pattern(t()) :: :ok

Stop the pattern. Anything it has already started is left to finish sounding.

stop_score(stage \\ __MODULE__)

@spec stop_score(t()) :: :ok

Stop the transport. Anything it has already started is left to finish sounding.

update_loop(name, score)

Swap a loop's score without moving where it has got to.

at: :round, the default, holds the new score until the loop comes round; at: :now takes effect on the next chunk. :body replaces the loop's body. Ignored for a name that is not running.

update_loop(name, score, opts)

update_loop(stage, name, score, opts)

@spec update_loop(t(), term(), TuningFork.Score.t(), keyword()) :: :ok

update_pattern(pattern)

Swap the playing pattern without moving the position.

opts are TuningFork.Pattern.Player.update/3's: at: :cycle, the default, holds it until the next cycle line; at: :now takes effect on the next chunk. Ignored when no pattern is playing.

update_pattern(pattern, opts)

update_pattern(stage, pattern, opts)

@spec update_pattern(t(), TuningFork.Pattern.t(), keyword()) :: :ok

update_score(stage \\ __MODULE__, score)

@spec update_score(t(), TuningFork.Score.t()) :: :ok

Swap the playing score without moving the position.

Heard from the next chunk. Notes already sounding finish as they were. Ignored when no score is playing.