- TypeScript 55.2%
- Vue 43.1%
- JavaScript 1.5%
- CSS 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .github/workflows | ||
| .vscode | ||
| _img | ||
| playground | ||
| scripts | ||
| src | ||
| tests/e2e | ||
| .editorconfig | ||
| .gitignore | ||
| biome.json | ||
| build.config.ts | ||
| bun.lock | ||
| bunfig.toml | ||
| codecov.yml | ||
| LICENSE | ||
| package.json | ||
| playwright.config.ts | ||
| README.md | ||
| renovate.json | ||
| tsconfig.json | ||
| vitest.config.ts | ||
Audio Recorder (Nuxt Module)
Audio Recorder is a powerful Nuxt.js module that provides advanced audio recording, processing, and management capabilities for web applications. This module includes Vue components, composables, and utilities for seamless audio recording integration with client-side storage and audio format conversion.
Features
- Audio Recording: Web-based audio recording with AudioWorklet and PCM capture
- Audio Visualization: Real-time audio visualization during recording
- Session Management: Persistent audio session storage with IndexedDB
- Audio Conversion: FFmpeg integration for PCM to MP3 conversion and concatenation
- Chunked Recording: Configurable chunk intervals for efficient storage management
- Storage Services: Client-side audio chunk storage with ArrayBuffer for optimal performance
- Session Explorer: Browse and manage recorded audio sessions
- Abandoned Recording Recovery: Automatic detection and recovery of interrupted recordings
- iOS Support: Improved background recording support for iOS devices
- Internationalization: Built-in i18n support (English and German)
Technology Stack
- Framework: Nuxt.js module with Vue 3
- Package Manager: Bun
- Audio Processing: FFmpeg WebAssembly
- Database: Dexie for IndexedDB management
- Animation: Motion-V for smooth UI transitions
- Testing: Playwright for e2e testing
Setup
Prerequisites
- Node.js 18+
- Bun package manager
- Modern browser with MediaRecorder API support
Installation
Install the module to your Nuxt application:
Using Bun
bun add @dcc-bs/audio-recorder.bs.js
Using NPM
npm i @dcc-bs/audio-recorder.bs.js
Using Yarn
yarn add @dcc-bs/audio-recorder.bs.js
Configuration
Add it to your nuxt.config.ts:
export default defineNuxtConfig({
modules: [
'@dcc-bs/audio-recorder.bs.js',
],
})
under the vite config in nuxt.config.ts configure dexie:
vite: {
resolve: {
alias: {
dexie: "dexie/dist/dexie.mjs",
},
},
}
add the following line to your main CSS file:
@import '@dcc-bs/audio-recorder.bs.js';
Client-Only Rendering
All components in this module use the .client.vue suffix, which means Nuxt automatically renders them only on the client side. This is required because audio recording relies on browser APIs (MediaRecorder, AudioContext, IndexedDB) that are not available during server-side rendering.
Alternatively, you can also wrap any component with the <ClientOnly> component:
<template>
<ClientOnly>
<AudioRecorder />
</ClientOnly>
</template>
See the Nuxt client-only pages documentation for more details on how Nuxt handles client-only rendering.
Usage
Basic Audio Recording
<template>
<div>
<AudioRecorder
:show-result="true"
:auto-start="false"
:store-to-db-interval="30"
:logger="customLogger"
@recording-started="onRecordingStarted"
@recording-stopped="onRecordingStopped"
/>
</div>
</template>
<script setup>
function customLogger(message) {
console.log('[AudioRecorder]:', message)
}
function onRecordingStarted(stream) {
console.log('Recording started:', stream)
}
function onRecordingStopped(audioBlob, audioUrl) {
console.log('Recording stopped:', { audioBlob, audioUrl })
}
</script>
Using Audio Recording Composable
RecordingOptions
| Property | Type | Default | Description |
|---|---|---|---|
onRecordingStarted |
(stream: MediaStream) => void |
() => {} |
Callback when recording starts, providing the MediaStream |
onRecordingStopped |
(audioBlob: Blob, audioUrl: string) => void |
() => {} |
Callback when recording stops with the audio data |
onError |
(error: string) => void |
() => {} |
Callback when an error occurs during recording |
storeToDbInterval |
number |
30 |
Interval in seconds to store audio chunks to IndexedDB |
onStore |
(mp3Blob: Blob) => void | Promise<void> |
() => {} |
Callback when an MP3 chunk is created during recording (fires every storeToDbInterval seconds) |
logger |
(msg: string) => void |
() => {} |
Custom logging function for debugging |
disableDefaultStore |
boolean |
false |
Disable the default IndexedDB storage. Useful when you want to handle storage yourself via the onStore callback |
Example
<script setup>
import { useAudioRecording, useAudioSessions } from '@dcc-bs/audio-recorder.bs.js'
const {
startRecording,
stopRecording,
recordingTime,
isRecording,
isProcessing,
audioUrl,
audioBlob,
currentSession,
error,
wasSuspended,
resetRecording
} = useAudioRecording({
storeToDbInterval: 30,
disableDefaultStore: false,
logger: console.log,
onRecordingStarted: (stream) => {
console.log('Recording started')
},
onRecordingStopped: (audioBlob, audioUrl) => {
console.log('Recording completed')
},
onError: (error) => {
console.error('Recording error:', error)
},
onStore: (mp3Blob) => {
console.log('MP3 chunk stored:', mp3Blob)
}
})
const { getMp3Blob, abandonedRecording, deleteAbandonedRecording } = useAudioSessions({
deleteOldSessionsDaysInterval: 7,
logger: console.log
})
// Start recording
await startRecording()
// Stop recording
await stopRecording()
// Download recorded audio as MP3 from a session
async function downloadAudio(sessionId) {
const blob = await getMp3Blob(sessionId)
if (blob) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `recording-${Date.now()}.mp3`
a.click()
URL.revokeObjectURL(url)
}
}
</script>
Audio Session Explorer
<template>
<AudioSessionExplorer />
</template>
Components
AudioRecorder
The main recording component with start/stop controls and audio visualization.
Props
| Property | Type | Default | Description |
|---|---|---|---|
showResult |
boolean |
true |
Whether to show the playback section after recording is complete |
autoStart |
boolean |
false |
Automatically start recording when the component is mounted |
storeToDbInterval |
number |
5 |
Interval in seconds to store audio chunks to IndexedDB (e.g., 30 for 30 seconds) |
logger |
(msg: string) => void |
undefined |
Optional custom logging function for debugging |
Events
| Event | Parameters | Description |
|---|---|---|
recording-started |
stream: MediaStream |
Emitted when recording starts successfully |
recording-stopped |
audioBlob: Blob, audioUrl: string |
Emitted when recording stops with the audio data |
Exposed Methods & Properties
The component exposes the following methods and reactive properties via template refs:
| Property/Method | Type | Description |
|---|---|---|
isRecording |
Ref<boolean> |
Reactive boolean indicating if recording is in progress |
startRecording |
() => Promise<void> |
Method to start audio recording |
stopRecording |
() => Promise<void> |
Method to stop audio recording |
recordingTime |
Ref<number> |
Recording time in seconds |
formattedRecordingTime |
ComputedRef<string> |
Formatted recording time as "MM:SS" |
error |
Ref<string | null> |
Current error message, if any |
audioUrl |
Ref<string | null> |
URL of the recorded audio blob |
Usage with Template Ref
<script setup lang="ts">
import { ref, computed } from 'vue';
import type { AudioRecorder } from '#components';
const recorderRef = ref<typeof AudioRecorder>()
const isRecording = computed(() => recorderRef.value?.isRecording)
const formattedTime = computed(() => recorderRef.value?.formattedRecordingTime)
function startRecording() {
recorderRef.value?.startRecording()
}
function stopRecording() {
recorderRef.value?.stopRecording()
}
</script>
<template>
<div>
<AudioRecorder ref="recorderRef" />
<button @click="startRecording">Start</button>
<button @click="stopRecording">Stop</button>
<p>Recording: {{ isRecording }}</p>
<p>Time: {{ formattedTime }}</p>
</div>
</template>
AudioVisualizer
Real-time audio waveform visualization component that displays frequency data as animated bars.
Props
| Property | Type | Required | Description |
|---|---|---|---|
stream |
MediaStream | undefined |
Yes | The audio stream to visualize |
isRecording |
boolean |
Yes | Whether recording is currently active (controls visualization updates) |
Events
This component does not emit any events.
Exposed Methods & Properties
This component does not expose any methods or properties via defineExpose.
Usage
<script setup lang="ts">
import { ref } from 'vue'
const audioStream = ref<MediaStream>()
const recordingState = ref(false)
// Get stream from getUserMedia or other source
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
audioStream.value = stream
recordingState.value = true
})
</script>
<template>
<div>
<AudioVisualizer
:stream="audioStream"
:is-recording="recordingState"
/>
</div>
</template>
Technical Details
- Uses Web Audio API with
AudioContextandAnalyserNode - FFT size: 256 for frequency analysis
- Updates visualization every 50ms when recording
- Displays 25 frequency bars (mirrored for visual symmetry)
- Automatically cleans up audio context when recording stops
- Responsive design with bars scaling based on frequency amplitude
AudioSessionExplorer
Browse and manage audio sessions component with customizable actions.
Props
| Property | Type | Default | Description |
|---|---|---|---|
showDownloadButton |
boolean |
true |
Whether to show the default download button |
showDeleteButton |
boolean |
true |
Whether to show the default delete button |
customActions |
CustomAction[] |
[] |
Array of custom action buttons to display before the download button |
CustomAction Interface
interface CustomAction {
label: string // Button text
icon?: string // Icon name (e.g., 'i-lucide-upload')
color?: string // Button color (default: 'primary')
variant?: string // Button variant (default: 'soft')
loading?: boolean // External loading state
disabled?: boolean // External disabled state
handler: (
sessionId: string,
mp3Blob: Blob,
deleteSession: () => Promise<void>
) => void | Promise<void>
}
Slots
| Slot | Props | Description |
|---|---|---|
actions |
session, getMp3Blob, deleteSession |
Custom action buttons slot for maximum flexibility |
Usage Examples
Basic usage:
<template>
<AudioSessionExplorer />
</template>
With custom actions:
<script setup lang="ts">
import type { CustomAction } from '@dcc-bs/audio-recorder.bs.js'
const customActions: CustomAction[] = [
{
label: 'Upload',
icon: 'i-lucide-upload',
color: 'blue',
handler: async (sessionId, mp3Blob, deleteSession) => {
// Upload the blob to your server
await uploadToServer(mp3Blob)
// Optionally delete the session after successful upload
await deleteSession()
}
},
{
label: 'Share',
icon: 'i-lucide-share',
color: 'green',
variant: 'outline',
handler: async (sessionId, mp3Blob) => {
// Share the audio blob
await shareAudio(mp3Blob)
// Session is kept (no deleteSession call)
}
}
]
</script>
<template>
<AudioSessionExplorer :custom-actions="customActions" />
</template>
Hide default buttons and use custom actions only:
<script setup>
const customActions = [
{
label: 'Process',
icon: 'i-lucide-cog',
handler: async (sessionId, mp3Blob, deleteSession) => {
const processed = await processAudio(mp3Blob)
if (processed) {
await deleteSession()
}
}
}
]
</script>
<template>
<AudioSessionExplorer
:custom-actions="customActions"
:show-download-button="false"
:show-delete-button="false"
/>
</template>
Using the actions slot for maximum control:
<template>
<AudioSessionExplorer>
<template #actions="{ session, getMp3Blob, deleteSession }">
<UButton
@click="async () => {
const blob = await getMp3Blob()
await customProcessing(blob, session.id)
}"
color="purple"
icon="i-lucide-sparkles"
>
Custom Action
</UButton>
</template>
</AudioSessionExplorer>
</template>
Combining custom actions with slot:
<script setup>
const customActions = [
{
label: 'Quick Upload',
icon: 'i-lucide-upload',
handler: async (sessionId, mp3Blob, deleteSession) => {
await quickUpload(mp3Blob)
await deleteSession()
}
}
]
</script>
<template>
<AudioSessionExplorer :custom-actions="customActions">
<template #actions="{ session, getMp3Blob }">
<!-- Additional custom button via slot -->
<UButton @click="specialAction(session)">
Special
</UButton>
</template>
</AudioSessionExplorer>
</template>
Features
- Session List: Displays all recorded audio sessions with metadata
- Download: Export sessions as MP3 files (can be disabled)
- Delete: Remove sessions from storage (can be disabled)
- Custom Actions: Add custom buttons with access to the MP3 blob and delete functionality
- Loading States: Automatic loading indicators for all actions
- Animations: Smooth transitions using Motion-V
- Empty State: User-friendly message when no sessions exist
Development
Start the development playground:
bun dev
Build the module:
bun dev:build
Prepare the development environment:
bun dev:prepare
Testing
Run unit tests:
bun test
Run tests with coverage:
bun test:coverage
Run end-to-end tests:
bun test:e2e
Run tests in watch mode:
bun test:watch
Code Quality
Format code with Biome:
bun lint
Run linting and fixes:
bun check
Architecture
Recording Pipeline
The module uses a modern AudioWorklet-based architecture for high-quality PCM audio capture:
- AudioWorklet PCM Capture: Raw PCM audio data is captured using a custom AudioWorkletProcessor (
pcm-recorder-worklet.js) - Chunked Storage: PCM data is accumulated in memory and periodically converted to MP3 chunks using FFmpeg.wasm
- IndexedDB Persistence: MP3 chunks are stored as ArrayBuffers in IndexedDB for efficient storage
- Final Concatenation: On recording completion, all MP3 chunks are concatenated into a single MP3 file
This architecture provides:
- Lower memory footprint compared to MediaRecorder blob accumulation
- Better iOS compatibility with AudioWorklet support
- Flexible chunk intervals for customizable storage patterns
- Direct MP3 output without intermediate WebM files
Database Schema
AudioSession: Tracks recording metadata
id,name,createdAt,chunkCount,totalSizechunkIds: Array maintaining chunk ordersampleRate,numChannels: PCM metadata for reconstruction
AudioChunk: Stores MP3 audio chunks
id,sessionId,createdAtbuffer: ArrayBuffer containing MP3 data
Project Structure
src/: Main module source codemodule.ts: Nuxt module entry pointruntime/: Runtime components and composablesassets/: AudioWorklet processor filescomponents/: Vue components for audio recordingcomposables/: Audio recording composables and FFmpeg integrationservices/: Audio storage and database servicesutils/: Utility functions for microphone handling and PCM processinglang/: Internationalization files
playground/: Development playground and examplestests/: Unit and integration testse2e/: End-to-end tests with Playwrightnuxt/: Nuxt-specific tests
Release
To release a new version:
bun release
This will run linting, tests, build the module, generate changelog, and push with tags.
License
MIT © Data Competence Center Basel-Stadt
Datenwissenschaften und KI
Developed with ❤️ by DCC - Data Competence Center
