Skip to main content

SonixPlayer

Play audio files with real-time pitch shifting, tempo control, volume management, and looping.

Quick Start

Kotlin

val player = SonixPlayer.create("path/to/song.mp3")
player.play()

// Later...
player.pause()
player.release()

Swift

let player = try await SonixPlayer.create(source: "path/to/song.mp3")
player.play()

// Later...
player.pause()
player.release()

Configuration

Presets

PresetKotlinSwiftDescription
DefaultSonixPlayerConfig.DEFAULT.defaultPlay once at normal volume
LoopingSonixPlayerConfig.LOOPING.loopingInfinite loop at normal volume

Builder

Kotlin

val config = SonixPlayerConfig.Builder()
.preset(SonixPlayerConfig.LOOPING)
.volume(0.8f)
.pitch(-2f)
.tempo(0.75f)
.onComplete { println("Done!") }
.build()

val player = SonixPlayer.create("song.mp3", config)

Swift

let config = SonixPlayerConfig.Builder()
.preset(.looping)
.volume(0.8)
.pitch(-2)
.tempo(0.75)
.onComplete { print("Done!") }
.build()

let player = try await SonixPlayer.create(source: "song.mp3", config: config)

Config Properties

PropertyTypeDefaultRangeDescription
volumeFloat1.00.0 – 1.0Initial playback volume
pitchFloat0.0-12 – +12Pitch shift in semitones
tempoFloat1.00.25 – 4.0Speed multiplier
loopCountInt11+ or -1Times to play (-1 = infinite)

Callbacks

Builder MethodKotlin SignatureSwift Signature
onComplete() -> Unit() -> Void
onLoopComplete(loopIndex: Int, totalLoops: Int) -> Unit(Int32, Int32) -> Void
onPlaybackStateChanged(isPlaying: Boolean) -> Unit(Bool) -> Void
onError(message: String) -> Unit(String) -> Void

Creating a Player

From File Path

val player = SonixPlayer.create("path/to/song.mp3")
let player = try await SonixPlayer.create(source: "path/to/song.mp3")

From Raw PCM Data

Kotlin

val pcmData: ByteArray = ...
val player = SonixPlayer.createFromPcm(
data = pcmData,
sampleRate = 44100,
channels = 1
)

Swift

let pcmData: Data = ...
let player = SonixPlayer.createFromPcm(
data: pcmData,
sampleRate: 44100,
channels: 1
)

Factory Methods

MethodParametersDescription
createsource, config, audioSessionCreate from file path (suspending)
createFromPcmdata, sampleRate, channels, config, audioSessionCreate from raw PCM bytes

Playback Controls

player.play()              // Start or resume
player.pause() // Pause playback
player.stop() // Stop and reset to beginning
player.seek(positionMs) // Seek to position in milliseconds
player.release() // Release all resources

Pitch and Tempo

Runtime Control

player.pitch = -2f    // 2 semitones down (lower key)
player.pitch = 0f // Original key
player.pitch = 3f // 3 semitones up (higher key)

player.tempo = 0.5f // Half speed
player.tempo = 1.0f // Normal speed
player.tempo = 2.0f // Double speed
PropertyRangeDescription
pitch-12 to +12Semitones shift without affecting speed
tempo0.25 to 4.0Speed multiplier without affecting pitch

Volume and Fading

player.volume = 0.5f   // Set to 50%

// Fade in from silence
player.volume = 0f
player.play()
player.fadeIn(targetVolume = 1f, durationMs = 500)

// Fade out before stopping
player.fadeOut(durationMs = 500)
player.pause()

// Smooth transition with easing
player.setVolumeSmooth(
targetVolume = 0.5f,
durationMs = 300,
easing = VolumeEasing.EaseInOut
)
MethodParametersDescription
fadeIntargetVolume: Float = 1f, durationMs: Long = 500Fade in (suspending)
fadeOutdurationMs: Long = 500Fade out to silence (suspending)
setVolumeSmoothtargetVolume, durationMs, easingVolume transition with easing (suspending)

Looping

// Fixed loop count
val config = SonixPlayerConfig.Builder()
.loopCount(3) // Play 3 times total
.onLoopComplete { loopIndex, total ->
println("Completed loop ${loopIndex + 1} of $total")
}
.build()

// Infinite loop via preset
val player = SonixPlayer.create("song.mp3", SonixPlayerConfig.LOOPING)

// Infinite loop via builder
val config = SonixPlayerConfig.Builder()
.loopForever()
.build()

// Change at runtime
player.loopCount = -1 // Infinite
player.loopCount = 5 // Play 5 times

Observing State

Kotlin (StateFlow)

// Current time (for seek bar)
player.currentTime.collect { timeMs ->
seekBar.progress = timeMs.toInt()
}

// Playing state (for play/pause button)
player.isPlaying.collect { playing ->
playButton.icon = if (playing) pauseIcon else playIcon
}

// Errors
player.error.collect { error ->
error?.let { showError(it.message) }
}

// Duration (not a flow - read directly)
val totalMs = player.duration

Swift (Observers)

let playingTask = player.observeIsPlaying { isPlaying in
self.isPlaying = isPlaying
}

let timeTask = player.observeCurrentTime { timeMs in
self.currentTimeMs = timeMs
}

let errorTask = player.observeError { error in
if let error = error {
self.showError(error.message)
}
}

// Cancel when done
playingTask.cancel()
timeTask.cancel()
errorTask.cancel()

Swift (Combine)

player.isPlayingPublisher
.receive(on: DispatchQueue.main)
.sink { isPlaying in
self.isPlaying = isPlaying
}
.store(in: &cancellables)

player.currentTimePublisher
.receive(on: DispatchQueue.main)
.sink { timeMs in
self.currentTimeMs = timeMs
}
.store(in: &cancellables)

StateFlows

StateFlowTypeDescription
currentTimeStateFlow<Long>Playback position in milliseconds
isPlayingStateFlow<Boolean>Whether currently playing
errorStateFlow<SonixError?>Error state

Properties

PropertyTypeDescription
durationLongTotal duration in milliseconds
asPlaybackInfoProviderPlaybackInfoProviderFor recording sync

Processing Tap

Access and modify audio buffers during playback for real-time effects:

Kotlin

player.setProcessingTap { buffer ->
// buffer is FloatArray — modify in-place
for (i in buffer.indices) {
buffer[i] *= 0.5f // Reduce volume by half
}
}

// Remove tap
player.setProcessingTap(null)

Swift

player.setProcessingTap { samples in
// samples is inout [Float] — modify in-place
for i in samples.indices {
samples[i] *= 0.5
}
}

// Remove tap
player.setProcessingTap(nil)

Listener Interface

Alternative to StateFlow observation and Builder callbacks:

player.setPlaybackListener(object : SonixPlayer.PlaybackListener {
override fun onPlaybackCompleted() {
println("Done!")
}
override fun onLoopCompleted(loopIndex: Int, totalLoops: Int) {
println("Loop $loopIndex of $totalLoops")
}
override fun onPlaybackStateChanged(isPlaying: Boolean) {
updatePlayButton(isPlaying)
}
override fun onError(message: String) {
showError(message)
}
})

Common Patterns

Music Player ViewModel

class MusicPlayerViewModel : ViewModel() {
private var player: SonixPlayer? = null

fun load(path: String) {
viewModelScope.launch {
player?.release()
player = SonixPlayer.create(path)
}
}

fun playPause() {
val p = player ?: return
if (p.isPlaying.value) p.pause() else p.play()
}

fun seek(positionMs: Long) {
player?.seek(positionMs)
}

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

Next Steps