Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New PCM Audio player use a AudioWorkletProcessor to offload work #46

Merged
merged 18 commits into from
Sep 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
// Remove from file watching
"files.watcherExclude": {
"**/.fvm": true
}
},
"cSpell.words": [
"Dimens"
]
}
5 changes: 5 additions & 0 deletions lib/common/ui/constants/ta_colors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import 'package:flutter/material.dart';

class TAColors {
static const SETTING_PRIMARY_COLOR = Color(0xFFB71C1C);
}
31 changes: 23 additions & 8 deletions lib/feature/settings/widget/sound_settings.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tesla_android/common/ui/constants/ta_colors.dart';
import 'package:tesla_android/common/ui/constants/ta_dimens.dart';
import 'package:tesla_android/feature/settings/bloc/audio_configuration_cubit.dart';
import 'package:tesla_android/feature/settings/bloc/audio_configuration_state.dart';
Expand Down Expand Up @@ -32,6 +33,7 @@ class SoundSettings extends SettingsSection {
icon: Icons.volume_down,
title: 'Volume',
trailing: _audioStateSlider(context, cubit, state),
dense: false,
),
const Padding(
padding: EdgeInsets.all(TADimens.PADDING_S_VALUE),
Expand Down Expand Up @@ -69,14 +71,27 @@ class SoundSettings extends SettingsSection {
Widget _audioStateSlider(BuildContext context, AudioConfigurationCubit cubit,
AudioConfigurationState state) {
if (state is AudioConfigurationStateSettingsFetched) {
return Slider(
divisions: 10,
min: 0,
max: 100,
value: state.volume.toDouble(),
onChanged: (double value) {
cubit.setVolume(value.toInt());
},
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
"${state.volume} %",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: TAColors.SETTING_PRIMARY_COLOR),
),
Slider(
divisions: 15,
min: 0,
max: 150,
value: state.volume.toDouble(),
onChanged: (double value) {
cubit.setVolume(value.toInt());
},
label: state.volume.toString(),
),
],
);
} else if (state is AudioConfigurationStateSettingsUpdateInProgress ||
state is AudioConfigurationStateLoading) {
Expand Down
5 changes: 3 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:tesla_android/common/di/ta_locator.dart';
import 'package:tesla_android/common/navigation/ta_page_factory.dart';
import 'package:tesla_android/common/ui/constants/ta_colors.dart';
import 'package:tesla_android/common/utils/logger.dart';

Future<void> main() async {
Expand Down Expand Up @@ -56,12 +57,12 @@ class TeslaAndroid extends StatelessWidget with Logger {
theme: ThemeData(
brightness: Brightness.light,
useMaterial3: true,
colorSchemeSeed: Colors.red.shade900,
colorSchemeSeed: TAColors.SETTING_PRIMARY_COLOR,
fontFamily: 'Roboto'),
darkTheme: ThemeData(
brightness: Brightness.dark,
useMaterial3: true,
colorSchemeSeed: Colors.red.shade900,
colorSchemeSeed: TAColors.SETTING_PRIMARY_COLOR,
fontFamily: 'Roboto'),
themeMode: ThemeMode.system,
initialRoute: _pageFactory.initialRoute,
Expand Down
75 changes: 17 additions & 58 deletions web/android.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,29 @@
display: none;
}
</style>
<script src="pcmplayer.js"></script>

<script src="reconnecting-websocket.js"></script>
<script src="audioplayback.js"></script>
<script src="estimator.js"></script>
</head>

<body>
<img id="image" />
<canvas id="canvas"></canvas>
<script>
var audioPlayer;


var audioWebsocketUrl;
var displayWebsocketUrl;
var gpsWebsocketUrl;
var touchScreenWebsocketUrl;

var audioSocket;

var displaySocket;
var gpsSocket;
var touchScreenSocket;

var isAudioEnabled;
var audioVolume;


var displayRenderer;
var displayBinaryType;
Expand All @@ -57,6 +57,8 @@
var gpsEstimator;
var gpsRunning = false;



window.parent.addEventListener(
"message",
(event) => {
Expand All @@ -77,30 +79,24 @@
gpsWebsocketUrl = config.gpsWebsocketUrl;
touchScreenWebsocketUrl = config.touchScreenWebsocketUrl;

//check if isAudioEnabled is a boolean
if (typeof config.isAudioEnabled !== "boolean") {
if (config.isAudioEnabled === "false") {
config.isAudioEnabled = false;
} else {
config.isAudioEnabled = true;
}
}

isAudioEnabled = config.isAudioEnabled;
audioVolume = config.audioVolume;

setAudioEnabled(config.isAudioEnabled);
setAudioVolume(config.audioVolume);

displayRenderer = config.displayRenderer;
displayBinaryType = config.displayBinaryType;
displayWidth = config.displayWidth;
displayHeight = config.displayHeight;

if (document.readyState === "complete") {
if (!audioSocket) createAudioSocket(audioWebsocketUrl);

if (!displaySocket) createDisplaySocket(displayWebsocketUrl, displayRenderer, displayBinaryType);
if (!gpsSocket) createGpsSocket(gpsWebsocketUrl);
if (!touchScreenSocket) createTouchScreenSocket(touchScreenWebsocketUrl);
} else {
document.addEventListener("DOMContentLoaded", function () {
if (!audioSocket) createAudioSocket(audioWebsocketUrl);

if (!displaySocket) createDisplaySocket(displayWebsocketUrl, displayRenderer, displayBinaryType);
if (!gpsSocket) createGpsSocket(gpsWebsocketUrl);
if (!touchScreenSocket) createTouchScreenSocket(touchScreenWebsocketUrl);
Expand All @@ -110,31 +106,7 @@
false
);

function createAudioSocket(url) {
if (!isAudioEnabled) {
console.log("Audio: Disabled");
return;
}
audioSocket = new ReconnectingWebSocket(url, null, { binaryType: 'arraybuffer' });

audioSocket.onopen = () => {
log("Audio: Websocket connection established")
};

audioSocket.onclose = () => {
log("Audio: Websocket connection closed")
};

audioSocket.onerror = error => {
log("Audio: " + error)
};

audioSocket.onmessage = (event) => {
if (typeof audioPlayer != "undefined" && event.data instanceof ArrayBuffer) {
audioPlayer.feed(event.data);
}
};
}

function createDisplaySocket(url, renderer, binaryType) {
if (!displayRendererAdded) {
Expand Down Expand Up @@ -211,26 +183,13 @@
};
}


window.parent.addEventListener("click", function (e) {
if (!audioPlayer && isAudioEnabled) {
audioPlayer = new PCMPlayer({
inputCodec: "Float32",
channels: 2,
sampleRate: 48000,
flushTime: 200,
});

if (pcmPlayerNode == null && isAudioEnabled) {
startAudioPlayback();
}
});
function feedPlayer(data) {
if (typeof audioPlayer != "undefined" && isAudioEnabled) {
audioPlayer.feed(data);
}
}
function setPlayerVolume(volume) {
if (typeof audioPlayer != "undefined" && isAudioEnabled) {
audioPlayer.volume(volume);
}
}

async function checkPermissionAndStartUpdates() {
const permissionStatus = await navigator.permissions.query({ name: 'geolocation' });
Expand Down
81 changes: 81 additions & 0 deletions web/audioplayback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@

let audioContext = null
let audioSocket = null;
let pcmPlayerNode = null;
let gainNode = null;

let isAudioEnabled;
let audioVolume;


async function startAudioPlayback() {

console.log('Before AudioContext start');
audioContext = new AudioContext({ sampleRate: 48000, channels: 2, latencyHint: 'interactive' });

await audioContext.audioWorklet.addModule('pcmplayer-processor.js');
console.log("starting audioContext");

pcmPlayerNode = new AudioWorkletNode(audioContext, 'pcm-player-processor', { outputChannelCount: [2] });

if (audioVolume != null && audioVolume !== 1) {
let gainValue = audioVolume;
console.log("Gain value: " + gainValue);
gainNode = new GainNode(audioContext, { gain: gainValue });
pcmPlayerNode.connect(gainNode);
gainNode.connect(audioContext.destination);
} else {
pcmPlayerNode.connect(audioContext.destination);
}

if (!audioSocket) createAudioSocket(audioWebsocketUrl);

audioContext.resume();
console.log('after AudioContext resume');
}

function setAudioVolume(volume) {
console.log("Audio: Volume set to " + volume);
audioVolume = volume * 1.0;
}

function setAudioEnabled(enabled) {
//check if isAudioEnabled is a boolean
if (typeof enabled !== "boolean") {
if (enabled === "false") {
isAudioEnabled = false;
} else {
isAudioEnabled = true;
}
} else {
isAudioEnabled = enabled;
}
}


function createAudioSocket(url) {
if (!isAudioEnabled) {
console.log("Audio: Disabled");

} else {
console.log("Audio: Enabled");

audioSocket = new ReconnectingWebSocket(url, null, { binaryType: 'arraybuffer' });

audioSocket.onopen = () => {
log("Audio: Websocket connection established")
};

audioSocket.onclose = () => {
log("Audio: Websocket connection closed")
};

audioSocket.onerror = error => {
log("Audio: " + error)
};

audioSocket.onmessage = (event) => {
pcmPlayerNode.port.postMessage(event.data);
};
}
}
66 changes: 66 additions & 0 deletions web/pcmplayer-processor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
class PCMPlayerProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.samples = new Float32Array();
this.port.onmessage = this.handleMessage.bind(this);
}

handleMessage(event) {

const data = event.data;


// getting the formatted buffer.
const formattedData = this.getFormattedValue(data);
// truncate data if the buffer is 2x larger than the sampleRate
const maxSamples = this.sampleRate * 2;
const totalSamples = this.samples.length + data.length;

if (totalSamples > maxSamples) {
const samplesToDiscard = totalSamples - maxSamples;
this.samples = this.samples.subarray(samplesToDiscard);
}

// the code is starting to copy data from a buffer. It then creates a new Float32Array to store the copied data.
const tmp = new Float32Array(this.samples.length + formattedData.length);

tmp.set(this.samples, 0);
// Copy the new data passed in, starting from the historical buffer position
tmp.set(formattedData, this.samples.length);
// code is assigning a new buffer of data to the samples variable. The interval timer will then play the data from the samples variable.

this.samples = tmp;

}

getFormattedValue(data) {
if (data.constructor == ArrayBuffer) {
return new Float32Array(data);
} else {
return new Float32Array(data.buffer);
}
}

process(inputs, outputs, parameters) {
const output = outputs[0];
const bufferSize = output[0].length * 2;
if (this.samples.length < bufferSize) {
return true;
}

let theseSamples = this.samples.subarray(0, bufferSize);
//split the samples into left and right channels
let left = theseSamples.filter((_, i) => i % 2 === 0);
let right = theseSamples.filter((_, i) => i % 2 === 1);

output[0].set(left);
output[1].set(right);

this.samples = this.samples.subarray(bufferSize);

return true;
}

}

registerProcessor('pcm-player-processor', PCMPlayerProcessor);
Loading