Skill v1.0.0
currentTrusted Publisher100/100version: "1.0.0" name: lipsync description: >- Add audio-driven avatar mouths to an XR Blocks app with the lipsync addon — heuristic vowel-formant viseme mapping that turns any MediaStream (mic or remote peer's voice) into mouth shapes on a StylizedFace canvas decal attached to an avatar's head. No ML runtime, no model download. Use when authoring or debugging avatar mouths in single-user demos or in multiplayer scenes paired with xb-netblocks (every remote peer's voice stream drives their own face). Public surface: LipsyncMouth (the driver), xb.StylizedFace (the decal, in xrblocks core), session.voice.onTrack for the netblocks hook, plus the lower-level FormantVisemeMapper, MfccExtractor, computeAudioFeatures and types (VisemeWeights, VisemeTarget, AudioFeatures) for plugging in a model-based mapper later. For the DSP pipeline, caveats, and samples read this folder's README.md.
lipsync — audio-driven mouths for XR Blocks
lipsync turns any avatar with a head pivot into a face that visibly mouths along to a MediaStream. Mental model: a per-frame FFT + formant analyser writes viseme weights into a `target`; the `target` (typically `xb.StylizedFace`) re-rasterises a small canvas decal anchored to the head sphere's local `-Z`. No model download, no ML runtime.
Full reference (DSP pipeline, samples, caveats, public surface): `README.md`. Samples: `samples/`.
When to use
Pair with `netblocks` so every remote peer's voice stream drives their own avatar's mouth, turning shared rooms from silent spheres into faces that visibly speak. Standalone use is fine too: a TTS playback, an NPC, or a single-user puppet head. The face primitive (xb.StylizedFace) is in xrblocks core, so any consumer can drive it via setVisemes(VisemeWeights), not just lipsync.
For ML-grade phoneme accuracy, the lower-level pieces (FormantVisemeMapper, MfccExtractor, computeAudioFeatures) and types are exported so a model-based mapper can slot in without touching the addon's public surface.
Mental model
MediaStream→ WebAudioAnalyserNode→ byte frequency + time-domain buffers each frame.computeAudioFeaturesextracts RMS, voicing, F1, F2, and a few band energies.FormantVisemeMappermaps F1/F2 to six viseme weights (jawOpen,aa,oo,oh,ee,consonant) with frame-rate-independent smoothing (1 - exp(-dt / tau)).LipsyncMouthwrites the weights to itstargetviasetVisemes(...).xb.StylizedFacere-rasterises a 256×256 canvas (one dark mouth ellipse, optional eyes) and uploads it as aCanvasTextureon a small plane.
Public surface
| Symbol | Where | Purpose | |
|---|---|---|---|
LipsyncMouth | xrblocks/addons/lipsync/index.js | The driver xb.Script. Constructor: (stream: MediaStream, {target, audioContext?, fftSize?, silenceThreshold?, silenceHoldMs?}). | |
xb.StylizedFace | xrblocks core (import {StylizedFace} from 'xrblocks') | The face decal. Options: headRadius, textureSize, showEyes. | |
RemoteUserAvatar.face | xrblocks/addons/netblocks | Already a StylizedFace, ready for LipsyncMouth to drive. | |
FormantVisemeMapper, MfccExtractor, computeAudioFeatures | xrblocks/addons/lipsync/index.js | Pure modules a future ML mapper can plug into. | |
Types: VisemeWeights, VisemeTarget, FormantVisemeMapperOptions, MfccExtractorOptions, AudioFeatures, AudioFeatureInputs | xrblocks/addons/lipsync/index.js | Shared shapes for custom drivers. |
Lifecycle and ownership
- The caller owns the
MediaStream.LipsyncMouth.dispose()disconnects audio nodes but never stops tracks. If you got the stream fromgetUserMedia, stop the tracks yourself when done. - The caller owns the
AudioContextwhen passed in. Always passaudioContextto reuse a shared context for any scene with more than one mouth (browsers cap contexts at around six per page). If omitted,LipsyncMouthcreates its own and closes it on dispose. - The caller owns the
targetface.dispose()resets it to rest pose so a speaker who stops mid-vowel never leaves their avatar's mouth frozen open, but never disposes the face itself. - Instances are one-shot. After dispose, construct a new
LipsyncMouth.
Browser quirks worth knowing
- Microphone access requires HTTPS in modern browsers. Use
localhostor a real cert for cross-device testing. - Browsers can drop a
MediaStreamAudioSourceNodeunless the same stream is also being pumped by anHTMLMediaElement.LipsyncMouthcreates a muted off-DOM<audio>primer per stream to keep WebAudio alive. Same workaroundSpatialVoiceuses. - High-pitched voices (children, sopranos) push formants up and reduce vowel separation. Speaker-relative normalisation would help and is a sensible follow-up.