Skip to main content

SonixMetronome

Programmable metronome with distinct downbeat and beat sounds, configurable BPM, and visual beat tracking.

Quick Start

Kotlin

val metronome = SonixMetronome.create(
samaSamplePath = "/path/to/sama.wav",
beatSamplePath = "/path/to/beat.wav"
)

// Wait for samples to load, then start
metronome.isInitialized.collect { ready ->
if (ready) metronome.start()
}

// Later...
metronome.stop()
metronome.release()

Swift

let metronome = SonixMetronome.create(
samaSamplePath: "/path/to/sama.wav",
beatSamplePath: "/path/to/beat.wav"
)

// Wait for samples to load, then start
let initTask = metronome.observeIsInitialized { ready in
if ready { metronome.start() }
}

// Later...
metronome.stop()
metronome.release()

Configuration

Factory Method

ParameterTypeDefaultDescription
samaSamplePathStringPath to downbeat audio sample (required)
beatSamplePathStringPath to regular beat audio sample (required)
bpmFloat120Initial tempo (30–300)
beatsPerCycleInt4Beats per cycle

Builder

For advanced configuration with callbacks:

Kotlin

val metronome = SonixMetronome.Builder()
.samaSamplePath("/path/to/sama.wav")
.beatSamplePath("/path/to/beat.wav")
.bpm(120f)
.beatsPerCycle(4)
.volume(0.8f)
.onBeat { beatIndex -> updateBeatIndicator(beatIndex) }
.onError { error -> showError(error) }
.build()

Swift

let metronome = SonixMetronome.Builder()
.samaSamplePath(path: "/path/to/sama.wav")
.beatSamplePath(path: "/path/to/beat.wav")
.bpm(bpm: 120)
.beatsPerCycle(count: 4)
.volume(volume: 0.8)
.build()

Builder Parameters

MethodTypeDefaultDescription
samaSamplePathStringDownbeat audio path (required)
beatSamplePathStringBeat audio path (required)
bpmFloat120Tempo (clamped 30–300)
beatsPerCycleInt4Beats per cycle
volumeFloat0.8Initial volume (clamped 0–1)
onBeat(beatIndex: Int) -> UnitCalled on each beat
onError(error: String) -> UnitCalled on error

Playback Controls

metronome.start()         // Start (only works when isInitialized is true)
metronome.stop() // Stop
metronome.release() // Release all resources

Runtime Adjustments

metronome.setBpm(140f)     // Change tempo (30–300)
metronome.volume = 0.5f // Change volume (0.0–1.0)
metronome.setBpm(bpm: 140)
metronome.volume = 0.5
Property/MethodRangeNotes
setBpm(bpm)30–300Takes effect on next beat
volume0.0–1.0Immediate effect
beatsPerCycleRead-only; set at creation time

Observing State

Kotlin (StateFlow)

metronome.isInitialized.collect { ready ->
startButton.isEnabled = ready
}

metronome.isPlaying.collect { playing ->
playButton.icon = if (playing) stopIcon else playIcon
}

metronome.currentBeat.collect { beat ->
highlightBeat(beat) // 0-based index
}

metronome.bpm.collect { currentBpm ->
bpmLabel.text = "${currentBpm.toInt()} BPM"
}

metronome.error.collect { error ->
error?.let { showError(it.message) }
}

Swift (Observers)

let beatTask = metronome.observeCurrentBeat { beat in
self.currentBeat = beat
}

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

let initTask = metronome.observeIsInitialized { ready in
self.isReady = ready
}

let bpmTask = metronome.observeBpm { bpm in
self.currentBpm = bpm
}

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

Swift (Combine)

metronome.currentBeatPublisher
.receive(on: DispatchQueue.main)
.sink { self.currentBeat = $0 }
.store(in: &cancellables)

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

StateFlows

StateFlowTypeDescription
currentBeatStateFlow<Int>Current beat index (0-based, wraps at beatsPerCycle)
isPlayingStateFlow<Boolean>Whether metronome is running
bpmStateFlow<Float>Current tempo
isInitializedStateFlow<Boolean>Whether samples are loaded and ready
errorStateFlow<SonixError?>Error state

Properties

PropertyTypeDescription
beatsPerCycleIntNumber of beats per cycle (read-only)
volumeFloatCurrent volume (0.0–1.0, mutable)

Common Patterns

Metronome ViewModel

class MetronomeViewModel : ViewModel() {
private var metronome: SonixMetronome? = null

val currentBeat = MutableStateFlow(0)
val isPlaying = MutableStateFlow(false)
val isReady = MutableStateFlow(false)

fun initialize(samaPath: String, beatPath: String) {
metronome = SonixMetronome.Builder()
.samaSamplePath(samaPath)
.beatSamplePath(beatPath)
.bpm(120f)
.beatsPerCycle(4)
.onBeat { beat -> currentBeat.value = beat }
.build()

viewModelScope.launch {
metronome!!.isInitialized.collect { isReady.value = it }
}
viewModelScope.launch {
metronome!!.isPlaying.collect { isPlaying.value = it }
}
}

fun togglePlayback() {
val m = metronome ?: return
if (m.isPlaying.value) m.stop() else m.start()
}

fun setBpm(bpm: Float) {
metronome?.setBpm(bpm)
}

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

Next Steps