WebAssembly bindings for Phira's prpr-auto-offset
delay-alignment library, plus a TypeScript-first npm package that wraps it.
Given an audio track and a chart's note events, the library estimates the timing offset between when notes are placed and when the corresponding hit sounds actually play. It runs entirely in the browser via WebAssembly.
- Rust crate:
crates/prpr-auto-offset-wasm— a thinwasm-bindgenwrapper over the upstream crate. - npm package:
@teamflos/prpr-auto-offset-wasm— an ergonomic, validated TypeScript facade.
Demo.
To preview locally, run make demo, then serve site/ over HTTP (for example,
python -m http.server 8080 --directory site). Without Make:
cd npm
npm ci
npm run build:wasm:web
cd ..
node scripts/build-demo.mjs
python -m http.server 8080 --directory siteFor GitHub Pages, set Settings → Pages → Build and deployment → Source → GitHub Actions.
The Pages workflow builds from source and deploys on pushes to main or manual dispatch.
All asset URLs are relative, including the worker and WASM, so repository subpaths work.
audio PCM ──► audio frontend ──► "novelty" signal N(t)
note events ─► note frontend ──► note signal M(t)
N(t), M(t) ──► normalized cross-correlation ──► offset + confidence
Audio frontends: EnergyDiff, SpectralFlux (diagnostic) and SuperFlux (recommended).
Note frontends: GaussianNote (diagnostic) and WeightedGaussianNote (recommended).
estimate_auto_offset(options)— one-call entry point. Builds a frontend + note signal from raw PCM and note events, then runs the alignment. Returns a result object with.offset,.correlation,.reliable, and.correlationCurve.- Low-level
SignalHandlefactories (superflux,energyDiff,spectralFlux,gaussianNote,weightedGaussianNote) +estimate/estimate_with. compute_spectrogramfor visualization.
import { estimateAutoOffset } from "@teamflos/prpr-auto-offset-wasm";
const res = estimateAutoOffset({
pcm: audioPcm, // Float32Array, mono
sampleRate: 44100,
notes: [
{ time: 1.0, kind: "tap" },
{ time: 1.25, kind: "flick" },
],
});
// res: { offset, correlation, rawPeak, noteEnergy, audioEnergy, reliable, correlationCurve }Important
offset is absolute time, not a chart offset. To get the value you should set on the
chart, subtract searchCenterSec — i.e. the chart author's configured offset when it is
set, otherwise 0:
chartOffset = offset − searchCenterSec
Example: if the chart author set offset = 0.05s and you pass searchCenterSec = 0.05,
a detected offset = 0.112s means the chart needs a +62ms correction.
Note
reliable is only the algorithm's internal gate — correlation > 0.2 — a bare lower
bound that real charts easily exceed. It is not a quality scale. To judge quality, use the
reference bands that the Phira offset panel draws on the correlation curve:
0.35— poor / lower bound: below this the note pattern has essentially no usable match (in a 2000-chart sample only ~6 per 1000 scored below it).0.6–0.75— typical / usual: the range where most charts land.
Treat reliable as a minimal sanity check, and interpret quality with the bands above.
When you can't run a bundler (raw <script type="module">, CDN), use the ./web entry, which
exports the raw wasm-bindgen API, plus an async init you must call first.
Unlike the main TypeScript entry, it uses numeric enum codes, noteEvents, and
alignConfig; results expose curve_offsets() / curve_values() and must be freed:
import init, { estimate_auto_offset } from "@teamflos/prpr-auto-offset-wasm/web";
await init();
const res = estimate_auto_offset({
pcm: audioPcm, sampleRate: 44100,
noteEvents: [{ time: 1, kind: 0 }], // 0=tap, 1=hold, 2=flick, 3=drag
frontend: 2, noteMethod: 1, // SuperFlux + weighted Gaussian
alignConfig: { searchCenterSec: 0, searchRangeSec: 0.3, samplingIntervalSec: 0.005 },
});
try { console.log(res.offset, res.correlation); }
finally { res.free(); }Build frontends individually and combine them:
import { superflux, weightedGaussianNote, estimateWith } from "@teamflos/prpr-auto-offset-wasm";
const audio = superflux(pcm, 44100); // or spectralFlux / energyDiff
const note = weightedGaussianNote(notes, 0.02, noteConfig); // or gaussianNote
const res = estimateWith(audio, note, durationSec, { searchCenterSec: authorOffset });
const vals = audio.sample(new Float64Array([1.0, 1.5, 2.0])); // sample at arbitrary timesPrerequisites: Rust (with wasm32-unknown-unknown), wasm-pack,
Node.js, and npm/pnpm.
# install the TypeScript build dependency first
cd npm && npm ci && cd ..
# wasm (bundler target) → npm/pkg
make wasm
# TypeScript facade → npm/dist
make npm
# Node smoke test (builds a nodejs-target wasm into tests/pkg-node)
make testRun everything end-to-end:
make buildFrom npm/, run:
npm ci
npm pack --dry-run # rebuilds bundler + web WASM, compiles TS, checks package contents
npm publish --access publicprepack requires Rust, the wasm32 target, and wasm-pack. It copies the repository README
and GPL license into the package and rejects missing JS, declarations, or WASM binaries.
The package is not published by CI. Before a release, also run make test. The main entry
is for WASM-capable bundlers; use /web for native browser modules. The version is currently
0.1.0; keep the npm and Cargo workspace versions aligned when bumping it.
wasm-opt is disabled in the crate's wasm-pack metadata. To optimize the shipped .wasm,
run:
wasm-opt --enable-bulk-memory --enable-nontrapping-float-to-int -O \
pkg/prpr_auto_offset_wasm_bg.wasm -o pkg/prpr_auto_offset_wasm_bg.wasmGPL-3.0-only, matching the upstream prpr-auto-offset crate, which this project builds on.