react-native-voice-hold
TypeScript icon, indicating that this package has built-in type declarations

1.0.7 • Public • Published

🎙️ react-native-voice-hold

npm version npm license

Enhanced React Native Voice Recognition Library with Hold Recording Support

A robust React Native voice recognition library with critical bug fixes for React Native 0.76+ and the New Architecture. This package resolves the major event listener issues that break speech-to-text functionality in the latest React Native versions.

🚨 Key Fixes & Improvements

React Native 0.76+ Compatibility

  • Fixed: Critical event listener bug in React Native 0.76+ with New Architecture
  • Solution: Implemented DeviceEventEmitter pattern for reliable event handling
  • Result: Speech-to-text results now work properly in RN 0.76+ and 0.80+

🔥 Critical Fix: Empty Final Results

  • Issue: Android Speech Recognition API sometimes returns empty final results even when partial results contain valid transcription
  • Symptoms: onSpeechResults callback receives empty array [] despite successful partial results
  • Root Cause: Device-specific speech recognition quirks, short speech segments, background noise, timing issues
  • Solution: Implemented comprehensive fallback mechanism using partial results
    • Timeout Fallback: 1-second timeout ensures transcript processing even if final results fail
    • Partial Result Storage: Stores meaningful partial results for fallback use
    • Smart Fallback Logic: Uses partial results when final results are empty
    • Error Recovery: Proper cleanup and interruption handling

🎯 Enhanced Hold Recording

  • New: startHoldRecording() and stopHoldRecording() methods
  • Feature: Continuous recording without auto-stop timeouts
  • Use Case: Perfect for voice messages and long-form dictation

🔧 Production Ready

  • Architecture: Full New Architecture (TurboModules) support
  • Platforms: iOS 11+ and Android API 21+
  • Stability: Comprehensive error handling and state management

📱 Installation

npm install react-native-voice-hold

iOS Setup (Required)

cd ios && pod install

Android Setup

Add microphone permissions to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />

🚀 Quick Start

Basic Usage (Fixed for RN 0.76+)

import Voice from 'react-native-voice-hold';
import { DeviceEventEmitter } from 'react-native';

const VoiceComponent = () => {
  const [results, setResults] = useState<string[]>([]);
  const [isListening, setIsListening] = useState(false);

  useEffect(() => {
    // ✅ CRITICAL: Use DeviceEventEmitter for RN 0.76+ compatibility
    const onSpeechResults = DeviceEventEmitter.addListener(
      'onSpeechResults',
      (e: any) => {
        console.log('📝 Speech results:', e.value);
        setResults(e.value);
      }
    );

    const onSpeechStart = DeviceEventEmitter.addListener(
      'onSpeechStart',
      () => {
        console.log('🎤 Speech started');
        setIsListening(true);
      }
    );

    const onSpeechEnd = DeviceEventEmitter.addListener(
      'onSpeechEnd',
      () => {
        console.log('🔇 Speech ended');
        setIsListening(false);
      }
    );

    return () => {
      onSpeechResults.remove();
      onSpeechStart.remove();
      onSpeechEnd.remove();
    };
  }, []);

  const startListening = async () => {
    try {
      await Voice.start('en-US');
    } catch (error) {
      console.error('❌ Error starting voice recognition:', error);
    }
  };

  const stopListening = async () => {
    try {
      await Voice.stop();
    } catch (error) {
      console.error('❌ Error stopping voice recognition:', error);
    }
  };

  return (
    <View>
      <Button
        title={isListening ? 'Stop Listening' : 'Start Listening'}
        onPress={isListening ? stopListening : startListening}
      />
      {results.map((result, index) => (
        <Text key={index}>{result}</Text>
      ))}
    </View>
  );
};

Hold Recording (New Feature)

import Voice from 'react-native-voice-hold';

// Start continuous recording (perfect for voice messages)
const startHoldRecording = async () => {
  try {
    await Voice.startHoldRecording('en-US', {
      // Hold mode disables silence timeouts
      continuous: true,
      maximumWaitTime: 300000, // 5 minutes max
    });
    console.log('🎙️ Hold recording started');
  } catch (error) {
    console.error('❌ Error starting hold recording:', error);
  }
};

// Stop hold recording manually
const stopHoldRecording = async () => {
  try {
    await Voice.stopHoldRecording();
    console.log('⏹️ Hold recording stopped');
  } catch (error) {
    console.error('❌ Error stopping hold recording:', error);
  }
};

🔥 Critical Fix: Empty Final Results Solution

Hook Implementation (Recommended)

import { useVoiceRecognition } from 'react-native-voice-hold';

const VoiceAssistant = () => {
  const [state, actions] = useVoiceRecognition({
    locale: 'en-US',
    onStart: () => {
      console.log('🎤 Speech started');
      // Clear any existing timeout and reset state
    },
    onEnd: () => {
      console.log('🔇 Speech ended - processing...');
      // 🔥 CRITICAL FIX: 1-second timeout ensures transcript processing
      // even if final results don't come
    },
    onResults: results => {
      console.log('📝 Final results received:', results);
      if (results.length > 0) {
        // Process final results normally
        processTranscript(results[0]);
      } else {
        // 🔥 CRITICAL FIX: Use partial results as fallback
        console.log('⚠️ Final results empty, using partial results fallback');
        // Hook automatically handles fallback logic
      }
    },
    onPartialResults: partial => {
      console.log('🔄 Partial results:', partial);
      // 🔥 CRITICAL FIX: Hook stores partial results for fallback
    },
  });

  // The hook automatically handles all fallback logic!
  return (
    <View>
      <TouchableOpacity
        onPressIn={() => actions.startHoldRecording()}
        onPressOut={() => actions.stopListening()}
      >
        <Text>Hold to Speak</Text>
      </TouchableOpacity>
    </View>
  );
};

Direct Library Implementation

import React, { useEffect, useState, useRef } from 'react';
import { DeviceEventEmitter, Platform } from 'react-native';
import Voice from 'react-native-voice-hold';

const VoiceComponent = () => {
  const [results, setResults] = useState<string[]>([]);
  const [isListening, setIsListening] = useState(false);
  
  // 🔥 CRITICAL FIX: Add refs for fallback mechanism
  const lastPartialResult = useRef<string>('');
  const resultsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const isInterrupted = useRef<boolean>(false);

  useEffect(() => {
    const onSpeechStart = DeviceEventEmitter.addListener('onSpeechStart', () => {
      console.log('🎤 Speech started');
      setIsListening(true);
      // Clear any existing timeout
      if (resultsTimeoutRef.current) {
        clearTimeout(resultsTimeoutRef.current);
        resultsTimeoutRef.current = null;
      }
      lastPartialResult.current = '';
    });

    const onSpeechEnd = DeviceEventEmitter.addListener('onSpeechEnd', () => {
      console.log('🔇 Speech ended - processing...');
      // 🔥 CRITICAL FIX: Add timeout to ensure we process transcript
      resultsTimeoutRef.current = setTimeout(() => {
        console.log('⏰ Timeout: No final results received, using partial results');
        const fallbackTranscript = lastPartialResult.current;
        if (fallbackTranscript.trim() && !isInterrupted.current) {
          console.log('🔄 Timeout fallback transcript:', fallbackTranscript);
          handleTranscript(fallbackTranscript);
        }
      }, 1000); // Wait 1 second for final results
    });

    const onSpeechResults = DeviceEventEmitter.addListener('onSpeechResults', (e) => {
      console.log('📝 Speech results:', e.value);
      // Clear the timeout since we got results
      if (resultsTimeoutRef.current) {
        clearTimeout(resultsTimeoutRef.current);
        resultsTimeoutRef.current = null;
      }

      if (e.value && e.value.length > 0) {
        const finalTranscript = e.value[0];
        console.log('⚡ Fast transcript:', finalTranscript);
        handleTranscript(finalTranscript);
      } else {
        // 🔥 CRITICAL FIX: Use partial results as fallback
        console.log('⚠️ Final results empty, checking partial results...');
        const fallbackTranscript = lastPartialResult.current;
        if (fallbackTranscript.trim()) {
          console.log('🔄 Using fallback transcript:', fallbackTranscript);
          handleTranscript(fallbackTranscript);
        }
      }
    });

    const onSpeechPartialResults = DeviceEventEmitter.addListener('onSpeechPartialResults', (e) => {
      console.log('🔄 Partial results:', e.value);
      if (e.value && e.value.length > 0) {
        const partialTranscript = e.value[0];
        // 🔥 CRITICAL FIX: Store the last partial result for fallback
        if (partialTranscript.trim()) {
          lastPartialResult.current = partialTranscript;
        }
      }
    });

    return () => {
      onSpeechStart.remove();
      onSpeechEnd.remove();
      onSpeechResults.remove();
      onSpeechPartialResults.remove();
      // Clear timeout on cleanup
      if (resultsTimeoutRef.current) {
        clearTimeout(resultsTimeoutRef.current);
        resultsTimeoutRef.current = null;
      }
      Voice.destroy();
    };
  }, []);

  const handleTranscript = (transcript: string) => {
    console.log('🎯 Processing transcript:', transcript);
    setResults([transcript]);
  };

  // ... rest of component
};

📖 Complete API Reference

Standard Methods

  • Voice.start(locale) - Start voice recognition
  • Voice.stop() - Stop voice recognition
  • Voice.cancel() - Cancel voice recognition
  • Voice.destroy() - Clean up resources
  • Voice.isAvailable() - Check if voice recognition is available
  • Voice.isRecognizing() - Check if currently recognizing

Hold Recording Methods (New)

  • Voice.startHoldRecording(locale, options) - Start continuous recording
  • Voice.stopHoldRecording() - Stop hold recording

Event Listeners (DeviceEventEmitter - RN 0.76+ Compatible)

// ✅ CORRECT: Use DeviceEventEmitter
DeviceEventEmitter.addListener('onSpeechStart', handler);
DeviceEventEmitter.addListener('onSpeechRecognized', handler);
DeviceEventEmitter.addListener('onSpeechEnd', handler);
DeviceEventEmitter.addListener('onSpeechError', handler);
DeviceEventEmitter.addListener('onSpeechResults', handler);
DeviceEventEmitter.addListener('onSpeechPartialResults', handler);
DeviceEventEmitter.addListener('onSpeechVolumeChanged', handler);

// ❌ BROKEN: Direct assignment (doesn't work in RN 0.76+)
Voice.onSpeechResults = handler; // DON'T USE

🔄 Migration from react-native-voice

  1. Install the new package:

    npm uninstall @react-native-voice/voice
    npm install react-native-voice-hold
  2. Update your imports:

    // Old

import Voice from '@react-native-voice/voice';

// New import Voice from 'react-native-voice-hold';


3. **Update event listeners for RN 0.76+ compatibility:**
```typescript
// Old (broken in RN 0.76+)
Voice.onSpeechResults = (e) => console.log(e.value);

// New (works in all RN versions)
DeviceEventEmitter.addListener('onSpeechResults', (e) => console.log(e.value));
  1. Add fallback logic for empty final results:
    // The useVoiceRecognition hook automatically handles this!
    // Or implement the fallback pattern shown above for direct usage

🐛 Troubleshooting

Empty Final Results

Problem: onSpeechResults receives empty array despite successful partial results.

Solution: ✅ Fixed in this package - The hook and examples above include comprehensive fallback logic.

Event Listeners Not Working (RN 0.76+)

Problem: Speech events not firing in React Native 0.76+.

Solution: ✅ Fixed - Use DeviceEventEmitter pattern instead of direct assignment.

Hold Recording Not Working

Problem: Hold recording stops unexpectedly.

Solution: ✅ Fixed - Proper hold mode state management prevents premature stopping.

📄 License

MIT License - see LICENSE file for details.

🤝 Contributing

Contributions are welcome! Please read our contributing guidelines and submit pull requests.


Built with ❤️ for the React Native community

Package Sidebar

Install

npm i react-native-voice-hold

Weekly Downloads

403

Version

1.0.7

License

MIT

Unpacked Size

301 kB

Total Files

56

Last publish

Collaborators

  • malek0x1