Voice Recorder
Build a voice recorder with level monitoring, format options, and playback.
What You'll Build
A complete voice recording app:
- Record to M4A, MP3, or WAV
- Real-time level meter
- Playback recorded audio
- List and manage recordings
Implementation
Data Model
data class Recording(
val id: String = UUID.randomUUID().toString(),
val name: String,
val path: String,
val durationMs: Long,
val format: AudioFormat,
val createdAt: Long = System.currentTimeMillis()
)
ViewModel
class VoiceRecorderViewModel : ViewModel() {
private var recorder: SonixRecorder? = null
private var player: SonixPlayer? = null
private val _state = MutableStateFlow(RecorderState())
val state: StateFlow<RecorderState> = _state.asStateFlow()
data class RecorderState(
val isRecording: Boolean = false,
val isPlaying: Boolean = false,
val recordingDuration: Long = 0,
val playbackPosition: Long = 0,
val playbackDuration: Long = 0,
val level: Float = 0f,
val recordings: List<Recording> = emptyList(),
val selectedFormat: AudioFormat = AudioFormat.M4A
)
private val recordingsDir: String
get() = "${context.filesDir}/recordings"
init {
loadRecordings()
}
fun setFormat(format: AudioFormat) {
_state.update { it.copy(selectedFormat = format) }
}
fun startRecording(name: String) {
val extension = when (_state.value.selectedFormat) {
AudioFormat.M4A -> "m4a"
AudioFormat.MP3 -> "mp3"
AudioFormat.WAV -> "wav"
}
val path = "$recordingsDir/${name}_${System.currentTimeMillis()}.$extension"
recorder = SonixRecorder.create(
path,
SonixRecorderConfig.Builder()
.format(_state.value.selectedFormat)
.onLevelUpdate { level ->
_state.update { it.copy(level = level) }
}
.build()
)
recorder?.start()
_state.update { it.copy(isRecording = true) }
// Update duration
viewModelScope.launch {
recorder?.duration?.collect { duration ->
_state.update { it.copy(recordingDuration = duration) }
}
}
}
fun stopRecording() {
recorder?.stop()
recorder?.release()
val recording = Recording(
name = "Recording ${_state.value.recordings.size + 1}",
path = recorder?.outputPath ?: "",
durationMs = _state.value.recordingDuration,
format = _state.value.selectedFormat
)
recorder = null
_state.update {
it.copy(
isRecording = false,
recordingDuration = 0,
level = 0f,
recordings = it.recordings + recording
)
}
saveRecordings()
}
suspend fun playRecording(recording: Recording) {
player?.release()
player = SonixPlayer.create(recording.path)
_state.update {
it.copy(
isPlaying = true,
playbackDuration = player?.duration ?: 0
)
}
viewModelScope.launch {
player?.currentTime?.collect { position ->
_state.update { it.copy(playbackPosition = position) }
}
}
viewModelScope.launch {
player?.isPlaying?.collect { playing ->
if (!playing && _state.value.isPlaying) {
_state.update { it.copy(isPlaying = false, playbackPosition = 0) }
}
}
}
player?.play()
}
fun stopPlayback() {
player?.stop()
player?.release()
player = null
_state.update { it.copy(isPlaying = false, playbackPosition = 0) }
}
fun deleteRecording(recording: Recording) {
File(recording.path).delete()
_state.update { it.copy(recordings = it.recordings - recording) }
saveRecordings()
}
private fun loadRecordings() {
// Load from SharedPreferences or database
}
private fun saveRecordings() {
// Save to SharedPreferences or database
}
override fun onCleared() {
recorder?.release()
player?.release()
}
}
UI (Compose)
@Composable
fun VoiceRecorderScreen(viewModel: VoiceRecorderViewModel = viewModel()) {
val state by viewModel.state.collectAsState()
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// Format selector
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
AudioFormat.values().forEach { format ->
FilterChip(
selected = state.selectedFormat == format,
onClick = { viewModel.setFormat(format) },
label = { Text(format.name) }
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// Level meter
LevelMeter(
level = state.level,
modifier = Modifier.fillMaxWidth().height(40.dp)
)
// Duration display
Text(
text = formatDuration(state.recordingDuration),
fontSize = 48.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(24.dp))
// Record button
RecordButton(
isRecording = state.isRecording,
onClick = {
if (state.isRecording) {
viewModel.stopRecording()
} else {
viewModel.startRecording("Recording")
}
}
)
Spacer(modifier = Modifier.height(32.dp))
// Recordings list
Text("Recordings", style = MaterialTheme.typography.titleMedium)
LazyColumn {
items(state.recordings) { recording ->
RecordingItem(
recording = recording,
isPlaying = state.isPlaying,
onPlay = { viewModel.playRecording(it) },
onStop = { viewModel.stopPlayback() },
onDelete = { viewModel.deleteRecording(it) }
)
}
}
}
}
@Composable
fun LevelMeter(level: Float, modifier: Modifier = Modifier) {
Canvas(modifier = modifier) {
val width = size.width
val height = size.height
// Background
drawRoundRect(
color = Color.Gray.copy(alpha = 0.3f),
cornerRadius = CornerRadius(8f)
)
// Level bar
val levelWidth = width * level
val color = when {
level < 0.5f -> Color.Green
level < 0.8f -> Color.Yellow
else -> Color.Red
}
drawRoundRect(
color = color,
size = Size(levelWidth, height),
cornerRadius = CornerRadius(8f)
)
}
}
@Composable
fun RecordButton(isRecording: Boolean, onClick: () -> Unit) {
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(if (isRecording) Color.Red else Color.Red.copy(alpha = 0.7f))
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
if (isRecording) {
Box(
modifier = Modifier
.size(24.dp)
.background(Color.White, RoundedCornerShape(4.dp))
)
} else {
Box(
modifier = Modifier
.size(24.dp)
.background(Color.White, CircleShape)
)
}
}
}
Key Features
Format Selection
AudioFormat.M4A // AAC compressed, small files, good quality
AudioFormat.MP3 // Universal compatibility
AudioFormat.WAV // Uncompressed, large files, lossless
Quality Presets
SonixRecorderConfig.VOICE // 16kHz mono - smallest files
SonixRecorderConfig.STANDARD // 44.1kHz stereo - balanced
SonixRecorderConfig.HIGH // 48kHz stereo - best quality
Real-time Level Monitoring
recorder?.level?.collect { level ->
// level is 0.0 to 1.0
updateVUMeter(level)
}
Next Steps
- Recording Audio Guide - Detailed recording options
- Playing Audio Guide - Playback features
- Demo App - Full source