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 vueRequires 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 storeThat'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 namePluginResultis the component).
