diff --git a/_config.yml b/_config.yml index a2dfab19962..fc27df04c6a 100644 --- a/_config.yml +++ b/_config.yml @@ -17,7 +17,7 @@ ## => Site Settings ############################## text_skin: default # "default" (default), "dark", "forest", "ocean", "chocolate", "orange" -highlight_theme: default # "default" (default), "tomorrow", "tomorrow-night", "tomorrow-night-eighties", "tomorrow-night-blue", "tomorrow-night-bright" +highlight_theme: tomorrow-night # "default" (default), "tomorrow", "tomorrow-night", "tomorrow-night-eighties", "tomorrow-night-blue", "tomorrow-night-bright" url : # the base hostname & protocol for your site e.g. https://www.someone.com baseurl : # does not include hostname title : 我是小飞飞 @@ -88,7 +88,7 @@ mathjax: # false (default), true mathjax_autoNumber: # false (default), true ## Mermaid -mermaid: # false (default), true +mermaid: true # false (default), true ## Chart chart: # false (default), true diff --git a/_posts/2022-08-27-01-audio-resampe.md b/_posts/2022-08-27-01-audio-resampe.md new file mode 100644 index 00000000000..83b9f112503 --- /dev/null +++ b/_posts/2022-08-27-01-audio-resampe.md @@ -0,0 +1,373 @@ +--- +title: FFPeg 音频 +tags: FFMpeg +--- +### 一些概念 + +音频有几个重要的参数: + +- 采样率(sample_rate,单位是Hz); +- 通道数(channels); +- 采样格式(sample_fmt,见AVSampleFormat,其实就是采样的精度,位数越多表示越精细,当然占用的空间也会比较大)。 + +在ffmpeg中,音频数据的存储格式也有 `planar` 和 `packed` 之分,`planar` 表示每个通道数据单独存储,`packed` 表示通道数据交叉存储,在 `AVSampleFormat` 的类型末尾带P的就表示是 `planar` 格式的,比如双声道,用 L 表示左声道,R 表示右声道,那么: + +- packed 的存储为:LRLRLRLRLRLRLRLR; +- planar 的存储为:LLLLRRRRLLLLRRRR。 + +### 采样格式 +采样格式定义在 `libavutil/samplefmt.h` 中 + +```c +enum AVSampleFormat { + AV_SAMPLE_FMT_NONE = -1, + AV_SAMPLE_FMT_U8, ///< unsigned 8 bits + AV_SAMPLE_FMT_S16, ///< signed 16 bits + AV_SAMPLE_FMT_S32, ///< signed 32 bits + AV_SAMPLE_FMT_FLT, ///< float + AV_SAMPLE_FMT_DBL, ///< double + + AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar + AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar + AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar + AV_SAMPLE_FMT_FLTP, ///< float, planar + AV_SAMPLE_FMT_DBLP, ///< double, planar + AV_SAMPLE_FMT_S64, ///< signed 64 bits + AV_SAMPLE_FMT_S64P, ///< signed 64 bits, planar + + AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically +}; +``` +### 通道布局(channel layout) +在 `AVCodecContext` 的结构体中有 `channel_layout` 和 `request_channel_layout` 这两个参数,它们的类型是 `uint64_t` ,表示无符号 64 位整型。 + +很多人对这个参数无从下手,因为不知道它表示什么,该怎么使用它,反正我是一脸懵逼! + +`request_channel_layout` 表示你期待的通道布局,而 `channel_layout` 表示实际的通道布局,`channel_layout` 是由解码器设置的。 + +`channel_layout` 的值转换成二进制后,有多少个1就表示多少个通道。在 `channel_layout.h` 头文件中定义了有关它的一些掩码,通过这些掩码的组合就能凑成多种通道布局例如: + +- AV_CH_LAYOUT_STEREO 是立体声(2通道),其通道的存放顺序为 LEFT \| RIGHT; +- AV_CH_LAYOUT_4POINT0 是4通道,其通道的存放顺序为 LEFT \| RIGHT \| FRONT-CENTER \| BACK-CENTER + +有了 `channel_layout` ,我们就知道了通道的顺序,这样我们就可以随意取得我们指定的通道的数据。这里注意一点的是,sdl 不支持音频平面格式(`planar`),因此如果用sdl播放音频必须先得转成 `packed` 格式。 + +以下是channel_layout.h的部分摘抄: + +```c +/** + * @defgroup channel_masks Audio channel masks + * + * A channel layout is a 64-bits integer with a bit set for every channel. + * The number of bits set must be equal to the number of channels. + * The value 0 means that the channel layout is not known. + * @note this data structure is not powerful enough to handle channels + * combinations that have the same channel multiple times, such as + * dual-mono. + * + * @{ + */ +#define AV_CH_FRONT_LEFT 0x00000001 +#define AV_CH_FRONT_RIGHT 0x00000002 +#define AV_CH_FRONT_CENTER 0x00000004 +#define AV_CH_LOW_FREQUENCY 0x00000008 +#define AV_CH_BACK_LEFT 0x00000010 +#define AV_CH_BACK_RIGHT 0x00000020 +#define AV_CH_FRONT_LEFT_OF_CENTER 0x00000040 +#define AV_CH_FRONT_RIGHT_OF_CENTER 0x00000080 +#define AV_CH_BACK_CENTER 0x00000100 +#define AV_CH_SIDE_LEFT 0x00000200 +#define AV_CH_SIDE_RIGHT 0x00000400 +#define AV_CH_TOP_CENTER 0x00000800 +#define AV_CH_TOP_FRONT_LEFT 0x00001000 +#define AV_CH_TOP_FRONT_CENTER 0x00002000 +#define AV_CH_TOP_FRONT_RIGHT 0x00004000 +#define AV_CH_TOP_BACK_LEFT 0x00008000 +#define AV_CH_TOP_BACK_CENTER 0x00010000 +#define AV_CH_TOP_BACK_RIGHT 0x00020000 +#define AV_CH_STEREO_LEFT 0x20000000 ///< Stereo downmix. +#define AV_CH_STEREO_RIGHT 0x40000000 ///< See AV_CH_STEREO_LEFT. +#define AV_CH_WIDE_LEFT 0x0000000080000000ULL +#define AV_CH_WIDE_RIGHT 0x0000000100000000ULL +#define AV_CH_SURROUND_DIRECT_LEFT 0x0000000200000000ULL +#define AV_CH_SURROUND_DIRECT_RIGHT 0x0000000400000000ULL +#define AV_CH_LOW_FREQUENCY_2 0x0000000800000000ULL + +/** Channel mask value used for AVCodecContext.request_channel_layout + to indicate that the user requests the channel order of the decoder output + to be the native codec channel order. */ +#define AV_CH_LAYOUT_NATIVE 0x8000000000000000ULL + +/** + * @} + * @defgroup channel_mask_c Audio channel layouts + * @{ + * */ +#define AV_CH_LAYOUT_MONO (AV_CH_FRONT_CENTER) +#define AV_CH_LAYOUT_STEREO (AV_CH_FRONT_LEFT|AV_CH_FRONT_RIGHT) +#define AV_CH_LAYOUT_2POINT1 (AV_CH_LAYOUT_STEREO|AV_CH_LOW_FREQUENCY) +#define AV_CH_LAYOUT_2_1 (AV_CH_LAYOUT_STEREO|AV_CH_BACK_CENTER) +#define AV_CH_LAYOUT_SURROUND (AV_CH_LAYOUT_STEREO|AV_CH_FRONT_CENTER) +#define AV_CH_LAYOUT_3POINT1 (AV_CH_LAYOUT_SURROUND|AV_CH_LOW_FREQUENCY) +#define AV_CH_LAYOUT_4POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_CENTER) +#define AV_CH_LAYOUT_4POINT1 (AV_CH_LAYOUT_4POINT0|AV_CH_LOW_FREQUENCY) +#define AV_CH_LAYOUT_2_2 (AV_CH_LAYOUT_STEREO|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT) +#define AV_CH_LAYOUT_QUAD (AV_CH_LAYOUT_STEREO|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT) +#define AV_CH_LAYOUT_5POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT) +#define AV_CH_LAYOUT_5POINT1 (AV_CH_LAYOUT_5POINT0|AV_CH_LOW_FREQUENCY) +#define AV_CH_LAYOUT_5POINT0_BACK (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT) +#define AV_CH_LAYOUT_5POINT1_BACK (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_LOW_FREQUENCY) +#define AV_CH_LAYOUT_6POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_CENTER) +#define AV_CH_LAYOUT_6POINT0_FRONT (AV_CH_LAYOUT_2_2|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER) +#define AV_CH_LAYOUT_HEXAGONAL (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_BACK_CENTER) +#define AV_CH_LAYOUT_6POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_CENTER) +#define AV_CH_LAYOUT_6POINT1_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_BACK_CENTER) +#define AV_CH_LAYOUT_6POINT1_FRONT (AV_CH_LAYOUT_6POINT0_FRONT|AV_CH_LOW_FREQUENCY) +#define AV_CH_LAYOUT_7POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT) +#define AV_CH_LAYOUT_7POINT0_FRONT (AV_CH_LAYOUT_5POINT0|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER) +#define AV_CH_LAYOUT_7POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT) +#define AV_CH_LAYOUT_7POINT1_WIDE (AV_CH_LAYOUT_5POINT1|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER) +#define AV_CH_LAYOUT_7POINT1_WIDE_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER) +#define AV_CH_LAYOUT_OCTAGONAL (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_CENTER|AV_CH_BACK_RIGHT) +#define AV_CH_LAYOUT_HEXADECAGONAL (AV_CH_LAYOUT_OCTAGONAL|AV_CH_WIDE_LEFT|AV_CH_WIDE_RIGHT|AV_CH_TOP_BACK_LEFT|AV_CH_TOP_BACK_RIGHT|AV_CH_TOP_BACK_CENTER|AV_CH_TOP_FRONT_CENTER|AV_CH_TOP_FRONT_LEFT|AV_CH_TOP_FRONT_RIGHT) +#define AV_CH_LAYOUT_STEREO_DOWNMIX (AV_CH_STEREO_LEFT|AV_CH_STEREO_RIGHT) +``` + +### 常用api +`int64_t av_get_default_channel_layout(int nb_channels)`: 这个函数可以根据通道的个数获得默认的 `channel_layout`; + +`int av_get_channel_layout_nb_channels(uint64_t channel_layout)`: 根据通道布局获得对应的通道数; + +`int av_get_channel_layout_channel_index(uint64_t channel_layout,uint64_t channel)`: 获得单通道在通道布局中的下标,注意 `channel` 必须是单通道的,比如获得 `AV_CH_BACK_CENTER` 在 `AV_CH_LAYOUT_4POINT0` 中的下标,可以得到结果为 3,根据这个下标就可以取到对应通道的数据了。 + +### 音频解码 +这里有个官方例子:[FFmpeg: decode_audio.c](http://ffmpeg.org/doxygen/trunk/decode_audio_8c-example.html) + +以下是跟音频相关的常用参数: + +```c +typedef struct AVCodecContext { + +/* audio only */ +int sample_rate; ///< samples per second +int channels; ///< number of audio channels + +/** +* audio sample format +* - encoding: Set by user. +* - decoding: Set by libavcodec. +*/ + enum AVSampleFormat sample_fmt; ///< sample format + + /* The following data should not be initialized. */ + /** + * Number of samples per channel in an audio frame. + * + * - encoding: set by libavcodec in avcodec_open2(). Each submitted frame + * except the last must contain exactly frame_size samples per channel. + * May be 0 when the codec has AV_CODEC_CAP_VARIABLE_FRAME_SIZE set, then the + * frame size is not restricted. + * - decoding: may be set by some decoders to indicate constant frame size + */ +int frame_size; + +/** + * Audio cutoff bandwidth (0 means "automatic") + * - encoding: Set by user. + * - decoding: unused + */ + int cutoff; + + /** + * Audio channel layout. + * - encoding: set by user. + * - decoding: set by user, may be overwritten by libavcodec. + */ + uint64_t channel_layout; + /** + * Request decoder to use this channel layout if it can (0 for default) + * - encoding: unused + * - decoding: Set by user. + */ + uint64_t request_channel_layout; + /** + * Type of service that the audio stream conveys. + * - encoding: Set by user. + * - decoding: Set by libavcodec. + */ + enum AVAudioServiceType audio_service_type; + + /** + * desired sample format + * - encoding: Not used. + * - decoding: Set by user. + * Decoder will decode to this format if it can. + */ + enum AVSampleFormat request_sample_fmt; +} +``` +其中 `frame_size` 的意思就是一个 `packet` 中的采样数,比如采样率是 `48000`, `frame_size = 1152`, 则表示每秒有 `48000` 次采样,而每个 `packet` 有 `1152` 次采样,因此一个 `packet` 的时间是 `1152/48000 * 1000 = 24毫秒`。`channel_layout` 是声道布局,表示多声道的个数和顺序,有了这个顺序才能顺利取到需要的数据。 + +### libswresample + +`libswresample` 主要是用于音频的重采样和格式转换的,包含如下功能: + +- 采样频率转换:对音频的采样频率进行转换的处理,例如把音频从一个高的 `44100Hz` 的采样频率转换到 `8000Hz`;从高采样频率到低采样频率的音频转换是一个有损的过程; +- 声道格式转换:对音频的声道格式进行转换的处理,例如立体声转换为单声道;当输入通道不能映射到输出流时,这个过程是有损的,因为它涉及不同的增益因素和混合; +- 采样格式转换:对音频的样本格式进行转换的处理,例如把 `s16(AV_SAMPLE_FMT_S16)` 的 PCM 数据转换为 `s8格式` 或者 `f32` 的 PCM 数据;此外提供了 `Packed` 和 `Planar` 包装格式之间相互转换的功能; + +当音频的采样率与播放器的采样率不一致时,那么想在播放器正常播放,就需要对音频进行重采样,否则可能会出现音频变速的问题。 + +### 音频转换 +音频转换一般就是指 `planar` 和 `packed` 的互转,或者声道之间的转换。当我们解码后的音频数据是 `planar` 的,而我们的播放器却只支持 `packed` 的,那么我们就需要将 `planar` 转为 `packed`,例如是双声道的话就是要将原本为 `LLLLRRRR` 的数据变为 `LRLRLRLR`,知道这个原理后,其实两个for循环就能搞定这次转换,如下: + +```c + data_size = av_get_bytes_per_sample(dec_ctx->sample_fmt); + for (i = 0; i < frame->nb_samples; i++) + for (ch = 0; ch < dec_ctx->channels; ch++) + fwrite(frame->data[ch] + data_size*i, 1, data_size, outfile); +``` +以上只是为了更好的理解转换过程,实际上ffmpeg已经提供了相关的接口来帮助我们转换,如非特别需求,建议还是使用ffmpeg提供的转换接口,在ffmpeg中,转换主要包含3个步骤: + +- 实例化 `SwrContext`; +- 计算转换后的 `sample` 个数; +- 调用 `swr_convert` 进行转换; + +转换的参考代码如下,主要的 api 都在 `libswresample/swresample.h`: + +```c +uint8_t **input; +int in_samples; + +//第一种方式创建SwrContext +//SwrContext *swr = swr_alloc(); +// av_opt_set_channel_layout(swr, "in_channel_layout", AV_CH_LAYOUT_5POINT1, 0); +// av_opt_set_channel_layout(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0); +// av_opt_set_int(swr, "in_sample_rate", 48000, 0); +// av_opt_set_int(swr, "out_sample_rate", 44100, 0); +// av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_FLTP, 0); +// av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_S32, 0); + +//第二种方式创建SwrContext,以下代码作用等同于上面的 +SwrContext *swr = swr_alloc_set_opts(NULL, // we're allocating a new context + AV_CH_LAYOUT_STEREO, // out_ch_layout + AV_SAMPLE_FMT_S32, // out_sample_fmt + 44100, // out_sample_rate + AV_CH_LAYOUT_5POINT1, // in_ch_layout + AV_SAMPLE_FMT_FLTP, // in_sample_fmt + 48000, // in_sample_rate + 0, // log_offset + NULL); // log_ctx + + +//在得到SwrContext后就要进行初始化 ,如果SwrContext的参数有任何变化,则必须再次调用以下初始化函数 +swr_init(swr); + +//这里演示修改了第三个参数为AV_SAMPLE_FMT_S16,则需要再次调用swr_init +swr = swr_alloc_set_opts(swr, + AV_CH_LAYOUT_STEREO, // out_ch_layout + AV_SAMPLE_FMT_S16, // out_sample_fmt + 44100, // out_sample_rate + AV_CH_LAYOUT_5POINT1, // in_ch_layout + AV_SAMPLE_FMT_FLTP, // in_sample_fmt + 48000, // in_sample_rate + 0, // log_offset + NULL); // log_ctx + +swr_init(swr);//再次调用 + +//计算转换后的采样数samples,计算公式为 in_samples*out_sample_rate=out_samples*in_sample_rate +//该运算在数学上等价于a * b / c,最后一个参数可以支持多种取舍 +int out_samples = av_rescale_rnd( + swr_get_delay(swr /*获取下一个输入样本相对于下一个输出样本将经历的延迟*/, 48000/*输入采样率 */) + in_samples, + 44100, //输出采样率 + 48000, //输入采样率 + AV_ROUND_UP);//表示向上取整,如3/2=2 + +//根据转换后的音频参数分配一块缓冲来存储数据 +uint8_t * output[8];//用于存储转换后的数据 +//分配一个样本缓冲区,并相应地填充数据指针和行大小 +//可以使用av_freep(&output [0])释放分配的样本缓冲区 + av_samples_alloc(&output, //[out] + NULL, //[out] + 2, //通道数 + out_samples, //采样数 + AV_SAMPLE_FMT_S16, //采样格式 + 0); //对齐,0--默认,1--不对齐 + + out_samples = swr_convert(swr, + &output, //转换后的数据 + out_samples, + input, //要转换的数据 + in_samples); + +if(swr_get_out_samples(swr,0)>0){//表示有缓冲数据 +//通过设置in和in_count为0将缓存中的全部处理完毕,这通常是最后一步,如果没有这步,则可能最后的音频数据会存在缓冲中没有全部转换出来 + out_samples = swr_convert(swr, + &output, + out_samples, + NULL, + 0); +} + +swr_free(&swr);//最后释放 + +``` +- swr_convert: 在转换过程中如果输入采样数大于输出采样数,那么超出的部分会被Swresample缓存起来,因此输出采样数这个参数要根据输入采样数和已经存在的缓存进行计算,否则可能会导致缓存的采样数越来越多,内存一直在上涨;当in和in_count都为0时,就表示要把缓存中数据都输出出来了; +- swr_get_out_samples: 函数的意思是获得下一个输出样本缓冲的数量,相同的输入返回值并不是一样的,这取决于内部的缓存采样数的多少;我们知道,swr_convert在调用后,如果输入的采样数比输出的采样数大,那么Swresample便会对超出的那部分进行缓存,如果输入一直比输出大,那么内存就会一直上涨,为此,我们需要swr_get_out_samples这个函数让我们得知输出应该为多大才能把缓冲里的数据也带走,比如Swresample里已经有10个采样数的缓存了,此时输入如果为100个采样数,那么我们希望输出为110(这是在不改变采样率的情况下),这样就能把所有数据都输出,缓存也清空了,就不会引起内存上涨,那么怎么得到这个110呢,通过swr_get_out_samples(swr,100)=110;如果swr_get_out_samples(swr,0)就表示获得Swresample已经缓存的采样数;最后总结一下,swr_get_out_samples就是根据你的输入采样数,得到应该取走的输出采样数,如果输入采样数为0,那么就能得到已经缓存的采样数,得到这个输出采样数后,我们才知道应该通过av_samples_alloc为输出样本分配多大的缓存空间; +- swr_get_delay: FFMPEG 转码的过程中 , 可能没有一次性将一帧数据处理完毕 , 如输入了 20 个数据 , 一般情况下 20 个数据都能处理完毕 , 有时还会出现只处理了 19 个 , 剩余的 1 个数据就积压在了缓冲区中的情况 , 如果这种积压在缓冲区中的数据过大 , 会造成很大的音频延迟 , 甚至内存崩溃;所以每次音频处理时 , 都尝试将上一次积压的音频采样数据加入到本次处理的数据中 , 防止出现音频延迟的情况; + + +当然可以参考官方的 [Demo](http://ffmpeg.org/doxygen/trunk/resampling_audio_8c-example.html) + +### 相关计算 + +`AVFrame.nb_samples`( `AVFrame.nb_samples` 和 `AVCodecContext.frame_size` 实际上是同一个东西,他们相等)表示的是每帧每个通道的采样数,`aac` 一般是 `1024(LC)`或 `2048(HE)`,`mp3` 一般是 `1152`。 + +A HE-AAC v1 or v2 audio frame contains 2048 PCM samples per channel +(there is also one mode with 1920 samples per channel but this is +only for special purposes such as DAB+ digital radio). These +bits/frame figures are average figures where each AAC frame generally +has a different size in bytes. To calculate the same for AAC-LC just +use 1024 instead of 2048 PCM samples per frame and channel. For +AAC-LD/ELD it is either 480 or 512 PCM samples per frame and channel. +{:.info} + +我们知道从解码器解码出一帧,如果是视频的话,那这一帧通常就是一个画面的数据,而对于音频来说,这一帧大概只包含几十毫秒长的音频数据,那么怎么知道这一帧究竟是表示多长的音频呢? + +首先我们要清楚 `AVFrame.nb_samples` 等于 `AVCodecContext.farme_size`,它表示的意思就是每帧每个通道的采样数,有了它,我们就可以计算出一帧包含的音频时长,因为音频都有一个采样率,它表示一秒采样多少次,现在我们知道了采样数,那么一除就可以知道时长了,如下: + +一帧时长(ms) = 一帧采样数 / 采样率 * 1000 +{:.info} + +在 ffmpeg 中一帧采样数就是 `AVFrame.nb_samples` 或者 `AVCodecContext.farme_size`,而采样率就是 `AVCodecContext.sample_rate`,比如采样率是 `48000`,而`frame_size=1152`,那么就可以得出这一帧的音频时长为`1152/48000*1000 = 24毫秒`。 + +此时你已经知道了一帧的音频时长,假如你还想知道这一帧的数据大小那该怎么计算呢? + +我们知道,每次采样后都需要保存这个采样的数据,那么用多大来保存它呢,这就看 `AVSampleFormat` 了,比如 `AV_SAMPLE_FMT_S16` 就表示用 16 位即 2 个字节来保存一个采样数据,当然位数越多,能保存的数据就会更多,音质肯定就会越好。 + +我们知道 `AV_SAMPLE_FMT_S16` 是占 2 个字节,`AV_SAMPLE_FMT_S32` 是占 4 个字节,在 ffmpeg 中提供了一个函数 `av_get_bytes_per_sample()`,它可以帮我们计算出 `AVSampleFormat` 究竟是几个字节,比如: + +- av_get_bytes_per_sample(AV_SAMPLE_FMT_S16) = 2 +- av_get_bytes_per_sample(AV_SAMPLEAV_SAMPLE_FMT_S32) = 4 + +有了上面的分析,我们就可以得出: `一帧的数据大小 = 采样数 * 存储采样的位数 * 声道数` + +这一帧的数据都保存在 `AVFrame.data` 里,假如是双声道,`planar` 格式,那么我们想取出左声道的数据的话,那得先知道左声道的数据放在 `AVFrame.data` 这个数组的哪个下标,在ffmpeg中 `av_get_channel_layout_channel_index()` 这个函数就能帮助我们得到下标,然后我们就可以愉快取出数据了。 + +根据上面的分析,我们还可以轻松计算出 1 秒能包含多少帧音频,假如我们知道了采样率,采样格式,一帧的采样数,那么我们就可以计算出1秒包含的音频帧数。比如采样率是 `88200`,采样格式是 `AV_SAMPLE_FMT_S16` (这么写太长了,这里用s16代替下),一帧的采样数是 `1024`,那么一帧数据量: + +1024 * 2 * av_get_bytes_per_sample(s16) = 4096 个字节 +{:.info} + +一秒可以编码的帧数: + +88200/(1024 * 2 * av_get_bytes_per_sample(s16)) = 21.5 帧数据 +{:.info} + +### 参考 + +[计算音频延迟](https://blog.csdn.net/shulianghan/article/details/104871223/?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-4--blog-83818443.pc_relevant_default&spm=1001.2101.3001.4242.3&utm_relevant_index=7) +[FFMpeg 音频重采样 Demo](http://ffmpeg.org/doxygen/trunk/resampling_audio_8c-example.html) +[ffmpeg音频存储格式packed和planar](https://blog.csdn.net/qq_18998145/article/details/97394595) +[FFmpeg关于nb_smples,frame_size以及profile的解释](https://blog.csdn.net/eydwyz/article/details/78748241) \ No newline at end of file diff --git a/_posts/2022-08-27-02-audio-base.md b/_posts/2022-08-27-02-audio-base.md new file mode 100644 index 00000000000..58ad62bd654 --- /dev/null +++ b/_posts/2022-08-27-02-audio-base.md @@ -0,0 +1,102 @@ +--- +title: 音频基础知识 +tags: FFMpeg +--- +## 前言 +此篇为总结音频的各种基础知识,不定期更新。另外此篇为网上各大博客总结的合集,参考链接放在了后面。 +## 音频基础知识 +### 声音是什么 +声音(sound)是由物体振动产生的声波。声音作为一种机械波,频率在20 Hz~20 kHz之间的声音是可以被人耳识别的。-----[百度百科](https://baike.baidu.com/item/%E5%A3%B0%E9%9F%B3/33686?fr=aladdin) + +### 音频录制 +最简单的音频录制流程为: + +```mermaid +graph LR; + A[设备采集] + B[模数转换] + C[存储] + + A-->B + B-->C +``` + +播放端流程相反: + +```mermaid +graph LR; + A[音频文件] + B[数模转换] + C[播放器播放] + + A-->B + B-->C +``` + +### 模数转换 +模拟信号转化为数字信号的流程 + +![Image]({{ site.baseurl }}/assets/images/ffmpeg/baseaudio/audiobase.jpeg) + +- 采样: 对模拟信号隔一定的时间间隔取一个点(图中交点) +- 量化: 给纵坐标加刻度,根据近似取整数值,使采样得到的点的值都是整数。 +- 编码:对量化取得的整数值按二进制进行编码 +- 数字信号:把编码得到的 0 和 1 的序列变现为高低电平的信号 + +上述整个模数转换的过程称为:脉冲编码调制(PulseCodeModulation),简称PCM。由上面的模数转换可知,PCM 格式文件存储的内容实际上就是编码得到的序列。 + +#### 采样率 +每秒的采样次数。 + +对于声音来说,从上图采样的过程可以看的出来,采样率越高,即每秒内的采样点越多,采样点越密集,也就意味着离散信号对模拟信号的还原度越高。 + +采样点无限多即连成原来的模拟信号曲线。当然,无限多的点无限多的数据存储和 cpu 也不允许啊。那什么样的采样率比较合适呢? + +根据 “奈奎斯特采样理论”:当对被采样的模拟信号进行还原时,其最高频率只有采样频率的一半。 +{:.info} + +换句话说:`要想重构完整的模拟信号,采样率要是模拟信号频率的两倍以上。`{:.info} + +鉴于所讲,人听觉范围:20 Hz -20 KHz。所以对于给人听的音频,采样率要宁杀错不放过,要大于40 KHz 才不会使人感觉到音质受损。 + +由此得出,**合适的采样率 一般大于 40 KHz,注意是一般,采样率小一些高音会受损,不过除了对那些能听出热情火电和清冽水电的耳机党外,略小应该也不明显,平常 8k,16k 都有。** + +为什么大多数音频采样率采用 44.1K?不是 40K,也不是 44K,非要带个 0.1? +{:.error} + +在高深莫测的数学领域有一个神奇的数字即 `44100`,前四个素数的平方的乘积 + +**44100:2x2x3x3x5x5x7x7 = 44100** + +历史原因:**早期录像机:245 扫描线,一行 3 个数据块,60 场频:245x3x60=44100,现在也有在推 48kHz,甚至 96kHz 的。** + +#### 采样深度 + +即量化时纵坐标的量化粒度。 + +在上图模拟信号中,纵坐标代表的是声音的响度,即音量。 + +由上图量化过程可知,当量化的粒度越细,离散点取的值也就越接近模拟信号实际的值,如果量化时,采样深度为 1bit,即量化只能为 1 和 0,那每首歌听起来都像忐忑,一惊一乍的,要么播放最大声音,要么没有声音。 + +**只有采样深度越深,即量化的粒度越细,采样出来的数据,才更接近实际的音量。** + +现在一般用 16 位来表示一个采样数据的音量大小,即一个采集的样本点中,音量大小用 16 位二进制数来表示音量大小。 + +#### 声道和立体声 + +- 单声道 Monaural:只有一个采集器采集音频做以上图中的流程处理; +- 立体音 Stereophonic:两个或两个以上声道数,多个设备录音,然后数据编码整合到一起。常见的有 双声道 4.1 环绕立体声(四个音源加一个低音喇叭来加强低音)5.1 、7.1 环绕立体声(杜比音效:装逼知识点); + +#### PCM 数据 + +![Image]({{ site.baseurl }}/assets/images/ffmpeg/baseaudio/audiobase-pcm.jpeg) + +你没有看错,就是这么简单。对于 8 位采样深度的单声道,8 位表示一个采样的音量,都是这样的采样数据。 + +pcm 就是原始数据,没有 header,全是 body,满满的干货。双声道就是两个声道采样序列交替排列。 + +播放 PCM:由上面格式也可以看出 pcm 没有 header,没有采样率等信息,所以播放时需要告诉播放器:采样率、采样深度、声道数。 + +## 参考 + +[音频相关的基础知识](https://blog.csdn.net/weixin_39864591/article/details/110812277) \ No newline at end of file diff --git a/_posts/2022-08-27-03-player01.md b/_posts/2022-08-27-03-player01.md new file mode 100644 index 00000000000..c4d453f4839 --- /dev/null +++ b/_posts/2022-08-27-03-player01.md @@ -0,0 +1,622 @@ +--- +title: 写一个播放器之一 +tags: FFMpeg +Mermaid: true +--- + +## 前言 +年初进入一家音视频的公司,而在这方面感觉太菜,于是决定学习相关的知识。从零实现一个播放器我觉得是一个入门的好办法,于是参考了这个 [ffmpeg-video-player](https://github.com/rambodrahmani/ffmpeg-video-player),跟着其中的 `tutorial` 从简单到复杂。 + +完成这个 `tutorial` 系列,再去深入研究 `FFMpeg` 的 `ffplay` 播放器就相对简单许多(当然 `ijkplayer` 也不在话下(:逃))。 + +## 开始 +首先研究 [tutorial03](https://github.com/rambodrahmani/ffmpeg-video-player/tree/master/tutorial03)。 +### FFMpeg 的一些初始化 + +#### 1、创建 AVFormatContext + +```c +AVFormatContext * pFormatCtx = NULL; +ret = avformat_open_input(&pFormatCtx, argv[1], NULL, NULL); +if (ret < 0) +{ + printf("Could not open file %s.\n", argv[1]); + return -1; +} + +// read packets of a media file to get stream information +ret = avformat_find_stream_info(pFormatCtx, NULL); +if (ret < 0) +{ + printf("Could not find stream information %s.\n", argv[1]); + return -1; +} + +// print detailed information about the input or output format +av_dump_format(pFormatCtx, 0, argv[1], 0); +``` +主要是读取音视频文件(`I/O context`),然后打印音视频文件的一些信息 + +#### 2、找到音频或者视频的索引 + +```c +int videoStream = -1; +int audioStream = -1; + +// loop through the streams that have been found +for (int i = 0; i < pFormatCtx->nb_streams; i++) +{ + // look for video stream + if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && videoStream < 0) + { + videoStream = i; + } + + // look for audio stream + if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && audioStream < 0) + { + audioStream = i; + } +} + +// return with error in case no video stream was found +if (videoStream == -1) +{ + printf("Could not find video stream.\n"); + return -1; +} + +// return with error in case no audio stream was found +if (audioStream == -1) +{ + printf("Could not find audio stream.\n"); + return -1; +} +``` +#### 3、生成音频解码上下文 + + +首先找到音频解码器 + +```c +// 找到一个音频解码器 +const AVCodec * aCodec = NULL; +aCodec = avcodec_find_decoder(pFormatCtx->streams[audioStream]->codecpar->codec_id); +if (aCodec == NULL) +{ + printf("Unsupported codec!\n"); + return -1; +} +``` + +生成音频解码上下文 `context` + +```c +// 生成音频解码上下文 +AVCodecContext * aCodecCtx = NULL; +aCodecCtx = avcodec_alloc_context3(aCodec); +ret = avcodec_parameters_to_context(aCodecCtx, pFormatCtx->streams[audioStream]->codecpar); +if (ret != 0) +{ + printf("Could not copy codec context.\n"); + return -1; +} +``` +#### 4、配置 SDL Audio 音频参数 + +```c + // audio specs containers + SDL_AudioSpec wanted_specs; + SDL_AudioSpec specs; + + // set audio settings from codec info + wanted_specs.freq = aCodecCtx->sample_rate; // 采样率 + wanted_specs.format = AUDIO_S16SYS; // 音频格式 + wanted_specs.channels = aCodecCtx->channels; // 声道 + wanted_specs.silence = 0; // 静音 + wanted_specs.samples = SDL_AUDIO_BUFFER_SIZE; // 一帧里面有多少个样本 + wanted_specs.callback = audio_callback; // 音频数据回调 + wanted_specs.userdata = aCodecCtx; // 传递的参数 + + // Uint32 audio device id + SDL_AudioDeviceID audioDeviceID; + + // open audio device + audioDeviceID = SDL_OpenAudioDevice( // [1] + NULL, + 0, + &wanted_specs, + &specs, + SDL_AUDIO_ALLOW_FORMAT_CHANGE + ); + + // SDL_OpenAudioDevice returns a valid device ID that is > 0 on success or 0 on failure + if (audioDeviceID == 0) + { + printf("Failed to open audio device: %s.\n", SDL_GetError()); + return -1; + } +``` +这里面的几个概念:`采样率`、`音频格式`、`声道`、`样本(samples)` 要了解清楚。 + +#### 5、打开音频上下文(重要) + +```c +// initialize the audio AVCodecContext to use the given audio AVCodec +ret = avcodec_open2(aCodecCtx, aCodec, NULL); +if (ret < 0) +{ + printf("Could not open audio codec.\n"); + return -1; +} +``` +#### 6、视频解码器以及上下文 + +这个和音频的处理一样 + +```c +// retrieve video codec +const AVCodec * pCodec = NULL; +pCodec = avcodec_find_decoder(pFormatCtx->streams[videoStream]->codecpar->codec_id); +if (pCodec == NULL) +{ + printf("Unsupported codec!\n"); + return -1; +} + +// retrieve video codec context +AVCodecContext * pCodecCtx = NULL; +pCodecCtx = avcodec_alloc_context3(pCodec); +ret = avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoStream]->codecpar); +if (ret != 0) +{ + printf("Could not copy codec context.\n"); + return -1; +} + +// initialize the video AVCodecContext to use the given video AVCodec +ret = avcodec_open2(pCodecCtx, pCodec, NULL); +if (ret < 0) +{ + printf("Could not open codec.\n"); + return -1; +} +``` +### 视频处理部分 +我们使用 `av_read_frame` 函数不断的读取每一个 `AVPacket` 包(这是还没解码的数据),简单来说就是下面这样: + +```c +while (av_read_frame(pFormatCtx, pPacket) >= 0) { + // video stream found + if (pPacket->stream_index == videoStream) + { + // do something + } + else if (pPacket->stream_index == audioStream) + { + // audio stream found + // do something + } +} +``` +#### 解码 +向解码器发送一个 `AVPacket` 包,然后获取解码后的视频帧 + +```c +// 向解码器发送一个 AVPacket 包 +ret = avcodec_send_packet(pCodecCtx, pPacket); +if (ret < 0) +{ + printf("Error sending packet for decoding.\n"); + return -1; +} + +while (ret >= 0) +{ + // 获取解码后的视频帧 + ret = avcodec_receive_frame(pCodecCtx, pFrame); + + /* + 0: 将会成功返回视频解码帧 + AVERROR(EAGAIN): 现在无法获取解码后的视频帧,需要再送一个 Packet 包 + AVERROR_EOF: 已经到文件末尾了 + 其它:解码错误 + */ + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) // [3] + { + break; + } + else if (ret < 0) + { + printf("Error while decoding.\n"); + return -1; + } + // 后续的处理 ... +} +``` +#### 视频帧的缩放 +`SwsContext` 的初始化 + +```c +struct SwsContext * sws_ctx = NULL; +sws_ctx = sws_getContext( + pCodecCtx->width, // 源图像的宽 + pCodecCtx->height, // 源图像的高 + pCodecCtx->pix_fmt, // 源图像的像素格式 + pCodecCtx->width, // 目标图像的宽 + pCodecCtx->height, // 目标图像的高 + AV_PIX_FMT_YUV420P, // 目标图像的像素格式 + SWS_BILINEAR, // 选择缩放算法(只有当输入输出图像大小不同时有效),一般选择SWS_FAST_BILINEAR + NULL, // 输入图像的滤波器信息, 若不需要传NULL + NULL, // 输出图像的滤波器信息, 若不需要传NULL + NULL // 特定缩放算法需要的参数(?),默认为NULL +); +``` +这里有几篇文章可以帮助理解它使用: + +[sws_getContext() 使用](https://blog.csdn.net/davidsguo008/article/details/72537832) + +[FFmpeg源代码简单分析:libswscale的sws_getContext()](https://blog.csdn.net/leixiaohua1020/article/details/44305697?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EESLANDING%7Edefault-2-44305697-blog-72537832.pc_relevant_multi_platform_whitelistv4eslandingctr2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EESLANDING%7Edefault-2-44305697-blog-72537832.pc_relevant_multi_platform_whitelistv4eslandingctr2&utm_relevant_index=3) + +[sws_getContext函数参数介绍](https://www.cnblogs.com/nanqiang/p/10116518.html) + +使用 `sws_scale`: + +```c +sws_scale( + sws_ctx, + (uint8_t const * const *)pFrame->data, + pFrame->linesize, + 0, + pCodecCtx->height, + pict->data, + pict->linesize +); + +/* +1、参数 SwsContext *c, 转换格式的上下文。也就是 sws_getContext 函数返回的结果。 +2、参数 const uint8_t *const srcSlice[], 输入图像的每个颜色通道的数据指针。其实就是解码后的AVFrame中的data[]数组。因为不同像素的存储格式不同,所以srcSlice[]维数 +也有可能不同。 + +以YUV420P为例,它是planar格式,它的内存中的排布如下: +YYYYYYYY UUUU VVVV +使用FFmpeg解码后存储在AVFrame的data[]数组中时: +data[0]——-Y分量, Y1, Y2, Y3, Y4, Y5, Y6, Y7, Y8…… +data[1]——-U分量, U1, U2, U3, U4…… +data[2]——-V分量, V1, V2, V3, V4…… +linesize[]数组中保存的是对应通道的数据宽度 , +linesize[0]——-Y分量的宽度 +linesize[1]——-U分量的宽度 +linesize[2]——-V分量的宽度 + +而RGB24,它是packed格式,它在data[]数组中则只有一维,它在存储方式如下: +data[0]: R1, G1, B1, R2, G2, B2, R3, G3, B3, R4, G4, B4…… +这里要特别注意,linesize[0]的值并不一定等于图片的宽度,有时候为了对齐各解码器的CPU,实际尺寸会大于图片的宽度,这点在我们编程时(比如OpengGL硬件转换/渲染)要特别注意,否则解码出来的图像会异常。 + +3、参数const int srcStride[],输入图像的每个颜色通道的跨度。也就是每个通道的行字节数,对应的是解码后的AVFrame中的linesize[]数组。根据它可以确立下一行的起始位置,不过stride和width不一定相同,这是因为: +a.由于数据帧存储的对齐,有可能会向每行后面增加一些填充字节这样 stride = width + N; +b.packet色彩空间下,每个像素几个通道数据混合在一起,例如RGB24,每个像素3字节连续存放,因此下一行的位置需要跳过3*width字节。 + +4、参数int srcSliceY, int srcSliceH,定义在输入图像上处理区域,srcSliceY是起始位置,srcSliceH是处理多少行。如果srcSliceY=0,srcSliceH=height,表示一次性处理完整个图像。这种设置是为了多线程并行,例如可以创建两个线程,第一个线程处理 [0, h/2-1]行,第二个线程处理 [h/2, h-1]行。并行处理加快速度。 + +5、参数uint8_t *const dst[], const int dstStride[]定义输出图像信息(输出的每个颜色通道数据指针,每个颜色通道行字节数) +*/ +``` + +[ffmepg中sws_scale() 函数解析](https://blog.csdn.net/yaojinjian1995/article/details/118105883) + +#### SDL 渲染 +这个比较简单: + +```c +// the area of the texture to be updated +SDL_Rect rect; +rect.x = 0; +rect.y = 0; +rect.w = pCodecCtx->width; +rect.h = pCodecCtx->height; + +// update the texture with the new pixel data +SDL_UpdateYUVTexture( + texture, + &rect, + pict->data[0], + pict->linesize[0], + pict->data[1], + pict->linesize[1], + pict->data[2], + pict->linesize[2] +); + +// clear the current rendering target with the drawing color +SDL_RenderClear(renderer); + +// copy a portion of the texture to the current rendering target +SDL_RenderCopy(renderer, texture, NULL, NULL); + +// update the screen with any rendering performed since the previous call +SDL_RenderPresent(renderer); + +``` + +### 音频处理部分 +由于音频播放是由系统回调,所以在获取到音频数据包 `AVPacket` 之后需要立即存储起来以便播放时使用。所以需要一个队列把这些音频包暂时存储起来。 + +关于队列部分,这里先略过去。 + +#### 音频解码 +我们之前注册的音频回调函数 `audio_callback`,音频解码部分在这里处理: + +```c +static void audio_callback(void * userdata, Uint8 * stream, int len) { + +} +``` +我们从音频队列中获取数据包之后,就送到音频解码器去解码,这点和视频的解码是一样的: + +```c +for (;;) +{ + while (audio_pkt_size > 0) + { + int got_frame = 0; + int ret = avcodec_receive_frame(aCodecCtx, avFrame); + if (ret == 0) + { + got_frame = 1; + } + + if (ret == AVERROR(EAGAIN)) + { + ret = 0; + } + + if (ret == 0) { + ret = avcodec_send_packet(aCodecCtx, avPacket); + } + + if (ret == AVERROR(EAGAIN)) + { + ret = 0; + } + else if (ret < 0) + { + printf("avcodec_receive_frame error"); + return -1; + } + else + { + len1 = avPacket->size; + } + + if (len1 < 0) + { + // if error, skip frame + audio_pkt_size = 0; + break; + } + + audio_pkt_data += len1; + audio_pkt_size -= len1; + data_size = 0; + + if (got_frame) + { + // audio resampling + // do something + } + if (data_size <= 0) + { + // no data yet, get more frames + continue; + } + // we have the data, return it and come back for more later + return data_size; + } + + if (avPacket->data) + { + // wipe the packet + av_packet_unref(avPacket); + } + + // get more audio AVPacket + int ret = packet_queue_get(&audioq, avPacket, 1); + + // if packet_queue_get returns < 0, the global quit flag was set + if (ret < 0) + { + return -1; + } + + audio_pkt_data = avPacket->data; + audio_pkt_size = avPacket->size; +} +``` + +#### 重采样 +什么是重采样? + +通俗的讲,重采样就是改变音频的采样率、sample format(采样格式)、声道数(channel)等参数,使之按照我们期望的参数输出。 +{:.info} + +为什么要重采样 + +因为当原有的音频参数不满足我们实际要求时,比如说在FFmpeg解码音频的时候,不同的音源有不同的格式和采样率等,所以在解码后的数据中的这些参数也会不一致(最新的FFmpeg解码音频后,音频格式为AV_SAMPLE_FMT_TLTP);如果我们接下来需要使用解码后的音频数据做其它操作的话,然而这些参数的不一致会导致有很多额外工作,此时直接对其进行重采样的话,获取我们制定的音频参数,就会方便很多。 +再比如说,在将音频进行SDL播放的时候,因为当前的SDL2.0不支持plannar格式,也不支持浮点型的,而最新的FFpemg会将音频解码为AV_SAMPLE_FMT_FLTP,这个时候进行对它重采样的话,就可以在SDL2.0上进行播放这个音频了。 +{:.info} + +我们使用 `SwrContext` 进行重采样并设置相应的参数 + +```c + SwrContext * swr_ctx = swr_alloc(); + // 原音频采样的声道数 + av_opt_set_int( // 3 + swr_ctx, + "in_channel_layout", + in_channel_layout, + 0 + ); + + // 原音频的采样率 + av_opt_set_int( + swr_ctx, + "in_sample_rate", + audio_decode_ctx->sample_rate, + 0 + ); + + // 原音频的采样格式 + av_opt_set_sample_fmt( + swr_ctx, + "in_sample_fmt", + audio_decode_ctx->sample_fmt, + 0 + ); + + // 重采样音频的声道数 + av_opt_set_int( + swr_ctx, + "out_channel_layout", + out_channel_layout, + 0 + ); + + // 重采样音频的采样率 + av_opt_set_int( + swr_ctx, + "out_sample_rate", + out_sample_rate, + 0 + ); + + // 重采样音频的输出格式 + av_opt_set_sample_fmt( + swr_ctx, + "out_sample_fmt", + out_sample_fmt, + 0 + ); + // 初始化 + ret = swr_init(swr_ctx); +``` + +分配重采样的音频的空间: + +```c + int out_linesize = 0; + uint8_t ** resampled_data = NULL; + + ret = av_samples_alloc_array_and_samples( + &resampled_data, /// 重采样分配内存空间 + &out_linesize, /// 采样个数的最大大小字节空间 + out_nb_channels, + out_nb_samples, + out_sample_fmt, + 0 + ); +``` +但是这里要注意 `音频延迟` 的问题 + +转码的过程中 , 输入 10 个数据 , 并不一定都能处理完毕并输出 10 个数据 , 可能处理输出了 8 个数据 +还剩余 2 个数据没有处理。那么在下一次处理的时候 , 需要将上次没有处理完的两个数据处理了 ; +如果不处理上次的2个数据 , 那么数据会一直积压 , 如果积压数据过多 , 最终造成很大的延迟 , 甚至崩溃 +因此每次处理的时候 , 都要尝试将上次剩余没有处理的数据加入到本次处理的数据中。如果计算出的 delay 一直等于 0 , 说明没有积压数据 +{:.info} + +```c +// 计算音频延迟 +int64_t delay = swr_get_delay(swrContext , avFrame->sample_rate); + +/* + 将 a 个数据 , 由 c 采样率转换成 b 采样率后 , 返回多少数据 + int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding rnd) av_const; + 下面的方法时将 avFrame->nb_samples 个数据 , 由 avFrame->sample_rate 采样率转为 44100 采样率 + 返回的数据个数 + AV_ROUND_UP : 向上取整 + */ + +// 计算输出样本个数 +int64_t out_count = av_rescale_rnd( + avFrame->nb_samples + delay, //本次要处理的数据个数 + 44100, + avFrame->sample_rate , + AV_ROUND_UP ); +``` + + + +音频重采样: + +```c +/* + int swr_convert( + struct SwrContext *s, //上下文 + uint8_t **out, //输出的缓冲区 ( 需要计算 ) + int out_count, //输出的缓冲区最大可接受的样本个数 ( 需要计算 ) + const uint8_t **in , //输入的数据 + int in_count); //输入的样本个数 +返回值 : 转换后的采样个数 , 是样本个数 , 每个样本是 16 位 , 两个字节 ; + samples_out_count 是每个通道的样本数 , samples_out_count * 2 是立体声双声道样本个数 + samples_out_count * 2 * 2 是字节个数 + */ + +ret = swr_convert( + swr_ctx, + resampled_data, + out_nb_samples, + (const uint8_t **) decoded_audio_frame->data, + decoded_audio_frame->nb_samples + ); + +// 计算输出样本的字节数 +resampled_data_size = av_samples_get_buffer_size( + &out_linesize, + out_nb_channels, + ret, + out_sample_fmt, + 1 + ); +``` +## 流程图 + +```mermaid +graph LR; + F[音视频文件] + P[AVPacket] + VP[VideoPacket] + VD[VideoDecord] + SC[Scale] + SDL[SDL Display] + AP[AudioPacket] + AQ[音频队列] + ASC[SDL Audio Call Back] + GAP[处理音频包] + AD[AudioDecord] + RDS[重采样] + APlay[播放] + + F--read-->P + P-->VP + VP-->VD + VD-->SC + SC-->SDL + + P-->AP + AP--store-->AQ + ASC-->GAP + + AQ-.获取包.-GAP + GAP-->AD + AD-->RDS + + RDS-->APlay +``` + + + +## 参考 + +[ffmpeg-video-player](https://github.com/rambodrahmani/ffmpeg-video-player) + +[音频延迟](https://blog.csdn.net/shulianghan/article/details/104871223/?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-4--blog-83818443.pc_relevant_default&spm=1001.2101.3001.4242.3&utm_relevant_index=7) \ No newline at end of file diff --git a/assets/images/ffmpeg/baseaudio/audiobase-pcm.jpeg b/assets/images/ffmpeg/baseaudio/audiobase-pcm.jpeg new file mode 100644 index 00000000000..37c43c25dcb Binary files /dev/null and b/assets/images/ffmpeg/baseaudio/audiobase-pcm.jpeg differ diff --git a/assets/images/ffmpeg/baseaudio/audiobase.jpeg b/assets/images/ffmpeg/baseaudio/audiobase.jpeg new file mode 100644 index 00000000000..e98f9731094 Binary files /dev/null and b/assets/images/ffmpeg/baseaudio/audiobase.jpeg differ