Moonshine Voice provides native Android support through Maven, making it straightforward to add voice transcription to your Android apps.
Installation
Add Version to Dependencies
Add the Moonshine Voice version to gradle/libs.versions.toml: [ versions ]
moonshineVoice = "0.0.49"
[ libraries ]
moonshine-voice = { group = "ai.moonshine" , name = "moonshine-voice" , version.ref = "moonshineVoice" }
Add to App Dependencies
In your app/build.gradle.kts, add the library: dependencies {
implementation (libs.moonshine.voice)
// ... other dependencies
}
Add Microphone Permission
In AndroidManifest.xml, add the permission: < uses-permission android:name = "android.permission.RECORD_AUDIO" />
Add Model Files to Assets
Download models and add to app/src/main/assets/: # Download English models
pip install moonshine-voice
python -m moonshine_voice.download --language en
# Copy to assets folder
cp -r /path/to/downloaded/models app/src/main/assets/base-en
Quick Start Example
Download and try the pre-built example:
# Download example app
wget https://github.com/moonshine-ai/moonshine/releases/latest/download/android-examples.tar.gz
tar -xzf android-examples.tar.gz
# Open in Android Studio
# File > Open > Select 'Transcriber' folder
Build and run on your device to see live transcription.
Basic Implementation
Activity with Live Transcription
Here’s a complete example using MicTranscriber:
package ai.moonshine.example;
import ai.moonshine.voice.JNI;
import ai.moonshine.voice.MicTranscriber;
import ai.moonshine.voice.TranscriptEvent;
import ai.moonshine.voice.TranscriptEventListener;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
public class MainActivity extends AppCompatActivity {
private RecyclerView messagesRecyclerView ;
private TranscriptAdapter adapter ;
private FloatingActionButton recordButton ;
private MicTranscriber transcriber ;
private boolean isTranscribing = false ;
@ Override
protected void onCreate ( Bundle savedInstanceState ) {
super . onCreate (savedInstanceState);
setContentView ( R . layout . activity_main );
// Setup RecyclerView
messagesRecyclerView = findViewById ( R . id . messagesRecyclerView );
adapter = new TranscriptAdapter ();
messagesRecyclerView . setLayoutManager ( new LinearLayoutManager ( this ));
messagesRecyclerView . setAdapter (adapter);
// Create transcriber
transcriber = new MicTranscriber ( this );
// Load model from assets
transcriber . loadFromAssets (
this ,
"base-en" ,
JNI . MOONSHINE_MODEL_ARCH_BASE
);
// Add event listener
transcriber . addListener (event ->
event . accept ( new TranscriptEventListener () {
@ Override
public void onLineStarted ( TranscriptEvent . LineStarted e ) {
runOnUiThread (() -> {
adapter . addLine ( "..." );
messagesRecyclerView . smoothScrollToPosition (
adapter . getItemCount () - 1
);
});
}
@ Override
public void onLineTextChanged ( TranscriptEvent . LineTextChanged e ) {
runOnUiThread (() -> {
adapter . updateLastLine ( e . line . text );
messagesRecyclerView . smoothScrollToPosition (
adapter . getItemCount () - 1
);
});
}
@ Override
public void onLineCompleted ( TranscriptEvent . LineCompleted e ) {
runOnUiThread (() -> {
adapter . updateLastLine ( e . line . text );
});
}
})
);
// Request microphone permission
if ( checkSelfPermission ( android . Manifest . permission . RECORD_AUDIO )
== PackageManager . PERMISSION_GRANTED ) {
transcriber . onMicPermissionGranted ();
} else {
requestPermissions (
new String []{ android . Manifest . permission . RECORD_AUDIO },
1
);
}
// Setup record button
recordButton = findViewById ( R . id . recordButton );
recordButton . setOnClickListener (v -> toggleRecording ());
}
private void toggleRecording () {
if (isTranscribing) {
transcriber . stop ();
isTranscribing = false ;
recordButton . setBackgroundTintList (
android . content . res . ColorStateList . valueOf (
android . graphics . Color . parseColor ( "#ffffff" )
)
);
} else {
transcriber . start ();
isTranscribing = true ;
recordButton . setBackgroundTintList (
android . content . res . ColorStateList . valueOf (
android . graphics . Color . parseColor ( "#ff6b6b" )
)
);
}
}
@ Override
public void onRequestPermissionsResult ( int requestCode ,
@ NonNull String [] permissions ,
@ NonNull int [] grantResults ) {
super . onRequestPermissionsResult (requestCode, permissions, grantResults);
if (requestCode == 1 ) {
if ( grantResults . length > 0
&& grantResults[ 0 ] == PackageManager . PERMISSION_GRANTED ) {
transcriber . onMicPermissionGranted ();
}
}
}
@ Override
protected void onDestroy () {
super . onDestroy ();
if (transcriber != null ) {
transcriber . close ();
}
}
}
Simple RecyclerView Adapter
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class TranscriptAdapter extends RecyclerView.Adapter < TranscriptAdapter . ViewHolder > {
private List < String > lines = new ArrayList <>();
public void addLine ( String text ) {
lines . add (text);
notifyItemInserted ( lines . size () - 1 );
}
public void updateLastLine ( String text ) {
if ( ! lines . isEmpty ()) {
int lastIndex = lines . size () - 1 ;
lines . set (lastIndex, text);
notifyItemChanged (lastIndex);
}
}
@ NonNull
@ Override
public ViewHolder onCreateViewHolder (@ NonNull ViewGroup parent , int viewType ) {
View view = LayoutInflater . from ( parent . getContext ())
. inflate ( android . R . layout . simple_list_item_1 , parent, false );
return new ViewHolder (view);
}
@ Override
public void onBindViewHolder (@ NonNull ViewHolder holder , int position ) {
holder . textView . setText ( lines . get (position));
}
@ Override
public int getItemCount () {
return lines . size ();
}
static class ViewHolder extends RecyclerView.ViewHolder {
TextView textView ;
ViewHolder ( View view ) {
super (view);
textView = view . findViewById ( android . R . id . text1 );
}
}
}
Model Architectures
Use the appropriate constant from JNI class:
import ai.moonshine.voice.JNI;
// Available architectures
JNI . MOONSHINE_MODEL_ARCH_TINY // 0 - Smallest (26M params)
JNI . MOONSHINE_MODEL_ARCH_BASE // 1 - Good balance (58M params)
JNI . MOONSHINE_MODEL_ARCH_TINY_STREAMING // 2 - Tiny with streaming (34M params)
JNI . MOONSHINE_MODEL_ARCH_SMALL_STREAMING // 3 - High accuracy (123M params)
JNI . MOONSHINE_MODEL_ARCH_MEDIUM_STREAMING // 4 - Best accuracy (245M params)
Example usage:
// Load Base model
transcriber . loadFromAssets (
context,
"base-en" ,
JNI . MOONSHINE_MODEL_ARCH_BASE
);
// Load Tiny Streaming model
transcriber . loadFromAssets (
context,
"tiny-streaming-en" ,
JNI . MOONSHINE_MODEL_ARCH_TINY_STREAMING
);
Loading Models
From Assets
The recommended approach for bundled models:
transcriber . loadFromAssets (
context,
"base-en" , // Folder name in assets/
JNI . MOONSHINE_MODEL_ARCH_BASE
);
From File Path
For downloaded or external models:
import ai.moonshine.voice.Transcriber;
Transcriber transcriber = new Transcriber (
context,
"/sdcard/Download/base-en" , // Full path to model folder
JNI . MOONSHINE_MODEL_ARCH_BASE
);
Event-Driven Interface
Moonshine uses a visitor pattern for type-safe event handling:
transcriber . addListener (event -> {
event . accept ( new TranscriptEventListener () {
@ Override
public void onLineStarted ( TranscriptEvent . LineStarted e ) {
// New speech segment detected
String text = e . line . text ;
long lineId = e . line . lineId ;
double startTime = e . line . startTime ;
}
@ Override
public void onLineTextChanged ( TranscriptEvent . LineTextChanged e ) {
// Text updated during speech
String updatedText = e . line . text ;
}
@ Override
public void onLineCompleted ( TranscriptEvent . LineCompleted e ) {
// Speech segment finished
String finalText = e . line . text ;
double duration = e . line . duration ;
}
});
});
Supported Languages
Download and bundle models for multiple languages:
// English
transcriber . loadFromAssets (context, "base-en" , JNI . MOONSHINE_MODEL_ARCH_BASE );
// Spanish
transcriber . loadFromAssets (context, "base-es" , JNI . MOONSHINE_MODEL_ARCH_BASE );
// Japanese
transcriber . loadFromAssets (context, "base-ja" , JNI . MOONSHINE_MODEL_ARCH_BASE );
// Korean
transcriber . loadFromAssets (context, "tiny-ko" , JNI . MOONSHINE_MODEL_ARCH_TINY );
Available: English, Spanish, Japanese, Korean, Mandarin, Vietnamese, Ukrainian, Arabic
Threading Considerations
Event callbacks are NOT called on the UI thread. Always use runOnUiThread() when updating UI:
transcriber . addListener (event ->
event . accept ( new TranscriptEventListener () {
@ Override
public void onLineTextChanged ( TranscriptEvent . LineTextChanged e ) {
// Wrong - will crash
// textView.setText(e.line.text);
// Correct - safe UI update
runOnUiThread (() -> {
textView . setText ( e . line . text );
});
}
})
);
Build Configuration
Minimum SDK Requirements
android {
defaultConfig {
minSdk = 24 // Android 7.0 or higher
targetSdk = 35
}
}
ProGuard Rules
If using ProGuard/R8, add these rules:
# Moonshine Voice
-keep class ai.moonshine.voice.** { *; }
-keepclassmembers class ai.moonshine.voice.** { *; }
Model Selection
Model APK Size RAM Latency (Pixel 7) Tiny +26MB ~80MB ~50ms Tiny Streaming +34MB ~90MB ~40ms Base +58MB ~150MB ~70ms Small Streaming +123MB ~280MB ~120ms Medium Streaming +245MB ~500MB ~200ms
Tips
Use App Bundles - Google Play will only download models for user’s device
On-Demand Delivery - Download models after install to reduce initial size
Test on Low-End Devices - Ensure acceptable performance on entry-level phones
Monitor Memory - Large models can cause issues on devices with limited RAM
Common Issues
Model Files Not Found
Ensure models are in app/src/main/assets/ and structured correctly:
app/src/main/assets/
└── base-en/
├── encoder_model.ort
├── decoder_model_merged.ort
└── tokenizer.bin
Microphone Permission Denied
Check permission status before starting:
import android.Manifest;
import androidx.core.content.ContextCompat;
if ( ContextCompat . checkSelfPermission ( this , Manifest . permission . RECORD_AUDIO )
!= PackageManager . PERMISSION_GRANTED ) {
// Request permission
ActivityCompat . requestPermissions (
this ,
new String []{ Manifest . permission . RECORD_AUDIO },
REQUEST_RECORD_AUDIO
);
} else {
// Permission already granted
transcriber . onMicPermissionGranted ();
}
App Crashes on Start
Common causes:
Missing native libraries (check ABI filters)
Model files corrupted or incomplete
Insufficient device memory
Missing onMicPermissionGranted() call
Gradle Sync Issues
If package doesn’t resolve:
Check Maven repository is accessible
Verify version number is correct
Sync project with Gradle files
Invalidate caches and restart Android Studio
Example Projects
The repository includes a complete example:
Transcriber - Full Android app with live microphone transcription
Located in examples/android/Transcriber/
Demonstrates RecyclerView integration, permissions, and event handling
Next Steps
API Reference Detailed Android API documentation
Models Available models and architectures
Examples More Android examples
Troubleshooting Common issues and solutions