理解PCM音频数据,使用QAudioOutput播放音频的两种方法

末蓝、 2024-04-19 15:25 101阅读 0赞

【写在前面】

因为最近需要写 FFmpeg 播放音频的文章,所以就先写了这篇文章。

并且,FFmpeg 解码出来的音频是 PCM 原始音频数据。

然后,我使用 Qt 的 QAudioOutput 作为底层音频输出(输出设备)。

本篇主要内容:

1、音频基础概念

2、PCM数据格式

3、QAudioOutput 的使用方法( 两种 )


【正文开始】

  • 在介绍 PCM 之前,必须先了解一些音频基础概念:

采样率( Sample Rate ):每秒采样的频率,即每秒样本数,单位 HZ ,常见的频率有 22050HZ、44100HZ( CD级 )。并且人耳的听力范围是 20HZ ~ 20000HZ,因此超过 48000HZ 就没有意义了( 根据采样定理 )。

采样定理:采样在进行模拟/数字信号的转换过程中,当采样频率fs.max大于信号中最高频率fmax的2倍时(fs.max>2fmax),采样之后的数字信号完整地保留了原始信号中的信息,一般实际应用中保证采样频率为信号最高频率的2.56~4倍,采样定理又称奈奎斯特定理。

样本大小( Sample Size ):每个样本的大小,也就是振幅,假如样本大小为16 Bit = 2 ^ 16 = 65536,即能够记录 65535 个数,常见的有 8Bit、16Bit( CD级 ),而到了 32Bit 意义就不大了。

声道( Sound Channel ):指声音在录制或播放时,在不同空间位置采集或回放的相互独立的音频信号,常见的有单声道,双声道,四声道,这个倒是声道越多效果越好。

比特率( Bit Rate ):单位( bps 比特每秒 ),可以使用 比特率 = 采样率 x 样本大小 x 声音通道数 来计算。

字节序( Byte Ordering ):字节序,这个大家应该都知道,分大端和小端,一般都是小端字节序(低位低地址,高位高地址)。

  • 好了,现在开始介绍 PCM 格式:

PCM 全名脉冲编码调制( Pulse Code Modulation ),它是原始的音频脉冲数据,在一般情况下,一帧PCM数据包含2048个样本( 数据帧,注意是一帧而不是一秒 )。

因此,对于一帧PCM数据,使用小端字节序存储为(每个 [ ] 为一个样本):

内存地址: | 低地址 | >>>>>>> | 高地址 |

8Bit 单声道:[ 8 bit ] - [ 8 bit ] - [ 8 bit ] - [ 8 bit ] } 总 2048个。

8Bit 双声道:[ 8 bit 声道1 ] - [ 8 bit 声道2 ] - [ 8 bit 声道1 ] - [ 8 bit 声道2 ] } 总 2048个。

16Bit 单声道:[ 8 bit 低位 + 8 bit 高位 ] - [ 8 bit 低位 + 8 bit 高位 ] - [ 8 bit 低位 + 8 bit 高位 ] - [ 8 bit 低位 + 8 bit 高位 ] } 总 2048个。

16Bit 双声道:[ 8 bit 低位 + 8 bit 高位 声道1 ] - [ 8 bit 低位 + 8 bit 高位 声道2 ] - [ 8 bit 低位 + 8 bit 高位 声道1 ] - [ 8 bit 低位 + 8 bit 高位 声道2 ] } 总 2048个。

到这里,我们已经大概了解了 PCM 数据格式,现在可以尝试手动生成一份 PCM 数据:

  1. QByteArray generateRandomPCM()
  2. {
  3. qsrand(uint(time(nullptr)));
  4. //幅度,因为sampleSize = 16bit
  5. qint16 amplitude = INT16_MAX;
  6. //单声道
  7. int channels = 1;
  8. //采样率
  9. int samplerate = 8000;
  10. //持续时间ms
  11. int duration = 20000;
  12. //总样本数
  13. int n_samples = int(channels * samplerate * (duration / 1000.0));
  14. QByteArray data;
  15. QDataStream out(&data, QIODevice::WriteOnly);
  16. out.setByteOrder(QDataStream::LittleEndian);
  17. for (int i = 0; i < n_samples; i++) {
  18. qint16 sample = qrand() % amplitude;
  19. out << sample;
  20. }
  21. QFile file("raw");
  22. file.open(QIODevice::WriteOnly);
  23. file.write(data);
  24. file.close();
  25. return data;
  26. }

利用这个函数,可以生成一份 8000HZ,16Bit,单声道,持续 20s 的随机 PCM 数据,部分波形如下:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTEyODMyMjY_size_16_color_FFFFFF_t_70

呃。。这种都是噪音,听不出什么来,于是我尝试生成一些有规律的:

  1. QByteArray generatePCM()
  2. {
  3. //幅度,因为sampleSize = 16bit
  4. qint16 amplitude = INT16_MAX;
  5. //单声道
  6. int channels = 1;
  7. //采样率
  8. int samplerate = 8000;
  9. //持续时间ms
  10. int duration = 20;
  11. //总样本数
  12. int n_samples = int(channels * samplerate * (duration / 1000.0));
  13. //声音频率
  14. int frequency = 100;
  15. bool reverse = false;
  16. QByteArray data;
  17. QDataStream out(&data, QIODevice::WriteOnly);
  18. out.setByteOrder(QDataStream::LittleEndian);
  19. for (int i = 0; i < 1000; i++) {
  20. for (int j = 0; j < n_samples; j++) {
  21. qreal radians = qreal(2.0 * M_PI * j * frequency / qreal(samplerate));
  22. qint16 sample = qint16(qSin(radians) * amplitude);
  23. out << sample;
  24. }
  25. if (!reverse) {
  26. if (frequency < 2000) {
  27. frequency += 100;
  28. } else reverse = true;
  29. } else {
  30. if (frequency > 100) {
  31. frequency -= 100;
  32. } else reverse = false;
  33. }
  34. }
  35. QFile file("raw");
  36. file.open(QIODevice::WriteOnly);
  37. file.write(data);
  38. file.close();
  39. return data;
  40. }

这种生成的音频就是,频率(声调)由低到高再到低,部分波形如下:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTEyODMyMjY_size_16_color_FFFFFF_t_70 1

到这里,PCM 相关的就讲解完了。

  • 现在 PCM 数据有了,那么如何在Qt中播放它呢?答案是 QAudioOutput。

QAudioOutput是Qt中播放音频的类( 另一个是QSound,但只支持WAV ),要使用它,需要在pro中加入 QT += multimedia

我们先来看看代码:

  1. int main(int argc, char *argv[])
  2. {
  3. QCoreApplication a(argc, argv);
  4. QByteArray pcm1 = generatePCM();
  5. QAudioFormat format;
  6. format.setCodec("audio/pcm");
  7. format.setSampleRate(8000);
  8. format.setSampleSize(16);
  9. format.setSampleType(QAudioFormat::SignedInt);
  10. format.setChannelCount(1);
  11. format.setByteOrder(QAudioFormat::LittleEndian);
  12. QAudioOutput output(format, qApp);
  13. QIODevice *device = output.start();
  14. QTimer *timer_play = new QTimer(qApp);
  15. timer_play->setTimerType(Qt::PreciseTimer);
  16. QObject::connect(timer_play, &QTimer::timeout, [&]{
  17. if (pcm1.size() > 0) {
  18. int readSize = output.periodSize();
  19. int chunks = output.bytesFree() / readSize;
  20. while (chunks) {
  21. QByteArray samples = pcm1.mid(0, readSize);
  22. int len = samples.size();
  23. pcm1.remove(0, len);
  24. if (len) device->write(samples);
  25. if (len != readSize) break;
  26. chunks--;
  27. }
  28. }
  29. });
  30. timer_play->start(100);
  31. return a.exec();
  32. }

1、创建一个 QAudioFormat ,它描述了音频的格式,setCodec()只支持 PCM,而其他的参数根据前面所讲,你应该知道是什么了吧。

2、根据 QAudioFormat 创建一个 QAudioOutput,它是用来管理音频设备( 即声卡 )的。

3、使用 start() 返回一个指向实际音频设备的指针,往这个里面写入数据就能够播放出声音了。

4、我使用定时器来定时写入数据,这样可以保证声音是连续的。

5、periodSize() 返回一个周期所必需要的数据量,而 bytesFree() 返回内部缓冲区的空闲空间的字节数,也就是说,我们只需要每次写入所需的数据量 periodSize()**,然后直到填充满内部缓沖 **bytesFree() 即可实现连续播放。

至此,第一种 QAudioOutput 的使用方法讲解完毕。

  • 第二种 QAudioOutput 的使用方法:

QAudioOutput 的 start() 函数的重载版本 start(QIODevice *),它需要传入一个 QIODevice 指针,然后由 QAudioOutput 进行读取。

因此,我们需要实现一个自己的IODevice,并实现 readData() 和 writeData() (纯虚函数,必须实现):

  1. class AudioDevice : public QIODevice
  2. {
  3. public:
  4. AudioDevice(const QByteArray &data, QObject *parent = nullptr) : QIODevice(parent), m_data(data) { }
  5. ~AudioDevice() { }
  6. virtual qint64 readData(char *data, qint64 maxlen);
  7. virtual qint64 writeData(const char *data, qint64 len){
  8. Q_UNUSED(data);
  9. Q_UNUSED(len);
  10. return 0;
  11. }
  12. private:
  13. QByteArray m_data;
  14. };
  15. qint64 AudioDevice::readData(char *data, qint64 maxlen)
  16. {
  17. if (m_data.size() >= maxlen) {
  18. QByteArray d = m_data.mid(0, int(maxlen));
  19. memcpy(data, d.data(), size_t(d.size()));
  20. m_data.remove(0, int(maxlen));
  21. return d.size();
  22. } else {
  23. QByteArray d = m_data;
  24. memcpy(data, d.data(), size_t(d.size()));
  25. m_data.clear();
  26. return d.size();
  27. }
  28. }

因为 QAudioOutput 需要数据时在提供的 QIODdevice 中读取,所以实现 readData() 即可。

然后我们就可以使用它了:

  1. int main(int argc, char *argv[])
  2. {
  3. QCoreApplication a(argc, argv);
  4. QAudioFormat format;
  5. format.setCodec("audio/pcm");
  6. format.setSampleRate(8000);
  7. format.setSampleSize(16);
  8. format.setSampleType(QAudioFormat::SignedInt);
  9. format.setChannelCount(1);
  10. format.setByteOrder(QAudioFormat::LittleEndian);
  11. QAudioOutput output(format, qApp);
  12. QByteArray pcm1 = generatePCM();
  13. AudioDevice *device = new AudioDevice(pcm1, qApp);
  14. device->open(QIODevice::ReadOnly);
  15. output.start(device);
  16. return a.exec();
  17. }

【结语】

其实说起来,QAudioOutput 的两种方并没有太大的区别,所以使用哪种就看个人喜好了。

然后本篇已经很详细的讲了音频基础,PCM 的格式以及如何简单的生成它。(啊。写了一天,累死我了。。2019092011585246.jpg)

最后,所有的代码在:https://download.csdn.net/download/u011283226/11789311 (可能需要点积分,但我不想放在Github上Ծ‸ Ծ )。

发表评论

表情:
评论列表 (有 0 条评论,101人围观)

还没有评论,来说两句吧...

相关阅读

    相关 音频基础知识 - PCM 浅析

    PCM浅析 最近有个需求:对音频裁剪时,裁剪条的纵坐标必须是音频音量,以帮助用户更好的选择音频区域,所以就需要快速准确的提取出音频的音量列表。本文主要介绍下从mp4文件中

    相关 PCM音频振幅知识

    一.声音的相关概念 声音是介质振动在听觉系统中产生的反应。声音总可以被分解为不同频率不同强度正弦波的叠加(傅里叶变换)。 ![PCM音量控制 - 第1张 | 剑痴乎