背景
在音频中,相位差是指两个或多个音频信号之间的相位差异或相位偏移。相位是描述周期性信号的位置或偏移的概念,它表示信号波形在给定时间点的位置。
当两个音频信号具有相同的频率,但在给定时间点上的相位不同时,就会出现相位差。相位差通常以角度(度、弧度)或时间(秒、样本)的形式表示。
相位差可以对音频信号的声音特性产生重要影响。当两个声波的相位差是零时,它们处于同相位,会相互增强。当相位差为180度时,它们处于反相位,会相互抵消。
如果视频左右声道有非常严重的相位差,那么当有的设备将立体声转为单声道时会出现相互抵消的情况,导致声音听不清。比如我们将 aripods 一只耳机放入耳机仓,则会自动将立体声转换成单声道输出到单独的一只耳机里面。
要理解这个问题,则需要先大概了解下直播的录音原理。
声音的采集原理
音频采样就是通过麦克风等录音设备将声音信号(声音是由物体震动产生的一种疏密相间的纵波)转化为电信号(声音传递的能量会导致麦克风内部的电压发生变化从而产生电流,可参考动圈麦),然后经过采样,量化,编码的方式将电信号转换成数字信号,最后得到一组二进制码流的过程。上述模数转换的过程也被称为PCM(Pulse Code Modulation,脉冲编码调制)。PCM编码是一种最原始的音频编码,其他编码方式都是在此基础上再次编码和压缩的。

采样
采样是指在时间轴上对模拟信号进行数字化,采样频率是指单位时间内从连续信号中提取并组成离散信号的采样个数,单位是Hz。而且奈奎斯特采样定理指出,如果信号是无限的,并且采样频率高于信号带宽的两倍,那么原来的连续信号就可以从采样样本中完全重建出来。
还没弄明白的点:为什么采样的时候是 48000 Hz 而直播的时候声音的帧率是50 Hz 而视频的是25 Hz,即没有保持原有的采集频率也没和视频帧率保持一致。
量化
量化是指在振幅轴上对模拟信号进行数字化,但由于抽样信号在时间轴上虽然是离散的信号,但仍然是模拟信号,其取值在一定范围内可以有无限多个值(比如49.999999....) ,所以为了用准确的数字表示采样值,我们需要对采样值进行“取整”。量化位宽是指用几位二进制数来存储采样的数据,量化位数越大,声音的质量越高。常用的位宽有8bit和16bit。
编码
编码是指将量化后的采样的十进制数字码流转换成给定字长(量化位深)的二进制码流的过程。
数字信号
上图中就是 8bit 的位宽,所以每三位就一个分割的辅助线。这样声音就采集完毕了。
后期的复盘发现
- 下载录制的 ts 文件
- 通过 ffmpeg 抽出其中的音频存储为 wav 格式
- 然后使用 Audacity 分析音频文件发现左右声道出现了比较严重的相位差(波峰波谷镜像相反的情况)

监控的代码实现
所以我想如何将这种监控自动化呢?周期性做如下操作
- 通过 ffmpeg 来抓取 rtmp 流,录制 30s
- 然后提取音频存储为 wav 格式
- 进行分析
- 自定义报警指标
具体的分析代码,只用最基本的 javax.sound 包即可分析,首先读取到 wav 的元信息,知道其声道数、位宽(前面说的每个量化采样的声音数据大小),然后逐帧分析。每一帧分为前一个位宽长度的数据是左声道、后一个位宽长度的数据是右声道。所以每次读取一帧长度的数据到缓冲区里面,然后分割分组存储到两个List 里面,下面是我自己本地写的一个小 demo
    @RequestMapping("/audio")
    @ResponseBody
    public Map<String, List<Integer>> audio(@RequestParam(name = "id", defaultValue = "unknown id") String id,@RequestParam(name = "offset") Integer offset) {
        // 读取 WAV 文件
        File file = new File("/Users/zhoumengkang/Downloads/rtmp/src/main/resources/" + id + ".wav");
        try {
            AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(file);
            AudioFormat audioFormat = audioInputStream.getFormat();
            int numChannels = audioFormat.getChannels();
            int sampleRate = (int) audioFormat.getSampleRate();
            int bitDepth = audioFormat.getSampleSizeInBits();
            int bytesPerSample = audioInputStream.getFormat().getSampleSizeInBits() / 8;
            // 打印元数据信息
            // 声道数
            System.out.println("Channels: " + numChannels);
            // 采样频率
            System.out.println("Sample Rate: " + sampleRate);
            // 声音量化的位宽
            System.out.println("Bit Depth: " + bitDepth);
            System.out.println("FrameLength: " + audioInputStream.getFrameLength());
            long size = audioInputStream.getFrameLength() / audioFormat.getFrameSize();
            // 总共有多少帧
            System.out.println("FrameSize: " + size);
            // 每帧的数据大小(包含左右两个声道)
            byte[] buffer = new byte[audioFormat.getFrameSize()];
            HashMap<String, List<Integer>> objectObjectHashMap = new HashMap<>();
            List<Integer> left = new ArrayList<>();
            List<Integer> right = new ArrayList<>();
            // 每次读取一个帧的数据
            int n = 0;
            int i = 0;
            while ((audioInputStream.read(buffer)) != -1) {
                if (i < offset) {
                    i++;
                    continue;
                }
                byte[] leftChannelBuffer = new byte[bytesPerSample];
                byte[] rightChannelBuffer = new byte[bytesPerSample];
                System.arraycopy(buffer, 0, leftChannelBuffer, 0, bytesPerSample);
                System.arraycopy(buffer, bytesPerSample, rightChannelBuffer, 0, bytesPerSample);
                // 获取左声道和右声道的值
                int leftValue = bytesToInt(leftChannelBuffer);
                int rightValue = bytesToInt(rightChannelBuffer);
                left.add(leftValue);
                right.add(rightValue);
                n++;
                if (n >= 2000) {
                    break;
                }
                // 处理声道值的逻辑...
                System.out.println(leftValue + "\t+" + rightValue + "\t=" + (leftValue + rightValue));
            }
            objectObjectHashMap.put("left", left);
            objectObjectHashMap.put("right", right);
            return objectObjectHashMap;
            // 其他处理逻辑...
        } catch (UnsupportedAudioFileException | IOException e) {
            e.printStackTrace();
            return null;
        }
    }然后弄一个图标工具,通过 ajax调用该接口。我尝试分析了下相位差很大的音频文件,结果与之前的工具相似,如下

上面是2000帧的对比,如果只是1000帧

这样如果左右声道合并,就会出现声音在每帧里面的数据变得特别小,振幅特别小

对比左声道和合成单声道之后的数据

我也找了一个本地的音频文件(旧红颜.mp3)进行了分析,左右声道有很小的相位差。
蓝色的是左声道,第一张图绿色是右声道,第二张图绿色是混音之后的单声道,可见转成单声道之后,信号基本都是增强。


后面要做的就是如何做好监控和防止误报。如果能把这个方案贡献给视频云团队就更好了,作为一些大会的音频质量的监控。