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

animalese-web

v0.1.0

Published

Web Audio API animalese (Animal Crossing speech) synthesis with real-time per-letter playback and typewriter sync callbacks

Readme

animalese-web

Modern Web Audio API library for synthesizing Animal Crossing "animalese" speech with real-time streaming playback, typewriter sync callbacks, and full playback control.

A ground-up rewrite of animalese.js using modern web standards. Try the live demo

Features

  • Real-time streaming — audio plays letter-by-letter via Web Audio API lookahead scheduling, not generated all at once
  • Typewriter synconLetter callback fires in sync with each letter's audio, perfect for text reveal effects
  • Playback control — stop, pause, and resume speech with SpeechHandle
  • Async-friendlyfinished Promise resolves when speech completes naturally
  • Per-call overrides — change pitch, speed, or volume for individual speak() calls (great for character voices)
  • Custom audio routing — connect output to any AudioNode for effects chains
  • Zero runtime dependencies — ~5 KB ESM / ~4 KB CJS
  • Full TypeScript — complete type definitions with JSDoc
  • Dual package — ESM + CJS with proper exports field, tree-shakeable

Comparison with animalese.js

| Aspect | animalese.js | animalese-web | |---|---|---| | Audio engine | Synchronous WAV data URI via new Audio() | Web Audio API with lookahead scheduling | | Pitch shifting | Sample-index stepping (aliasing artifacts) | Native playbackRate (hardware-interpolated) | | Playback control | None — fire and forget | stop / pause / resume / finished Promise | | Typewriter sync | None | onLetter callback synced to audio time | | Runtime deps | 3 (riffwave.js, Blob.js, FileSaver.js) | 0 | | Types | None | Full TypeScript | | Module format | Global script tag | ESM + CJS dual package | | Word shortening | First + last character | First character only (matches AC games) | | Concurrent speech | Not supported | Multiple overlapping speak() calls | | Volume control | Not supported | Per-call volume via GainNode |

Why rewrite?

The original animalese.js pioneered the concept — full credit to Acedio for the idea and the sample library. But its approach of generating an entire WAV file synchronously and playing it through new Audio() means:

  • No way to stop, pause, or control playback once started
  • No way to sync visual effects (like typewriter text) to the audio
  • Pitch shifting via sample-index stepping introduces aliasing artifacts
  • Three runtime dependencies for WAV encoding and file saving that aren't needed for web playback

animalese-web solves all of these by building on the Web Audio API, which provides precise scheduling, native pitch shifting, and fine-grained playback control.

Installation

npm install animalese-web

The package includes the animalese.wav sample library (172 KB) in assets/. You can serve it statically or import it depending on your bundler setup.

Quick Start

import { Animalese } from "animalese-web";

const ctx = new AudioContext();
const animalese = new Animalese(ctx);

// Load the sample library (do this once)
await animalese.load("/animalese.wav");

// Speak!
animalese.speak("Hello! Welcome to my island!");

With typewriter sync

const handle = animalese.speak("Hello! Welcome to my island!", {
  onLetter({ char, index }) {
    // Reveal text up to the current letter
    display.textContent = text.slice(0, index + 1);
  },
  onComplete() {
    // Speech finished naturally
    display.textContent = text;
  },
});

API Reference

new Animalese(ctx, config?)

Creates an Animalese synthesizer instance.

  • ctx — An AudioContext. You own this and must handle autoplay policy (call ctx.resume() on a user gesture before speaking).
  • config — Optional default configuration, overridable per speak() call.

AnimaleseConfig

| Option | Type | Default | Description | |---|---|---|---| | basePitch | number | 1.0 | Base pitch multiplier. >1 = higher, <1 = lower | | pitchRange | number | 0.25 | Random pitch variation per letter (e.g., 0.25 = +/- 0.125) | | letterDuration | number | 0.075 | Duration of each letter in seconds | | shortenWords | boolean | false | Shorten words to first letter only (AC style) | | volume | number | 1.0 | Volume from 0.0 to 1.0 | | destination | AudioNode | ctx.destination | Audio node to connect output to |

animalese.load(source)

async load(source: string | ArrayBuffer | Response): Promise<void>

Loads and decodes the WAV sample library. Must be called once before speak(). Accepts a URL string, an ArrayBuffer, or a fetch() Response. Idempotent — subsequent calls reload the samples.

animalese.isLoaded

get isLoaded(): boolean

Returns true if samples have been loaded and the instance is ready to speak. Useful for gating UI (e.g., disabling a "Speak" button until ready).

animalese.speak(text, options?)

speak(text: string, options?: SpeakOptions): SpeechHandle

Speaks text with animalese synthesis. Returns immediately with a SpeechHandle for controlling playback.

  • Multiple concurrent speak() calls will overlap — stop the previous handle if sequential speech is desired.
  • Throws if load() has not been called.

animalese.dispose()

dispose(): void

Releases the decoded sample buffers from memory. After calling dispose(), load() must be called again before speaking. Useful for cleanup in single-page applications.

SpeakOptions

All AnimaleseConfig fields can be overridden per call, plus:

| Option | Type | Description | |---|---|---| | onLetter | (letter: ScheduledLetter) => void | Fires when each letter starts playing. Primary hook for typewriter sync. | | onComplete | () => void | Fires when speech finishes naturally (not on stop()). |

SpeechHandle

Returned by speak(). Controls an active speech session.

| Member | Type | Description | |---|---|---| | stop() | () => void | Stop playback immediately. Cannot be resumed. | | pause() | () => void | Pause playback. Position is saved for resume. | | resume() | () => void | Resume playback after pause. | | finished | Promise<void> | Resolves when speech finishes naturally or is stopped. | | state | 'playing' \| 'paused' \| 'stopped' \| 'finished' | Current playback state. |

ScheduledLetter

Object passed to the onLetter callback.

| Field | Type | Description | |---|---|---| | char | string | The character from the original input string | | index | number | Position in the original input string | | time | number | AudioContext.currentTime when this letter was scheduled | | isSilent | boolean | true for spaces, punctuation, and other non-letter characters |

Recipes

Pause and resume

const handle = animalese.speak("A long piece of dialogue...");

// Later...
handle.pause();
console.log(handle.state); // "paused"

// Resume from where we left off
handle.resume();

Await completion

const handle = animalese.speak("First line of dialogue.");
await handle.finished;

animalese.speak("Second line, after the first finishes.");

Character voices with per-call pitch

// Low, grumbly voice
animalese.speak("I'm Tom Nook.", { basePitch: 0.7 });

// High, peppy voice
animalese.speak("Hi there!", { basePitch: 1.5, pitchRange: 0.3 });

Custom audio destination

// Route through a reverb effect
const convolver = ctx.createConvolver();
convolver.connect(ctx.destination);

const animalese = new Animalese(ctx, { destination: convolver });

Stopping previous speech

let currentHandle: SpeechHandle | null = null;

function say(text: string) {
  currentHandle?.stop();
  currentHandle = animalese.speak(text, {
    onComplete() { currentHandle = null; },
  });
}

Browser Autoplay Policy

Modern browsers block audio playback until a user gesture (click, tap, keypress) has occurred. You must resume the AudioContext inside a user-initiated event handler:

button.addEventListener("click", () => {
  if (ctx.state === "suspended") ctx.resume();
  animalese.speak("Hello!");
});

How It Works

The animalese.wav file contains 26 sequential letter samples (A–Z), each 150ms long. When load() is called, the file is decoded via decodeAudioData() and sliced into 26 individual AudioBuffer objects — one per letter. This is sample-rate independent; the Web Audio API handles resampling automatically.

When speak() is called, input text is mapped to a sequence of letter indices (A=0, B=1, ..., Z=25, everything else = silence). If shortenWords is enabled, each word is reduced to its first letter.

Two concurrent loops drive playback:

  • Scheduler loop (setTimeout, 25ms interval) — looks 100ms ahead and schedules AudioBufferSourceNode.start() calls at precise future times using AudioContext.currentTime. Each letter gets a pitch of basePitch + random variation. This lookahead prevents audio dropouts while keeping cancel latency low.

  • Callback loop (requestAnimationFrame) — compares AudioContext.currentTime against scheduled letter times and fires onLetter callbacks in sync with actual audio playback. This separation ensures visual updates run at display refresh rate while audio scheduling remains reliable.

Pause saves the current position and stops all active audio nodes. Resume restarts both loops from the saved position with fresh timing.

Demo

Live demo — try it in your browser.

The demo/ directory contains an interactive demo with:

  • Text input with typewriter text reveal
  • Real-time sliders for pitch, variation, speed, and volume
  • Pause / resume / stop controls
  • Word shortening toggle

Run it locally:

pnpm install
pnpm dev

Opens on http://localhost:3000.

Credits

  • Original animalese.js by Acedio — the idea and the animalese.wav sample library
  • Animal Crossing is a trademark of Nintendo. This project is fan-made and is not affiliated with or endorsed by Nintendo.

License

MIT