drum

package
v0.0.0-...-d5534e6 Latest Latest
Warning

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

Go to latest
Published: Aug 9, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Index

Constants

View Source
const (
	TrackCount       = 7
	PatternBankCount = 4
	MaxChainLength   = 16
	MaxCountInBars   = 2
	NoBank           = -1

	// MaxSteps is the pattern capacity. SetStepCount sets the master/displayed
	// loop and initializes every track to that length; SetTrackLength then lets
	// tracks diverge for polymeter. Steps are 16th notes, so 16 steps span one
	// 4/4 bar.
	MaxSteps = 16
)
View Source
const PatternSize = TrackCount * MaxSteps
View Source
const ProtocolVersion = 6

ProtocolVersion identifies the semantics of the AlgoDrum JS API and the worker/worklet transport snapshots built on top of it. audioWorker.ts pins the same number and refuses to run against a mismatching engine, while wasmEngine.ts uses it to cache-bust the independently cached worklet.

Bump it whenever an existing entry point changes meaning rather than existing: argument order or count, the unit or range of an argument, the shape of EngineState, or what a call does. Purely *adding* a method needs no bump — the worker's REQUIRED_METHODS already rejects an engine that lacks one.

This is unrelated to persistence.ts's FORMAT_VERSION, which versions saved patterns rather than the live API.

Variables

This section is empty.

Functions

func VoiceName

func VoiceName(track int) string

VoiceName returns the display name of one voice, or "" for a bad track.

Types

type BassDrum

type BassDrum struct {
	// contains filtered or unexported fields
}

func NewBassDrum

func NewBassDrum(sr float64) *BassDrum

func (*BassDrum) IsActive

func (v *BassDrum) IsActive() bool

func (*BassDrum) Param

func (b *BassDrum) Param(index int) float64

Param returns the normalized position of one parameter, or 0 for an out-of-range index.

func (*BassDrum) ParamSpecs

func (b *BassDrum) ParamSpecs() []ParamSpec

ParamSpecs returns the voice's parameter descriptors, in index order.

func (*BassDrum) SetDecay

func (v *BassDrum) SetDecay(amount float64)

func (*BassDrum) SetParam

func (v *BassDrum) SetParam(index int, value01 float64)

func (*BassDrum) Tick

func (v *BassDrum) Tick() float64

func (*BassDrum) Trigger

func (v *BassDrum) Trigger(velocity float64)

type Cymbal

type Cymbal struct {
	// contains filtered or unexported fields
}

func NewCymbal

func NewCymbal(sr float64) *Cymbal

func (*Cymbal) IsActive

func (v *Cymbal) IsActive() bool

func (*Cymbal) Param

func (b *Cymbal) Param(index int) float64

Param returns the normalized position of one parameter, or 0 for an out-of-range index.

func (*Cymbal) ParamSpecs

func (b *Cymbal) ParamSpecs() []ParamSpec

ParamSpecs returns the voice's parameter descriptors, in index order.

func (*Cymbal) SetDecay

func (v *Cymbal) SetDecay(amount float64)

func (*Cymbal) SetParam

func (v *Cymbal) SetParam(index int, value01 float64)

func (*Cymbal) Tick

func (v *Cymbal) Tick() float64

func (*Cymbal) Trigger

func (v *Cymbal) Trigger(velocity float64)

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is the drum machine sequencer and mixer.

func NewEngine

func NewEngine(sr float64) *Engine

NewEngine creates a drum engine at the given sample rate. A non-finite or non-positive rate falls back to defaultSampleRate; any other value is clamped to [minSampleRate, maxSampleRate].

func (*Engine) ActiveBank

func (e *Engine) ActiveBank() int

ActiveBank reports the bank currently driving the sequencer.

func (*Engine) BeginStart

func (e *Engine) BeginStart()

BeginStart records a requested start before the browser's asynchronous audio graph is ready. Render does not advance the sequencer in this state; SetRunning(true) commits the transition once audio output has resumed.

func (*Engine) ChainPosition

func (e *Engine) ChainPosition() int

ChainPosition reports the active entry within Chain. It is runtime state and resets to zero on Stop.

func (*Engine) CopyPattern

func (e *Engine) CopyPattern(dst *[PatternSize]float32)

CopyPattern writes the full pattern into caller-owned storage without allocating. float32 matches the WASM wire format.

func (*Engine) CurrentStep

func (e *Engine) CurrentStep() int

func (*Engine) IsIdle

func (e *Engine) IsIdle() bool

IsIdle reports whether Render is producing nothing but silence and can be stopped being called: the output has stayed below engineSilence for idleConfirmS while the transport was not playing and no humanize-delayed hit was armed. It is the engine's half of the "stop the audio graph when there is nothing to hear" contract — the worklet/worker side decides what to do with it.

This truncates a decaying tail rather than rendering it to the last denormal: the reverb and the voice envelopes are exponential and never reach exactly zero, so waiting for a bit-exact zero would mean never idling at all. What is discarded is everything below −120 dBFS, which is over two orders of magnitude under the quantisation step of the 16-bit output it eventually reaches and far under the noise floor of any playback chain, so the cut is inaudible by construction. The consequence to be aware of is that renders are no longer bit-identical to a build without idling once a tail crosses the threshold — hence the tests hold idling to "nothing above engineSilence was lost" rather than to sample equality.

func (*Engine) Muted

func (e *Engine) Muted(track int) bool

Muted reports one track's mute state. Invalid tracks report false.

func (*Engine) Pause

func (e *Engine) Pause()

Pause freezes sequencer time and delayed humanized hits while allowing already-triggered voices and effects to ring out. SetRunning(true) resumes from the held fractional position; SetRunning(false) performs a full stop.

func (*Engine) QueuedBank

func (e *Engine) QueuedBank() int

QueuedBank reports the standalone bank requested for a future master wrap, or NoBank when there is no outstanding request.

func (*Engine) Render

func (e *Engine) Render(buf []float32)

Render fills buf with mono audio samples.

The invariants the loop relies on — a positive duration for every step, the playhead inside the loop, no pending trigger past its deadline — are checked per buffer, on entry and on exit, only in builds tagged `drumassert`; the shipped build compiles assertValid away to nothing (see assert.go).

func (*Engine) ReplacePatternBank

func (e *Engine) ReplacePatternBank(bank int, state PatternBankState) error

ReplacePatternBank atomically validates and replaces one complete rhythmic bank. Invalid indexes are silent no-ops, matching the indexed setter contract; malformed state is reported without mutating any bank.

func (*Engine) ReplaceState

func (e *Engine) ReplaceState(state EngineState) error

ReplaceState atomically validates and normalizes a full user-state snapshot, then replaces the engine's corresponding values. Structural mistakes and every NaN/Inf reject the whole snapshot before mutation. Finite numeric input follows the setters' existing contract and is clamped into its valid range. Runtime state (transport position, active tails and smoothing positions) is preserved so applying a preset during playback does not restart the machine.

func (*Engine) RequestBank

func (e *Engine) RequestBank(bank int)

RequestBank selects a standalone bank. Stopped requests take effect immediately; playing and paused requests are last-write-wins at a master wrap. Chain mode owns selection and therefore ignores manual requests.

func (*Engine) SetCell

func (e *Engine) SetCell(bank, track, step int, velocity float64)

SetCell sets a cell's velocity, clamped to [0, 1] (0 = off). Steps are addressable up to MaxSteps regardless of the active step count, so shrinking and re-growing the pattern is lossless.

Out-of-range contract: an invalid track or step index is a silent no-op, as is a non-finite velocity — the cell keeps its previous value. Every indexed setter behaves this way (SetVolume, SetDecay), because the JS bridge feeds unvalidated arguments straight through and must never take the engine down.

func (*Engine) SetCellCondition

func (e *Engine) SetCellCondition(bank, track, step int, condition TriggerCondition)

SetCellCondition sets one cell's loop/fill condition. Unknown numeric codes are rejected so persisted state cannot silently acquire different semantics.

func (*Engine) SetCellHumanize

func (e *Engine) SetCellHumanize(bank, track, step int, amount float64)

SetCellHumanize sets one cell's multiplier for the global Humanize amount. Defaults are 1, so older patterns retain their existing global response.

func (*Engine) SetCellProbability

func (e *Engine) SetCellProbability(bank, track, step int, probability float64)

SetCellProbability sets one cell's probability multiplier. It is combined with the global Probability control at trigger time. Defaults are 1, so old patterns and the mechanical render path remain sample-exact.

func (*Engine) SetCellRepeats

func (e *Engine) SetCellRepeats(bank, track, step, repeats int)

SetCellRepeats sets the number of evenly spaced hits emitted by an eligible cell. Invalid indexes no-op and counts clamp to [1, 4].

func (*Engine) SetChain

func (e *Engine) SetChain(chain []int)

SetChain atomically replaces the 1..MaxChainLength bank sequence. It is a stopped-only edit so the cursor and already-rendered lookahead cannot drift from the persisted configuration.

func (*Engine) SetChainEnabled

func (e *Engine) SetChainEnabled(enabled bool)

SetChainEnabled toggles chain playback while stopped. Enabling selects the first chain entry; disabling returns to the standalone selection.

func (*Engine) SetCountInBars

func (e *Engine) SetCountInBars(bars int)

SetCountInBars selects a stopped-start count-in of zero, one or two fixed 4/4 bars. A count already in progress keeps the length latched at its start.

func (*Engine) SetDecay

func (e *Engine) SetDecay(track int, amount float64)

SetDecay sets per-track decay amount, clamped to [0, 1]. An out-of-range track or a non-finite amount is a silent no-op (see SetCell).

func (*Engine) SetFillMode

func (e *Engine) SetFillMode(enabled bool)

SetFillMode enables or disables cells marked TriggerFillOnly. It is semantic configuration state rather than transport state so snapshots and shares can reproduce what the engine will play.

func (*Engine) SetHumanize

func (e *Engine) SetHumanize(h float64)

SetHumanize sets the humanize amount, clamped to [0, 1]. It jitters each hit's timing (delayed up to humanize·15 ms) and scales its velocity by up to ±humanize·20%. 0 = mechanical (default). A non-finite value is rejected and leaves the current amount unchanged.

func (*Engine) SetMetronomeEnabled

func (e *Engine) SetMetronomeEnabled(enabled bool)

SetMetronomeEnabled controls quarter-note clicks during ordinary playback. Count-in clicks are always audible regardless of this setting.

func (*Engine) SetMuted

func (e *Engine) SetMuted(track int, muted bool)

SetMuted changes one track's mute state without changing its stored volume. Render ramps toward zero while muted and back toward the stored volume when unmuted, so both transitions inherit the zipper-noise protection of the volume control.

func (*Engine) SetPattern

func (e *Engine) SetPattern(bank int, velocities []float64)

SetPattern atomically replaces the full flat track-major pattern (index = track*MaxSteps + step). A wrong-sized snapshot or any non-finite entry is rejected as a whole; finite velocities are clamped to [0, 1].

func (*Engine) SetPhysicalTomParam

func (e *Engine) SetPhysicalTomParam(track, index int, value01 float64)

SetPhysicalTomParam updates one Tom's independent physical parameter bank. It is valid while either model is selected so A/B edits survive a model switch. Invalid tracks, indices, and non-finite values are ignored.

func (*Engine) SetProbability

func (e *Engine) SetProbability(p float64)

SetProbability sets the global probability multiplier, clamped to [0, 1]. Each hit's effective chance is this value times its CellProbability. A global value of 1 leaves cell probabilities unscaled (the default), while 0 silences every sequenced hit. A non-finite value is rejected and leaves the current probability unchanged.

func (*Engine) SetReverb

func (e *Engine) SetReverb(amount float64)

SetReverb sets the target reverb amount in [0, 1]. Render smooths the wet gain to that target; 0 = fully dry, 1 = maximum (wet=reverbMaxWet, RT60=4 s). A non-finite amount is rejected and leaves the current setting unchanged.

func (*Engine) SetRunning

func (e *Engine) SetRunning(running bool)

func (*Engine) SetStepCount

func (e *Engine) SetStepCount(bank, count int)

SetStepCount sets the master/displayed loop length, clamped to [1, MaxSteps], and applies it to every track as the backwards-compatible global-length operation. Call SetTrackLength afterward to create a polymeter. Cells beyond a new length keep their contents (see SetCell). Step durations are recomputed because swing pairs steps within the master loop.

func (*Engine) SetSwing

func (e *Engine) SetSwing(swing float64)

SetSwing sets the swing amount, clamped to [0, maxSwing]. A non-finite value is rejected and leaves the current swing unchanged.

func (*Engine) SetTempo

func (e *Engine) SetTempo(bpm float64)

SetTempo sets the tempo, clamped to [minTempoBPM, maxTempoBPM]. A non-finite value is rejected and leaves the current tempo unchanged.

func (*Engine) SetTomModel

func (e *Engine) SetTomModel(track int, model TomModel)

SetTomModel explicitly selects one Tom track's implementation. Procedural remains the default, and invalid tracks or values are ignored. Switching resets both sides so a dormant tail cannot resume later.

func (*Engine) SetTrackLength

func (e *Engine) SetTrackLength(bank, track, count int)

SetTrackLength sets one track's independent loop length, clamped to [1, MaxSteps]. The master StepCount continues to define the displayed playhead and swing loop; track playheads advance continuously across master wraps, which is what makes non-dividing lengths a true polymeter.

func (*Engine) SetVoiceParam

func (e *Engine) SetVoiceParam(track, index int, value01 float64)

SetVoiceParam sets one of a voice's synthesis parameters from a normalized [0, 1] position; see params.go for the per-voice tables. An out-of-range track or index, or a non-finite value, is a silent no-op (see SetCell).

Unlike SetVolume/SetDecay the engine keeps no mirror of the value — it lives in the voice, reachable via Voice.Param.

func (*Engine) SetVolume

func (e *Engine) SetVolume(track int, vol float64)

SetVolume sets per-track volume, clamped to [0, 1]. The change ramps in over ~volSmoothTauS inside Render to avoid zipper noise. An out-of-range track or a non-finite volume is a silent no-op (see SetCell).

func (*Engine) State

func (e *Engine) State() EngineState

State returns a deep snapshot of every user-controlled value. Callers own all returned slices and may modify them without aliasing the live engine.

func (*Engine) TomModel

func (e *Engine) TomModel(track int) TomModel

TomModel reports one Tom track's selected implementation. Invalid tracks report the procedural default.

func (*Engine) TransportSnapshot

func (e *Engine) TransportSnapshot() TransportSnapshot

TransportSnapshot returns the engine-owned transport state, its logical playhead and a revision that identifies the current transport epoch.

func (*Engine) TriggerVoice

func (e *Engine) TriggerVoice(track int, velocity float64)

TriggerVoice fires one voice immediately, independent of the sequencer, so the UI can audition a voice while the transport is stopped. An out-of-range track, a non-finite velocity, or a velocity of 0 is a silent no-op.

Triggering advances a noise voice's RNG stream, so an audition shifts the noise a later rendered hit will draw — see docs/voices.md.

func (*Engine) Validate

func (e *Engine) Validate() error

Validate reports every engine invariant that is currently broken, joined into one error (nil when the state is sound). The setters already refuse out-of-contract input (see validFloat), so a violation here means the engine corrupted itself — the C11 swing bug rewrote step lengths from a valid SetStepCount call — and that class of defect is otherwise silent: Render's final clamp mutes a stray NaN and a wrong step length only sounds off.

It is a pure read of the engine's own state and allocates nothing while the state is sound, so it is cheap enough for tests, the fuzz target and the per-render assertion built under the `drumassert` tag (see assert.go).

type EngineState

type EngineState struct {
	TempoBPM         float64
	Swing            float64
	Reverb           float64
	Probability      float64
	Humanize         float64
	FillMode         bool
	MetronomeEnabled bool
	CountInBars      int
	Banks            []PatternBankState
	StandaloneBank   int
	ChainEnabled     bool
	Chain            []int
	Tracks           []TrackState
}

EngineState is the complete user-controlled engine state. Each bank owns its rhythmic configuration; mixer, voice and global performance controls remain shared. Active/queued bank and chain cursor are runtime-only. The transport/playheads, conditional pass history, live smoothing values, active tails and RNG position are deliberately runtime state rather than preset state.

type HiHat

type HiHat struct {
	// contains filtered or unexported fields
}

func NewHiHat

func NewHiHat(sr float64) *HiHat

func (*HiHat) IsActive

func (v *HiHat) IsActive() bool

func (*HiHat) Param

func (b *HiHat) Param(index int) float64

Param returns the normalized position of one parameter, or 0 for an out-of-range index.

func (*HiHat) ParamSpecs

func (b *HiHat) ParamSpecs() []ParamSpec

ParamSpecs returns the voice's parameter descriptors, in index order.

func (*HiHat) SetDecay

func (v *HiHat) SetDecay(amount float64)

func (*HiHat) SetParam

func (v *HiHat) SetParam(index int, value01 float64)

func (*HiHat) Tick

func (v *HiHat) Tick() float64

func (*HiHat) Trigger

func (v *HiHat) Trigger(velocity float64)

type ParamSpec

type ParamSpec = tomparams.Spec

The spec type and its normalized→engineering mapping live in github.com/cwbudde/algo-tom/tomparams, because the physical Tom's mapping had to move there — an offline fitter that reused a copy would measure a different instrument than the one that ships — and the five procedural voices are described by the very same curve machinery.

These are deliberately *aliases*, not defined types. A defined type would give this package its own Map, its own byte-step snap and its own Default derivation, and a drift between the two copies would silently retune the shipped sound with nothing but ears to catch it. As aliases there is exactly one of each in existence, cmd/gen-voiceparams needs no source change, and web/src/engine/voiceParams.generated.ts comes out byte-identical.

func PhysicalTomSpecs

func PhysicalTomSpecs() []ParamSpec

PhysicalTomSpecs returns the generated descriptor source for the physical Tom editor. Its indices are stable persistence and WASM command addresses.

func SpecsForTrack

func SpecsForTrack(track int) []ParamSpec

SpecsForTrack returns the parameter descriptors of one voice, or nil for an out-of-range track. Used by cmd/gen-voiceparams to generate the TypeScript mirror the UI renders from.

type PatternBankState

type PatternBankState struct {
	StepCount         int
	Pattern           []float64
	CellProbabilities []float64
	CellHumanize      []float64
	CellConditions    []TriggerCondition
	CellRepeats       []uint8
	TrackLengths      []int
}

PatternBankState is one owned rhythmic snapshot. Cell arrays are flat track-major with PatternSize entries; TrackLengths is engine-major.

type Percussion

type Percussion struct {
	// contains filtered or unexported fields
}

func NewPercussion

func NewPercussion(sr float64) *Percussion

func (*Percussion) IsActive

func (v *Percussion) IsActive() bool

func (*Percussion) Param

func (b *Percussion) Param(index int) float64

Param returns the normalized position of one parameter, or 0 for an out-of-range index.

func (*Percussion) ParamSpecs

func (b *Percussion) ParamSpecs() []ParamSpec

ParamSpecs returns the voice's parameter descriptors, in index order.

func (*Percussion) SetDecay

func (v *Percussion) SetDecay(amount float64)

func (*Percussion) SetParam

func (v *Percussion) SetParam(index int, value01 float64)

func (*Percussion) Tick

func (v *Percussion) Tick() float64

func (*Percussion) Trigger

func (v *Percussion) Trigger(velocity float64)

type Snare

type Snare struct {
	// contains filtered or unexported fields
}

func NewSnare

func NewSnare(sr float64) *Snare

func (*Snare) IsActive

func (v *Snare) IsActive() bool

func (*Snare) Param

func (b *Snare) Param(index int) float64

Param returns the normalized position of one parameter, or 0 for an out-of-range index.

func (*Snare) ParamSpecs

func (b *Snare) ParamSpecs() []ParamSpec

ParamSpecs returns the voice's parameter descriptors, in index order.

func (*Snare) SetDecay

func (v *Snare) SetDecay(amount float64)

func (*Snare) SetParam

func (v *Snare) SetParam(index int, value01 float64)

func (*Snare) Tick

func (v *Snare) Tick() float64

func (*Snare) Trigger

func (v *Snare) Trigger(velocity float64)

type Tom

type Tom struct {
	// contains filtered or unexported fields
}

func NewTom

func NewTom(sr float64) *Tom

func NewTom2

func NewTom2(sr float64) *Tom

NewTom2 creates the higher-pitched second tom with its own editable bank.

func (*Tom) IsActive

func (v *Tom) IsActive() bool

func (*Tom) Param

func (b *Tom) Param(index int) float64

Param returns the normalized position of one parameter, or 0 for an out-of-range index.

func (*Tom) ParamSpecs

func (b *Tom) ParamSpecs() []ParamSpec

ParamSpecs returns the voice's parameter descriptors, in index order.

func (*Tom) Reset

func (v *Tom) Reset()

func (*Tom) SetDecay

func (v *Tom) SetDecay(amount float64)

func (*Tom) SetParam

func (v *Tom) SetParam(index int, value01 float64)

func (*Tom) Tick

func (v *Tom) Tick() float64

func (*Tom) Trigger

func (v *Tom) Trigger(velocity float64)

type TomModel

type TomModel uint8

TomModel selects the implementation used by either Tom track.

const (
	// TomModelProcedural preserves the original swept-sine Tom.
	TomModelProcedural TomModel = iota
	// TomModelPhysical selects the experimental double-headed modal model.
	TomModelPhysical
)

type TomState

type TomState struct {
	Model          TomModel
	PhysicalParams []float64
}

TomState carries the state unique to the two Tom tracks. PhysicalParams is present even for a procedural Tom, so switching models never discards the inactive bank and a snapshot does not need to instantiate the physical model.

type TrackState

type TrackState struct {
	Volume      float64
	Decay       float64
	Muted       bool
	VoiceParams []float64
	Tom         *TomState
}

TrackState is one engine-major track. Volume remains meaningful while Muted is true; muting changes only the smoothed render target. VoiceParams always describes the procedural/ordinary voice bank, including on a Tom whose physical model is currently selected.

type TransportSnapshot

type TransportSnapshot struct {
	State    TransportState
	Step     int
	Revision uint64
}

TransportSnapshot identifies one transport epoch and its playhead. Revision changes at every state transition, which lets the main thread reject chunks rendered before a Stop, Pause or restart even when they reach the speakers later from the worklet's queue.

type TransportState

type TransportState string

TransportState is the semantic transport state exposed at the WASM boundary. The sequencer remains the authority for this value; the worker and UI only mirror snapshots returned by TransportSnapshot.

const (
	TransportStopped    TransportState = "stopped"
	TransportStarting   TransportState = "starting"
	TransportCountingIn TransportState = "counting-in"
	TransportPlaying    TransportState = "playing"
	TransportPaused     TransportState = "paused"
)

type TriggerCondition

type TriggerCondition uint8

TriggerCondition controls whether an active cell is eligible on a given pass through its track's independent loop. The numeric values are part of the EngineState/WASM wire contract; append new values rather than reordering these constants.

const (
	TriggerAlways TriggerCondition = iota
	TriggerEvery2
	TriggerEvery3
	TriggerEvery4
	TriggerFirstLoop
	TriggerFillOnly
	TriggerNotPreviousFired
)

type Voice

type Voice interface {
	// Trigger starts (or restarts) the voice; velocity in [0, 1] scales
	// the level of the whole hit.
	Trigger(velocity float64)
	Tick() float64
	// SetDecay trims the voice's base decay time by decayScaleMin + amount.
	// The base itself is a synthesis parameter (see params.go), so the
	// effective decay is base × (decayScaleMin + amount).
	SetDecay(amount float64)
	// SetParam sets one synthesis parameter from a normalized [0, 1]
	// position; an out-of-range index or non-finite value is a no-op.
	SetParam(index int, value01 float64)
	Param(index int) float64
	ParamSpecs() []ParamSpec
}

Voice is a single-shot drum synthesizer voice.

Jump to

Keyboard shortcuts

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