loads a resource
- A file in res/raw or assets for every cue
- An async load step before the first play can happen
- Fixed pitch and length baked into the recording
- APK grows with every sound in the set
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.
// blip blip — click
// kotlin · audiotrack
object ClickSound {
const val SAMPLE_RATE = 44_100
private const val DURATION = 0.11
val samples: ShortArray by lazy { synthesise() }
private fun synthesise(): ShortArray {
val frames = (SAMPLE_RATE * DURATION).toInt()
val out = ShortArray(frames)
var phase = 0.0
…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.
Tells the system this is a short interface cue, so it ducks and routes like one instead of like music.
USAGE_ASSISTANCE_SONIFICATIONThe shape of your data: 16-bit PCM, mono, at the sample rate you generated. It has to match the array exactly.
ENCODING_PCM_16BITThe whole sound is written once and lives in the track, rather than being streamed in chunks. Right choice for anything this short.
setTransferMode(MODE_STATIC)Copy the ShortArray in, then play. No res/raw lookup, no SoundPool load callback to wait on first.
track.write(samples, 0, samples.size)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()
}@Composable
fun SaveButton(onSave: () -> Unit) {
Button(onClick = {
playPcm(ClickSound.samples, ClickSound.SAMPLE_RATE)
onSave()
}) { Text("Save") }
}This is the shape of the technique done by hand. 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.
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.
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.
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.
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.
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.
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.
Open Blip Blip and give it a voice.