npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pixi-rn/sound

v0.5.0

Published

@pixi/sound on React Native, backed by react-native-audio-api — the real Web Audio graph, filters and all.

Downloads

129

Readme

@pixi-rn/sound

@pixi/sound on React Native, backed by react-native-audio-api — a real Web Audio graph: filters, per-instance volume and speed, sprites, decodeAudioData. Plus a small set of playback helpers on top (throttling, music-track switching, toggleable filter chains).

Install

npm install @pixi-rn/sound @pixi/sound react-native-audio-api
import { sound, addSound, initAudio } from '@pixi-rn/sound';

useEffect(() => {
  initAudio();
  void addSound('coin', require('./assets/coin.wav'));
}, []);

sound.play('coin');

Always import from @pixi-rn/sound — never import ... from '@pixi/sound' directly, and don't import react-native-audio-api yourself either. This package re-exports everything @pixi/sound exports.

[!WARNING] react-native-audio-api is native code, so this needs a development or EAS build. It cannot run in Expo Go and cannot reach a device over OTA.

Bring the engine up

Call initAudio() from an effect — never at module scope or in a render body. It's safe to call more than once (a no-op after the first success) and it never throws:

useEffect(() => {
  if (!initAudio()) {
    const { lastError } = soundDiagnostics();
    // fall back to a silent game — audio is an enhancement
  }
}, []);

Importing the package by itself does nothing native — that only happens inside initAudio().

initAudio() also:

  • claims the platform audio session, so the first cue plays without a delay
  • primes the Web Audio graph with a silent buffer
  • binds AppState so audio pauses while the app is backgrounded and resumes on return (covers iOS's transient inactive, e.g. the app switcher, too)

soundDiagnostics() returns { ready, lastError }. lastError: null means nothing threw — it does not mean audio is actually producing sound; no platform reports that back.

Loading clips

require('./clip.wav'), a URL string, or an ArrayBuffer of already-decoded bytes are all valid sources.

const coin = await addSound('coin', require('./assets/coin.wav'), { volume: 0.8 });
coin.play();

loadSounds decodes a batch, one at a time, and returns whichever succeeded — a clip that fails to decode is skipped rather than failing the whole batch:

const clips = await loadSounds(
  { jump: require('./jump.wav'), coin: require('./coin.wav') },
  {
    options: (name) => ({ volume: name === 'coin' ? 0.6 : 0.85, preload: true }),
    onError: (name, err) => console.warn(`sound "${name}" failed`, err),
    cancelled: () => unmounted, // stop and destroy the in-flight clip
  },
);
clips.jump?.play();

Every buffer is uncompressed float32 PCM in memory — seconds × sampleRate × channels × 4 bytes. Fine for short effects; a minutes-long track costs tens of MB, so measure before loading one.

Playback helpers

throttle — rate-limit a bursty cue

const playCoin = throttle(() => coin.play(), 90);
for (const c of collectedThisFrame) playCoin(); // at most one play every 90ms

TrackSwitcher — swap between looping tracks

For background music or ambience beds: switching pauses the track that's no longer current instead of destroying it, so resuming picks up where it left off.

const music = new TrackSwitcher<'menu' | 'level'>();
music.add('menu', menuTrack);
music.add('level', levelTrack);

music.play(inMenu ? 'menu' : 'level');
music.play(muted ? null : inMenu ? 'menu' : 'level'); // null = silence
music.setVolume(settings.musicVolume);

Every method is safe to call redundantly — play/setVolume only touch the audio graph on an actual change. add() can register a track that finishes loading after play() already asked for it; it starts immediately once added. TrackSwitcher doesn't own its Sound objects — destroy them yourself when you're done with them.

FilterGroup — toggle a filter chain across a set of clips

const caveEcho = new FilterGroup(() => [new filters.ReverbFilter(1.6, 2.5)]);
caveEcho.add(jumpClip);
caveEcho.add(footstepLoop);

caveEcho.set(inCave); // on/off; a no-op if the state didn't change

buildFilters runs once, the first time the chain turns on, and the result is reused — build a filter like ReverbFilter more than once per toggle and you pay its setup cost (its impulse response is generated in a JS loop) every time. A clip added while the chain is on picks it up immediately.

Filters

filters is @pixi/sound's own export, re-exported unchanged — ReverbFilter, EqualizerFilter, DistortionFilter, StereoFilter, TelephoneFilter, MonoFilter. Apply per clip (clip.filters = [...]) rather than through sound.filtersAll, which is context-wide and reaches every other sound too, including music.

Known gaps

  • No DynamicsCompressorNode. sound.context.compressor is a unity-gain passthrough, not a real compressor — reading threshold/knee/ratio off it gets you undefined. There is no master limiting: many loud simultaneous sources can clip where a browser wouldn't.
  • OfflineAudioContext only implements decodeAudioData. If you need real offline rendering (startRendering), construct react-native-audio-api's own OfflineAudioContext directly.
  • document.createElement('audio').canPlayType() always returns ''. There's no HTML <audio> here; this is what keeps @pixi/sound on its WebAudio code path.

Relationship to pixi-rn/audio

pixi-rn ships a smaller audio module of its own, built on expo-audio.

| | pixi-rn/audio | @pixi-rn/sound | | ------------------------------------ | --------------- | ------------------------ | | backend | expo-audio | react-native-audio-api | | model | pooled players | a real Web Audio graph | | filters, sprites, per-instance pitch | no | yes | | Expo Go | yes | no |

Reach for @pixi-rn/sound when you want the audio graph. Reach for pixi-rn/audio when you want a handful of clips to fire cheaply and Expo Go support matters.

Licence

MIT.