Walks a score in time, a block at a time, handing out the notes that have come due.
transport = Transport.new(score, 44_100)
{pcm, transport} = Transport.advance(transport, 512, 2)
Summary
Functions
The next frames frames of the piece, and the transport advanced past them.
Where a looping score has got to seconds in: which time round, and which beat of it.
Which beat the transport is on, read through the score's tempo map.
Whether the transport has run past the end of its score and nothing is still sounding.
A span holding a whole number of every score in scores, at least at_least seconds long.
A transport at the start of a score, running at rate samples per second.
Stop advancing. Notes already sounding stop with it rather than ringing on.
Whether a score is waiting for the loop to come round before it takes over.
Carry on from where it stopped.
Frames left before the transport reaches the end of its score.
Render scores looping together for seconds, without a sound device.
Render looping scores that are worked out again each time they come round.
How many times the transport has been round its loop.
Jump to beat, read through the score's tempo map.
How many voices are sounding right now.
Swap the score without stopping.
Types
@type t() :: %TuningFork.Transport{ chains: %{required(keyword()) => {TuningFork.Fx.Live.t(), non_neg_integer()}}, frame: non_neg_integer(), loop: boolean(), next: TuningFork.Score.t() | nil, playing: boolean(), rate: pos_integer(), rounds: non_neg_integer(), score: TuningFork.Score.t(), sounding: [{non_neg_integer(), TuningFork.Voice.Live.t(), keyword()}] }
Functions
@spec advance(t(), pos_integer(), pos_integer()) :: {binary(), t()}
The next frames frames of the piece, and the transport advanced past them.
The result is always exactly frames frames for channels channels. Notes falling inside
the block start sounding at their own sample, notes already sounding carry on, and what has
finished is dropped. A paused transport returns silence and does not move.
@spec at(TuningFork.Score.t(), number()) :: %{ beat: float(), rounds: non_neg_integer() }
Where a looping score has got to seconds in: which time round, and which beat of it.
iex> score = TuningFork.Score.new(bpm: 120, beats: 4)
iex> TuningFork.Transport.at(score, 3.0)
%{beat: 2.0, rounds: 1}seconds is time played in total, so rounds counts up without limit. A score of no length
is always at the start.
What a front end asks to show a loop's position when the sound is not coming from a
TuningFork.Stage.
Which beat the transport is on, read through the score's tempo map.
Whether the transport has run past the end of its score and nothing is still sounding.
Always false when looping.
@spec loop_seconds([TuningFork.Score.t()], number()) :: float()
A span holding a whole number of every score in scores, at least at_least seconds long.
iex> two = TuningFork.Score.new(bpm: 120, beats: 4)
iex> three = TuningFork.Score.new(bpm: 120, beats: 3)
iex> TuningFork.Transport.loop_seconds([two, three], 4)
6.0Four beats at 120 bpm is two seconds and three beats is one and a half, so they come back together at six. Asking for eight seconds of that pair gives twelve.
Lengths are matched to the millisecond. Where they have no common multiple under a minute the
longest is used instead. An empty list is at_least.
@spec new(TuningFork.Score.t(), pos_integer(), keyword()) :: t()
A transport at the start of a score, running at rate samples per second.
Options
:loop— go back to the start on reaching the end, default false:playing— whether it is running, default true
Stop advancing. Notes already sounding stop with it rather than ringing on.
Whether a score is waiting for the loop to come round before it takes over.
Carry on from where it stopped.
@spec remaining(t()) :: non_neg_integer()
Frames left before the transport reaches the end of its score.
iex> score = TuningFork.Score.new(bpm: 120, beats: 4)
iex> TuningFork.Transport.remaining(TuningFork.Transport.new(score, 44_100))
88200Zero once it is past the end. What a caller adds to the clock to get the frame the next round begins on.
@spec render_loops([TuningFork.Score.t()], pos_integer(), number(), pos_integer()) :: binary()
Render scores looping together for seconds, without a sound device.
pcm = Transport.render_loops([bass, drums], 44_100, 8.0)Each score keeps its own length and its own place in it, exactly as TuningFork.Stage's
named loops do — a four-beat loop and a three-beat one drift against each other rather than
being stretched to match. What comes back is signed 16-bit little-endian PCM, ready for
TuningFork.Wav.encode/2.
The result is exactly seconds long and closes without a seam: whatever is still sounding at
the end is folded back over the beginning, so a note held across the loop point is heard at
the start of the next time round.
Pass loop_seconds/2 as seconds to get a span that holds every score a whole number of
times. A span ending mid-loop starts that loop again from the top halfway through a bar.
@spec render_rounds( [{term(), TuningFork.Score.t(), (-> term()) | nil}], pos_integer(), number(), pos_integer() ) :: binary()
Render looping scores that are worked out again each time they come round.
pcm = Transport.render_rounds([{:bass, first, body}], 44_100, 12.0)Each loop is {name, score, body}. A body is what TuningFork.Part.Source.compile/1
gives: called for each round with that round's own frame, under name, so
TuningFork.Tick counters step and TuningFork.State values are read as they stand — the
same thing a TuningFork.Stage does with a running loop, done ahead of time instead.
A nil body plays its score every time round. A body that will not run leaves the round
before it playing again.
Otherwise this is render_loops/4: the same span, the same fold over the loop point.
@spec rounds(t()) :: non_neg_integer()
How many times the transport has been round its loop.
Zero until the first time it reaches the end. A loop that is not looping never counts past
zero. This is what sync-style waiting watches.
Jump to beat, read through the score's tempo map.
Whatever was sounding is dropped.
@spec sounding(t()) :: non_neg_integer()
How many voices are sounding right now.
@spec update(t(), TuningFork.Score.t(), keyword()) :: t()
Swap the score without stopping.
The position is kept, so the new score is heard from the next block rather than from the top. Notes already sounding finish as the score read when they started.
When it takes over
at: :now, the default, is the next blockat: :roundholds it until the loop comes round, so it starts on the downbeat
A transport that is not looping has no round to wait for, so at: :round is at: :now
there. Only one score can be waiting; a second replaces the first.