Cauldron2D.Audio (Cauldron2D v0.2.2)

Copy Markdown View Source

Sound effects and music for one listener, over one TuningFork stage.

A game emits the events that happened — :brick, {:explosion, {x, y}} — and supplies a function from an event's name to the voice it makes. Each instance is a process; a game with one player uses the default one, a served game starts one per session with a sink that reaches that player.

Cauldron2D.Audio.start(sounds: &MyGame.Sound.voice/1, sfx: 0.85, music: 0.35)
Cauldron2D.Audio.play([:brick, {:wall, {12.0, 3.0}}])
Cauldron2D.Audio.bed(Cauldron2D.Audio, "mygame/level-3", fn -> MyGame.Music.render(3) end)

{:ok, audio} = Cauldron2D.Audio.start_link(name: nil, sink: {TuningFork.Sink.Pulse, server: "tcp:127.0.0.1:<port>"})
Cauldron2D.Audio.listener(audio, {player_x, player_y})
Cauldron2D.Audio.play(audio, events)

Every function takes the instance first. The ones with a fixed number of arguments default it to the process registered as Cauldron2D.Audio; play_piece/4 and bed/4 take it explicitly.

Rules

  • start/1 or start_link/1 comes first. It records the sound table and opens the stage; when this machine has no sound the stage stays silent and every call is a no-op returning :ok. on?/1 reports which.
  • An event is any term the table knows, or {event, {x, y}} placing it. With a listener set, a placed event is scaled by its distance — full within one tile, silent at :earshot tiles — and panned by its side; an unplaced one plays at full level, centred.
  • keyed/4, moved/2 and stopped/2 build events for sounds that keep sounding under a key: one replaces another under the same key, and each is placed again when the listener moves, when moved/2 moves it, and when the effects level changes.
  • The stage sounds at most :voices sounds at once and lets the oldest go past that; a game sets it through Cauldron2D.Client.Game audio/0.
  • Effects and music have separate levels, set by levels/3; a muted/1 event silences both until another lets them sound.
  • Beds are keyed by a string. A render runs once, in a task, and is kept in memory and on disk via TuningFork.Cache; a bed finishing after another key was asked for is dropped. A render that raises is logged and never plays.
  • clear_bed/1 and resume_bed/1 cover both playback modes.
  • Music with sections and layers is Cauldron2D.Audio.Music.

Summary

Functions

Loop the PCM render produces under key; see the module rules for caching.

Returns a specification to start this module under a supervisor.

Stop the music, live or rendered.

The effects and music levels an instance starts with: %{sfx: 0.85, music: 0.35}.

The event that sounds name under key at at. Whatever sounds under key is let go first, over :fade seconds (default 0.05). The sound plays until it ends or stopped/2 lets it go; a run sounds as one, its voices' own pans left out. With loop: true a sound that is a voice playing a recording goes round its recording — its own loop, or the whole of it — until stopped/2 lets it go; any other sound plays once.

Set the effects and music levels, each 0.0 to 1.0.

Where the listener is, for placed events.

The event that moves the sound under key to at, any at keyed/4 takes. Nothing happens when nothing sounds under key.

Silence everything, or restore it.

The event that silences everything — effects and music — while on? is true, and lets it sound again at the levels set when false. Sounds started meanwhile are kept at no level, and come back with the rest.

Whether a stage with somewhere to play is running.

Sound events in order through the table given at start: a name — any term — or {name, {x, y}} placed against the listener; one the table answers nil for, or has no clause for, is silent.

Play score on a loop, live or from a render, under key.

Sound voice directly, scaled by the effects level.

The key last given to bed/4 or play_piece/4, or nil.

Start the music last asked for again.

The stage this instance plays through, or nil.

Start the default instance, registered as Cauldron2D.Audio, replacing one already running. Returns whether sound is possible.

Start an instance linked to the caller.

The event that lets the sound under key go over fade seconds, default 0.05.

Types

event()

@type event() :: term() | {term(), {number(), number()}}

instance()

@type instance() :: GenServer.server()

sound()

@type sound() ::
  TuningFork.Voice.t() | [{non_neg_integer(), TuningFork.Voice.t()}] | nil

Functions

bed(audio, key, render, opts \\ [])

@spec bed(instance(), String.t(), (-> binary()), keyword()) :: :ok

Loop the PCM render produces under key; see the module rules for caching.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

clear_bed(audio \\ __MODULE__)

@spec clear_bed(instance()) :: :ok

Stop the music, live or rendered.

default_levels()

@spec default_levels() :: %{sfx: float(), music: float()}

The effects and music levels an instance starts with: %{sfx: 0.85, music: 0.35}.

keyed(key, name, at \\ nil, opts \\ [])

The event that sounds name under key at at. Whatever sounds under key is let go first, over :fade seconds (default 0.05). The sound plays until it ends or stopped/2 lets it go; a run sounds as one, its voices' own pans left out. With loop: true a sound that is a voice playing a recording goes round its recording — its own loop, or the whole of it — until stopped/2 lets it go; any other sound plays once.

at is one of:

  • {x, y} — placed against the listener, and placed again when the listener moves
  • nil — full level, centred
  • {:level, gain, pan} — the game's own level, gain 0.0 upward and pan from -1.0 left to 1.0 right; the listener does not move it, moved/2 does

The effects level scales every one of them, and the voice's own gain and pan apply on top. Options: :fade, the seconds it takes to replace another, default 0.05; :loop, true to sound round and round until stopped; :music, true for the sound to be instead the recording :recordings gives for name, played in its own channels as a looping layer of the stage's music (TuningFork.Stage.layers/3) from its own beginning, at the music level and its placement's gain — moved/2 changes that gain, stopped/2 drops the layer, and cueing a Cauldron2D.Audio.Music piece replaces these layers. Raises ArgumentError for any other at.

Audio.play(audio, [Audio.keyed({:entity, 12, :voice}, :announce, {40.0, 8.0})])
Audio.play(audio, [Audio.keyed({:entity, 12, :weapon}, :spark, {:level, 0.4, -0.6})])

levels(audio \\ __MODULE__, sfx, music)

@spec levels(instance(), number(), number()) :: :ok

Set the effects and music levels, each 0.0 to 1.0.

listener(audio \\ __MODULE__, pos)

@spec listener(instance(), {number(), number()} | nil) :: :ok

Where the listener is, for placed events.

moved(key, at)

The event that moves the sound under key to at, any at keyed/4 takes. Nothing happens when nothing sounds under key.

Audio.play(audio, [Audio.moved({:entity, 12, :weapon}, {:level, 0.2, 0.9})])

mute(audio \\ __MODULE__, muted?)

@spec mute(instance(), boolean()) :: :ok

Silence everything, or restore it.

muted(on?)

@spec muted(boolean()) :: {module(), :muted, boolean()}

The event that silences everything — effects and music — while on? is true, and lets it sound again at the levels set when false. Sounds started meanwhile are kept at no level, and come back with the rest.

on?(audio \\ __MODULE__)

@spec on?(instance()) :: boolean()

Whether a stage with somewhere to play is running.

play(audio \\ __MODULE__, events)

@spec play(instance(), [event()]) :: :ok

Sound events in order through the table given at start: a name — any term — or {name, {x, y}} placed against the listener; one the table answers nil for, or has no clause for, is silent.

play_piece(audio, key, score, opts \\ [])

@spec play_piece(instance(), String.t(), TuningFork.Score.t(), keyword()) :: :ok

Play score on a loop, live or from a render, under key.

Options

  • :mode — :live or :rendered, overriding the instance's default
  • :render — a zero-arity function returning the PCM to loop in :rendered mode. Default: renders score at the stage's rate
  • :fingerprint — keys the on-disk render together with key. Default "0"

play_voice(audio \\ __MODULE__, voice)

@spec play_voice(instance(), TuningFork.Voice.t()) :: :ok

Sound voice directly, scaled by the effects level.

playing(audio \\ __MODULE__)

@spec playing(instance()) :: String.t() | nil

The key last given to bed/4 or play_piece/4, or nil.

resume_bed(audio \\ __MODULE__)

@spec resume_bed(instance()) :: :ok

Start the music last asked for again.

stage(audio \\ __MODULE__)

@spec stage(instance()) :: pid() | nil

The stage this instance plays through, or nil.

start(opts \\ [])

@spec start(keyword()) :: boolean()

Start the default instance, registered as Cauldron2D.Audio, replacing one already running. Returns whether sound is possible.

Takes the options of start_link/1.

start_link(opts \\ [])

@spec start_link(keyword()) :: GenServer.on_start()

Start an instance linked to the caller.

The instance lives as long as the caller: when the caller exits, for any reason, the instance stops, and a stage it started stops with it, closing its sink. A stage handed in with :stage is left running.

Options

  • :name — a registered name, or nil for none. Default Cauldron2D.Audio
  • :sounds — a function from an event name to a sound/0. Default: silent
  • :sink — a TuningFork.Sink module, or {module, sink_opts}. Default: TuningFork.Sink.Speaker when that package is present, else nothing plays
  • :stage — an already running stage to use instead of starting one
  • :mode — the default for play_piece/4, :live or :rendered. Default :live
  • :sfx, :music — levels 0.0 to 1.0. Default default_levels/0
  • :recordings — a function from an event name to its recording, %{rate: rate, channels: channels, pcm: pcm} (signed 16-bit little-endian), or nil, for keyed music (keyed/4's music: true). Default: none
  • :master — :sfx to have the effects level scale the music as well, the music sounding at music × sfx; :none for separate levels. Default :none
  • :earshot — tiles at which a placed event falls silent. Default 30.0
  • :music_dir — the directory rendered music is kept in; a game's is Cauldron2D.Client.Game.music_dir/1. Default TuningFork.Cache.dir/0
  • :static_music — play the first section cued and nothing after, rendered once and looped; see Cauldron2D.Audio.Music. Default false
  • :parallel — synthesise the music's voices on every core, for one player on a machine of their own (the terminal, the desktop); a server with many players leaves it off. Default false
  • :rate, :channels, :chunk, :voices, :lead, :fx — the stage's settings. Default 44_100, 2, 256, 12, 2048, []: the sink is kept 2048 frames (46 ms at 44.1 kHz) ahead of playback, in a ring of four times that, so a few milliseconds of scheduling jitter never leave it empty

stopped(key, fade \\ 0.05)

@spec stopped(term(), number()) :: Cauldron2D.Audio.Keyed.t()

The event that lets the sound under key go over fade seconds, default 0.05.