Skip to content

Commit

Permalink
Audio transmit works!
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Ketchel committed May 11, 2023
1 parent 16b5f9b commit 7eddc3c
Show file tree
Hide file tree
Showing 5 changed files with 317 additions and 4 deletions.
5 changes: 4 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_loggy/flutter_loggy.dart';
import 'package:loggy/loggy.dart';
import 'package:opus_dart/opus_dart.dart';
import 'package:opus_flutter/opus_flutter.dart' as opus_flutter;
import 'package:provider/provider.dart';

import '/providers/mumble_provider.dart';
import '/views/mumble_ui/mumble_ui.dart';

void main() {
void main() async {
Loggy.initLoggy(
filters: [
// BlacklistFilter([NetworkLoggy])
Expand All @@ -19,6 +21,7 @@ void main() {

// runApp(const MyApp());

initOpus(await opus_flutter.load());
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<MumbleProvider>(create: (_) => MumbleProvider())
Expand Down
155 changes: 155 additions & 0 deletions lib/providers/flutter_sound_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import 'dart:async';

import 'package:audio_session/audio_session.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:permission_handler/permission_handler.dart';

const int tSampleRate = 44000;
typedef _Fn = void Function();

class FlutterSoundProvider extends ChangeNotifier {
FlutterSoundPlayer? _mPlayer = FlutterSoundPlayer();
FlutterSoundRecorder? _mRecorder = FlutterSoundRecorder();
bool _mPlayerIsInited = false;
bool _mRecorderIsInited = false;
bool _mplaybackReady = false;
String? _mPath;
StreamSubscription? _mRecordingDataSubscription;

bool get isInitialized => _mPlayerIsInited && _mRecorderIsInited;

FlutterSoundProvider() {
// Be careful : openAudioSession return a Future.
// Do not access your FlutterSoundPlayer or FlutterSoundRecorder before the completion of the Future
// _mPlayer!.openPlayer().then((value) {
// _mPlayerIsInited = true;
// notifyListeners();
// });
// _openRecorder();
}

Future<void> initialize() async {
await _mPlayer!.openPlayer().then((value) {
_mPlayerIsInited = true;
notifyListeners();
});
await _openRecorder();
}

Future<void> _openRecorder() async {
var status = await Permission.microphone.request();
if (status != PermissionStatus.granted) {
throw RecordingPermissionException('Microphone permission not granted');
}
await _mRecorder!.openRecorder();

final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
avAudioSessionCategoryOptions:
AVAudioSessionCategoryOptions.allowBluetooth |
AVAudioSessionCategoryOptions.defaultToSpeaker,
avAudioSessionMode: AVAudioSessionMode.spokenAudio,
avAudioSessionRouteSharingPolicy:
AVAudioSessionRouteSharingPolicy.defaultPolicy,
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
flags: AndroidAudioFlags.none,
usage: AndroidAudioUsage.voiceCommunication,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
androidWillPauseWhenDucked: true,
));

_mRecorderIsInited = true;
notifyListeners();
}

@override
void dispose() {
// stopPlayer();
_mPlayer!.closePlayer();
_mPlayer = null;

// stopRecorder();
_mRecorder!.closeRecorder();
_mRecorder = null;
super.dispose();
}
//
// Future<void> stopRecorder() async {
// await _mRecorder!.stopRecorder();
// if (_mRecordingDataSubscription != null) {
// await _mRecordingDataSubscription!.cancel();
// _mRecordingDataSubscription = null;
// }
// _mplaybackReady = true;
// }

get startRecorder => _mRecorder!.startRecorder;
get stopRecorder => _mRecorder!.stopRecorder;
// ---------------------- Here is the code to record to a Stream ------------

// Future<void> record() async {
// assert(_mRecorderIsInited && _mPlayer!.isStopped);
// var sink = await createFile();
// var recordingDataController = StreamController<Food>();
// _mRecordingDataSubscription =
// recordingDataController.stream.listen((buffer) {
// if (buffer is FoodData) {
// sink.add(buffer.data!);
// }
// });
// await _mRecorder!.startRecorder(
// toStream: recordingDataController.sink,
// codec: Codec.pcm16,
// numChannels: 1,
// sampleRate: tSampleRate,
// );
// setState(() {});
// }

// _Fn? getRecorderFn() {
// if (!_mRecorderIsInited || !_mPlayer!.isStopped) {
// return null;
// }
// return _mRecorder!.isStopped
// ? record
// : () {
// stopRecorder().then((value) => notifyListeners());
// };
// }

void play() async {
assert(_mPlayerIsInited &&
_mplaybackReady &&
_mRecorder!.isStopped &&
_mPlayer!.isStopped);
await _mPlayer!.startPlayer(
fromURI: _mPath,
sampleRate: tSampleRate,
codec: Codec.pcm16,
numChannels: 1,
whenFinished: () {
notifyListeners();
}); // The readability of Dart is very special :-(
notifyListeners();
}

Future<void> stopPlayer() async {
await _mPlayer!.stopPlayer();
}

// _Fn? getPlaybackFn() {
// if (!_mPlayerIsInited || !_mplaybackReady || !_mRecorder!.isStopped) {
// return null;
// }
// return _mPlayer!.isStopped
// ? play
// : () {
// stopPlayer().then((value) => notifyListeners());
// };
// }
}
88 changes: 86 additions & 2 deletions lib/providers/mumble_provider.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import 'dart:async';
import 'dart:io';

import 'package:dumble/dumble.dart';
import 'package:dumble/dumble.dart' hide Permission;
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:loggy/loggy.dart';
import 'package:opus_dart/opus_dart.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:pttoc_test/providers/connection_options.dart';
import 'package:pttoc_test/providers/lib/mumble_log.dart';

import 'flutter_sound_provider.dart';

class MumbleProvider extends ChangeNotifier with NetworkLoggy {
MumbleClient? client;
late StreamSubscription<Uint8List> audioStream;

late FlutterSoundProvider flutterSound = FlutterSoundProvider();

MumbleLog mumbleLog = MumbleLog();
final pttChannel = MethodChannel('com.rgnets.pttoc/ptt');
final pttChannel = const MethodChannel('com.rgnets.pttoc/ptt');
bool transmitting = false;

Future<void> connect(
Expand All @@ -28,6 +36,10 @@ class MumbleProvider extends ChangeNotifier with NetworkLoggy {
password: password,
pingTimeout: const Duration(seconds: 20));

if (!flutterSound.isInitialized) {
await flutterSound.initialize();
}

return MumbleClient.connect(
options:
await createConnectionsOptionsWithCertificate(connectionOptions),
Expand Down Expand Up @@ -109,6 +121,76 @@ class MumbleProvider extends ChangeNotifier with NetworkLoggy {
mumbleLog.internal("Starting transmit");
transmitting = true;
notifyListeners();
if (await Permission.microphone.request().isGranted && client != null) {
// Permission is granted, continue with audio recording and transmission

// audioStream = await MicrophoneStream().audioStream;
// client!.audio.sendAudio(codec: codec) .microphoneStream(audioStream)

// Stream<Uint8List>? micStream = await MicStream.microphone(
// sampleRate: 48000,
// channelConfig: ChannelConfig.CHANNEL_IN_MONO,
// audioFormat: AudioFormat.ENCODING_PCM_8BIT,
// );
//

HapticFeedback.vibrate();
const int inputSampleRate = 16000;
const int frameTimeMs = 40; // use frames of 40ms
const FrameTime frameTime = FrameTime.ms40;
const int outputSampleRate = 48000;
const int channels = 1;

StreamOpusEncoder<int> encoder = StreamOpusEncoder.bytes(
frameTime: frameTime,
floatInput: false,
sampleRate: inputSampleRate,
channels: channels,
application: Application.voip);

AudioFrameSink audioOutput =
client!.audio.sendAudio(codec: AudioCodec.opus);
//
// micStream!
// .asyncMap((List<int> bytes) async {
// return bytes;
// })
// .transform(encoder)
// .map((Uint8List audioBytes) => AudioFrame.outgoing(frame: audioBytes))
// .pipe(audioOutput);

var recordingDataController = StreamController<Food>();
Stream<Uint8List>? audioStream = recordingDataController.stream
.map((event) => (event as FoodData).data!);
// var _mRecordingDataSubscription =
// recordingDataController.stream.listen((buffer) {
// if (buffer is FoodData) {
// // sink.add(buffer.data!);
// print(buffer.data!);
// }
// });

audioStream
.asyncMap((List<int> bytes) async {
return bytes;
})
.transform(encoder)
.map((Uint8List audioBytes) => AudioFrame.outgoing(frame: audioBytes))
.pipe(audioOutput);

flutterSound.startRecorder(
toStream: recordingDataController.sink,
codec: Codec.pcm16,
numChannels: channels,
sampleRate: inputSampleRate,
);

notifyListeners();
} else {
// Permission is not granted, show an error message or request again
transmitting = false;
notifyListeners();
}
// // await new Future.delayed(
// // const Duration(seconds: 5)); // Wait a few seconds before we start talking
// StreamOpusEncoder<int> encoder = StreamOpusEncoder.bytes(
Expand Down Expand Up @@ -136,6 +218,8 @@ class MumbleProvider extends ChangeNotifier with NetworkLoggy {

void stopTransmit() {
mumbleLog.internal("Stopping transmit");
flutterSound.stopRecorder();
HapticFeedback.vibrate();
transmitting = false;
notifyListeners();
}
Expand Down
Loading

0 comments on commit 7eddc3c

Please sign in to comment.