Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Units

Each physical unit the library handles has a unit type. A value is checked once, when it is built from a number, a JSON field or, for a type with a suffix, a unit string, and is valid by type afterwards. Not every public function takes a unit type. The constructors and the ratio and gain conversions take a bare float, and so do many signal-processing and rhythm helpers and some fields of the estimators. For example, music::rhythm::ticks_for_ms takes a time in milliseconds and a tempo as plain numbers.

The code examples on this page come from examples/units.rs, which CI compiles. cargo run --example units prints their results.

Unit types

Each type holds an f64 that is never NaN or infinite. Its constructor returns an error that names the type and the accepted range, for example Normalized out of range: got 1.5, expected 0.0 to 1.0. The rustdoc of each type states its exact range.

TypeHolds
Hza frequency, zero or more
Secondsa duration, zero or more
Semitonesan interval, either sign
Centsan interval in hundredths of a semitone, either sign
Octavesan interval, either sign
Decibelsa level, either sign
Percenta percentage of a unipolar or a bipolar control
Normalizeda knob position
MidiPitcha fractional MIDI note
Correlationa Pearson correlation, -1 to 1
Degreesan angle, such as a phase difference, either sign
Bpma tempo, 4 to 1000 beats per minute
Ratioa ratio of two values of one unit, zero or more

Percent has two ranges, fixed by its type parameter. Percent<Unipolar> is for controls that go one way from zero. Percent<Bipolar> is for controls that go both ways, such as a modulation amount. A unipolar value widens to a bipolar one; the reverse is checked.

Bpm cannot be the spread of a confidence, because its range excludes zero; a tempo spread would be given in Ratio or Percent.

No milliseconds type exists. A value in milliseconds converts to Seconds where it enters, with Seconds::from_millis or the ms suffix.

Conversions

MIDI pitch and frequency use equal temperament with note 69, A4, at 440 Hz:

Every MIDI pitch has a frequency, so Hz::from a MidiPitch cannot fail. The reverse fails for 0 Hz, which has no pitch, and for frequencies outside notes 0 to 127, about 8.18 Hz to 12 544 Hz. A note number from a MIDI file converts with MidiPitch::try_from a u8, which rejects numbers above 127.

Intervals convert to frequency ratios. One semitone is 100 cents, one octave 1200 cents:

Decibels convert two ways. An amplitude gain scales a magnitude, such as a sample or a spectral magnitude weight. A power ratio scales an energy, such as the share of a frequency band:

A gain or a ratio of zero has no decibel or cent value, so the inverse conversions return an error for it. The forward conversions return an error beyond a conservative cap, such as 1000 octaves, that keeps the ratio finite and nonzero.

The conversion methods are Cents::to_ratio, Cents::from_ratio, Semitones::to_ratio, Octaves::to_ratio, Decibels::to_amplitude_gain, Decibels::from_amplitude_gain, Decibels::to_power_ratio and Decibels::from_power_ratio.

    let a4 = Hz::from(MidiPitch::new(69.0)?);
    let pitch = MidiPitch::try_from(Hz::new(220.0)?)?;
    let detune = Cents::new(7.0)?.to_ratio()?;
    let gain = Decibels::new(-6.0)?.to_amplitude_gain()?;

Unit strings

The agent’s edit list writes each value as a unit string: a number, optional whitespace and a suffix. The parser accepts these suffixes, and only these, matched case-sensitively:

SuffixUnitType
HzhertzHz
kHzkilohertzHz
ssecondsSeconds
msmillisecondsSeconds
ctcentsCents
stsemitonesSemitones
octoctavesOctaves
dBdecibelsDecibels
%percentPercent
midiMIDI note numberMidiPitch

A target type accepts only its own suffixes. A suffix of another unit, an unknown suffix and a missing number are errors that list the accepted suffixes. A bare number for a control with a physical unit is an error; only counts, bools, menu labels and the stored value of a control without a physical unit take bare values. The parsed value then passes the range check of its type, so "-5 Hz" fails with the range of Hz, and "-40%" parses for a bipolar control but fails for a unipolar one.

    let cutoff: Hz = "1.2 kHz".parse()?;
    let pre_delay: Seconds = "20 ms".parse()?;
    let amount: Percent<Bipolar> = "-40%".parse()?;
    // A suffix of another unit is an error that lists the accepted suffixes.
    let wrong = "1.2 s".parse::<Hz>();

Each type in the suffix table prints in a form that parses back to the same value, such as 1200 Hz or -40%. Normalized, Correlation, Degrees, Bpm and Ratio have no suffix, so they have neither a unit string nor a printed form.

JSON forms

Values reach the library in two JSON forms, and both parse through the same range checks:

  • layers.json holds plain numbers with the unit in the field name, such as "cutoff_hz": 410 or "attack_s": 0.02. An integer is accepted for a float field. A string in a number field fails with the parser’s type message.
  • The agent’s edit list holds unit strings, such as "value": "410 Hz". The edit-list contract (#94) documents where each string goes.

A value outside its range fails with the type’s message, and the JSON reader adds the line and column. NaN and infinity never reach a file, because no unit type can hold them.

Confidence

Every estimate in the layer report carries a Confidence:

  • a level: low, medium or high. The estimator stores it from its own signals; the type does not derive it;
  • one or more reasons. Each pairs a stable code, such as active_frames, with the value of the signal behind it: a count, a number or a bool;
  • an optional spread, never negative, in the unit the type parameter names. A cutoff is a frequency, but its spread is in octaves, so its confidence is a Confidence<Octaves>. An estimate without a spread, such as a menu choice, uses Confidence<NoSpread>;
  • or a “no estimate” state. A layer with too little evidence reports no value, no level and no spread, and at least one reason.

The report writes one object per confidence. spread is left out when there is none:

{"level": "medium", "reasons": [{"code": "active_frames", "value": 412}], "spread": 0.3, "estimate": true}
{"reasons": [{"code": "too_few_notes", "value": 2}], "estimate": false}

Reading a report applies the same rules: a level on a “no estimate” state, an estimate without a level, an empty reason list, a negative spread and an unknown field or code are errors.

    let frames = Reason::new(ReasonCode::ActiveFrames, Signal::Count(412))?;
    let cutoff = Confidence::estimate(Level::Medium, Some(Octaves::new(0.3)?), vec![frames])?;
    // {"level":"medium","reasons":[{"code":"active_frames","value":412}],"spread":0.3,"estimate":true}
    println!("{}", serde_json::to_string(&cutoff)?);

    let notes = Reason::new(ReasonCode::TooFewNotes, Signal::Count(2))?;
    let envelope = Confidence::<Seconds>::no_estimate(vec![notes])?;
    // {"reasons":[{"code":"too_few_notes","value":2}],"estimate":false}
    println!("{}", serde_json::to_string(&envelope)?);

Unit traps

Some Serum 2 controls store a knob position where a reader expects a physical unit. Such a control takes a Normalized value. No conversion from Normalized to Hz or Cents exists, because the law from knob to unit is not measured yet. A cutoff in Hz therefore has no unit type for its knob: the preset writer will map it through the interim law of the parameter registry (#107) and mark the value uncalibrated.

Each claim below has a build column and a status: verified in a Serum 2.1.5 session, reported by an outside source, or open. The outside source records no build, so every row says not recorded and is reported. The probe presets of #42 will check each claim on build 2.1.5 and fill in the build.

ControlStored asUnit typeBuildStatus
Voice filter cutoffknob position 0 to 1Normalizednot recordedreported
FX Distortion frequencyknob position 0 to 1Normalizednot recordedreported
FX Filter frequencyknob position 0 to 1Normalizednot recordedreported
EQ, Phaser and Chorus frequencieshertzHznot recordedreported
Unison detuneknob position 0 to 1Normalizednot recordedreported
Reverb pre-delaymillisecondsSeconds, convertednot recordedreported
Delay timesecondsSecondsnot recordedreported
Modulation amountpercent, -100 to 100Percent<Bipolar>not recordedreported

Sources