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

SFZ instruments: a text map of recordings by key, velocity and round robin, read into a
pitched `TuningFork.Sample.Bank` the kit plays by name, with the recordings fetched
one by one as notes ask for them.

    {:ok, "fingerbass", 39} = TuningFork.Sfz.load("fingerbass")
    TuningFork.Kit.voice(%{s: "fingerbass", note: "e2"}, 0.5)

The library carries no instruments. The application registers the ones it uses —
`register/2`, or the `:sfz` key of the `:tuning_fork` environment in the same shape —
each with its source, its licence and who to credit; `instruments/0` lists them.
`load/2` takes a registered name, a `github:` source, a URL or a local path. The kit
loads a registered name itself the first time a pattern plays it, so `s("fingerbass")`
in Strudel needs nothing more than the registration.

    config :tuning_fork, sfz: %{"ours" => %{source: "priv/sfz/ours.sfz", licence: "CC0 1.0", credit: "us", what: "a bell"}}

What is read of the format: `<control>`, `<global>`, `<master>`, `<group>` and
`<region>` headers, opcodes on any line in any number, values with spaces, `//`
comments, `#define` and `#include`, `default_path`, `sample`, `key`, `lokey`, `hikey`,
`pitch_keycenter`, `lovel`, `hivel`, `tune`, `transpose`, `volume`, `loop_mode`,
`loop_start`, `loop_end`, `trigger`, `sw_default`, `sw_last` and `locc`. A bank takes one
velocity layer (the regions whose range holds `:velocity`, default 100), every round
robin of a note as one of its files, and leaves out release triggers, regions that need
a controller raised (a pedal, a keyswitch away from the default) and, with `:keys`,
regions outside a range. Envelopes, filters, modulation and controllers are not read.

# `bank`

```elixir
@type bank() :: %{required(float()) =&gt; [entry()]}
```

# `entry`

```elixir
@type entry() :: {String.t(), keyword()}
```

# `instrument`

```elixir
@type instrument() :: %{
  :source =&gt; String.t(),
  :licence =&gt; String.t(),
  :credit =&gt; String.t(),
  :what =&gt; String.t(),
  optional(:keys) =&gt; Range.t()
}
```

An instrument by name: where its SFZ file is, its licence, who to credit, what it is, and the keys to read.

# `region`

```elixir
@type region() :: %{required(String.t()) =&gt; String.t()}
```

# `t`

```elixir
@type t() :: %{control: %{required(String.t()) =&gt; String.t()}, regions: [region()]}
```

# `bank`

```elixir
@spec bank(t(), keyword()) :: bank()
```

The bank a parsed file maps: each recorded pitch, as a MIDI number that may be
fractional, to its files in order of round robin, each `{file, opts}` with `:loop` and
`:gain` as `TuningFork.Sample.Bank.put/3` takes them. Files are resolved against
`:base`, the file's own path or URL, and `default_path`.

## Options

  * `:base` — required; the path or URL the file was read from
  * `:velocity` — the layer to take, default 100
  * `:keys` — a range of MIDI numbers; regions whose key centre lies outside it are left out

# `instrument?`

```elixir
@spec instrument?(String.t()) :: boolean()
```

Whether `name` is one of `instruments/0`.

# `instruments`

```elixir
@spec instruments() :: %{required(String.t()) =&gt; instrument()}
```

The instruments the application has registered, by name: each a `:source` for
`load/2`, its `:licence`, who to `:credit`, `:what` it is and, when given, the `:keys`
read. The library registers none of its own.

# `load`

```elixir
@spec load(String.t(), keyword()) ::
  {:ok, String.t(), non_neg_integer()} | {:error, term()}
```

Read an instrument into the bank: one of `instruments/0` by name, or a source as
`url/1` takes it, or a local path. The file and its includes are read now; the
recordings are fetched as they are played, or all at once with `:prefetch`.

Returns `{:ok, name, notes}` with how many notes the bank holds, or `{:error, reason}`.

## Options

  * `:name` — the bank name, default the instrument's name or the file's basename
  * `:velocity`, `:keys` — as `bank/2` takes them; a registered instrument brings its own `:keys`
  * `:prefetch` — start fetching every recording in the background, default `false`

# `parse`

```elixir
@spec parse(String.t(), keyword()) :: t()
```

The file as headers merged down to regions: `:control` is the `<control>` opcodes, and
each of `:regions` carries its own opcodes over its group's, master's and the global
ones, with `#define` names replaced. `:include` is a function of an `#include` path
returning `{:ok, text}` or `:error`; without it, or when it says `:error`, the include is
left out.

# `register`

```elixir
@spec register(%{required(String.t()) =&gt; instrument()}) :: :ok
```

Register every instrument in `instruments`, a map of name to instrument as `register/2` takes them.

# `register`

```elixir
@spec register(String.t(), instrument()) :: :ok
```

Register an instrument under `name`, so `load/2` and the kit know it by that name.
`instrument` must carry `:source`, `:licence`, `:credit` and `:what`; `:keys` is optional.

    TuningFork.Sfz.register("fingerbass", %{
      source: "github:freepats/electric-bass-YR/master/FingerBassYR 20190930.sfz",
      licence: "CC0 1.0",
      credit: "FreePats, Yamaha RBX bass",
      what: "an electric bass, fingered"
    })

# `url`

```elixir
@spec url(String.t()) :: String.t()
```

The URL of a source: `github:user/repo/branch/path/to/file.sfz` becomes the raw file on
GitHub, a URL is kept, and either has its spaces and other reserved characters escaped.

---

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