ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
analysis.h File Reference

Audio Analysis and Debugging Interface. More...

Go to the source code of this file.

Data Structures

struct  audio_analysis_stats_t
 Measurements used to diagnose sent or received audio quality. More...

Functions

int audio_analysis_init (void)
 Measurements used to diagnose sent or received audio quality.
void audio_analysis_track_sent_sample (float sample)
 Track sent audio sample.
void audio_analysis_track_sent_packet (size_t size)
 Track sent packet.
void audio_analysis_track_received_sample (float sample)
 Track received audio sample.
void audio_analysis_track_received_packet (size_t size)
 Track received packet.
const audio_analysis_stats_t * audio_analysis_get_sent_stats (void)
 Get sent audio statistics.
const audio_analysis_stats_t * audio_analysis_get_received_stats (void)
 Get received audio statistics.
void audio_analysis_print_report (void)
 Print audio analysis report.
void audio_analysis_set_aec3_metrics (double echo_return_loss, double echo_return_loss_enhancement, uint64_t delay_ns)
 Set AEC3 echo cancellation metrics.
void audio_analysis_destroy (void)
 Cleanup audio analysis.

Detailed Description

Audio Analysis and Debugging Interface.

Provides audio quality analysis for troubleshooting audio issues. Tracks sent and received audio characteristics for debugging.

Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
2025

Definition in file analysis.h.

Function Documentation

◆ audio_analysis_destroy()

void audio_analysis_destroy ( void )

Cleanup audio analysis.

Definition at line 885 of file analysis.c.

885 {
886 g_analysis_enabled = false;
887
888 // Close WAV files if they were open
889 if (g_sent_wav) {
890 wav_writer_close(g_sent_wav);
891 g_sent_wav = NULL;
892 log_info("Closed sent audio WAV file");
893 }
894 if (g_received_wav) {
895 wav_writer_close(g_received_wav);
896 g_received_wav = NULL;
897 log_info("Closed received audio WAV file");
898 }
899}
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
void wav_writer_close(wav_writer_t *writer)
Close WAV file and finalize header.
Definition wav_writer.c:116

References log_info, and wav_writer_close().

◆ audio_analysis_get_received_stats()

const audio_analysis_stats_t * audio_analysis_get_received_stats ( void )

Get received audio statistics.

Returns
Pointer to analysis stats (do not free)

Definition at line 509 of file analysis.c.

509 {
510 g_received_stats.dc_offset =
511 g_received_stats.total_samples ? (float)(g_received_mean / g_received_stats.total_samples) : 0.0f;
512 g_received_stats.rms_level =
513 g_received_rms_sample_count ? sqrtf(g_received_rms_accumulator / g_received_rms_sample_count) : 0.0f;
514 return &g_received_stats;
515}

References audio_analysis_stats_t::dc_offset.

Referenced by audio_analysis_print_report().

◆ audio_analysis_get_sent_stats()

const audio_analysis_stats_t * audio_analysis_get_sent_stats ( void )

Get sent audio statistics.

Returns
Pointer to analysis stats (do not free)

Definition at line 503 of file analysis.c.

503 {
504 g_sent_stats.dc_offset = g_sent_stats.total_samples ? (float)(g_sent_mean / g_sent_stats.total_samples) : 0.0f;
505 g_sent_stats.rms_level = g_sent_rms_sample_count ? sqrtf(g_sent_rms_accumulator / g_sent_rms_sample_count) : 0.0f;
506 return &g_sent_stats;
507}

References audio_analysis_stats_t::dc_offset.

Referenced by audio_analysis_print_report().

◆ audio_analysis_init()

int audio_analysis_init ( void )

Measurements used to diagnose sent or received audio quality.

Initialize audio analysis

Returns
0 on success, negative on error

Definition at line 113 of file analysis.c.

113 {
114 SAFE_MEMSET(&g_sent_stats, sizeof(g_sent_stats), 0, sizeof(g_sent_stats));
115 SAFE_MEMSET(&g_received_stats, sizeof(g_received_stats), 0, sizeof(g_received_stats));
116
117 g_sent_mean = g_received_mean = 0.0;
118 g_sent_rms_accumulator = g_received_rms_accumulator = 0.0f;
119 g_sent_rms_sample_count = g_received_rms_sample_count = 0;
120
121 // Reset stuttering/gap tracking
122 SAFE_MEMSET(g_received_gap_intervals_ms, sizeof(g_received_gap_intervals_ms), 0, sizeof(g_received_gap_intervals_ms));
123 g_received_gap_count = 0;
124 g_received_silence_start_sample = 0;
125 g_received_last_silence_end_sample = 0;
126 SAFE_MEMSET(g_received_packet_times_ns, sizeof(g_received_packet_times_ns), 0, sizeof(g_received_packet_times_ns));
127 g_received_packet_times_count = 0;
128 SAFE_MEMSET(g_received_packet_sizes, sizeof(g_received_packet_sizes), 0, sizeof(g_received_packet_sizes));
129
130 // Reset echo detection
131 SAFE_MEMSET(g_echo_buffer, sizeof(g_echo_buffer), 0, sizeof(g_echo_buffer));
132 g_echo_buffer_pos = 0;
133 g_echo_correlation_sample_count = 0;
134 for (int i = 0; i < ECHO_DELAY_COUNT; i++) {
135 g_echo_correlation_strength[i] = 0;
136 g_echo_match_count[i] = 0;
137 }
138 g_detected_echo_delay_ms = 0;
139
140 // Reset beep detection
141 SAFE_MEMSET(g_received_beep_window, sizeof(g_received_beep_window), 0, sizeof(g_received_beep_window));
142 g_received_beep_window_idx = 0;
143 g_received_beep_events = 0;
144 g_received_tonal_samples = 0;
145 g_in_beep_burst = false;
146 g_beep_burst_samples = 0;
147
148 int64_t now_us = (int64_t)time_ns_to_us(time_get_ns());
149
150 g_sent_stats.timestamp_start_ns = now_us;
151 g_received_stats.timestamp_start_ns = now_us;
152
153 g_sent_last_sample = 0.0f;
154 g_received_last_sample = 0.0f;
155 g_sent_last_packet_time_us = now_us;
156 g_received_last_packet_time_us = now_us;
157
158 // Initialize WAV file dumping if enabled
159 if (wav_dump_enabled()) {
160 char tmp[PLATFORM_MAX_PATH_LENGTH];
161 if (platform_get_temp_dir(tmp, sizeof(tmp))) {
162 char path[PLATFORM_MAX_PATH_LENGTH];
163 safe_snprintf(path, sizeof(path), "%s/sent_audio.wav", tmp);
164 g_sent_wav = wav_writer_open(path, 48000, 1);
165 safe_snprintf(path, sizeof(path), "%s/received_audio.wav", tmp);
166 g_received_wav = wav_writer_open(path, 48000, 1);
167 }
168 if (g_sent_wav) {
169 log_info("Dumping sent audio to temp dir");
170 }
171 if (g_received_wav) {
172 log_info("Dumping received audio to temp dir");
173 }
174 }
175
176 g_analysis_enabled = true;
177 log_info("Audio analysis enabled");
178 return 0;
179}
#define ECHO_DELAY_COUNT
Definition analysis.c:88
#define SAFE_MEMSET(dest, dest_size, ch, count)
Definition common.h:484
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:95
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
bool wav_dump_enabled(void)
Check if audio dumping is enabled via environment.
Definition wav_writer.c:142
wav_writer_t * wav_writer_open(const char *filepath, int sample_rate, int channels)
Open WAV file for writing.
Definition wav_writer.c:52

References ECHO_DELAY_COUNT, log_info, PLATFORM_MAX_PATH_LENGTH, SAFE_MEMSET, safe_snprintf(), wav_dump_enabled(), and wav_writer_open().

◆ audio_analysis_print_report()

void audio_analysis_print_report ( void )

Print audio analysis report.

Definition at line 526 of file analysis.c.

526 {
527 if (!g_analysis_enabled) {
528 return;
529 }
530
531 int64_t now_us = (int64_t)time_ns_to_us(time_get_ns());
532
533 g_sent_stats.timestamp_end_ns = now_us;
534 g_received_stats.timestamp_end_ns = now_us;
535
536 int64_t sent_duration_ms = (g_sent_stats.timestamp_end_ns - g_sent_stats.timestamp_start_ns) / NS_PER_MS_INT;
537 int64_t recv_duration_ms = (g_received_stats.timestamp_end_ns - g_received_stats.timestamp_start_ns) / NS_PER_MS_INT;
538
541 float sent_rms = sent->rms_level;
542 float recv_rms = received->rms_level;
543
544 log_plain("================================================================================");
545 log_plain(" AUDIO ANALYSIS REPORT ");
546 log_plain("================================================================================");
547 log_plain("SENT AUDIO (Microphone Capture):");
548 log_plain(" Duration: %lld ms", (long long)sent_duration_ms);
549 log_plain(" Total Samples: %llu", (unsigned long long)g_sent_stats.total_samples);
550 log_plain(" Peak Level: %.4f (should be < 1.0)", g_sent_stats.peak_level);
551 log_plain(" RMS Level: %.4f (audio energy/loudness)", sent_rms);
552 log_plain(" DC Offset: %.6f (signal mean)", sent->dc_offset);
553 log_plain(" Clipping Events: %llu samples (%.2f%%)", (unsigned long long)g_sent_stats.clipping_count,
554 g_sent_stats.total_samples > 0 ? (100.0 * g_sent_stats.clipping_count / g_sent_stats.total_samples) : 0);
555 log_plain(" Silent Samples: %llu samples (%.2f%%)", (unsigned long long)g_sent_stats.silent_samples,
556 g_sent_stats.total_samples > 0 ? (100.0 * g_sent_stats.silent_samples / g_sent_stats.total_samples) : 0);
557 if (g_sent_max_silence_burst > 0) {
558 log_plain(" Max Silence Burst: %llu samples", (unsigned long long)g_sent_max_silence_burst);
559 }
560 log_plain(" Packets Sent: %u", g_sent_stats.packets_count);
561 log_plain(" Status: %s", g_sent_stats.clipping_count > 0 ? "CLIPPING DETECTED!" : "OK");
562
563 log_plain("RECEIVED AUDIO (Playback):");
564 log_plain(" Duration: %lld ms", (long long)recv_duration_ms);
565 log_plain(" Total Samples: %llu", (unsigned long long)g_received_stats.total_samples);
566 log_plain(" Peak Level: %.4f", g_received_stats.peak_level);
567 log_plain(" RMS Level: %.4f (audio energy/loudness)", recv_rms);
568 log_plain(" DC Offset: %.6f (signal mean)", received->dc_offset);
569 log_plain(" Clipping Events: %llu samples (%.2f%%)", (unsigned long long)g_received_stats.clipping_count,
570 g_received_stats.total_samples > 0
571 ? (100.0 * g_received_stats.clipping_count / g_received_stats.total_samples)
572 : 0);
573 log_plain(" Silent Samples: %llu samples (%.2f%%)", (unsigned long long)g_received_stats.silent_samples,
574 g_received_stats.total_samples > 0
575 ? (100.0 * g_received_stats.silent_samples / g_received_stats.total_samples)
576 : 0);
577 if (g_received_max_silence_burst > 0) {
578 log_plain(" Max Silence Burst: %llu samples", (unsigned long long)g_received_max_silence_burst);
579 }
580 double low_energy_pct =
581 g_received_stats.total_samples > 0 ? (100.0 * g_received_low_energy_samples / g_received_stats.total_samples) : 0;
582 log_plain(" Very Quiet Samples: %llu samples (%.1f%%) [amplitude < 0.05]",
583 (unsigned long long)g_received_low_energy_samples, low_energy_pct);
584 log_plain(" Packets Received: %u", g_received_stats.packets_count);
585 log_plain(" Status: %s", g_received_stats.total_samples == 0 ? "NO AUDIO RECEIVED!" : "Receiving");
586
587 log_plain("QUALITY METRICS (Scratchy/Distorted Audio Detection):");
588 log_plain("SENT:");
589 log_plain(" Jitter Events: %llu (rapid amplitude changes)", (unsigned long long)g_sent_stats.jitter_count);
590 log_plain(" Discontinuities: %llu (packet arrival gaps > 100ms)",
591 (unsigned long long)g_sent_stats.discontinuity_count);
592 log_plain(" Max Gap Between Packets: %u ms (expected ~20ms per frame)", g_sent_stats.max_gap_ns);
593
594 log_plain("RECEIVED:");
595 log_plain(" Jitter Events: %llu (rapid amplitude changes)",
596 (unsigned long long)g_received_stats.jitter_count);
597 log_plain(" Discontinuities: %llu (packet arrival gaps > 100ms)",
598 (unsigned long long)g_received_stats.discontinuity_count);
599 log_plain(" Max Gap Between Packets: %u ms (expected ~20ms per frame)", g_received_stats.max_gap_ns);
600
601 // Beep/tone artifact detection
602 if (g_received_beep_events > 0 || g_received_tonal_samples > 0) {
603 double tonal_pct =
604 g_received_stats.total_samples > 0 ? (100.0 * g_received_tonal_samples / g_received_stats.total_samples) : 0;
605 log_plain("BEEP/TONE ARTIFACTS:");
606 log_plain(" Beep Events: %llu (short tonal bursts < 500ms)",
607 (unsigned long long)g_received_beep_events);
608 log_plain(" Tonal Samples: %llu samples (%.1f%%) [consistent frequency content]",
609 (unsigned long long)g_received_tonal_samples, tonal_pct);
610
611 if (g_received_beep_events > 10) {
612 log_plain(" 🔴 BEEPING DETECTED: %llu short tonal bursts - likely codec artifacts or system sounds!",
613 (unsigned long long)g_received_beep_events);
614 log_plain(" Possible causes:");
615 log_plain(" - Opus codec producing tonal artifacts during silence/transitions");
616 log_plain(" - Buffer underruns creating synthetic tones");
617 log_plain(" - AEC3 suppressor resonance");
618 log_plain(" - System notification sounds bleeding through");
619 } else if (g_received_beep_events > 3) {
620 log_plain(" ⚠️ Some beep artifacts detected (%llu events)", (unsigned long long)g_received_beep_events);
621 }
622 }
623
624 log_plain("DIAGNOSTICS:");
625 if (g_sent_stats.peak_level == 0) {
626 log_plain(" No audio captured from microphone!");
627 }
628 if (g_received_stats.total_samples == 0) {
629 log_plain(" No audio received from server!");
630 } else if (g_received_stats.peak_level < 0.01f) {
631 log_plain(" ⚠️ Received audio is very quiet (peak < 0.01)");
632 }
633 if (g_sent_stats.clipping_count > 0) {
634 log_plain(" Microphone input is clipping - reduce microphone volume");
635 }
636
637 // Echo detection diagnostics
638 log_plain("ECHO DETECTION (Echo Cancellation Quality Check):");
639 if (g_echo_correlation_sample_count > 0 && g_sent_stats.total_samples > 0) {
640 uint64_t max_matches = 0;
641 int best_delay_idx = -1;
642
643 // Find which delay has the most matches (if any)
644 for (int i = 0; i < ECHO_DELAY_COUNT; i++) {
645 if (g_echo_match_count[i] > max_matches) {
646 max_matches = g_echo_match_count[i];
647 best_delay_idx = i;
648 }
649 }
650
651 double echo_threshold_pct = 5.0; // If > 5% of samples match at a delay, it's echo
652
653 if (best_delay_idx >= 0) {
654 double match_pct = (100.0 * g_echo_match_count[best_delay_idx]) / g_echo_correlation_sample_count;
655 log_plain(" Echo correlation at different delays:");
656 for (int i = 0; i < ECHO_DELAY_COUNT; i++) {
657 double pct = (100.0 * g_echo_match_count[i]) / g_echo_correlation_sample_count;
658 const char *status = pct > echo_threshold_pct ? "⚠️ ECHO DETECTED" : "✓ OK";
659 log_plain(" %3u ms delay: %.1f%% match rate %s", g_echo_delays_ms[i], pct, status);
660 }
661
662 if (match_pct > echo_threshold_pct) {
663 g_detected_echo_delay_ms = g_echo_delays_ms[best_delay_idx];
664 log_plain(" 🔴 ECHO CANCELLATION NOT WORKING: Strong echo at %u ms delay!", g_detected_echo_delay_ms);
665 log_plain(" Received audio contains %.1f%% samples matching sent audio from %u ms ago", match_pct,
666 g_detected_echo_delay_ms);
667 } else {
668 log_plain(" ✓ Echo cancellation working: No significant echo detected");
669 }
670 }
671 } else {
672 log_plain(" Insufficient data for echo detection (need both sent and received audio)");
673 }
674
675 // AEC3 metrics from WebRTC (if available)
676 if (g_aec3_metrics_available) {
677 log_plain("AEC3 METRICS (from WebRTC GetMetrics()):");
678 log_plain(" Echo Return Loss (ERL): %.2f dB (how much echo is attenuated; >10 dB is good)",
679 g_aec3_echo_return_loss);
680 log_plain(" Echo Return Loss Enhancement (ERLE): %.2f dB (residual echo suppression)",
681 g_aec3_echo_return_loss_enhancement);
682 log_plain(" Estimated Echo Delay: %d ms", g_aec3_delay_ns);
683
684 if (g_aec3_echo_return_loss > 10.0) {
685 log_plain(" ✓ Good echo attenuation (ERL > 10 dB)");
686 } else if (g_aec3_echo_return_loss > 3.0) {
687 log_plain(" ⚠️ Moderate echo attenuation (3-10 dB)");
688 } else {
689 log_plain(" 🔴 Poor echo attenuation (ERL < 3 dB)");
690 }
691 }
692
693 // Audio quality diagnostics
694 if (recv_rms < 0.005f) {
695 log_plain(" ⚠️ CRITICAL: Received audio RMS is extremely low (%.6f) - barely audible!", recv_rms);
696 } else if (recv_rms < 0.02f) {
697 log_plain(" ⚠️ WARNING: Received audio RMS is low (%.6f) - may sound quiet or muddy", recv_rms);
698 }
699
700 // Silence analysis
701 double received_silence_pct = g_received_stats.total_samples > 0
702 ? (100.0 * g_received_stats.silent_samples / g_received_stats.total_samples)
703 : 0;
704
705 if (received_silence_pct > 30.0) {
706 log_plain(" ⚠️ SCRATCHY AUDIO DETECTED: Too much silence in received audio!");
707 log_plain(" - Silence: %.1f%% of received samples (should be < 10%%)", received_silence_pct);
708 log_plain(" - Max silence burst: %llu samples", (unsigned long long)g_received_max_silence_burst);
709 log_plain(" - This creates jittery/choppy playback between audio bursts");
710 } else if (received_silence_pct > 15.0) {
711 log_plain(" ⚠️ WARNING: Moderate silence detected (%.1f%%)", received_silence_pct);
712 }
713
714 // Sharp transition analysis (clicks/pops)
715 double sent_sharp_pct =
716 g_sent_transition_samples > 0 ? (100.0 * g_sent_sharp_transitions / g_sent_transition_samples) : 0;
717 double recv_sharp_pct =
718 g_received_transition_samples > 0 ? (100.0 * g_received_sharp_transitions / g_received_transition_samples) : 0;
719
720 // Zero crossing rate analysis (spectral content)
721 // Music: 1-5%, Speech: 5-15%, Static/Noise: 15-50%
722 double sent_zero_cross_pct =
723 g_sent_stats.total_samples > 0 ? (100.0 * g_sent_zero_crossings / g_sent_stats.total_samples) : 0;
724 double recv_zero_cross_pct =
725 g_received_stats.total_samples > 0 ? (100.0 * g_received_zero_crossings / g_received_stats.total_samples) : 0;
726
727 log_plain("WAVEFORM ANALYSIS (Is it clean music or corrupted/static?):");
728 log_plain("SENT AUDIO:");
729 log_plain(" Zero crossings: %.2f%% of samples (music: 1-5%%, noise: 15-50%%)", sent_zero_cross_pct);
730 log_plain(" Sharp transitions (clicks/pops): %.2f%% of samples", sent_sharp_pct);
731 log_plain(" Clipping samples: %llu (%.3f%%)", (unsigned long long)g_sent_clipping_samples,
732 g_sent_stats.total_samples > 0 ? (100.0 * g_sent_clipping_samples / g_sent_stats.total_samples) : 0);
733
734 log_plain("RECEIVED AUDIO:");
735 log_plain(" Zero crossings: %.2f%% of samples (music: 1-5%%, noise: 15-50%%)", recv_zero_cross_pct);
736 log_plain(" Sharp transitions (clicks/pops): %.2f%% of samples", recv_sharp_pct);
737 log_plain(" Clipping samples: %llu (%.3f%%)", (unsigned long long)g_received_clipping_samples,
738 g_received_stats.total_samples > 0 ? (100.0 * g_received_clipping_samples / g_received_stats.total_samples)
739 : 0);
740 log_plain(" Zero crossing increase: %.2f%% higher than sent (indicates corruption)",
741 recv_zero_cross_pct - sent_zero_cross_pct);
742
743 // Musicality verdict
744 log_plain("SOUND QUALITY VERDICT:");
745 if (recv_zero_cross_pct > 10.0) {
746 log_plain(" ⚠️ SOUNDS LIKE STATIC/DISTORTED: Excessive zero crossings (%.2f%%) = high frequency noise",
747 recv_zero_cross_pct);
748 log_plain(" Increase from sent: %.2f%% (waveform corruption detected)",
749 recv_zero_cross_pct - sent_zero_cross_pct);
750 log_plain(" Likely causes: Opus codec artifacts, jitter buffer issues, or packet delivery gaps");
751 } else if (recv_zero_cross_pct - sent_zero_cross_pct > 3.0) {
752 log_plain(" ⚠️ SOUNDS CORRUPTED: Zero crossing rate increased by %.2f%% (should be ±0.5%%)",
753 recv_zero_cross_pct - sent_zero_cross_pct);
754 log_plain(" Indicates waveform distortion from network/processing artifacts");
755 } else if (recv_sharp_pct > 2.0) {
756 log_plain(" ⚠️ SOUNDS LIKE STATIC: High click/pop rate (%.2f%%) indicates audio artifacts", recv_sharp_pct);
757 log_plain(" Likely causes: Packet loss, jitter buffer issues, or frame discontinuities");
758 } else if (g_received_clipping_samples > (g_received_stats.total_samples / 1000)) {
759 log_plain(" ⚠️ SOUNDS DISTORTED: Significant clipping detected (%.3f%%)",
760 100.0 * g_received_clipping_samples / g_received_stats.total_samples);
761 log_plain(" Likely causes: AGC too aggressive, gain too high, or codec compression artifacts");
762 } else if (low_energy_pct > 50.0 && recv_rms < 0.05f) {
763 log_plain(" ⚠️ SOUNDS MUDDY/QUIET: Over 50%% very quiet samples + low RMS");
764 log_plain(" Audio may sound unclear or like background noise rather than music");
765 } else if (received_silence_pct > 10.0) {
766 log_plain(" ⚠️ SOUNDS SCRATCHY: Excessive silence (%.1f%%) causes dropouts", received_silence_pct);
767 } else if (recv_rms > 0.08f && recv_zero_cross_pct < 6.0 && recv_sharp_pct < 1.0 &&
768 g_received_clipping_samples == 0) {
769 log_plain(" ✓ SOUNDS LIKE MUSIC: Good RMS (%.4f), clean waveform (%.2f%% zero crossings), minimal artifacts",
770 recv_rms, recv_zero_cross_pct);
771 log_plain(" Audio quality acceptable for communication");
772 } else {
773 log_plain(" ? BORDERLINE: Check specific metrics above");
774 }
775
776 // Low energy audio analysis
777 if (low_energy_pct > 50.0) {
778 log_plain(" ⚠️ WARNING: Over 50%% of received samples are very quiet (< 0.05 amplitude)");
779 log_plain(" - This makes audio sound muddy, unclear, or hard to understand");
780 log_plain(" - Caused by: Mixing other clients' audio with your own at wrong levels");
781 }
782
783 // Stuttering/periodic gap detection using packet inter-arrival times
784 if (g_received_packet_times_count >= 5) {
785 uint32_t inter_arrival_times_ms[MAX_PACKET_SAMPLES - 1];
786 uint32_t inter_arrival_count = 0;
787 uint32_t min_interval_ms = 0xFFFFFFFF;
788 uint32_t max_interval_ms = 0;
789 uint64_t sum_intervals_ms = 0;
790 uint32_t intervals_around_50ms = 0; // Count intervals ~40-60ms
791
792 // Calculate inter-packet arrival times
793 for (uint32_t i = 1; i < g_received_packet_times_count; i++) {
794 uint64_t prev_ns = g_received_packet_times_ns[i - 1];
795 uint64_t curr_ns = g_received_packet_times_ns[i];
796
797 uint64_t gap_ns = time_elapsed_ns(prev_ns, curr_ns);
798 uint32_t gap_ms = (uint32_t)time_ns_to_ms(gap_ns);
799
800 inter_arrival_times_ms[inter_arrival_count++] = gap_ms;
801 if (gap_ms < min_interval_ms)
802 min_interval_ms = gap_ms;
803 if (gap_ms > max_interval_ms)
804 max_interval_ms = gap_ms;
805 sum_intervals_ms += gap_ms;
806
807 // Check if interval is ~50ms (within 15ms tolerance for network jitter)
808 if (gap_ms >= 35 && gap_ms <= 70) {
809 intervals_around_50ms++;
810 }
811 }
812
813 uint32_t avg_interval_ms = (uint32_t)(sum_intervals_ms / inter_arrival_count);
814 uint32_t interval_consistency = (intervals_around_50ms * 100) / inter_arrival_count;
815
816 // Calculate how much audio is in each packet
817 // Total decoded samples / number of packets = average samples per packet
818 // At 48kHz, 960 samples = 1 Opus frame = 20ms
819 double avg_samples_per_packet =
820 g_received_stats.total_samples > 0 ? (double)g_received_stats.total_samples / inter_arrival_count : 0;
821 double frames_per_packet = avg_samples_per_packet / 960.0; // 960 samples = 1 frame @ 48kHz
822 double ms_audio_per_packet = frames_per_packet * 20.0; // 20ms per frame
823
824 // Detect if stuttering is periodic (consistent ~50ms intervals)
825 if (intervals_around_50ms >= (inter_arrival_count * 2 / 3)) {
826 // More than 66% of packets are ~50ms apart - clear periodic stuttering
827 log_plain(" 🔴 PERIODIC STUTTERING DETECTED: Server sends packets every ~%u ms (should be ~20ms)!",
828 avg_interval_ms);
829 log_plain(" - Packet inter-arrival: %u-%u ms (avg: %u ms)", min_interval_ms, max_interval_ms, avg_interval_ms);
830 log_plain(" - %u/%u packets (~%u%%) are ~50ms apart (CLEAR STUTTERING PATTERN)", intervals_around_50ms,
831 inter_arrival_count, interval_consistency);
832
833 log_plain(" - PACKET ANALYSIS:");
834 log_plain(" - Total audio samples: %llu over %u packets", (unsigned long long)g_received_stats.total_samples,
835 inter_arrival_count);
836 log_plain(" - Avg samples per packet: %.0f (= %.2f Opus frames = %.1f ms)", avg_samples_per_packet,
837 frames_per_packet, ms_audio_per_packet);
838
839 if (frames_per_packet < 1.5) {
840 log_plain(" - ❌ PROBLEM: Each packet contains < 1.5 frames (should be 2-3 frames!)");
841 log_plain(" - With only %.1f frames per packet arriving every %u ms, there are gaps between chunks",
842 frames_per_packet, avg_interval_ms);
843 log_plain(" - Audio plays for ~%.0f ms, then %u ms gap, then plays again", ms_audio_per_packet,
844 avg_interval_ms - (uint32_t)ms_audio_per_packet);
845 } else if (frames_per_packet > 2.5) {
846 log_plain(" - ✓ Packets contain %.1f frames (~%.0f ms audio each)", frames_per_packet,
847 ms_audio_per_packet);
848 log_plain(" - Should play smoothly if jitter buffer is large enough");
849 log_plain(" - If still stuttering, issue is jitter buffer depth or timing precision");
850 } else {
851 log_plain(" - Packets contain %.1f frames (~%.0f ms)", frames_per_packet, ms_audio_per_packet);
852 log_plain(" - Borderline: buffer needs to hold %.0f ms to bridge %.u ms gap", ms_audio_per_packet,
853 avg_interval_ms - (uint32_t)ms_audio_per_packet);
854 }
855 } else if (avg_interval_ms > 30) {
856 log_plain(" ⚠️ AUDIO DELIVERY INCONSISTENCY: Server packets arrive every ~%u ms (expected ~20ms)",
857 avg_interval_ms);
858 log_plain(" - Interval range: %u-%u ms", min_interval_ms, max_interval_ms);
859 log_plain(" - This causes dropouts and buffering issues");
860 }
861 }
862
863 // Packet delivery gaps
864 if (g_received_stats.max_gap_ns > 40) {
865 log_plain(" ⚠️ DISTORTION DETECTED: Packet delivery gaps too large!");
866 log_plain(" - Max gap: %u ms (should be ~20ms for smooth audio)", g_received_stats.max_gap_ns);
867 if (g_received_stats.max_gap_ns > 80) {
868 log_plain(" - SEVERE: Gaps > 80ms cause severe distortion and dropouts");
869 } else if (g_received_stats.max_gap_ns > 50) {
870 log_plain(" - Gaps > 50ms cause noticeable distortion");
871 }
872 }
873 if (g_received_stats.discontinuity_count > 0) {
874 log_plain(" Packet delivery discontinuities: %llu gaps > 100ms detected",
875 (unsigned long long)g_received_stats.discontinuity_count);
876 }
877 if (g_received_stats.jitter_count > (g_received_stats.total_samples / 100)) {
878 log_plain(" High jitter detected: > 1%% of samples have rapid amplitude changes");
879 log_plain(" - May indicate buffer underruns from sparse packet delivery");
880 }
881
882 log_plain("================================================================================");
883}
const audio_analysis_stats_t * audio_analysis_get_sent_stats(void)
Get sent audio statistics.
Definition analysis.c:503
#define MAX_PACKET_SAMPLES
Definition analysis.c:76
const audio_analysis_stats_t * audio_analysis_get_received_stats(void)
Get received audio statistics.
Definition analysis.c:509
unsigned int uint32_t
Definition common.h:62
unsigned long long uint64_t
Definition common.h:63
#define log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
Definition log/log.h:665
#define NS_PER_MS_INT
Definition time.h:168
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition time.c:123
Measurements used to diagnose sent or received audio quality.
Definition analysis.h:31

References audio_analysis_get_received_stats(), audio_analysis_get_sent_stats(), audio_analysis_stats_t::dc_offset, ECHO_DELAY_COUNT, log_plain, MAX_PACKET_SAMPLES, NS_PER_MS_INT, audio_analysis_stats_t::rms_level, and time_elapsed_ns().

◆ audio_analysis_set_aec3_metrics()

void audio_analysis_set_aec3_metrics ( double echo_return_loss,
double echo_return_loss_enhancement,
uint64_t delay_ns )

Set AEC3 echo cancellation metrics.

Parameters
echo_return_lossEcho return loss (dB) - how much echo is attenuated
echo_return_loss_enhancementAdditional echo suppression (dB)
delay_nsEstimated echo delay in nanoseconds

Definition at line 517 of file analysis.c.

517 {
518 // Store AEC3 metrics for reporting
519 // These come from WebRTC EchoControl::GetMetrics() call
520 g_aec3_echo_return_loss = echo_return_loss;
521 g_aec3_echo_return_loss_enhancement = echo_return_loss_enhancement;
522 g_aec3_delay_ns = delay_ns;
523 g_aec3_metrics_available = true;
524}

Referenced by client_audio_pipeline_process_duplex().

◆ audio_analysis_track_received_packet()

void audio_analysis_track_received_packet ( size_t size)

Track received packet.

Parameters
sizePacket size in bytes

Definition at line 470 of file analysis.c.

470 {
471 (void)size; // Unused parameter - reserved for future per-packet analysis
472 if (!g_analysis_enabled)
473 return;
474
475 uint64_t now_ns = time_get_ns();
476 int64_t now_us = (int64_t)time_ns_to_us(now_ns);
477
478 // Track packet timing for stuttering detection
479 if (g_received_packet_times_count < MAX_PACKET_SAMPLES) {
480 g_received_packet_times_ns[g_received_packet_times_count++] = now_ns;
481 }
482
483 // Detect gaps between consecutive packets (discontinuity)
484 if (g_received_stats.packets_count > 0) {
485 int64_t gap_us = now_us - g_received_last_packet_time_us;
486 int32_t gap_ms = (int32_t)(gap_us / 1000);
487
488 // Expected: ~20ms per Opus frame, flag if gap > 100ms
489 if (gap_ms > 100) {
490 g_received_stats.discontinuity_count++;
491 }
492
493 // Track max gap
494 if (gap_ms > (int32_t)g_received_stats.max_gap_ns) {
495 g_received_stats.max_gap_ns = (uint32_t)gap_ms;
496 }
497 }
498
499 g_received_last_packet_time_us = now_us;
500 g_received_stats.packets_count++;
501}

References MAX_PACKET_SAMPLES.

◆ audio_analysis_track_received_sample()

void audio_analysis_track_received_sample ( float sample)

Track received audio sample.

Parameters
sampleAudio sample value

Definition at line 280 of file analysis.c.

280 {
281 if (!g_analysis_enabled)
282 return;
283
284 g_received_stats.total_samples++;
285
286 // Track peak level
287 float abs_sample = fabsf(sample);
288 if (abs_sample > g_received_stats.peak_level) {
289 g_received_stats.peak_level = abs_sample;
290 }
291
292 // Track clipping (samples > 1.0) - indicates distortion
293 if (abs_sample > 1.0f) {
294 g_received_stats.clipping_count++;
295 g_received_clipping_samples++;
296 }
297
298 // Detect sharp transitions (sudden amplitude jumps > 0.3) - indicates clicks/pops/artifacts
299 float amp_change = fabsf(sample - g_received_last_sample);
300 if (amp_change > 0.3f) {
301 g_received_sharp_transitions++;
302 }
303 g_received_transition_samples++;
304
305 // Accumulate for mean calculation
306 g_received_mean += sample;
307
308 // Detect zero crossings (waveform crossing zero) - indicates spectral content
309 // Use file-scope static variable for prev sample tracking
310 // (This function is called from the protocol reception thread, separate from the
311 // audio capture thread, so using distinct static variables is safe)
312 static float s_received_prev_sample_for_zero_crossing = 0.0f;
313 if ((s_received_prev_sample_for_zero_crossing > 0 && sample < 0) ||
314 (s_received_prev_sample_for_zero_crossing < 0 && sample > 0)) {
315 g_received_zero_crossings++;
316 }
317 s_received_prev_sample_for_zero_crossing = sample;
318
319 // Track silence and low-energy audio
320 if (abs_sample < 0.001f) {
321 g_received_stats.silent_samples++;
322 g_received_silence_burst++;
323
324 // Track when silence started
325 if (g_received_silence_burst == 1) {
326 g_received_silence_start_sample = g_received_stats.total_samples;
327 }
328 } else {
329 // Silence ended - track gap interval and max burst length
330 if (g_received_silence_burst > 0) {
331 // Calculate time gap between end of last silence and start of this one
332 if (g_received_last_silence_end_sample > 0) {
333 uint64_t samples_between = g_received_silence_start_sample - g_received_last_silence_end_sample;
334 uint32_t ms_between = (uint32_t)(samples_between * 1000 / 48000); // Convert samples to ms at 48kHz
335
336 // Track the gap interval if we have room
337 if (g_received_gap_count < MAX_GAP_SAMPLES) {
338 g_received_gap_intervals_ms[g_received_gap_count++] = ms_between;
339 }
340 }
341
342 g_received_last_silence_end_sample = g_received_stats.total_samples;
343
344 // Track max burst length
345 if (g_received_silence_burst > g_received_max_silence_burst) {
346 g_received_max_silence_burst = g_received_silence_burst;
347 }
348 }
349 g_received_silence_burst = 0;
350 }
351
352 // Track very quiet audio (< 0.05 amplitude) which contributes to muddy/quiet perception
353 if (abs_sample < 0.05f) {
354 g_received_low_energy_samples++;
355 }
356
357 // Detect jitter: rapid amplitude changes > 0.5 between consecutive samples
358 float delta = fabsf(sample - g_received_last_sample);
359 if (delta > 0.5f) {
360 g_received_stats.jitter_count++;
361 }
362 g_received_last_sample = sample;
363
364 // Accumulate for RMS calculation
365 g_received_rms_accumulator += sample * sample;
366 g_received_rms_sample_count++;
367
368 // Echo detection: check if received sample matches sent sample from N ms ago
369 // This detects if echo cancellation is working (it shouldn't find matches)
370 if (g_echo_correlation_sample_count < 500000) { // Limit to first ~10 seconds
371 for (int delay_idx = 0; delay_idx < ECHO_DELAY_COUNT; delay_idx++) {
372 // Calculate sample delay: delay_ms * (sample_rate / 1000)
373 uint32_t delay_samples = (g_echo_delays_ms[delay_idx] * 48000) / 1000;
374
375 // Get sent sample from that delay ago (from circular buffer)
376 uint64_t sent_pos;
377 if (g_echo_buffer_pos >= delay_samples) {
378 sent_pos = g_echo_buffer_pos - delay_samples;
379 } else {
380 sent_pos = (g_echo_buffer_pos + ECHO_BUFFER_SIZE) - delay_samples;
381 }
382
383 float sent_sample = g_echo_buffer[sent_pos];
384
385 // Check if samples match (correlation threshold = 0.1)
386 float diff = fabsf(sample - sent_sample);
387 if (diff < 0.1f && fabsf(sent_sample) > 0.01f) { // Only count if sent is not silence
388 g_echo_match_count[delay_idx]++;
389 g_echo_correlation_strength[delay_idx] += (0.1f - diff); // Accumulate strength
390 }
391 }
392 g_echo_correlation_sample_count++;
393 }
394
395 // Beep/tone artifact detection
396 // Store sample in sliding window for frequency analysis
397 g_received_beep_window[g_received_beep_window_idx] = sample;
398 g_received_beep_window_idx = (g_received_beep_window_idx + 1) % BEEP_WINDOW_SIZE;
399
400 // Analyze window every 10ms (480 samples at 48kHz)
401 if (g_received_beep_window_idx == 0 && g_received_stats.total_samples > BEEP_WINDOW_SIZE) {
402 // Calculate zero-crossing rate in this window
403 int zero_crossings = 0;
404 float min_amp = 1.0f, max_amp = 0.0f;
405 float sum_amp = 0.0f;
406 float prev = g_received_beep_window[0];
407
408 for (int i = 1; i < BEEP_WINDOW_SIZE; i++) {
409 float curr = g_received_beep_window[i];
410 float abs_curr = fabsf(curr);
411
412 // Track amplitude range
413 if (abs_curr > max_amp)
414 max_amp = abs_curr;
415 if (abs_curr < min_amp)
416 min_amp = abs_curr;
417 sum_amp += abs_curr;
418
419 // Count zero crossings
420 if ((prev > 0 && curr < 0) || (prev < 0 && curr > 0)) {
421 zero_crossings++;
422 }
423 prev = curr;
424 }
425
426 float avg_amp = sum_amp / BEEP_WINDOW_SIZE;
427 float amp_range = max_amp - min_amp;
428
429 // A beep/tone has:
430 // 1. High zero-crossing rate (>20 per 10ms = >2000Hz equivalent, or 5-20 = 500-2000Hz)
431 // 2. Consistent amplitude (range/avg < 0.5 means sine-wave like)
432 // 3. Non-trivial amplitude (avg > 0.02)
433 bool is_tonal = (zero_crossings >= 5 && zero_crossings <= 100) && // 500Hz-10kHz range
434 (avg_amp > 0.02f) && // Not silence
435 (amp_range < avg_amp * 1.5f); // Relatively consistent amplitude
436
437 if (is_tonal) {
438 g_received_tonal_samples += BEEP_WINDOW_SIZE;
439
440 if (!g_in_beep_burst) {
441 // Starting a new beep burst
442 g_in_beep_burst = true;
443 g_beep_burst_samples = BEEP_WINDOW_SIZE;
444 } else {
445 g_beep_burst_samples += BEEP_WINDOW_SIZE;
446 }
447 } else {
448 if (g_in_beep_burst) {
449 // Beep burst ended
450 // Only count as beep event if it was short (< 500ms = 24000 samples)
451 // Long tonal sounds are likely music, not artifacts
452 if (g_beep_burst_samples > 0 && g_beep_burst_samples < 24000) {
453 g_received_beep_events++;
454 g_received_stats.beep_events = g_received_beep_events;
455 }
456 g_in_beep_burst = false;
457 g_beep_burst_samples = 0;
458 }
459 }
460
461 g_received_stats.tonal_samples = g_received_tonal_samples;
462 }
463
464 // Write to WAV file if enabled
465 if (g_received_wav) {
466 wav_writer_write(g_received_wav, &sample, 1);
467 }
468}
#define MAX_GAP_SAMPLES
Definition analysis.c:69
#define ECHO_BUFFER_SIZE
Definition analysis.c:82
#define BEEP_WINDOW_SIZE
Definition analysis.c:105
int wav_writer_write(wav_writer_t *writer, const float *samples, int num_samples)
Write audio samples to WAV file.
Definition wav_writer.c:98

References BEEP_WINDOW_SIZE, ECHO_BUFFER_SIZE, ECHO_DELAY_COUNT, MAX_GAP_SAMPLES, and wav_writer_write().

Referenced by audio_process_received_samples().

◆ audio_analysis_track_sent_packet()

void audio_analysis_track_sent_packet ( size_t size)

Track sent packet.

Parameters
sizePacket size in bytes

Definition at line 253 of file analysis.c.

253 {
254 (void)size; // Unused parameter - reserved for future per-packet analysis
255 if (!g_analysis_enabled)
256 return;
257
258 int64_t now_us = (int64_t)time_ns_to_us(time_get_ns());
259
260 // Detect gaps between consecutive packets (discontinuity)
261 if (g_sent_stats.packets_count > 0) {
262 int64_t gap_us = now_us - g_sent_last_packet_time_us;
263 int32_t gap_ms = (int32_t)(gap_us / 1000);
264
265 // Expected: ~20ms per Opus frame, flag if gap > 100ms
266 if (gap_ms > 100) {
267 g_sent_stats.discontinuity_count++;
268 }
269
270 // Track max gap
271 if (gap_ms > (int32_t)g_sent_stats.max_gap_ns) {
272 g_sent_stats.max_gap_ns = (uint32_t)gap_ms;
273 }
274 }
275
276 g_sent_last_packet_time_us = now_us;
277 g_sent_stats.packets_count++;
278}

◆ audio_analysis_track_sent_sample()

void audio_analysis_track_sent_sample ( float sample)

Track sent audio sample.

Parameters
sampleAudio sample value

Definition at line 181 of file analysis.c.

181 {
182 if (!g_analysis_enabled)
183 return;
184
185 g_sent_stats.total_samples++;
186
187 // Track peak level
188 float abs_sample = fabsf(sample);
189 if (abs_sample > g_sent_stats.peak_level) {
190 g_sent_stats.peak_level = abs_sample;
191 }
192
193 // Track clipping (samples > 1.0) - indicates distortion
194 if (abs_sample > 1.0f) {
195 g_sent_stats.clipping_count++;
196 g_sent_clipping_samples++;
197 }
198
199 // Detect sharp transitions (sudden amplitude jumps > 0.3) - indicates clicks/pops
200 float amp_change = fabsf(sample - g_sent_last_sample);
201 if (amp_change > 0.3f) {
202 g_sent_sharp_transitions++;
203 }
204 g_sent_transition_samples++;
205
206 // Accumulate for mean calculation
207 g_sent_mean += sample;
208
209 // Detect zero crossings (waveform crossing zero) - indicates spectral content
210 // Use file-scope static variable for prev sample tracking
211 // (This function is only called from the audio capture thread, but using file-scope
212 // static is clearer and avoids shadowing the existing g_sent_last_sample variable)
213 static float s_sent_prev_sample_for_zero_crossing = 0.0f;
214 if ((s_sent_prev_sample_for_zero_crossing > 0 && sample < 0) ||
215 (s_sent_prev_sample_for_zero_crossing < 0 && sample > 0)) {
216 g_sent_zero_crossings++;
217 }
218 s_sent_prev_sample_for_zero_crossing = sample;
219
220 // Track silence (very low level)
221 if (abs_sample < 0.001f) {
222 g_sent_stats.silent_samples++;
223 g_sent_silence_burst++;
224 } else {
225 // Silence ended - track max burst length
226 if (g_sent_silence_burst > g_sent_max_silence_burst) {
227 g_sent_max_silence_burst = g_sent_silence_burst;
228 }
229 g_sent_silence_burst = 0;
230 }
231
232 // Detect jitter: rapid amplitude changes > 0.5 between consecutive samples
233 float delta = fabsf(sample - g_sent_last_sample);
234 if (delta > 0.5f) {
235 g_sent_stats.jitter_count++;
236 }
237 g_sent_last_sample = sample;
238
239 // Accumulate for RMS calculation
240 g_sent_rms_accumulator += sample * sample;
241 g_sent_rms_sample_count++;
242
243 // Write to WAV file if enabled
244 if (g_sent_wav) {
245 wav_writer_write(g_sent_wav, &sample, 1);
246 }
247
248 // Store in echo detection buffer (circular)
249 g_echo_buffer[g_echo_buffer_pos] = sample;
250 g_echo_buffer_pos = (g_echo_buffer_pos + 1) % ECHO_BUFFER_SIZE;
251}

References ECHO_BUFFER_SIZE, and wav_writer_write().