Voice Narration
Overview
Voice Narration allows users to attach an audio recording to their posts, adding a personal, human layer to written content. This feature is designed with CommonPlace's calm principles in mind: audio never autoplays, always uses preload="none", and playback stops with a gentle fade-out when the player scrolls out of view.
The system consists of three main parts: the VoiceContext that manages global audio settings and device selection, the VoiceRecorder for capturing audio during post composition, and the VoicePlayer for playback within post cards. Users can select their preferred microphone and speaker devices, and can globally disable all audio playback.
Voice recordings are capped at 60 seconds by default and are stored in a dedicated Supabase storage bucket (post-voices). The recorder supports multiple audio formats (WebM with Opus, MP4, OGG) and automatically selects the best format supported by the browser.
Relevant Invariants
- Invariant #9: "Time Is Not Weaponized" -- No autoplay, no typing indicators for voice recording. Audio is purely opt-in by the listener.
- Invariant #2: "Calm Is the Default State" -- Audio is
preload="none"and fades out when scrolled away rather than cutting abruptly. - Invariant #18: "Defaults Are Moral Decisions" -- Audio is disabled-by-default for playback (if the user has set that preference), and voice notes are always optional.
User Experience
Recording Flow
- Within the post composer (quick or advanced), a "Add voice (optional)" button is shown.
- Clicking it requests microphone access (if not already granted).
- A recording interface appears with a red recording dot, elapsed time, and maximum duration indicator.
- The user clicks "Stop" to end recording (or recording auto-stops at the max duration).
- The recording is immediately attached to the post with a preview player showing the duration.
- The user can re-record or remove the voice note before posting.
Playback Flow
- Posts with voice notes show a small volume icon in the post header.
- A play button with duration display appears within the post card.
- Clicking play starts playback. A progress bar shows the current position.
- If the player scrolls out of view (less than 10% visible), playback fades out over ~500ms and pauses.
- Users can set their preferred output device in voice settings.
- Users who have globally disabled audio will not see voice players at all.
Technical Implementation
Key Files
| File | Purpose |
|---|---|
src/components/voice/VoiceRecorder.jsx | Recording component with MediaRecorder API, timer, max duration enforcement, re-record, and remove functionality (248 lines) |
src/components/voice/VoicePlayer.jsx | Playback component with play/pause toggle, progress bar, compact mode, fade-out on scroll, and device output selection (155 lines) |
src/components/voice/VoiceContext.jsx | React context providing global audio settings: audio disabled toggle, device selection, device enumeration (158 lines) |
VoiceRecorder
The recorder uses the browser's MediaRecorder API with the following configuration:
- Audio constraints:
echoCancellation: true,noiseSuppression: true,autoGainControl: true - MIME type selection: Tries formats in order:
audio/webm;codecs=opus,audio/webm,audio/mp4,audio/ogg;codecs=opus,audio/ogg, then browser default - Data collection:
start(1000)requests data every second for reliable recording - Timer:
setIntervalincrements duration every second; auto-stops atmaxDuration(default 60 seconds) - Output: Calls
onVoiceReady({ blob, url, duration, mimeType })when stopped
States: idle (show record button), recording (show timer + stop), attached (show preview + re-record/remove), unsupported (render nothing)
VoicePlayer
The player uses a standard <audio> element with preload="none":
- Compact mode: Small inline button with duration badge and progress overlay
- Full mode: Larger play button with "Voice" label, duration, and separate progress bar
- Scroll fade-out: Uses
IntersectionObserver(threshold 0.1) to detect when the player leaves the viewport. When playing and less than 10% visible, volume fades from 1.0 to 0 in steps of 0.1 every 50ms, then pauses. - Output device: Uses
setSinkId()API when available to route audio to the user's preferred output device. - Global disable: If
audioDisabledis true in VoiceContext, the player renders nothing.
VoiceContext
The context manages:
- Audio disabled preference: Stored in the
profiles.viewer_disable_audiocolumn. Toggled viatoggleAudio(). - Input/output device selection: Stored in
localStorage(voice_input_device,voice_output_device). Devices are enumerated on mount without requesting permission; labels become available after the first recording grants permission. - Device change listener: Listens for
devicechangeevents to update available devices when hardware changes.
Storage
Voice recordings are uploaded to the post-voices Supabase storage bucket with the path format {user_id}/{timestamp}-voice.{ext}. The public URL is stored in the post's voice_url column, with voice_duration_ms and voice_mime as metadata.
Database Columns (on posts table)
| Column | Type | Purpose |
|---|---|---|
voice_url | text | Public URL of the voice recording |
voice_duration_ms | integer | Duration in milliseconds |
voice_mime | text | MIME type of the recording |
Edge Cases
| Scenario | Behavior |
|---|---|
| Browser doesn't support MediaRecorder | Recorder state set to unsupported; component renders nothing |
| Microphone access denied | Error message: "Microphone access denied. Please allow microphone access." |
| No microphone found | Error message: "No microphone found." |
| Recording exceeds max duration | Auto-stopped at the limit; duration capped |
| Voice upload fails | Error is logged but post creation continues without the voice note |
| User has audio globally disabled | VoicePlayer returns null; voice indicator still shows in post header |
| Player scrolls out of view while playing | Audio fades out over ~500ms, then pauses; volume resets to 1.0 for next play |
setSinkId not supported by browser | Fallback to default output device; warning logged |
Related
- Posts -- Voice notes are attached to posts
- Advanced Composer -- Voice recorder available in the full editor
Last updated: 2026-02-07