FFmpeg 通过 showwavespic 获取音频的频谱图
FFmpeg 的 showwavespic 滤镜如何得到频谱图
音频数据通常由波形图像表示。
FFmpeg 通过使用 showwavespic 可以得到音频数据的频谱图
ffmpeg -i input -filter_complex "showwavespic=s=640x120" -frames:v 1 output.png
运行上面一条命令之后,即可得到一张如下的图片:
output.png那么 FFmpeg 是如何将音频数据转换为波形图的呢?
首先通过命令我们知道使用了名为showwavespic
的滤镜,根据名字大概猜想此滤镜就是生成频谱图的关键所在。
所以,我么直接定位到 showwavespic
的定义处:
// showwavespic 滤镜的输入
static const AVFilterPad showwavespic_inputs[] = {
{
.name = "default",
.type = AVMEDIA_TYPE_AUDIO,
.config_props = showwavespic_config_input,
.filter_frame = showwavespic_filter_frame,
},
{ NULL }
};
// showwavespic 滤镜的输出
static const AVFilterPad showwavespic_outputs[] = {
{
.name = "default",
.type = AVMEDIA_TYPE_VIDEO,
.config_props = config_output, // 配置下一个滤镜的相关参数(例如输出frame 的宽、高)
.request_frame = request_frame,
},
{ NULL }
};
AVFilter ff_avf_showwavespic = {
.name = "showwavespic", // 输入的音频转换为频谱图输出
.description = NULL_IF_CONFIG_SMALL("Convert input audio to a video output single picture."),
.init = init, // 初始化方法
.uninit = uninit,
.query_formats = query_formats, // 滤镜支持的格式
.priv_size = sizeof(ShowWavesContext),
.inputs = showwavespic_inputs,
.outputs = showwavespic_outputs,
.priv_class = &showwavespic_class,
};
通过参考其他资源,理清楚滤镜的工作流程。花费几天的时间阅读 FFmpeg 的源码,生成波形图的原理 -- 解码音频文件得到音频裸数据 —> 通过 showwavespic 滤镜处理PCM数据得到波形图
showwavespic 滤镜是如何处理 PCM 数据得到波形图的呢?
PCM 数据
首先我们要了解什么是 PCM 音频数据:
PCM(Pulse Code Modulation)称为脉冲编码调制,PCM 音频数据是未经压缩的音频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准的数字音频数据。
存储格式
如果是单声道的音频文件,采样数据按时间的先后顺序依次存入(有时也会采用LRLRLR方式存储,只是另一个声道的数据为0),如果是双声道的话就按照LRLRLR的方式存储。
PCM音频数据存储格式.png单声道
+------+------+------+------+------+------+------+------+------+
| 500 | 300 | -100 | -20 | -300 | 900 | -200 | -50 | 250 |
+------+------+------+------+------+------+------+------+------+
每个采样的整数的大小最小为 -32768,最大为 32768。根据采样数据的位置和值画一个图的话,就会得到像播放器上那样的波浪形图。
波形图.png立体声的采样是每一个 frame 是一个 16bit 的采样点。左右声道的数据交叉存放。
那么采样数据的绝对值按照生成图片的高的比例即可得出振幅。频率通过生成图片的宽计算得到。
- 音频文件解码得到 PCM(音频裸数据), 统计音频的采样总数
- 以 采样总数 / 输出图片的宽度 为波形图统计频率
- 采样数据的绝对值 * 生成图片的高度 / 32768 计算得出振幅大小
- 滤镜处理流程
-
流程详情
-
init
-- showwavespic 滤镜的初始化static av_cold int init(AVFilterContext *ctx) { // showwaves 滤镜的私有数据 ShowWavesContext *showwaves = ctx->priv; if (!strcmp(ctx->filter->name, "showwavespic")) { // 如果是 showwavespic 滤镜 showwaves->single_pic = 1; // 使用 cline 的绘图 mode showwaves->mode = MODE_CENTERED_LINE; } return 0; }
-
showwavespic_config_input
-- 配置 showwavespic 相关属性static int showwavespic_config_input(AVFilterLink *inlink) { // showwavespic 滤镜 AVFilterContext *ctx = inlink->dst; // 滤镜私有参数 ShowWavesContext *showwaves = ctx->priv; if (showwaves->single_pic) { // 声道采样数据的和(初始化数组内存空间) showwaves->sum = av_mallocz_array(inlink->channels, sizeof(*showwaves->sum)); if (!showwaves->sum) return AVERROR(ENOMEM); } return 0; }
-
config_output
-- 配置输出图像的参数 & showwavespic 滤镜参数static int config_output(AVFilterLink *outlink) { // 代码较长,省略 ... // 采样的x、y坐标 showwaves->buf_idx = 0; if (!(showwaves->buf_idy = av_mallocz_array(nb_channels, sizeof(*showwaves->buf_idy)))) { av_log(ctx, AV_LOG_ERROR, "Could not allocate showwaves buffer\n"); return AVERROR(ENOMEM); } // 输出图片的宽高、宽高比、帧率 outlink->w = showwaves->w; outlink->h = showwaves->h; outlink->sample_aspect_ratio = (AVRational){1,1}; // 1 outlink->frame_rate = av_div_q((AVRational){inlink->sample_rate,showwaves->n}, (AVRational){showwaves->w,1}); // 设置 draw_sample & get_h 函数 ... // 默认使用的颜色为: red|green|... colors = av_strdup(showwaves->colors); if (!colors) return AVERROR(ENOMEM); /* multiplication factor, pre-computed to avoid in-loop divisions */ x = 255 / ((showwaves->split_channels ? 1 : nb_channels) * showwaves->n); // 255/2 if (outlink->format == AV_PIX_FMT_RGBA) { uint8_t fg[4] = { 0xff, 0xff, 0xff, 0xff }; // 左声道为红色,右声道为绿色 for (ch = 0; ch < nb_channels; ch++) { char *color; color = av_strtok(ch == 0 ? colors : NULL, " |", &saveptr); if (color) av_parse_color(fg, color, -1, ctx); showwaves->fg[4*ch + 0] = fg[0] * x / 255.; showwaves->fg[4*ch + 1] = fg[1] * x / 255.; showwaves->fg[4*ch + 2] = fg[2] * x / 255.; showwaves->fg[4*ch + 3] = fg[3] * x / 255.; } } else { for (ch = 0; ch < nb_channels; ch++) showwaves->fg[4 * ch + 0] = x; } av_free(colors); }
-
showwavespic_filter_frame
-- 配置 showwavespic 滤镜的参数(初始化输出frame、音频帧等)static int showwavespic_filter_frame(AVFilterLink *inlink, AVFrame *insamples) { // showwavespic 滤镜 AVFilterContext *ctx = inlink->dst; // showwavespic 滤镜与其下一个滤镜之间的联系 AVFilterLink *outlink = ctx->outputs[0]; // showwavespic 滤镜的私有数据 ShowWavesContext *showwaves = ctx->priv; // 输入数据 int16_t *p = (int16_t *)insamples->data[0]; int ret = 0; if (showwaves->single_pic) { struct frame_node *f; // 给 showwaves 滤镜的输出图片 frame 分配一个空的buffer ret = alloc_out_frame(showwaves, p, inlink, outlink, insamples); if (ret < 0) goto end; /* queue the audio frame (audio frame 队列)*/ f = av_malloc(sizeof(*f)); if (!f) { ret = AVERROR(ENOMEM); goto end; } f->frame = insamples; f->next = NULL; // showwavespic 滤镜的音频队列 if (!showwaves->last_frame) { showwaves->audio_frames = showwaves->last_frame = f; } else { showwaves->last_frame->next = f; showwaves->last_frame = f; } // 总音频采样数 showwaves->total_samples += insamples->nb_samples; return 0; } end: av_frame_free(&insamples); return ret; }
-
request_frame
-- 请求滤镜处理后的 framestatic int request_frame(AVFilterLink *outlink) { ShowWavesContext *showwaves = outlink->src->priv; AVFilterLink *inlink = outlink->src->inputs[0]; int ret; ret = ff_request_frame(inlink); if (ret == AVERROR_EOF && showwaves->outpicref) { // 读取完所有的 frame if (showwaves->single_pic) push_single_pic(outlink); // 生成频谱图 else push_frame(outlink); } return ret; }
push_single_pic
-- 根据采样数据生成频谱图,并传给下一个滤镜static int push_single_pic(AVFilterLink *outlink) { // showwavespic 滤镜 AVFilterContext *ctx = outlink->src; // showwavespic 与上一个滤镜之间的联系 AVFilterLink *inlink = ctx->inputs[0]; // showwavespic 滤镜的私有数据 ShowWavesContext *showwaves = ctx->priv; // max_samples -- 音频总采样数 / 输出图片的宽(频率) int64_t n = 0, max_samples = showwaves->total_samples / outlink->w; // 输出 frame AVFrame *out = showwaves->outpicref; struct frame_node *node; // 声道数 const int nb_channels = inlink->channels; const int ch_height = showwaves->split_channels ? outlink->h / nb_channels : outlink->h; // h const int linesize = out->linesize[0]; const int pixstep = showwaves->pixstep; // 4 int col = 0; int64_t *sum = showwaves->sum; if (max_samples == 0) { av_log(ctx, AV_LOG_ERROR, "Too few samples\n"); return AVERROR(EINVAL); } av_log(ctx, AV_LOG_DEBUG, "Create frame averaging %"PRId64" samples per column\n", max_samples); memset(sum, 0, nb_channels); // 循环从滤镜 audio 队列中取出 frame for (node = showwaves->audio_frames; node; node = node->next) { int i; const AVFrame *frame = node->frame; // 当前 frame 的数据 const int16_t *p = (const int16_t *)frame->data[0]; // 当前 frame 的采样数 for (i = 0; i < frame->nb_samples; i++) { int ch; for (ch = 0; ch < nb_channels; ch++) sum[ch] += abs(p[ch + i*nb_channels]) << 1; if (n++ == max_samples) { for (ch = 0; ch < nb_channels; ch++) { int16_t sample = sum[ch] / max_samples; uint8_t *buf = out->data[0] + col * pixstep; int h; if (showwaves->split_channels) buf += ch*ch_height*linesize; av_assert0(col < outlink->w); h = showwaves->get_h(sample, ch_height); showwaves->draw_sample(buf, ch_height, linesize, &showwaves->buf_idy[ch], &showwaves->fg[ch * 4], h); sum[ch] = 0; } col++; n = 0; } } } return push_frame(outlink); }
-