@ -107,11 +107,17 @@ static float g_vad_win[RADIO_VAD_WINDOW_16K]; /* накопи
static int g_vad_win_len = 0 ;
static struct radio_vad_fsm g_vad_fsm ;
static uint64_t g_last_rx_tb = 0 ; /* последний принятый аудио-кадр (busy) */
static uint64_t g_last_rx_node = 0 ; /* источник блокировки авто-PTT */
/* TX-статистика (кадрирование/кодирование), выводится раз в секунду пока идёт PTT. */
static uint64_t g_tx_stats_tb = 0 ;
static uint64_t g_vad_stats_tb = 0 ;
static float g_vad_peak_probability = 0 ; /* максимум между логами, чтобы не пропускать короткую речь */
/* Сводка раз в секунду; TRACE показывает каждое окно, DEBUG — решения и причины отказа. */
static struct {
uint64_t stats_tb , last_window_tb , total_windows , max_inference_tb , max_gap_tb ;
unsigned windows , speech , busy , confirm_resets , errors ;
float min_probability , max_probability , max_input_rms , max_model_rms ;
int last_busy ;
} g_vad_diag ;
static uint32_t g_tx_frames = 0 ; /* кадров за интервал */
static uint32_t g_tx_bytes = 0 ; /* суммарный размер opus за интервал */
static uint32_t g_tx_tail = 0 ; /* нулевых отсчётов дополнения хвоста за интервал */
@ -410,7 +416,7 @@ int radio_audio_start(uint64_t group_id, int capture_channels) {
g_group_id = group_id ;
+ + g_listen_generation ;
g_active = 1 ;
g_last_rx_tb = 0 ;
g_last_rx_tb = g_last_rx_node = 0 ;
memset ( g_sources , 0 , sizeof ( g_sources ) ) ;
pthread_mutex_unlock ( & g_mtx ) ;
DEBUG_INFO ( DEBUG_CATEGORY_RADIO , " %s: listening grp=%016llx " , RADIO_AUDIO_ID , ( unsigned long long ) group_id ) ;
@ -486,17 +492,22 @@ int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_e
DEBUG_WARN ( DEBUG_CATEGORY_RADIO , " %s: silero_vad_create_default failed — VAD auto-PTT disabled " , RADIO_AUDIO_ID ) ;
}
if ( ! g_vad_mode & & g_vad_compressor ) { audio_compressor_destroy ( g_vad_compressor ) ; g_vad_compressor = NULL ; }
DEBUG_INFO ( DEBUG_CATEGORY_RADIO , " %s: VAD compressor=%s max_gain=%.0fdB shared_with_TX=enabled,target,history,rise,release "
" block=%dms buffering=%dms vad=%d " , RADIO_AUDIO_ID ,
DEBUG_INFO ( DEBUG_CATEGORY_DEBUG , " VAD config group=%016llx requested=%d ready=%d ch=%d threshold=%.3f confirm=%dx32ms "
" hangover=%dms busy_hold=%dms prefetch=%dms compressor=%s max_gain=%.0fdB target=%.1fdBFS "
" history=%dms rise=%.1fx release=%.1fx block=%dms buffering=%dms " , ( unsigned long long ) group_id ,
vad_enabled , g_vad_mode , capture_channels , g_vad_threshold , RADIO_VAD_CONFIRM_WINDOWS ,
g_vad_hangover_ms , RADIO_VAD_BUSY_TB / 10 , RADIO_PREFETCH_MS ,
g_vad_compressor ? " on " : ( compressor_enabled ? " unavailable " : " off " ) , accfg . max_gain_db ,
accfg . block_duration_ms , g_vad_compressor ? accfg . block_duration_ms : 0 , g_vad_mode ) ;
20.0f * log10f ( accfg . target_level ) , accfg . lookback_ms , accfg . rise_rate_per_sec ,
accfg . release_rate_per_sec , accfg . block_duration_ms , g_vad_compressor ? accfg . block_duration_ms : 0 ) ;
}
g_vad_pcm_frames = 0 ; g_vad_input_sum_sq = 0 ;
g_vad_input_rms = g_vad_output_rms = 0 ;
if ( g_vad_mode ) {
g_vad_win_len = 0 ;
radio_vad_fsm_reset ( & g_vad_fsm ) ;
g_vad_stats_tb = 0 ; g_vad_peak_probability = 0 ;
memset ( & g_vad_diag , 0 , sizeof ( g_vad_diag ) ) ;
g_vad_diag . last_busy = - 1 ;
}
g_vad_manual_ptt = g_transmitting = g_tx_pcm_frames = g_vad_decimation = 0 ;
g_vad_sum = 0 ; g_tx_burst = 0 ;
@ -518,6 +529,11 @@ int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_e
void radio_audio_capture_stop ( void ) {
radio_tx_lock ( ) ;
if ( g_encoder ) {
if ( g_vad_mode ) DEBUG_INFO ( DEBUG_CATEGORY_DEBUG , " VAD capture stop group=%016llx windows=%llu talking=%d manual=%d "
" transmitting=%d confirm=%d window_tail=%d pcm_tail=%d gain=%.1fdB " ,
( unsigned long long ) g_group_id , ( unsigned long long ) g_vad_diag . total_windows ,
g_vad_fsm . talking , g_vad_manual_ptt , g_transmitting , g_vad_fsm . confirm_count ,
g_vad_win_len , g_vad_pcm_frames , audio_compressor_gain_db ( g_vad_compressor ) ) ;
if ( g_transmitting ) {
radio_audio_flush_tx ( ) ;
radio_audio_post_talk ( g_inst ? g_inst - > ua : NULL , g_group_id , 0 ) ;
@ -612,9 +628,21 @@ static void radio_audio_talk(void* arg) {
int current = owner & & talk - > listen_generation = = g_listen_generation & & g_active & & g_group_id = = talk - > group_id ;
pthread_mutex_unlock ( & g_mtx ) ;
if ( talk - > begin ) {
if ( current & & get_time_tb ( ) - talk - > created_tb < = 2000 & & radio_talk_begin ( talk - > inst , talk - > group_id ) = = 0 ) {
g_net_generation = talk - > generation ; g_net_burst = talk - > burst ; g_net_group = talk - > group_id ;
uint64_t age_tb = get_time_tb ( ) - talk - > created_tb ;
if ( ! current | | age_tb > 2000 ) {
DEBUG_WARN ( DEBUG_CATEGORY_DEBUG , " VAD/PTT BEGIN discarded group=%016llx generation=%llu burst=%llu "
" owner=%d current=%d queue_age=%.1fms limit=200ms " , ( unsigned long long ) talk - > group_id ,
( unsigned long long ) talk - > generation , ( unsigned long long ) talk - > burst , owner , current , age_tb / 10.0 ) ;
return ;
}
int rc = radio_talk_begin ( talk - > inst , talk - > group_id ) ;
if ( rc = = 0 ) {
g_net_generation = talk - > generation ; g_net_burst = talk - > burst ; g_net_group = talk - > group_id ;
DEBUG_DEBUG ( DEBUG_CATEGORY_DEBUG , " VAD/PTT BEGIN dispatched group=%016llx generation=%llu burst=%llu queue_age=%.1fms " ,
( unsigned long long ) talk - > group_id , ( unsigned long long ) talk - > generation ,
( unsigned long long ) talk - > burst , age_tb / 10.0 ) ;
} else DEBUG_WARN ( DEBUG_CATEGORY_DEBUG , " VAD/PTT BEGIN failed group=%016llx rc=%d queue_age=%.1fms " ,
( unsigned long long ) talk - > group_id , rc , age_tb / 10.0 ) ;
} else if ( g_net_generation = = talk - > generation & & g_net_burst = = talk - > burst & & g_net_group = = talk - > group_id ) {
if ( owner ) radio_talk_end ( talk - > inst , talk - > group_id ) ;
g_net_generation = g_net_burst = g_net_group = 0 ;
@ -758,7 +786,7 @@ static void radio_audio_update_transmission(void) {
}
if ( radio_audio_post_talk ( g_inst ? g_inst - > ua : NULL , g_group_id , desired ) ! = 0 ) { u_free ( prefetch ) ; return ; }
g_transmitting = desired ;
DEBUG_INFO ( DEBUG_CATEGORY_RADIO , " %s: TX %s generation=%llu burst=%llu manual=%d vad=%d " , RADIO_AUDIO_ID ,
DEBUG_INFO ( DEBUG_CATEGORY_DEBUG , " %s: TX %s generation=%llu burst=%llu manual=%d vad=%d " , RADIO_AUDIO_ID ,
desired ? " BEGIN " : " FIN " , ( unsigned long long ) g_tx_generation , ( unsigned long long ) g_tx_burst ,
g_vad_manual_ptt , g_vad_fsm . talking ) ;
if ( desired & & g_vad_mode ) radio_audio_send_prefetch ( prefetch ) ;
@ -767,7 +795,7 @@ static void radio_audio_update_transmission(void) {
void radio_audio_capture_discontinuity ( uint64_t group_id ) {
radio_tx_lock ( ) ;
if ( g_active & & g_group_id = = group_id & & g_encoder ) {
DEBUG_WARN ( DEBUG_CATEGORY_RADIO , " %s: capture discontinuity tail=%d transmitting=%d vad_tail=%d vad_gain=%.1fdB " ,
DEBUG_WARN ( DEBUG_CATEGORY_DEBUG , " %s: capture discontinuity tail=%d transmitting=%d vad_tail=%d vad_gain=%.1fdB " ,
RADIO_AUDIO_ID , g_tx_pcm_frames , g_transmitting , g_vad_pcm_frames , audio_compressor_gain_db ( g_vad_compressor ) ) ;
radio_audio_flush_tx ( ) ;
g_vad_win_len = g_vad_decimation = 0 ; g_vad_sum = 0 ;
@ -777,7 +805,8 @@ void radio_audio_capture_discontinuity(uint64_t group_id) {
g_prefetch_write = g_prefetch_frames = 0 ;
if ( g_vad ) silero_vad_reset ( g_vad ) ;
radio_vad_fsm_reset ( & g_vad_fsm ) ;
g_vad_stats_tb = 0 ; g_vad_peak_probability = 0 ;
memset ( & g_vad_diag , 0 , sizeof ( g_vad_diag ) ) ;
g_vad_diag . last_busy = - 1 ;
radio_audio_update_transmission ( ) ;
}
radio_tx_unlock ( ) ;
@ -791,37 +820,86 @@ static void radio_audio_vad_sample(const int16_t* frame) {
g_vad_win [ g_vad_win_len + + ] = g_vad_sum / ( 3.0f * 32768.0f ) ;
g_vad_decimation = 0 ; g_vad_sum = 0 ;
if ( g_vad_win_len ! = RADIO_VAD_WINDOW_16K ) return ;
if ( ! g_vad_compressor ) {
double sum_sq = 0 ;
for ( int i = 0 ; i < RADIO_VAD_WINDOW_16K ; + + i ) sum_sq + = ( double ) g_vad_win [ i ] * g_vad_win [ i ] ;
g_vad_input_rms = g_vad_output_rms = sqrtf ( ( float ) ( sum_sq / RADIO_VAD_WINDOW_16K ) ) ;
double sum_sq = 0 ;
float peak = 0 ;
for ( int i = 0 ; i < RADIO_VAD_WINDOW_16K ; + + i ) {
sum_sq + = ( double ) g_vad_win [ i ] * g_vad_win [ i ] ;
if ( fabsf ( g_vad_win [ i ] ) > peak ) peak = fabsf ( g_vad_win [ i ] ) ;
}
float model_rms = sqrtf ( ( float ) ( sum_sq / RADIO_VAD_WINDOW_16K ) ) ;
if ( ! g_vad_compressor ) g_vad_input_rms = g_vad_output_rms = model_rms ;
float probability = 0 ;
uint64_t started = get_time_tb ( ) ;
pthread_mutex_unlock ( & g_mtx ) ;
int result = silero_vad_process ( g_vad , g_vad_win , & probability ) ;
pthread_mutex_lock ( & g_mtx ) ;
if ( result ! = 0 )
DEBUG_ERROR ( DEBUG_CATEGORY_RADIO , " %s: VAD process failed " , RADIO_AUDIO_ID ) ;
g_vad_win_len = 0 ;
uint64_t now = get_time_tb ( ) ;
uint64_t inference_tb = now - started ;
uint64_t gap_tb = g_vad_diag . last_window_tb ? now - g_vad_diag . last_window_tb : 0 ;
g_vad_diag . last_window_tb = now ;
+ + g_vad_diag . total_windows ;
+ + g_vad_diag . windows ;
if ( inference_tb > g_vad_diag . max_inference_tb ) g_vad_diag . max_inference_tb = inference_tb ;
if ( gap_tb > g_vad_diag . max_gap_tb ) g_vad_diag . max_gap_tb = gap_tb ;
if ( result ! = 0 ) {
+ + g_vad_diag . errors ;
DEBUG_ERROR ( DEBUG_CATEGORY_DEBUG , " VAD inference failed group=%016llx window=%llu rc=%d inference=%.1fms " ,
( unsigned long long ) g_group_id , ( unsigned long long ) g_vad_diag . total_windows , result , inference_tb / 10.0 ) ;
}
struct radio_vad_fsm before = g_vad_fsm ;
int busy = g_last_rx_tb & & now - g_last_rx_tb < RADIO_VAD_BUSY_TB ;
int64_t rx_age_ms = g_last_rx_tb ? ( int64_t ) ( ( now - g_last_rx_tb ) / 10 ) : - 1 ;
int transition = radio_vad_fsm_update ( & g_vad_fsm , probability , g_vad_threshold , RADIO_VAD_CONFIRM_WINDOWS ,
( uint64_t ) g_vad_hangover_ms * 10 , busy , now ) ;
if ( transition ) {
DEBUG_DEBUG ( DEBUG_CATEGORY_RADIO , " %s: VAD %s probability=%.3f threshold=%.2f busy=%d hangover=%dms " , RADIO_AUDIO_ID ,
transition > 0 ? " START " : " STOP " , probability , g_vad_threshold , busy , g_vad_hangover_ms ) ;
}
uint64_t silence_tb = g_vad_fsm . silence_since_tb ? g_vad_fsm . silence_since_tb : before . silence_since_tb ;
double silence_ms = silence_tb ? ( now - silence_tb ) / 10.0 : 0 ;
const char * reason = result ! = 0 ? " inference_error " : busy ? " rx_busy " : transition > 0 ? " confirmed " :
transition < 0 ? " silence_timeout " : g_vad_fsm . talking ?
( probability > = g_vad_threshold ? " speech " : " hangover " ) :
probability > = g_vad_threshold ? " confirming " : before . confirm_count ? " confirm_reset " : " below_threshold " ;
radio_audio_update_transmission ( ) ;
if ( probability > g_vad_peak_probability ) g_vad_peak_probability = probability ;
if ( ! g_vad_stats_tb | | now - g_vad_stats_tb > = RADIO_AUDIO_STATS_TB ) {
DEBUG_DEBUG ( DEBUG_CATEGORY_RADIO , " %s: VAD probability=%.3f peak=%.3f threshold=%.2f confirm=%d/%d busy=%d manual=%d transmitting=%d "
" compressor=%s input_rms=%.6f output_rms=%.6f gain=%.1fdB clips=%u " ,
RADIO_AUDIO_ID , probability , g_vad_peak_probability , g_vad_threshold , g_vad_fsm . confirm_count ,
RADIO_VAD_CONFIRM_WINDOWS , busy , g_vad_manual_ptt , g_transmitting , g_vad_compressor ? " on " : " off " ,
g_vad_input_rms , g_vad_output_rms , audio_compressor_gain_db ( g_vad_compressor ) ,
audio_compressor_clip_count ( g_vad_compressor ) ) ;
g_vad_stats_tb = now ;
g_vad_peak_probability = 0 ;
if ( transition | | busy ! = g_vad_diag . last_busy | | before . confirm_count ! = g_vad_fsm . confirm_count | |
! ! before . silence_since_tb ! = ! ! g_vad_fsm . silence_since_tb ) {
DEBUG_DEBUG ( DEBUG_CATEGORY_DEBUG , " VAD decision group=%016llx window=%llu reason=%s probability=%.3f threshold=%.3f "
" talking=%d->%d confirm=%d->%d/%d silence=%.1f/%dms busy=%d rx_node=%016llx rx_age=%lldms "
" manual=%d transmitting=%d input=%.1fdBFS model=%.1fdBFS gain=%.1fdB inference=%.1fms gap=%.1fms " ,
( unsigned long long ) g_group_id , ( unsigned long long ) g_vad_diag . total_windows , reason , probability , g_vad_threshold ,
before . talking , g_vad_fsm . talking , before . confirm_count , g_vad_fsm . confirm_count , RADIO_VAD_CONFIRM_WINDOWS ,
silence_ms , g_vad_hangover_ms , busy , ( unsigned long long ) g_last_rx_node , ( long long ) rx_age_ms ,
g_vad_manual_ptt , g_transmitting , 20.0f * log10f ( fmaxf ( g_vad_input_rms , 0.000001f ) ) ,
20.0f * log10f ( fmaxf ( model_rms , 0.000001f ) ) , audio_compressor_gain_db ( g_vad_compressor ) ,
inference_tb / 10.0 , gap_tb / 10.0 ) ;
}
DEBUG_TRACE ( DEBUG_CATEGORY_DEBUG , " VAD window=%llu reason=%s probability=%.4f model_rms=%.6f peak=%.6f "
" input_rms=%.6f agc_rms=%.6f confirm=%d talking=%d busy=%d manual=%d tx=%d inference=%.1fms gap=%.1fms " ,
( unsigned long long ) g_vad_diag . total_windows , reason , probability , model_rms , peak , g_vad_input_rms , g_vad_output_rms ,
g_vad_fsm . confirm_count , g_vad_fsm . talking , busy , g_vad_manual_ptt , g_transmitting , inference_tb / 10.0 , gap_tb / 10.0 ) ;
g_vad_diag . last_busy = busy ;
if ( probability > = g_vad_threshold ) + + g_vad_diag . speech ;
if ( busy ) + + g_vad_diag . busy ;
if ( before . confirm_count & & ! g_vad_fsm . confirm_count & & transition ! = 1 ) + + g_vad_diag . confirm_resets ;
if ( g_vad_diag . windows = = 1 | | probability < g_vad_diag . min_probability ) g_vad_diag . min_probability = probability ;
if ( probability > g_vad_diag . max_probability ) g_vad_diag . max_probability = probability ;
if ( g_vad_input_rms > g_vad_diag . max_input_rms ) g_vad_diag . max_input_rms = g_vad_input_rms ;
if ( model_rms > g_vad_diag . max_model_rms ) g_vad_diag . max_model_rms = model_rms ;
if ( ! g_vad_diag . stats_tb ) g_vad_diag . stats_tb = now ;
if ( now - g_vad_diag . stats_tb > = RADIO_AUDIO_STATS_TB ) {
DEBUG_DEBUG ( DEBUG_CATEGORY_DEBUG , " VAD summary group=%016llx span=%.1fms windows=%u audio=%ums speech=%u "
" busy=%u confirm_resets=%u errors=%u probability=%.3f..%.3f threshold=%.3f reason=%s "
" input_peak=%.1fdBFS model_peak=%.1fdBFS gain=%.1fdB clips=%u inference_max=%.1fms gap_max=%.1fms "
" manual=%d talking=%d tx=%d tx_pending=%u control_pending=%u dropped=%u " ,
( unsigned long long ) g_group_id , ( now - g_vad_diag . stats_tb ) / 10.0 , g_vad_diag . windows , g_vad_diag . windows * 32 ,
g_vad_diag . speech , g_vad_diag . busy , g_vad_diag . confirm_resets , g_vad_diag . errors ,
g_vad_diag . min_probability , g_vad_diag . max_probability , g_vad_threshold , reason ,
20.0f * log10f ( fmaxf ( g_vad_diag . max_input_rms , 0.000001f ) ) ,
20.0f * log10f ( fmaxf ( g_vad_diag . max_model_rms , 0.000001f ) ) , audio_compressor_gain_db ( g_vad_compressor ) ,
audio_compressor_clip_count ( g_vad_compressor ) , g_vad_diag . max_inference_tb / 10.0 , g_vad_diag . max_gap_tb / 10.0 ,
g_vad_manual_ptt , g_vad_fsm . talking , g_transmitting , g_tx_pending , g_control_pending , g_tx_dropped ) ;
g_vad_diag . stats_tb = now ;
g_vad_diag . windows = g_vad_diag . speech = g_vad_diag . busy = g_vad_diag . confirm_resets = g_vad_diag . errors = 0 ;
g_vad_diag . min_probability = g_vad_diag . max_probability = g_vad_diag . max_input_rms = g_vad_diag . max_model_rms = 0 ;
g_vad_diag . max_inference_tb = g_vad_diag . max_gap_tb = 0 ;
}
}
@ -838,7 +916,7 @@ static void radio_audio_vad_feed(const int16_t* frame) {
g_vad_input_rms = sqrtf ( ( float ) ( g_vad_input_sum_sq / count ) ) ;
g_vad_pcm_frames = 0 ; g_vad_input_sum_sq = 0 ;
if ( audio_compressor_process_frame ( g_vad_compressor , g_vad_pcm , g_vad_pcm , count ) ! = 0 ) {
DEBUG_ERROR ( DEBUG_CATEGORY_RADIO , " %s: VAD compressor frame failed count=%d — auto-PTT disabled until capture restart " ,
DEBUG_ERROR ( DEBUG_CATEGORY_DEBUG , " %s: VAD compressor frame failed count=%d — auto-PTT disabled until capture restart " ,
RADIO_AUDIO_ID , count ) ;
g_vad_mode = 0 ; g_prefetch_write = g_prefetch_frames = 0 ;
radio_vad_fsm_reset ( & g_vad_fsm ) ;
@ -891,7 +969,7 @@ void radio_audio_on_frame(struct UTUN_INSTANCE* inst, uint64_t group_id,
uint64_t now = get_time_tb ( ) ;
pthread_mutex_lock ( & g_mtx ) ;
if ( g_active & & group_id = = g_group_id ) {
if ( len > 0 ) g_last_rx_tb = now ; /* busy-сигнал для VAD : чужой аудио-кадр */
if ( len > 0 ) { g_last_rx_tb = now ; g_last_rx_node = src_node_id ; } /* busy-сигнал: чужой аудио-кадр */
struct radio_source * s = radio_src_acquire ( src_node_id , stream_id ) ;
if ( s ) {
if ( len > 0 ) radio_src_push_frame ( s , seq , opus , len , now ) ;