@ -25,7 +25,7 @@ import com.utun.chat.ChatApplication
* - play - поток : NativeLib . callAudioPull ( C : джиттер - буфер → декод → SoundTouch ) → AudioTrack . write
*
* Воспроизведение тянет PCM из единого голосового стека в C ( call _audio ) : адаптивный
* джиттер - буфер ( кодированные кадры ) + time - stretch держат задержку ~ 60 мс и догоняют
* джиттер - буфер ( кодированные кадры ) с минимальным запасом 20 мс + CallDelay догоняют
* после сетевых бёрстов . На Kotlin - стороне своего джиттера больше нет .
* /
class CallAudioEngine {
@ -71,13 +71,19 @@ class CallAudioEngine {
@Volatile private var playFramesPlayed = 0L
@Volatile private var capFramesRead = 0L
private var playbackBuffer : CallPlaybackBuffer ? = null // под trackLock
@Volatile private var routeGeneration = 0L
private data class OutputSample ( val atUs : Long , val generation : Long , val latency : CallOutputLatency )
@Volatile private var outputSample : OutputSample ? = null
@Volatile private var startElapsedMs = 0L
@Volatile private var captureElapsedMs = 0L
private val mainHandler = Handler ( Looper . getMainLooper ( ) )
private val trackRouteListener = AudioRouting . OnRoutingChangedListener { output ->
synchronized ( trackLock ) {
val track = audioTrack
if ( active && track != null && output === track ) configurePlaybackBuffer ( track , " route " )
if ( active && track != null && output === track ) {
routeGeneration ++
configurePlaybackBuffer ( track , " route " )
}
}
}
private val aecDelayRunnable = Runnable { measureAecDelay ( ) }
@ -86,6 +92,12 @@ class CallAudioEngine {
fun isActive ( ) : Boolean = active
fun outputLatency ( id : Long ) : CallOutputLatency {
val sample = outputSample
return if ( active && id == callId && sample != null && sample . generation == routeGeneration &&
System . nanoTime ( ) / 1000 - sample . atUs in 0. . 500 _000 ) sample . latency else CallOutputLatency ( )
}
/** Позднее создание Telecom Connection: сначала снимаем собственную заявку на audio mode. */
fun bindTelecom ( connection : UtunConnection ) {
if ( ! active || router . telecom === connection ) return
@ -128,6 +140,7 @@ class CallAudioEngine {
val nativeMs = SystemClock . elapsedRealtime ( ) - startElapsedMs
callId = id
routeGeneration = 0 ; outputSample = null
router . bindTelecom ( TelecomCallManager . connectionFor ( id ) )
muted = false
failed . set ( false )
@ -194,12 +207,13 @@ class CallAudioEngine {
val minQueryMs = SystemClock . elapsedRealtime ( ) - createdAt
val trackBuf = ( trackMin * 2 ) . coerceAtLeast ( FRAME _SAMPLES * 4 )
val constructorAt = SystemClock . elapsedRealtime ( )
val track = AudioTrack (
AudioAttributes . Builder ( ) . setUsage ( AudioAttributes . USAGE _VOICE _COMMUNICATION )
. setContentType ( AudioAttributes . CONTENT _TYPE _SPEECH ) . build ( ) ,
AudioFormat . Builder ( ) . setSampleRate ( SAMPLE _RATE ) . setEncoding ( AudioFormat . ENCODING _PCM _16BIT )
. setChannelMask ( AudioFormat . CHANNEL _OUT _MONO ) . build ( ) ,
trackBuf , AudioTrack . MODE _STREAM , AudioManager . AUDIO _SESSION _ID _GENERATE )
val track = AudioTrack . Builder ( )
. setAudioAttributes ( AudioAttributes . Builder ( ) . setUsage ( AudioAttributes . USAGE _VOICE _COMMUNICATION )
. setContentType ( AudioAttributes . CONTENT _TYPE _SPEECH ) . build ( ) )
. setAudioFormat ( AudioFormat . Builder ( ) . setSampleRate ( SAMPLE _RATE ) . setEncoding ( AudioFormat . ENCODING _PCM _16BIT )
. setChannelMask ( AudioFormat . CHANNEL _OUT _MONO ) . build ( ) )
. setBufferSizeInBytes ( trackBuf ) . setTransferMode ( AudioTrack . MODE _STREAM )
. setPerformanceMode ( AudioTrack . PERFORMANCE _MODE _LOW _LATENCY ) . build ( )
val constructorMs = SystemClock . elapsedRealtime ( ) - constructorAt
audioTrack = track
if ( ! active ) return @runWorker
@ -489,7 +503,7 @@ class CallAudioEngine {
val stamp = AudioTimestamp ( )
while ( ! Thread . currentThread ( ) . isInterrupted ) {
if ( active ) adjustPlaybackBuffer ( track )
if ( trace . enabled && active ) trace . device = samplePlaybackDevice ( track , stamp )
if ( active ) trace . device = samplePlaybackDevice ( track , stamp )
trace . flush ( )
Thread . sleep ( 200 )
}
@ -518,6 +532,7 @@ class CallAudioEngine {
/** Только reporter: Binder/внутренние locks AudioTrack не должны вызываться здесь из playback. */
private fun samplePlaybackDevice ( track : AudioTrack , stamp : AudioTimestamp ) : CallAudioTrace . DeviceSample {
val beginNs = System . nanoTime ( )
val generation = routeGeneration
val stampOk = track . getTimestamp ( stamp )
val stampEndNs = System . nanoTime ( )
val route = track . routedDevice
@ -525,12 +540,13 @@ class CallAudioEngine {
val underruns = track . underrunCount . toLong ( )
val bufferFrames = track . bufferSizeInFrames . toLong ( )
val startFrames = if ( Build . VERSION . SDK _INT >= 31 ) track . startThresholdInFrames . toLong ( ) else bufferFrames
val performanceMode = track . performanceMode
val endNs = System . nanoTime ( )
return CallAudioTrace . DeviceSample (
route ?. id ?. toLong ( ) ?: - 1 , route ?. type ?. toLong ( ) ?: - 1 , underruns , bufferFrames , startFrames ,
if ( stampOk ) stamp . framePosition else - 1 , if ( stampOk ) stamp . nanoTime else - 1 , stampOk ,
beginNs / 1000 , endNs / 1000 , ( stampEndNs - beginNs ) / 1000 ,
( routeEndNs - stampEndNs ) / 1000 , ( endNs - routeEndNs ) / 1000 )
( routeEndNs - stampEndNs ) / 1000 , ( endNs - routeEndNs ) / 1000 , generation , performanceMode )
}
private fun playLoop ( trace : CallAudioTrace ) {
@ -546,6 +562,9 @@ class CallAudioEngine {
}
val epoch = snapshot [ 0 ]
val head = CallPlaybackHead ( )
val playbackClock = CallPlaybackClock ( )
var clockMode = " "
var outputUs = - 1L
var clockFailed = false
val diagnostic = LongArray ( 8 )
val row = LongArray ( CallAudioTrace . WIDTH )
@ -590,6 +609,7 @@ class CallAudioEngine {
return
}
Thread . sleep ( 2 )
continue
} else {
blockedAt = 0L
offset += written
@ -604,13 +624,26 @@ class CallAudioEngine {
observedPlayed = played
playFramesPlayed = played
operationNs = System . nanoTime ( )
NativeLib . callAudioProgress ( callId , epoch , played , nowUs )
val device = trace . device
val anchor = playbackClock . update ( playFramesWritten , played , nowUs , routeGeneration ,
device . generation , device . stampFrame , device . stampNs / 1000 )
outputUs = if ( anchor ?. measured == true ) ( anchor . atUs - nowUs ) . coerceAtLeast ( 0 ) else - 1
outputSample = OutputSample ( nowUs , routeGeneration , CallOutputLatency (
( ( playFramesWritten - played ) * 1000 / SAMPLE _RATE ) . toInt ( ) ,
if ( outputUs >= 0 ) ( outputUs / 1000 ) . toInt ( ) else - 1 ) )
if ( anchor != null ) NativeLib . callAudioProgress ( callId , epoch , anchor . position , anchor . atUs , nowUs , anchor . measured )
progressCallNs += System . nanoTime ( ) - operationNs
val mode = if ( anchor == null ) " unavailable " else if ( anchor . measured ) " timestamp " else " head-estimate "
if ( mode != clockMode ) {
LogManager . addLog ( if ( mode == " unavailable " ) " WARN " else " INFO " , " CallAudio " ,
" playback clock id= $callId mode= $mode routeGeneration= $routeGeneration outputUs= $outputUs " )
clockMode = mode
}
} else if ( ! clockFailed ) {
clockFailed = true
playFramesPlayed = - 1
// Отрицательная позиция немедленно инвалидирует снимок в native.
NativeLib . callAudioProgress ( callId , epoch , - 1 , nowUs )
NativeLib . callAudioProgress ( callId , epoch , - 1 , nowUs , nowUs , false )
LogManager . addLog ( " ERROR " , " CallAudio " ,
" playback clock invalid id= $callId epoch= $epoch written= $playFramesWritten ; audio continues " )
}
@ -627,6 +660,7 @@ class CallAudioEngine {
diagnostic . copyInto ( row , 16 ) ; row [ 24 ] = offset . toLong ( )
row [ 25 ] = headCallNs / 1000 ; row [ 26 ] = progressCallNs / 1000 ; row [ 27 ] = writeSpanUs
row [ 28 ] = diagnosticUs ; row [ 29 ] = nowUs - pullAtNs / 1000
row [ 35 ] = outputUs
trace . record ( row )
}
@ -680,7 +714,8 @@ class CallAudioEngine {
val level = if ( actual == requested && ( change == null || change . underruns == 0 ) ) " INFO " else " WARN "
LogManager . addLog ( level , " CallAudio " ,
" playback buffering id= $callId reason= $reason requestedFrames= $requested actualFrames= $actual " +
" previousFrames= $previous startFrames= $start capacityFrames= $capacity underruns= ${change?.underruns ?: 0} pacing=device " )
" previousFrames= $previous startFrames= $start capacityFrames= $capacity underruns= ${change?.underruns ?: 0} " +
" performanceRequested= ${AudioTrack.PERFORMANCE_MODE_LOW_LATENCY} performanceActual= ${track.performanceMode} pacing=device " )
if ( playReady && ( previous != actual || reason == " route " ) ) {
// Очередь сначала принимает новый размер; устаревшая задача не затронет следующий звонок.
mainHandler . post {