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

webapp-voice

v0.1.1

Published

Reusable voice recording + realtime analysis (engine, DSP, analysis plugins, Vue components) with a host-owned persistence port.

Readme

webapp-voice

Reusable voice recording + realtime analysis for Vue 3: a microphone capture/analysis engine, DSP primitives, pluggable analysis modules (vibrato, clarity, pressedness, nasality) and ready-made Vue components (spectrogram, meters, vowel triangle, playback bar, recording library) — all behind a host-owned persistence port, so you decide where recordings live (memory, IndexedDB, your API, S3…).

  • No Nuxt, no auth, no router, no HTTP. Only peer dependency: vue ^3.5.
  • Works in any Vue 3 app (Vite, Nuxt, or other bundlers that resolve ESM + asset URLs).
  • The analysis engine can be used headless (no components) — it emits a reactive feature stream you can render however you like.

Install

npm install webapp-voice vue

Requires a secure context (https or localhost) — microphone capture needs it.


60-second integration

import { createVoiceAnalyzer, InMemoryRecordingStore } from 'webapp-voice'
import 'webapp-voice/style.css'                       // component styles (only if you use the components)

const voice = createVoiceAnalyzer({ store: new InMemoryRecordingStore() })

voice.start()          // asks for the mic, begins capture + analysis + recording
voice.state.f0         // reactive: current pitch in Hz (0 when unvoiced)
voice.plugins          // reactive analysis results (vibrato, clarity, …)
await voice.stop()     // ends the session → voice.pendingRecording is set
await voice.savePending('take 1')   // persists via YOUR store

That's the whole engine. Everything below is reference detail.


Complete minimal wiring (plain Vue 3, no Nuxt)

A runnable version lives in examples/minimal-host.

<script setup lang="ts">
import { reactive, onMounted } from 'vue'
import {
  createVoiceAnalyzer, IndexedDbRecordingStore,
  Controls, Spectrogram, PitchMeter, VolumeMeter, VowelTriangle,
  PluginResult, PlayBar, PlaybackSpectrogram, RecordingsModal, SaveRecordingModal,
  type OverlayConfig,
} from 'webapp-voice'
import 'webapp-voice/style.css'

const v = createVoiceAnalyzer({ store: new IndexedDbRecordingStore() })
const overlays = reactive<OverlayConfig>({ pitch: true, formants: true, harmonics: false })
onMounted(() => v.refreshRecordings())          // load the saved library
</script>

<template>
  <Controls
    :status="v.state.status" :error="v.state.error" :error-kind="v.state.errorKind"
    @start="v.start" @stop="v.stop" @pause="v.pauseSession" @resume="v.resumeSession" />

  <Spectrogram :engine="v.engine" :active="v.state.status === 'running'" :overlays="overlays" />
  <PitchMeter :f0="v.state.f0" :voiced="v.state.voiced" />
  <VolumeMeter :db="v.state.db" />
  <VowelTriangle :voiced="v.state.voiced" :formants="v.state.formants" />

  <PluginResult
    v-for="p in v.plugins" :key="p.id" :plugin="p" @calibrate="v.calibratePlugin" />

  <!-- Save prompt appears after stop() -->
  <SaveRecordingModal
    v-if="v.pendingRecording" :pending="v.pendingRecording"
    @save="v.savePending" @discard="v.discardPending" />

  <!-- Library + playback -->
  <RecordingsModal
    :recordings="v.recordings"
    @play="v.playRecording" @remove="v.removeRecording"
    @download="v.downloadRecording" @close="() => {}" />
  <PlaybackSpectrogram
    v-if="v.playback.active" :data="v.playbackSpectro"
    :duration="v.playback.duration" :time="v.playback.time" @seek="v.seekPlayback" />
  <PlayBar
    v-if="v.playback.active" :time="v.playback.time" :duration="v.playback.duration"
    :playing="v.playback.playing" :name="v.playback.name"
    @toggle="v.togglePlay" @close="v.closePlayback" />
</template>

API reference

createVoiceAnalyzer(options) → analyzer

createVoiceAnalyzer({
  store: RecordingStore,        // REQUIRED — where recordings are persisted (see below)
  plugins?: AnalysisPlugin[],   // default: the built-in 4 (createDefaultPlugins())
  workletUrl?: string,          // default: the bundled worklet (see "Worklet")
})

The returned analyzer object (all state is Vue-reactive):

| Member | Type | Notes | |---|---|---| | state | reactive | status 'idle'\|'starting'\|'running'\|'paused'\|'error', error, errorKind, f0, voiced, clarity, rms, db, formants:number[], sampleRate | | plugins | PluginState[] | live analysis results, one per plugin | | engine | AudioEngine | pass to <Spectrogram :engine>; also emits raw features (engine.onFeature) | | start() | Promise<void> | mic → capture + analysis + recording; sets errorKind on failure | | stop() | Promise<void> | ends session; populates pendingRecording | | pauseSession() / resumeSession() | Promise<void> | pause/resume mic + recording | | setPluginEnabled(id, on) / calibratePlugin(id) | | toggle / calibrate a plugin | | recordings | RecordingItem[] | the saved library (metadata; call refreshRecordings() to load) | | pendingRecording | PendingRecording \| null | the just-stopped take awaiting save/discard | | refreshRecordings() | Promise<void> | reload recordings from the store | | savePending(name?) / discardPending() | | persist / drop the pending take. savePending may throw RecordingStoreError | | removeRecording(id) / downloadRecording(item) | Promise<void> | delete / download a saved take | | playback | reactive | active, playing, time, duration, name | | playbackSpectro | Spectrogram \| null | spectrogram of the playing take | | playRecording(item) / togglePlay() / seekPlayback(t) / closePlayback() | | playback controls |

Components (props → events)

All components are presentational — you own the wiring. Import webapp-voice/style.css once.

| Component | Props | Events | |---|---|---| | Controls | status, error, errorKind | start stop pause resume config recordings | | Spectrogram | engine: AudioEngine, active: boolean, overlays: OverlayConfig | — | | PitchMeter | f0: number, voiced: boolean | — | | VolumeMeter | db: number | — | | VowelTriangle | voiced: boolean, formants: number[], active?: boolean | — | | PluginResult | plugin: PluginState | calibrate: [id] | | SaveRecordingModal | pending: PendingRecording | save: [name] discard | | RecordingsModal | recordings: RecordingItem[] | play: [item] remove: [id] download: [item] close | | PlayBar | time, duration, playing, name | toggle close | | PlaybackSpectrogram | data: Spectrogram\|null, duration, time | seek: [t] viewport: [{start,end,overflow}] | | SpectrogramMinimap | data, duration, time, viewport? | pan: [frac] | | ConfigModal | overlays: OverlayConfig | close | | CollapsibleCard | title: string, modelValue?: boolean | update:modelValue: [boolean] |


Persistence is host-owned (the port)

createVoiceAnalyzer never assumes local vs cloud vs backend. It talks only to a RecordingStore you supply:

interface RecordingStore {
  add(rec: RecordingInput): Promise<string>   // persist; return an id
  list(): Promise<RecordingMeta[]>            // metadata only, newest first (no bytes)
  getBlob(id: string): Promise<Blob>          // fetch the audio bytes on demand
  delete(id: string): Promise<void>
}

interface RecordingInput { name: string; createdAt: number; durationMs: number; mimeType: string; size: number; blob: Blob }
interface RecordingMeta  { id: string; name: string; createdAt: number; durationMs: number; mimeType: string; size: number }
// RecordingItem === RecordingMeta

// Throw this from your store so the host can gate (e.g. redirect to login):
class RecordingStoreError extends Error { code: 'auth-required'|'forbidden'|'not-found'|'quota'|'unknown' }

Included adapters:

  • InMemoryRecordingStore — non-durable, session-scoped (tests/demos).
  • IndexedDbRecordingStore — durable, fully local in the browser. new IndexedDbRecordingStore(dbName?).
  • S3RecordingStore — opt-in cloud (below).

Custom backend — implement the four methods:

const store: RecordingStore = {
  async add(rec)      { const { id } = await api.upload(rec); return id },
  async list()        { return api.listMetas() },
  async getBlob(id)   { return api.download(id) },
  async delete(id)    { await api.remove(id) },
}
createVoiceAnalyzer({ store })

Opt-in cloud via S3RecordingStore

Credential-agnostic — it never holds cloud secrets. You supply presign/list/delete hooks (backed by a small signing endpoint); the adapter does the raw PUT/GET.

import { S3RecordingStore } from 'webapp-voice'
const store = new S3RecordingStore({
  presignPut:   (meta) => api.presignPut(meta),   // → { url, headers? }
  presignGet:   (id)   => api.presignGet(id),      // → string (GET url)
  listMetas:    ()     => api.listMetas(),         // → RecordingMeta[]
  deleteObject: (id)   => api.deleteObject(id),
})

Exported but never on a default path — nothing uses it unless you construct it.


Custom analysis plugin

A plugin receives only the per-frame FeatureVector (never raw audio) and returns a result.

import { createVoiceAnalyzer, createDefaultPlugins, type AnalysisPlugin } from 'webapp-voice'

const loudnessGuard: AnalysisPlugin = {
  id: 'loudness', name: 'Loudness', description: 'Warns when too quiet', scored: false,
  process: (fv) => ({ label: fv.db > -30 ? 'OK' : 'Too quiet', confidence: 1 }),
}
createVoiceAnalyzer({ store, plugins: [...createDefaultPlugins(), loudnessGuard] })

AnalysisPlugin: { id, name, description, help?, scored?, process(fv), reset?(), calibrate?() }. Its results surface in analyzer.plugins (as PluginState) — render with <PluginResult>.


Theming (CSS custom properties)

The components read theme colors from CSS custom properties on the host — define them once on :root (or any ancestor) and every component themes together:

--bg, --panel, --panel-2, --text, --muted, --border, --accent, --accent-2, --ok, --danger.


The worklet loads automatically

The AudioWorklet processor is bundled into the package (inlined) and resolved at build time, so it works out of the box — no asset copying, no public/ wiring. Override only if needed: createVoiceAnalyzer({ store, workletUrl: '/my/worklet.js' }).


Headless (no components)

Skip the components entirely and drive your own UI from the reactive stream:

const v = createVoiceAnalyzer({ store })
v.engine.onFeature((fv) => { /* fv.f0, fv.formants, fv.spectrum, fv.rms … per frame */ })
v.start()

Full export list

createVoiceAnalyzer, createDefaultPlugins · AudioEngine, EngineError, PluginManager · the 4 plugin factories (createVibratoPlugin, createHelderheidPlugin, createGedruktheidPlugin, createNasaliteitPlugin) · RecordingStoreError, InMemoryRecordingStore, IndexedDbRecordingStore, S3RecordingStore · the 13 components · types: VoiceAnalyzer, VoiceAnalyzerOptions, RecordingItem, PendingRecording, FeatureVector, AnalysisPlugin, PluginResultData, PluginState, RecordingStore, RecordingInput, RecordingMeta, RecordingStoreErrorCode, OverlayConfig, EngineErrorKind, EngineStatus, AudioEngineOptions, S3RecordingStoreOptions.

Note: the plugin-result type is exported as PluginResultData (the name PluginResult is the component).