Merge recording enc and dec wav files
This commit is contained in:
@ -73,6 +73,10 @@ import androidx.media.AudioAttributesCompat
|
||||
import androidx.media.AudioFocusRequestCompat
|
||||
import androidx.media.AudioManagerCompat
|
||||
import com.tutpro.baresip.Utils.toCircle
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
import java.nio.charset.Charset
|
||||
@ -1095,9 +1099,31 @@ class BaresipService: Service() {
|
||||
history.stopTime = GregorianCalendar()
|
||||
history.startTime = if (completedElsewhere) history.stopTime else call.startTime
|
||||
history.rejected = call.rejected
|
||||
if (call.startTime != null && call.dumpfiles[0] != "")
|
||||
history.recording = call.dumpfiles
|
||||
history.add()
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (call.startTime != null && call.dumpfiles[0] != "") {
|
||||
delay(500)
|
||||
val rxFile = File(call.dumpfiles[0])
|
||||
val txFile = File(call.dumpfiles[1])
|
||||
val mergedFile = File(filesPath, "${rxFile.nameWithoutExtension}_stereo.wav")
|
||||
|
||||
if (Utils.mergeWavFiles(rxFile, txFile, mergedFile)) {
|
||||
Log.d(TAG, "Automatic merge succeeded.")
|
||||
history.recording = arrayOf(mergedFile.absolutePath, "")
|
||||
try {
|
||||
rxFile.delete()
|
||||
txFile.delete()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Could not delete temporary raw files after merge: ${e.message}")
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Automatic merge failed. Storing raw file paths as fallback.")
|
||||
history.recording = call.dumpfiles
|
||||
}
|
||||
}
|
||||
history.add()
|
||||
}
|
||||
|
||||
ua.account.missedCalls = ua.account.missedCalls || missed
|
||||
}
|
||||
if (!Utils.isVisible()) {
|
||||
|
||||
@ -40,6 +40,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@ -50,15 +51,17 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.tutpro.baresip.CallRow.Details
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.FileInputStream
|
||||
import java.text.DateFormat
|
||||
import java.util.GregorianCalendar
|
||||
|
||||
@ -254,8 +257,8 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val recording = detail.recording
|
||||
val decPlayer = MediaPlayer()
|
||||
val encPlayer = MediaPlayer()
|
||||
val mediaPlayer = remember { MediaPlayer() }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
@ -263,93 +266,99 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
|
||||
message = "",
|
||||
)
|
||||
|
||||
if (recording[0] != "") {
|
||||
val isRawState = recording[0] != "" && recording[1] != ""
|
||||
val isMergedState = recording[0] != "" && recording[1] == ""
|
||||
|
||||
if (isRawState || isMergedState) {
|
||||
Text(
|
||||
text = durationText,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.padding(end = 12.dp)
|
||||
.clickable(onClick = {
|
||||
if (!decPlayer.isPlaying && !encPlayer.isPlaying) {
|
||||
decPlayer.reset()
|
||||
encPlayer.reset()
|
||||
Log.d(TAG, "Playing recordings ${recording[0]} and ${recording[1]}")
|
||||
decPlayer.apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
setOnPreparedListener {
|
||||
encPlayer.apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
setOnPreparedListener {
|
||||
it.start()
|
||||
decPlayer.start()
|
||||
Log.d(TAG, "Started players")
|
||||
showDialog.value = true
|
||||
}
|
||||
setOnCompletionListener {
|
||||
Log.d(TAG, "Stopping encPlayer")
|
||||
it.stop()
|
||||
showDialog.value = false
|
||||
}
|
||||
try {
|
||||
val file = recording[0]
|
||||
val encFile = File(file)
|
||||
.copyTo(
|
||||
File(
|
||||
BaresipService.filesPath +
|
||||
"/tmp/encode.wav"
|
||||
), true
|
||||
)
|
||||
val encUri = encFile.toUri()
|
||||
setDataSource(ctx, encUri)
|
||||
prepareAsync()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "encPlayer IllegalArgumentException: $e")
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "encPlayer IOException: $e")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "encPlayer Exception: $e")
|
||||
if (!mediaPlayer.isPlaying) {
|
||||
mediaPlayer.reset()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
var finalFile: File? = null
|
||||
|
||||
if (isRawState) {
|
||||
val fileIn = File(recording[0])
|
||||
val fileOut = File(recording[1])
|
||||
val mergedFileName = "merged_${fileIn.nameWithoutExtension}_${fileOut.nameWithoutExtension}.wav"
|
||||
val mergedFile = File(BaresipService.filesPath + "/tmp", mergedFileName)
|
||||
|
||||
if (mergedFile.exists()) {
|
||||
Log.d(TAG, "Using already merged file: ${mergedFile.name}")
|
||||
finalFile = mergedFile
|
||||
} else {
|
||||
File(BaresipService.filesPath + "/tmp").mkdirs()
|
||||
if (Utils.mergeWavFiles(fileIn, fileOut, mergedFile)) {
|
||||
finalFile = mergedFile
|
||||
}
|
||||
}
|
||||
|
||||
// If merge successful, update state and delete originals
|
||||
if (finalFile != null && finalFile.exists()) {
|
||||
// Update the object state
|
||||
// We put the merged path in [0] and clear [1]
|
||||
recording[0] = finalFile.absolutePath
|
||||
recording[1] = ""
|
||||
|
||||
// Delete the original raw files
|
||||
try {
|
||||
if (fileIn.exists()) fileIn.delete()
|
||||
if (fileOut.exists()) fileOut.delete()
|
||||
|
||||
// Persist changes so the app remembers the file is merged
|
||||
CallHistoryNew.save()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "MergeWav: Failed to delete original files: ${e.message}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We are already in merged state
|
||||
Log.d(TAG, "Using already merged file: ${recording[0]}")
|
||||
finalFile = File(recording[0])
|
||||
}
|
||||
setOnCompletionListener {
|
||||
Log.d(TAG, "Stopping decPlayer")
|
||||
it.stop()
|
||||
showDialog.value = false
|
||||
}
|
||||
try {
|
||||
val file = recording[1]
|
||||
val decFile = File(file)
|
||||
.copyTo(
|
||||
File(
|
||||
BaresipService.filesPath +
|
||||
"/tmp/decode.wav"
|
||||
), true
|
||||
)
|
||||
val decUri = decFile.toUri()
|
||||
setDataSource(ctx, decUri)
|
||||
prepareAsync()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "decPlayer IllegalArgumentException: $e")
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "decPlayer IOException: $e")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "decPlayer Exception: $e")
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (finalFile != null && finalFile.exists()) {
|
||||
try {
|
||||
mediaPlayer.apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
val fis = FileInputStream(finalFile)
|
||||
setDataSource(fis.fd)
|
||||
fis.close()
|
||||
setOnPreparedListener {
|
||||
it.start()
|
||||
Log.d(TAG, "Started playback")
|
||||
showDialog.value = true
|
||||
}
|
||||
setOnCompletionListener {
|
||||
Log.d(TAG, "Playback complete")
|
||||
showDialog.value = false
|
||||
it.reset()
|
||||
}
|
||||
prepareAsync()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Playback failed: $e")
|
||||
Toast.makeText(ctx, "Playback error", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(ctx, "Failed to process audio file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (decPlayer.isPlaying && encPlayer.isPlaying) {
|
||||
decPlayer.stop()
|
||||
encPlayer.stop()
|
||||
} else {
|
||||
mediaPlayer.stop()
|
||||
mediaPlayer.reset()
|
||||
showDialog.value = false
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@ -22,8 +22,7 @@ class CallHistoryNew(val aor: String, val peerUri: String, val direction: String
|
||||
val aorSpecificHistory = BaresipService.callHistory.filter { it.aor == this.aor }
|
||||
if (aorSpecificHistory.size > CALL_HISTORY_SIZE) {
|
||||
val oldestToRemove = aorSpecificHistory.first()
|
||||
if (oldestToRemove.recording[0].isNotEmpty())
|
||||
deleteRecording(oldestToRemove.recording)
|
||||
deleteRecording(oldestToRemove.recording)
|
||||
BaresipService.callHistory.remove(oldestToRemove)
|
||||
}
|
||||
save()
|
||||
@ -44,8 +43,7 @@ class CallHistoryNew(val aor: String, val peerUri: String, val direction: String
|
||||
for (i in BaresipService.callHistory.indices.reversed()) {
|
||||
val h = BaresipService.callHistory[i]
|
||||
if (h.aor == aor) {
|
||||
if (h.recording[0] != "")
|
||||
deleteRecording(h.recording)
|
||||
deleteRecording(h.recording)
|
||||
BaresipService.callHistory.removeAt(i)
|
||||
}
|
||||
}
|
||||
|
||||
@ -478,8 +478,7 @@ private fun loadCallHistory(aor: String): MutableList<CallRow> {
|
||||
|
||||
private fun removeFromHistory(callHistory: MutableState<List<CallRow>>, callRow: CallRow) {
|
||||
for (details in callRow.details) {
|
||||
if (details.recording[0] != "")
|
||||
CallHistoryNew.deleteRecording(details.recording)
|
||||
CallHistoryNew.deleteRecording(details.recording)
|
||||
BaresipService.callHistory.removeAll {
|
||||
it.startTime == details.startTime && it.stopTime == details.stopTime
|
||||
}
|
||||
|
||||
@ -52,6 +52,8 @@ import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.SocketException
|
||||
import java.net.URL
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.security.KeyStore
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.CertificateException
|
||||
@ -1146,6 +1148,108 @@ object Utils {
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two mono WAV files into a single stereo WAV file.
|
||||
* @param file1 The first WAV file (Left channel)* @param file2 The second WAV file (Right channel)
|
||||
* @param outFile The destination file
|
||||
* @return True if successful, False otherwise
|
||||
*/
|
||||
fun mergeWavFiles(file1: File, file2: File, outFile: File): Boolean {
|
||||
|
||||
Log.d(TAG, "MergeWav: Input1 size=${file1.length()}, Input2 size=${file2.length()}")
|
||||
|
||||
try {
|
||||
val in1 = FileInputStream(file1)
|
||||
val in2 = FileInputStream(file2)
|
||||
val out = FileOutputStream(outFile)
|
||||
|
||||
val header1 = ByteArray(44)
|
||||
val header2 = ByteArray(44)
|
||||
|
||||
if (in1.read(header1) != 44 || in2.read(header2) != 44) {
|
||||
Log.e(TAG, "MergeWav: Failed to read headers")
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse data sizes from the input headers (Little Endian, offset 40)
|
||||
val dataSize1 = ByteBuffer.wrap(header1, 40, 4).order(ByteOrder.LITTLE_ENDIAN).int
|
||||
val dataSize2 = ByteBuffer.wrap(header2, 40, 4).order(ByteOrder.LITTLE_ENDIAN).int
|
||||
|
||||
Log.d(TAG, "MergeWav: DataChunk1=$dataSize1, DataChunk2=$dataSize2")
|
||||
|
||||
// Since we are converting 2x Mono to 1x Stereo, the size doubles.
|
||||
// If one file is shorter, we will pad it with silence (0s).
|
||||
val maxDataSize = kotlin.math.max(dataSize1, dataSize2)
|
||||
val totalDataSize = maxDataSize * 2
|
||||
|
||||
Log.d(TAG, "MergeWav: Calculated Target DataSize=$totalDataSize")
|
||||
|
||||
// Prepare new header based on header1
|
||||
val newHeader = header1.clone()
|
||||
newHeader[22] = 2 // Channels = Stereo
|
||||
newHeader[32] = 4 // BlockAlign = 2 * 16bit / 8 = 4
|
||||
|
||||
val sampleRate = ByteBuffer.wrap(header1, 24, 4).order(ByteOrder.LITTLE_ENDIAN).int
|
||||
val byteRate = sampleRate * 2 * 2 // SampleRate * Channels * Bits/8
|
||||
|
||||
ByteBuffer.wrap(newHeader, 28, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(byteRate)
|
||||
ByteBuffer.wrap(newHeader, 40, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(totalDataSize)
|
||||
ByteBuffer.wrap(newHeader, 4, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(totalDataSize + 36)
|
||||
|
||||
out.write(newHeader)
|
||||
|
||||
val buffer1 = ByteArray(2)
|
||||
val buffer2 = ByteArray(2)
|
||||
val silence = ByteArray(2) // Default 0s
|
||||
|
||||
// Loop until the LONGEST file is finished
|
||||
var bytesRead1: Int
|
||||
var bytesRead2: Int
|
||||
var totalBytesWritten = 0
|
||||
|
||||
// We use a do-while or simpler loop structure to handle uneven lengths
|
||||
while (true) {
|
||||
bytesRead1 = in1.read(buffer1)
|
||||
bytesRead2 = in2.read(buffer2)
|
||||
|
||||
// If both are done, stop
|
||||
if (bytesRead1 == -1 && bytesRead2 == -1) break
|
||||
|
||||
// Write Left Channel (File 1)
|
||||
if (bytesRead1 != -1) {
|
||||
out.write(buffer1, 0, bytesRead1)
|
||||
totalBytesWritten += bytesRead1
|
||||
} else {
|
||||
// File 1 ended, write silence
|
||||
out.write(silence)
|
||||
totalBytesWritten += 2
|
||||
}
|
||||
|
||||
// Write Right Channel (File 2)
|
||||
if (bytesRead2 != -1) {
|
||||
out.write(buffer2, 0, bytesRead2)
|
||||
totalBytesWritten += bytesRead2
|
||||
} else {
|
||||
// File 2 ended, write silence
|
||||
out.write(silence)
|
||||
totalBytesWritten += 2
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "MergeWav: Finished. Actual bytes written: $totalBytesWritten")
|
||||
|
||||
in1.close()
|
||||
in2.close()
|
||||
out.close()
|
||||
|
||||
return true
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "MergeWav Failed: $e")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun listFilesInDirectory(directoryPath: String): List<File> {
|
||||
val directory = File(directoryPath)
|
||||
|
||||
Reference in New Issue
Block a user