From 37d99b4848ed33550a280a53f34f87b54d4ab378 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Sun, 16 Aug 2026 17:02:32 +0300 Subject: [PATCH] Added iLBC audio codec --- app/src/main/cpp/CMakeLists.txt | 5 + .../kotlin/com/tutpro/baresip/AudioScreen.kt | 390 ++++++------------ .../com/tutpro/baresip/AudioViewModel.kt | 194 +++++++++ .../main/kotlin/com/tutpro/baresip/Config.kt | 13 +- .../com/tutpro/baresip/SettingsScreen.kt | 88 ++-- .../com/tutpro/baresip/SettingsViewModel.kt | 14 +- app/src/main/res/values-fi/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 8 files changed, 396 insertions(+), 314 deletions(-) create mode 100644 app/src/main/kotlin/com/tutpro/baresip/AudioViewModel.kt diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index b01adc80..7c258ff2 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -34,6 +34,10 @@ add_library(lib_g729 STATIC IMPORTED) set_target_properties(lib_g729 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/g729/lib/${ANDROID_ABI}/libbcg729.a) +add_library(lib_ilbc STATIC IMPORTED) +set_target_properties(lib_ilbc PROPERTIES IMPORTED_LOCATION + ${distribution_DIR}/ilbc/lib/${ANDROID_ABI}/libilbc.a) + add_library(lib_codec2 STATIC IMPORTED) set_target_properties(lib_codec2 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/codec2/lib/${ANDROID_ABI}/libcodec2.a) @@ -83,6 +87,7 @@ target_link_libraries( lib_g722 lib_g722_1 lib_g729 + lib_ilbc lib_codec2 lib_amrnb lib_amrwb diff --git a/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt index d134ab0d..bc935784 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt @@ -1,6 +1,5 @@ package com.tutpro.baresip -import android.content.Context import android.content.Intent import android.media.RingtoneManager import android.net.Uri @@ -44,11 +43,11 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -58,6 +57,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.net.toUri +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable @@ -71,10 +71,12 @@ enum class Result { fun NavGraphBuilder.audioScreenRoute(navController: NavController) { composable("audio") { val ctx = LocalContext.current + val audioViewModel = viewModel() AudioScreen( + viewModel = audioViewModel, onBack = { navController.navigateUp() }, checkOnClick = { - when (checkOnClick(ctx)) { + when (audioViewModel.saveSettings(ctx)) { Result.OK -> navController.navigateUp() Result.RESTART -> { navController.previousBackStackEntry @@ -91,7 +93,16 @@ fun NavGraphBuilder.audioScreenRoute(navController: NavController) { @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun AudioScreen(onBack: () -> Unit, checkOnClick: () -> Unit) { +private fun AudioScreen( + viewModel: AudioViewModel, + onBack: () -> Unit, + checkOnClick: () -> Unit +) { + val ctx = LocalContext.current + LaunchedEffect(Unit) { + viewModel.loadSettings(ctx) + } + Scaffold( modifier = Modifier.fillMaxSize().imePadding(), containerColor = MaterialTheme.colorScheme.background, @@ -126,42 +137,16 @@ private fun AudioScreen(onBack: () -> Unit, checkOnClick: () -> Unit) { } } ) { - contentPadding -> AudioContent(contentPadding) + contentPadding -> AudioContent(viewModel, contentPadding) } } -private var newCallVolume = BaresipService.callVolume -private var oldMicGain = "" -private var newMicGain = "" -private var oldSpeakerPhone = BaresipService.speakerPhone -private var newSpeakerPhone = oldSpeakerPhone -private var oldAudioModules = ArrayList() -private var newAudioModules = mutableMapOf() -private var oldOpusBitrate = "" -private var newOpusBitrate = oldOpusBitrate -private var oldOpusPacketLoss = "" -private var newOpusPacketLoss = oldOpusPacketLoss -private var newAudioDelay = BaresipService.audioDelay.toString() -private var newToneCountry = BaresipService.toneCountry -private var newRingtoneUri = "" - -private var save = false - private val alertTitle = mutableStateOf("") private val alertMessage = mutableStateOf("") private val showAlert = mutableStateOf(false) @Composable -private fun AudioContent(contentPadding: PaddingValues) { - - oldSpeakerPhone = Config.variable("speaker_phone") == "yes" - newSpeakerPhone = oldSpeakerPhone - oldAudioModules = Config.variables("module") - for (module in Config.audioModules) - newAudioModules[module] = oldAudioModules.contains("${module}.so") - oldOpusBitrate = Config.variable("opus_bitrate") - oldOpusPacketLoss = Config.variable("opus_packet_loss") - if (!BaresipService.agcAvailable) oldMicGain = Config.variable("augain") +private fun AudioContent(viewModel: AudioViewModel, contentPadding: PaddingValues) { if (showAlert.value) AlertDialog( @@ -177,37 +162,30 @@ private fun AudioContent(contentPadding: PaddingValues) { modifier = Modifier .fillMaxWidth() .padding(contentPadding) - .padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 4.dp) + .padding(top = 16.dp, bottom = 8.dp, start = 16.dp, end = 4.dp) .verticalScrollbar(scrollState) .verticalScroll(state = scrollState), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - Ringtone() - ToneCountry() - SpeakerPhone() - CallVolume() - MicGain() - AudioModules() - OpusBitRate() - OpusPacketLoss() - AudioDelay() + Ringtone(viewModel) + ToneCountry(viewModel) + SpeakerPhone(viewModel) + CallVolume(viewModel) + MicGain(viewModel) + AudioModules(viewModel) + OpusBitRate(viewModel) + OpusPacketLoss(viewModel) + IlbcMode(viewModel) + AudioDelay(viewModel) } } @Composable -private fun Ringtone() { +private fun Ringtone(viewModel: AudioViewModel) { val ringToneTitle = stringResource(R.string.ringtone) val selectRingToneMessage = stringResource(R.string.select_ringtone) - val ctx = LocalContext.current - var ringtoneUri by remember { - mutableStateOf( - if (Preferences(ctx).ringtoneUri == "") - RingtoneManager.getActualDefaultRingtoneUri(ctx, RingtoneManager.TYPE_RINGTONE).toString() - else - Preferences(ctx).ringtoneUri!! - ) - } - newRingtoneUri = ringtoneUri + val ringtoneUri by viewModel.ringtoneUri.collectAsState() + val launcher = rememberLauncherForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result: ActivityResult -> @@ -218,8 +196,7 @@ private fun Ringtone() { @Suppress("DEPRECATION") result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) if (uri != null) { - ringtoneUri = uri.toString() - newRingtoneUri = ringtoneUri + viewModel.ringtoneUri.value = uri.toString() } } } @@ -256,7 +233,7 @@ private fun Ringtone() { } @Composable -private fun ToneCountry() { +private fun ToneCountry(viewModel: AudioViewModel) { Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, @@ -275,20 +252,19 @@ private fun ToneCountry() { }, fontSize = 18.sp ) + val currentToneCountry by viewModel.toneCountry.collectAsState() val isDropDownExpanded = remember { mutableStateOf(false) } val countryNames = arrayListOf("BG", "BR", "DE", "CZ", "ES", "FI", "FR", "GB", "JP", "NO", "NZ", "SE", "RU", "US") val countryValues = arrayListOf("bg", "br", "de", "cz", "es", "fi", "fr", "uk", "jp", "no", "nz", "se", "ru", "us") - val itemPosition = remember { - val index = countryValues.indexOf(BaresipService.toneCountry) - mutableIntStateOf(if (index != -1) index else countryValues.indexOf("us")) - } + val itemPosition = countryValues.indexOf(currentToneCountry).let { if (it != -1) it else countryValues.indexOf("us") } + Box { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { isDropDownExpanded.value = true } ) { - Text(text = countryNames[itemPosition.intValue]) + Text(text = countryNames[itemPosition]) Icon( imageVector = Icons.Filled.ArrowDropDown, contentDescription = null, @@ -304,8 +280,7 @@ private fun ToneCountry() { text = { Text(text = name) }, onClick = { isDropDownExpanded.value = false - itemPosition.intValue = index - newToneCountry = countryValues[index] + viewModel.toneCountry.value = countryValues[index] } ) if (index < 10) @@ -317,7 +292,7 @@ private fun ToneCountry() { } @Composable -private fun SpeakerPhone() { +private fun SpeakerPhone(viewModel: AudioViewModel) { Row( Modifier.fillMaxWidth().padding(end=10.dp), verticalAlignment = Alignment.CenterVertically, @@ -335,19 +310,18 @@ private fun SpeakerPhone() { }, fontSize = 18.sp ) - var speakerPhone by remember { mutableStateOf(oldSpeakerPhone) } + val speakerPhone by viewModel.speakerPhone.collectAsState() Switch( checked = speakerPhone, onCheckedChange = { - speakerPhone = it - newSpeakerPhone = speakerPhone + viewModel.speakerPhone.value = it } ) } } @Composable -private fun CallVolume() { +private fun CallVolume(viewModel: AudioViewModel) { Row( Modifier.fillMaxWidth().padding(end=10.dp), verticalAlignment = Alignment.CenterVertically, @@ -365,20 +339,19 @@ private fun CallVolume() { }, fontSize = 18.sp ) + val currentCallVolume by viewModel.callVolume.collectAsState() val isDropDownExpanded = remember { mutableStateOf(false) } val volNames = listOf("--", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10") val volValues = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) - val itemPosition = remember { - val index = volValues.indexOf(BaresipService.callVolume) - mutableIntStateOf(if (index != -1) index else 0) - } + val itemPosition = volValues.indexOf(currentCallVolume).let { if (it != -1) it else 0 } + Box { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { isDropDownExpanded.value = true } ) { - Text(text = volNames[itemPosition.intValue]) + Text(text = volNames[itemPosition]) Icon( imageVector = Icons.Filled.ArrowDropDown, contentDescription = null, @@ -394,8 +367,7 @@ private fun CallVolume() { text = { Text(text = vol) }, onClick = { isDropDownExpanded.value = false - itemPosition.intValue = index - newCallVolume = volValues[index] + viewModel.callVolume.value = volValues[index] }) if (index < 10) HorizontalDivider(thickness = 1.dp) @@ -406,7 +378,7 @@ private fun CallVolume() { } @Composable -private fun MicGain() { +private fun MicGain(viewModel: AudioViewModel) { if (!BaresipService.agcAvailable) Row( Modifier.fillMaxWidth().padding(end = 10.dp), @@ -415,15 +387,11 @@ private fun MicGain() { ) { val microphoneGainTitle = stringResource(R.string.microphone_gain) val microphoneGainHelp = stringResource(R.string.microphone_gain_help) - var micGain by remember { mutableStateOf(oldMicGain) } - newMicGain = micGain + val micGain by viewModel.micGain.collectAsState() OutlinedTextField( value = micGain, placeholder = { Text(microphoneGainTitle) }, - onValueChange = { - micGain = it - newMicGain = micGain - }, + onValueChange = { viewModel.micGain.value = it }, modifier = Modifier .fillMaxWidth() .clickable { @@ -439,7 +407,8 @@ private fun MicGain() { } @Composable -private fun AudioModules() { +private fun AudioModules(viewModel: AudioViewModel) { + val audioModules by viewModel.audioModules.collectAsState() Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start, @@ -454,27 +423,29 @@ private fun AudioModules() { alertMessage.value = audioModulesHelp showAlert.value = true }) - for (module in Config.audioModules) + for (module in Config.audioModules) { Row(horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 18.dp, end = 10.dp) ) { Text(text = String.format(stringResource(R.string.bullet_item), module), fontSize = 18.sp) Spacer(modifier = Modifier.weight(1f)) - var checked by remember { mutableStateOf(oldAudioModules.contains("${module}.so")) } + val checked = audioModules[module] ?: false Switch( checked = checked, onCheckedChange = { - checked = it - newAudioModules[module] = checked + val newMap = audioModules.toMutableMap() + newMap[module] = it + viewModel.audioModules.value = newMap } ) } + } } } @Composable -private fun OpusBitRate() { +private fun OpusBitRate(viewModel: AudioViewModel) { Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, @@ -482,15 +453,11 @@ private fun OpusBitRate() { ) { val opusBitRateTitle = stringResource(R.string.opus_bit_rate) val opusBitRateHelp = stringResource(R.string.opus_bit_rate_help) - var opusBitrate by remember { mutableStateOf(oldOpusBitrate) } - newOpusBitrate = opusBitrate + val opusBitrate by viewModel.opusBitrate.collectAsState() OutlinedTextField( value = opusBitrate, placeholder = { Text(opusBitRateTitle) }, - onValueChange = { - opusBitrate = it - newOpusBitrate = opusBitrate - }, + onValueChange = { viewModel.opusBitrate.value = it }, modifier = Modifier .fillMaxWidth() .clickable { @@ -506,7 +473,7 @@ private fun OpusBitRate() { } @Composable -private fun OpusPacketLoss() { +private fun OpusPacketLoss(viewModel: AudioViewModel) { Row( Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp), verticalAlignment = Alignment.CenterVertically, @@ -514,15 +481,11 @@ private fun OpusPacketLoss() { ) { val opusPacketLossTitle = stringResource(R.string.opus_packet_loss) val opusPacketLossHelp = stringResource(R.string.opus_packet_loss_help) - var opusPacketLoss by remember { mutableStateOf(oldOpusPacketLoss) } - newOpusPacketLoss = opusPacketLoss + val opusPacketLoss by viewModel.opusPacketLoss.collectAsState() OutlinedTextField( value = opusPacketLoss, placeholder = { Text(opusPacketLossTitle) }, - onValueChange = { - opusPacketLoss = it - newOpusPacketLoss = opusPacketLoss - }, + onValueChange = { viewModel.opusPacketLoss.value = it }, modifier = Modifier .fillMaxWidth() .clickable { @@ -538,7 +501,66 @@ private fun OpusPacketLoss() { } @Composable -private fun AudioDelay() { +private fun IlbcMode(viewModel: AudioViewModel) { + Row( + Modifier.fillMaxWidth().padding(end = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + val ilbcModeTitle = stringResource(R.string.ilbc_mode) + val ilbcModeHelp = stringResource(R.string.ilbc_mode_help) + Text( + text = ilbcModeTitle, + modifier = Modifier + .weight(1f) + .clickable { + alertTitle.value = ilbcModeTitle + alertMessage.value = ilbcModeHelp + showAlert.value = true + }, + fontSize = 18.sp + ) + val currentIlbcMode by viewModel.ilbcMode.collectAsState() + val isDropDownExpanded = remember { mutableStateOf(false) } + val modeNames = listOf("20ms", "30ms") + val modeValues = listOf("20", "30") + val itemPosition = modeValues.indexOf(currentIlbcMode).let { if (it != -1) it else 1 } + + Box { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable { isDropDownExpanded.value = true } + ) { + Text(text = modeNames[itemPosition]) + Icon( + imageVector = Icons.Filled.ArrowDropDown, + contentDescription = null, + modifier = Modifier.size(36.dp) + ) + } + DropdownMenu( + expanded = isDropDownExpanded.value, + onDismissRequest = { isDropDownExpanded.value = false } + ) { + modeNames.forEachIndexed { index, name -> + DropdownMenuItem( + text = { Text(text = name) }, + onClick = { + isDropDownExpanded.value = false + viewModel.ilbcMode.value = modeValues[index] + } + ) + if (index < modeNames.size - 1) + HorizontalDivider(thickness = 1.dp) + } + } + } + } +} + +@Composable +private fun AudioDelay(viewModel: AudioViewModel) { Row( Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp, bottom = 12.dp), verticalAlignment = Alignment.CenterVertically, @@ -546,15 +568,11 @@ private fun AudioDelay() { ) { val audioDelayTitle = stringResource(R.string.audio_delay) val audioDelayHelp = stringResource(R.string.audio_delay_help) - var audioDelay by remember { mutableStateOf(BaresipService.audioDelay.toString()) } - newAudioDelay = audioDelay + val currentAudioDelay by viewModel.audioDelay.collectAsState() OutlinedTextField( - value = audioDelay, + value = currentAudioDelay, placeholder = { Text(audioDelayTitle) }, - onValueChange = { - audioDelay = it - newAudioDelay = audioDelay - }, + onValueChange = { viewModel.audioDelay.value = it }, modifier = Modifier .fillMaxWidth() .clickable { @@ -568,155 +586,3 @@ private fun AudioDelay() { ) } } - -private fun checkOnClick(ctx: Context): Result { - - var restart = false - save = false - - if (Preferences(ctx).ringtoneUri != newRingtoneUri) { - Preferences(ctx).ringtoneUri = newRingtoneUri - BaresipService.rt = RingtoneManager.getRingtone(ctx, newRingtoneUri.toUri()) - } - - if (BaresipService.toneCountry != newToneCountry) { - BaresipService.toneCountry = newToneCountry - Config.replaceVariable("tone_country", newToneCountry) - save = true - } - - if (BaresipService.callVolume != newCallVolume) { - BaresipService.callVolume = newCallVolume - Config.replaceVariable("call_volume", newCallVolume.toString()) - save = true - } - - if (!BaresipService.agcAvailable) { - var gain = newMicGain.trim() - if (!gain.contains(".")) gain = "$gain.0" - if (gain != oldMicGain) { - if (!checkMicGain(gain)) { - alertTitle.value = ctx.getString(R.string.notice) - alertMessage.value = "${ctx.getString(R.string.invalid_microphone_gain)}: $gain." - showAlert.value = true - return Result.ERROR - } - if (gain == "1.0") { - Api.module_unload("augain") - Config.removeVariableValue("module", "augain.so") - Config.replaceVariable("augain", "1.0") - } - else { - if (oldMicGain == "1.0") { - if (Api.module_load("augain") != 0) { - alertTitle.value = ctx.getString(R.string.error) - alertMessage.value = ctx.getString(R.string.failed_to_load_module) + ": augain.so" - showAlert.value = true - return Result.ERROR - } - Config.addVariable("module", "augain.so") - } - Config.replaceVariable("augain", gain) - Api.cmd_exec("augain $gain") - } - save = true - } - } - - if (newSpeakerPhone != oldSpeakerPhone) { - Config.replaceVariable("speaker_phone", if (newSpeakerPhone) "yes" else "no") - BaresipService.speakerPhoneAuto = newSpeakerPhone - save = true - } - - for (module in Config.audioModules) { - if (newAudioModules[module] != null) { - if (newAudioModules[module]!!) { - if (!oldAudioModules.contains("${module}.so")) { - if (Api.module_load("${module}.so") != 0) { - alertTitle.value = ctx.getString(R.string.error) - alertMessage.value = "${ctx.getString(R.string.failed_to_load_module)}: ${module}.so" - showAlert.value = true - return Result.ERROR - } - Config.addVariable("module", "${module}.so") - save = true - } - } - else if (oldAudioModules.contains("${module}.so")) { - Api.module_unload("${module}.so") - Config.removeVariableValue("module", "${module}.so") - for (ua in BaresipService.uas.value) - ua.account.removeAudioCodecs(module) - Account.saveAccounts() - save = true - } - } - } - - if (newOpusBitrate != oldOpusBitrate) { - if (!checkOpusBitRate(newOpusBitrate)) { - alertTitle.value = ctx.getString(R.string.notice) - alertMessage.value = "${ctx.getString(R.string.invalid_opus_bitrate)}: $newOpusBitrate." - showAlert.value = true - return Result.ERROR - } - Config.replaceVariable("opus_bitrate", newOpusBitrate) - restart = true - save = true - } - - if (newOpusPacketLoss != oldOpusPacketLoss) { - if (!checkOpusPacketLoss(newOpusPacketLoss)) { - alertTitle.value = ctx.getString(R.string.notice) - alertMessage.value = "${ctx.getString(R.string.invalid_opus_packet_loss)}: $newOpusPacketLoss" - showAlert.value = true - return Result.ERROR - } - Config.replaceVariable("opus_packet_loss", newOpusPacketLoss) - restart = true - save = true - } - - val audioDelay = newAudioDelay.trim() - if (audioDelay != BaresipService.audioDelay.toString()) { - if (!checkAudioDelay(audioDelay)) { - alertTitle.value = ctx.getString(R.string.notice) - alertMessage.value = String.format(ctx.getString(R.string.invalid_audio_delay), audioDelay) - showAlert.value = true - return Result.ERROR - } - Config.replaceVariable("audio_delay", audioDelay) - BaresipService.audioDelay = audioDelay.toLong() - save = true - } - - if (save) Config.save() - - return if (restart) Result.RESTART else Result.OK -} - -private fun checkMicGain(micGain: String): Boolean { - val number = - try { - micGain.toDouble() - } catch (_: NumberFormatException) { - return false - } - return number >= 1.0 -} - -private fun checkOpusBitRate(opusBitRate: String): Boolean { - val number = opusBitRate.toIntOrNull() ?: return false - return (number >= 6000) && (number <= 510000) -} - -private fun checkOpusPacketLoss(opusPacketLoss: String): Boolean { - val number = opusPacketLoss.toIntOrNull() ?: return false - return (number >= 0) && (number <= 100) -} - -private fun checkAudioDelay(audioDelay: String): Boolean { - val number = audioDelay.toIntOrNull() ?: return false - return (number >= 100) && (number <= 3000) -} diff --git a/app/src/main/kotlin/com/tutpro/baresip/AudioViewModel.kt b/app/src/main/kotlin/com/tutpro/baresip/AudioViewModel.kt new file mode 100644 index 00000000..280c0666 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/AudioViewModel.kt @@ -0,0 +1,194 @@ +package com.tutpro.baresip + +import android.content.Context +import android.media.RingtoneManager +import androidx.core.net.toUri +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow + +class AudioViewModel : ViewModel() { + + val speakerPhone = MutableStateFlow(false) + val callVolume = MutableStateFlow(0) + val micGain = MutableStateFlow("") + val audioModules = MutableStateFlow(mutableMapOf()) + val opusBitrate = MutableStateFlow("") + val opusPacketLoss = MutableStateFlow("") + val ilbcMode = MutableStateFlow("") + val audioDelay = MutableStateFlow("") + val toneCountry = MutableStateFlow("") + val ringtoneUri = MutableStateFlow("") + + private var isLoaded = false + var oldSpeakerPhone = false + var oldCallVolume = 0 + var oldMicGain = "" + var oldAudioModules = ArrayList() + var oldOpusBitrate = "" + var oldOpusPacketLoss = "" + var oldIlbcMode = "" + var oldAudioDelay = "" + var oldToneCountry = "" + var oldRingtoneUri = "" + + fun loadSettings(ctx: Context) { + if (isLoaded || !Config.isInitialized()) return else isLoaded = true + + oldSpeakerPhone = Config.variable("speaker_phone") == "yes" + speakerPhone.value = oldSpeakerPhone + + oldCallVolume = BaresipService.callVolume + callVolume.value = oldCallVolume + + if (!BaresipService.agcAvailable) { + oldMicGain = Config.variable("augain") + micGain.value = oldMicGain + } + + oldAudioModules = Config.variables("module") + val modulesMap = mutableMapOf() + for (module in Config.audioModules) + modulesMap[module] = oldAudioModules.contains("${module}.so") + + audioModules.value = modulesMap + + oldOpusBitrate = Config.variable("opus_bitrate") + opusBitrate.value = oldOpusBitrate + + oldOpusPacketLoss = Config.variable("opus_packet_loss") + opusPacketLoss.value = oldOpusPacketLoss + + oldIlbcMode = Config.variable("ilbc_mode") + ilbcMode.value = oldIlbcMode + + oldAudioDelay = Config.variable("audio_delay") + if (oldAudioDelay == "") oldAudioDelay = BaresipService.audioDelay.toString() + audioDelay.value = oldAudioDelay + + oldToneCountry = BaresipService.toneCountry + toneCountry.value = oldToneCountry + + oldRingtoneUri = Preferences(ctx).ringtoneUri ?: "" + ringtoneUri.value = oldRingtoneUri + } + + fun saveSettings(ctx: Context): Result { + var restart = false + var save = false + + if (Preferences(ctx).ringtoneUri != ringtoneUri.value) { + Preferences(ctx).ringtoneUri = ringtoneUri.value + BaresipService.rt = RingtoneManager.getRingtone(ctx, ringtoneUri.value.toUri()) + } + + if (BaresipService.toneCountry != toneCountry.value) { + BaresipService.toneCountry = toneCountry.value + Config.replaceVariable("tone_country", toneCountry.value) + save = true + } + + if (BaresipService.callVolume != callVolume.value) { + BaresipService.callVolume = callVolume.value + Config.replaceVariable("call_volume", callVolume.value.toString()) + save = true + } + + if (!BaresipService.agcAvailable) { + var gain = micGain.value.trim() + if (gain.isNotEmpty() && !gain.contains(".")) gain = "$gain.0" + if (gain != oldMicGain) { + if (!checkMicGain(gain)) return Result.ERROR + if (gain == "1.0") { + Api.module_unload("augain") + Config.removeVariableValue("module", "augain.so") + Config.replaceVariable("augain", "1.0") + } + else { + if (oldMicGain == "1.0") { + if (Api.module_load("augain") != 0) return Result.ERROR + Config.addVariable("module", "augain.so") + } + Config.replaceVariable("augain", gain) + Api.cmd_exec("augain $gain") + } + save = true + } + } + + if (speakerPhone.value != oldSpeakerPhone) { + Config.replaceVariable("speaker_phone", if (speakerPhone.value) "yes" else "no") + BaresipService.speakerPhoneAuto = speakerPhone.value + save = true + } + + for (module in Config.audioModules) { + val enabled = audioModules.value[module] ?: false + if (enabled != oldAudioModules.contains("${module}.so")) { + if (enabled) { + if (Api.module_load("${module}.so") != 0) return Result.ERROR + Config.addVariable("module", "${module}.so") + } + else { + Api.module_unload("${module}.so") + Config.removeVariableValue("module", "${module}.so") + for (ua in BaresipService.uas.value) + ua.account.removeAudioCodecs(module) + Account.saveAccounts() + } + save = true + } + } + + if (opusBitrate.value != oldOpusBitrate) { + if (!checkOpusBitRate(opusBitrate.value)) return Result.ERROR + Config.replaceVariable("opus_bitrate", opusBitrate.value) + restart = true + save = true + } + + if (opusPacketLoss.value != oldOpusPacketLoss) { + if (!checkOpusPacketLoss(opusPacketLoss.value)) return Result.ERROR + Config.replaceVariable("opus_packet_loss", opusPacketLoss.value) + restart = true + save = true + } + + if (ilbcMode.value != oldIlbcMode) { + Config.replaceVariable("ilbc_mode", ilbcMode.value) + restart = true + save = true + } + + val delay = audioDelay.value.trim() + if (delay != oldAudioDelay) { + if (!checkAudioDelay(delay)) return Result.ERROR + Config.replaceVariable("audio_delay", delay) + BaresipService.audioDelay = delay.toLong() + save = true + } + + if (save) Config.save() + + return if (restart) Result.RESTART else Result.OK + } + + private fun checkMicGain(micGain: String): Boolean { + val number = micGain.toDoubleOrNull() ?: return false + return number >= 1.0 + } + + private fun checkOpusBitRate(opusBitRate: String): Boolean { + val number = opusBitRate.toIntOrNull() ?: return false + return (number >= 6000) && (number <= 510000) + } + + private fun checkOpusPacketLoss(opusPacketLoss: String): Boolean { + val number = opusPacketLoss.toIntOrNull() ?: return false + return (number >= 0) && (number <= 100) + } + + private fun checkAudioDelay(audioDelay: String): Boolean { + val number = audioDelay.toIntOrNull() ?: return false + return (number >= 100) && (number <= 3000) + } +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/Config.kt b/app/src/main/kotlin/com/tutpro/baresip/Config.kt index ad9658b8..5fb89fd3 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Config.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Config.kt @@ -10,7 +10,7 @@ import java.nio.charset.StandardCharsets object Config { private val configPath = BaresipService.filesPath + "/config" - val audioModules = listOf("opus", "amr", "libg722", "g7221", "g729", "codec2", "g711") + val audioModules = listOf("opus", "amr", "ilbc", "libg722", "g7221", "g729", "codec2", "g711") private lateinit var config: String private lateinit var previousConfig: String private lateinit var previousLines: List @@ -213,8 +213,9 @@ object Config { val previousModules = previousVariables("module") for (module in audioModules) if ("${module}.so" in previousModules || - (module == "libg722" && "g722.so" in previousModules)) - config = "${config}module ${module}.so\n" + (module == "libg722" && "g722.so" in previousModules) || + module == "ilbc") + config = "${config}module ${module}.so\n" Utils.aecAgcNsCheck() @@ -236,6 +237,12 @@ object Config { else "${config}opus_packet_loss $opusPacketLoss\n" + val ilbcMode = previousVariable("ilbc_mode") + config = if (ilbcMode == "") + "${config}ilbc_mode 30\n" + else + "${config}ilbc_mode $ilbcMode\n" + val audioDelay = previousVariable("audio_delay") if (audioDelay != "") { config = "${config}audio_delay $audioDelay\n" diff --git a/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt index a880c228..42c47ee1 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt @@ -89,9 +89,7 @@ import java.io.FileInputStream import java.util.Locale import androidx.core.net.toUri -private var restart = false private val showRestartDialog = mutableStateOf(false) -private var save = false fun NavGraphBuilder.settingsScreenRoute( navController: NavController, @@ -105,14 +103,14 @@ fun NavGraphBuilder.settingsScreenRoute( navController = navController, settingsViewModel = viewModel, onBack = { - if (restart) + if (viewModel.restart) showRestartDialog.value = true else navController.navigateUp() }, checkOnClick = { if (checkOnClick(ctx, viewModel)) { - if (restart) + if (viewModel.restart) showRestartDialog.value = true else navController.navigateUp() @@ -142,19 +140,23 @@ private fun SettingsScreen( ?.observeAsState() LaunchedEffect(audioResult?.value) { + Log.d(TAG, "audio_settings_result observed: ${audioResult?.value}") if (audioResult?.value == true) { Log.d(TAG, "Got result from AudioSettings: true") - restart = true + settingsViewModel.restart = true + showRestartDialog.value = true navController.currentBackStackEntry ?.savedStateHandle ?.remove("audio_settings_result") } } - LaunchedEffect(null) { - save = false - restart = false - settingsViewModel.loadSettings(ctx) + LaunchedEffect(Unit) { + if (!settingsViewModel.restart) { + settingsViewModel.save = false + settingsViewModel.restart = false + settingsViewModel.loadSettings(ctx) + } areSettingsLoaded = true } @@ -502,8 +504,8 @@ private fun SettingsContent( inputStream.close() Config.replaceVariable("sip_certificate", certPath) viewModel.tlsCertificateFile.value = true - save = true - restart = true + viewModel.save = true + viewModel.restart = true } catch (e: Error) { alertTitle.value = errorTitleText alertMessage.value = readCertError + ": " + e.message @@ -561,8 +563,8 @@ private fun SettingsContent( Utils.putFileContents(certPath, content) Config.replaceVariable("sip_certificate", certPath) viewModel.tlsCertificateFile.value = true - save = true - restart = true + viewModel.save = true + viewModel.restart = true } shouldShowRequestPermissionRationale(activity, permission) -> showAlertDialog.value = true @@ -575,8 +577,8 @@ private fun SettingsContent( else { Config.removeVariable("sip_certificate") Utils.deleteFile(File(BaresipService.filesPath + "/cert.pem")) - save = true - restart = true + viewModel.save = true + viewModel.restart = true } } ) @@ -645,7 +647,7 @@ private fun SettingsContent( val inputStream = ctx.contentResolver.openInputStream(uri) as FileInputStream caCertsFile.copyInputStreamToFile(inputStream) inputStream.close() - restart = true + viewModel.restart = true } catch (e: Error) { alertTitle.value = errorTitleText alertMessage.value = readCaCertsError + ": " + e.message @@ -696,7 +698,7 @@ private fun SettingsContent( } File(BaresipService.filesPath + "/ca_certs.crt").writeBytes(content) viewModel.caFile.value = true - restart = true + viewModel.restart = true } shouldShowRequestPermissionRationale(activity, permission) -> { dialogTitle.value = noticeTitleText @@ -716,7 +718,7 @@ private fun SettingsContent( Utils.selectInputFile(caCertsRequest) else { Utils.deleteFile(File(BaresipService.filesPath + "/ca_certs.crt")) - restart = true + viewModel.restart = true } } ) @@ -991,7 +993,7 @@ private fun SettingsContent( if (results[Manifest.permission.READ_PHONE_NUMBERS] == true) Log.d(TAG, "READ_PHONE_NUMBERS permission granted") BaresipService.instance?.addMobileUserAgent() - restart = true + viewModel.restart = true } val dialerRoleRequest = rememberLauncherForActivityResult( @@ -1006,14 +1008,14 @@ private fun SettingsContent( ) if (Utils.checkPermissions(ctx, permissions)) { BaresipService.instance?.addMobileUserAgent() - restart = true + viewModel.restart = true } else requestPermissionLauncher.launch(permissions) } else BaresipService.instance?.addMobileUserAgent() - restart = true + viewModel.restart = true } Switch( checked = defaultDialer, @@ -1037,7 +1039,7 @@ private fun SettingsContent( Log.e(TAG, "ActivityNotFound exception: ${e.message}") } BaresipService.instance?.addMobileUserAgent() - restart = true + viewModel.restart = true } } ) @@ -1067,7 +1069,7 @@ private fun SettingsContent( Config.replaceVariable("mobile_account", if (it) "yes" else "no") Config.save() BaresipService.instance?.addMobileUserAgent() - restart = true + viewModel.restart = true } ) } @@ -1105,7 +1107,7 @@ private fun SettingsContent( ) { _ -> val isHeld = roleManager.isRoleHeld(RoleManager.ROLE_SMS) viewModel.defaultMessaging.value = isHeld - restart = true + viewModel.restart = true } Switch( checked = defaultMessaging, @@ -1280,7 +1282,7 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean { if ((Config.variable("auto_start") == "yes") != viewModel.autoStart.value) { Config.replaceVariable("auto_start", if (viewModel.autoStart.value) "yes" else "no") - save = true + viewModel.save = true } val listenAddr = viewModel.listenAddress.value.trim() @@ -1292,14 +1294,14 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean { return false } Config.replaceVariable("sip_listen", listenAddr) - save = true - restart = true + viewModel.save = true + viewModel.restart = true } if (Config.variable("net_af").lowercase() != viewModel.addressFamily.value) { Config.replaceVariable("net_af", viewModel.addressFamily.value) - save = true - restart = true + viewModel.save = true + viewModel.restart = true } val transportProtocols = viewModel.transportProtocols.value @@ -1315,8 +1317,8 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean { Config.removeVariable("sip_transports") if (transportProtocols.isNotEmpty()) Config.replaceVariable("sip_transports", transportProtocols) - save = true - restart = true + viewModel.save = true + viewModel.restart = true } val dnsServers = addMissingPorts(viewModel.dnsServers.value @@ -1345,13 +1347,13 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean { Config.updateDnsServers(BaresipService.dnsServers) } // Api.net_dns_debug() - save = true + viewModel.save = true } if ((Config.variable("sip_verify_server") == "yes") != viewModel.verifyServer.value) { Config.replaceVariable("sip_verify_server", if (viewModel.verifyServer.value) "yes" else "no") Api.config_verify_server_set(viewModel.verifyServer.value) - save = true + viewModel.save = true } val userAgent = viewModel.userAgent.value.trim() @@ -1367,14 +1369,14 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean { Config.replaceVariable("user_agent", userAgent) else Config.removeVariable("user_agent") - save = true - restart = true + viewModel.save = true + viewModel.restart = true } if ((Config.variable("sip_cuser_random") == "yes") != viewModel.uniqueContactUri.value) { Config.replaceVariable("sip_cuser_random", if (viewModel.uniqueContactUri.value) "yes" else "no") - save = true - restart = true + viewModel.save = true + viewModel.restart = true } val darkTheme = viewModel.darkTheme.value @@ -1387,14 +1389,14 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean { BaresipService.darkTheme.value = darkTheme AppCompatDelegate.setDefaultNightMode(newDisplayTheme) Config.replaceVariable("dark_theme", if (darkTheme) "yes" else "no") - save = true + viewModel.save = true } val dynamicColors = viewModel.dynamicColors.value if (BaresipService.dynamicColors.value != dynamicColors) { BaresipService.dynamicColors.value = dynamicColors Config.replaceVariable("dynamic_colors", if (dynamicColors) "yes" else "no") - save = true + viewModel.save = true } val colorblind = viewModel.colorblind.value @@ -1405,14 +1407,14 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean { val baresipService = Intent(ctx, BaresipService::class.java) baresipService.action = "Update Notification" ContextCompat.startForegroundService(ctx, baresipService) - save = true + viewModel.save = true } val proximitySensing = viewModel.proximitySensing.value if ((Config.variable("proximity_sensing") == "yes") != proximitySensing) { Config.replaceVariable("proximity_sensing", if (proximitySensing) "yes" else "no") BaresipService.proximitySensing = proximitySensing - save = true + viewModel.save = true } val debug = viewModel.debug.value @@ -1421,7 +1423,7 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean { Config.replaceVariable("log_level", logLevelString) Api.log_level_set(logLevelString.toInt()) Log.logLevelSet(logLevelString.toInt()) - save = true + viewModel.save = true } val sipTrace = if (debug) viewModel.sipTrace.value else false @@ -1430,7 +1432,7 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean { Api.uag_enable_sip_trace(sipTrace) } - if (save) Config.save() + if (viewModel.save) Config.save() return true } diff --git a/app/src/main/kotlin/com/tutpro/baresip/SettingsViewModel.kt b/app/src/main/kotlin/com/tutpro/baresip/SettingsViewModel.kt index 6272b76d..c24b8f4e 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/SettingsViewModel.kt @@ -7,7 +7,9 @@ import android.content.Context.ROLE_SERVICE import android.os.Build import android.os.PowerManager import androidx.appcompat.app.AppCompatDelegate -import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import java.io.File @@ -35,6 +37,9 @@ class SettingsViewModel: ViewModel() { val debug = MutableStateFlow(false) val sipTrace = MutableStateFlow(false) + var restart by mutableStateOf(false) + var save by mutableStateOf(false) + private var isLoaded = false fun loadSettings(ctx: Context) { @@ -45,10 +50,7 @@ class SettingsViewModel: ViewModel() { listenAddress.value = Config.variable("sip_listen") - val familyValues = listOf("", "ipv4", "ipv6") - val itemPosition = - mutableIntStateOf(familyValues.indexOf(Config.variable("net_af").lowercase())) - addressFamily.value = familyValues[itemPosition.intValue] + addressFamily.value = Config.variable("net_af").lowercase() transportProtocols.value = Config.variable("sip_transports") @@ -87,4 +89,4 @@ class SettingsViewModel: ViewModel() { sipTrace.value = BaresipService.sipTrace } -} \ No newline at end of file +} diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 110aefa2..0be8ac9d 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -450,6 +450,9 @@ Audion odotusviive (millisekunneissa) soitetun puhelun alkaessa. Aseta korkeampi arvo, jos et kuule vastaajan ääntä heti, kun puhelu alkaa. Virheellinen audioviive \'%1$s\'. Sallittu arvo on välillä 100–3000. + iLBC-moodi + Valitsee iLBC-koodekin kehyksen keston. + 30ms tarjoaa paremman äänenlaadun alhaisilla bittinopeuksilla, kun taas 20ms:ssä on pienempi viive. Oletus äänen voimakkuus Jos valittu, puhelun äänen voimakkuus asteikolla 1–10. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ba997d7c..d2aae6af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -429,6 +429,9 @@ Time (in milliseconds) to wait audio from callee when call is established. Set to a higher value if you miss audio from callee at the beginning of the call. Invalid Audio Delay \'%1$s\'. Valid values are from 100 to 3000. + iLBC Mode + Selects the default iLBC frame duration. + 30ms provides better low-bitrate quality, while 20ms has lower latency. Default Call Volume If set, default call audio volume at scale 1–10. Tone Country