Skip to content

Instantly share code, notes, and snippets.

@willsmanley
Last active June 22, 2024 17:43
Show Gist options
  • Save willsmanley/62d8374edf4d9e90ebb2021db4941f32 to your computer and use it in GitHub Desktop.
Save willsmanley/62d8374edf4d9e90ebb2021db4941f32 to your computer and use it in GitHub Desktop.
Flutter Retell
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