block_process
Builtin index · Signal processing
block_process(x, f, [block=256])
Input: x, f, block · Output: no output
What it is
x: a vector or Signal; f: a function (a value or the name of a user function) called once per block; block: block length in samples, positional or named, default 256. Cuts x into consecutive blocks, calls f on each, and concatenates the results. The last block is short when the length is not a multiple of block — passed as-is, never zero-padded. A block of a Signal is itself a Signal at the same Fs, so f can read the rate off its own argument. Carries no state between blocks — see the note below.
Overloads
One call shape — this function is not overloaded.
| Code | Input | Output | Description |
block_process(x, f, [block=256]) | x vector or Signalx: a vector or Signal.f —f: a function (a value or the name of a user function) called once per block.block optional (default 256)block: block length in samples, positional or named, default 256. | not documented yet | Carries no state between blocks — see the note below. |
Examples
From the documentation, and run by the build -- the output and the figure below are what the engine actually produced. Simplest first.
s = signal(sin(2 * pi * 7 .* ((0 to 999) / 1000)), 1000)
level = block_process(s, (b) := rms(b), block = 250)
print("{length(level)} blocks at {level.Fs} Hz") # 4 blocks at 4 Hz
4 blocks at 4 Hz
from signal-processing
How it works
block_process(x, f, [block=256]) — cut x into consecutive blocks of block samples, call f once per block, and concatenate what comes back. The OFFLINE half of docs/design/toolkit-signal.md §5's "streaming is the same code" idea: write a per-block algorithm once, run it over a whole signal without hand-rolling the chunking loop and its off-by-one at the last, short block. WHY NOT THE SPEC'S OWN SHAPE. §5 writes it as proc = processor(block: 256) { |x| ... } then y = proc(s). Two things are wrong with taking that literally. The trailing { |x| ... } block-with-pipe-params is not Qu syntax at all (§17.2's lambdas are (x) := e, x => e, function ... end). More importantly proc(s) CALLS A VALUE THAT IS NOT A FUNCTION, and Qu does not do that today — probed 2026-09-18 against this binary, calling a Model or a List lands in "unknown function st", the plain undefined-name path. Making it work would mean deciding the open "does Qu grow general object/method dispatch, or does everything stay flat functions?" question, and that is Ahmed's call, not a call this builtin gets to make on the way past. So: the flat function form, which delivers the same capability, invents no syntax, and is a thin wrapper away from the object form if that question is ever settled the other way. The collection comes first, so |> and x.block_process(f) both compose — callable_pair is the same shared resolver map/pmap/filter use, so a function VALUE or the NAME of a user function both work and the opposite argument order still runs with their usual once-per-run notice. STATE — READ THIS BEFORE REACHING FOR IT AS A FILTER. f is called on each block with NOTHING threaded between calls, so an algorithm needing history across a block boundary (a running IIR's biquad state, overlap-add reconstruction) does NOT behave like its streaming self here: it restarts at every boundary, which shows up as block-edge artifacts. That is a real limitation and deliberately not papered over. Qu's existing answer for per-sample filter state is the explicit state = filter_next(state, x) threading pair directly below; a block-level equivalent would need a state-threading protocol, and inventing one unilaterally is the same kind of unasked-for design decision as the object-call question above. What this IS right for: per-block work independent of history — blockwise FFT/RMS metering, level detection, gain, any pure elementwise map. THE RATE OF THE RESULT. If f returns as many samples as it was handed, the output has the input's length and a Signal keeps its fs. If f reduces each block to ONE number (the metering case) the result is genuinely sampled at fs / block, and it comes back tagged that way rather than carrying the original rate — a metering series labelled at the input's fs is exactly §0's "an axis that was kept in the programmer's head". Any other output length has no rate this can name, so it comes back a plain Vec instead of a Signal whose fs would be a guess.
From the note above this builtin in the interpreter, where the reasoning was written down as it was built.