qt 实现音视频的分贝检测系统

项目场景:

目前的产品经常播放m3u8流,有的视频声音正常,有的视频声音就偏低,即使放到最大音量声音也是比较小,所以就产生了某种需求,能否自动感知视频声音的大小,如果发现声音比较小的情况,就自动放大比如系统音量增益等。


解决该问题所尝试的研究

一、命令行工具

1、tinymix

linux中主流的音频体系结构是ALSA(Advanced Linux Sound Architecture),ALSA在内核驱动层提供了alsa-driver,在应用层提供了alsa-lib,应用程序只需要调用alsa-lib提供的API就可以完成对底层硬件的操作。但是Android中没有使用标准的ALSA,而是一个ALSA的简化版叫做tinyalsa。Android中使用tinyalsa控制管理所有模式的音频通路,我们也可以使用tinyalsa提供的工具进行查看、调试。

编译tinyalsa后生成四个小工具,本次尝试使用tinymix工具来解决问题
tinymix是一个可以在Android平台上进行音频控制的命令行工具。它是Android Open Source Project (AOSP)中的一部分,可以被用于控制Android设备上的音量。

tinymix 命令可以控制音量、开关声音、调整声道平衡和控制麦克风增益等等。这个工具的主要用途是在不影响当前正在运行的程序的情况下,调整音频设置。

  1. tinymix
  2. tinyplay
  3. tinycap
  4. tinypcminfo

如下图所示,直接输入tinymix可以得到音频通路相关的各项配置参数。也可以通过添加参数修改其中的配置,如下面的系统命令通过设置 VBC DACL DG 和 VBC DACR DG ,便是设置数字增益,其范围是0~126,不过实际运行过程中发现,比如设置了 tinymix VBC DACL DG 3,过几秒之后,再查看发现其值又回到了26,不太清楚具体缘由。
qt 实现音视频的分贝检测系统_第1张图片

system(QString("tinymix -D 0 \"VBC DACL DG Set\" %1").arg(20).toLatin1().data()); //14
system(QString("tinymix -D 0 \"VBC DACR DG Set\" %1").arg(20).toLatin1().data());

2、pactl

查看一下设备索引

pactl list sinks

可以看到目前的参数
qt 实现音视频的分贝检测系统_第2张图片

设置绝对音量,0%-100%,1表示声卡号。

pactl set-sink-volume 1 90%

设置相对音量,增大10%

pactl set-sink-volume 1 +10%

设置相对音量,减小10%

pactl set-sink-volume 1 -10%

增加3db

pactl set-sink-volume 1 +3dB

该命令可以尝试实现

二、检测程序

通过实时检测视频中音频信息,计算出分贝值,来判断该视频的声音大小。

1.PCM数据基础

PCM(Pulse Code Modulation)也被称为脉冲编码调制,是数字通信的编码方式之一。PCM中的声音数据没有被压缩,它将输入的模拟信号进行采样、量化和编码,用二进制进行编码的数来代表模拟信号的幅度,即标准的数字音频数据。

采样率

采样率表示音频信号每秒的数字快照数。该速率决定了音频文件的频率范围。采样率越高,数字波形的形状越接近原始模拟波形。低采样率会限制可录制的频率范围,这可导致录音表现原始声音的效果不佳。一般数字音频常用的采样率电话频率8kHz、CD频率44.1kHz、DVD频率48kHz。

位深度

位深度决定动态范围。采样声波时,为每个采样指定最接近原始声波振幅的振幅值。较高的位深度可提供更多可能的振幅值,产生更大的动态范围、更低的噪声基准和更高的保真度。普通的CD是16-bit。

通道

通道个数。常见的音频有立体声(stereo)和单声道(mono)两种类型,立体声包含左声道和右声道。另外还有环绕立体声等其它不太常用的类型。

Sign

表示样本数据是否是有符号位,比如用一字节表示的样本数据,有符号的话表示范围为-128 ~ 127,无符号是0 ~ 255。

字节序

字节序是little-endian还是big-endian。通常均为little-endian
PCM信号的两个重要指标是采样频率和量化精度,当在播放音乐时,应用程序从存储介质中读取音频数据(MP3、WMA、AAC等),经过解码后,最终送到音频驱动程序中的就是PCM数据,反过来,在录音时,音频驱动不停地把采样所得的PCM数据送回给应用程序,由应用程序完成压缩、存储等任务。下面我们展开介绍下PCM音频的存储及操作

PCM音频数据存储方式

如果是单声道的音频文件,采样数据按时间的先后顺序依次存入(有的时候也会采用LRLRLR方式存储,只是另一个声道的数据为0),如果是双声道的话就按照LRLRLR的方式存储,存储的时候与字节序有关。

2.数据提取

涉及到多通道的数据如何排列和提取
按照双声道的LRLRLR的PCM音频数据可以通过将它们交叉的读出来的方式来分离左右声道的数据。

int pcm_s16le_split(const char* file, const char* out_lfile, const char* out_rfile) {
     FILE *fp = fopen(file, "rb+");
     if (fp == NULL) {
         printf("open %s failed\n", file);
         return -1;
     }
     FILE *fp1 = fopen(out_lfile, "wb+");
     if (fp1 == NULL) {
         printf("open %s failed\n", out_lfile);
         return -1;
     }
     FILE *fp2 = fopen(out_rfile, "wb+");
     if (fp2 == NULL) {
         printf("open %s failed\n", out_rfile);
         return -1;
     }
     char * sample = (char *)malloc(4);
     while(!feof(fp)) {
         fread(sample, 1, 4, fp);
         //L
         fwrite(sample, 1, 2, fp1);
         //R
         fwrite(sample + 2, 1, 2, fp2);
     }
     free(sample);
     fclose(fp);
     fclose(fp1);
     fclose(fp2);
     return 0;
 }

3. 分贝计算(参考网上资源)

公式:

在这里插入图片描述

参数:Pref:就是声音总的振幅最大值;Prms:就是当前声音的振幅值;Lp:就是我们需要的声音分贝值了。

比如:我们声音是无符号16bit深度的,那么其每个采样点的值应该在(02^16-1既:065535)范围内,带入公式我们可以计算到(不用除以最大振幅值):20*log(65535)=96.32db,所以根据这个我们只要拿到某个采样点的振幅值,也就是当前声音采样点转成16bit后的值就可以计算出相应的分贝值了。那么怎么求声音采样点的振幅呢?这是一个问题,不过也有解决办法了。

获取pcm声音采样点的振幅:

这里以我项目中用OpenSL来播放FFmpeg重采样生成的PCM声音为例,PCM声音是重采样为无符号16bit的深度的,然后我们需要得到某一时间(一般是零点几毫秒)PCM所在内存的地址和PCM声音的大小,而16bit也就是16bit/8bit=2byte,在c语言中2byte用short int来表示,因此我们可以从PCM所在地址里面按顺序取出2个byte的数据然后转化成short int的值就可以拿到当前采样点的振幅了,获取的方式是用c语言中的memcpy拷贝2个字节的数据求值就可以了。(注:因为采用点很密集,如果每个采用点都计算一下分贝的话,会消耗一定的性能或者导致声音播放不连贯,所这里采用取其绝对值和的平均值就可以了,因为在这段时间内,我们看不出任何的区别。)

/**
* 获取所有振幅之平均值 计算db (振幅最大值 2^16-1 = 65535 最大值是 96.32db)
* 16 bit == 2字节 == short int
* 无符号16bit:96.32=20*lg(65535);
*
* @param pcmdata 转换成char类型,才可以按字节操作
* @param size pcmdata的大小
* @return
*/
 
int Audio::getPcmDB(const unsigned char *pcmdata, size_t size) {
 
    int db = 0;
    short int value = 0;
    double sum = 0;
 
    for(int i = 0; i < size; i += 2)
    {
        memcpy(&value, pcmdata+i, 2); //获取2个字节的大小(值)
        sum += abs(value); //绝对值求和
    }
    sum = sum / (size / 2); //求平均值(2个字节表示一个振幅,所以振幅个数为:size/2个)
 
    if(sum > 0)
    {
        db = (int)(20.0*log10(sum));
    }
    return db;
 
}

本项目解决方案

1、效果图

qt 实现音视频的分贝检测系统_第3张图片
1 支持单文件检测
2 支持目录检测
3 支持常用的音视频格式比如 mp3、 mp4、 wav、 mov等
4 支持检测报告输出
5 在线的资源播放依赖于系统的解码能力,比如windows下依赖 directshow linux下依赖 gstreamer
6 目前只提供windows版本

注意:windows下选择m3u8文件需支持其传输协议,需要安装directshow库支持
提供绿色免安装版本,直接运行QAudio.exe即可。

2、关键代码

其中最核心的类QAudioProbe

player = new QMediaPlayer(this);
probe  = new QAudioProbe; //探测器
probe->setSource(player);

connect(probe,&QAudioProbe::audioBufferProbed,
        this,&MainWindow::processBuffer); //关联函数
connect(player,&QMediaPlayer::stateChanged,
        this,&MainWindow::onStateChanged);
void MainWindow::processBuffer(const QAudioBuffer &buffer)
{
    //qDebug() << buffer.sampleCount() <
    QAudioFormat audioFormat=buffer.format();//缓冲区格式
    getMaxAmplitude(audioFormat);
    //qDebug() << audioFormat.channelCount() << audioFormat.sampleSize() << audioFormat.sampleRate() << audioFormat.bytesPerFrame();
    if(m_paraUpdated == false)
    {
        //处理探测到的缓冲区
        ui->spin_byteCount->setValue(buffer.byteCount());//缓冲区字节数
        ui->spin_duration->setValue(buffer.duration()/1000);//缓冲区时长
        ui->spin_frameCount->setValue(buffer.frameCount());//缓冲区帧数
        ui->spin_sampleCount->setValue(buffer.sampleCount());//缓冲区采样数

        ui->spin_channelCount->setValue(audioFormat.channelCount()); //通道数
        ui->spin_sampleSize->setValue(audioFormat.sampleSize());//采样大小
        ui->spin_sampleRate->setValue(audioFormat.sampleRate());//采样率
        ui->spin_bytesPerFrame->setValue(audioFormat.bytesPerFrame());//每帧字节数

        if (audioFormat.byteOrder()==QAudioFormat::LittleEndian)
            ui->edit_byteOrder->setText("LittleEndian");//字节序
        else
            ui->edit_byteOrder->setText("BigEndian");

        ui->edit_codec->setText(audioFormat.codec());//编码格式

        if (audioFormat.sampleType()==QAudioFormat::SignedInt)//采样点类型
            ui->edit_sampleType->setText("SignedInt");
        else if(audioFormat.sampleType()==QAudioFormat::UnSignedInt)
            ui->edit_sampleType->setText("UnSignedInt");
        else if(audioFormat.sampleType()==QAudioFormat::Float)
            ui->edit_sampleType->setText("Float");
        else
            ui->edit_sampleType->setText("Unknown");

        m_paraUpdated = true;
    }


    Q_ASSERT(audioFormat.sampleSize() % 8 == 0);
    const int channelBytes = audioFormat.sampleSize() / 8;
    const int sampleBytes = audioFormat.channelCount() * channelBytes;
    //   Q_ASSERT(len % sampleBytes == 0);
    //   const int numSamples = len / sampleBytes;

    quint32 maxValue = 0;
    double sum = 0;
    int db = 0;
    const unsigned char *ptr = reinterpret_cast<const unsigned char *>(buffer.data());
    int frameCount = buffer.frameCount();
    int channelCount = audioFormat.channelCount();
    int nnum = 0;
    for (int i = 0; i < frameCount; ++i) {
        for (int j = 0; j < channelCount; ++j) {
            quint32 value = 0;

            if (audioFormat.sampleSize() == 8 && audioFormat.sampleType() == QAudioFormat::UnSignedInt) {
                value = *reinterpret_cast<const quint8*>(ptr);
            } else if (audioFormat.sampleSize() == 8 && audioFormat.sampleType() == QAudioFormat::SignedInt) {
                value = qAbs(*reinterpret_cast<const qint8*>(ptr));
            } else if (audioFormat.sampleSize() == 16 && audioFormat.sampleType() == QAudioFormat::UnSignedInt) {
                if (audioFormat.byteOrder() == QAudioFormat::LittleEndian)
                    value = qFromLittleEndian<quint16>(ptr);
                else
                    value = qFromBigEndian<quint16>(ptr);
            } else if (audioFormat.sampleSize() == 16 && audioFormat.sampleType() == QAudioFormat::SignedInt) {
                if (audioFormat.byteOrder() == QAudioFormat::LittleEndian)
                    value = qAbs(qFromLittleEndian<qint16>(ptr));
                else
                    value = qAbs(qFromBigEndian<qint16>(ptr));
            } else if (audioFormat.sampleSize() == 32 && audioFormat.sampleType() == QAudioFormat::UnSignedInt) {
                if (audioFormat.byteOrder() == QAudioFormat::LittleEndian)
                    value = qFromLittleEndian<quint32>(ptr);
                else
                    value = qFromBigEndian<quint32>(ptr);
            } else if (audioFormat.sampleSize() == 32 && audioFormat.sampleType() == QAudioFormat::SignedInt) {
                if (audioFormat.byteOrder() == QAudioFormat::LittleEndian)
                    value = qAbs(qFromLittleEndian<qint32>(ptr));
                else
                    value = qAbs(qFromBigEndian<qint32>(ptr));
            } else if (audioFormat.sampleSize() == 32 && audioFormat.sampleType() == QAudioFormat::Float) {
                value = qAbs(*reinterpret_cast<const float*>(ptr) * 0x7fffffff); // assumes 0-1.0
            }

            sum += qAbs(value);
            maxValue = qMax(value, maxValue);
            ptr += channelBytes;
            nnum++;
        }
    }

    //qDebug() << "***" << frameCount*channelCount << nnum << channelBytes;
    sum = sum / (frameCount*channelCount);
    //sum = sum / (frameCount);

    maxValue = qMin(maxValue, m_maxAmplitude);
    m_level = qreal(maxValue) / m_maxAmplitude;
    emit update();
    db = (int)(20.0*log10(sum ));
    if(db > 0)
    {
        m_sumDb += db;
        m_processedFrame++;
        qDebug() <<__func__ << "level =" << m_level << db;
    }
    m_totalFrame += buffer.frameCount();
    ui->LabFrameValue->setText(QString::number(m_totalFrame));

}
void MainWindow::getMaxAmplitude(QAudioFormat audioFormat)
{
    if(m_maxAmplitude != 0)
    {
        //qDebug() << __func__ << m_maxAmplitude;
        return;
    }
    switch (audioFormat.sampleSize()) {
    case 8:
        switch (audioFormat.sampleType()) {
        case QAudioFormat::UnSignedInt:
            m_maxAmplitude = 255;
            break;
        case QAudioFormat::SignedInt:
            m_maxAmplitude = 127;
            break;
        default:
            break;
        }
        break;
    case 16:
        switch (audioFormat.sampleType()) {
        case QAudioFormat::UnSignedInt:
            m_maxAmplitude = 65535;
            break;
        case QAudioFormat::SignedInt:
            m_maxAmplitude = 32767;
            break;
        default:
            break;
        }
        break;

    case 32:
        switch (audioFormat.sampleType()) {
        case QAudioFormat::UnSignedInt:
            m_maxAmplitude = 0xffffffff;
            break;
        case QAudioFormat::SignedInt:
            m_maxAmplitude = 0x7fffffff;
            break;
        case QAudioFormat::Float:
            m_maxAmplitude = 0x7fffffff; // Kind of
        default:
            break;
        }
        break;

    default:
        break;
    }
    qDebug() << __func__ << "m_maxAmplitude =" << m_maxAmplitude;
}

源码已提交,可在此下载 https://download.csdn.net/download/u011942101/88251529

你可能感兴趣的:(《Qt,项目实战经历全记录》,qt,音视频,分贝)