Documentation
¶
Overview ¶
Package tomparams is the normalized knob bank for the physical Tom, and the mapping from it to the SI-valued configuration the model consumes.
It exists as its own package because two very different callers need exactly the same mapping and must not each carry a copy: the shipped voice, which reads knob positions out of a UI and a share link, and the offline fitter, which searches over those same positions. A fit performed against a second spelling of the mapping measures a different instrument than the one that ships, and nothing downstream would say so — see Config.
The Spec type here was the drum machine's per-voice parameter descriptor, and is shared with it by type alias rather than duplicated, for the same reason.
Index ¶
Constants ¶
const ( ParamDiameter = iota ParamBatterTension ParamResonantTension ParamDamping ParamStrikeRadius ParamStrikeAngle ParamHardness ParamShellDepth ParamCavityCoupling ParamNonlinearity ParamPickupRadius ParamPickupAngle ParamQuality ParamAsymmetry ParamAsymmetryAxis ParamDampingTilt ParamAttackLevel ParamAttackTone )
The physical Tom's parameters use their own persistence bank, separate from the drum machine's procedural Tom table. An application can therefore A/B the two models without one reinterpreting or overwriting the other's settings.
Damping takes two controls because it needs two degrees of freedom. DAMP scales every loss rate at once, and D.TILT redistributes them across frequency: at 0 the decay law is flat (every mode rings for the same time, which is what the model used to do), at 1 it is the calibrated constant-Q law, and above that the high modes die progressively sooner. One knob could only ever move the whole envelope up and down, never change its shape, and the shape was the defect.
The index constants are the persistence and WASM command addresses: append only, never reorder.
const ByteStep = 1.0 / 255.0
ByteStep is one step of the UI's 8-bit persistence quantisation. Map snaps to Shipped within half a step of Default; see Map.
const DecayScaleMin = 0.5
DecayScaleMin defines what the drum machine's persisted per-track decay byte means: SetDecay(amount) scales a voice's base decay time by DecayScaleMin + amount, i.e. 0.5×–1.5× of the base. Changing it would silently reinterpret every existing share link.
const NeutralDecayAmount = 1 - DecayScaleMin
NeutralDecayAmount is the strip DEC position at which the knob contributes nothing: DecayScaleMin + amount == 1, so the loss law is left as the parameter bank set it. Offline callers that want to fit DAMP alone should pass it rather than reaching for a bare 0.5.
Variables ¶
var ErrParamCount = errors.New("physical tom parameter count")
ErrParamCount reports a normalized bank of the wrong width.
Functions ¶
func Config ¶
func Config(values01 []float64, decayAmount, sampleRateHz float64) (physical.PhysicalDrum, error)
Config maps a normalized parameter bank to the SI configuration the physical model consumes.
values01 holds one 0..1 knob position per Specs() entry, in the same order; decayAmount is the strip DEC position, also 0..1. Both are clamped rather than rejected, matching the setter contract everywhere else.
It is exported because it is the *only* correct spelling of this mapping — the constant-ζ retune rule, the DAMP/DEC/D.TILT composition and the resonant head's reduced asymmetry are all calibration decisions with their own evidence. Offline tools that build a configuration from knob positions must reuse it, or they measure a different instrument than the one that ships.
func Defaults ¶
func Defaults() []float64
Defaults is a fresh normalized bank at every knob's default position — the shipped drum, and the right starting point for a UI or a fit seed.
func ScaleHeadLosses ¶
ScaleHeadLosses multiplies one head's whole loss law by scale, which is exactly what DAMP does and nothing else: the frequency tilt is left alone, so the shape of the decay across the mode series does not move.
It exists for offline experiments that need to reach outside DAMP's own range. A fit that pins DAMP against a bound has said something about the bound rather than about the drum, and the only way to tell which is to look past it — but widening the shipped spec to find out would move every stored preset, since presets hold normalized positions. Applying an extra factor here answers the question without touching the product.
Nothing in the engine calls it. If a fitted value ever justifies moving the spec, that is a calibration decision with its own migration, not this.
Types ¶
type Kind ¶
type Kind uint8
Kind selects a spec's curve.
The two curves a Spec can take. Frequencies and times are always KindExp: the ear hears ratios, not differences.
type Spec ¶
type Spec struct {
ID string // stable persistence key; append-only, never reordered
Label string // knob face, kept short
Name string // accessible-name fragment, e.g. "pitch sweep start"
Unit string // "Hz", "s", "" …
Choices []string
Kind Kind
Min float64
Max float64
Shipped float64
Default float64
Digits int // display precision
}
Spec describes one tweakable synthesis parameter.
Shipped is the constant the voice has always used and Default is its normalized position, derived by inverting the curve rather than typed by hand — so the table cannot drift from the sound it describes.
func Choice ¶
Choice builds a discrete selector rendered by the same normalized knob as continuous parameters. shipped is the zero-based selected choice.
func Exp ¶
Exp builds an exponentially mapped parameter. Frequencies and times are always exponential: the ear hears ratios, not differences.
func Specs ¶
func Specs() []Spec
Specs returns the descriptor source for the physical Tom editor. Its indices are stable persistence and WASM command addresses.
The slice is shared rather than copied, matching the drum engine's own accessor: the table is read-only by convention and copying it per call would allocate on a path the WASM bridge takes often.
func (Spec) Map ¶
Map converts a normalized knob position to engineering units.
Within half a persistence byte step of Default it returns Shipped exactly. Persistence quantises every scalar to one byte, so a default of 0.4648 round-trips as 0.4667 — without this snap, saving and reloading would retune a 200 Hz body to 205 Hz on every untouched voice. The dead zone is ±0.2 %, i.e. sub-pixel on the Knob's 150 px full sweep, and reads as a detent at the default position.
func (Spec) Unmap ¶
Unmap is Map's inverse: the normalized position at which this spec reads the given engineering value. Out-of-range values clamp to the ends of the range.
It exists so that a caller who knows a value in the unit the instrument is actually described in — a head diameter in metres, a tension in N/m — can state it without hand-inverting the curve. cmd/fit-physical's -set does exactly that, and getting the exponential inversion wrong by hand is a silent error: 0.2032 m on SIZE is normalized 0.2098, and a plausible-looking 0.2032 typed into -fix is 0.2027 m of *normalized position*, i.e. 0.203 m — which happens to be close enough to look right and is a coincidence of this one parameter's range.
Round-tripping is exact except across Map's default snap: Map returns Shipped verbatim within half a persistence byte of Default, so Unmap(Map(x)) can differ from x inside that dead zone. Unmap(Shipped) is Default, which is the direction that matters here.