Last active
June 22, 2024 17:43
-
-
Save willsmanley/62d8374edf4d9e90ebb2021db4941f32 to your computer and use it in GitHub Desktop.
Flutter Retell
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:typed_data'; | |
import 'package:record/record.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:web_socket_channel/web_socket_channel.dart'; | |
import 'package:http/http.dart' as http; | |
import 'dart:async'; | |
import 'package:mp_audio_stream/mp_audio_stream.dart'; | |
var apiToken = 'ADD API TOKEN HERE'; | |
class AudioTestFinalEcho extends StatefulWidget { | |
const AudioTestFinalEcho({super.key}); | |
@override | |
State<AudioTestFinalEcho> createState() => AudioTestFinalEchoState(); | |
} | |
class AudioTestFinalEchoState extends State<AudioTestFinalEcho> { | |
final record = AudioRecorder(); | |
late WebSocketChannel _channel; | |
final audioStream = getAudioStream(); | |
bool _isPlaying = false; | |
StreamSubscription? _audioStreamSubscription; | |
@override | |
void initState() { | |
super.initState(); | |
_registerCall(); | |
_initializeAudioStream(); | |
} | |
Future<void> _registerCall() async { | |
final response = await http.get( | |
Uri.parse('https://safe-gorge-65703-197182a5b4e2.herokuapp.com/call-id'), | |
headers: { | |
'X-API-TOKEN': apiToken, | |
'Content-Type': 'application/json', | |
}, | |
); | |
var callId = response.body.replaceAll(RegExp(r'^"|"$'), ''); | |
print('call id: $callId'); | |
_initializeWebsocketChannel(callId); | |
} | |
Future<void> _initializeAudioStream() async { | |
// 1 channel since its just speech, we don't care about L/R speakers | |
// todo: experiment with higher or lower sample rates. lower is less data, higher is better quality | |
// bufferMilliSec is the maximum amount of audio that can be played in one conversational turn | |
audioStream.init(channels: 1, sampleRate: 24000, bufferMilliSec: 25000); | |
} | |
Float32List _convertToFloat32(Uint8List data) { | |
// Assuming the input data is in PCM 16-bit signed little-endian format | |
final int len = data.length ~/ 2; | |
final Float32List float32Data = Float32List(len); | |
final ByteData byteData = ByteData.sublistView(data); | |
for (int i = 0; i < len; i++) { | |
float32Data[i] = byteData.getInt16(i * 2, Endian.little) / 32768.0; | |
} | |
return float32Data; | |
} | |
void _initializeWebsocketChannel(String callId) { | |
print('Initializing websocket channel'); | |
_channel = WebSocketChannel.connect( | |
Uri.parse('wss://api.retellai.com/audio-websocket/$callId')); | |
print('Channel initialized'); | |
_startPlayerStream(); | |
_startRecording(); | |
} | |
void _startPlayerStream() { | |
_audioStreamSubscription = _channel.stream.listen((data) { | |
print('data ${data.length}'); | |
if (data is List<int>) { | |
Float32List convertedData = _convertToFloat32(Uint8List.fromList(data)); | |
audioStream.push(convertedData); | |
if (!_isPlaying) { | |
print('playing'); | |
setState(() { | |
_isPlaying = true; | |
}); | |
audioStream.resume(); | |
} | |
} else { | |
// todo: clear the audioStream | |
// setState(() { | |
// _isPlaying = false; | |
// }); | |
} | |
}); | |
} | |
Future<void> _startRecording() async { | |
if (!await record.hasPermission()) { | |
print('Missing recording permission...'); | |
} | |
const RecordConfig config = RecordConfig( | |
encoder: AudioEncoder.pcm16bits, | |
sampleRate: 24000, | |
numChannels: 1, | |
); | |
Stream<Uint8List>? stream = await record.startStream(config); | |
stream.listen((chunk) async { | |
_channel.sink.add(chunk); | |
}); | |
} | |
@override | |
void dispose() { | |
record.dispose(); | |
_channel.sink.close(); | |
super.dispose(); | |
_audioStreamSubscription?.cancel(); | |
audioStream.uninit(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('Audio Test'), | |
), | |
body: const Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [], | |
), | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment