美文网首页iOS音频开发音视频从入门到放弃
FFmpeg录制音频PCM,WAV, PCM 转WAV

FFmpeg录制音频PCM,WAV, PCM 转WAV

作者: lieon | 来源:发表于2021-07-13 11:31 被阅读0次

    音频录制与播放命令

    • 录制
    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文件格式 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数据计算 dataChunkSizeriffChunkSize
    • 写入头部数据到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);
            
        });
    }
    

    相关文章

      网友评论

        本文标题:FFmpeg录制音频PCM,WAV, PCM 转WAV

        本文链接:https://www.haomeiwen.com/subject/ymxdpltx.html