tonelisten-react-native
Version:
ToneListen React Native Framework - Audio tone detection for React Native apps
731 lines (572 loc) • 22.3 kB
Markdown
# ToneListen React Native Framework
A React Native framework for audio tone detection and in-app content delivery, based on patented tone technology. This library provides real-time dual-tone multi-frequency (DTMF) detection, bridge tone recognition, and automatic content display using the Goertzel algorithm, optimized for React Native applications.
## Features
- **Real-time Audio Processing**: Uses Web Audio API for high-performance audio analysis
- **Goertzel Algorithm**: Efficient frequency detection with Hanning windowing
- **Dual Tone Detection**: Recognizes DTMF tone pairs with configurable tolerance
- **Bridge Tone Detection**: Identifies bridge tones for sequence validation with local peak analysis
- **Sequence State Machine**: Tracks and emits complete tone sequences with timer-based emissions
- **In-App Content Delivery**: Automatic content display with customizable UI
- **API Integration**: Built-in API service for content retrieval
- **React Native Integration**: Easy-to-use hooks, components, and automatic modal
- **TypeScript Support**: Full TypeScript definitions included
- **Cross-Platform**: Works on both iOS and Android React Native apps
## Installation
```bash
npm install tonelisten-react-native
```
### Peer Dependencies
```bash
npm install react-native-audio-recorder-player react-native-permissions
```
### iOS Setup
After installation, run:
```bash
cd ios && pod install
```
If you updated to version 1.0.61 or later (iOS playback decode support), rebuild your iOS app:
1. Open Xcode, stop any running build
2. Product → Clean Build Folder (Cmd+Shift+K)
3. Build & Run (Cmd+R)
This is required because native iOS code changed.
## Quick Start
### 1. Basic Tone Detection
```typescript
import React, { useEffect } from 'react';
import { ToneListenReactNative } from 'tonelisten-react-native';
const App = () => {
useEffect(() => {
const toneListener = new ToneListenReactNative({
clientId: '562', // Your client ID
apiKey: 'your-user-specific-api-key', // Get this from your admin dashboard
baseURL: 'https://api.toneadmin.com',
// CORS proxy not needed if API has proper CORS headers
debug: true,
enableInAppContent: true, // Enable automatic content display
});
// Initialize and start
toneListener.initialize().then(() => {
toneListener.start();
});
return () => {
toneListener.destroy();
};
}, []);
return <YourAppComponent />;
};
```
### 2. Automatic Content Display (Recommended)
The framework includes an automatic content modal that displays images, videos, and other content when tone sequences are detected:
```typescript
import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import ToneListenReactNative, {
InAppContentModal,
InAppContentPayload,
NotificationCenter
} from 'tonelisten-react-native';
const App = () => {
const [modalVisible, setModalVisible] = useState(false);
const [modalContent, setModalContent] = useState<InAppContentPayload | null>(null);
useEffect(() => {
// Set up automatic content display
const notificationCenter = NotificationCenter.getInstance();
notificationCenter.addObserver('InAppContentReceived', (content) => {
console.log('Content received:', content);
setModalContent(content);
setModalVisible(true);
});
// Initialize ToneListen
const toneListener = new ToneListenReactNative({
clientId: '562',
apiKey: 'your-api-key',
baseURL: 'https://api.toneadmin.com',
enableInAppContent: true, // This enables automatic content fetching
});
toneListener.initialize().then(() => {
toneListener.start();
});
return () => {
toneListener.destroy();
notificationCenter.removeObserver('InAppContentReceived');
};
}, []);
return (
<View style={{ flex: 1 }}>
{/* Your app content */}
{/* Automatic content modal */}
<InAppContentModal
visible={modalVisible}
content={modalContent}
onClose={() => setModalVisible(false)}
/>
</View>
);
};
```
### 3. Custom UI Implementation
If you prefer to build your own UI instead of using the automatic modal, you can listen for notifications and create custom components:
```typescript
import React, { useEffect, useState } from 'react';
import { View, Text, Image, Modal, TouchableOpacity } from 'react-native';
import ToneListenReactNative, {
NotificationCenter,
InAppContentPayload
} from 'tonelisten-react-native';
const CustomContentDisplay = () => {
const [content, setContent] = useState<InAppContentPayload | null>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const notificationCenter = NotificationCenter.getInstance();
notificationCenter.addObserver('InAppContentReceived', (receivedContent) => {
console.log('Custom UI received content:', receivedContent);
setContent(receivedContent);
setVisible(true);
});
return () => {
notificationCenter.removeObserver('InAppContentReceived');
};
}, []);
const renderContent = () => {
if (!content) return null;
switch (content.actionType) {
case 'image':
return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 18, marginBottom: 10 }}>
{content.title}
</Text>
<Image
source={{ uri: content.actionUrl }}
style={{ width: 300, height: 200 }}
resizeMode="contain"
/>
</View>
);
case 'video':
return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 18, marginBottom: 10 }}>
{content.title}
</Text>
<Text>Video: {content.actionUrl}</Text>
</View>
);
case 'webpage':
return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 18, marginBottom: 10 }}>
{content.title}
</Text>
<Text>{content.actionUrl}</Text>
</View>
);
default:
return (
<View style={{ padding: 20 }}>
<Text>{content.title || 'Content received'}</Text>
</View>
);
}
};
return (
<Modal visible={visible} transparent animationType="slide">
<View style={{
flex: 1,
backgroundColor: 'rgba(0,0,0,0.8)',
justifyContent: 'center',
alignItems: 'center'
}}>
<View style={{
backgroundColor: 'white',
borderRadius: 10,
padding: 20,
maxWidth: '90%',
maxHeight: '80%'
}}>
{renderContent()}
<TouchableOpacity
style={{
backgroundColor: '#007AFF',
padding: 10,
borderRadius: 5,
marginTop: 20
}}
onPress={() => setVisible(false)}
>
<Text style={{ color: 'white', textAlign: 'center' }}>Close</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
};
const App = () => {
useEffect(() => {
const toneListener = new ToneListenReactNative({
clientId: '562',
apiKey: 'your-api-key',
baseURL: 'https://api.toneadmin.com',
enableInAppContent: true,
});
toneListener.initialize().then(() => {
toneListener.start();
});
return () => {
toneListener.destroy();
};
}, []);
return (
<View style={{ flex: 1 }}>
{/* Your app content */}
<CustomContentDisplay />
</View>
);
};
```
### 2b. Playback Decode (loud media + accurate detection)
To detect tones embedded in your app’s media at full playback volume, use playback decode.
- iOS and Android supported (Android supported in 1.0.62+)
- Automatically stops the mic engine while decoding
- Works with local or remote media URLs (remote URLs are downloaded to a temp file under the hood)
```typescript
// When starting media playback in your app
await toneListener.startPlaybackDecode(mediaUrl); // returns true/false
// ...start your AV playback (video/audio) as normal...
// When pausing/stopping media
await toneListener.stopPlaybackDecode(); // Restores mic-based detection
```
Debug logs to expect when working:
- iOS: "PlaybackDecode: audio session set to category=playback, mode=moviePlayback"
- iOS: "PlaybackDecode: reader started → rate=48000.0Hz, channels=…"
- Android: playback decode running on background thread (no session log)
- Both: "NativeAudioPipeline: AudioFrame source -> playback" (frames come from media)
If frames show "source -> mic" during playback, playback decode isn’t active.
### 4. Using the Hook (Alternative Approach)
```typescript
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useToneDetection } from 'tonelisten-react-native';
const ToneDetectionScreen = () => {
const {
isListening,
isInitialized,
lastSequence,
startListening,
stopListening
} = useToneDetection({
clientId: '562',
apiKey: 'your-api-key',
baseURL: 'https://api.toneadmin.com',
enableInAppContent: true,
onSequence: (event) => {
console.log('Sequence detected:', event.sequence);
}
});
return (
<View style={{ padding: 20 }}>
<Text>Status: {isListening ? 'Listening' : 'Stopped'}</Text>
{lastSequence && <Text>Last Sequence: {lastSequence}</Text>}
<Button
title={isListening ? 'Stop' : 'Start'}
onPress={isListening ? stopListening : startListening}
/>
</View>
);
};
```
## User-Specific API Key Authentication
The framework uses user-specific API keys to ensure only authorized users can access tone detection endpoints. Each user has their own unique API key that must be obtained from your admin dashboard.
### Getting User-Specific API Keys
1. **Access Admin Dashboard**: Log into your ToneListen admin dashboard
2. **Navigate to Users**: Go to the Users section
3. **Find Target User**: Locate the user you want to create an app for
4. **Generate API Key**: Click the key icon (🔑) next to the user to regenerate their API key
5. **Copy API Key**: Copy the generated API key (it won't be shown again)
6. **Configure App**: Use the API key in your React Native app configuration
### API Key Security
- **User-Specific**: Each API key is tied to a specific user account
- **Company Validation**: The framework validates that the `clientId` matches the user's company
- **Automatic Logging**: All API requests are logged with user information
- **Key Rotation**: API keys can be regenerated at any time for security
### Example Configuration
```typescript
const toneListener = new ToneListenReactNative({
clientId: '562', // Must match the user's company ID
apiKey: 'a1b2c3d4e5f6...', // User-specific API key from admin dashboard
baseURL: 'https://api.toneadmin.com',
debug: true,
});
```
## Configuration
### ToneListenReactNative Configuration
```typescript
interface ToneListenReactNativeConfig {
// Audio Configuration
sampleRate?: number; // Audio sample rate (default: 44100)
bufferSize?: number; // Audio buffer size (default: 4800)
tolerance?: number; // Frequency matching tolerance in Hz (default: 5)
bridgeTolerance?: number; // Bridge tone tolerance in Hz (default: 10)
minPower?: number; // Minimum power threshold (default: 0.01)
// API Configuration
clientId?: string; // Your client ID
apiKey?: string; // Your API key
baseURL?: string; // API base URL (default: 'https://api.toneadmin.com')
corsProxyBaseURL?: string; // CORS proxy URL for web platforms
useProxyOnWeb?: boolean; // Use proxy on web platforms (default: true)
// Behavior Configuration
autoStart?: boolean; // Start automatically (default: false)
debug?: boolean; // Enable debug logging (default: false)
enableInAppContent?: boolean; // Enable automatic content display (default: false)
// Callbacks
onSequence?: (event: ToneSequenceEvent) => void;
onDualTone?: (result: DualToneResult) => void;
onBridgeTone?: (result: BridgeToneResult) => void;
onError?: (error: Error) => void;
onPermissionDenied?: () => void;
onPresentContent?: (content: InAppContentPayload) => void; // Custom content handler
}
```
### InAppContentModal Configuration
```typescript
interface InAppContentModalProps {
visible: boolean; // Whether modal is visible
content?: InAppContentPayload | null; // Content to display
onClose: () => void; // Close handler
}
```
### InAppContentPayload
```typescript
interface InAppContentPayload {
actionType: 'image' | 'video' | 'webpage' | 'text'; // Content type
actionUrl?: string; // URL for the content
actionData?: string; // Additional data
title?: string; // Content title
}
```
## API Reference
### ToneListenReactNative
Main class for tone detection and content delivery.
#### Methods
- `initialize()`: Initialize the tone detection system
- `start()`: Start listening for tones
- `stop()`: Stop listening for tones
- `startPlaybackDecode(url: string)`: iOS only. Start decoding media for detection while keeping device playback loud
- `stopPlaybackDecode()`: iOS only. Stop media decoding and return to mic-based detection
- `updateConfig(config)`: Update configuration
- `getState()`: Get current detection state
- `destroy()`: Clean up resources
#### Events
- `TLRN_AudioFrame` payload now includes an optional `source` field on iOS:
```ts
{
sampleRate: number;
buffer: number[]; // Float32 PCM
source?: 'mic' | 'playback';
}
```
### NotificationCenter
Singleton class for handling notifications between framework and app.
#### Methods
- `getInstance()`: Get singleton instance
- `addObserver(name, callback)`: Add notification observer
- `removeObserver(name, callback?)`: Remove notification observer
- `post(name, object?, userInfo?)`: Post notification
#### Notification Names
- `'InAppContentReceived'`: Fired when content is received from API
### useToneDetection Hook
React hook for easy integration.
#### Returns
- `isListening`: Whether currently listening
- `isInitialized`: Whether system is initialized
- `error`: Current error state
- `lastSequence`: Last detected sequence
- `lastDualTone`: Last detected dual tone
- `lastBridgeTone`: Last detected bridge tone
- `startListening()`: Start listening function
- `stopListening()`: Stop listening function
- `updateConfig(config)`: Update configuration function
- `getState()`: Get current state function
## Content Types
The framework supports various content types that can be displayed automatically:
### Image Content
```typescript
{
actionType: 'image',
actionUrl: 'https://example.com/image.jpg',
title: 'Image Title'
}
```
### Video Content
```typescript
{
actionType: 'video',
actionUrl: 'https://example.com/video.mp4',
title: 'Video Title'
}
```
### Webpage Content
```typescript
{
actionType: 'webpage',
actionUrl: 'https://example.com/page',
title: 'Webpage Title'
}
```
### Text Content
```typescript
{
actionType: 'text',
actionData: 'Your text content here',
title: 'Text Title'
}
```
## Permissions
The framework requires microphone and location permissions for full functionality. Make sure to add the necessary permissions to your app:
### iOS (Info.plist)
```xml
<!-- Microphone permission for tone detection -->
<key>NSMicrophoneUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your microphone</string>
<!-- Location permissions for enhanced tone detection features -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to provide location-based content when tones are detected.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs location access to provide location-based content when tones are detected.</string>
```
### Android (AndroidManifest.xml)
```xml
<!-- Microphone permission for tone detection -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Location permissions for enhanced tone detection features -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- Internet permission for API calls -->
<uses-permission android:name="android.permission.INTERNET" />
```
### Permission Descriptions
- **NSMicrophoneUsageDescription**: Required for audio recording and tone detection
- **NSLocationWhenInUseUsageDescription**: Allows location access while the app is in use
- **NSLocationAlwaysAndWhenInUseUsageDescription**: Allows location access both when in use and in background
- **ACCESS_COARSE_LOCATION**: Provides approximate location (network-based)
- **ACCESS_FINE_LOCATION**: Provides precise location (GPS-based)
- **RECORD_AUDIO**: Required for microphone access on Android
- **INTERNET**: Required for API calls to the ToneListen backend
## CORS and Web Support
### What is CORS?
CORS (Cross-Origin Resource Sharing) is a browser security feature that blocks requests between different domains. When your React Native app runs on web platforms, browsers enforce CORS policies that can prevent API calls to your ToneListen server.
### CORS Proxy Solutions
#### Option 1: Use Provided CORS Proxy (Recommended)
The framework includes built-in CORS proxy support. Simply configure your proxy URL:
```typescript
const toneListener = new ToneListenReactNative({
clientId: '562',
apiKey: 'your-api-key',
baseURL: 'https://api.toneadmin.com',
corsProxyBaseURL: 'https://proxy.toneadmin.com', // Public CORS proxy
useProxyOnWeb: true, // Only use proxy on web platforms
});
```
#### Option 2: Set Up Your Own CORS Proxy
If you prefer to run your own proxy, you can use a simple Node.js server:
```javascript
// cors-proxy.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const cors = require('cors');
const app = express();
// Enable CORS for all routes
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-api-key']
}));
// Proxy requests to your API
app.use('/api', createProxyMiddleware({
target: 'https://api.toneadmin.com',
changeOrigin: true,
pathRewrite: {
'^/api': '' // Remove /api prefix
}
}));
app.listen(3001, () => {
console.log('CORS proxy running on port 3001');
});
```
#### Option 3: Fix CORS at API Level (Best Long-term Solution)
Configure your API server to include proper CORS headers:
```javascript
// Express.js example
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*'); // Or specific domains
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, x-api-key');
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
```
### Platform-Specific Behavior
- **iOS/Android**: No CORS proxy needed - direct API calls work
- **Web**: CORS proxy required unless API has proper CORS headers
- **React Native Web**: CORS proxy required
The framework automatically detects the platform and only uses the proxy when running on web platforms.
## Platform Support
- **iOS**: 11.0+
- **Android**: API level 21+
- **React Native**: 0.60+
- **Web**: Supported with CORS proxy
## Development
```bash
# Install dependencies
npm install
# Build the library
npm run build
# Run tests
npm test
# Lint code
npm run lint
```
## Troubleshooting
### Content Not Displaying
1. **Check API Configuration**: Ensure `clientId`, `apiKey`, and `baseURL` are correct
2. **Verify NotificationCenter**: Make sure you're using `NotificationCenter.getInstance()` (singleton)
3. **Check Content Type**: Ensure your API returns valid `actionType` and `actionUrl`
4. **Enable Debug Logging**: Set `debug: true` to see detailed logs
### Common Issues
- **Modal not appearing**: Check that `enableInAppContent: true` is set
- **API calls failing**: Verify your API key and client ID
- **Permissions denied**: Ensure microphone permissions are granted
- **Web compatibility**: Use CORS proxy for web applications
- **Playback sounds quiet on iOS**: Use `startPlaybackDecode(url)` before starting media, and `stopPlaybackDecode()` when stopping. Check logs for "AudioFrame source -> playback". Ensure you performed a clean Xcode rebuild after installing 1.0.61+.
### iOS App Transport Security (ATS) for media URLs
Playback decode downloads remote media with `URLSession`. Prefer HTTPS media URLs. If you must use HTTP or older TLS, add an ATS exception in your app `Info.plist` for the media domain.
```xml
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>your-media-domain.com</key>
<dict>
<key>NSIncludesSubdomains</key><true/>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key><true/>
<key>NSTemporaryExceptionMinimumTLSVersion</key><string>TLSv1.2</string>
</dict>
</dict>
<!-- Keep arbitrary loads off unless required -->
<key>NSAllowsArbitraryLoads</key><false/>
<key>NSAllowsArbitraryLoadsForMedia</key><false/>
<key>NSAllowsArbitraryLoadsInWebContent</key><false/>
<key>NSAllowsLocalNetworking</key><true/>
</dict>
```
## Based On
This React Native framework is based on the iOS ToneListen framework version 19, maintaining compatibility with the same frequency tables and detection algorithms while adapting to React Native's cross-platform constraints and capabilities.
## License
MIT License - see LICENSE file for details.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Support
For issues and questions, please use the [GitHub Issues](https://github.com/tonetelegenics/tonelisten-react-native/issues) page.