Skip to main content

API Patterns

VoxaTrace APIs follow consistent patterns that make them predictable and easy to learn.

The Three-Tier Pattern

Every VoxaTrace API supports three usage tiers, letting you choose the right level of complexity for your needs.

Tier 1: Presets (80% of users)

Use predefined configurations. Zero learning curve.

// Just works with sensible defaults
val player = SonixPlayer.create("song.mp3")
val detector = CalibraPitch.createDetector()
val recorder = SonixRecorder.create("/path/to/output.m4a")

Tier 2: Builder (15% of users)

Discover options through IDE autocomplete. Type-safe configuration.

// Explore options with autocomplete
val config = PitchDetectorConfig.Builder()
.algorithm(PitchAlgorithm.SWIFT_F0)
.voiceType(VoiceType.WesternSoprano)
.enableProcessing()
.build()

val detector = CalibraPitch.createDetector(config, modelProvider = { ... })

Tier 3: .copy() (5% of users)

Direct access to all parameters. For power users who know exactly what they want.

// Precise control when you need it
val config = PitchDetectorConfig.PRECISE.copy(
confidenceThreshold = 0.6f,
amplitudeGateDb = -50f
)

Config + Factory Pattern

APIs separate configuration from creation:

// Config is immutable data
data class SonixPlayerConfig(
val volume: Float = 1f,
val pitch: Float = 0f,
val tempo: Float = 1f
)

// Factory creates the instance
val player = SonixPlayer.create(source, config)

Benefits:

  • Reusable configs: Create once, use many times
  • Testable: Pass mock configs in tests
  • Discoverable: Builder shows all options

Presets

Each config class provides static presets for common use cases:

// Player presets
SonixPlayerConfig.DEFAULT // Play once at normal volume
SonixPlayerConfig.LOOPING // Infinite loop

// Recorder presets
SonixRecorderConfig.VOICE // 16kHz mono for speech/singing
SonixRecorderConfig.STANDARD // 44.1kHz stereo
SonixRecorderConfig.HIGH // 48kHz stereo high fidelity

// Detector presets
PitchDetectorConfig.BALANCED // Good tradeoff
PitchDetectorConfig.PRECISE // Higher accuracy, more CPU
PitchDetectorConfig.RELAXED // Lower latency, less accuracy

Start with presets, then customize with Builder if needed.

Resource Lifecycle

All resources follow consistent lifecycle patterns:

Create → Use → Release

// Create
val player = SonixPlayer.create("song.mp3")

// Use
player.play()
player.pause()

// Release (ALWAYS do this!)
player.release()

AutoCloseable Support

Use Kotlin's use block for automatic cleanup:

CalibraPitch.createDetector().use { detector ->
val point = detector.detect(samples, sampleRate)
// detector.close() called automatically
}

Suspend Functions

Some creation functions are suspending (marked with suspend):

// Player loading is async (decodes audio)
suspend fun createPlayer() {
val player = SonixPlayer.create("song.mp3") // suspend
player.play()
}

// Session preparation is async (extracts features)
suspend fun startSession() {
val session = CalibraLiveEval.create(...)
session.prepareSession() // suspend
session.startPracticingSegment(0)
}

Ownership

Resources are either owned or borrowed:

Owned Resources

The receiving object is responsible for cleanup:

// CalibraLiveEval OWNS the detector
val session = CalibraLiveEval.create(
reference = material,
detector = detector // Session takes ownership
)
session.closeSession() // Detector is released here

Borrowed Resources

The caller remains responsible for cleanup:

// CalibraLiveEval BORROWS player and recorder
val session = CalibraLiveEval.create(
reference = material,
detector = detector,
player = player, // Caller owns
recorder = recorder // Caller owns
)

session.closeSession() // Only releases detector
player.release() // Caller must release
recorder.release() // Caller must release

The documentation and KDoc always specify ownership.

StateFlow Observation

Real-time state is exposed via Kotlin StateFlows:

// Observe playback position
player.currentTime.collect { timeMs ->
updateSeekBar(timeMs)
}

// Observe recording level
recorder.level.collect { level ->
updateVuMeter(level)
}

// Observe session state
session.state.collect { state ->
updateUI(state)
}

In Swift, use the provided async sequences:

for await timeMs in player.currentTimeStream() {
updateSeekBar(timeMs)
}

Callbacks vs StateFlows

Both are available. Choose based on preference:

StateFlows (Reactive)

// Observe all updates
session.phase.collect { phase ->
updatePhaseUI(phase)
}

Callbacks (Event-based)

// Register specific handlers
session.onPhaseChanged { phase ->
updatePhaseUI(phase)
}

session.onSegmentComplete { result ->
showScore(result)
}

Sample Rate Handling

VoxaTrace handles sample rate conversions internally:

// Pass any sample rate - internal resampling to 16kHz
val point = detector.detect(samples, sampleRate = 48000)

// Works the same
val point = detector.detect(samples, sampleRate = 44100)
val point = detector.detect(samples, sampleRate = 16000)

This means you don't need to resample audio yourself. Just pass the hardware sample rate.

Error Handling

Errors are communicated via:

Return Values

val result = session.finishPracticingSegment()
if (result == null) {
// Session wasn't in PRACTICING state
}

StateFlow

player.error.collect { error ->
if (error != null) {
showError(error.message)
}
}

Callbacks

SonixPlayerConfig.Builder()
.onError { message -> showError(message) }
.build()

Swift Interop

Kotlin Multiplatform generates Swift-friendly APIs. Some patterns to note:

Companion Objects

// Kotlin
SonixPlayer.create(...)
// Swift (extensions hide .companion)
SonixPlayer.create(source: "song.mp3")

Named Parameters

// Kotlin
detector.detect(samples, sampleRate = 48000)
// Swift
detector.detect(samples: samples, sampleRate: 48000)

Suspending Functions

// Kotlin
suspend fun create(): SonixPlayer
// Swift (async/await)
func create() async throws -> SonixPlayer

Next Steps