Skip to main content

Playing Audio

A complete guide to audio playback with SonixPlayer.

What You'll Learn

  • Load and play audio files
  • Control pitch, tempo, and volume
  • Handle looping and seeking
  • React to playback events
  • Use processing taps for real-time effects

Prerequisites

  • VoxaTrace installed
  • Audio file accessible (in app bundle or filesystem)

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()

Loading Audio

From File Path

// Absolute path
val player = SonixPlayer.create("/storage/emulated/0/Music/song.mp3")

// Relative to app data directory
val player = SonixPlayer.create("${context.filesDir}/song.mp3")

From Assets (Android)

// Copy asset to cache first, then load
val assetPath = copyAssetToCache("songs/song.mp3")
val player = SonixPlayer.create(assetPath)

From Bundle (iOS)

if let url = Bundle.main.url(forResource: "song", withExtension: "mp3") {
let player = try await SonixPlayer.create(source: url.path)
}

From Raw PCM Data

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

Playback Controls

Basic Controls

player.play()    // Start or resume
player.pause() // Pause playback
player.stop() // Stop and reset to beginning

Seeking

// Seek to 30 seconds
player.seek(30_000) // milliseconds

// Get current position
player.currentTime.collect { timeMs ->
updateSeekBar(timeMs)
}

// Get total duration
val totalMs = player.duration

Pitch and Tempo

Pitch Shifting

Change the key without affecting speed:

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

Range: -12 to +12 semitones (one octave each direction).

Tempo Control

Change the speed without affecting pitch:

player.tempo = 0.5f   // Half speed (slow practice)
player.tempo = 1.0f // Normal speed
player.tempo = 1.25f // 25% faster
player.tempo = 2.0f // Double speed

Range: 0.25x to 4.0x.

Combine Both

// Transpose to user's key AND slow down for practice
player.pitch = userKeyOffset
player.tempo = 0.75f

Volume and Fading

Set Volume

player.volume = 0.5f  // 50% volume
player.volume = 1.0f // Full volume
player.volume = 0.0f // Muted

Fade In/Out

// Start silent, then fade in
player.volume = 0f
player.play()
player.fadeIn(targetVolume = 1f, durationMs = 500)

// Before stopping, fade out
player.fadeOut(durationMs = 500)
player.pause()

Smooth Volume Transitions

// Smoothly transition with easing
player.setVolumeSmooth(
targetVolume = 0.5f,
durationMs = 300,
easing = VolumeEasing.EaseInOut
)

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()

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

Infinite Loop

// Option 1: Preset
val player = SonixPlayer.create("song.mp3", SonixPlayerConfig.LOOPING)

// Option 2: Builder
val config = SonixPlayerConfig.Builder()
.loopForever()
.build()

// Option 3: Runtime
player.loopCount = -1 // -1 means infinite

Observing State

StateFlow (Reactive)

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

// 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) }
}

Callbacks (Event-based)

val config = SonixPlayerConfig.Builder()
.onComplete { println("Playback finished") }
.onLoopComplete { loop, total -> println("Loop $loop/$total") }
.onPlaybackStateChanged { playing -> updateUI(playing) }
.onError { message -> showError(message) }
.build()

Listener Interface

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)
}
})

Processing Tap

Access audio buffers during playback for real-time processing:

// Apply effects during playback
val effects = CalibraEffects.create(EffectsPreset.VOCAL_CHAIN)

player.setProcessingTap { buffer ->
effects.process(buffer) // Modifies buffer in-place
}

player.play()

// Remove tap when done
player.setProcessingTap(null)
effects.release()

Use cases:

  • Apply reverb/compression to backing tracks
  • Visualize audio waveform
  • Extract features for analysis

Integration with Recording

Sync recording with playback for karaoke:

val player = SonixPlayer.create("backing-track.mp3")
val recorder = SonixRecorder.create(
"recording.m4a",
SonixRecorderConfig.Builder()
.playbackSyncProvider(player.asPlaybackInfoProvider)
.build()
)

// Start synchronized
player.play()
recorder.start()

// Recording timestamps align with playback position

Platform Considerations

iOS

// Audio session is automatically configured
// Supports: M4A, MP3, WAV, AAC, and other system formats

Android

// ExoPlayer is used internally
// Supports: M4A, MP3, WAV, OGG, FLAC, and other ExoPlayer formats

Audio Formats

FormatiOSAndroidNotes
M4A/AACYesYesRecommended for compression
MP3YesYesUniversal compatibility
WAVYesYesUncompressed, large files
OGGNoYesAndroid only
FLACYesYesLossless compression

Common Patterns

Music Player

class MusicPlayerViewModel {
private var player: SonixPlayer? = null

suspend fun load(path: String) {
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)
}

fun cleanup() {
player?.release()
player = null
}
}

Karaoke Player

class KaraokePlayer(
private val backingTrack: String,
private val vocalGuide: String
) {
private var backing: SonixPlayer? = null
private var guide: SonixPlayer? = null

suspend fun prepare() {
backing = SonixPlayer.create(backingTrack)
guide = SonixPlayer.create(vocalGuide)
}

fun play(withGuide: Boolean) {
backing?.play()
if (withGuide) {
guide?.volume = 0.3f
guide?.play()
}
}

fun setKey(semitones: Float) {
backing?.pitch = semitones
guide?.pitch = semitones
}

fun setTempo(factor: Float) {
backing?.tempo = factor
guide?.tempo = factor
}

fun toggleGuide(enabled: Boolean) {
guide?.volume = if (enabled) 0.3f else 0f
}

fun release() {
backing?.release()
guide?.release()
}
}

Next Steps