Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion docs/how-it-works/audiograph.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ rather than its last block.
| `CONST` | | 0: the value, in counts | that value every sample |
| `OSC` | pitch, fm, pwm, sync | `OSC_FREQUENCY`, `OSC_WAVEFORM` (1 saw, 2 pulse, 4 triangle, 8 sine; a sum plays those added), `OSC_PULSE_WIDTH`, `OSC_LEVEL`, `OSC_PITCH_OCTAVES`, `OSC_FM_DEPTH`, `OSC_PWM_DEPTH` | audiooscillator's band-limited waveform |
| `LADDER` | audio, cutoff | `LADDER_CUTOFF_HZ`, `LADDER_RESONANCE`, `LADDER_DRIVE`, `LADDER_POLES`, `LADDER_PASSBAND_COMP`, `LADDER_OVERSAMPLE`, `LADDER_MIX`, `LADDER_CUTOFF_OCTAVES` | audioladder's filter, the cutoff input read every sample |
| `BIQUAD` | audio | `BIQUAD_MODE`, `BIQUAD_FREQUENCY`, `BIQUAD_Q`, `BIQUAD_GAIN_DB`, `BIQUAD_MIX` | audiobiquad's float biquad |
| `BIQUAD` | audio | `BIQUAD_MODE`, `BIQUAD_FREQUENCY`, `BIQUAD_Q`, `BIQUAD_GAIN_DB`, `BIQUAD_MIX`, `BIQUAD_RAMP` ms 0..1000 (default 20) | audiobiquad's float biquad, [walking to new coefficients](#biquad-moves) |
| `FBDELAY` | audio | `DELAY_MS`, `DELAY_FEEDBACK`, `DELAY_MIX`, `DELAY_DAMPING_HZ`, `DELAY_WOW_HZ`, `DELAY_WOW_DEPTH_MS` and audioecho's other options by number; built with `arg` = the line's length in ms; `table(node, int16_array)` sets its wow shape | audioecho's FeedbackDelay |
| `ENV` | gate | `ENV_ATTACK`, `ENV_DECAY`, `ENV_RELEASE` in seconds, `ENV_SUSTAIN` 0..1 | an ADSR, 0..`UNIT`, each stage a one-pole approach, the attack aimed past 1.0 so it arrives with a slope |
| `LFO` | | `LFO_RATE` Hz, `LFO_AMPLITUDE` counts, `LFO_SHAPE` (`LFO_TRIANGLE`, `LFO_SINE`, `LFO_SAW`, `LFO_SQUARE`, `LFO_RANDOM`) | that shape, ±amplitude, [below](#lfo-shapes) |
Expand Down Expand Up @@ -116,6 +116,19 @@ modules that wrap them (`audiooscillator`, `audioladder`, `audiobiquad`,
An input left `None` is open: an oscillator without a pitch input plays
`OSC_FREQUENCY`, a ladder without a cutoff input sits at `LADDER_CUTOFF_HZ`.

### Biquad moves

A new frequency, Q or gain doesn't land on a `BIQUAD` at once: the filter walks
to it, so a jump doesn't thump. `BIQUAD_RAMP` says for how long, in milliseconds
from 0 to 1000, and the default of 20 is what `audiobiquad.Biquad` always does.
The walk is a power of two frames, the one nearest what you asked for: 20 ms at
48 kHz is 1024 frames, and 0 is one frame, the shortest there is.

Set it low when a value moves every block. Each move starts the walk again from
where it had got to, so a filter envelope on a 20 ms walk trails behind what it
is told and opens slowly. One frame follows it the way `synthio.Biquad` does.
The new value takes effect at the next move, and setting it isn't a move.

### LFO shapes

Every shape comes from one phase that starts at 0 and has taken one step by
Expand Down
1 change: 1 addition & 0 deletions src/audiograph/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ static const mp_rom_map_elem_t audiograph_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR_BIQUAD_Q), MP_ROM_INT(AUDIODSP_BIQUAD_F32_OPT_Q) },
{ MP_ROM_QSTR(MP_QSTR_BIQUAD_GAIN_DB), MP_ROM_INT(AUDIODSP_BIQUAD_F32_OPT_GAIN_DB) },
{ MP_ROM_QSTR(MP_QSTR_BIQUAD_MIX), MP_ROM_INT(AUDIODSP_BIQUAD_F32_OPT_MIX) },
{ MP_ROM_QSTR(MP_QSTR_BIQUAD_RAMP), MP_ROM_INT(AUDIODSP_BIQUAD_F32_OPT_RAMP) },
{ MP_ROM_QSTR(MP_QSTR_DELAY_MS), MP_ROM_INT(AUDIODSP_FEEDBACK_DELAY_OPT_DELAY_MS) },
{ MP_ROM_QSTR(MP_QSTR_DELAY_FEEDBACK), MP_ROM_INT(AUDIODSP_FEEDBACK_DELAY_OPT_FEEDBACK) },
{ MP_ROM_QSTR(MP_QSTR_DELAY_MIX), MP_ROM_INT(AUDIODSP_FEEDBACK_DELAY_OPT_MIX) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ static const mp_rom_map_elem_t audiograph_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR_BIQUAD_Q), MP_ROM_INT(AUDIODSP_BIQUAD_F32_OPT_Q) },
{ MP_ROM_QSTR(MP_QSTR_BIQUAD_GAIN_DB), MP_ROM_INT(AUDIODSP_BIQUAD_F32_OPT_GAIN_DB) },
{ MP_ROM_QSTR(MP_QSTR_BIQUAD_MIX), MP_ROM_INT(AUDIODSP_BIQUAD_F32_OPT_MIX) },
{ MP_ROM_QSTR(MP_QSTR_BIQUAD_RAMP), MP_ROM_INT(AUDIODSP_BIQUAD_F32_OPT_RAMP) },
{ MP_ROM_QSTR(MP_QSTR_DELAY_MS), MP_ROM_INT(AUDIODSP_FEEDBACK_DELAY_OPT_DELAY_MS) },
{ MP_ROM_QSTR(MP_QSTR_DELAY_FEEDBACK), MP_ROM_INT(AUDIODSP_FEEDBACK_DELAY_OPT_FEEDBACK) },
{ MP_ROM_QSTR(MP_QSTR_DELAY_MIX), MP_ROM_INT(AUDIODSP_FEEDBACK_DELAY_OPT_MIX) },
Expand Down
3 changes: 2 additions & 1 deletion src/cpython/_audiodsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -1394,7 +1394,7 @@ static PyObject *biquad_f32_state_configure(
double value;
if (!PyArg_ParseTuple(args, "id:configure", &option, &value)) return NULL;
if (option < AUDIODSP_BIQUAD_F32_OPT_MODE ||
option > AUDIODSP_BIQUAD_F32_OPT_MIX) {
option > AUDIODSP_BIQUAD_F32_OPT_RAMP) {
PyErr_SetString(PyExc_ValueError, "unknown biquad option");
return NULL;
}
Expand Down Expand Up @@ -5117,6 +5117,7 @@ static int graph_add_constants(PyObject *module) {
{"BIQUAD_Q", AUDIODSP_BIQUAD_F32_OPT_Q},
{"BIQUAD_GAIN_DB", AUDIODSP_BIQUAD_F32_OPT_GAIN_DB},
{"BIQUAD_MIX", AUDIODSP_BIQUAD_F32_OPT_MIX},
{"BIQUAD_RAMP", AUDIODSP_BIQUAD_F32_OPT_RAMP},
{"DELAY_MS", AUDIODSP_FEEDBACK_DELAY_OPT_DELAY_MS},
{"DELAY_FEEDBACK", AUDIODSP_FEEDBACK_DELAY_OPT_FEEDBACK},
{"DELAY_MIX", AUDIODSP_FEEDBACK_DELAY_OPT_MIX},
Expand Down
1 change: 1 addition & 0 deletions src/cpython/audiograph.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
BIQUAD_Q = _K["BIQUAD_Q"]
BIQUAD_GAIN_DB = _K["BIQUAD_GAIN_DB"]
BIQUAD_MIX = _K["BIQUAD_MIX"]
BIQUAD_RAMP = _K["BIQUAD_RAMP"]
DELAY_MS = _K["DELAY_MS"]
DELAY_FEEDBACK = _K["DELAY_FEEDBACK"]
DELAY_MIX = _K["DELAY_MIX"]
Expand Down
14 changes: 9 additions & 5 deletions src/shared/audiodsp_filter_f32.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ void audiodsp_biquad_f32_config_init(audiodsp_biquad_f32_config_t *config,
config->q = 0.7071067811865475f;
config->gain_db = 0.0f;
config->mix = 1.0f;
config->ramp_ms = (float)AUDIODSP_BIQUAD_F32_RAMP_MS;
config->derived = false;
}

Expand Down Expand Up @@ -107,6 +108,9 @@ void audiodsp_biquad_f32_configure(audiodsp_biquad_f32_config_t *config,
case AUDIODSP_BIQUAD_F32_OPT_MIX:
config->mix = clampf(value, 0.0f, 1.0f);
break;
case AUDIODSP_BIQUAD_F32_OPT_RAMP:
config->ramp_ms = clampf(value, 0.0f, 1000.0f);
break;
default:
break;
}
Expand Down Expand Up @@ -338,14 +342,13 @@ static void biquad_leave_svf(audiodsp_biquad_f32_state_t *state,
}
}

// Frames in a move: the power of two nearest AUDIODSP_BIQUAD_F32_RAMP_MS.
// Frames in a move: the power of two nearest `ramp_ms`, and at least one.
// A power of two because the walk's per-frame ratio is then a chain of
// square roots, which IEEE rounds correctly everywhere -- pow() is three
// functions on three targets that nearly agree, and this has to render the
// same bytes on all of them (see audiodsp_trig.h).
static uint32_t biquad_ramp_shift(uint32_t sample_rate) {
const double frames = (double)sample_rate *
(double)AUDIODSP_BIQUAD_F32_RAMP_MS / 1000.0;
static uint32_t biquad_ramp_shift(uint32_t sample_rate, float ramp_ms) {
const double frames = (double)sample_rate * (double)ramp_ms / 1000.0;
uint32_t shift = 0;
while (shift < 20u &&
(double)(1u << shift) * 1.4142135623730951 < frames) {
Expand Down Expand Up @@ -411,7 +414,8 @@ static void biquad_begin_move(const audiodsp_biquad_f32_config_t *config,
state->ramp_left = 0u;
return;
}
const uint32_t shift = biquad_ramp_shift(config->sample_rate);
const uint32_t shift = biquad_ramp_shift(config->sample_rate,
config->ramp_ms);
const uint32_t frames = 1u << shift;
state->g_step = (float)biquad_root_chain(
(double)config->svf_g / (double)state->g, shift);
Expand Down
13 changes: 10 additions & 3 deletions src/shared/audiodsp_filter_f32.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ typedef enum {
AUDIODSP_BIQUAD_F32_OPT_Q,
AUDIODSP_BIQUAD_F32_OPT_GAIN_DB,
AUDIODSP_BIQUAD_F32_OPT_MIX,
AUDIODSP_BIQUAD_F32_OPT_RAMP,
} audiodsp_biquad_f32_option_t;

//: Everything derived from the constructor/`set()` arguments, held apart from
Expand All @@ -99,6 +100,11 @@ typedef struct {
// 0..1, a crossfade between the untouched signal and the filtered one --
// `audiofilters.Filter`'s convention (Filter.c:288), not `Echo`'s 0..2.
float mix;
// 0..1000 ms: how long a coefficient move takes, rounded to the nearest
// power of two frames, and 0 is one frame. Read when a move begins, so a
// change takes effect at the next move, not during one. The default is
// AUDIODSP_BIQUAD_F32_RAMP_MS.
float ramp_ms;
// Written by audiodsp_biquad_f32_config_finish() from everything above.
float b0, b1, b2, a1, a2;
// The same section as a state-variable filter (Simper's trapezoidal
Expand Down Expand Up @@ -153,7 +159,7 @@ typedef struct {
// do not mean the same thing under another: state a 20 kHz low-pass built,
// read through a 20 Hz one, is a full-scale step that rings for 100 ms
// (audiocomponents#162). So when the coefficients change, the kernel stops
// running transposed direct form II for `AUDIODSP_BIQUAD_F32_RAMP_MS`: it
// running transposed direct form II for the config's `ramp_ms`: it
// carries the memory over into a state-variable filter's two integrators --
// whose words are the band and low outputs, and so mean the same thing at
// every corner -- walks that SVF's corner and damping geometrically and its
Expand Down Expand Up @@ -181,8 +187,9 @@ typedef struct {
bool primed;
} audiodsp_biquad_f32_state_t;

//: How long a coefficient move takes. 20 ms, the ramp
//: audiocomponents' docs/control-smoothing.md gives every control move.
//: How long a coefficient move takes unless AUDIODSP_BIQUAD_F32_OPT_RAMP says
//: otherwise. 20 ms, the ramp audiocomponents' docs/control-smoothing.md gives
//: every control move.
#define AUDIODSP_BIQUAD_F32_RAMP_MS 20u

void audiodsp_biquad_f32_config_init(audiodsp_biquad_f32_config_t *config,
Expand Down
3 changes: 2 additions & 1 deletion src/shared/audiodsp_graph.c
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ int audiodsp_graph_add(audiodsp_graph_t *graph, audiodsp_graph_kind_t kind,
node->param[AUDIODSP_BIQUAD_F32_OPT_Q] = d->config.q;
node->param[AUDIODSP_BIQUAD_F32_OPT_GAIN_DB] = d->config.gain_db;
node->param[AUDIODSP_BIQUAD_F32_OPT_MIX] = d->config.mix;
node->param[AUDIODSP_BIQUAD_F32_OPT_RAMP] = d->config.ramp_ms;
break;
}
case AUDIODSP_GRAPH_FBDELAY: {
Expand Down Expand Up @@ -1295,7 +1296,7 @@ static void derive(audiodsp_graph_t *graph, audiodsp_graph_node_t *n) {
}
case AUDIODSP_GRAPH_BIQUAD: {
graph_biquad_t *d = n->data;
for (uint32_t i = 0; i <= AUDIODSP_BIQUAD_F32_OPT_MIX; ++i) {
for (uint32_t i = 0; i <= AUDIODSP_BIQUAD_F32_OPT_RAMP; ++i) {
audiodsp_biquad_f32_configure(&d->config,
(audiodsp_biquad_f32_option_t)i, n->param[i]);
}
Expand Down
37 changes: 34 additions & 3 deletions tests/parity/graph_nodes_probe.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""audiograph's PHASER, LAG, LFO shapes, SAMPLE_HOLD, TWOPOLE, OVERDRIVE, PAN, WAVEFOLD, LPG, MODAL, STRUCK, PICKUP, TREMOLO, DECAY, PINK, CRUSH, ACID, SPRING, ADSR2, GLISS, FOLLOW, WAVETABLE and ROTARY, rendered for verify_graph.py.
"""audiograph's PHASER, LAG, LFO shapes, SAMPLE_HOLD, TWOPOLE, OVERDRIVE, PAN, WAVEFOLD, LPG, MODAL, STRUCK, PICKUP, TREMOLO, DECAY, PINK, CRUSH, ACID, SPRING, ADSR2, GLISS, FOLLOW, WAVETABLE, ROTARY and BIQUAD_RAMP, rendered for verify_graph.py.

graph_nodes_probe.py # prints the render, then GRAPH NODES PASS
graph_nodes_probe.py --fault phaser # one phaser frequency a block late
Expand All @@ -24,6 +24,7 @@
graph_nodes_probe.py --fault follow # one release time a block late
graph_nodes_probe.py --fault wavetable # one frequency change a block late
graph_nodes_probe.py --fault rotary # one speed change a block late
graph_nodes_probe.py --fault ramp # one BIQUAD_RAMP change a block late

The same file runs on CPython, MicroPython and CircuitPython and prints every
sample each of these nodes writes, every block. ``verify_graph.py`` runs it on
Expand Down Expand Up @@ -62,7 +63,10 @@
drawbar voices: one on a written pitch, gate and percussion line, its key
coming down again while the last note still sounds, and one with no gate and
its frequency stepped; and two rotary speakers, the sawtooth driven through
slow, fast and stop and the noise clean and fast, both sides printed.
slow, fast and stop and the noise clean and fast, both sides printed. And four
biquads on the sawtooth with a cutoff that moves each block: one that never
sets BIQUAD_RAMP, one that sets it to its default and must render the same, one
at the shortest, one frame, and one that changes its ramp part way through.

Every value handed to the graph is a short binary fraction or an integer, so a
single-precision MicroPython parses the same floats a double one does.
Expand All @@ -88,7 +92,7 @@

FAULTS = ("phaser", "lag", "lfo", "hold", "twopole", "overdrive", "pan", "wavefold", "lpg",
"modal", "struck", "pickup", "tremolo", "decay", "pink", "crush", "acid", "spring",
"adsr2", "gliss", "follow", "wavetable", "rotary")
"adsr2", "gliss", "follow", "wavetable", "rotary", "ramp")
FAULT = None
if "--fault" in sys.argv:
FAULT = sys.argv[sys.argv.index("--fault") + 1]
Expand Down Expand Up @@ -135,6 +139,20 @@ def node(name, kind, inputs, arg=0.0):
g.set(ph6, ag.PHASER_FEEDBACK, -0.375)
g.set(ph6, ag.PHASER_MIX, 0.75)

# -- biquads with a cutoff moving each block, at the walks BIQUAD_RAMP gives
ramp_nodes = []
for name, ramp in (("biquad_walk", None), ("biquad_twenty", 20.0), ("biquad_snap", 0.0),
("biquad_moved", 20.0)):
index, _ = node(name, ag.BIQUAD, (saw_b,))
g.set(index, ag.BIQUAD_Q, 2.0)
if ramp is not None:
g.set(index, ag.BIQUAD_RAMP, ramp)
ramp_nodes.append(index)
check(g.get(ramp_nodes[0], ag.BIQUAD_RAMP) == 20.0, "BIQUAD_RAMP starts at 20 ms")
walk_block = None
ramp_same = True
ramp_differs = False

# -- one LFO of each shape
lfos = []
for name, shape, rate in (("triangle", ag.LFO_TRIANGLE, 5.5),
Expand Down Expand Up @@ -438,6 +456,10 @@ def node(name, kind, inputs, arg=0.0):
step = centre % 64
tri = step if step < 32 else 64 - step
g.set(ph4, ag.PHASER_FREQUENCY, 100.0 + 125.0 * tri)
for index in ramp_nodes:
g.set(index, ag.BIQUAD_FREQUENCY, 250.0 + 125.0 * tri)
if b == (41 if FAULT == "ramp" else 40):
g.set(ramp_nodes[3], ag.BIQUAD_RAMP, 2.5)

# the note line: a new note every few blocks, one mid-block; a bend that
# moves every sample; legato on two blocks in three
Expand Down Expand Up @@ -649,6 +671,12 @@ def node(name, kind, inputs, arg=0.0):
for name, buffer in watched:
g.read(buffer, block)
print("node %d %s %s" % (b, name, " ".join([str(v) for v in block])))
if name == "biquad_walk":
walk_block = list(block)
elif name == "biquad_twenty":
ramp_same = ramp_same and list(block) == walk_block
elif name == "biquad_snap":
ramp_differs = ramp_differs or list(block) != walk_block
stats = seen.setdefault(name, [0, block[0], block[0], block[0]])
for v in block:
if v != stats[3]:
Expand Down Expand Up @@ -749,6 +777,9 @@ def node(name, kind, inputs, arg=0.0):
check(seen[name][0] > 5000 and seen[name][2] > 1000,
"the rotary speaker's %s renders sound" % name)

check(ramp_same, "BIQUAD_RAMP set to its default renders what no setting renders")
check(ramp_differs, "the shortest BIQUAD_RAMP moves the sound")

if failures:
for what in failures:
print("FAIL " + what)
Expand Down
3 changes: 2 additions & 1 deletion tests/parity/verify_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
verify_graph.py --micropython PATH --fault twopole # must FAIL (and overdrive, pan, wavefold, lpg, modal)
verify_graph.py --micropython PATH --fault decay # must FAIL (and pink, crush, acid, spring, adsr2, gliss, follow)
verify_graph.py --micropython PATH --fault wavetable # must FAIL (and rotary)
verify_graph.py --micropython PATH --fault ramp # must FAIL
verify_graph.py --micropython PATH --fault drain # must FAIL (and split)
verify_graph.py --micropython PATH --fault offbeat # must FAIL

Expand Down Expand Up @@ -55,7 +56,7 @@
("runner", HERE / "graph_parity_probe.py", ("lsb", "late")),
("nodes", HERE / "graph_nodes_probe.py", ("phaser", "lag", "lfo", "hold", "twopole", "overdrive", "pan", "wavefold", "lpg",
"modal", "struck", "pickup", "tremolo", "decay", "pink", "crush", "acid", "spring", "adsr2", "gliss", "follow",
"wavetable", "rotary")),
"wavetable", "rotary", "ramp")),
("source", HERE / "graph_source_probe.py", ("drain", "split")),
("carry", HERE / "graph_carry_probe.py", ("offbeat",)),
)
Expand Down
Loading
Loading