Volume control - davidpanderson/Numula GitHub Wiki
Volume representation
from numula.vol_names import *
In Numula, the volume (loudness) of a note is represented by floating point 0..1 (soft to loud). This is mapped linearly to MIDI 2..127 (on Pianoteq, 1 doesn't sound for some reason).
There are three "modes" of volume adjustment. In each case there is an adjustment factor X, which may vary continuously over time.
VOL_MULT
: the note volume is multiplied by X,
which typically is in [0, 2].
This maps the default volume 0.5 to the full range [0, 1].
Adjustments that use this mode can be applied in any order.
VOL_ADD
: X is added to the note volume.
X is typically around .1 or .2.
This is useful for bringing out melody notes when the overall volume is low.
This can be combined with multiplicative adjustment, but the order matters.
VOL_SET
: the note volume is set to X/2. X is in [0, 2].
(The division by 2 means that you use the same scale as for VOL_MULT
).
Multiple adjustments can result in levels > 1; if this happens, a warning message is shown and the volume is set to 1.
The default volume of a note is 0.5. The following constants are intended for use as multiplicative factors; they map the default value to the full 0..1 range. You can change them if you like (see below).
pppp = .05
pppp_ = .10
_ppp = .16
ppp = .23
ppp_ = .30
_pp = .38
pp = .45
pp_ = .52
_p = .60
p = .67
p_ = .74
_mp = .82
mp = .89
mp_ = .96
mm = 1
_mf = 1.04
mf = 1.11
mf_ = 1.18
_f = 1.26
f = 1.33
f_ = 1.40
_ff = 1.48
ff = 1.55
ff_ = 1.62
_fff = 1.68
fff = 1.77
fff_ = 1.84
_ffff = 1.92
ffff = 1.99
Volume control typically involves multiple "layers" of adjustment:
-
The first layer does long-term volume change, mapping note volumes from their initial value of .5 to the [0, 1] range.
-
Subsequent layers adjust the volume on shorter time scales, either by multiplication or addition. For example, a multiplicative adjustment by
mf
adds a slight accent andmp
a slight attenuation, regardless of the note's current volume.mm
leaves the volume fixed.
Varying volume over time
You can adjust the volume of some or all notes in a Score
using a PFT.
The value of the PFT is the factor X as described above.
The PFT segments are typically Linear
or ExpCurve
.
An additional PFT primitive is available:
Accent(value: float)
This specifies the value of the PFT at the current time. It overrides the "closed" attributes of the previous and following segments.
Note: Numula provides a compact textual notation for defining volume-control PFTs. This is usually more convenient than the function calls described here.
To apply a volume PFT to a Score
:
Score.vol_adjust_pft(
pft: PFT, t0: float=0, selector: Selector=None, mode=VOL_MULT
)
This adjusts the volume of the selected notes starting at t0; i.e. the factor X for note N is the value of PFT at (N.time - t0).
For example:
ns.vol_adjust_pft(
[
Linear(pp, ff, 4/4),
Linear(f, p, 4/4, closed_start=False)
], selector=lambda n: 'rh' in n.tags
)
scales the volume of right-hand notes based on a PFT that goes from pp to ff over one measure, then f to p over another measure.
Note-level volume adjustment
To adjust the volume of a set of notes:
Score.vol_adjust(factor: float, selector: Selector=None, mode=VOL_MULT)
where factor
is a adjustment factor
and selector"
is a note selector function.
For example, the following "voices" to the top and bottom: it scales middle notes by .7 and bottom notes by .9.
ns.vol_adjust(.7, lambda n: 'top' not in n.tags and 'bottom' not in n.tags)
ns.vol_adjust(.9, lambda n: 'bottom' in n.tags)
In some cases you might want the scale factor to depend on the note.
Score.vol_adjust_func(
func: NoteToFloat, selector: Selector=None, mode=VOL_MULT
)
In this case func
is a function (possibly a lambda function) that
takes a Note
argument and returns an adjustment factor.
Example: metric emphasis
The following de-emphasizes the weak beats of 4/4 measures:
ns.vol_adjust(.9, lambda n: n.measure_offset == 2)
ns.vol_adjust(.8, lambda n: n.measure_offset in [1,3])
ns.vol_adjust(.7, lambda n: n.measure_offset not in [0,1,2,3])
Globally scaling volume
Score.vol_scale(v0: float, v1: float)
Linearly scale the volume of all notes so that [0, 1] is mapped to [v0, v1]. This can be useful if your synthesizer has a limited range of useful velocities.
Other adjustment modes
The examples above use the VOL_MULT
mode:
volumes are multiplied by a scale factor in [0, 2].
You can also use two other modes:
VOL_ADD
: the adjustment factor is added to note volumes.
This can be used to to emphasize or de-emphasize individual notes.
You might find it more useful that VOL_MULT
at low volume levels,
where the effects of multiplication may be hard to hear.
The offsets are typically small (.05 - 0.2 or so).
VOL_SET
: the adjustment factor is divided by two
(so that you can use the same pppp
- ffff
range
as VOL_MULT
and assigned to the note volume.
If the adjustment factor is negative, the note volume is not changed;
this lets you define PFTs that set note volumes during some time periods
and not others.
Random volume perturbation
Score.v_random_uniform(min: float, max: float, selector: Selector=None):
Score.v_random_normal(stddev: float, max_sigma: float, selector: Selector=None):
These functions scale the volumes of selected notes by a random amount.
For v_random_uniform()
, the scale factor is chosen from a uniform distribution between min
and max
.
For v_random_normal()
,
the scale factor is chosen from a normal distribution with mean one
and the given standard deviation.
Factors with sigma > max_sigma are not used.
You might find this useful for adding human-like imperfection.
Redefining volume constants
To redefine the volume constants:
import numula.vol_name
numula.vol_name.mp = .85
Do this before importing notate_nuance
.