双卡座录音左右声道不一致音频录制后出现错位是什么原因?

目录参考lswr功能介绍lswr使用说明示例代码 1. 参考[1] FFmpeg/Libswresample Documentation [2] FFmpeg/Libswresample Detailed Description [3] FFmpeg/doc/examples/resampling_audio.c
2. lswr功能介绍FFmpeg中重采样的功能由libswresample(后面简写为lswr)提供。lswr提供了高度优化的转换音频的采样频率、声道格式或样本格式的功能。功能说明:采样频率转换:对音频的采样频率进行转换的处理,例如把音频从一个高的44100Hz的采样频率转换到8000Hz。从高采样频率到低采样频率的音频转换是一个有损的过程。API提供了多种的重采样选项和算法。声道格式转换:对音频的声道格式进行转换的处理,例如立体声转换为单声道。当输入通道不能映射到输出流时,这个过程是有损的,因为它涉及不同的增益因素和混合。样本格式转换:对音频的样本格式进行转换的处理,例如把s16的PCM数据转换为s8格式或者f32的PCM数据。此外提供了Packed和Planar包装格式之间相互转换的功能,Packed和Planar的区别见FFmpeg中Packed和Planar的PCM数据区别。 此外,还提供了一些其他音频转换的功能如拉伸和填充,通过专门的设置来启用。3. lswr使用说明重采样的处理流程:创建上下文环境:重采样过程上下文环境为SwrContext数据结构。参数设置:转换的参数设置到SwrContext中。SwrContext初始化:swr_init()。分配样本数据内存空间:使用av_samples_alloc_array_and_samples、av_samples_alloc等工具函数。开启重采样转换:通过重复地调用swr_convert来完成。重采样转换完成, 释放相关资源:通过swr_free()释放SwrContext。 下面是示例程序的一个流程图:函数说明:swr_alloc() :创建SwrContext对象。av_opt_set_*():设置输入和输出音频的信息。swr_init(): 初始化SwrContext。av_samples_alloc_array_and_samples:根据音频格式分配相应大小的内存空间。av_samples_alloc:根据音频格式分配相应大小的内存空间。用于转换过程中对输出内存大小进行调整。swr_convert:进行重采样转换。 3.1 创建上下文环境重采样过程上下文环境为SwrContext数据结构(SwrContext的定义没有对外暴露)。创建SwrContext的方式有两种:swr_alloc() : 创建SwrContext之后再通过AVOptions的API来设置参数。swr_alloc_set_opts():在创建SwrContext的同时设置必要的参数。 两个函数的定义如下:struct SwrContext* swr_alloc()
struct SwrContext* swr_alloc_set_opts(struct SwrContext *
s, //如果为NULL则创建一个新的SwrContext,否则对已有的SwrContext进行参数设置
int64_t
out_ch_layout, //输出的声道格式,AV_CH_LAYOUT_*
enum AVSampleFormat
out_sample_fmt,
int
out_sample_rate,
int64_t
in_ch_layout,
enum AVSampleFormat
in_sample_fmt,
int
in_sample_rate,
int
log_offset,
void *
log_ctx
)
3.2 参数设置参数设置的方式有两种:AVOptions的APIswr_alloc_set_opts():如果第一个参数为NULL则创建一个新的SwrContext,否则对已有的SwrContext进行参数设置。【相关学习资料推荐,点击下方链接免费报名,先码住不迷路~】音视频免费学习地址:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发【免费分享】音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击788280672加群免费领取~假定要进行如下的重采样转换:“f32le格式、采样频率48kHz、5.1声道格式”的PCM数据
转换为
“s16le格式、采样频率44.1kHz、立体声格式”的PCM数据swr_alloc()的使用方式如下所示:SwrContext *swr = swr_alloc();
av_opt_set_channel_layout(swr, "in_channel_layout", AV_CH_LAYOUT_5POINT1, 0);
av_opt_set_channel_layou(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_FLPT, 0);
av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);swr_alloc_set_opts()的使用方式如下所示:SwrContext *swr = swr_alloc_set_opts(NULL,
// we're allocating a new context
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_ctx3.3 SwrContext初始化:swr_init()参数设置好之后必须调用swr_init()对SwrContext进行初始化。如果需要修改转换的参数:重新进行参数设置。再次调用swr_init()。 3.4 分配样本数据内存空间转换之前需要分配内存空间用于保存重采样的输出数据,内存空间的大小跟通道个数、样本格式需要、容纳的样本个数都有关系。libavutil中的samples处理API提供了一些函数方便管理样本数据,例如av_samples_alloc()函数用于分配存储sample的buffer。av_sample_alloc()的定义如下:/**
* @param[out] audio_data
输出数组,每个元素是指向一个通道的数据的指针。
* @param[out] linesize
aligned size for audio buffer(s), may be NULL
* @param nb_channels
通道的个数。
* @param nb_samples
每个通道的样本个数。
* @param align
buffer size alignment (0 = default, 1 = no alignment)
* @return
成功返回大于0的数,错误返回负数。
*/
int av_samples_alloc(uint8_t **audio_data, int *linesize, int nb_channels,
int nb_samples, enum AVSampleFormat sample_fmt, int align);3.5 开启重采样转换重采样转换是通过重复地调用swr_convert()来完成的。swr_convert()函数的定义如下: * @param out
输出缓冲区,当PCM数据为Packed包装格式时,只有out[0]会填充有数据。
* @param out_count
每个通道可存储输出PCM数据的sample数量。
* @param in
输入缓冲区,当PCM数据为Packed包装格式时,只有in[0]需要填充有数据。
* @param in_count
输入PCM数据中每个通道可用的sample数量。
*
* @return
返回每个通道输出的sample数量,发生错误的时候返回负数。
*/
int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
const uint8_t **in , int in_count);说明:如果没有提供足够的空间用于保存输出数据,采样数据会缓存在swr中。可以通过 swr_get_out_samples()来获取下一次调用swr_convert在给定输入样本数量下输出样本数量的上限,来提供足够的空间。如果是采样频率转换,转换完成后采样数据可能会缓存在swr中,它期待你提供更多的输入数据。如果实际上并不需要更多输入数据,通过调用swr_convert(),其中参数in_count设置为0来获取缓存在swr中的数据。转换结束之后需要冲刷swr_context的缓冲区,通过调用swr_convert(),其中参数in设置为NULL,参数in_count设置为0。 下面的代码演示了重采样转换处理的流程,其中假定依照上面的参数设置、get_input()和handle_output()已经定义好。uint8_t **input;
int in_samples;
while (get_input(&input, &in_samples)) {
uint8_t *output;
int out_samples = av_rescale_rnd(swr_get_delay(swr, 48000) +
in_samples, 44100, 48000, AV_ROUND_UP);
av_samples_alloc(&output, NULL, 2, out_samples,
AV_SAMPLE_FMT_S16, 0);
out_samples = swr_convert(swr, &output, out_samples,
input, in_samples);
handle_output(output, out_samples);
av_freep(&output);
}3.6 重采样转换完成, 释放相关资源转换结束之后,需要调用av_freep(&audio_data[0])来释放内存。4. 示例代码[3] 示例的代码。/**
* @example resampling_audio.c
* libswresample API use example.
*/
#include <libavutil/opt.h>
#include <libavutil/channel_layout.h>
#include <libavutil/samplefmt.h>
#include <libswresample/swresample.h>
static int get_format_from_sample_fmt(const char **fmt,
enum AVSampleFormat sample_fmt)
{
int i;
struct sample_fmt_entry {
enum AVSampleFormat sample_fmt; const char *fmt_be, *fmt_le;
} sample_fmt_entries[] = {
{ AV_SAMPLE_FMT_U8,
"u8",
"u8"
},
{ AV_SAMPLE_FMT_S16, "s16be", "s16le" },
{ AV_SAMPLE_FMT_S32, "s32be", "s32le" },
{ AV_SAMPLE_FMT_FLT, "f32be", "f32le" },
{ AV_SAMPLE_FMT_DBL, "f64be", "f64le" },
};
*fmt = NULL;
for (i = 0; i < FF_ARRAY_ELEMS(sample_fmt_entries); i++) {
struct sample_fmt_entry *entry = &sample_fmt_entries[i];
if (sample_fmt == entry->sample_fmt) {
*fmt = AV_NE(entry->fmt_be, entry->fmt_le);
return 0;
}
}
fprintf(stderr,
"Sample format %s not supported as output format\n",
av_get_sample_fmt_name(sample_fmt));
return AVERROR(EINVAL);
}
/**
* Fill dst buffer with nb_samples, generated starting from t.
*/
static void fill_samples(double *dst, int nb_samples, int nb_channels, int sample_rate, double *t)
{
int i, j;
double tincr = 1.0 / sample_rate, *dstp = dst;
const double c = 2 * M_PI * 440.0;
/* generate sin tone with 440Hz frequency and duplicated channels */
for (i = 0; i < nb_samples; i++) {
*dstp = sin(c * *t);
for (j = 1; j < nb_channels; j++)
dstp[j] = dstp[0];
dstp += nb_channels;
*t += tincr;
}
}
int main(int argc, char **argv)
{
int64_t src_ch_layout = AV_CH_LAYOUT_STEREO, dst_ch_layout = AV_CH_LAYOUT_SURROUND;
int src_rate = 48000, dst_rate = 44100;
uint8_t **src_data = NULL, **dst_data = NULL;
int src_nb_channels = 0, dst_nb_channels = 0;
int src_linesize, dst_linesize;
int src_nb_samples = 1024, dst_nb_samples, max_dst_nb_samples;
enum AVSampleFormat src_sample_fmt = AV_SAMPLE_FMT_DBL, dst_sample_fmt = AV_SAMPLE_FMT_S16;
const char *dst_filename = NULL;
FILE *dst_file;
int dst_bufsize;
const char *fmt;
struct SwrContext *swr_ctx;
double t;
int ret;
if (argc != 2) {
fprintf(stderr, "Usage: %s output_file\n"
"API example program to show how to resample an audio stream with libswresample.\n"
"This program generates a series of audio frames, resamples them to a specified "
"output format and rate and saves them to an output file named output_file.\n",
argv[0]);
exit(1);
}
dst_filename = argv[1];
dst_file = fopen(dst_filename, "wb");
if (!dst_file) {
fprintf(stderr, "Could not open destination file %s\n", dst_filename);
exit(1);
}
/* create resampler context */
swr_ctx = swr_alloc();
if (!swr_ctx) {
fprintf(stderr, "Could not allocate resampler context\n");
ret = AVERROR(ENOMEM);
goto end;
}
/* set options */
av_opt_set_int(swr_ctx, "in_channel_layout",
src_ch_layout, 0);
av_opt_set_int(swr_ctx, "in_sample_rate",
src_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", src_sample_fmt, 0);
av_opt_set_int(swr_ctx, "out_channel_layout",
dst_ch_layout, 0);
av_opt_set_int(swr_ctx, "out_sample_rate",
dst_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", dst_sample_fmt, 0);
/* initialize the resampling context */
if ((ret = swr_init(swr_ctx)) < 0) {
fprintf(stderr, "Failed to initialize the resampling context\n");
goto end;
}
/* allocate source and destination samples buffers */
src_nb_channels = av_get_channel_layout_nb_channels(src_ch_layout);
ret = av_samples_alloc_array_and_samples(&src_data, &src_linesize, src_nb_channels,
src_nb_samples, src_sample_fmt, 0);
if (ret < 0) {
fprintf(stderr, "Could not allocate source samples\n");
goto end;
}
/* compute the number of converted samples: buffering is avoided
* ensuring that the output buffer will contain at least all the
* converted input samples */
max_dst_nb_samples = dst_nb_samples =
av_rescale_rnd(src_nb_samples, dst_rate, src_rate, AV_ROUND_UP);
/* buffer is going to be directly written to a rawaudio file, no alignment */
dst_nb_channels = av_get_channel_layout_nb_channels(dst_ch_layout);
ret = av_samples_alloc_array_and_samples(&dst_data, &dst_linesize, dst_nb_channels,
dst_nb_samples, dst_sample_fmt, 0);
if (ret < 0) {
fprintf(stderr, "Could not allocate destination samples\n");
goto end;
}
t = 0;
do {
/* generate synthetic audio */
fill_samples((double *)src_data[0], src_nb_samples, src_nb_channels, src_rate, &t);
/* compute destination number of samples */
dst_nb_samples = av_rescale_rnd(swr_get_delay(swr_ctx, src_rate) +
src_nb_samples, dst_rate, src_rate, AV_ROUND_UP);
if (dst_nb_samples > max_dst_nb_samples) {
av_freep(&dst_data[0]);
ret = av_samples_alloc(dst_data, &dst_linesize, dst_nb_channels,
dst_nb_samples, dst_sample_fmt, 1);
if (ret < 0)
break;
max_dst_nb_samples = dst_nb_samples;
}
/* convert to destination format */
ret = swr_convert(swr_ctx, dst_data, dst_nb_samples, (const uint8_t **)src_data, src_nb_samples);
if (ret < 0) {
fprintf(stderr, "Error while converting\n");
goto end;
}
dst_bufsize = av_samples_get_buffer_size(&dst_linesize, dst_nb_channels,
ret, dst_sample_fmt, 1);
if (dst_bufsize < 0) {
fprintf(stderr, "Could not get sample buffer size\n");
goto end;
}
printf("t:%f in:%d out:%d\n", t, src_nb_samples, ret);
fwrite(dst_data[0], 1, dst_bufsize, dst_file);
} while (t < 10);
if ((ret = get_format_from_sample_fmt(&fmt, dst_sample_fmt)) < 0)
goto end;
fprintf(stderr, "Resampling succeeded. Play the output file with the command:\n"
"ffplay -f %s -channel_layout %"PRId64" -channels %d -ar %d %s\n",
fmt, dst_ch_layout, dst_nb_channels, dst_rate, dst_filename);
end:
fclose(dst_file);
if (src_data)
av_freep(&src_data[0]);
av_freep(&src_data);
if (dst_data)
av_freep(&dst_data[0]);
av_freep(&dst_data);
swr_free(&swr_ctx);
return ret < 0;
}作者:smallest_one原文链接:https://www.jianshu.com/p/bf5e54f553a4
摘要本文介绍了音画不同步问题的五个因素:编码和封装阶段、网络传输阶段、播放器中的处理阶段、源内容产生的问题以及转码和编辑。针对这些因素,提出了相应的解决方案,如使用标准化工具、选择强大的传输协议、自适应缓冲等。此外,介绍了第三方音视频服务商如即构的解决方案,包括优化的编解码器、动态码率调整、前向纠错和包重传等。最后,强调了开发者需要密切监控应用性能并与音视频服务提供商合作,以确保最佳的音画同步体验。一、前言对于音视频开发者来说,掌握排查问题的技术技巧方法是非常必要的,排查问题的技术方法也能够帮助开发者更好地了解音视频技术的原理和工作机制,从而更加深入地理解音视频开发中遇到的各种问题。即构基于多年实时互动领域技术的沉淀和客户服务保障,我们将推出《视频直播技术FAQ》系列文章,将实时互动技术领域的常见问题和经验分享出来,同时会针对具体问题附上业务通识和常用解决方案以及案例经验,希望本系列能成为你手边的音视频通识册子,帮助到开发者们快速定位问题并找到合适的解决方案。音画不同步是许多开发者经常遇到的一个棘手问题。当音频和视频数据不完全同步时,使视频播放显得非常不自然,观众会有很差的观看体验。因此,了解其原因并掌握相应的解决策略显得尤为重要。我们将深入剖析音画不同步的原因,包括硬件设备、编解码处理、网络传输等方面可能导致的不同步情况。同时,我们将提供一系列方法来识别、排查和解决音画不同步问题,以确保用户在观看音视频内容时能够获得更好的体验,并为开发者提供相应的解决方案。二、音画不同步基本概念和表现音画不同步:又被称为口音不同步或AV同步问题,是指在观看视频时,画面与声音之间存在明显的时间差,导致用户观感受到的打断。具体表现为:先声后影:声音先于画面出现,使观众听到声音但尚未看到对应的画面。先影后声:画面先于声音出现,使观众先看到画面但稍后才能听到对应的声音。三、时间戳-音画不同步的核心影响因素指挥棒作用: 时间戳在音视频处理中起着“指挥棒”的作用,它决定了音频和视频什么时候播放、如何播放。处理时间戳的正确性和连续性是确保音画同步,为用户提供无缝、高质量的音视频体验的关键。不准确或不连续的时间戳可能导致一系列的播放问题,其中最明显和令人困扰的便是音画不同步。解决其他问题的基础:时间戳是解决其他音画不同步相关问题的关键。例如,网络波动导致的数据包延迟或丢失可以通过使用时间戳进行包的重新排序或补偿来解决。正确和连续的时间戳为这些操作提供了必要的信息。与其他技术的互动:时间戳是许多音视频技术的基础,如缓冲、前向纠错、适应性流等。它们都依赖于准确和连续的时间戳来优化和同步音视频流的传输和播放。时间戳,通常表示为“PTS”(Presentation Time Stamp),是附加到音频或视频样本上的一个标记,用于指示该样本应当在何时被呈现或播放。这意味着,不论数据是否被实时传输,播放器都可以根据时间戳来正确地同步并播放音频和视频帧。在音视频领域,时间戳是一个至关重要的概念。音视频内容在创建、传输和播放过程中都依赖时间戳来确保同步。时间戳是与每一帧(音频或视频)相关联的标记,用来表示该帧应该在何时进行播放。理想情况下,音频和视频的时间戳应该紧密匹配,这样在播放时,声音和画面就会同步。如果处理不当,可能会导致音画不同步、帧丢失或其他相关问题。四、音画不同步产生的原因音画同步问题的核心在于“时间戳”。当音画不同步发生时,其背后的内在逻辑是:在音视频处理、传输或播放的某个环节,音频和视频之间的这种时间戳匹配性被破坏。这可能是由于多种原因,如网络延迟、编解码延迟、播放器处理策略、源内容问题等。以下是关于时间戳导致音画不同步的核心原因:时间戳的不准确性:如果在录制、采集、编码或封装过程中,音频和视频的时间戳不准确,那么在播放时,它们将不会同步播放。时间戳的处理:在传输或流化过程中,尤其是在实时音视频通信或直播中,网络波动可能会导致部分数据包的延迟或丢失。当这些数据包包含关键的时间戳信息时,可能会导致音画不同步。播放器的处理方式:播放器需要根据时间戳来解码和渲染音频和视频帧。如果播放器不能正确地解析或使用这些时间戳,或者在处理缓冲、丢包等问题时未能维护时间戳的一致性,也会导致音画不同步。总之,确保音画同步的关键是在整个音视频处理链路中正确地生成、维护和使用时间戳。任何破坏时间戳一致性的因素都可能导致音画不同步。音画不同步本质上是由于音频和视频帧的时间戳不匹配导致的,这种不匹配可能在任何音视频内容的生命周期阶段发生,从创作、传输到播放。为了修复这种不同步,通常需要在受影响的环节进行校准或同步操作,确保音频和视频帧的时间戳再次匹配。以下是根据上述因素的一些建议和解决策略:编码和封装阶段:标准化工具:使用经过时间测试且广泛认可的编码和封装工具可以降低时间戳错误的风险。封装格式选择:了解并选择适合应用场景的封装格式。例如,对于流媒体HLS可能更为合适网络 传输:强大的传输协议:使用像RTMP或WebRTC这样的协议,它们内置了对音画同步的支持。前向纠错:应用前向纠错技术,减少因网络问题导致的数据包丢失。适应性 流:根据网络条件调整码率和质量,减少数据包丢失和延迟。播放器处理:选择成熟播放器:成熟的播放器往往对时间戳处理有更强的容错性。自适应缓冲:动态调整缓冲长度,确保流畅播放同时减少同步问题。硬件加速检查:确保所使用的硬件加速器与播放器兼容,并正确处理时间戳的排序。源内容问题:校准录制设备:确保摄像机和麦克风在录制时是同步的。监控录制:实时监测录制内容,以便快速发现和解决任何不同步问题。转码和编辑:非破坏性编辑:选择可以非破坏性处理音视频的编辑软件,避免改变原始时间戳。标准转码工具:与编码和封装阶段一样,选择经过验证的转码工具。确保音画同步是一个复杂的任务,涉及整个音视频处理链路的多个阶段。不过,通过理解可能的问题来源并采用恰当的技术策略,可以最大程度地减少或消除音画不同步问题。五、音画不同步解决方案下面我们将针对每一个阶段影响音画不同步因素进行拆解,并根据不同因素提供一些建议和解决策略,处理时间戳的正确性和连续性是关键。原因1:编码和封装阶段产生的音画不同步问题产生原因:生成时间戳的不准确性:在录制、编码或封装过程中,如果源设备或软件没有正确生成时间戳,可能导致同步问题。封装格式:不同的封装格式如MP4, MKV, TS等处理时间戳的方式可能会有所不同,如果不当可能会引入不同步。解决方案:标准化工具:使用经过时间测试且广泛认可的编码和封装工具可以降低时间戳错误的风险。如FFmpeg、第三方音视频厂商的编解码工具:即构。封装格式选择:了解并选择适合应用场景的封装格式。例如,对于流媒体,MPEG-DASH或HLS可能更为合适。原因2:网络传输阶段产生的音画不同步问题产生原因:数据包的丢失:在网络不稳定的情况下,音频或视频数据包可能丢失,导致播放器收到的时间戳信息不连续。数据包的延迟:网络波动或拥塞可能导致音频或视频数据包延迟,导致时间戳在播放时不连续。重传机制:为了弥补丢失的数据包,某些协议可能会重新发送数据包,这可能会导致时间戳的乱序或重复。解决方案:强大的传输协议:使用开源协议RTMP或WebRTC这样的协议,它们内置了对音画同步的支持。前向纠错:应用前向纠错技术,减少因网络问题导致的数据包丢失。适应性流:根据网络条件调整码率和质量,减少数据包丢失和延迟。原因3:播放器中的处理阶段产生的音画不同步问题产生原因:缓冲策略:播放器为了提供流畅的播放体验,可能会缓存一定量的音视频数据,这可能会影响音画的同步。错误的时间戳解析:播放器在解析音视频流的时间戳时,可能由于编码、封装或其他问题而出现错误。硬件加速:某些硬件加速解码器在处理音视频流时,可能不完全遵循标准的时间戳处理逻辑,导致不同步。解决方案:选择成熟播放器:成熟的播放器往往对时间戳处理有更强的容错性。自适应缓冲:动态调整缓冲长度,确保流畅播放同时减少同步问题。硬件加速检查:确保所使用的硬件加速器与播放器兼容,并正确处理时间戳。原因4:源内容产生的音画不同步问题产生原因:录制时的不同步:如果在录制过程中音视频源本身就存在不同步问题,那么即使时间戳是准确的,播放时仍会出现音画不同步的现象。解决方案:校准录制设备:确保摄像机和麦克风在录制时是同步的。监控录制:实时监测录制内容,以便快速发现和解决任何不同步问题。原因5:转码和编辑产生原因:在后期编辑或转码过程中,如果音视频轨道被分离并且分别处理,可能会导致时间戳的不一致。转码工具或编辑软件可能会对原始的时间戳进行修改或重新生成,如果处理不当,可能会引入不同步问题。解决方案:非破坏性编辑:选择可以非破坏性处理音视频的编辑软件,避免改变原始时间戳。标准转码工具:与编码和封装阶段一样,选择经过验证的转码工具。考虑到上述因素,维持音画同步需要在整个音视频处理链路中对时间戳进行持续、准确的管理和维护。关于 音画不同步 的问题的排查和解决方案就介绍到这里了,在下一篇文章中,我们将深入探讨视频花屏、绿屏这个话题。

我要回帖

更多关于 卡座录音左右声道不一致 的文章

 

随机推荐