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 and mp 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.