@thehoneyjar/sound-engine
v0.2.4
Published
Shared sound engine for THJ apps with music and effects management
Readme
@thehoneyjar/sound-engine
Shared sound engine for THJ apps with advanced music and effects management.
Features
- Centralized audio management - Single source of truth for all audio assets
- Lazy loading - Audio files are loaded on demand with caching
- Music management - Seamless transitions, crossfades, and background music control
- Sound effects - Category-based sound variants with fallback support
- React hooks - Easy integration with
useSoundEffectsanduseMusicState - Local storage integration - Respects user sound preferences
- Performance optimized - Limited concurrent sounds, efficient caching
Usage
Provider Setup
Wrap your application with the SoundEngineProvider once so every component shares the same engine instance, manifest, and configuration.
import { SoundEngineProvider } from "@thehoneyjar/sound-engine";
export function AppShell({ children }: { children: React.ReactNode }) {
return <SoundEngineProvider>{children}</SoundEngineProvider>;
}Basic Sound Effects
import { useSoundEffects } from "@thehoneyjar/sound-engine";
function MyComponent() {
const { playSound } = useSoundEffects();
return (
<button
onMouseEnter={() => playSound("buttonHover")}
onClick={() => {
playSound("buttonClickPlay"); // Category-specific sound
// Do something...
playSound("success");
}}
>
Click Me
</button>
);
}Music Management
import { useMusicState } from "@thehoneyjar/sound-engine";
function GameScreen() {
const { playMusic, stopMusic, crossfadeTo, isPlaying } = useMusicState();
useEffect(() => {
// Start background music
playMusic("lobbyMusic", { fadeIn: true });
return () => {
// Clean up on unmount
stopMusic(true); // Fade out
};
}, []);
const handleVictory = () => {
// Crossfade to victory music
crossfadeTo("victoryMusic", 1000);
};
return <div>Game content...</div>;
}Pause and Resume Music
New in v0.2.0: Music playback now supports pause/resume with position preservation!
import { useSoundtrackDirector } from "@thehoneyjar/sound-engine";
function MusicPlayer() {
const { pause, resume, play, status } = useSoundtrackDirector();
const handlePause = async () => {
// Pause with fade out (preserves playback position)
await pause(true);
};
const handleResume = async () => {
// Resume from paused position with fade in
await resume(true);
};
return (
<div>
<p>Now Playing: {status.title}</p>
<p>Status: {status.isPlaying ? "Playing" : "Paused"}</p>
<button onClick={handlePause}>Pause</button>
<button onClick={handleResume}>Resume</button>
</div>
);
}Key Differences:
pause()- Pauses music and preserves playback positionresume()- Continues from paused positionstop()- Stops music and resets to beginningplay()- Smart: resumes if same track is paused, otherwise starts from beginning
Automatic Resume:
When you call play() on a paused track, it automatically resumes from the saved position:
await pause(true); // Pause at position 45.2 seconds
await play({ fadeIn: true }); // Automatically resumes from 45.2 secondsAdvanced Usage with Tetris Example
import { useSoundEffects } from "@thehoneyjar/sound-engine";
function TetrisGame() {
const { playSound, playMusic, stopMusic } = useSoundEffects();
// Different music for different game states
const updateMusic = async (gameState: string) => {
switch (gameState) {
case "menu":
await playMusic("tetrisIdleMusic");
break;
case "playing":
await playMusic("tetrisDefaultMusic");
break;
case "highScore":
await playMusic("tetrisHighScoreMusic", { fadeIn: true });
break;
}
};
// Sound effects for game actions
const handlePieceMove = () => {
void playSound("tetrisPieceMove");
};
const handleLineClear = (lines: number) => {
if (lines === 4) {
void playSound("tetris4Lines"); // Special sound for Tetris
} else {
void playSound("tetrisLineClear");
}
};
return <div>Tetris game...</div>;
}Sound Categories
Button sounds support categories for different UI sections:
play- Coin/token soundguts- Heavy/impact soundcreate- Light/magical soundearn- Cash register/cha-ching sound
playSound("buttonClick"); // Default sound
playSound("buttonClickPlay"); // Play section sound
playSound("buttonClickGuts", { volume: 0.5 }); // Guts section at 50% volumeConfiguration
You can still imperatively configure the engine, but prefer the provider for React apps:
import { SoundEngineProvider } from "@thehoneyjar/sound-engine";
const config = {
basePath: "/assets/audio",
defaultVolume: 0.8,
preloadSounds: ["buttonClick", "tetrisIdleMusic"],
};
export function AppShell({ children }: { children: React.ReactNode }) {
return <SoundEngineProvider config={config}>{children}</SoundEngineProvider>;
}Category Controllers
Landing surfaces use slot-based sounds (hover primary/secondary, press, click). Build a controller with createSoundController and reuse it across interactive elements.
import { useSoundEffects } from "@thehoneyjar/sound-engine";
function CtaButton({ id }: { id: string }) {
const { createSoundController } = useSoundEffects();
const controller = useMemo(
() =>
createSoundController({
category: "cta",
id,
hoverStrategy: "primaryThenSecondary",
preload: true,
}),
[createSoundController, id],
);
return (
<button
onMouseEnter={() => controller.hover()}
onMouseLeave={() => controller.resetHover()}
onMouseDown={() => controller.press()}
onClick={() => controller.click()}
>
Call to Action
</button>
);
}Available Sounds
Sound Effects
- Universal:
buttonClick,buttonHover,ctaButtonClick,ctaButtonHover - Voice:
henloVoice - Feedback:
success,error,notification,coinDrop - Piggy Bank:
piggyOink,piggyClick,piggyDrag,piggyRelease - Tetris:
tetrisMenuNav,tetrisMenuSelect,tetrisPieceMove,tetrisPieceRotate,tetrisHardDrop,tetrisLineClear,tetris4Lines,tetrisGameOver
Music Tracks
- Tetris:
tetrisIdleMusic,tetrisDefaultMusic,tetrisHighScoreMusic - General:
lobbyMusic,gameMenuMusic,victoryMusic,defeatMusic
Adding New Sounds
Extend the default library via the provider. You can register new effects, music tracks, and category slots in one place:
import { SoundEngineProvider } from "@thehoneyjar/sound-engine";
const customLibrary = {
effects: {
myNewSound: {
path: "/sounds/custom/my-new-sound.mp3",
defaultVolume: 0.42,
tags: ["custom"],
},
},
categories: {
custom: {
slots: {
click: "myNewSound",
},
preload: ["click"],
},
},
};
export function AppShell({ children }: { children: React.ReactNode }) {
return (
<SoundEngineProvider library={customLibrary}>
{children}
</SoundEngineProvider>
);
}Publishing
This package is configured for private distribution. Before publishing make sure you are authenticated with the correct registry (npm login --scope @thj). Then use the helpers:
bun run clean
bun run build
npm publish --access restrictedAlternatively, reference a git tag directly:
npm install git+ssh://[email protected]:<org>/henlo-monorepo.git#sound-engine-v0.1.0The build output is emitted to dist/ and included automatically via the package files field.
