Skip to main content

SonixRecorder

Capture microphone input and save recordings as M4A, MP3, or WAV files with real-time audio level monitoring.

Quick Start

Kotlin

val recorder = SonixRecorder.create("/path/to/output.m4a")
recorder.start()
// ... user sings ...
recorder.stop()
recorder.release()

Swift

let recorder = SonixRecorder.create(outputPath: "/path/to/output.m4a")
recorder.start()
// ... user sings ...
recorder.stop()
recorder.release()

Configuration

Presets

PresetKotlinSwiftSample RateChannelsBitrate
VoiceSonixRecorderConfig.VOICE.voice16 kHzMono64 kbps
StandardSonixRecorderConfig.STANDARD.standard44.1 kHzStereo128 kbps
HighSonixRecorderConfig.HIGH.high48 kHzStereo256 kbps

Builder

Kotlin

val config = SonixRecorderConfig.Builder()
.preset(SonixRecorderConfig.STANDARD)
.format(AudioFormat.MP3)
.echoCancellation(true)
.onRecordingStopped { path -> println("Saved to: $path") }
.build()

val recorder = SonixRecorder.create("/path/to/output.mp3", config)

Swift

let config = SonixRecorderConfig.Builder()
.preset(.standard)
.format(.mp3)
.echoCancellation(true)
.onRecordingStopped { path in print("Saved to: \(path)") }
.build()

let recorder = SonixRecorder.create(outputPath: "/path/to/output.mp3", config: config)

Config Properties

PropertyTypeDefaultDescription
formatAudioFormatM4AOutput format (M4A, MP3, WAV)
sampleRateInt16000Sample rate in Hz
channelsInt11 = mono, 2 = stereo
bitrateInt64000Encoder bitrate (bps)
echoCancellationBooleanfalseEnable acoustic echo cancellation
audioBufferSizeMsInt40Audio capture buffer size (ms)

Callbacks

Builder MethodSignatureDescription
onRecordingStarted() -> UnitRecording started
onRecordingStopped(outputPath: String) -> UnitRecording saved
onError(message: String) -> UnitError occurred
onLevelUpdate(level: Float) -> UnitAudio level changed (0.0–1.0)
onStateChange(RecordingState) -> UnitState machine transition

Creating a Recorder

With Output Path

val recorder = SonixRecorder.create(
outputPath = "/path/to/output.m4a",
config = SonixRecorderConfig.VOICE,
audioSession = AudioMode.RECORDING
)
let recorder = SonixRecorder.create(
outputPath: "/path/to/output.m4a",
config: .voice,
audioSession: .recording
)

With Temporary Path

For cases where you don't need a specific output location (e.g., real-time analysis):

val recorder = SonixRecorder.createTemporary()
let recorder = SonixRecorder.createTemporary()

Recording Controls

recorder.start()    // Begin recording
recorder.stop() // Stop and save to file
recorder.release() // Release all resources

Recording State

The recorder follows a state machine:

Idle → Starting → Recording → Stopping → Encoding → Finished
↘ Error
sealed class RecordingState {
object Idle
object Starting
data class Recording(val durationMs: Long)
object Stopping
object Encoding
data class Finished(val outputPath: String?)
data class Error(val message: String)
}

Observing State

Kotlin (StateFlow)

// Recording status
recorder.isRecording.collect { recording ->
recordButton.isEnabled = !recording
stopButton.isEnabled = recording
}

// Duration
recorder.duration.collect { durationMs ->
timerLabel.text = formatTime(durationMs)
}

// Audio level (for VU meter)
recorder.level.collect { level ->
vuMeter.setLevel(level) // 0.0 to 1.0
}

// Full state machine
recorder.state.collect { state ->
when (state) {
is RecordingState.Recording -> showRecording(state.durationMs)
is RecordingState.Encoding -> showEncoding()
is RecordingState.Finished -> showDone(state.outputPath)
is RecordingState.Error -> showError(state.message)
else -> {}
}
}

Swift (Observers)

let recordingTask = recorder.observeIsRecording { isRecording in
self.isRecording = isRecording
}

let durationTask = recorder.observeDuration { durationMs in
self.durationMs = durationMs
}

let levelTask = recorder.observeLevel { level in
self.audioLevel = level
}

// Cancel when done
recordingTask.cancel()
durationTask.cancel()
levelTask.cancel()

Swift (Combine)

recorder.isRecordingPublisher
.receive(on: DispatchQueue.main)
.sink { self.isRecording = $0 }
.store(in: &cancellables)

recorder.levelPublisher
.receive(on: DispatchQueue.main)
.sink { self.audioLevel = $0 }
.store(in: &cancellables)

StateFlows

StateFlowTypeDescription
isRecordingStateFlow<Boolean>Whether currently recording
durationStateFlow<Long>Recording duration in milliseconds
levelStateFlow<Float>Audio level (0.0 = silence, 1.0 = loud)
errorStateFlow<SonixError?>Error state
stateStateFlow<RecordingState>Full state machine
audioBuffersSharedFlow<AudioBuffer>Raw audio buffers for real-time processing

Properties

PropertyTypeDescription
actualSampleRateIntHardware sample rate (may differ from configured)
bufferPoolAvailableIntBuffers available in pool
bufferPoolWasExhaustedBooleanWhether pool was ever exhausted
latestBufferAudioBuffer?Most recent audio buffer
synchronizedTimeMsLongPlayback-synchronized time

Real-time Audio Access

Access raw audio buffers for visualization or analysis:

Kotlin

recorder.audioBuffers.collect { buffer ->
val samples = buffer.toFloatArray()
pitchDetector.detect(samples, buffer.sampleRate)
}

Swift (Resampled Stream)

for await samples in recorder.audioBuffers(resampledTo: 16000) {
pitchDetector.detect(samples: samples, sampleRate: 16000)
}

Segment Recording

Record specific portions within a session (for practice apps):

// Enable in config
val config = SonixRecorderConfig.Builder()
.enableSegmentRecording(outputDirectory = "/path/to/segments/")
.build()

val recorder = SonixRecorder.create("/path/to/full.m4a", config)
recorder.start()

// Record a segment
recorder.startRecordingSegment(segmentIndex = 0)
// ... user performs section ...
val segmentPath = recorder.stopRecordingSegment(segmentIndex = 0)

// Query segments
val segments: List<SegmentInfo> = recorder.getRecordedSegments()
recorder.clearSegments() // Clear for retry

Playback Sync

Synchronize recording with a backing track for karaoke:

val player = SonixPlayer.create("backing-track.mp3")

val config = SonixRecorderConfig.Builder()
.playbackSyncProvider(player.asPlaybackInfoProvider)
.build()

val recorder = SonixRecorder.create("recording.m4a", config)

player.play()
recorder.start()

// Recording timestamps align with playback position
val syncedTime = recorder.synchronizedTimeMs

Listener Interface

recorder.setRecordingListener(object : SonixRecorder.RecordingListener {
override fun onRecordingStarted() { println("Started") }
override fun onRecordingStopped(outputPath: String) { println("Saved: $outputPath") }
override fun onStateChanged(state: RecordingState) { updateUI(state) }
override fun onLevelUpdate(level: Float) { vuMeter.setLevel(level) }
override fun onDurationUpdate(durationMs: Long) { updateTimer(durationMs) }
override fun onError(message: String) { showError(message) }
override fun onSegmentSaved(segmentIndex: Int, filePath: String) {
println("Segment $segmentIndex saved to $filePath")
}
})

Common Patterns

Recording ViewModel

class RecordingViewModel : ViewModel() {
private var recorder: SonixRecorder? = null

val isRecording = MutableStateFlow(false)
val duration = MutableStateFlow(0L)
val audioLevel = MutableStateFlow(0f)

fun startRecording(outputPath: String) {
val config = SonixRecorderConfig.Builder()
.preset(SonixRecorderConfig.VOICE)
.format(AudioFormat.MP3)
.build()

recorder = SonixRecorder.create(outputPath, config)

viewModelScope.launch { recorder!!.isRecording.collect { isRecording.value = it } }
viewModelScope.launch { recorder!!.duration.collect { duration.value = it } }
viewModelScope.launch { recorder!!.level.collect { audioLevel.value = it } }

recorder!!.start()
}

fun stopRecording() {
recorder?.stop()
}

override fun onCleared() {
recorder?.release()
}
}

Platform Notes

  • iOS: Requires microphone permission in Info.plist. Hardware sample rate may differ from requested — check actualSampleRate.
  • Android: Requires RECORD_AUDIO permission. Audio focus handled automatically.

Next Steps