tomparams

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 4 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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.

View Source
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

View Source
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

func ScaleHeadLosses(head *physical.Head, scale float64)

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.

const (
	KindLinear Kind = iota
	KindExp
)

The two curves a Spec can take. Frequencies and times are always KindExp: the ear hears ratios, not differences.

func (Kind) String

func (k Kind) String() string

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

func Choice(id, label, name string, choices []string, shipped int) Spec

Choice builds a discrete selector rendered by the same normalized knob as continuous parameters. shipped is the zero-based selected choice.

func Exp

func Exp(id, label, name, unit string, minVal, maxVal, shipped float64, digits int) Spec

Exp builds an exponentially mapped parameter. Frequencies and times are always exponential: the ear hears ratios, not differences.

func Lin

func Lin(id, label, name, unit string, minVal, maxVal, shipped float64, digits int) Spec

Lin builds a linearly mapped parameter, used for levels and mixes.

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

func (s Spec) Map(value01 float64) float64

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

func (s Spec) Unmap(value float64) float64

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL