Skip to main content

API Patterns

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

Initialization Contract

Initialize the SDK once, at app startup, before calling any facade:

VT.initialize(/* license / platform context */)

Every public facade across tona, tessera, accura, common, calibra, and sonix calls VT.ensureInitialized() before doing any work. If VT.initialize* has not completed successfully, that call throws VoxaTraceNotInitializedException — so initialize before constructing detectors, players, or analyzers.

  • Factory methods (create()) are the gate: they call ensureInitialized(), so instance methods on the returned object don't re-check.
  • One-shot utility methods (batch process, compute*, detect*, the MusicTheory math helpers) each guard individually, since there's no prior factory.
// Throws VoxaTraceNotInitializedException if VT.initialize* was not called
val detector = PitchDetection.createDetector()

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.
// SonixPlayer.create suspends (it decodes audio), so call it from a coroutine.
suspend fun setup() {
val player = SonixPlayer.create("song.mp3")
val detector = PitchDetection.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 = PitchDetection.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 // 16 kHz mono, 64 kbps
SonixRecorderConfig.STANDARD // 44.1 kHz mono, 128 kbps
SonixRecorderConfig.HIGH // 44.1 kHz stereo, 192 kbps

// 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:

PitchDetection.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.currentTime {
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