Components
Time and sequencing
Blocks
A redux as a device you can plug in: a number of bits, a downsample factor and a mix as a state, a panel that draws the stairs the bits make, a Web Audio engine with a sample-hold worklet and the tools an agent drives it with.
Audio UI
"use client";
import { useState } from "react";Roughens the sound like an early sampler: it keeps only some bits of each sample and holds each sample for several. The panel draws the stairs the bits make against the straight line of a clean sound; at 8 bits they are too fine to see, take it down to 3 or 4. It is a device like the compressor: a state, a panel that draws it, an engine that plays it and tools an agent calls, each its own file.
import { ReduxPanel } from "@/components/audio-ui/devices/redux/panel";
import { reduxDevice } from "@/components/audio-ui/devices/redux/device";
const [state, setState] = useState(() => reduxDevice.defaults());
<ReduxPanel onValueChange={setState} value={state} />;The state is a plain object keyed by parameter id, so a host stores it, saves it with its project and sends it where it likes. quantize(input, bits) is one step of the quantizer and REDUX_WORKLET the source of the worklet.
import { createReduxEngine } from "@/components/audio-ui/devices/redux/engine";
const engine = createReduxEngine(ctx, state);
source.connect(engine.input);
engine.output.connect(ctx.destination);
engine.update(nextState); // parameters ramp: no click
engine.dispose();The bits are a quantizer table in a WaveShaperNode, rebuilt only when they change. The sample hold is an AudioWorklet loaded from a blob, so there is no file to serve: until it is ready the bits are already crushed and the rate is not yet reduced, and engine.ready resolves to false in a context that cannot run worklets. The hold takes whole samples, not a filtered resampling, so it aliases, which is the sound.
import { deviceTools } from "@audio-ui/core/device";
import { reduxDevice } from "@/components/audio-ui/devices/redux/device";
const [set, reset] = deviceTools(reduxDevice);set and reset change the state, with the ranges the device enforces: a value out of range is brought in and reported, a wrong one is refused by name, and a call never throws.
Every control is a native input: the knobs are sliders named after their parameter, with the value and unit as the text value, the switches are toggle buttons, and everything takes the keyboard.