学习雷神的博客
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_gray
、simplest_yuv420_halfy
、simplest_yuv420_border
、simplest_yuv420_graybar
、simplest_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通过对模拟信息进行采样、量化、编码三个过程,将声音的模拟信息(电信号)转为数字信息,交给计算机处理,它有三个最重要的特征:采样率、声道数、采样格式,两大模式:打包模式、平面模式。
- 声道布局包括:单声道(Mono,M)和立体声(Stereo,S),
- 常见的采样率有:44100、48000、16000,
- 采样格式:S16le、S32le、flt、dbl,还有对应的xxxp,p表示平面模式(Plnar)。
音频数据在传输过程中也有字节顺序,分为大端序(Big Endian,be)和小端序(Little Endian,le)两种。常见的为小端字节序。
更多详细的介绍内容:【音视频 | PCM】PCM格式详解
测试博主的函数:simplest_pcm16le_split
、simplest_pcm16le_halfvolumeleft
、simplest_pcm16le_doublespeed
、simplest_pcm16le_to_pcm8
、simplest_pcm16le_cut_singlechannel
、simplest_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;
}