MIDI & Audio Input¶
This guide covers the midi() and audio() functions for animating shader parameters with real-time external input.
Using in the Demo¶
The shader demo includes MIDI and Audio toggle buttons in the toolbar. It also attempts to enable the relevant manager when applying a program that uses live input. Click midi or audio to control access:
MIDI: Click the
midibutton to enable Web MIDI API access. The demo will automatically detect connected MIDI controllers.Audio: Click the
audiobutton to enable microphone input. Your browser will request permission to access the microphone.
The demo’s audio toggle supplies the aggregate frequency analyser. Selecting
individual audio devices/channels and supplying audioBand.raw require a
host with those capture paths, such as Noisedeck. The language does not open
devices or create operating-system audio ports by itself.
Once enabled, you can use midi() and audio() in your DSL programs:
search synth
// React to MIDI velocity and audio bass at the same time
noise(
scaleX: midi(channel: 1, min: 0.1, max: 1),
speed: audio(band: audioBand.low, min: 0.25, max: 0.75)
).write(o0)
render(o0)
Quick Start¶
MIDI Input¶
Control a parameter with MIDI velocity from channel 1:
search synth
noise(scaleX: midi(channel: 1, min: 0.1, max: 1)).write(o0)
Audio Input¶
React to bass frequencies in the audio input:
search synth
noise(scaleX: audio(band: audioBand.low, min: 0.1, max: 0.5)).write(o0)
Automation values are normalized percentages. min: 0.1, max: 0.5 maps
the source across 10%–50% of the receiving effect parameter’s range. Those
bounds are not absolute parameter values.
midi() Function¶
The midi() function provides automation values from MIDI controller input.
Syntax¶
midi(channel, mode?, min?, max?, sensitivity?, cc: N, nrpn: N, name: "...", id: "...")
midi(zone: midiZone.lower, members: N, mode: midiMode.pressure, name: "...", id: "...")
cc, nrpn, zone, members, name, and id are keyword-only.
Use either channel or zone. members is optional and requires
zone; nrpn is required only for midiMode.nrpn. Existing positional
arguments retain their order.
Parameters¶
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
int (1-16) |
required without |
Fixed MIDI channel to listen to. Cannot be combined with |
|
|
none |
|
|
int (1-15) |
automatic |
Manual member-channel count for the selected zone. Omit to follow received MPE zone configuration, or use 15 until configured. |
|
|
|
How to interpret MIDI data |
|
number or automation |
0 |
Minimum normalized output (0–1) |
|
number or automation |
1 |
Maximum normalized output (0–1) |
|
number or automation |
1 |
Trigger falloff rate (higher = faster decay) |
|
integer |
1 |
Controller number for CC modes. Keyword-only. 0–127 for |
|
int (0-16382) |
required for NRPN mode |
NRPN parameter address. Address 16383 is reserved for null selection. |
|
quoted string |
none |
Readable MIDI input name. Keyword-only. |
|
quoted string |
none |
Exact MIDI input ID. Keyword-only. Requires |
MIDI Modes¶
Mode |
Description |
|---|---|
|
Value from note number (0-127), ignores gate state |
|
Value from note number only while key is held |
|
Value from velocity only while key is held |
|
Note value with decay envelope from note-on |
|
Velocity with decay envelope (default) |
|
Last 7-bit value of the selected controller, normalized from 0–127 |
|
Last paired 14-bit controller value, normalized from 0–16383 |
|
Last value of the selected NRPN parameter, normalized from 0–16383 |
|
14-bit pitch-bend position divided by 16383; neutral is 8192 |
|
Channel pressure divided by 127 |
|
Polyphonic key pressure divided by 127, for the selected note key |
Numeric mode values 0–6 retain their existing meanings. The added modes are
nrpn: 7, pitchBend: 8, pressure: 9, and polyPressure: 10.
Use the enum names in programs for readability.
Control Change (CC)¶
Use CC modes for knobs and faders. Select the device, MIDI channel, controller, and resolution independently:
search synth
let knob = midi(channel: 1, mode: midiMode.cc, cc: 74,
name: "Controller", id: "browser-port-id")
let fader = midi(channel: 2, mode: midiMode.cc14, cc: 1,
name: "Controller", id: "browser-port-id")
noise(scaleX: knob, speed: fader).write(o0)
For midiMode.cc14, cc: N selects the most significant byte (MSB)
controller, and controller N + 32 supplies the least significant byte
(LSB). For example, cc: 1 pairs controllers 1 and 33. The normalized value
is (MSB * 128 + LSB) / 16383. This follows the
MIDI 1.0 Control Change pairing convention.
Use the separate NRPN and pitch-bend modes for those message formats.
Each device and MIDI channel retains its own bytes. Either byte updates the value immediately using the retained other byte; a byte not yet received is zero. Messages from different devices or channels never form a pair. An unselected aggregate source uses the complete value from the input that most recently updated that controller pair.
Updating on either byte also supports sources that omit unchanged bytes. If an MSB arrives before its new LSB, the intermediate value still contains the old LSB and can produce a brief step. The runtime does not wait for an LSB or apply a pairing timeout. Hosts normally sample the latest value once per frame.
CC values hold until another matching message or a state reset/disconnection.
Notes and note-off messages do not change them, and sensitivity has no
effect on CC modes. Initially the value resolves to min; controller
resets can set protocol defaults without receiving that controller directly
(for example, CC11 becomes 127). MPE zone changes also initialize CC74 to 64.
Invalid CC numbers or channel numbers produce diagnostics and resolve to
min. All channel-based modes require static integer channels 1–16.
NRPN parameters¶
NRPN uses CC99/98 to select a 14-bit parameter address, followed by Data Entry
CC6/38 for its value. Select the address with nrpn:
search synth
let parameter = midi(channel: 3, mode: midiMode.nrpn, nrpn: 4097,
name: "Controller", id: "browser-port-id")
noise(scaleX: parameter).write(o0)
Selectors and stored values are isolated by physical port, MIDI channel, and
parameter address. Both selector bytes must arrive before the first write;
later changes to one selector byte retain the other. Selecting another
parameter does not erase the previous value. CC6 sets the coarse byte and
clears that parameter’s fine byte; CC38 updates its fine byte using its own
retained coarse byte. This differs from the hold-other-byte policy for
ordinary cc14. The normalized value is (MSB * 128 + LSB) / 16383.
CC101/100 switch Data Entry to RPN, so an RPN transaction cannot overwrite the previously selected NRPN. Null address 16383 disables parameter writes. CC96/97 increment/decrement the selected NRPN by one, saturating at 0 and 16383 and ignoring the message’s value byte. This is the scalar adapter’s step policy: the MIDI specification leaves NRPN step units to the device manufacturer. See the MIDI 1.0 addenda (RP-018).
Before a matching parameter value arrives, the binding returns min.
Values survive note-off, selector changes, and Reset All Controllers (CC121).
CC121 nulls parameter selectors, centers pitch bend, and clears pressure,
while preserving Sound Controllers 70–79, including MPE CC74. Full state
reset or port disconnection clears stored parameter values. See
Reset All Controllers.
midiMode.cc14 with cc: 6 still reads the ordinary Data Entry byte
pair without regard to the selected parameter. Use midiMode.nrpn when
the parameter address matters.
MPE and pressure¶
MPE assigns notes to member channels. Use zone: midiZone.lower or
zone: midiZone.upper to follow a held note as the controller allocates
channels. The lower zone’s manager is channel 1 and its members ascend from
channel 2; the upper manager is channel 16 and its members descend from
channel 15. Device identity remains independent of zone selection.
search synth
let pressure = midi(zone: midiZone.lower, mode: midiMode.pressure,
name: "MPE Controller", id: "browser-port-id")
let bend = midi(zone: midiZone.lower, mode: midiMode.pitchBend,
name: "MPE Controller", id: "browser-port-id")
let slide = midi(zone: midiZone.lower, mode: midiMode.cc, cc: 74,
name: "MPE Controller", id: "browser-port-id")
noise(scaleX: pressure, scaleY: bend, speed: slide).write(o0)
Without members, each port follows received MPE Configuration Messages:
RPN 6 (CC101=0, CC100=6), followed by CC6 count 0–15 on manager channel 1 or
16. CC38 has no effect on this configuration. Count 0 disables that zone;
the newest configuration takes overlapping members from the other zone.
Until configuration is received, a binding uses all 15 possible members of
its selected zone. An explicit members: 1 through members: 15 overrides
discovery for that binding. The enum values are midiZone.lower = 0 and
midiZone.upper = 1. See the
MPE specification.
When zone configuration changes a channel’s membership or manager role, the runtime clears its held notes and resets expression. It initializes CC74 to 64 as a neutral timbre default for this adapter. This is separate from CC121, which preserves the existing CC74 value. Configuration therefore can end the currently selected gesture; later Note On messages select new notes.
Each binding returns one scalar. It selects the newest held note by Note On
arrival order across eligible members and ports. Controller messages do not
change note priority. Releasing that note immediately falls back to the
newest remaining held note; no held note returns min in every mode.
Sustain does not retain released notes for this selection. Distinct keys on
one member remain tracked; a repeated Note On for the same port/channel/key
replaces that key’s previous note. An old key’s Note Off cannot release a
different key that later reused the member channel.
The selected note supplies note number, velocity, and trigger time to note
modes, and its member supplies CC, NRPN, bend, and pressure. Expression
received before Note On remains available. Polyphonic pressure uses that
note’s key. Without a zone, polyPressure uses the fixed channel’s latest
note key; pitchBend and pressure read the fixed channel directly.
These are raw member gestures. Pitch bend is a normalized controller
position, not a semitone value; RPN 0 pitch sensitivity does not rescale it.
The adapter does not combine manager and member expression or synthesize a
polyphonic instrument. Read manager controls separately with channel: 1
or channel: 16. On a connected fixed channel, bend starts at its neutral
8192/16383 position and pressure starts at zero. sensitivity affects
only the existing trigger modes.
Protocol boundaries¶
MIDI 2.0 controllers and general RPN parameter automation are not exposed. MPE zone configuration uses RPN 6 internally.
Choose CC mode according to the controller’s transmitted messages. Encoder sensitivity settings described as high resolution do not by themselves prove that the device sends paired 14-bit CC. These modes interpret absolute values; they do not accumulate relative encoder increments.
Trigger Modes¶
The triggerNote and velocity modes include automatic decay from the note-on event. The sensitivity parameter controls how quickly the value fades:
sensitivity: 1- Decays over ~1 secondsensitivity: 5- Decays over ~200mssensitivity: 0.5- Decays over ~2 seconds
Both modes return to min when the channel’s gate closes, even if the decay
has not finished. Fixed-channel note modes summarize that channel’s most
recent note state. MPE zone bindings use the held-note selection described
above. Neither creates a separate automation output for every held key.
Selecting a MIDI input¶
Without name or id, midi() reads the aggregate MIDI state for
backward compatibility. To bind automation to one input, persist both the
human-readable name and the browser-provided ID:
midi(
channel: 1,
mode: midiMode.gateVelocity,
name: "Launchkey Mini",
id: "browser-port-id"
)
An id match is authoritative. A name-only selector is allowed, but it must
match exactly one connected input. Otherwise, the source resolves to its
minimum. name and id are quoted, keyword-only fields, and id is
invalid without name.
Device selection and MIDI channel selection are independent: several bindings can listen to different channels on one input, or the same channel number on different inputs. IDs are opaque host-provided values; copy them from the host’s device selection UI rather than inventing or deriving them from names.
Examples¶
search synth, filter
// Basic velocity response
noise(scaleX: midi(channel: 1)).write(o0)
// Note pitch controls warp strength
noise().warp(strength: midi(channel: 1, mode: midiMode.noteChange, min: 0, max: 1)).write(o0)
// Velocity with fast decay for percussive response
noise().bloom(intensity: midi(channel: 10, mode: midiMode.velocity, sensitivity: 5, min: 0, max: 1)).write(o0)
// Sustained note control
noise(scaleX: midi(channel: 2, mode: midiMode.gateVelocity, min: 0.1, max: 1)).write(o0)
audio() Function¶
The audio() function provides automation values from audio input analysis.
Syntax¶
audio(band, min?, max?, channel: N, name: "...", id: "...")
channel, name, and id are optional, keyword-only arguments.
Parameters¶
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
|
required |
Frequency band to sample |
|
number or automation |
0 |
Minimum normalized output (0–1) |
|
number or automation |
1 |
Maximum normalized output (0–1) |
|
integer (1–32) |
none |
One-based capture channel. Without |
|
quoted string |
none |
Readable audio input name. Keyword-only. Requires |
|
quoted string |
none |
Exact audio device ID. Keyword-only. Requires |
Audio Bands¶
Band |
Description |
|---|---|
|
Low-frequency level supplied by the host (bass/kick) |
|
Mid-frequency level supplied by the host |
|
High-frequency level supplied by the host (for example, hi-hats) |
|
Overall level supplied by the host |
|
Bipolar time-domain signal. Maps -1…1 onto the normalized 0…1 range. |
Frequency boundaries depend on the host’s analyser, FFT size, and sample rate.
band chooses an analysis value; it does not select an input channel.
Selecting an audio input¶
audio() reads the legacy aggregate analyser when no selector is present.
Use channel alone to select one channel on the default input. For a named
device, supply both name and channel; include id when the host
exposes one. This distinguishes devices with duplicate names:
audio(
band: audioBand.raw,
channel: 2,
name: "USB Audio Interface",
id: "browser-device-id"
)
The ID is authoritative. Without an ID, the name must match exactly one
connected device in the complete device inventory, including devices that are
not currently being captured. A disconnected, missing, ambiguous, or
unavailable channel resolves to min without falling back to another input.
audioBand.raw also resolves to min until the host has supplied a real
time-domain sample. Once ready, raw silence (zero) maps to the midpoint between
min and max.
Device and channel are separate dimensions. These bindings select two channels from one device, a channel from another device, and a default-device channel:
search synth
let left = audio(audioBand.low, channel: 1,
name: "Interface A", id: "device-a")
let right = audio(audioBand.high, channel: 2,
name: "Interface A", id: "device-a")
let second = audio(audioBand.vol, channel: 1,
name: "Interface B", id: "device-b")
let defaultRight = audio(audioBand.vol, channel: 2)
noise(scaleX: left, scaleY: right, speed: second, loopScale: defaultRight).write(o0)
Capture capabilities and channel limits¶
The language accepts channels 1–32. This is the Web Audio processing ceiling, not a promise that a physical input exposes 32 capture channels. The host must use the delivered stream’s channel count, and expose only channels it can actually process. Noisedeck shows that count after capture or a device probe; unknown counts remain marked unknown, and unavailable authored selections remain visible as unavailable.
The Chromium 152 Linux PulseAudio and ALSA input implementations request two channels per device. The PulseAudio compatibility path also applies when PipeWire provides that service. Chromium’s shared native input manager additionally enforces a three-channel stream guard and a browser-wide limit of 16 native input streams. Neither limit guarantees a particular number of simultaneously usable devices. See the pinned PulseAudio input implementation, ALSA input implementation, native capture limits, and Web Audio channel limit.
A 240-channel mixer therefore cannot expose all of its inputs through this capture path. Higher channel counts require a different capture integration.
Chromium’s Web Audio bridge is a second, separate limit. The sink that feeds a
MediaStreamAudioSourceNode from any track is fixed to a stereo layout, so
a wider track reaches the audio graph as a two-channel fold, whatever count the
track’s settings report. Channels past the second are unreachable through
createMediaStreamSource. Noisedeck reads wider tracks frame by frame with
MediaStreamTrackProcessor and plays them into the graph from an
AudioWorklet; without that API it reports two channels. See the pinned
Web Audio media stream sink.
Hosts that rely on createMediaStreamSource alone must report two channels
for such devices.
Repeated requests for the same physical device reuse Chromium’s native source;
they do not create independently patchable virtual input ports. See
Chromium device reuse.
Other hosts and backends must report their own delivered capabilities.
Validating a control-voltage input¶
Noisedeck’s raw capture uses the signed mean of each audio render quantum per channel. Browser tests with generated constant signals verify that this path preserves positive and negative offsets. They do not certify DC preservation through an operating-system virtual device or a physical interface.
Validate the exact capture route before relying on it for control voltages:
Send a constant offset and observe the selected
audioBand.rawvalue for several seconds. It should hold steady; decay toward zero indicates that the route is filtering out DC.Repeat with the opposite polarity, then a very slow bipolar LFO to check that its shape and sign survive.
Repeat for the selected channels on every device, with simultaneous inputs active. Record the browser/desktop version, OS, audio backend, delivered channel counts, and processing settings.
When testing with VCV Rack’s Audio-2 module, disable DC blocker in the module’s context menu. It is enabled by default and applies a 10 Hz high-pass filter, which removes a constant offset before the virtual audio device receives it. See the Rack Audio implementation.
Check source and virtual-device volume controls when comparing amplitudes. For example, BlackHole 0.5.0 applies its master volume to captured samples. Record that gain independently of the measured signal, or use a known unity gain, before comparing expected DC levels and LFO amplitudes. See the BlackHole capture implementation.
Digital routing from an application such as VCV Rack avoids an analog
converter, but the complete route still needs measurement. Physical modular
signals require an interface that passes DC. Conversion between volts and
normalized samples depends on the source and interface; audioBand.raw is
not a voltage measurement.
Examples¶
search synth, filter
// React to bass
noise(scaleX: audio(band: audioBand.low, min: 0.1, max: 0.5)).write(o0)
// Hi-hat triggers brightness
noise().bloom(intensity: audio(band: audioBand.high, min: 0, max: 1)).write(o0)
// Overall volume controls speed
noise().warp(speed: audio(band: audioBand.vol, min: 0.25, max: 0.75)).write(o0)
Combining with Other Automation¶
midi(), audio(), and osc() work alongside one another, and an
automation source can drive a numeric field on another automation descriptor.
Use let bindings to keep nested programs readable:
search synth
let floor = audio(band: audioBand.low)
let rate = midi(channel: 1, min: floor, max: 1)
let carrier = osc(type: oscKind.sine, speed: rate)
noise(scaleX: carrier).write(o0)
These numeric fields support nesting:
osc():min,max,speed,offset, andseedmidi():min,max, andsensitivityaudio():minandmax
Enum selectors, channel numbers, and MIDI cc, nrpn, and members are static: they accept
literals or let aliases that resolve to valid literals, not live
automation. Device names and IDs must remain quoted string literals. For
example, let controller = 74 can be used as cc: controller.
The compiler rejects cycles and supports up to eight
nested levels beneath the outer descriptor. The evaluator integrates
oscillator rate modulation over normalized time.
This integration does not depend on previously rendered frames. Live MIDI and
audio still use the current input state, and note decay uses wall-clock time;
seeking does not replay an earlier external signal.
Nested results map onto the receiving field’s range: 0–1 for min/max,
0–10 for MIDI sensitivity, −20–20 for oscillator speed, −1–1 for
offset, and 1–9999 for seed. A literal sensitivity: 1 is a direct
value; an automation result of 1 feeding sensitivity maps to 10.
Host Integration¶
For application developers integrating MIDI/Audio input with the pipeline. Capture and device discovery belong to the host; writing a DSL selector alone does not create a stream. See Shader Pipeline Integration for renderer setup.
Setting Up External State¶
// With an initialized CanvasRenderer, create and attach shared state.
const midiState = renderer.setMidiState()
const audioState = renderer.setAudioState()
Updating MIDI State¶
// DSL/API channels are 1–16; the wire status nibble is 0–15.
const midiChannel = 1
const statusChannel = midiChannel - 1
// Handle MIDI note on and note off.
midiState.handleMessage([0x90 | statusChannel, 60, 100])
midiState.handleMessage([0x80 | statusChannel, 60, 0])
// Controller 74, 7-bit value 100.
midiState.handleMessage([0xb0 | statusChannel, 74, 100])
// A 14-bit pair: controller 1 = MSB, controller 33 = LSB.
midiState.handleMessage([0xb0 | statusChannel, 1, 64])
midiState.handleMessage([0xb0 | statusChannel, 33, 1]) // 8193 / 16383
// Legacy fixed-channel aggregate state can also be updated directly.
midiState.getChannel(1).noteOn(60, 100)
Direct mutation of a root channel does not identify a physical source and
does not feed MPE zone selection. Use handleMessage(data, port) for normal
host integration, including MPE, so aggregate and per-port state stay aligned.
For per-input selectors, supply the full browser inventory before opening
ports. Register each port after it opens successfully and pass its identity
with each message. The root state still receives the message for unselected
midi() calls, while the registered port keeps isolated channel state:
midiState.setPortInventory(Array.from(access.inputs.values(), input => ({
id: input.id, name: input.name, connected: input.state !== 'disconnected'
})))
await input.open()
const port = { id: input.id, name: input.name }
midiState.registerPort(port)
midiState.handleMessage(message.data, port)
const ports = midiState.getPorts()
Inventory must include physically connected inputs whose opens are pending
or failed, so name-only selection can detect ambiguity. These inputs have no
live state until registered; exact selectors remain at min. Refresh the
inventory on device changes and call disconnectPort(input.id) when an
operational port disconnects. Install lifecycle handling before awaiting
opens so a disconnect during startup cannot leave stale state.
MidiState.getPorts() returns registered ports, including their disconnected
history. MidiInputManager.getPorts() returns the complete physical
inventory, including failed opens and retained disconnected entries. Its
connected flag describes physical presence, not successful opening.
Feed each received message once to the root state with its identity; do not also feed it directly to the isolated port state. This keeps aggregate CC14 pairing consistent with per-port values.
The same message entry point decodes NRPN, bend, both pressure formats, and
MPE configuration. For example, after registering port:
// Channel 3: NRPN address 4097, value 8193.
for (const [controller, value] of [[99, 32], [98, 1], [6, 64], [38, 1]]) {
midiState.handleMessage([0xb2, controller, value], port)
}
// Lower MPE zone with eight members (channels 2-9).
for (const [controller, value] of [[101, 0], [100, 6], [6, 8]]) {
midiState.handleMessage([0xb0, controller, value], port)
}
midiState.handleMessage([0xe1, 0, 64], port) // Member 2: neutral bend.
midiState.handleMessage([0xd1, 100], port) // Channel pressure has two bytes.
midiState.handleMessage([0xb1, 74, 96], port) // Member slide before Note On.
midiState.handleMessage([0x91, 60, 100], port)
midiState.handleMessage([0xa1, 60, 80], port) // Key pressure for note 60.
Updating Audio State¶
// The host has connected its aggregate source to this analyser.
function updateAudio() {
audioState.updateFromAnalyser(analyser)
requestAnimationFrame(updateAudio)
}
updateFromAnalyser() updates the bands, overall level, and 16-bin fft.
Supply full spectrum and waveform data through setSpectrum() and
setWaveform() when needed. Neither waveform data nor direct assignment to
raw marks raw input ready; use setRaw() for that.
For selected-device capture:
Supply the complete discovered device inventory for name resolution.
Open each requested device and inspect the delivered channel count.
Register each captured device and publish analyzed values per channel.
Supply bipolar
rawsamples foraudioBand.raw.
audioState.setDeviceInventory(discoveredDevices.map(device => ({
id: device.deviceId, name: device.label, connected: true
})))
audioState.registerDevice({
id: device.deviceId,
name: device.label,
channelCount: 2
})
audioState.setChannelValues(device.deviceId, 2, {
low: 0.25,
mid: 0.1,
high: 0.05,
vol: 0.14,
raw: -0.2
})
const devices = audioState.getDevices()
The example registers a stream that actually delivers two channels. Do not
substitute the mixer’s advertised channel count. Split the captured stream
before analyzing individual channels. registerDevice() only registers
state; it does not capture audio.
Default-device channels have separate state from the legacy aggregate:
audioState.registerDefaultChannels(2) // Actual delivered count.
const right = audioState.getDefaultChannelState(2)
right.updateFromAnalyser(rightChannelAnalyser)
right.setRaw(-0.2)
// When the raw processor stops or loses its input:
right.setRawUnavailable()
audioState.setDeviceRawUnavailable(device.deviceId)
// When captures end:
audioState.disconnectDefaultInput()
audioState.disconnectDevice(device.deviceId)
reset() clears values and raw readiness while retaining device identities
and connection flags. Use the disconnect methods when a capture ends.
resetAggregate() clears only the aggregate bands, raw readiness, sample
arrays, and smoothing history. Use it when stopping or replacing the legacy
source while selected-device and default-channel captures remain active.
Refresh the full inventory on device changes. Use
pipeline.getAudioInputRequirements() after compilation to collect selected
device/channel demands, including static aliases and nested automation. It
returns { needsLegacy, needsLegacyRaw, selected }; selected entries contain
{ name, id, channel, needsRaw }. Entries without an identity select default
device channels. The host can share one capture among all bindings for a device.
MidiState API¶
class MidiState {
channels: Record<number, MidiChannelState> // keys 1-16
mpeZones: { lower: number | null, upper: number | null } // per-port discovery
getChannel(n: number): MidiChannelState // Get channel 1-16
handleMessage(data: Uint8Array, port?: { id, name }): void
setPortInventory(ports: Array<{ id, name, connected? }>): void
registerPort({ id, name }): MidiState | null
disconnectPort(id: string): void
getPortState({ id?, name? }): MidiState | null
getPorts(): Array<{ id, name, connected }>
getZoneVoice({ zone, members? }): object | null // newest held member note
reset(): void
}
class MidiChannelState {
key: number // Current note (0-127)
velocity: number // Current velocity (0-127)
gate: number // 1 if note on, 0 if off
time: number // Timestamp of last note on (Date.now())
cc: Uint8Array // 128 retained 7-bit controller values
cc14: Uint16Array // 32 retained paired 14-bit values
pitchBend: number // Unsigned 14-bit position; neutral 8192
pressure: number // Channel pressure (0-127)
polyPressure: Uint8Array // Pressure for each of 128 keys
nrpn: Map<number, number> // Parameter address to unsigned 14-bit value
noteOn(key: number, velocity: number): void
noteOff(key?: number): void
controlChange(controller: number, value: number): void
reset(): void
}
AudioState API¶
class AudioState {
low: number // Low frequency band (0-1)
mid: number // Mid frequency band (0-1)
high: number // High frequency band (0-1)
vol: number // Overall volume (0-1)
raw: number // Bipolar time-domain value (-1 to 1)
rawReady: boolean
fft: Float32Array // 16 normalized frequency bins
spectrum: Float32Array // 128 normalized frequency bins
waveform: Float32Array // 128 normalized time-domain samples
setBands(low, mid, high): void
setRaw(value): void
setRawUnavailable(): void
updateFromAnalyser(analyser, smoothing?): void
setSpectrum(frequencyData: Uint8Array): void
setWaveform(timeDomainData: Uint8Array): void
setDeviceInventory(devices: Array<{ id, name, connected? }>): void
registerDevice({ id, name, channelCount }): object | null
setChannelValues(id, channel, { low?, mid?, high?, vol?, raw? }): boolean
disconnectDevice(id: string): void
setDeviceRawUnavailable(id: string): void
getDeviceChannelState({ id?, name?, channel }): AudioState | null
getDevices(): Array<{ id, name, connected, channelCount }>
registerDefaultChannels(channelCount: number): Map<number, AudioState> | null
getDefaultChannelState(channel: number): AudioState | null
disconnectDefaultInput(): void
resetAggregate(): void
reset(): void
}
Technical Notes¶
MIDI channels are 1-indexed (1-16) matching standard MIDI conventions
MIDI and audio
min/maxvalues are normalized percentages of the receiving effect parameter’s range. Bounds are clamped to 0–1;max: 10does not request an absolute effect value of 10.Audio band values use the normalized 0–1 range. The
audioBand.rawband first maps its bipolar -1…1 signal onto that range.The runtime calculates trigger decay with
Date.now()for frame-independent animation.A nested automation result maps onto the receiving numeric field’s declared range, just as it maps onto an effect parameter’s range.