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
@type t() :: GenServer.server()
Functions
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.
Which beat the transport is on, or nil if no score is playing.
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.
Set how loud the bed is, from 0.0 to 1.0, from the next chunk. Values outside that range are clamped.
@spec bed_position(t()) :: non_neg_integer() | nil
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.
See Supervisor.
@spec clear_bed(t()) :: :ok
Stop the bed. Anything sounding over it keeps playing.
Cycles per minute from cycles per second.
Cycles per second from cycles per minute.
iex> TuningFork.Stage.cps_of(120)
2.0
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.
@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})
@spec hush(t()) :: :ok
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.
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. Defaultfalse:at—{:bar, frames}delays the change until the playhead next reaches a multiple offrames. Default: at once:fade_ms— cross-fade from the old set to the new over this many milliseconds. Default0
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.
@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.
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.
@spec play(t(), TuningFork.Voice.t()) :: :ok
Sound a voice. Returns immediately; the voice is rendered in the stage. Ignored while muted.
Sound already-rendered PCM.
It must be at the stage's rate and channel count. Ignored while muted.
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.
@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.
@spec sounding(t()) :: non_neg_integer()
How many voices are sounding right now, the transport's included.
@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, defaultTuningFork.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— aTuningFork.Sink, defaultTuningFork.Sink.configured/0:sink_opts— passed to the sink'sopen/1, with:rateand:channelsfilled 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 whatTuningFork.Fx.Live.new/3takes:limit— the thresholdTuningFork.Mixer.soft_clip/2rounds peaks off above, 0.0 to 1.0, default 0.7;nilfor none:scope— frames of recent audio kept forscope/2, default 4096
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
@spec start_loop(t(), term(), TuningFork.Score.t(), keyword()) :: :ok
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.
@spec start_pattern(t(), TuningFork.Pattern.t(), keyword()) :: :ok
@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.
@spec stop_all(t()) :: :ok
Silence everything currently sounding. The bed and any transport keep going.
Stop one loop. What it has already started is left to finish sounding.
@spec stop_loops(t()) :: :ok
Stop every loop at once.
@spec stop_pattern(t()) :: :ok
Stop the pattern. Anything it has already started is left to finish sounding.
@spec stop_score(t()) :: :ok
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.
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.
@spec update_loop(t(), term(), TuningFork.Score.t(), keyword()) :: :ok
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.
@spec update_pattern(t(), TuningFork.Pattern.t(), keyword()) :: :ok
@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.