Overview
The AudioStreamInterface enables you to integrate any audio source with RealtimeTranscriber. Whether you’re using a third-party audio library, custom hardware, or a specialized audio pipeline, implementing this interface allows seamless integration.
AudioStreamInterface
All audio stream adapters must implement this interface to work with the realtime transcription system.
Interface Definition
export interface AudioStreamInterface {
initialize ( config : AudioStreamConfig ) : Promise < void >
start () : Promise < void >
stop () : Promise < void >
isRecording () : boolean
onData ( callback : ( data : AudioStreamData ) => void ) : void
onError ( callback : ( error : string ) => void ) : void
onStatusChange ( callback : ( isRecording : boolean ) => void ) : void
onEnd ? ( callback : () => void ) : void
release () : Promise < void >
}
Configuration Types
Configuration object passed to initialize() Audio sample rate in Hz. Whisper requires 16kHz.
Number of audio channels. Whisper requires mono (1 channel).
Bits per audio sample. Standard is 16-bit PCM.
Size of audio buffer in bytes. Affects latency and memory usage.
Platform-specific audio source identifier (Android MediaRecorder constants).
Data structure delivered via the onData callback Raw PCM audio data as bytes (16-bit PCM).
Sample rate of this audio chunk.
Number of channels in this audio chunk.
Timestamp when this data was captured (milliseconds since epoch).
Method Requirements
initialize(config)
Called once to set up the audio stream with configuration parameters.
config
AudioStreamConfig
required
Stream configuration including sample rate, channels, buffer size
Requirements:
Configure the underlying audio system
Validate configuration parameters
Set up internal state
Should be idempotent (safe to call multiple times)
start()
Begin capturing audio and emitting data through the onData callback.
Requirements:
Start audio capture
Begin calling onData callback with audio chunks
Call onStatusChange(true) when recording starts
Throw error if not initialized
stop()
Stop capturing audio.
Requirements:
Stop audio capture
Stop emitting data callbacks
Call onStatusChange(false) when recording stops
Should be safe to call even if not recording
isRecording()
Return current recording state.
true if currently capturing audio, false otherwise
onData(callback)
Register callback for receiving audio data chunks.
Function called with each audio chunk: (data: AudioStreamData) => void
Requirements:
Store callback for later invocation
Call whenever new audio data is available
Should handle case where callback is not set
onError(callback)
Register callback for error handling.
Function called on errors: (error: string) => void
Requirements:
Call when audio system errors occur
Provide descriptive error messages
Should not throw, only invoke callback
onStatusChange(callback)
Register callback for recording status changes.
Function called on status change: (isRecording: boolean) => void
Requirements:
Call when recording starts (true)
Call when recording stops (false)
onEnd(callback) [Optional]
Register callback for when audio stream ends naturally (e.g., file playback completes).
Function called when stream ends: () => void
release()
Clean up resources and prepare for disposal.
Requirements:
Stop recording if active
Release native audio resources
Clear callbacks
Reset internal state
Should be idempotent
Built-in Adapter: AudioPcmStreamAdapter
The library includes a production-ready adapter using @fugood/react-native-audio-pcm-stream.
Example Implementation
import LiveAudioStream from '@fugood/react-native-audio-pcm-stream'
import type { AudioStreamInterface , AudioStreamConfig , AudioStreamData } from '../types'
import { base64ToUint8Array } from '../../utils/common'
export class AudioPcmStreamAdapter implements AudioStreamInterface {
private isInitialized = false
private recording = false
private config : AudioStreamConfig | null = null
private dataCallback ?: ( data : AudioStreamData ) => void
private errorCallback ?: ( error : string ) => void
private statusCallback ?: ( isRecording : boolean ) => void
async initialize ( config : AudioStreamConfig ) : Promise < void > {
if ( this . isInitialized ) {
await this . release ()
}
try {
this . config = config || null
LiveAudioStream . init ({
sampleRate: config . sampleRate || 16000 ,
channels: config . channels || 1 ,
bitsPerSample: config . bitsPerSample || 16 ,
audioSource: config . audioSource || 6 ,
bufferSize: config . bufferSize || 16 * 1024 ,
wavFile: '' ,
})
LiveAudioStream . on ( 'data' , this . handleAudioData . bind ( this ))
this . isInitialized = true
} catch ( error ) {
const errorMessage = error instanceof Error ? error . message : 'Unknown error'
this . errorCallback ?.( errorMessage )
throw new Error ( `Failed to initialize: ${ errorMessage } ` )
}
}
async start () : Promise < void > {
if ( ! this . isInitialized ) {
throw new Error ( 'Not initialized' )
}
if ( this . recording ) return
LiveAudioStream . start ()
this . recording = true
this . statusCallback ?.( true )
}
async stop () : Promise < void > {
if ( ! this . recording ) return
await LiveAudioStream . stop ()
this . recording = false
this . statusCallback ?.( false )
}
isRecording () : boolean {
return this . recording
}
onData ( callback : ( data : AudioStreamData ) => void ) : void {
this . dataCallback = callback
}
onError ( callback : ( error : string ) => void ) : void {
this . errorCallback = callback
}
onStatusChange ( callback : ( isRecording : boolean ) => void ) : void {
this . statusCallback = callback
}
async release () : Promise < void > {
if ( this . recording ) {
await this . stop ()
}
this . isInitialized = false
this . config = null
this . dataCallback = undefined
this . errorCallback = undefined
this . statusCallback = undefined
}
private handleAudioData ( base64Data : string ) : void {
if ( ! this . dataCallback ) return
try {
const audioData = base64ToUint8Array ( base64Data )
const streamData : AudioStreamData = {
data: audioData ,
sampleRate: this . config ?. sampleRate || 16000 ,
channels: this . config ?. channels || 1 ,
timestamp: Date . now (),
}
this . dataCallback ( streamData )
} catch ( error ) {
const errorMessage = error instanceof Error ? error . message : 'Processing error'
this . errorCallback ?.( errorMessage )
}
}
}
Custom Adapter Example
Here’s a minimal custom adapter for testing or file-based input:
import type { AudioStreamInterface , AudioStreamConfig , AudioStreamData } from 'whisper.rn'
export class FileAudioAdapter implements AudioStreamInterface {
private config : AudioStreamConfig | null = null
private dataCallback ?: ( data : AudioStreamData ) => void
private errorCallback ?: ( error : string ) => void
private statusCallback ?: ( isRecording : boolean ) => void
private intervalId ?: NodeJS . Timeout
private isPlaying = false
private audioFileData : Uint8Array
private currentPosition = 0
constructor ( audioFileData : Uint8Array ) {
this . audioFileData = audioFileData
}
async initialize ( config : AudioStreamConfig ) : Promise < void > {
this . config = config
this . currentPosition = 0
}
async start () : Promise < void > {
if ( this . isPlaying ) return
this . isPlaying = true
this . statusCallback ?.( true )
// Simulate streaming by sending chunks at regular intervals
const chunkSize = ( this . config ?. bufferSize || 4096 )
const interval = ( chunkSize / ( this . config ?. sampleRate || 16000 )) * 1000
this . intervalId = setInterval (() => {
if ( this . currentPosition >= this . audioFileData . length ) {
this . stop ()
return
}
const chunk = this . audioFileData . slice (
this . currentPosition ,
this . currentPosition + chunkSize
)
this . dataCallback ?.({
data: chunk ,
sampleRate: this . config ?. sampleRate || 16000 ,
channels: this . config ?. channels || 1 ,
timestamp: Date . now (),
})
this . currentPosition += chunkSize
}, interval )
}
async stop () : Promise < void > {
if ( ! this . isPlaying ) return
if ( this . intervalId ) {
clearInterval ( this . intervalId )
}
this . isPlaying = false
this . statusCallback ?.( false )
}
isRecording () : boolean {
return this . isPlaying
}
onData ( callback : ( data : AudioStreamData ) => void ) : void {
this . dataCallback = callback
}
onError ( callback : ( error : string ) => void ) : void {
this . errorCallback = callback
}
onStatusChange ( callback : ( isRecording : boolean ) => void ) : void {
this . statusCallback = callback
}
async release () : Promise < void > {
await this . stop ()
this . config = null
this . dataCallback = undefined
this . errorCallback = undefined
this . statusCallback = undefined
}
}
Usage with RealtimeTranscriber
Once you’ve implemented an adapter, use it with RealtimeTranscriber:
import { RealtimeTranscriber } from 'whisper.rn/src/realtime-transcription'
import { FileAudioAdapter } from './FileAudioAdapter'
// Create your custom adapter
const audioData = loadAudioFile () // Your audio loading logic
const audioStream = new FileAudioAdapter ( audioData )
// Initialize transcriber with custom adapter
const transcriber = new RealtimeTranscriber (
{
whisperContext ,
audioStream , // Your custom adapter
},
{
audioSliceSec: 30 ,
transcribeOptions: { language: 'en' },
},
{
onTranscribe : ( event ) => {
console . log ( 'Transcription:' , event . data ?. result )
},
}
)
await transcriber . start ()
Best Practices
Always wrap audio system calls in try-catch
Use error callback instead of throwing
Provide descriptive error messages
Handle edge cases (already initialized, not initialized, etc.)
Release native resources in release()
Clear callbacks to prevent memory leaks
Consider audio buffer size vs. memory usage
Use typed arrays (Uint8Array) for efficient data transfer
Testing Your Adapter
Test your custom adapter thoroughly:
import { YourCustomAdapter } from './YourCustomAdapter'
describe ( 'YourCustomAdapter' , () => {
let adapter : YourCustomAdapter
beforeEach (() => {
adapter = new YourCustomAdapter ()
})
afterEach ( async () => {
await adapter . release ()
})
test ( 'initialize configures the adapter' , async () => {
await adapter . initialize ({ sampleRate: 16000 , channels: 1 })
expect ( adapter . isRecording ()). toBe ( false )
})
test ( 'start begins recording' , async () => {
await adapter . initialize ({ sampleRate: 16000 , channels: 1 })
await adapter . start ()
expect ( adapter . isRecording ()). toBe ( true )
})
test ( 'onData callback receives audio data' , async () => {
const dataCallback = jest . fn ()
adapter . onData ( dataCallback )
await adapter . initialize ({ sampleRate: 16000 , channels: 1 })
await adapter . start ()
// Wait for data
await new Promise ( resolve => setTimeout ( resolve , 1000 ))
expect ( dataCallback ). toHaveBeenCalled ()
const data = dataCallback . mock . calls [ 0 ][ 0 ]
expect ( data . sampleRate ). toBe ( 16000 )
expect ( data . channels ). toBe ( 1 )
})
test ( 'release cleans up resources' , async () => {
await adapter . initialize ({ sampleRate: 16000 , channels: 1 })
await adapter . start ()
await adapter . release ()
expect ( adapter . isRecording ()). toBe ( false )
})
})