Synths written in Elixir, as defsynth, which mxs builds when a note starts; the guide "Instruments" introduces them.
A module that uses Mx.Instrument declares each synth's options with attr lines, as a Phoenix function component declares its assigns, and envelope_attrs/1 for the seven options of the note's envelope.
defsynth defines a synth as a function of one map, the note's options, with clauses that match on it; defsound and defsoundp define pieces a synth calls.
defeffect defines an effect as a function of the sound coming in, one channel, and a map of its options, and a song applies it by name with fx:.
When the module compiles, each clause runs once with the note's options as placeholders, and what its body does with them is recorded as a graph.
Data written in the module takes any Elixir, while a placeholder takes only arithmetic, comparisons, select/3 and the vocabulary of Mx.Instrument.Vocabulary.
An if on a placeholder fails then, with its file and line, and so does a synth that ends with neither note_envelope/2 nor rings/2.
A module compiled outside mix's own compilation, such as one a song file defines, becomes known to mx as it compiles: songs play its synths by name, and mxs builds them from then on, so a defsynth saved in a song plays without restarting mx.
An option is read once, when the note starts, unless the synth reads it with follow/1, which glides with each change the song makes while the note sounds.
Summary
Functions
Declares an option of the next defsynth.
Defines an effect, a function of the sound coming in and a map of its options, which returns the sound going out.
Defines a piece of sound other modules' synths can call.
Defines a piece of sound only this module's synths call.
Defines a clause of a synth, a function of the note's options that returns its sound.
Declares the seven options of the note's envelope: attack, decay, sustain, release, attack_level, decay_level and sustain_level.
Makes a module's synths known to mx: songs can play them, and a running mxs engine builds them.
The options the clauses of a known synth written in Elixir match on.
The modules whose synths mx knows, in the order they became known.
Registers each synth and effect of module with the mxs engine engine, as mx's NIF module, Mx.Engine.Mxs.Native, starts it.
A synth's options as mxs declares them, for its catalog.
Functions
Declares an option of the next defsynth.
An attr line applies to the next defsynth after it, and only that one.
Declare the options again before each synth.
The synth's documentation lists them under "Options", so a @doc written above the first attr line gains that section.
name is an atom, the key the synth reads from its map: played.vel reads the option :vel.
type says what the value means: :float, :note for a MIDI note number, :beats for a length in beats, or :atom for a choice among names.
Options
:default- the value a note gets when its song gives none.:range- the lowest and highest value the option takes, as a range literal such as0..1. A song that sets a value outside it is refused when the song is saved.:values- for an:atomoption, the names it takes, as a list.:required- whentrue, the option is marked as one a note has to set. Defaults tofalse.:doc- one sentence saying what the option does, for the synth's documentation and catalog.
Examples
@doc "A marimba note."
attr :note, :note, default: 52, doc: "The pitch."
attr :vel, :float, default: 0.6, range: 0.05..1, doc: "How hard the bar is struck."
defsynth marimba(hit) do
...
endAn option that picks among names lists them in values:.
Clauses of the synth can then match on each name:
attr :bol, :atom, values: [:na, :tun, :te], default: :na, doc: "The stroke."
Defines an effect, a function of the sound coming in and a map of its options, which returns the sound going out.
attr lines before it declare its options, as for a synth.
The effect shapes each channel on its own, so the sound coming in is one channel, and a song applies it by name with fx:.
Examples
attr :level, :float, default: 0.5, range: 0..1, doc: "How loud the sound comes out."
defeffect quieter(sound, opts) do
sound |> gain(opts.level)
end
Defines a piece of sound other modules' synths can call.
It is a public function whose body takes the same arithmetic as a synth's.
Use defsoundp/2 for a piece only this module needs.
Examples
defsound mallet(hz, corner, slope), do: slope * max(log2(hz / corner), 0)
Defines a piece of sound only this module's synths call.
It is the private form of defsound/2, so other modules cannot call it.
Examples
defsoundp above(hz, corner), do: max(log2(hz / corner), 0)
defsoundp mallet(hz, corner, slope) do
slope * above(hz, corner)
end
Defines a clause of a synth, a function of the note's options that returns its sound.
The argument is a pattern over the map of options.
The synth is named after the function, and a song plays it by that name.
Clauses of one synth sit next to each other, and the one whose pattern matches the note plays.
Mx records a match only on atoms and numbers, such as %{bol: :na}.
The body takes the arithmetic, comparisons and select/3 described in the moduledoc, and ends with note_envelope/2 or rings/2.
Use select/3 where you would write an if on an option.
Examples
One clause, reading its options by key:
attr :note, :note, default: 52, doc: "The pitch."
envelope_attrs()
defsynth beep(played) do
sine(hz: hz(played.note))
|> note_envelope(played)
endA clause per stroke, matched on the option's value:
attr :bol, :atom, values: [:na, :tun], default: :na, doc: "The stroke."
defsynth drum(%{bol: :na} = hit), do: ...
defsynth drum(%{bol: :tun} = hit), do: ...
Declares the seven options of the note's envelope: attack, decay, sustain, release, attack_level, decay_level and sustain_level.
Each has the usual default and documentation, as the module's @envelope lists them.
Pass a keyword list to give some of them another default; the others keep the usual one.
Like attr/3, it applies to the next defsynth.
Examples
The usual envelope:
envelope_attrs()A note that holds a long time and fades in 0.3 beats:
envelope_attrs sustain: 1000, release: 0.3
Makes a module's synths known to mx: songs can play them, and a running mxs engine builds them.
A module compiled outside mix's own compilation becomes known on its own, so call this only for a module compiled some other way. Calling it again for the same module is harmless.
Examples
Mx.Instrument.known(Mx.Instruments.Flute)
The options the clauses of a known synth written in Elixir match on.
The modules whose synths mx knows, in the order they became known.
Examples
Mx.Instrument.modules()
#=> [Mx.Instruments.Marimba, Mx.Instruments.Flute]
Registers each synth and effect of module with the mxs engine engine, as mx's NIF module, Mx.Engine.Mxs.Native, starts it.
Returns :ok once every one is registered, or {:error, reason} at the first the engine refuses; the ones after it are not registered.
known/1 calls it for a running engine, so call it yourself only with an engine you started.
A synth's options as mxs declares them, for its catalog.
synth is one entry of a module's __synths__/0.
Each option becomes a map with its name, kind, values, default, default_name, low, high, required and doc.
Numbers are floats, and a missing default, low or high is nil.