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

guzheng-sound

v0.1.1

Published

Tiny dependency-free Web Audio engine that makes the browser sound like a guzheng (古筝): plucked notes, press-bends, tremolo, vibrato and glissando.

Readme

guzheng-sound

npm version CI license: MIT

A tiny, dependency-free Web Audio engine that makes the browser sound like a guzheng (古筝). It uses Karplus–Strong plucked-string synthesis — no samples, no network, ~5 KB gzip (ESM build) — and supports the techniques that make a guzheng a guzheng:

  • 🎵 Notes — pluck any of the 21 strings (or your own tuning)
  • ↗️ Bendable — press-bends (the left-hand technique: mi→4, la→7) and live pitch glides
  • 〰️ Tremolo (摇指) — rapid repeated plucking
  • 🌊 Vibrato — LFO pitch wobble
  • Glissando (花指) — string sweeps

Ships as ESM and CommonJS with TypeScript types. It targets any browser with the Web Audio API (all current Chromium, Firefox, and WebKit browsers); a CI-tested browser matrix is tracked for a future release. Use it in plain HTML, React, Vue, Svelte, or any bundler.

Install

npm install guzheng-sound

Or straight from a CDN, no build step:

<script type="module">
  import { Guzheng } from "https://esm.sh/[email protected]";
  const gz = new Guzheng();
  document.querySelector("button").addEventListener("click", () => gz.pluck(10));
</script>

Quick start

import { Guzheng } from "guzheng-sound";

const gz = new Guzheng();

// Browsers only allow audio after a user gesture — pluck inside a click/tap.
button.addEventListener("pointerdown", () => {
  gz.pluck(10); // string 10 = middle D (D4) in the default tuning
});

Autoplay: the AudioContext is created lazily on your first sound. Make that first call happen inside a click/keydown/pointer handler and everything just works. You can also call await gz.resume() from a gesture.

Notes

Strings are indexed 0 (lowest) to 20 (highest). You can also target a pitch:

gz.pluck(0);                       // lowest string
gz.pluck(20, { velocity: 0.6 });   // highest string, softer
gz.pluckNote("A4");                // nearest string to A4
gz.pluckMidi(62);                  // nearest string to MIDI 62 (D4)

gz.setTranspose(2);                // capo up a whole tone (affects later plucks)

Bendable strings

Guzheng players press a string behind the bridge to raise its pitch — that's how the pentatonic instrument produces 4 and 7. Bend during the attack, or bend a ringing string live:

// Press-bend up a semitone as it's plucked (string 3 "mi" → "fa"/4):
gz.pluck(2, { bend: 1 });

// Whole-tone bend (string "la" → "7"), in cents if you prefer:
gz.pluck(4, { bendCents: 200 });

// Bend a note *while* it rings, then release it back:
const voice = gz.pluck(7);
setTimeout(() => voice?.bend(1), 200);   // glide up a semitone
setTimeout(() => voice?.bend(0), 600);   // ease back down

Tremolo (摇指)

const roll = gz.tremolo(10, { intervalMs: 55 });
// ...later
roll.stop();

// Or roll for a fixed time:
gz.tremolo(10, { durationMs: 1500 });

Vibrato

gz.pluck(13, { vibrato: true });
gz.pluck(13, { vibrato: { rateHz: 6, depthCents: 20 } });

Glissando (花指)

gz.glissando(0, 20);                 // sweep up the whole instrument
gz.glissando(20, 0, { stepMs: 24 }); // faster sweep down

Bring your own AudioContext / tuning

import { Guzheng, standardTuning, noteToMidi } from "guzheng-sound";

const ctx = new AudioContext();
const gz = new Guzheng({
  audioContext: ctx,        // share a context with the rest of your app
  tuning: standardTuning(21, noteToMidi("D2")!), // 21 strings from D2 (the default)
  reverb: true,
  reverbMix: 0.25,
  masterVolume: 0.9,
  sustainSeconds: 8
});

Any tuning works — pass an array of MIDI note numbers, one per string:

const gzG = new Guzheng({ tuning: standardTuning(21, noteToMidi("G2")!) }); // G pentatonic

Cleanup

gz.stopAll();  // silence every ringing string
gz.dispose();  // stop everything and close the context (if this instance made it)

API

new Guzheng(options?)

| option | type | default | description | | --- | --- | --- | --- | | audioContext | AudioContext | created lazily | share an existing context | | tuning | number[] | 21-string D pentatonic | MIDI note per string, low→high | | reverb | boolean | true | add a subtle convolution reverb | | reverbMix | number | 0.25 | reverb wet amount (0–1) | | masterVolume | number | 0.9 | master gain (0–1) | | sustainSeconds | number | 8 | rendered decay length per string |

Methods: pluck(i, opts?), pluckMidi(midi, opts?), pluckNote(name, opts?), tremolo(i, opts?), glissando(from, to, opts?), nearestString(midi), setTranspose(semitones), getTranspose(), resume(), stopAll(), dispose(). Properties: context, tuning, stringCount. Static: Guzheng.isSupported(), Guzheng.DEFAULT_TUNING.

pluck / tremolo / glissando options

velocity (0–1), bend (semitones) / bendCents, vibrato (true or { rateHz, depthCents }), transpose (semitones), when (context time). tremolo adds intervalMs, durationMs. glissando adds stepMs.

Voice (returned by pluck)

  • voice.bend(semitones, glideSeconds?) — glide the pitch (0 = un-bent)
  • voice.release(fadeSeconds?) — fade out then stop
  • voice.stop() — stop immediately

Helpers

  • standardTuning(stringCount?, rootMidi?)number[]
  • midiToFrequency(midi)number
  • noteToMidi("F#3")number | null

Performance & memory

Buffers are synthesised lazily and cached per string, per instance. The first pluck of a given string renders that string's decay buffer on the main thread; subsequent plucks reuse it. With the defaults (21 strings, sustainSeconds: 8, 48 kHz) a fully-exercised instance holds roughly 32 MB of mono buffers, plus the reverb impulse. To reduce first-note latency and memory:

  • lower sustainSeconds (shorter tail, smaller buffers);
  • share one AudioContext (and ideally one Guzheng) across your app;
  • pluck the strings you need once up front to pre-warm their buffers.

A first-note latency/memory benchmark across low-end mobile browsers is tracked for a future release.

Development

npm install        # also builds via the prepare hook
npm run build      # emit ESM + CJS + types into dist/
npm test           # build, then run the node:test suite
npm run typecheck  # type-only check
npm run lint       # typecheck + prettier --check
npm run format     # prettier --write

See CONTRIBUTING.md before opening a pull request.

License

MIT © 2026 ericphamhoangdev