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.