Cauldron2D.Audio.Music (Cauldron2D v0.1.3)

Copy Markdown View Source

Music in sections and layers, switched on bar boundaries and mixed per listener.

A game declares a piece — its tempo, its layer names, and for each section what every layer plays — and then cues it from its own state. The names are the game's; nothing here knows what a section or a layer is for.

Cauldron2D.Audio.Music.piece(audio, "orbit", %{
  bpm: 112,
  beats_per_bar: 4,
  layers: [:drums, :bass, :pad],
  sections: %{
    calm: %{drums: calm_drums, bass: calm_bass, pad: pad},
    tense: %{drums: tense_drums, bass: tense_bass, pad: pad}
  },
  fingerprint: "3"
})

Cauldron2D.Audio.Music.cue(audio, %{piece: "orbit", section: :tense, layers: %{drums: 1.0, bass: 1.0, pad: 0.4}})

What a layer plays is either performed live or rendered once:

  • a Strudel chain as a string — ~s|s("bd*4").bank("RolandTR909")| — is performed live on the listener's stage. A section's chains play as one stack, each layer's level applied to its chain as its velocity — multiplying whatever gain the chain sets, never replacing it — and the music level to the whole, sounding notes included, at the piece's bpm / beats_per_bar / 60 cycles per second. Nothing is rendered ahead, so a cue takes effect at once: a new section at the next cycle line, new gains on the next block. Every layer of a section must be a chain for the section to play this way
  • a TuningFork.Score, a zero-arity function returning PCM, or PCM as {:pcm, binary} is rendered once, two stems at a time in tasks, and kept through TuningFork.Cache in the game's music directory under piece/section/layer and a fingerprint of the piece's own, what was written, the tempo, the rate and channels — so a changed layer is rendered again. A cue moves its section to the front of what is still to render and takes effect when the section is ready — at the next bar boundary of the playing section, with a cross-fade; new gains at once

The music level from Cauldron2D.Audio.levels/3 scales either.

A director made static (Cauldron2D.Audio started with static_music: true) plays the first section it is cued and no other: the section's chains are rendered once, as one pattern, over the piece's :loop cycles (default 8) — whatever still sounds at the end folded back over the start, so it loops without a seam — kept through TuningFork.Cache under piece/section/static and the fingerprint, and looped as a bed. Every later cue is ignored until stop/1. Rendering costs once a section and rate on a node; playing it back costs a copy a chunk.

Summary

Functions

Ask for a section of a piece with the given layer gains; see the module rules.

The piece and section playing, as {piece, section}, or nil.

Declare a piece and start rendering every section of it.

Fetch every recording and soundfont pieces play, in the background, so that their first bars are not synthesised while the files arrive. Walks the first 64 cycles of every chain; a chain that does not read is skipped. Returns at once.

The section of spec a static director loops for piece, as PCM at rate and channels: rendered now and kept if it was not before, read back if it was, in :dir (default TuningFork.Cache.dir/0; a game's is Cauldron2D.Client.Game.music_dir/1). A server that will play static music calls this for its pieces as it starts, so the first listener does not wait for the rendering.

Types

cue()

@type cue() :: %{
  :piece => String.t(),
  :section => atom(),
  optional(:layers) => %{required(atom()) => number()}
}

renderable()

@type renderable() ::
  String.t() | TuningFork.Score.t() | (-> binary()) | {:pcm, binary()}

spec()

@type spec() :: %{
  :bpm => number(),
  :beats_per_bar => pos_integer(),
  :layers => [atom()],
  :sections => %{required(atom()) => %{required(atom()) => renderable()}},
  optional(:fingerprint) => String.t()
}

t()

@type t() :: %{
  pieces: map(),
  rendered: map(),
  wanted: cue() | nil,
  playing: {String.t(), atom()} | nil
}

Functions

cue(audio \\ Cauldron2D.Audio, cue)

@spec cue(Cauldron2D.Audio.instance(), cue()) :: :ok

Ask for a section of a piece with the given layer gains; see the module rules.

current(audio \\ Cauldron2D.Audio)

@spec current(Cauldron2D.Audio.instance()) :: {String.t(), atom()} | nil

The piece and section playing, as {piece, section}, or nil.

piece(audio \\ Cauldron2D.Audio, name, spec)

@spec piece(Cauldron2D.Audio.instance(), String.t(), spec()) :: :ok

Declare a piece and start rendering every section of it.

prefetch(pieces)

@spec prefetch([{String.t(), spec()}]) :: :ok

Fetch every recording and soundfont pieces play, in the background, so that their first bars are not synthesised while the files arrive. Walks the first 64 cycles of every chain; a chain that does not read is skipped. Returns at once.

render_static(piece, spec, section, rate, channels, opts \\ [])

@spec render_static(String.t(), spec(), atom(), pos_integer(), 1 | 2, keyword()) ::
  binary()

The section of spec a static director loops for piece, as PCM at rate and channels: rendered now and kept if it was not before, read back if it was, in :dir (default TuningFork.Cache.dir/0; a game's is Cauldron2D.Client.Game.music_dir/1). A server that will play static music calls this for its pieces as it starts, so the first listener does not wait for the rendering.

stop(audio \\ Cauldron2D.Audio)

@spec stop(Cauldron2D.Audio.instance()) :: :ok

Stop the music.