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.

  • 0 raw resources
  • 16-bit pcm, mono
  • views + compose
export
// 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
    …
paste into an Android Studio project
The studio's export dialog. One export unlocks all three languages, so the same sound ships to Android, iOS, and the web without being redesigned.

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.

SoundPool

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
AudioTrack · MODE_STATIC

writes an array

  • Samples come from a ShortArray you generate
  • Nothing to load — the array is ready when the class is
  • Pitch, length, and brightness are constants in Kotlin
  • A few hundred bytes of code instead of an asset
the playback pattern

build a track, write
the samples, play.

Four builder calls and two method calls. The only part that changes between a click, a toggle, and an error is the contents of the array.
  1. 01

    AudioAttributes

    Tells the system this is a short interface cue, so it ducks and routes like one instead of like music.

    USAGE_ASSISTANCE_SONIFICATION
  2. 02

    AudioFormat

    The shape of your data: 16-bit PCM, mono, at the sample rate you generated. It has to match the array exactly.

    ENCODING_PCM_16BIT
  3. 03

    MODE_STATIC

    The 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)
  4. 04

    write() + play()

    Copy the ShortArray in, then play. No res/raw lookup, no SoundPool load callback to wait on first.

    track.write(samples, 0, samples.size)
ClickSound.ktandroid media
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()
}
SaveButton.ktjetpack compose
@Composable
fun SaveButton(onSave: () -> Unit) {
  Button(onClick = {
    playPcm(ClickSound.samples, ClickSound.SAMPLE_RATE)
    onSave()
  }) { Text("Save") }
}
synthesise()ShortArrayAudioTrack · MODE_STATIC0k30k-24k20k-14k12k-9k7k-5k4kplay()no file involved
A generator fills a ShortArray with 16-bit samples, AudioTrack copies it in once, and play() sounds it. There is no res/raw lookup anywhere in this path.

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.

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.

faq

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