@waveform-playlist/annotations
The annotations package provides components and utilities for adding time-based annotations to audio waveforms. Annotations can be created, edited, dragged, and exported in multiple formats.
Installation
npm install @waveform-playlist/annotations @waveform-playlist/browser react styled-components
Peer Dependencies
@waveform-playlist/browser
Browser package (workspace:*)
Styled Components 6.0.0 or later
DnD Kit 6.0.0+ for drag-and-drop
Main Exports
Provider
Registers annotation components with the browser package. Wrap your app with this to enable annotation support.
Components
Single annotation box with text label
Visual annotation region with drag handles
Container for multiple annotation boxes
Complete annotation track with all annotations
Editable annotation label text
Control Components
Toggle continuous play through annotations
Toggle linking annotation endpoints (moving one adjusts neighbors)
Toggle annotation editing mode
DownloadAnnotationsButton
Export annotations as JSON or Aeneas format
Hooks
useAnnotationControls(options)
Provides annotation state and control functions (create, update, delete, navigate)
Parsers
Parse Aeneas format annotation file to AnnotationData array
serializeAeneas(annotations)
Serialize AnnotationData array to Aeneas format string
Re-exported Types
From @waveform-playlist/core:
Annotation with id, start, end, label, and optional color
Export format: 'json' | 'aeneas'
Configuration: annotations array, editable, linkEndpoints, continuousPlay, etc.
Event types for annotation interactions
Action types: create, update, delete, select
Usage Example
Basic Integration
import {
WaveformPlaylistProvider,
PlaylistVisualization,
PlaylistAnnotationList,
} from '@waveform-playlist/browser';
import {
AnnotationProvider,
ContinuousPlayCheckbox,
EditableCheckbox,
DownloadAnnotationsButton,
} from '@waveform-playlist/annotations';
import type { AnnotationData } from '@waveform-playlist/core';
import { useState } from 'react';
function App() {
const [annotations, setAnnotations] = useState<AnnotationData[]>([
{ id: '1', start: 1.0, end: 3.5, label: 'Intro' },
{ id: '2', start: 3.5, end: 8.2, label: 'Verse 1' },
{ id: '3', start: 8.2, end: 12.0, label: 'Chorus' },
]);
const [tracks, setTracks] = useState([...]);
return (
<AnnotationProvider>
<WaveformPlaylistProvider
tracks={tracks}
onTracksChange={setTracks}
annotationList={{
annotations,
editable: true,
linkEndpoints: false,
continuousPlay: false,
}}
onAnnotationsChange={setAnnotations} // Required for edits to persist!
>
<div>
<EditableCheckbox />
<ContinuousPlayCheckbox />
<DownloadAnnotationsButton />
</div>
<PlaylistVisualization />
<PlaylistAnnotationList />
</WaveformPlaylistProvider>
</AnnotationProvider>
);
}
Custom Annotation Controls
import { useAnnotationControls } from '@waveform-playlist/annotations';
import { usePlaylistControls } from '@waveform-playlist/browser';
function CustomAnnotationControls() {
const {
annotations,
selectedAnnotation,
createAnnotation,
updateAnnotation,
deleteAnnotation,
selectNext,
selectPrevious,
} = useAnnotationControls({
initialAnnotations: [],
editable: true,
});
const { currentTime, seek } = usePlaylistControls();
const handleAddAnnotation = () => {
createAnnotation({
start: currentTime,
end: currentTime + 5.0,
label: 'New Annotation',
});
};
const handleDeleteSelected = () => {
if (selectedAnnotation) {
deleteAnnotation(selectedAnnotation.id);
}
};
const handleNavigateNext = () => {
const next = selectNext();
if (next) {
seek(next.start);
}
};
return (
<div>
<button onClick={handleAddAnnotation}>Add Annotation</button>
<button onClick={handleDeleteSelected}>Delete Selected</button>
<button onClick={handleNavigateNext}>Next Annotation</button>
<div>Annotations: {annotations.length}</div>
</div>
);
}
Keyboard Navigation
import { useAnnotationKeyboardControls } from '@waveform-playlist/browser';
import { useAnnotationControls } from '@waveform-playlist/annotations';
function AnnotationEditor() {
const annotationControls = useAnnotationControls({
initialAnnotations: myAnnotations,
editable: true,
});
// Enable keyboard shortcuts:
// - Arrow keys: Navigate annotations
// - Delete/Backspace: Delete selected annotation
// - Enter: Play selected annotation
// - E: Toggle editing
useAnnotationKeyboardControls({
annotationControls,
enableAutoScroll: true,
});
return <PlaylistAnnotationList />;
}
Import/Export Annotations
import {
parseAeneas,
serializeAeneas,
type AeneasFragment,
} from '@waveform-playlist/annotations';
import type { AnnotationData } from '@waveform-playlist/core';
// Load Aeneas format file
const aeneasText = await fetch('/annotations.txt').then(r => r.text());
const annotations: AnnotationData[] = parseAeneas(aeneasText);
// Export to Aeneas format
const aeneasOutput = serializeAeneas(annotations);
console.log(aeneasOutput);
// Output:
// 0.000|1.500|First segment
// 1.500|3.200|Second segment
// 3.200|5.800|Third segment
// Export to JSON
const jsonOutput = JSON.stringify(annotations, null, 2);
const blob = new Blob([jsonOutput], { type: 'application/json' });
const url = URL.createObjectURL(blob);
Custom Annotation Rendering
import { AnnotationBox } from '@waveform-playlist/annotations';
import type { AnnotationData } from '@waveform-playlist/core';
function CustomAnnotationBox({ annotation }: { annotation: AnnotationData }) {
return (
<AnnotationBox
annotation={annotation}
style={{
backgroundColor: annotation.color || '#3b82f6',
opacity: 0.3,
borderLeft: '2px solid #1d4ed8',
borderRight: '2px solid #1d4ed8',
}}
/>
);
}
Integration Pattern
This package uses an integration context pattern:
@waveform-playlist/browser defines AnnotationIntegrationContext (interface + context)
- This package provides
AnnotationProvider that supplies components/functions
- Browser components use
useAnnotationIntegration() and gracefully return null if unavailable
This allows annotations to be optional - the browser package works without this package installed.
Important Notes
Always Pair annotationList with onAnnotationsChange
Critical: When using annotationList on WaveformPlaylistProvider, always provide onAnnotationsChange. Without the callback, annotation edits won’t persist and a console warning fires:
<WaveformPlaylistProvider
annotationList={{ annotations, editable: true }}
onAnnotationsChange={setAnnotations} // Required!
>
Context Hook Throws
useAnnotationIntegration() throws if used without <AnnotationProvider>. This follows the Kent C. Dodds context pattern - fail fast with a clear error:
// This will throw:
function MyComponent() {
const integration = useAnnotationIntegration(); // Error: Must wrap with AnnotationProvider
}
// Correct usage:
<AnnotationProvider>
<MyComponent /> {/* Now it works */}
</AnnotationProvider>
Annotation Data Structure
Annotations use floating-point seconds for start/end times:
interface AnnotationData {
id: string; // Unique identifier
start: number; // Start time in seconds (float)
end: number; // End time in seconds (float)
label: string; // Text label
color?: string; // Optional color (CSS color)
}
Aeneas format is a simple text-based format:
startTime|endTime|label
0.000|1.500|First segment
1.500|3.200|Second segment
3.200|5.800|Third segment
- Times in seconds with 3 decimal places
- Pipe-delimited
- One annotation per line
- UTF-8 encoding
- Browser - Provides
useAnnotationKeyboardControls and annotation integration context
- Core - Defines
AnnotationData type
- UI Components - Base visualization components