Added iLBC audio codec

This commit is contained in:
Juha Heinanen
2026-08-16 17:02:32 +03:00
parent d6f6bd37d3
commit 37d99b4848
8 changed files with 396 additions and 314 deletions

View File

@ -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

View File

@ -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<AudioViewModel>()
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<String>()
private var newAudioModules = mutableMapOf<String, Boolean>()
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)
}

View File

@ -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<String, Boolean>())
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<String>()
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<String, Boolean>()
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)
}
}

View File

@ -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<String>
@ -213,7 +213,8 @@ object Config {
val previousModules = previousVariables("module")
for (module in audioModules)
if ("${module}.so" in previousModules ||
(module == "libg722" && "g722.so" in previousModules))
(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"

View File

@ -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<Boolean>("audio_settings_result")
}
}
LaunchedEffect(null) {
save = false
restart = false
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
}

View File

@ -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")

View File

@ -450,6 +450,9 @@
<string name="audio_delay_help">Audion odotusviive (millisekunneissa) soitetun puhelun alkaessa.
Aseta korkeampi arvo, jos et kuule vastaajan ääntä heti, kun puhelu alkaa.</string>
<string name="invalid_audio_delay">Virheellinen audioviive \'%1$s\'. Sallittu arvo on välillä 1003000.</string>
<string name="ilbc_mode">iLBC-moodi</string>
<string name="ilbc_mode_help">Valitsee iLBC-koodekin kehyksen keston.
30ms tarjoaa paremman äänenlaadun alhaisilla bittinopeuksilla, kun taas 20ms:ssä on pienempi viive.</string>
<string name="default_call_volume">Oletus äänen voimakkuus</string>
<string name="default_call_volume_help">Jos valittu, puhelun äänen voimakkuus
asteikolla 110.</string>

View File

@ -429,6 +429,9 @@
<string name="audio_delay_help">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.</string>
<string name="invalid_audio_delay">Invalid Audio Delay \'%1$s\'. Valid values are from 100 to 3000.</string>
<string name="ilbc_mode">iLBC Mode</string>
<string name="ilbc_mode_help">Selects the default iLBC frame duration.
30ms provides better low-bitrate quality, while 20ms has lower latency.</string>
<string name="default_call_volume">Default Call Volume</string>
<string name="default_call_volume_help">If set, default call audio volume at scale 110.</string>
<string name="tone_country">Tone Country</string>