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.
- 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)
- 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
- Node.js 18+
- Bun package manager
- Modern browser with MediaRecorder API support
Install the module to your Nuxt application:
bun add @dcc-bs/audio-recorder.bs.jsnpm i @dcc-bs/audio-recorder.bs.jsyarn add @dcc-bs/audio-recorder.bs.jsAdd 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';<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><script setup>
import { useAudioRecording, useAudioSessions } from '@dcc-bs/audio-recorder.bs.js'
const {
startRecording,
stopRecording,
recordingTime,
isRecording,
audioUrl,
error
} = useAudioRecording({
storeToDbInterval: 30, // Store chunks every 30 seconds
logger: console.log,
onRecordingStarted: (stream) => {
console.log('Recording started')
},
onRecordingStopped: (audioBlob, audioUrl) => {
console.log('Recording completed')
},
onError: (error) => {
console.error('Recording error:', error)
}
})
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><template>
<AudioSessionExplorer />
</template>The main recording component with start/stop controls and audio visualization.
| 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 |
| 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 |
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 |
<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>Real-time audio waveform visualization component that displays frequency data as animated bars.
| Property | Type | Required | Description |
|---|---|---|---|
stream |
MediaStream | undefined |
Yes | The audio stream to visualize |
isRecording |
boolean |
Yes | Whether recording is currently active (controls visualization updates) |
This component does not emit any events.
This component does not expose any methods or properties via defineExpose.
<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>- 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
Browse and manage audio sessions component with customizable actions.
| 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 |
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>
}| Slot | Props | Description |
|---|---|---|
actions |
session, getMp3Blob, deleteSession |
Custom action buttons slot for maximum flexibility |
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>- 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
Start the development playground:
bun devBuild the module:
bun dev:buildPrepare the development environment:
bun dev:prepareRun unit tests:
bun testRun tests with coverage:
bun test:coverageRun end-to-end tests:
bun test:e2eRun tests in watch mode:
bun test:watchFormat code with Biome:
bun lintRun linting and fixes:
bun checkThe 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
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
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
To release a new version:
bun releaseThis will run linting, tests, build the module, generate changelog, and push with tags.
MIT © Data Competence Center Basel-Stadt
Datenwissenschaften und KI
Developed with ❤️ by DCC - Data Competence Center



