简述
在上一篇 Qt 之 WAV文件解析 中详细地分析了wav格式文件的文件头信息。通过QAudioInput实现录音功能,但是录音生成的文件并不能用播放器打开,就算更改后缀名也无法识别(有时候下载的一些音频文件通过修改文件名可以播放)。在Qt助手中将录音生成的文件保存为.raw格式,那么这个raw到底是什么格式呢?
其实看raw字面的意思是原始的、未处理的、未加工的,从此看来QAudioInput 生成的音频文件未经过处理,即文件保存的数据全部为音频数据,没有文件头,播放器识别不了。好了,既然知道这个原因导致播放器播不了,那么我们就手动给.raw文件添加上头信息,转为wav格式,这样不仅可以通过QAudioOutput来播放,同时播放器也能够播放该音频文件。
代码之路
代码思路
这里主要是通过QAudioInput来生成音频文件,录音结束后,将.raw音频文件转为.wav格式文件,在 Qt 之 WAV文件解析 中介绍了wav文件头的数据结构,这里我们只要将这个结构的数据加在.raw文件的头部即可,代码中通过addWavHeader方法将.raw文件转成.wav文件。播放音频文件利用QAudioOutput类即可,既可播放.raw文件也可以播放.wav文件。我们这里就直接播放重新生成的.wav格式的音频文件。
仔细看代码有点多,其实实现很简单,这里我添加的一些代码是用来实现一个简单完整的小录音机功能。
#include "myaudioinput.h"
#include <QAudioDeviceInfo>
#include <QDebug>
#include <QMessageBox>
#define RAW_RECORD_FILENAME "F:/audio/test.raw"
#define WAV_RECORD_FILENAME "F:/audio/test.wav"
const qint64 TIME_TRANSFORM = 1000 * 1000;
struct WAVFILEHEADER
{
char RiffName[4];
unsigned long nRiffLength;
char WavName[4];
char FmtName[4];
unsigned long nFmtLength;
unsigned short nAudioFormat;
unsigned short nChannleNumber;
unsigned long nSampleRate;
unsigned long nBytesPerSecond;
unsigned short nBytesPerSample;
unsigned short nBitsPerSample;
char DATANAME[4];
unsigned long nDataLength;
};
MyAudioInput::MyAudioInput(QWidget *parent)
: QWidget(parent)
, m_isRecord(false)
, m_isPlay(false)
, m_RecordTimerId(0)
, m_RecordTime(0)
{
ui.setupUi(this);
connect(ui.pButtonRecord, SIGNAL(clicked()), this, SLOT(onStartRecord()));
connect(ui.pButtonStopRecord, SIGNAL(clicked()), this, SLOT(onStopRecording()));
connect(ui.pButtonPlay, SIGNAL(clicked()), this, SLOT(onPlay()));
connect(ui.pButtonStopPlay, SIGNAL(clicked()), this, SLOT(onStopPlay()));
QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());
qDebug()<< "AudioDevice supportedCodecs : " << info.supportedCodecs();
}
MyAudioInput::~MyAudioInput()
{
}
void MyAudioInput::onStartRecord()
{
if (m_isPlay)
{
onStopPlay();
}
if (!m_isRecord)
{
QAudioDeviceInfo audioDeviceInfo = QAudioDeviceInfo::defaultInputDevice();
if (!audioDeviceInfo.isNull())
{
m_isRecord = true;
destinationFile.setFileName(RAW_RECORD_FILENAME);
destinationFile.open(QIODevice::WriteOnly | QIODevice::Truncate);
QAudioFormat format;
format.setSampleRate(8000);
format.setChannelCount(1);
format.setSampleSize(16);
format.setCodec("audio/pcm");
format.setByteOrder(QAudioFormat::LittleEndian);
format.setSampleType(QAudioFormat::UnSignedInt);
if (!audioDeviceInfo.isFormatSupported(format))
{
qDebug() << "Default format not supported, trying to use the nearest.";
format = audioDeviceInfo.nearestFormat(format);
}
m_audioInput = new QAudioInput(format, this);
m_audioInput->start(&destinationFile);
if (m_RecordTimerId == 0)
{
m_RecordTimerId = startTimer(100);
}
}
else
{
QMessageBox::information(NULL, tr("Record"), tr("Current No Record Device"));
}
}
else
{
QMessageBox::information(NULL, tr("Record"), tr("Current is Recording"));
}
}
void MyAudioInput::onStopRecording()
{
if (m_isRecord)
{
killTimer(m_RecordTimerId);
m_RecordTime = 0;
m_RecordTimerId = 0;
m_isRecord = false;
ui.labelTime->setText(QString("Idle : %1/S").arg(m_RecordTime));
if (m_audioInput != NULL)
{
m_audioInput->stop();
destinationFile.close();
delete m_audioInput;
m_audioInput = NULL;
}
if (addWavHeader(RAW_RECORD_FILENAME, WAV_RECORD_FILENAME) > 0)
QMessageBox::information(NULL, tr("Save"), tr("RecordFile Save Success"));
}
}
void MyAudioInput::onPlay()
{
if (!m_isRecord)
{
if (m_isPlay)
{
onStopPlay();
}
m_isPlay = true;
sourceFile.setFileName(WAV_RECORD_FILENAME);
sourceFile.open(QIODevice::ReadOnly);
QAudioFormat format;
format.setSampleRate(8000);
format.setChannelCount(1);
format.setSampleSize(16);
format.setCodec("audio/pcm");
format.setByteOrder(QAudioFormat::LittleEndian);
format.setSampleType(QAudioFormat::UnSignedInt);
QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());
qDebug() << info.supportedCodecs();
if (!info.isFormatSupported(format))
{
qWarning() << "Raw audio format not supported by backend, cannot play audio.";
return;
}
m_audioOutput = new QAudioOutput(format, this);
connect(m_audioOutput, SIGNAL(stateChanged(QAudio::State)), this, SLOT(handleStateChanged(QAudio::State)));
m_audioOutput->start(&sourceFile);
if (m_RecordTimerId == 0)
{
m_RecordTimerId = startTimer(100);
}
}
else
{
QMessageBox::information(NULL, tr("Record"), tr("Current is Recording"));
}
}
void MyAudioInput::onStopPlay()
{
if (m_isPlay)
{
killTimer(m_RecordTimerId);
m_RecordTime = 0;
m_RecordTimerId = 0;
m_isPlay = false;
ui.labelTime->setText(QString("Idle : %1/S").arg(m_RecordTime));
if (m_audioOutput != NULL)
{
m_audioOutput->stop();
sourceFile.close();
delete m_audioOutput;
m_audioOutput = NULL;
}
}
}
void MyAudioInput::handleStateChanged(QAudio::State state)
{
switch (state) {
case QAudio::IdleState:
onStopPlay();
break;
case QAudio::StoppedState:
if (m_audioOutput->error() != QAudio::NoError) {
}
break;
default:
break;
}
}
void MyAudioInput::timerEvent(QTimerEvent *event)
{
if (event->timerId() == m_RecordTimerId)
{
QString strState;
if (m_isRecord)
{
strState = "Recording";
m_RecordTime = m_audioInput->elapsedUSecs() / TIME_TRANSFORM;
}
else if (m_isPlay)
{
strState = "Playing";
m_RecordTime = m_audioOutput->elapsedUSecs() / TIME_TRANSFORM;
}
ui.labelTime->setText(QString("%1 : %2/S").arg(strState).arg(m_RecordTime));
}
}
qint64 MyAudioInput::addWavHeader(QString catheFileName , QString wavFileName)
{
WAVFILEHEADER WavFileHeader;
qstrcpy(WavFileHeader.RiffName, "RIFF");
qstrcpy(WavFileHeader.WavName, "WAVE");
qstrcpy(WavFileHeader.FmtName, "fmt ");
qstrcpy(WavFileHeader.DATANAME, "data");
WavFileHeader.nFmtLength = 16;
WavFileHeader.nAudioFormat = 1;
WavFileHeader.nChannleNumber = 1;
WavFileHeader.nSampleRate = 8000;
WavFileHeader.nBytesPerSample = 2;
WavFileHeader.nBytesPerSecond = 16000;
WavFileHeader.nBitsPerSample = 16;
QFile cacheFile(catheFileName);
QFile wavFile(wavFileName);
if (!cacheFile.open(QIODevice::ReadWrite))
{
return -1;
}
if (!wavFile.open(QIODevice::WriteOnly))
{
return -2;
}
int nSize = sizeof(WavFileHeader);
qint64 nFileLen = cacheFile.bytesAvailable();
WavFileHeader.nRiffLength = nFileLen - 8 + nSize;
WavFileHeader.nDataLength = nFileLen;
wavFile.write((char *)&WavFileHeader, nSize);
wavFile.write(cacheFile.readAll());
cacheFile.close();
wavFile.close();
return nFileLen;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
程序截图

点击录音开始录音,再次点击会弹出提示,当前正在录音,这样防止多次点击录音,导致重复录音。点击停止录音,结束录音,并将生成的.raw文件转成.wav文件,点击播放,则播放.wav音频文件。多次点击播放会重新播放,点击停止播放,则停止当前播放。
这里做了一些简单的逻辑判断,避免多次点击按钮导致生成的文件错误,同时在时钟事件中不断更新当前状态。
生成的文件截图



仔细对比两个文件,二者大小相差44个字节,这44个字节即为手动添加的wav文件头信息,而前者是不能用播放器打开,后者可以直接用播放器播放。
从上图中可以通过文件头来判断是否是一个wav文件,该文件的前4位为 “52 49 46 46” ,即为 “RIFF”。后面的数据也是按照wav文件头数据结构依次存储。有兴趣可以对照上一篇文章对wav文件头的介绍,将生成的wav文件头信息解析出来。
以下是对wav文件解析后的数据:
具体如何解析可以参考 Qt 之 解析wav文件的头信息(详细分析、对比不同wav文件的数据)。
特别注意
在利用QAudioInput生成音频时需要设置音频的格式(通过QAudioFormat来设置),这里设置的格式要与 转为wav文件时设置的一系列参数 以及 在用 QAudioOutput 进行播放时设置的格式要完全一致,否则会导致声音文件识别不了,或者播放声音不清楚或者就只能听见嗡嗡的声音,所以一定要保持格式的一致性。至于在格式中参数的取值,到底对生成的音频文件有什么影响,将在下篇中进行解答。
尾
下一篇将继续介绍用Qt直接生成wav格式的文件,不需要手动来添加wav文件头,同时也会用代码来解析一个wav文件的头信息,以及在生成时设置的一些格式参数对音频文件的影响等,下次见 。
代码下载
Qt之实现录音播放及raw(pcm)转wav格式
其他文章:
Qt 之 WAV文件解析
Qt 之 解析wav文件的头信息(详细分析、对比不同wav文件的数据)。