音频录制与播放命令
- 录制
ffmpeg -f avfoundation -i :0 out.wav
- 播放
- 播放PCM需要指定相关参数: ar:采样率 ac:声道数 f:采样格式
ffplay -ar 44100 -ac 2 -f s16le out.pcm
PCM音频录制步骤
- 获取输入格式对象
av_find_input_format
- 打开设备
avformat_open_input
- 创建输出缓冲区AVPacket
av_packet_alloc
- 不断地将音频写入输出缓冲区
-
将AVPacket的数据写入文件
-
- (void)record {
NSString *formatName = @"avfoundation";
NSString *deviceName = @":0";
NSString *filePath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject;
self.fileName = [filePath stringByAppendingPathComponent: @"record_out.pcm"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
// 获取输入格式对象
AVInputFormat *fmt = av_find_input_format([formatName UTF8String]);
if (!fmt) {
NSLog(@"获取输入格式对象失败");
return;
}
AVFormatContext *ctx = nullptr;
AVDictionary *option = nullptr;
// 打开设备
int ret = avformat_open_input(&ctx, deviceName.UTF8String, fmt, &option);
if (ret < 0) {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof(errbuf));
NSLog(@"打开设备失败:%@", [NSString stringWithUTF8String:errbuf]);
return;
}
NSString *fileName = self.fileName;
NSMutableData *pcmData = [NSMutableData new];
// 创建输出缓冲区AVPacket
AVPacket *pkt = av_packet_alloc();
NSInteger bufferSize = 1024 * 10;
// 不断地将音频写入输出缓冲区
while (!self.isInterruptionRequested) {
int ret = av_read_frame(ctx, pkt);
if (ret == 0) {
if (pcmData.length >= bufferSize) {
showSpec(ctx);
// 将AVPacket的数据写入文件
dispatch_barrier_async(queue, ^{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSRange writeRange = NSMakeRange(0, pcmData.length);
NSData *writeData = [pcmData subdataWithRange:writeRange];
if (![fileManager fileExistsAtPath:fileName]) {
[writeData writeToFile:fileName atomically:true];
[pcmData resetBytesInRange:writeRange];
[pcmData setLength:0];
NSLog(@"写入文件成功:%lu", (unsigned long)writeData.length);
} else {
NSFileHandle *filehandle = [NSFileHandle fileHandleForWritingAtPath:fileName];
[filehandle seekToEndOfFile];
[filehandle writeData:writeData];
[filehandle closeFile];
[pcmData resetBytesInRange:writeRange];
[pcmData setLength:0];
NSLog(@"写入文件成功:%lu", (unsigned long)writeData.length);
}
});
}
NSLog(@"---record---pkt: %d - pcmData: %lu", pkt->size, (unsigned long)pcmData.length);
[pcmData appendBytes:pkt->data length:pkt->size];
} else if (ret == AVERROR(EAGAIN) ) {
} else {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof(errbuf));
NSLog(@"打开设备失败:%@", [NSString stringWithUTF8String:errbuf]);
}
av_packet_unref(pkt);
}
NSFileHandle *filehandle = [NSFileHandle fileHandleForWritingAtPath:fileName];
[filehandle seekToEndOfFile];
[filehandle writeData:pcmData];
[filehandle closeFile];
NSLog(@"写入文件成功:%lu", (unsigned long)pkt->size);
[pcmData resetBytesInRange:NSMakeRange(0, pcmData.length)];
[pcmData setLength:0];
pcmData = nil;
av_packet_free(&pkt);
avformat_close_input(&ctx);
});
}
一些名词
- LSB(Least Sigificant Bit\Byte)最低有效位/字节 小端 (最低有效字节先被读取到)
- MSB (Most Significant Bit\Byte) 最高有效位/字节 大端 (最高有字节先被读取到)
AV_CODEC_ID_PCM_S32LE
AV_CODEC_ID_PCM_S32BE
AV_CODEC_ID_PCM_U32LE - 采样格式包含:
- 1.位深度(样本占多少位)
- 2.有符号,无符号,浮点数
- 3.大端(Big-Endian), 小端(Little-Endian)
PCM转WAV
WAV文件格式WAV文件格式 wav的格式
-
每一个chunk(数据块)都由3部分组成:
-
id
:chunk的标识 -
data size
:chunk的数据部分大小,字节为单位 -
data
: chunk的数据部分
-
-
整个WAV文件是一个RIFF chunk,它的data由3部分组成:
-
format
:文件类型 -
fmt chunk
: 音频参数相关的chunk, 它的data里面有采样率、声道数、位深度等参数信息 -
data chunk
: 音频数据相关的chunk, 它的data就是真正的音频数据(比如PCM数据)
-
-
RIFF chunk除去data chunk的data(音频数据)后,剩下的内容可以称为:WAV文件头,一般是44字节。
WAV文件头
// WAV文件头(44字节)
struct WavHeader {
// 整个riff
// RIFF chunk的id
uint8_t riffChunkId[4] = {'R', 'I', 'F', 'F'};
// RiFF chunk的data的大小,即文件总长度减去8字节(riffChunkId[4] + 自身长度)
uint32_t riffChunkSize;
// 格式
uint8_t format[4] = {'W', 'A', 'V', 'E'};
// fmt sub-chunk
uint8_t fmtChunkID[4] = {'f', 'm', 't', ' '};
//fmt chunk的大小:存储PCM数据时,是16
uint32_t fmtChunkDataSize = 16;
// 音频编码, 1表示普通型的PCM, 3表示Floating Point,针对采样格式为f32le的PCM
uint16_t audioFormat = 1;// AUDION_FORMAT_FLOAT;
// 声道数
uint16_t numChannels;
// 采样率
uint32_t sampleRate;
// 字节率 = sampleRate * blockAlign
uint32_t byteRate;
// 一个样本的字节数 = bitPerSample * numChannels >> 3
uint16_t blockAlign;
// 位深度,单声道下的一个样本的大小(单位:位)
uint16_t bitPerSample;
// data sub-chunk
uint8_t dataChunId[4] = {'d', 'a', 't', 'a'};
// data chunk的data大小: 音频数据的总长度,即文件总长度减去文件头的长度(一般是44)
uint32_t dataChunkSize;
};
PCM转WAV步骤
- 计算头部一个样本的字节数
- 计算头部字节率 byteRate
- 读取PCM数据
- 根据读取的PCM数据计算
dataChunkSize
,riffChunkSize
- 写入头部数据到wav文件
- 写入PCM数据到wav文件
+ (void)pcm2wav:(WavHeader *)header pcmfile:(NSString *)pcmFilename wavfile:(NSString *)wavfilename {
// 一个样本的字节数
header->blockAlign = header->bitPerSample * header->numChannels >> 3;
// 字节率
header->byteRate = header->sampleRate * header->blockAlign;
// 打开pcm文件
NSFileHandle *pcmhandle = [NSFileHandle fileHandleForReadingAtPath:pcmFilename];
if (!pcmhandle) {
NSLog(@"PCM文件打开失败");
return;
}
header->dataChunkSize = (uint32_t)pcmhandle.availableData.length;
header->riffChunkSize = header->dataChunkSize + sizeof(WavHeader) - sizeof(header->riffChunkId) - sizeof(header->riffChunkSize);
// 打开wav文件
NSError *error;
[[NSFileManager defaultManager]createFileAtPath:wavfilename contents:nil attributes:nil];
NSFileHandle *wavHandle = [NSFileHandle fileHandleForWritingToURL:[NSURL fileURLWithPath:wavfilename] error:&error];
if (error) {
NSLog(@"wav文件创建失败:%@", error.description);
[pcmhandle closeFile];
return;
}
// 写入头部
NSData *headerData = [NSData dataWithBytes:(void *)(header) length:sizeof(WavHeader)];
[wavHandle writeData:headerData];
// 写入PCM数据
[pcmhandle seekToFileOffset:0];
NSData *buf = [pcmhandle readDataOfLength:1024];
NSInteger size = buf.length;
while (size > 0) {
if (buf) {
[wavHandle writeData:buf];
[wavHandle seekToEndOfFile];
}
buf = [pcmhandle readDataOfLength:1024];
size = buf.length;
}
// 关闭文件
[pcmhandle closeFile];
[wavHandle closeFile];
}
WAV录音的步骤
-
获取输入格式
-
创建格式上下文
-
打开设备
-
获取输入流
-
获取音频参数
-
根据获取的音频参数,计算sampleRate,bitsPerSample, blockAlign,byteRate
-
写入WAV头部
-
写入PCM数据
-
根据PCM数据计算出dataChunkSize
-
录音结束,写入dataChunkSize (更新)
-
写入riffChunkDataSize(更新)
-
释放资源
-
采样大小的bitPerSample(位深度:单声道下的1个样本的大小)获取方式
- 通过采样格式获取
- 通过codec_id获取 av_get_bits_per_sample(params->codec_id);
- (void)record {
self.stop = false;
// 初始化数据包
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSString *formatName = @"avfoundation";
NSString *deviceName = @":0";
AVInputFormat *fmt = av_find_input_format([formatName UTF8String]);
if (!fmt) {
NSLog(@"获取输入格式对象失败");
return;
}
AVFormatContext *ctx = nullptr;
int ret = avformat_open_input(&ctx,
deviceName.UTF8String,
fmt, nullptr);
if (ret < 0) {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
NSLog(@"打开设备失败: %s", errbuf);
return;
}
NSString *filePath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject;
NSString *fileName = [filePath stringByAppendingPathComponent: @"record_out.wav"];
// 创建一个空文件
[[NSFileManager defaultManager]createFileAtPath:fileName contents:[NSData new] attributes:nil];
NSFileHandle *writeHandle = [NSFileHandle fileHandleForWritingToURL:[NSURL URLWithString:fileName] error:nil];
if (!writeHandle) {
NSLog(@"打开文件失败");
avformat_close_input(&ctx);
return;
}
// 获取输入流
AVStream *stream = ctx->streams[0];
// 获取音频参数
AVCodecParameters *params = stream->codecpar;
// 写入WAV文件头
WavHeader header;
header.sampleRate = params->sample_rate;
//
header.bitPerSample = av_get_bits_per_sample(params->codec_id);
header.numChannels = params->channels;
if (params->codec_id >= AV_CODEC_ID_PCM_F32BE) {
header.audioFormat = AUDION_FORMAT_FLOAT;
}
header.blockAlign = header.bitPerSample * header.numChannels >> 3;
header.byteRate = header.sampleRate * header.blockAlign;
[writeHandle seekToFileOffset:0];
[writeHandle writeData:[NSData dataWithBytes:(void *)&header length:sizeof(WavHeader)]];
[writeHandle seekToEndOfFile];
AVPacket *pkt = av_packet_alloc();
while (!self.stop) {
ret = av_read_frame(ctx, pkt);
if (ret == 0) {
[writeHandle writeData:[NSData dataWithBytes:pkt->data length:pkt->size]];
[writeHandle seekToEndOfFile];
header.dataChunkSize += pkt->size;
// 计算录音时长
unsigned long long ms = 1000.0 * header.dataChunkSize / header.byteRate;
NSLog(@"录音时长:%llu", ms);
} else if (ret == AVERROR(EAGAIN)) {
} else {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
NSLog(@"av_read_frame: %s", errbuf);
}
av_packet_unref(pkt);
}
// 写入dataChunkSize
[writeHandle seekToFileOffset:sizeof(WavHeader) - sizeof(header.dataChunkSize)];
[writeHandle writeData:[NSData dataWithBytes:(void *)&header.dataChunkSize length:sizeof(header.dataChunkSize)]];
// 写入riffChunkDataSize
[writeHandle seekToEndOfFile];
long long totalLen = [writeHandle offsetInFile];
header.riffChunkSize = uint32_t(totalLen - sizeof(header.riffChunkId) - sizeof(header.riffChunkSize));
[writeHandle seekToFileOffset:sizeof(header.riffChunkId)];
[writeHandle writeData:[NSData dataWithBytes:(void *)&header.riffChunkSize length:sizeof(header.riffChunkSize)]];
// 释放资源
av_packet_free(&pkt);
[writeHandle closeFile];
avformat_close_input(&ctx);
});
}
网友评论