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

gulp-mu-sound-atlas

v1.1.2

Published

Build sound atlases (Gulp plugin) and play them in the browser (runtime from µLib).

Readme

gulp-mu-sound-atlas

Builds a sound atlas from many short audio files: one combined audio blob (raw WAV or MP3) plus a JSON timing map that tells the client where each sound starts, how long it is and where it loops.

This package provides two parts:

| Part | Import | Environment | Purpose | | ---- | ------ | ----------- | ------- | | Build | gulp-mu-sound-atlas | Node.js / Gulp | Combine many audio files into one atlas | | Runtime | gulp-mu-sound-atlas/runtime | Browser | Load and play atlas files (from µLib Sounds.mjs) |

Identical sounds are de-duplicated, all inputs are resampled to a common format, and short silence gaps are inserted between sounds. The result is two files:

  • dataPath — the combined audio (.wav raw PCM, or .mp3 when doMp3 is set)
  • jsonPath — the timing map ({ sounds: { name: [start, duration, loopStart, loopEnd] }, ... })

Install

# Build tool (Gulp pipeline)
npm install --save-dev gulp-mu-sound-atlas

# Runtime is included in the same package — no extra install needed in the browser bundle
npm install gulp-mu-sound-atlas

Build (Gulp)

import SoundAtlas from 'gulp-mu-sound-atlas';

const soundatlas = new SoundAtlas();

function sounds() {
	return gulp.src('./dev/media/final/sounds/wav/*.wav')
		.pipe(soundatlas.Create({
			doMp3: false,
			dataPath: './skins/std/snds/app.sounds.wav',
			jsonPath: './skins/std/snds/app.sounds.json',
			log: true   // optional: per-file progress (default: false)
		}));
}

Loop points can be encoded in the file name, e.g. engine.ls.1000.le.5000.wav → loop from sample 1000 to 5000.

Build examples

MP3 atlas (smaller download, e.g. for mobile):

.pipe(soundatlas.Create({
	doMp3: true,
	mp3KBitRate: 128,
	dataPath: './public/snds/ui.sounds.mp3',
	jsonPath: './public/snds/ui.sounds.json'
}));

Mono atlas from stereo sources:

.pipe(soundatlas.Create({
	cnvNoOfChannels: 1,
	cnvStereo: soundatlas.CNV_STEREO_MONO,
	dataPath: './public/snds/app.sounds.wav',
	jsonPath: './public/snds/app.sounds.json'
}));

Gulp task wired into a watch/build pipeline:

import gulp from 'gulp';
import SoundAtlas from 'gulp-mu-sound-atlas';

const soundatlas = new SoundAtlas();

export function sounds() {
	return gulp.src('assets/sounds/**/*.wav')
		.pipe(soundatlas.Create({
			dataPath: 'dist/snds/app.sounds.wav',
			jsonPath: 'dist/snds/app.sounds.json'
		}));
}

export const build = gulp.series(sounds, /* …other tasks… */);

Input files click.wav, error.wav, engine.ls.1000.le.5000.wav become one app.sounds.wav plus a JSON map with keys click, error, engine.

Runtime (browser playback)

The build step produces two files that belong together in the same folder:

skins/std/snds/
  app.sounds.json   ← timing map (sound names → start, duration, loops)
  app.sounds.wav    ← combined audio blob (or .mp3 when doMp3 was set)

The runtime module Sounds (from µLib Sounds.mjs) loads both files, decodes the audio via the Web Audio API and plays individual sounds by name — without loading dozens of separate files at runtime.

Quick start

import Sounds, { Config } from 'gulp-mu-sound-atlas/runtime';

// 1. Unmute (defaults mute everything)
Config.ALL_AUDIO_MUTE = false;
Config.SOUND_CONTROL_MUTE = false;

// 2. Load atlas — JSON first, audio file is fetched automatically
Sounds.load('app.sounds.json', './skins/std/snds');

// 3. Wait until decode finishes, then play
Sounds.registerLoadEvent('myApp', () => {
	Sounds.play('button_up');
});

Pass the JSON file name to load(). The class strips the extension and loads app.sounds.wav (or .mp3) from the same folder. Default path if omitted: ./build/exresources/snds.

User gesture: Browsers may keep AudioContext suspended until the user interacts with the page. Call Sounds.play() from a click/tap handler, or resume the context manually: Sounds.AUDIOCONTEXT.resume().

JSON timing map

The build writes a JSON file like this:

{
  "isMP3": false,
  "sampleRate": 44100,
  "bitRate": 128,
  "sampleSize": 16,
  "noOfChannels": 2,
  "noOfSamples": 123456,
  "sounds": {
    "button_up": [0.0, 0.05, -1, -1],
    "engine":    [0.15, 2.0, 0.25, 1.75]
  }
}

Each entry in sounds maps a sound name to an array of four numbers (seconds):

| Index | Name | Meaning | | ----- | ---- | ------- | | 0 | start | Offset into the combined audio blob | | 1 | duration | Length of this sound | | 2 | loopStart | Loop start within the blob, or -1 for no loop | | 3 | loopEnd | Loop end within the blob, or -1 for no loop |

The runtime reads only sounds; the metadata fields are informational. Loop sounds are handled internally (sub-buffers are extracted so AudioBufferSourceNode.loop works correctly in all browsers).

Static properties

| Property | Type | Description | | -------- | ---- | ----------- | | Sounds.AUDIOCONTEXT | AudioContext | Shared Web Audio context used for decode and playback. | | Sounds.sounds | object | Registry of loaded sounds. Each entry: { buffer, info } where info is the timing array or null for a raw single file. | | Sounds.loadEvents | object | Registered load callbacks (internal map). |

Sounds.load(name, path?)

Load a sound atlas or a single audio file.

| Parameter | Type | Default | Description | | --------- | ---- | ------- | ----------- | | name | string | — | File name, e.g. 'app.sounds.json' or 'app.sounds.wav'. | | path | string | './build/exresources/snds' | Directory containing the atlas files (no trailing slash required). |

Behaviour:

  1. When name ends with .json, the timing map is fetched first. Placeholder entries are created for every sound name in json.sounds.
  2. The audio file is then loaded (name without extension, same folder). The response is decoded with AudioContext.decodeAudioData.
  3. Each sound entry receives the shared AudioBuffer plus its timing slice from the JSON. Loop sounds get an extracted sub-buffer automatically.
  4. All registered load callbacks (registerLoadEvent) are invoked after decode.

Calling load() again with the same name is a no-op (atlas already registered).

Loading audio only (no JSON): pass e.g. 'music.wav'. The entire file is registered under the base name with info: null and played from the start.

Sounds.play(name, vol?)

Play a sound by name.

| Parameter | Type | Default | Description | | --------- | ---- | ------- | ----------- | | name | string | — | Sound name as defined in the JSON map (e.g. 'button_up'). | | vol | number | 100 | Gain value passed to GainNode.gain (use 01 for normal levels, or 100 for legacy behaviour). |

Returns: AudioBufferSourceNode on success, otherwise null/undefined.

Notes:

  • Does nothing when the tab is hidden (document.hidden).
  • Does nothing when the sound is not loaded or the buffer is still null.
  • Loop sounds (info[2] >= 0) set source.loop, loopStart and loopEnd.
  • Non-loop sounds play the slice [start, start + duration].

Keep the returned node if you need to stop playback later (Sounds.stop(source)).

Sounds.playControl(name) / Sounds.playModal(name)

Convenience wrappers that respect mute flags and category volume from Config:

| Method | Uses | | ------ | ---- | | playControl(name) | Config.ALL_AUDIO_MUTE, SOUND_CONTROL_MUTE, SOUND_CONTROL_VOL | | playModal(name) | Config.ALL_AUDIO_MUTE, SOUND_MODAL_MUTE, SOUND_MODAL_VOL |

Returns the same as play() when not muted; otherwise undefined.

Typical use: UI clicks → playControl, dialog open/close → playModal.

Sounds.stop(source, force?)

Stop a playing AudioBufferSourceNode returned by play().

| Parameter | Type | Default | Description | | --------- | ---- | ------- | ----------- | | source | AudioBufferSourceNode | — | Node returned by play(). | | force | boolean | false | When false and the source is looping, only clears loop (fade-out by natural end). When true, calls stop() immediately. |

Returns: the source (loop released) or null after hard stop.

Sounds.registerLoadEvent(name, fn) / Sounds.unRegisterLoadEvent(name)

Register a callback that runs after the atlas audio has been decoded (and all sound entries are ready to play).

Sounds.registerLoadEvent('init', () => {
	console.log('Atlas ready:', Object.keys(Sounds.sounds));
});

// later
Sounds.unRegisterLoadEvent('init');

Multiple handlers can coexist; each is keyed by a unique name string.

Sounds.extractAudioBuffer(src, start, duration)

Low-level helper: copy a time range from an AudioBuffer into a new buffer.

| Parameter | Type | Description | | --------- | ---- | ----------- | | src | AudioBuffer | Source buffer (the combined atlas). | | start | number | Start time in seconds. | | duration | number | Length in seconds. |

Returns: new AudioBuffer. Used internally for loop sounds; rarely needed in application code.

Config (audio settings)

Import Config from gulp-mu-sound-atlas/runtime and override before playing:

| Property | Default | Description | | -------- | ------- | ----------- | | ALL_AUDIO_MUTE | true | Master mute — when true, no sound plays. | | SOUND_CONTROL_MUTE | false | Mute UI/control sounds (playControl). | | SOUND_CONTROL_VOL | 1 | Volume for control sounds (0–1). | | SOUND_MODAL_MUTE | true | Mute modal/dialog sounds (playModal). | | SOUND_MODAL_VOL | 1 | Volume for modal sounds (0–1). | | IS_CHROME | auto | Browser detection; affects loop playback timing on Chrome. |

Typical integration

End-to-end from Gulp build to button click:

import Sounds, { Config } from 'gulp-mu-sound-atlas/runtime';

Config.ALL_AUDIO_MUTE = false;
Config.SOUND_CONTROL_MUTE = false;

Sounds.load('app.sounds.json', './skins/std/snds');

Sounds.registerLoadEvent('app', () => {
	document.getElementById('saveBtn').addEventListener('click', () => {
		Sounds.playControl('button_up');
	});
});

// Optional helper (as used in µLib GUI apps):
function PlaySound(name) {
	return Sounds.playControl(name) ?? Sounds.play(name, 1);
}

Checklist:

  1. Run the Gulp task — deploy app.sounds.json + app.sounds.wav together.
  2. Import Sounds and configure Config mute/volume flags.
  3. Call Sounds.load('app.sounds.json', path) once at startup.
  4. Use registerLoadEvent or wait until Sounds.sounds['button_up'].buffer !== null.
  5. Call play / playControl / playModal from user interactions.

Direct import of the class only:

import Sounds from 'gulp-mu-sound-atlas/runtime/Sounds';
import Config from 'gulp-mu-sound-atlas/runtime/Config';

Examples

Play a UI click on button press (with AudioContext unlock on first interaction):

import Sounds, { Config } from 'gulp-mu-sound-atlas/runtime';

Config.ALL_AUDIO_MUTE = false;
Config.SOUND_CONTROL_MUTE = false;

Sounds.load('app.sounds.json', './dist/snds');

document.addEventListener('click', async (e) => {
	await Sounds.AUDIOCONTEXT.resume();          // unlock on first user gesture
	if (e.target.matches('button')) {
		Sounds.playControl('click');
	}
}, { once: false });

Looping engine sound — start and stop:

let engineSource = null;

document.getElementById('startEngine').addEventListener('click', () => {
	engineSource = Sounds.play('engine', 0.8);   // name from atlas JSON
});

document.getElementById('stopEngine').addEventListener('click', () => {
	engineSource = Sounds.stop(engineSource);    // releases loop first
	// Sounds.stop(engineSource, true);          // or: hard stop immediately
});

Modal dialog sounds (separate mute/volume from UI sounds):

Config.SOUND_MODAL_MUTE = false;
Config.SOUND_MODAL_VOL = 0.7;

function showErrorDialog() {
	Sounds.playModal('error');
	Sounds.playModal('confirm');
}

Master mute toggle (e.g. settings checkbox):

document.getElementById('muteAll').addEventListener('change', (e) => {
	Config.ALL_AUDIO_MUTE = e.target.checked;
});
// playControl / playModal respect ALL_AUDIO_MUTE automatically

Wait until atlas is ready (without registerLoadEvent):

Sounds.load('app.sounds.json', './dist/snds');

function whenReady(callback) {
	const check = () => {
		if (Sounds.sounds.click?.buffer != null) callback();
		else requestAnimationFrame(check);
	};
	check();
}

whenReady(() => Sounds.play('click', 1));

Full page setup (build output → runtime — copy-paste starting point):

<!-- index.html -->
<button id="btn">Save</button>
<script type="module">
import Sounds, { Config } from 'gulp-mu-sound-atlas/runtime';

Config.ALL_AUDIO_MUTE = false;
Config.SOUND_CONTROL_MUTE = false;

Sounds.load('app.sounds.json', './dist/snds');
Sounds.registerLoadEvent('ui', () => {
	document.getElementById('btn').onclick = () => {
		Sounds.AUDIOCONTEXT.resume();
		Sounds.playControl('button_up');
	};
});
</script>

Corresponding Gulp output (same base name, same folder):

dist/snds/app.sounds.json
dist/snds/app.sounds.wav

Options (build)

| Option | Type | Default | Description | | ----------------- | ------- | -------------------------- | -------------------------------------------------------------------------------------------- | | dataPath | string | ".mp3" | Output path of the combined audio blob. | | jsonPath | string | ".json" | Output path of the JSON timing map. | | doMp3 | boolean | false | Encode the atlas as MP3 (via lame) instead of raw WAV. | | mp3KBitRate | number | 128 | MP3 bit rate (kbit/s) when doMp3 is set. | | doCompare | boolean | false | Reserved. | | cnvSampleRate | number | CNV_SAMPLERATE_HIGHEST | Target sample rate, or …_HIGHEST / …_LOWEST to derive it from the inputs. | | cnvSampleSize | number | CNV_SAMPLESIZE_HIGHEST | Target sample size, or …_HIGHEST / …_LOWEST. | | cnvNoOfChannels | number | CNV_NOOFCHANNELS_HIGHEST | Target channel count, or …_HIGHEST / …_LOWEST. | | cnvStereo | number | CNV_STEREO_KEEP | CNV_STEREO_KEEP / CNV_STEREO_SPLIT / CNV_STEREO_MONO. | | cacheFile | string | dataPath + ".cache.json" | Build-cache marker file (see below). Set to "" / false to disable caching. | | log | boolean|function | false | Progress logging (adding sound file…, cache skip). trueconsole.log; pass a function to route messages (as gulp-mu-au / microCSS do). | | quickKey | string | (set by microAU) | Optional mtime/size fingerprint stored in the cache file (see below). |

The CNV_* constants are exposed on the SoundAtlas instance (e.g. soundatlas.CNV_STEREO_MONO).

Build cache (incremental rebuilds)

Generating an atlas (decode → resample → optional MP3 encode → write) is expensive. To avoid rebuilding when nothing changed, the plugin writes a small marker file (cacheFile) next to the atlas after each successful build.

The cache file stores two keys:

  • quickKey — SHA-1 of plugin version, options and source mtimes/sizes (no file reads). When it matches, callers such as gulp-mu-au can skip the entire pipeline before reading inputs.
  • key — SHA-1 of plugin version, options and SHA-1 content hashes of every input (computed when files are piped through the stream). Used as a fallback when the stream runs but outputs are unchanged.

Exported helpers for the fast path:

import SoundAtlas, { BuildQuickCacheKey, IsSoundAtlasCacheCurrent } from 'gulp-mu-sound-atlas';

On the next run it computes a signature from:

  • the plugin version,
  • the relevant options (doMp3, mp3KBitRate, the cnv* settings, dataPath, jsonPath), and
  • a SHA-1 content hash of every input file, in pipe order (slow path), or mtime + size per file (fast path via quickKey).

If dataPath, jsonPath and cacheFile all exist and the stored signature still matches, the rebuild is skipped (one line — no per-file adding sound file spam when log: false and the fast path applies):

gulp-mu-sound-atlas: atlas './app.sounds.wav' is up to date - skipping rebuild.

Any change to an input file, the option set or the plugin version invalidates the cache and triggers a full rebuild. A missing or corrupt cache/output also forces a rebuild. Pass cacheFile: "" to always rebuild.

Decoding MP3 input files (patched dependency)

audio-decode is pinned to 2.2.0, which pulls [email protected]. That decoder version has a bug in reset() (it calls this.free() before _common exists) that throws when MP3 input files are decoded. The fix is a one-line patch shipped in this package under patches/mpg123-decoder+0.4.12.patch.

Because npm cannot reliably auto-apply a patch to a transitive dependency from inside an installed package, the patch must be applied by the consuming project (this is how the reference projects eMP and AiDPix do it):

  1. Add patch-package to your project.
  2. Copy node_modules/gulp-mu-sound-atlas/patches/mpg123-decoder+0.4.12.patch into your own patches/ folder.
  3. Add "postinstall": "npx patch-package" to your package.json.
  4. Keep audio-decode pinned to 2.2.0 so the patch keeps matching.

Note: this only matters when you feed MP3 files as input. Producing an MP3 atlas from WAV inputs (doMp3: true) does not hit the bug.

When developing this module locally (npm install with devDependencies), the prepare script applies the patch automatically if mpg123-decoder is present in this package's node_modules. It is a no-op when the package is installed as a dependency in another project.

Changelog

1.1.2

  • Remove stray createMP3Data debug output during MP3 encoding
  • Default build logging to log: false (wrappers such as gulp-mu-au / microCSS provide their own messages)
  • Runtime errors use host errlog() when available instead of console.error

1.1.1

  • Expand README with runtime API reference and practical examples

1.1.0

  • Add browser runtime (gulp-mu-sound-atlas/runtime) from µLib Sounds.mjs
  • Document runtime API, config and dependency split in README
  • Guard prepare script so consumer installs are not affected

Dependencies

Build (index.mjs) — Node.js only

| Package | Purpose | | ------- | ------- | | audio-decode | Decode MP3/FLAC/Opus input files | | through2 | Gulp stream plugin | | plugin-error | Gulp-compatible errors | | wave-resampler | Sample-rate conversion | | lame.min.mjs (bundled) | MP3 output encoding |

Dev: patch-package (applies the mpg123-decoder fix during local development).

Runtime (runtime/) — browser only

No npm dependencies. Requires:

  • Web Audio API (AudioContext, decodeAudioData)
  • XMLHttpRequest for loading JSON and audio files
  • A user gesture may be required before AudioContext starts on some browsers

The runtime is derived from µLib Sounds.mjs with minimal helpers (Config, WebUtils, StringUtils) bundled under runtime/.

Load/decode errors call globalThis.errlog() when the host app provides it (AiDPix, microCSS family); otherwise they stay silent in production.

License

MIT