Axion SDK

The module a device UI imports: parameters, meters, hardware encoders, and LEDs.

A device UI talks to the platform through two surfaces:

  • axionSDK: a module you import composables and helpers from. This is the recommended surface. It’s registered as a virtual module for the SFC loader, so a runtime-loaded .vue can import { … } from 'axionSDK' with no bundler.
  • window.axionSDK: a lower-level global for the few things the composables don’t wrap (system state, raw meter subscription, preset dialogs).
import {
  useDeviceInstance,
  useMeterData,
  useMeterDataSmoothed,
  useParameterDock,
  globalState,
} from 'axionSDK';

Exports

Export Purpose
useDeviceInstance(instanceId?) Read/write this device’s parameters and read their metadata.
useMeterData(meterId, uuid?) Reactive meter level [0,1], ~15 Hz.
useMeterDataSmoothed(meterId, uuid?) Same, decayed for a VU-style needle.
useParameterDock(instanceId, options) Pin, reorder or drop the shared dock’s parameters for this device (every parameter is on it by default).
useHardware() Subscribe to the physical encoders / buttons / analog inputs; drive LEDs.
useDeviceContext() / provideDeviceContext() Ambient device UUID so composables can infer it without a prop.
pushLedLayer(), isLedOwner(), reclaimLedOwnership() Encoder-LED ownership (see LEDs).
updateEncoderLedsFromAssignments(), reclaimFootswitchLeds() Footswitch LED helpers.
ENCODER_CONFIG, scaleEncoderValue(), scaleEncoderValueQuadratic(), normalizeValue(), ledBrightness(), createEncoderHandler(), createParameterEncoderHandler() Encoder scaling and LED math.
globalState The reactive global store (system state, rig).

Parameters

useDeviceInstance(instanceId) is the main entry point:

const device = useDeviceInstance(props.instanceId);

device.getParameter(name)            // current value: number | undefined
device.setParameter(name, value)     // write a value
device.getParameterMetadata(name)    // { min, max, type, units, default, displayName, format }
device.getStateProperty(name)        // non-numeric state (see below): string | undefined
device.getPropertyOptions(name)      // async enum/file lists: Promise<PropertyOption[]>

Parameter names are the identity: they’re the keys in your manifest’s params, not indices or LV2 ports. Read reactively inside a computed, write from an event handler:

const drive = computed(() => device.getParameter('drive') ?? 0.5);
function onDrive(v) { device.setParameter('drive', v); }

setParameter mutates the instance’s reactive parameterValues, and Axion forwards the change to the audio engine. You don’t send messages yourself.

instanceId vs. context. If your component provides a device context
(provideDeviceContext), the instanceId? argument is optional and the
composables infer the UUID. In a top-level device SFC it’s simplest to pass
props.instanceId explicitly.

Value ranges: normalized vs. real units

The value you read and write is the same value the backend sees, and that differs by backend type:

  • LV2 devices use real engineering units (Hz, dB, Q), matching the plugin’s .ttl ranges.
  • VST3 devices use normalized [0,1].

So don’t hard-code ranges. Ask for them:

const meta = device.getParameterMetadata('drive');   // { min, max, ... }

and feed meta.min / meta.max to your knob. getParameterMetadata also returns a format token derived from the unit (hz, db, percentage, time-ms, degrees, or number) for rendering the value.

State properties (files, models)

Non-numeric state (an IR file, a NAM model path) is exposed as an ordinary parameter you read with getParameter(shortName). For pickers, fetch the available choices asynchronously:

const options = await device.getPropertyOptions('model');   // PropertyOption[]

Meters

Backends can expose output ports (levels, gain reduction, tuner pitch) that stream to the UI. Subscribe with useMeterData:

import { useMeterData } from 'axionSDK';

const output = useMeterData('output', props.instanceId);  // Ref<number> in [0,1]
// output.value updates ~15 Hz and auto-unsubscribes on unmount

The composable builds the full meter id as "<deviceUuid>:<meterId>", subscribes via window.axionSDK.meters.subscribe(...), and cleans up for you. Pass the device UUID (your instanceId) as the second argument unless you’ve provided a device context. Use useMeterDataSmoothed for a decayed value that reads like an analog meter. The meter id is the output port’s name as the backend exposes it; see Output ports & meter streaming.

Parameter dock and encoders

Custom device views automatically get a translucent, horizontally swipeable parameter dock. Each of its five visible slots maps to the physical encoder in the same position. The dock handles scaling, discrete enum/boolean steps, value formatting, LED rings, paging, and cleanup, so normal device UIs should not subscribe to encoder rotation themselves.

The default layout uses every numeric parameter from manifest.json, then the standard Input, Mix, and Output controls, so a device that declares nothing gets every parameter. To put the knobs painted on your panel first, pin them; everything you don’t list still follows, in default order:

import { useParameterDock } from 'axionSDK';

useParameterDock(() => props.instanceId, {
  first: ['drive', 'tone', 'level', 'device.mix'],
});

To drop, reorder or add parameters, give an order transform. It receives the list after first has been applied (parameter ids; null is an empty slot) and its result is final. Typical drops: a device’s own mix or gain in favour of the engine’s device.mix / device.gain (consistent CC mapping), or the engine’s device.mix when the device has its own blend:

useParameterDock(() => props.instanceId, {
  first: ['blend', 'drive', 'tone', 'level'],
  order: params => params.filter(p => p !== 'device.mix'),
});

order may read reactive state (e.g. the loaded model); the dock recomputes when it changes. Parameters it drops are logged once to the console ([ParameterDock] … not on the dock), so an accidental drop is easy to spot.

A plain array is shorthand for { first: array }. Nested arrays pin whole pages (each padded to five slots), and null leaves an encoder empty. Entries in first can also be { parameter: 'drive', label: 'Gain' }; order sees the id and the object is kept. The manifest can declare the same pinned list statically as customUI.parameterPages:

"customUI": {
  "enabled": true,
  "path": "ui/MyDevice.vue",
  "parameterPages": ["drive", "tone", "level", "device.mix"]
}

A runtime first takes precedence over customUI.parameterPages; a runtime registration with only order uses the manifest’s pages as first. The registration is removed automatically when the component unmounts. useParameterDock returns { currentPage }, a writable computed ref, so artwork with page arrows/readouts can stay in sync with dock swipes.

Per-parameter encoder feel is set in the manifest: "mapping": "log" turns frequency-like params at an even musical rate, and "encoderAcceleration": { "gain", "curve", "max", "threshold" } overrides the shared speed curve for a stepped (increment) param, e.g. a millisecond trim that must still sweep seconds on a fast spin. Per event the step is increment × min(max, 1 + gain × (|delta| − threshold)^curve); slow turns (|delta| ≤ threshold, default 1) move one increment.

Low-level hardware access

useHardware() subscribes to the physical rotary encoders (ids 0–9), their push-buttons, and analog inputs (e.g. an expression pedal). Every subscribe returns an unsubscribe function. Call it on unmount.

Use this API for interactions the shared parameter dock does not model. Do not use it to bind ordinary device parameters; while the device dock is visible it owns encoder rotation and LED feedback.

const hardware = useHardware();

const unsub = hardware.onEncoderButton(6, (event) => {
  if (event.pressed) openDeviceHelp();
});

onUnmounted(() => unsub());
Method Purpose
onEncoderRotate(id, cb) Rotation of a specific encoder.
onEncoderButton(id, cb) Press/release of a specific encoder’s button.
onAnyEncoderRotate(cb) / onAnyEncoderButton(cb) Any encoder.
onAnalogValue(bankId, analogId, cb) Analog input (12-bit, 0–4095). null matches any.
onAnalogConnect(bankId, analogId, cb) Pedal plug/unplug at the TRS jack.
setEncoderLed(id, r, g, b, w?) / setEncoderLeds([...]) Drive LEDs directly (prefer the ownership API below).

The composable handles a rotation guard (absorbs the 1–2 count jiggle from pressing a detentless encoder) for you.

Scaling raw encoder deltas

An encoder event gives you a raw delta, not a value. Convert it with the scaling helpers only when implementing a specialized non-parameter interaction. They apply the platform’s speed-dependent acceleration so a slow turn is precise and a fast spin covers the range:

scaleEncoderValue(current, delta, min, max)            // → new clamped value
scaleEncoderValueQuadratic(current, delta, min, max)   // more precise at low speed
normalizeValue(value, min, max)                        // → [0,1] for LED/visual feedback

A parameter-handler factory is also available, but custom UIs should declare their parameter dock instead:

const handler = createParameterEncoderHandler(
  () => device.getParameter('drive') ?? 0.5,
  (v) => device.setParameter('drive', v),
  () => device.getParameterMetadata('drive'),   // { min, max }
  { onChanged: (v, ev, normalized) => setLed(1, normalized) }
);
hardware.onEncoderRotate(1, handler);

Low-level LED ownership

The device parameter dock owns and paints the encoder LEDs while a custom view is open. Custom device UIs normally need no LED code. The ownership API below is for specialized screens that replace the ordinary device interaction; writes from a lower layer are intentionally blocked while the dock is active.

Each encoder has an RGB ring LED. Because other parts of the app also want to drive LEDs, claim ownership with pushLedLayer rather than calling setEncoderLeds blindly:

import { pushLedLayer, ledBrightness } from 'axionSDK';

let ledHandle = null;

onMounted(() => {
  // The redraw callback is invoked whenever your layer becomes the owner again.
  ledHandle = pushLedLayer('my-device', () => redrawLeds());
  redrawLeds();
});

onUnmounted(() => { ledHandle?.release(); });

function redrawLeds() {
  const n = normalizeValue(device.getParameter('drive') ?? 0.5, min, max);
  const b = ledBrightness(n);                 // perceptual curve, [0,1]
  ledHandle?.setEncoderLed(1, 0, Math.round(255 * b), 0, 0);   // green ring
}

pushLedLayer(name, redrawCallback) returns a handle with setEncoderLed, setEncoderLeds, and release. ledBrightness(normalized) applies a perceptual gamma so brightness tracks the knob evenly across its range.

window.axionSDK (low-level)

Reach for the global only for what the composables don’t cover:

Call Purpose
getDeviceInstance(instanceId) The raw instance: { parameterValues, uiState, … }. Mutating parameterValues[name] is equivalent to setParameter.
getSystemState() The full system/device state tree.
meters.subscribe(ids, cb) Raw meter subscription (useMeterData wraps this).
openChangePresetDialog(...), openSavePresetDialog(...) The preset dialogs.

Next

See it all together in the Worked example, then Styling for the touchscreen.