- Zig 87%
- Nix 13%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Zig's own fuzzer now runs with coverage, given a test binary built by LLVM, so the devshell's patched Zig goes and the fuzz tests ask for use_llvm. fuzz-run stays, for its time limit, seeds and replays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6qkRGrtbtr4re1HuETeo7 |
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| build.zig.zon.nix | ||
| flake.lock | ||
| flake.nix | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
zig-resample
Sample-rate conversion for audio in Zig: any rate to any other, on
interleaved f32, as a stream.
The API documentation, generated from the doc comments, is published at https://jeff.jcollie.page/zig-resample/.
const resample = @import("resample");
var r: resample.Resampler = try .init(gpa, .{
.in_rate = 44100,
.out_rate = 48000,
.channels = 2,
.quality = .medium,
});
defer r.deinit();
// As much of the input as it can take, into as much output as there is
// room for; call again with the rest.
const done = r.process(input, output);
// done.consumed input frames, done.produced output frames.
// At the end, until it returns 0:
const tail = r.flush(output);
Requires Zig 0.17.
What it does
It is bandlimited interpolation, as Julius O. Smith describes it. Each output sample is the input's value at a moment between its samples. That value is found by weighting the input samples around the moment by a windowed sinc: the ideal lowpass filter, cut off at the lower of the two rates' Nyquist frequencies, and shaped by a Kaiser window so that it ends. Going down in rate, the filter is stretched by the ratio. What the new rate cannot carry is then filtered out rather than folded back in as aliasing.
The filter is kept as a table: 512 values between each of its zero crossings, read between them linearly. There is one table whatever the ratio, and the ratio need not be a tidy fraction: 44 100 to 48 000 and 44 100 to 44 101 cost the same. Where each output falls between input samples is kept exactly, as a count in units of one output rate's worth of input, so a stream of any length never drifts.
| Quality | Zero crossings each side | Kaiser β | Passband |
|---|---|---|---|
fast |
16 | 7 | to 90% of the lower Nyquist frequency |
medium |
32 | 9 | to 94% |
best |
64 | 12 | to 96% |
After flush the output is exactly outputFrames(input frames) long, and it
is aligned with the input: the first output sample is the input's value at its
first sample, not the filter's start. Equal rates are a plain copy.
Any number of channels, and rates up to 2²⁴ Hz, which is far above any audio's and keeps every product of two rates and a frame count inside 64 bits.
Testing
$ nix develop
$ zig build test --summary all
$ zig build check # everything compiles, including the fuzz loop and docs server
$ zig build docs-serve # the API documentation, at http://localhost:8000/
$ reuse lint
The tests hold it to what it is for:
- a 1 kHz tone taken from 44.1 to 48 kHz comes out as the same tone, within 60, 80 and 100 dB for the three qualities;
- taken from 48 to 8 kHz, a 6 kHz tone, which 8 kHz cannot carry, is more than 80 dB down rather than folded back to 2 kHz, and a 3 kHz tone is kept within 0.01 dB;
- two channels stay apart through 44 100 to 44 101;
- fed in random pieces with random room, the output is the same, bit for bit, as fed whole;
- a step lands where it was, halfway at the moment it steps, not a filter's length later.
Fuzzing
tests/fuzz.zig holds one property over any pair of rates, any channel count
and quality, and any samples, fed in any pieces:
- all the input is taken, and flushed, exactly
outputFramescome out; - every output sample is finite, and within what the filter can make of the input's largest;
- the output fed in pieces is the output fed whole, bit for bit;
- what was allocated is freed.
$ zig build fuzz --fuzz # Zig's fuzzer, until interrupted
$ zig build fuzz --fuzz=1M # a bounded run, then a report
$ zig build fuzz-run # a minute
$ zig build fuzz-run -- --seconds 300 --target streams
$ zig build fuzz-run -Dfuzz-optimize=debug -- --input fuzz-findings/x.bin --target streams
zig build fuzz --fuzz is Zig's own fuzzer, steered by coverage, which it
finds only in a test binary built by LLVM; build.zig asks for that. A
finding prints input saved to '.zig-cache/f/crash' above its report.
fuzz-run is a loop of this project's own in tools/fuzz.zig, with no
coverage but with a time limit, a seed to repeat a run by, and a way to run
one input again; that file says more. A failing
input is written to fuzz-findings/, and --input runs it again.
Using it
$ zig fetch --save git+https://git.jcollie.dev/jeff/zig-resample.git#main
const resample = b.dependency("resample", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("resample", resample.module("resample"));
zig-sendspin's server uses it to play one source to players that each take their own rate.
Where this lives
The repository has three homes, and they hold the same history.
| Where | How to get it |
|---|---|
| Forgejo | git clone https://git.jcollie.dev/jeff/zig-resample.git |
| Tangled | git clone https://tangled.org/jcollie.dev/zig-resample |
| Radicle | rad clone rad:z344zt5S1AuiGNwaPGmNqx33Rgz91 |
A Radicle repository is findable only by its ID, so that one is written out in
full: rad:z344zt5S1AuiGNwaPGmNqx33Rgz91.
Licensing
MIT, following the REUSE specification: every file
says so, or REUSE.toml says it for it.
References cited
Kept in the zig-resample Zotero collection.
- Smith, Julius O., and Phil Gossett. "A Flexible Sampling-Rate Conversion Method." In ICASSP '84. IEEE International Conference on Acoustics, Speech, and Signal Processing, 9:112–115. San Diego, CA, USA: IEEE, 1984. https://doi.org/10.1109/ICASSP.1984.1172555. The method: a windowed sinc kept as a table and read between its entries, stretched when going down.
- Smith, Julius O. "Digital Audio Resampling Home Page." Center for Computer Research in Music and Acoustics (CCRMA), Stanford University. https://ccrma.stanford.edu/~jos/resample/. The same, worked through at length.
- Kaiser, J., and R. Schafer. "On the Use of the I0-Sinh Window for Spectrum Analysis." IEEE Transactions on Acoustics, Speech, and Signal Processing 28, no. 1 (February 1980): 105–107. https://doi.org/10.1109/TASSP.1980.1163349. The window, and how its β sets how far down its sidelobes are.