音视频基础学习


学习雷神的博客

1. RGB、YUV像素级别的数据的转化以及处理

分离YUV420p像素数据中的Y/U/V三个分量步骤:先读取YUV文件,然后按照YUV的分布特征读取出来并且保存。

关键读取代码如下:

for(int i=0;i<num;i++){ 
    fread(pic,1,w*h*3/2,fp);
    //Y
    fwrite(pic,1,w*h,fp1);//w*h
    //U
    fwrite(pic+w*h,1,w*h/4,fp2);//w*h的后1/4
    //V
    fwrite(pic+w*h*5/4,1,w*h/4,fp3);//w*h的5/4的后1/4
}

由于我没有找到Y、U、V分量数据保存后的显示软件,就学习并使用了ffmpeg将.y文件转为png之后查看效果。转换过程:

ffmpeg -s 256x256 -pix_fmt gray -f rawvideo -i output_420_y.y lena_y.png

ffmpeg参数解释:

参数 含义
-s 256*256 指定图像分辨,上述示例中为256*256
-pix_fmt gray 制定像素格式为单通道的灰度图(gray)
-f rawvideo 输入的是裸的原始图像数据,没有文件头(Y/U/V)
-i xxx 输入文件路径
lena_y.png 输出文件名字,自动调用PNG编码器保存为PNG图片

接下来分别测试了simplest_yuv420_graysimplest_yuv420_halfysimplest_yuv420_bordersimplest_yuv420_graybarsimplest_yuv420_psnr这些函数。

(1)、simplest_yuv420_border函数关键代码

for(int i=0;i<num;i++){
    fread(pic,1,w*h*3/2,fp);
    //Y
    for(int j=0;j<h;j++){
        for(int k=0;k<w;k++){
            if(k<border||k>(w-border)||j<border||j>(h-border)){
                pic[j*w+k]=255;//修改为白色
                //pic[j*w+k]=0;
            }
        }
    }
    fwrite(pic,1,w*h*3/2,fp1);
}

修改w*h周围的border个像素的值,这样就在图片周围加入了白色边框。

(2)、学习了PSNR,PSNR即峰值信噪比(Peak Signal to Noise Ratio),它是最基本的视频质量评估方法之一,基于图像的像素点,借助均方误差来计算图像失真情况,缺点也很明显,就是不符合人眼的主观感受。还要其他常见的图像质量检测方法如SSIM(Structural Similarity Index结构相似度)等,这两篇文章中说的非常详细:谈谈图像质量量化评估标准图像质量评估综述

simplest_yuv420_psnr函数的关键代码:

for(int i=0;i<num;i++){
    fread(pic1,1,w*h,fp1);
    fread(pic2,1,w*h,fp2);

    double mse_sum=0,mse=0,psnr=0;
    for(int j=0;j<w*h;j++){
        mse_sum+=pow((double)(pic1[j]-pic2[j]),2);
        if((pic1[j]-pic2[j]) != 0) {
            printf("pix1[100], %d; pix2[100], %d\n", pic1[j], pic2[j]);
        }
    }
    mse=mse_sum/(w*h);
    psnr=10*log10(255.0*255.0/mse);
    printf("%5.3f\n",psnr);
	//跳过接下来1/2
    fseek(fp1,w*h/2,SEEK_CUR);
    fseek(fp2,w*h/2,SEEK_CUR);
    // //U
    // fwrite(pic+w*h,1,w*h/4,fp2);
    // //V
    // fwrite(pic+w*h*5/4,1,w*h/4,fp3);
    // u和v各占1/4,加起来等于1/2,

}

2. PCM音频采样数据处理

PCM,脉冲编码调制,PCM通过对模拟信息进行采样、量化、编码三个过程,将声音的模拟信息(电信号)转为数字信息,交给计算机处理,它有三个最重要的特征:采样率、声道数、采样格式,两大模式:打包模式、平面模式。

  1. 声道布局包括:单声道(Mono,M)和立体声(Stereo,S),
  2. 常见的采样率有:44100、48000、16000,
  3. 采样格式:S16le、S32le、flt、dbl,还有对应的xxxp,p表示平面模式(Plnar)。

音频数据在传输过程中也有字节顺序,分为大端序(Big Endian,be)和小端序(Little Endian,le)两种。常见的为小端字节序。

更多详细的介绍内容:【音视频 | PCM】PCM格式详解

测试博主的函数:simplest_pcm16le_splitsimplest_pcm16le_halfvolumeleftsimplest_pcm16le_doublespeedsimplest_pcm16le_to_pcm8simplest_pcm16le_cut_singlechannelsimplest_pcm16le_to_wave。改写了一个函数simplest_pcm16le_cut_doublechannel

这些处理的数据都是PCM的原始音频数据,比较好处理,确定文件的采样格式之后,就和读取普通文件一样。

(1)、改善后的simplest_pcm16le_cut_singlechannel函数:

int simplest_pcm16le_cut_singlechannel(const char *url,int start_num,int dur_num){
	FILE *fp=fopen(url,"rb+");
	FILE *fp1=fopen("output_cut.pcm","wb+");
	FILE *fp_stat=fopen("output_cut.txt","wb+");

	unsigned char *sample=(unsigned char *)malloc(2);

	int cnt=0;
	while(fread(sample,1,2,fp) == 2){
		if(cnt>start_num&&cnt<=(start_num+dur_num)){
			fwrite(sample,1,2,fp1);
			// 1
			// short samplenum = sample[1];
			// samplenum=samplenum*256;
			// samplenum=samplenum+sample[0];
            
            // 2
			// short samplenum = sample[1]<<8 + sample[0];
			
            // 3
            short *samplenum = (short *)sample;

			fprintf(fp_stat,"%6d,",*samplenum);
			if(cnt%10==0)
				fprintf(fp_stat,"\n");
		}
		cnt++;
	}

	free(sample);
	fclose(fp);
	fclose(fp1);
	fclose(fp_stat);
	return 0;
}

该函数保存采样值部分的三种数值转换方法都是正确的,第三种是直接让系统自动处理小端字节序问题,按照小端顺序正确解释为short数据,而不需要像前两种一样将[1]位置的数据移动到前面以解释short型数据。

(2)、simplest_pcm16le_doublespeed函数关键代码

while(!feof(fp)){
    fread(sample,1,4,fp);
    if(cnt%2!=0){
        //L
        fwrite(sample,1,2,fp1);
        //R
        fwrite(sample+2,1,2,fp1);
    }
    cnt++;
}

该代码保存奇数位置的样本,这样就将播放速度加速,也就是变成了两倍速。同时为了便于更好的检查输出的样本值的波形图变化,使用了ffmpeg库的showwavespic滤镜,生成指定分辨率的音频波形图文件

ffmpeg -i output_l.wav -filter_complex "showwavespic=s=1280x240" -c:v png -y output_l_waveform.png

命令参数解释:

命令部分 说明
ffmpeg FFmpeg 是一个用于处理多媒体文件的命令行工具,支持音频和视频的转换、处理和滤镜应用。
-i output_l.wav -i 参数指定输入文件,这里是 output_l.wav,一个音频文件(通常为 WAV 格式)。FFmpeg 将使用此文件作为处理源。
-filter_complex “showwavespic=s=1280x240” -filter_complex 用于应用复杂的滤镜处理。 使用 showwavespic 滤镜,从音频生成波形图。 showwavespic 生成一张表示音频波形的静态图像。 s=1280x240 设置输出图像分辨率为 1280 像素宽、240 像素高。
-c:v png -c:v 指定输出视频的编码格式,这里使用 png,表示输出为一张 PNG 图像。 PNG 是一种无损图像格式,非常适合保存波形图。
-y -y 参数指示 FFmpeg 如果输出文件(output_l_waveform.png)已存在,则自动覆盖,无需用户确认。
output_l_waveform.png 输出文件的名称,生成一张包含输入音频波形可视化的 PNG 图像。

(3)、simplest_pcm16le_to_pcm8函数:

while(fread(sample,1,4,fp) == 4){
    short *samplenum16=NULL;
    char samplenum8=0;
    unsigned char samplenum8_u=0;
    //(-32768-32767)
    samplenum16=(short *)sample;
    samplenum8=(*samplenum16)>>8;
    //(0-255)
    samplenum8_u=samplenum8+128;
    //L
    fwrite(&samplenum8_u,1,1,fp1);
    
    samplenum16=(short *)(sample+2);
    samplenum8=(*samplenum16)>>8;
    samplenum8_u=samplenum8+128;
    //R
    fwrite(&samplenum8_u,1,1,fp1);
    cnt++;
}

该函数只将左、右声道的16-bit信息的高八位字节保留下来,同时修正了while(!feof(fp))可能多读一帧的风险。

这里解释为什么保留高八位信息,而不是低八位信息:

在转换为short型数据之后,高八位信息决定了波形的大致轮廓(整体形状和方向),低八位决定波形的细节、微小变化(精细结构),这样保留虽然失真,但是依旧可以听清楚原始音频,而保留低八位就只剩下微小抖动,没有了主信息,是不可取的。

(4)、simplest_pcm16le_to_wave函数,该函数原始代码会出现转码错误,有BUG,改正后的代码如下:

#pragma pack(push,1)
struct WAVE_HEADER {
    char fccID[4];       // "RIFF"
    uint32_t dwSize;     // 文件总长度 - 8
    char fccType[4];     // "WAVE"
};

struct WAVE_FMT {
    char fccID[4];          // "fmt "
    uint32_t dwSize;        // 固定为 16
    uint16_t wFormatTag;    // 1 = PCM
    uint16_t wChannels;     // 通道数
    uint32_t dwSamplesPerSec;   // 采样率
    uint32_t dwAvgBytesPerSec; // 每秒字节数
    uint16_t wBlockAlign;       // 一个采样帧的大小
    uint16_t uiBitsPerSample;   // 每个样本的位数
};

struct WAVE_DATA {
    char fccID[4];       // "data"
    uint32_t dwSize;     // 数据块大小(单位:字节)
};
#pragma pack(pop)
/**
 * Convert PCM16LE raw data to WAVE format
 * @param pcmpath      Input PCM file.
 * @param channels     Channel number of PCM file.
 * @param sample_rate  Sample rate of PCM file.
 * @param wavepath     Output WAVE file.
 */
int simplest_pcm16le_to_wave(const char *pcmpath,int channels,int sample_rate,const char *wavepath)
{
	if(channels==0||sample_rate==0){
		channels = 2;
		sample_rate = 44100;
	}
	const int bits_per_sample = 16;
	unsigned short m_pcmData;

    FILE *fpcm,*fpout;
	fpcm=fopen(pcmpath,"rb");
    if(fpcm == NULL) {
        printf("Open pcm file error\n");
		fclose(fpcm);
        return -1;
    }
	fpout=fopen(wavepath,"wb+");
    if(fpout == NULL) {
        printf("Create wav file error\n");
		fclose(fpout);
        return -1;
    }
	//WAVE_HEADER
	WAVE_HEADER pcmHEADER;
    memcpy(pcmHEADER.fccID,"RIFF",strlen("RIFF"));
	uint32_t riff_size_placeholder = 0;
	pcmHEADER.dwSize = riff_size_placeholder;
    memcpy(pcmHEADER.fccType,"WAVE",strlen("WAVE"));

	//WAVE_FMT
	WAVE_FMT pcmFMT;
	memcpy(pcmFMT.fccID,"fmt ",strlen("fmt "));
    pcmFMT.dwSize = 16;
    pcmFMT.wFormatTag = 1;
    pcmFMT.wChannels = channels;
    pcmFMT.dwSamplesPerSec = sample_rate;
    pcmFMT.dwAvgBytesPerSec = sample_rate * channels * bits_per_sample / 8;
    pcmFMT.wBlockAlign = channels * bits_per_sample / 8;
    pcmFMT.uiBitsPerSample = bits_per_sample;    

    //WAVE_DATA;
	WAVE_DATA pcmDATA;
    memcpy(pcmDATA.fccID,"data",strlen("data"));
    pcmDATA.dwSize = 0;

	// 统一写入
	fwrite(&pcmHEADER, sizeof(WAVE_HEADER), 1, fpout);
	fwrite(&pcmFMT, sizeof(WAVE_FMT), 1, fpout);
	fwrite(&pcmDATA, sizeof(WAVE_DATA), 1, fpout);

    uint8_t buffer[4096];
	size_t bytes_read, total_data_size = 0;
	while ((bytes_read = fread(buffer, 1, sizeof(buffer), fpcm)) > 0) {
		fwrite(buffer, 1, bytes_read, fpout);
        total_data_size += bytes_read;
	}

    pcmHEADER.dwSize = sizeof(WAVE_HEADER) + sizeof(WAVE_FMT) + sizeof(WAVE_DATA) + pcmDATA.dwSize - 8;
	pcmDATA.dwSize = total_data_size;

	fseek(fpout, 0, SEEK_SET);
	fwrite(&pcmHEADER, sizeof(WAVE_HEADER), 1, fpout);
	fseek(fpout, sizeof(WAVE_HEADER) + sizeof(WAVE_FMT), SEEK_SET);
	fwrite(&pcmDATA, sizeof(WAVE_DATA), 1, fpout);
	
	fclose(fpcm);
    fclose(fpout);
    return 0;
}

在结构体的声明阶段,还使用了#pragma pack,这是由于struct 结构体可能会自动填充对齐字节,加入这个#pragma pack结构,强制为单字节填充,让WAV的文件头格式的每个字段都有固定的大小和位置,编译器也不会对结构体成员进行内存对齐操作。

#pragma pack 是一个预处理指令,用于指定数据在内存中的对齐方式,push 是将原来的代码的对齐方式压栈保存,pop 是恢复原本的对齐方式。


3. H.264视频码流解析

H.264是一种对视频进行编解码的标准,用于将视频数据进行压缩。它是由一个个NALU组成的,每一个NALU之间通过起始码进行分割。对视频数据压缩时,H.264使用I帧、P帧、B帧的方式,I帧可以理解为关键帧(帧内压缩),P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,B帧是双向差别帧,也就是B帧记录的是本帧与前后帧的差别(具体比较复杂,有4种情况)。

详细介绍:H.264视频码流解析


4. AAC

AAC原始码流是由一个个的ADTS frame(音频数据传输流帧)组成的;每一帧由ADTS头(ADTS Header)和原始数据块(ADTS ES)组成。

详细介绍:AAC音频码流解析


5. FLV封装格式解析

FLV(FLash Video)是一种网络视频格式,用作流媒体格式,依赖于 Adobe Flash 插件进行解析与播放。但是渐渐的被抛弃了,现已被更加开放、兼容性更强的 HTML5 视频技术所替代。

总体上看,FLV包括文件头(File Header)和文件体(File Body)两部分,其中文件体由一系列的Tag组成。其中,每个Tag前面还包含了Previous Tag Size字段,表示前面一个Tag的大小。Tag的类型可以是视频、音频和Script,每个Tag只能包含以上三种类型的数据中的一种。

详细介绍:FLV封装格式FLV封装格式—音视频基础知识

由于原来的代码结构并没有那么清晰,所以我优化了代码结构,simplest_mediadata_flv.cpp。

#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
using namespace std;

#define TAG_TYPE_AUDIO 8
#define TAG_TYPE_VIDEO 9
#define TAG_TYPE_SCRIPT 18

#pragma pack(1)
struct FLV_HEADER {
    uint8_t signature[3];
    uint8_t version;
    uint8_t flags; // TypeFlagsReserved(5 bits)+TypeFlagsAudio(1 bit)+TypeFlagsReserved(1 bit)+TypeFlagsVideo(1 bit)
    uint32_t header_size;
};

typedef struct {
	uint8_t TagType;
	uint8_t DataSize[3];
	uint8_t Timestamp[3];
    uint8_t TimestampExtended;
	uint8_t StreamID[3]; // stream id
} TAG_HEADER;
#pragma pack()

uint32_t reverse_byte(uint8_t* p, size_t len) {
    uint32_t result = 0;
    for (size_t i = 0; i < len; i++) {
        uint32_t shift = 8 * (len - i - 1);
        result |= (*(p + i) << shift);
    }
    return result;
}

void print_flv_header(FILE* out, FLV_HEADER * flv) {
    fprintf(out, "============== FLV Header ==============\n");
    fprintf(out, "Signature:  0x %c %c %c\n", flv->signature[0], flv->signature[1], flv->signature[2]);
    fprintf(out, "Version:    0x %X\n", flv->version);
    fprintf(out, "Flags:      0x %X\n", flv->flags);
    fprintf(out, "HeaderSize: 0x %X\n", reverse_byte((uint8_t *)&flv->header_size, sizeof(flv->header_size)));
    fprintf(out, "========================================\n");
}

void print_tag_info(FILE* out, TAG_HEADER *tag_header) {
    string tag_type;
    switch(tag_header->TagType) {
        case TAG_TYPE_SCRIPT: tag_type = "SCRIPT"; break;
        case TAG_TYPE_AUDIO: tag_type = "AUDI0"; break;
        case TAG_TYPE_VIDEO: tag_type = "VIDEO"; break;
        default: tag_type = "UNKNOWN"; break;
    }
    int data_size = (tag_header->DataSize[0] << 16) +
                    (tag_header->DataSize[1] << 8) +
                    tag_header->DataSize[2];
    
    int timestamp = (tag_header->Timestamp[0] << 16) +
                    (tag_header->Timestamp[1] << 8) +
                    tag_header->Timestamp[2];

    timestamp |= (tag_header->TimestampExtended << 24);

    fprintf(out, "[%6s] %6d %6d |", tag_type.c_str(), data_size, timestamp);
}

void process_audio_tag(FILE *in_file, FILE *audio_file, TAG_HEADER *tag_header, FILE *out) {
    string audio_tag;
    uint8_t first_byte = fgetc(in_file);
    int audio_format = first_byte & 0xF0;
    audio_format >>= 4;
    int sample_rate = first_byte & 0x0c;
    sample_rate >>= 2;
    int sample_size = first_byte & 0x02;
    sample_size >>= 1;
    int channel = first_byte & 0x01;

    audio_tag += "| ";
    switch(audio_format) {
        case 0: audio_tag += "Linear PCM, platform endian"; break;
        case 1: audio_tag += "ADPCM"; break;
        case 2: audio_tag += "MP3"; break;
        case 3: audio_tag += "Linear PCM, little endian"; break;
        case 4: audio_tag += "Nellymoser 16-kHz mono"; break;
        case 5: audio_tag += "Nellymoser 8-kHz mono"; break;
        case 6: audio_tag += "Nellymoser"; break;
        case 7: audio_tag += "G.711 A-law logarithmic PCM"; break;
        case 8: audio_tag += "G.711 mu-law logarithmic PCM"; break;
        case 9: audio_tag += "reserved"; break;
        case 10: audio_tag += "AAC"; break;
        case 11: audio_tag += "Speex"; break;
        case 14: audio_tag += "MP3 8-Khz"; break;
        case 15: audio_tag += "Device-specific sound"; break;
        default: audio_tag += "UNKNOWN"; break;
    }

    audio_tag += "| ";    
    switch(sample_rate) {
        case 0: audio_tag += "5.5-kHz"; break;
        case 1: audio_tag += "1-kHz"; break;
        case 2: audio_tag += "22-kHz"; break;
        case 3: audio_tag += "44-kHz"; break;
        default: audio_tag += "UNKNOWN"; break;
    }

    audio_tag += "| ";    
    switch(sample_size) {
        case 0: audio_tag += "8 bit"; break;
        case 1: audio_tag += "16 bit"; break;
        default: audio_tag += "UNKNOWN"; break;
    }

    audio_tag += "| ";    
    switch(channel) {
        case 0: audio_tag += "Mono"; break;
        case 1: audio_tag += "Stereo"; break;
        default: audio_tag += "UNKNOWN"; break;
    }
    fprintf(out, "%s", audio_tag.c_str());

    int data_size = (tag_header->DataSize[0] << 16) |
                    (tag_header->DataSize[1] << 8) |
                    tag_header->DataSize[2];

    if (audio_file != nullptr) {
        fputc(first_byte, audio_file);  // 写入第一个字节(音频信息)
        for (int i = 1; i < data_size; i++) {
            fputc(fgetc(in_file), audio_file);
        }
    } else {
        fseek(in_file, data_size - 1, SEEK_CUR); // 已读1字节
    }
}

void process_video_tag(FILE *in_file, FILE *video_file, TAG_HEADER *tag_header, FLV_HEADER *flv, FILE *out) {
    string video_tag;
    u_char first_byte = fgetc(in_file);
    int frame_type = first_byte & 0xF0;
    frame_type >>= 4;
    int codec_id = first_byte & 0x0F;

    video_tag += "| ";    
    switch(frame_type) {
        case 1: video_tag += "key frame "; break;
        case 2: video_tag += "inter frame "; break;
        case 3: video_tag += "disposable inter frame "; break;
        case 4: video_tag += "generated keyframe"; break;
        case 5: video_tag += "video info/frame frame"; break;
        default: video_tag += "UNKNOWN"; break;
    }

    
    video_tag += "| ";
    switch(codec_id) {
        case 1: video_tag += "JPEG (currently unused)"; break;
        case 2: video_tag += "Sorenson H.263"; break;
        case 3: video_tag += "Screen video"; break;
        case 4: video_tag += "On2 VP6"; break;
        case 5: video_tag += "On2 VP6 with alpha channel"; break;
        case 6: video_tag += "Screen video version 2"; break;
        case 7: video_tag += "AVC"; break;
        default: video_tag += "UNKNOWN"; break;
    }
    fprintf(out, "%s", video_tag.c_str());

    fseek(in_file, -1, SEEK_CUR);

    size_t data_size = (tag_header->DataSize[0] << 16) |
                    (tag_header->DataSize[1] << 8) |
                    tag_header->DataSize[2];

    if (video_file != nullptr) {
        fwrite(tag_header, 1, sizeof(TAG_HEADER), video_file);
        for (size_t i = 0; i < data_size; i++) {
            fputc(fgetc(in_file), video_file);
        }
        uint32_t prev_tag_size = sizeof(TAG_HEADER) + data_size;
        fwrite(&prev_tag_size, 1, sizeof(prev_tag_size), video_file);
    } else {
        fseek(in_file, data_size, SEEK_CUR);
    }
}

int simplest_flv_parser1(const char* url) {
    int output_a = 1, output_v = 1;

    FILE *in_file = nullptr, *video_file = nullptr, *audio_file = nullptr;
    FILE *myout = stdout;

    FLV_HEADER flv;
    TAG_HEADER tag_header;
    uint32_t previous_tag_size;

    if((in_file = fopen(url, "rb")) == nullptr) {
        fprintf(myout, "Cannot open input file: %s\n", url);
        return -1;
    }
    if(fread(&flv, 1, sizeof(FLV_HEADER), in_file) != sizeof(FLV_HEADER)) {
        fprintf(myout, "Failed to read flvheader.\n");
        fclose(in_file);
        return -1;
    }
    print_flv_header(myout, &flv);
    fseek(in_file, reverse_byte((uint8_t *)&flv.header_size, sizeof(flv.header_size)), SEEK_SET);

    while(!feof(in_file)) {
        if(fread(&previous_tag_size, 1, sizeof(previous_tag_size), in_file) != sizeof(previous_tag_size)) {
            break;
        }
        if(fread(&tag_header, 1, sizeof(TAG_HEADER), in_file) != sizeof(TAG_HEADER)) {
            break;
        }

        print_tag_info(myout, &tag_header);

        switch(tag_header.TagType) {
            case TAG_TYPE_AUDIO:
                if(output_a && audio_file == nullptr) {
                    audio_file = fopen("output.mp3", "wb");
                }
                process_audio_tag(in_file, audio_file, &tag_header, myout);
                break;
            case TAG_TYPE_VIDEO:
                if (output_v && video_file == nullptr) {
                    video_file = fopen("output.flv", "wb");
                    if (video_file) {
                        fwrite(&flv, 1, sizeof(FLV_HEADER), video_file);
                        uint32_t header_tag_size = 0;
                        fwrite(&header_tag_size, 1, sizeof(header_tag_size), video_file);
                    }
                }
                process_video_tag(in_file, video_file, &tag_header, &flv, myout);
                break;
            default:
                int skip_size = (tag_header.DataSize[0] << 16) |
                                (tag_header.DataSize[1] << 8) |
                                tag_header.DataSize[2];
                fseek(in_file, skip_size, SEEK_CUR);
                break;
        }
        fprintf(myout, "\n");
    }
    if(video_file != nullptr) fclose(video_file);
    if(audio_file != nullptr) fclose(audio_file);
    if(in_file != nullptr) fclose(in_file);
    return 0;
}


文章作者: AllenMirac
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 AllenMirac !
  目录