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 onestack, each layer's level applied to its chain as itsvelocity— multiplying whatevergainthe chain sets, never replacing it — and the music level to the whole, sounding notes included, at the piece'sbpm / beats_per_bar / 60cycles 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 throughTuningFork.Cachein the game's music directory underpiece/section/layerand 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.
Stop the music.
Types
@type renderable() :: String.t() | TuningFork.Score.t() | (-> binary()) | {:pcm, binary()}
@type spec() :: %{ :bpm => number(), :beats_per_bar => pos_integer(), :layers => [atom()], :sections => %{required(atom()) => %{required(atom()) => renderable()}}, optional(:fingerprint) => String.t() }
Functions
@spec cue(Cauldron2D.Audio.instance(), cue()) :: :ok
Ask for a section of a piece with the given layer gains; see the module rules.
@spec current(Cauldron2D.Audio.instance()) :: {String.t(), atom()} | nil
The piece and section playing, as {piece, section}, or nil.
@spec piece(Cauldron2D.Audio.instance(), String.t(), spec()) :: :ok
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.
@spec stop(Cauldron2D.Audio.instance()) :: :ok
Stop the music.