Skip to content

Commit

Permalink
PcmPlayer: Leave transport in the flutter app, link with previous pla…
Browse files Browse the repository at this point in the history
…yer via JS Interop. It works better like this
  • Loading branch information
mikegapinski committed Apr 30, 2023
1 parent cbd021a commit 55e1fa2
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 138 deletions.
23 changes: 5 additions & 18 deletions lib/feature/audio/cubit/audio_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ class AudioCubit extends Cubit<AudioState> {

static const _sharedPreferencesKey = 'AudioCubit_isEnabled';

bool get _isAudioPlayerInitialised => _pcmAudioPlayer.isContextReady;

AudioCubit(
this._audioTransport,
this._pcmAudioPlayer,
Expand Down Expand Up @@ -49,23 +47,14 @@ class AudioCubit extends Cubit<AudioState> {
emit(state.copyWith(isEnabled: false));
}

void initialiseAudioPlayerIfNeeded() async {
if(_isAudioPlayerInitialised) {
return;
}
await _pcmAudioPlayer.initialize();
setVolume(state.volume);
}

void setVolume(double volume) {
emit(state.copyWith(volume: volume));
if (_isAudioPlayerInitialised) {
_pcmAudioPlayer.setVolume(volume);
}
_pcmAudioPlayer.setVolume(volume);
}

void _setInitialState() {
final shouldEnable = _sharedPreferences.getBool(_sharedPreferencesKey) ?? true;
final shouldEnable =
_sharedPreferences.getBool(_sharedPreferencesKey) ?? true;
if (shouldEnable) {
enableAudio();
} else {
Expand All @@ -84,14 +73,12 @@ class AudioCubit extends Cubit<AudioState> {
_audioTransport.maintainConnection();
_audioTransportPcmDataStreamSubscription =
_audioTransport.pcmDataSubject.listen((data) {
if (_isAudioPlayerInitialised) {
_feedAudioPlayer(data);
}
_feedAudioPlayer(data);
emit(state.copyWith(isWebSocketConnectionActive: true));
});
}

void _feedAudioPlayer(Uint8List pcmData) {
void _feedAudioPlayer(pcmData) {
_pcmAudioPlayer.feed(pcmData);
}
}
58 changes: 29 additions & 29 deletions lib/feature/audio/transport/audio_transport.dart
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import 'dart:html';
import 'dart:typed_data';

import 'package:flavor/flavor.dart';
import 'package:flutter/material.dart';
import 'package:injectable/injectable.dart';
import 'package:rxdart/rxdart.dart';
import 'package:web_socket_channel/html.dart';

@singleton
class AudioTransport {
final Flavor flavor;
final Uri webSocketUri;
final BehaviorSubject<Uint8List> pcmDataSubject = BehaviorSubject();
final BehaviorSubject<bool> connectionStateSubject = BehaviorSubject.seeded(false);
final BehaviorSubject pcmDataSubject = BehaviorSubject();
final BehaviorSubject<bool> connectionStateSubject =
BehaviorSubject.seeded(false);

static const String tag = "AudioTransport: ";

AudioTransport(this.flavor)
: webSocketUri = Uri.parse(flavor.getString(
"audioWebSocket",
)!);
"audioWebSocket",
)!);

HtmlWebSocketChannel? _webSocketChannel;
WebSocket? _webSocketChannel;

bool _keepConnectionAlive = false;

Expand All @@ -30,33 +30,33 @@ class AudioTransport {
}

void _connect() {
if(_keepConnectionAlive) {
_webSocketChannel = HtmlWebSocketChannel.connect(webSocketUri);
_webSocketChannel?.innerWebSocket.onOpen.listen((event) {
connectionStateSubject.add(true);
});
_webSocketChannel?.stream.listen(
(dynamic message) {
pcmDataSubject.add(message);
connectionStateSubject.add(true);
},
onDone: () {
debugPrint('${tag}channel closed');
connectionStateSubject.add(false);
_connect();
},
onError: (error) {
connectionStateSubject.add(false);
_connect();
},
);

if (_keepConnectionAlive) {
_webSocketChannel = WebSocket(flavor.getString(
"audioWebSocket",
)!)
..binaryType = 'arraybuffer';
_webSocketChannel?.onOpen.listen((event) {
connectionStateSubject.add(true);
});
_webSocketChannel?.onMessage.listen((MessageEvent e) {
ByteBuffer buf = e.data;
pcmDataSubject.add(buf.asByteData());
connectionStateSubject.add(true);
});
_webSocketChannel?.onClose.listen((event) {
connectionStateSubject.add(false);
_connect();
});
_webSocketChannel?.onError.listen((event) {
connectionStateSubject.add(false);
_connect();
});
}
}

void disconnect() {
_keepConnectionAlive = false;
_webSocketChannel?.sink.close();
_webSocketChannel?.close();
_webSocketChannel = null;
}
}
101 changes: 11 additions & 90 deletions lib/feature/audio/utils/pcm_player.dart
Original file line number Diff line number Diff line change
@@ -1,104 +1,25 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:injectable/injectable.dart';
import 'package:js/js.dart';
import 'package:js/js_util.dart';

typedef CreateAudioContextFunc = dynamic Function();
typedef FeedPlayerFunc = dynamic Function();

@JS('createAudioContext')
external CreateAudioContextFunc _createAudioContext();
@JS('feedPlayer')
external FeedPlayerFunc _feedPlayer(dynamic data);

@lazySingleton
class PcmAudioPlayer {
dynamic _audioContext;
dynamic _sourceNode;
dynamic _gainNode;
bool _isContextReady = false;
Float32List _samples = Float32List(0);

final int _sampleRate = 48000;
final int _numChannels = 2;
final int _bitsPerSample = 16;
final Duration _flushInterval = const Duration(milliseconds: 200);
dynamic _startTime;

// This method should be called after user interaction
Future<void> initialize() async {
_audioContext = _createAudioContext();
dynamic resumeMethod = getProperty(_audioContext, 'resume');
await promiseToFuture<void>(
callMethod(resumeMethod, 'call', [_audioContext]));
_isContextReady = true;

_gainNode = callMethod(_audioContext, 'createGain', []);
setProperty(getProperty(_gainNode, 'gain'), 'value', 1.0);
callMethod(
_gainNode, 'connect', [getProperty(_audioContext, 'destination')]);

_startTime = getProperty(_audioContext, 'currentTime');

Timer.periodic(_flushInterval, (Timer t) => _flush());
}

bool get isContextReady => _isContextReady;

void feed(Uint8List data) {
int bufferLength = data.length ~/ (_numChannels * (_bitsPerSample ~/ 8));
Float32List tempBuffer = Float32List(bufferLength);
int offset = 0;
typedef SetPlayerVolumeFunc = dynamic Function(dynamic volume);

for (int i = 0; i < bufferLength; ++i) {
int byteOffset = i * _numChannels * (_bitsPerSample ~/ 8) + offset;
int sample = data[byteOffset] + (data[byteOffset + 1] << 8);
tempBuffer[i] = (sample < 0x8000 ? sample : sample - 0x10000) / 0x8000;
}
@JS('setPlayerVolume')
external SetPlayerVolumeFunc _setPlayerVolume(dynamic volume);

Float32List newSamples = Float32List(_samples.length + tempBuffer.length);
newSamples.setAll(0, _samples);
newSamples.setAll(_samples.length, tempBuffer);
_samples = newSamples;
}

void _flush() {
if (_samples.isEmpty) return;
if (!_isContextReady) {
throw Exception(
'AudioContext not initialized or not ready. Call initialize() first.');
}

int bufferLength = _samples.length;
dynamic audioBuffer = callMethod(
_audioContext,
'createBuffer',
[1, bufferLength, _sampleRate],
);

Float32List targetBuffer = callMethod(audioBuffer, 'getChannelData', [0]);
for (int i = 0; i < bufferLength; ++i) {
targetBuffer[i] = _samples[i];
}

_sourceNode = callMethod(_audioContext, 'createBufferSource', []);
setProperty(_sourceNode, 'buffer', audioBuffer);
callMethod(_sourceNode, 'connect', [_gainNode]);

double duration = bufferLength / _sampleRate;
double startTime = getProperty(_audioContext, 'currentTime');
if (startTime < _startTime) {
startTime = _startTime;
}

callMethod(_sourceNode, 'start', [startTime, 0, duration]);

_startTime = startTime + duration;
_samples = Float32List(0);
@lazySingleton
class PcmAudioPlayer {
void feed(data) {
_feedPlayer(data);
}

void setVolume(double volume) {
if (_gainNode != null) {
setProperty(getProperty(_gainNode, 'gain'), 'value', volume);
}
_setPlayerVolume(volume);
}
}
1 change: 0 additions & 1 deletion lib/feature/touchscreen/touchscreen_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ class TouchScreenView extends StatelessWidget {
required BoxConstraints constraints,
required dynamic event,
}) {
audioCubit.initialiseAudioPlayerIfNeeded();
if(event is PointerDownEvent) {
cubit.handlePointerDownEvent(event, constraints);
} else if(event is PointerMoveEvent) {
Expand Down
25 changes: 25 additions & 0 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,30 @@
});
});
</script>
<script src="pcmplayer.js"></script>
<script>
var player;
window.addEventListener('click', function(e) {
if (typeof player == "undefined") {
player = new PCMPlayer({
inputCodec: 'Float32',
channels: 2,
sampleRate: 48000,
flushTime: 150,
});
}
});
function feedPlayer(data) {
if (typeof player != "undefined") {
player.feed(data);
}
};
function setPlayerVolume(volume) {
if (typeof player != "undefined") {
player.volume(volume);
}
};
</script>

</body>
</html>
Loading

0 comments on commit 55e1fa2

Please sign in to comment.