blip
android ui sounds,
no res/raw folder.
SoundPool is the usual answer for a UI click in Android, but it still expects a sound file in res/raw. AudioTrack in MODE_STATIC plays a PCM buffer you generate in code instead, which means a UI sound needs no file in the project at all.
why AudioTrack instead of SoundPool?
SoundPool is built around loading short samples from res/raw or assets — convenient for a fixed sound library, but it means a file for every cue and a load step before the first play. AudioTrack's Builder with MODE_STATIC writes PCM samples straight from a ShortArray you generate, so a synthesized sound needs no file and no load latency.
the playback pattern
This is the shape of the technique done by hand: build a static AudioTrack sized to a PCM buffer, write samples into it, and play. It is not what Blip Blip's own export produces byte for byte — the real export is longer, parameter-driven, and bakes in per-play randomized variation, harmonics, and any filter, drive, delay, or reverb stage as literal constants. Once a sound is shaped in the studio, the Kotlin export writes a fuller version of this pattern automatically.
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioTrack
fun playPcm(samples: ShortArray, sampleRate: Int) {
val track = AudioTrack.Builder()
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
)
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(sampleRate)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build()
)
.setBufferSizeInBytes(samples.size * 2)
.setTransferMode(AudioTrack.MODE_STATIC)
.build()
track.write(samples, 0, samples.size)
track.play()
}does this work with Jetpack Compose?
Yes — AudioTrack is a plain Android media API with no View or Activity dependency, so calling the play function from a Modifier.clickable lambda, a Button's onClick, or a ViewModel works the same as it would in a legacy View-based app.
do I need to reuse the AudioTrack instance?
For infrequent cues like a single button tap, building a fresh AudioTrack per play, as shown above, is simple and fine. For sounds that repeat rapidly — a rhythm, a scroll tick — keep one AudioTrack alive and reset or re-write its buffer instead of rebuilding it each time, the same way you would keep an AVAudioEngine alive on iOS.
where do the samples come from?
Shape a sound in Blip Blip's studio, then export it as Kotlin. The export writes the full AudioTrack setup above plus a function that generates the exact 16-bit PCM samples for that sound, including per-play pitch and timing variation, ready to paste into an Android Studio project.
the useful details.
Do I need a sound file in res/raw?
No. The exported Kotlin code generates 16-bit PCM samples at runtime and writes them into a static AudioTrack — there is no file to add to res/raw or assets.
Does this replace SoundPool entirely?
Not necessarily — SoundPool is still a reasonable choice for a fixed library of pre-recorded assets. AudioTrack in MODE_STATIC is the better fit specifically when the sound is generated rather than recorded.
Can I try this without writing any Kotlin myself?
Yes — design the sound in the studio and use the Kotlin export to get a complete, ready-to-paste implementation, including the parts this page only sketches.
ready to make
some noise?
Open Blip Blip and give it a voice.