理解PCM⾳频数据,使⽤QAudioOutput播放⾳频的两种⽅法【写在前⾯】
因为最近需要写 FFmpeg 播放⾳频的⽂章,所以就先写了这篇⽂章。
并且,FFmpeg 解码出来的⾳频是 PCM 原始⾳频数据。
然后,我使⽤ Qt 的 QAudioOutput 作为底层⾳频输出(输出设备)。
本篇主要内容:
1、⾳频基础概念
2、PCM数据格式
3、QAudioOutput 的使⽤⽅法( 两种 )
【正⽂开始】
在介绍 PCM 之前,必须先了解⼀些⾳频基础概念:
采样率( Sample Rate ):每秒采样的频率,即每秒样本数,单位 HZ ,常见的频率有 22050HZ、44100
HZ( 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 数据:
QByteArray generateRandomPCM()
{
qsrand(uint(time(nullptr)));
//幅度,因为sampleSize = 16bit
qint16 amplitude = INT16_MAX;
//单声道
int channels = 1;
//采样率
int samplerate = 8000;
//持续时间ms
int duration = 20000;
/
/总样本数
int n_samples = int(channels * samplerate * (duration / 1000.0));
QByteArray data;
QDataStream out(&data, QIODevice::WriteOnly);
out.setByteOrder(QDataStream::LittleEndian);
for (int i = 0; i < n_samples; i++) {
qint16 sample = qrand() % amplitude;
out << sample;
}
QFile file("raw");
file.open(QIODevice::WriteOnly);
file.write(data);
file.close();
return data;
}
利⽤这个函数,可以⽣成⼀份 8000HZ,16Bit,单声道,持续 20s 的随机 PCM 数据,部分波形如下:
呃。。这种都是噪⾳,听不出什么来,于是我尝试⽣成⼀些有规律的:
QByteArray generatePCM()
output的反义词
{
//幅度,因为sampleSize = 16bit
qint16 amplitude = INT16_MAX;
//单声道
int channels = 1;
//采样率
int samplerate = 8000;
//持续时间ms
int duration = 20;
//总样本数
int n_samples = int(channels * samplerate * (duration / 1000.0));
//声⾳频率
int frequency = 100;
bool reverse = false;
QByteArray data;
QDataStream out(&data, QIODevice::WriteOnly);
out.setByteOrder(QDataStream::LittleEndian);
for (int i = 0; i < 1000; i++) {
for (int j = 0; j < n_samples; j++) {
qreal radians = qreal(2.0 * M_PI * j  * frequency / qreal(samplerate));
qint16 sample = qint16(qSin(radians) * amplitude);
out << sample;
}
if (!reverse) {
if (frequency < 2000) {
frequency += 100;
} else reverse = true;
} else {
if (frequency > 100) {
frequency -= 100;
} else reverse = false;
}
}
QFile file("raw");
file.open(QIODevice::WriteOnly);
file.write(data);
file.close();
return data;
}
这种⽣成的⾳频就是,频率(声调)由低到⾼再到低,部分波形如下:
到这⾥,PCM 相关的就讲解完了。
现在 PCM 数据有了,那么如何在Qt中播放它呢?答案是 QAudioOutput。
QAudioOutput是Qt中播放⾳频的类( 另⼀个是QSound,但只⽀持WAV ),要使⽤它,需要在pro中加⼊  QT += multimedia
我们先来看看代码:
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QByteArray pcm1 = generatePCM();
QAudioFormat format;
format.setCodec("audio/pcm");
format.setSampleRate(8000);
format.setSampleSize(16);
format.setSampleType(QAudioFormat::SignedInt);
format.setChannelCount(1);
format.setByteOrder(QAudioFormat::LittleEndian);
QAudioOutput output(format, qApp);
QIODevice *device = output.start();
QTimer *timer_play = new QTimer(qApp);
timer_play->setTimerType(Qt::PreciseTimer);
QObject::connect(timer_play, &QTimer::timeout, [&]{
if (pcm1.size() > 0) {
int readSize = output.periodSize();
int chunks = output.bytesFree() / readSize;
while (chunks) {
QByteArray samples = pcm1.mid(0, readSize);
int len = samples.size();
if (len) device->write(samples);
if (len != readSize) break;
chunks--;
}
}
});
timer_play->start(100);
();
}
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() (纯虚函数,必须实现):
class AudioDevice : public QIODevice
{
public:
AudioDevice(const QByteArray &data, QObject *parent = nullptr) : QIODevice(parent), m_data(data) { }
~AudioDevice() { }
virtual qint64 readData(char *data, qint64 maxlen);
virtual qint64 writeData(const char *data, qint64 len){
Q_UNUSED(data);
Q_UNUSED(len);
return 0;
}
private:
QByteArray m_data;
};
qint64 AudioDevice::readData(char *data, qint64 maxlen)
{
if (m_data.size() >= maxlen) {
QByteArray d = m_data.mid(0, int(maxlen));
memcpy(data, d.data(), size_t(d.size()));
ve(0, int(maxlen));
return d.size();
} else {
QByteArray d = m_data;
memcpy(data, d.data(), size_t(d.size()));
m_data.clear();
return d.size();
}
}
因为 QAudioOutput 需要数据时在提供的 QIODdevice 中读取,所以实现 readData() 即可。        然后我们就可以使⽤它了:
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QAudioFormat format;
format.setCodec("audio/pcm");
format.setSampleRate(8000);
format.setSampleSize(16);
format.setSampleType(QAudioFormat::SignedInt);
format.setChannelCount(1);
format.setByteOrder(QAudioFormat::LittleEndian);
QAudioOutput output(format, qApp);
QByteArray pcm1 = generatePCM();
AudioDevice *device = new AudioDevice(pcm1, qApp);
device->open(QIODevice::ReadOnly);
output.start(device);
();
}
【结语】

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。