blip
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.

open the studio