Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/hitenpatel/songprint/llms.txt

Use this file to discover all available pages before exploring further.

songprint was built for Android and is battle-tested in RadioShake, a live-radio app that identifies songs as they stream in real time. This guide walks through a complete integration: recording audio, converting it to the format SignatureGenerator expects, generating the fingerprint, and posting it to the discovery endpoint — all wired together with Kotlin coroutines.

Add the dependency

songprint is distributed via JitPack. Add the JitPack repository and the dependency to your module’s build.gradle.kts:
// settings.gradle.kts or project-level build.gradle.kts
dependencyResolutionManagement {
    repositories {
        maven("https://jitpack.io")
    }
}
// app/build.gradle.kts
dependencies {
    implementation("com.github.hitenpatel:songprint:0.1.0")
}

Permissions

If you are capturing live audio from the device microphone, declare the RECORD_AUDIO permission in your AndroidManifest.xml and request it at runtime using ActivityCompat.requestPermissions before starting recording.
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
Add <uses-permission android:name="android.permission.INTERNET" /> to your manifest as well so that RecognitionClient can reach the Shazam discovery endpoint.
<uses-permission android:name="android.permission.INTERNET" />

Capturing and buffering PCM

The example below records approximately 12 seconds of audio using AudioRecord. Actual minimum buffer sizes and supported sample rates are device-dependent; always query AudioRecord.getMinBufferSize and verify the recorder initialised successfully before reading from it.
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import io.github.hitenpatel.songprint.Pcm

// Device-specific minimum buffer size — always query rather than hard-code.
val sampleRate = 44100
val channelConfig = AudioFormat.CHANNEL_IN_STEREO
val audioFormat = AudioFormat.ENCODING_PCM_16BIT
val minBuffer = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)

val recorder = AudioRecord(
    MediaRecorder.AudioSource.MIC,
    sampleRate,
    channelConfig,
    audioFormat,
    minBuffer * 4, // a larger buffer reduces the chance of overruns
)

check(recorder.state == AudioRecord.STATE_INITIALIZED) {
    "AudioRecord failed to initialise — check permissions and device support"
}

recorder.startRecording()

// Accumulate ~12 seconds of raw PCM bytes.
val targetBytes = sampleRate * 2 /* channels */ * 2 /* bytes/sample */ * 12 /* seconds */
val buffer = ByteArray(minBuffer)
val accumulated = mutableListOf<Byte>()

while (accumulated.size < targetBytes) {
    val read = recorder.read(buffer, 0, buffer.size)
    if (read > 0) accumulated.addAll(buffer.take(read))
}

recorder.stop()
recorder.release()

// Convert to ShortArray, then resample to 16kHz mono.
val rawBytes = accumulated.toByteArray()
val samples  = Pcm.bytesToShorts(rawBytes)
val mono16k  = Pcm.resampleToMono16k(samples, sampleRate = sampleRate, channels = 2)
If you are decoding a media file with MediaExtractor / MediaCodec rather than recording live, the same Pcm calls apply — just substitute the actual sample rate and channel count reported by the track’s MediaFormat.

Identifying on a background thread

Network and CPU work both belong off the main thread. Use Dispatchers.IO for the HTTP call and, if the audio buffer is large, for the signature generation as well.
import io.github.hitenpatel.songprint.RecognitionClient
import io.github.hitenpatel.songprint.SignatureGenerator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

// mono16k is the ShortArray produced in the previous section.
val json: String? = withContext(Dispatchers.IO) {
    val signature = SignatureGenerator().generateSignature(mono16k)
    RecognitionClient().recognise(signature, mono16k.size)
}
generateSignature runs in pure Kotlin and completes in the order of milliseconds for 12 seconds of audio. recognise is a blocking HTTP call and must not run on the main thread.

Parsing the result

The track object in the JSON response contains the song metadata. org.json.JSONObject is available on Android without adding any dependency.
import org.json.JSONObject

if (json != null) {
    val root = JSONObject(json)
    val matches = root.optJSONArray("matches")

    if (matches != null && matches.length() > 0) {
        val track = root.optJSONObject("track")

        val title    = track?.optString("title")    // e.g. "Bohemian Rhapsody"
        val artist   = track?.optString("subtitle") // e.g. "Queen"
        val coverArt = track?.optJSONObject("images")?.optString("coverart")
        val link     = track?.optJSONObject("share")?.optString("href")

        // Update your UI on the main thread.
        withContext(Dispatchers.Main) {
            titleView.text  = title
            artistView.text = artist
            // load coverArt URL with your image library of choice
        }
    } else {
        // matches is empty — song was not recognised
    }
} else {
    // null — non-200 HTTP response or network error
}
SignatureGenerator is not thread-safe. Create a new instance for each coroutine or thread. Instances are inexpensive — they allocate a handful of fixed-size DoubleArray buffers on construction.

Build docs developers (and LLMs) love