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

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)

# `t`

```elixir
@type t() :: %TuningFork.Transport{
  chains: %{required(keyword()) =&gt; {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()}]
}
```

# `advance`

```elixir
@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.

# `at`

```elixir
@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`.

# `beat`

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

Which beat the transport is on, read through the score's tempo map.

# `finished?`

```elixir
@spec finished?(t()) :: boolean()
```

Whether the transport has run past the end of its score and nothing is still sounding.

Always false when looping.

# `loop_seconds`

```elixir
@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.0

Four 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`.

# `new`

```elixir
@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

# `pause`

```elixir
@spec pause(t()) :: t()
```

Stop advancing. Notes already sounding stop with it rather than ringing on.

# `pending?`

```elixir
@spec pending?(t()) :: boolean()
```

Whether a score is waiting for the loop to come round before it takes over.

# `play`

```elixir
@spec play(t()) :: t()
```

Carry on from where it stopped.

# `remaining`

```elixir
@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))
    88200

Zero once it is past the end. What a caller adds to the clock to get the frame the next
round begins on.

# `render_loops`

```elixir
@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.

# `render_rounds`

```elixir
@spec render_rounds(
  [{term(), TuningFork.Score.t(), (-&gt; 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.

# `rounds`

```elixir
@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.

# `seek`

```elixir
@spec seek(t(), number()) :: t()
```

Jump to `beat`, read through the score's tempo map.

Whatever was sounding is dropped.

# `sounding`

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

How many voices are sounding right now.

# `update`

```elixir
@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 block
  * `at: :round` holds 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.

---

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