资讯动态

用 Bash 脚本递归批量提取视频音轨:从 MP4 到 FLAC/M4A 的自动化实践

发布时间:2026/10/9 14:22:31 来源:尧图企业网站定制
前阵子抽空整理硬盘里的视频资料发现一个很尴尬的事一个多小时的现场录像、MV合集、课程录屏其实我真正想留的是音频但视频部分又大又占地方。逐个用格式工厂转一次只能选一个目录子目录还得反复点想提取成 flac 或 m4a又不想打开视频编辑器。于是我干脆用 bash 写了一个批量递归转换 mp4 为 flac/m4a 的脚本把这件事变成一条命令的事。这篇文章就把这个脚本的来龙去脉、完整代码和我在实际使用中踩过的坑都摊开讲。适合手里有一大摞 mp4/mkv/mov、想统一整理成音频库的人也适合刚接触 bash 脚本、想学怎么安全处理「带空格和中文文件名」的朋友。我尽量把每一步设计背后的原因说清楚不是丢一段代码让你照抄就完事。1. 为什么会有这个需求从视频库到音乐库的整理困境先说场景。我的硬盘里常年堆着几类视频线下讲座的录像、歌手现场的观众视角、自己录的访谈素材还有一些老歌曲的 MV。这些内容大多只看一遍但音频会反复听。如果保留整个视频文件存储开销大而且想扔进播放器当音乐播还得在播放列表里把视频和音频混在一起。最早我也试过把 mp4 直接改后缀名成 .flac 或 .m4a结果播放器要么报错要么根本不识别。原因很好理解mp4 是容器里面装着视频流和音频流改后缀只是骗了文件管理器音频流本身没有被提取出来。真正需要做的事情是两条路一是把音频流无损拆出来装进 flac 或 m4a 容器二是如果原始音频是 aac 这类有损格式按需转换成 m4a/aac 以进一步压缩或者强行转成 flac 统一格式。1.1 什么场景需要转换音频而不是直接改后缀如果你只是想「能听」可以用播放器的「只播放音频」功能但那就无法脱离原视频文件做归档。想建一个纯音乐库、给手机同步、给车上 U 盘用就必须要独立音频文件。具体到格式选择原视频音轨是 PCM、ALAC、FLAC 这类无损流或者你录制时就是无损源那输出选 FLAC 最合适格式开放、标签支持好、主流播放器都认。原视频音轨是 AAC 128kbps 这类有损流你再转 FLAC 也不会变无损体积反而大了好几倍。这种情况我更建议直接输出 m4a容器里封装 AAC 编码省空间又通用。如果你在苹果生态里用M4A 容器 ALAC 编码是无损且体验最顺的选择。1.2 为什么不用现成 GUI 工具不是 GUI 工具不好而是它们大多数在「批量、递归、可控」这三件事上太弱。格式工厂能批量但对子目录的递归处理经常要手动添加每个文件夹很多软件的转码参数写死在 UI 里比如默认输出多少码率、采样率重采样成多少我想要的「无损提取」选项往往藏得很深还有处理特殊文件名时偶尔直接跳过或报错。相比之下bash ffmpeg 的优势是能处理任意深度的目录、参数完全可控、可以加日志、可以并行、还能在 NAS 和服务器上作为定时任务跑。这就是我坚持写脚本的原因。2. 核心工具选型与前置准备ffmpeg 仍是唯一靠谱的选择要完成这个转换绕不开 ffmpeg。它能读几乎所有格式的 mp4/mkv/mov能调用 flac、alac、aac 编码器还能精细控制音轨选择、元数据、封面嵌入。我试过用 mp4box、avconv 之类做音频提取但最终还是回到 ffmpeg因为它的语法最稳定、社区资料最多、遇到怪文件时调试手段也最丰富。2.1 ffmpeg 的安装与环境检查在 Debian/Ubuntu 系上安装很简单sudo apt update sudo apt install ffmpegmacOS 上用 Homebrewbrew install ffmpegWindows 上我建议直接启用 WSL 再装 Ubuntu然后在这个 Linux 环境里跑脚本路径处理和文件名编码都比纯 Windows 批处理舒服很多。装完先确认版本和可用的编码器ffmpeg -version ffmpeg -encoders | grep -E flac|alac|aac这条命令会列出 ffmpeg 编译时内置的音频编码器。我关注的三个重点是flac官方原生无损编码器基本都有。alacApple Lossless写入 .m4a 容器时用。aac有损音频编码器几乎所有构建都带原生 AAC但质量略逊于 libfdk_aac。如果你是在需要授权的情况下自编译 ffmpeg可以额外加入 libfdk_aac普通发行版自带版本一般只提供原生的aac日常转 256kbps 也没问题。2.2 先搞清你手上的文件影音流参数很多转换失败不是命令写错而是根本没搞清楚源文件的音频流到底是啥格式。所以我习惯在批量处理前先抽看一个样本ffprobe -v quiet -print_format json -show_streams some_file.mp4重点看codec_name、sample_rate、channels、sample_fmt、bit_rate这几个字段。比如codec_name是aac还是pcm_s16le直接决定转换策略。sample_fmt是fltp还是s32会影响后面 ALAC 编码是否报错。这一步别省尤其当你手头文件来源很杂的时候。3. 递归遍历与文件匹配用 find 替代脚本里的 for 循环陷阱写递归脚本的初学者最容易在这个环节翻车用for f in $(find . -name *.mp4)去遍历文件。这行命令看起来没问题但一旦文件名里有空格、中文、括号、$符号循环变量f就会在空格处断掉脚本就会把一个完整文件名拆成两三个文件然后报「文件不存在」。比如你有一个文件叫周杰伦 演唱会 (现场版).mp4$(find ...)展开后可能被当成周杰伦、演唱会、(现场版).mp4三个参数后果可想而知。3.1 正确处理方式find -print0 while read -d安全且标准的写法是这样的find $INPUT_DIR -type f \( -iname *.mp4 -o -iname *.mkv -o -iname *.mov \) -print0 | while IFS read -r -d f; do echo 正在处理: $f done这段代码里的三个关键点-print0让 find 用\0作为文件名的结尾分隔符。因为 Linux 文件名唯一不允许出现的字符就是/和\0用空字符分隔可以保证任何怪名字都安全。read -d 让 while 循环也按空字符读取刚好与-print0配对。IFS关闭内部字段分隔符避免把空格和制表符当成分隔符。3.2 扩展名大小写匹配视频文件的扩展名可能是.mp4、.MP4、.Mp4甚至有人把 m4v 也算作视频。为了覆盖这些情况我用-iname代替-name它做的是不区分大小写的匹配find $INPUT_DIR -type f \( \ -iname *.mp4 -o \ -iname *.m4v -o \ -iname *.mkv -o \ -iname *.mov -o \ -iname *.avi \ \) -print0不过也要提醒一句扩展名匹配只是入口筛选真正能不能转成功还要看 ffmpeg 能否解析这个文件。遇到某些损坏或者伪扩展名的文件ffmpeg 会报错脚本里必须有失败继续的逻辑而不是整体中止。4. 转换核心ffmpeg 输出 FLAC 和 M4A 的完整参数设计这里是最容易出现「参数堆砌」的部分。我先给一条最基础的转换命令再逐段解释每个参数的含义ffmpeg -i input.mp4 -vn -map 0:a:0 -c:a flac -compression_level 8 output.flac-i input.mp4输入文件。-vn不要任何视频流。如果有封面图作为「视频流」存在这个参数也会一并丢弃所以后面会提到封面保留问题。-map 0:a:0只取输入文件里的第 1 条音频流。视频文件里可能有多条音轨比如原声、评论音轨、多语言配音按需求选。不写这个的话ffmpeg 默认会选一条它认为最佳的音频流但规则并不够直观我习惯显式指定。-c:a flac音频编码器设为 FLAC。-compression_level 8FLAC 的压缩等级0 到 8等级越高体积越小但编码越慢。音频这种量级的数据CPU 多花几秒省下来的空间很有限所以我在服务器上常用-compression_level 5平衡速度与体积。-y覆盖输出文件批量重跑时有用。4.1 输出为 FLAC 的参数细节FLAC 的目标是无损归档所以不要轻易改动采样率和位深。ffmpeg 默认会用源音频流的采样格式做转换但如果源文件是 MP4 里常见的浮点 PCMfltpFLAC 编码器会转成整数 PCM这本身是无损可逆的吗严格说不是所有换算都 100% 无损失但实际听感没有区别。对于大多数场景不必纠结。如果你希望严格保持声道布局比如 5.1 环绕声不要被降混就不要加任何声道处理参数。一旦加了-ac 2ffmpeg 会强制合并声道这基本等于不可逆的有损操作。我早期犯过这个错把一堆 5.1 的视频音轨转成双声道 FLAC后来想再用环绕声就没了。4.2 输出为 M4A 的两种选择M4A 只是个容器里面可以封装 ALAC 无损或者 AAC 有损这个区别需要先分清楚。热词里有个「aac 与 m4a 哪个好」这是个典型的混淆aac 是编码格式m4a 是容器格式两者不在一个维度。如果需要无损就在 m4a 容器里装 ALAC如果需要有损压缩就装 AAC。对应命令# 无损 ALAC 封装进 m4a ffmpeg -i input.mp4 -vn -map 0:a:0 -c:a alac output.m4a # 有损 AAC码率 256kbps适合手机/网盘 ffmpeg -i input.mp4 -vn -map 0:a:0 -c:a aac -b:a 256k output.m4a需要注意ALAC 编码器对采样格式比较挑剔。如果你在转换 96kHz/24bit 的源文件时遇到Sample format not supported之类的报错可以试一下手动指定整数采样格式ffmpeg -i input.mp4 -vn -map 0:a:0 -c:a alac -sample_fmt s32p output.m4a如果是更老的 ffmpeg 版本可能还需要加-strict experimental新版本不再需要。这个问题在第 7 节还会展开说。4.3 多音轨选择与采样率重采样批量场景下一个源文件有多条音轨很常见。我处理过最多的是 8 条音轨的多语言电影。如果想提取第三条音轨-map 0:a:2就能拿到序号从 0 开始。但这里有一个容易踩的坑-map 0:a:2只表示第三条音频流如果该文件没有第三条ffmpeg 会直接报错并返回非零退出码。批量脚本里最好先探测音轨数量或者在转换前用ffprobe检查不然一条缺失就会让你去翻日志。下面是一个简单的检查片段count$(ffprobe -v error -select_streams a -show_entries streamindex -of csvp0 $f | wc -l) if [ $count -lt 3 ]; then echo 跳过 $f没有第3条音轨 continue fi关于采样率除非播放设备明确不支持 96kHz 这类高采样率否则我建议保持原采样率。FLAC 和 ALAC 都支持高采样率但部分手机 APP 和车载系统解码时会把 96kHz 降采样导致底噪变大甚至解码失败。如果目标设备老你可以加上-ar 48000但心里要清楚这不是严格意义上的无损了。5. 完整脚本分解每一段代码的作用与容错设计前面都是零散命令现在把完整的脚本贴出来。这个脚本是基于我自己的使用习惯写的它有配置区、日志、跳过机制和错误标记你可以直接抄也可以按需裁剪。#!/usr/bin/env bash set -uo pipefail # 配置区 INPUT_DIR${1:-.} # 第一个参数输入目录默认当前目录 OUTPUT_DIR${2:-./music} # 第二个参数输出目录默认 ./music FORMAT${FORMAT:-flac} # 目标格式flac 或 m4a CODEC${CODEC:-flac} # 编码器flac / alac / aac / copy BITRATE${BITRATE:-256k} # 仅且有损编码时使用 FORCE${FORCE:-0} # 设为 1 时覆盖已存在文件 JOBS${JOBS:-1} # 并发任务数1 为串行 LOG_FILE${LOG_FILE:-./convert.log} mkdir -p $OUTPUT_DIR # 日志函数 log() { echo [$(date %F %T)] $* | tee -a $LOG_FILE } # 转换函数 convert_one() { local src$1 local rel${src#$INPUT_DIR/} local base${rel%.*} local ext$FORMAT local dst$OUTPUT_DIR/$base.$ext if [ $CODEC flac ]; then extflac extra_opts(-c:a flac -compression_level ${COMPRESSION_LEVEL:-5}) elif [ $CODEC alac ]; then extm4a extra_opts(-c:a alac) elif [ $CODEC aac ]; then extm4a extra_opts(-c:a aac -b:a $BITRATE) elif [ $CODEC copy ]; then extm4a extra_opts(-c:a copy) else log 不支持的编码器: $CODEC return 1 fi dst$OUTPUT_DIR/$base.$ext # 已存在且未强制覆盖则跳过 if [ -f $dst ] [ $FORCE -ne 1 ]; then log SKIP $src (目标已存在) return 0 fi # 确保输出文件的子目录存在 mkdir -p $(dirname $dst) # 执行转换 if ffmpeg -hide_banner -loglevel error -nostdin -n \ -i $src \ -vn -map 0:a:0 \ ${extra_opts[]} \ -y $dst; then log OK $src - $dst return 0 else log FAIL $src (ffmpeg 退出码 $?) return 1 fi } export -f convert_one export INPUT_DIR OUTPUT_DIR FORMAT CODEC BITRATE COMPRESSION_LEVEL LOG_FILE # 主流程 total0 fail0 # 使用 while 循环逐个处理 find $INPUT_DIR -type f \( \ -iname *.mp4 -o -iname *.m4v -o \ -iname *.mkv -o -iname *.mov -o \ -iname *.avi \ \) -print0 2/dev/null | while IFS read -r -d f; do total$((total 1)) if convert_one $f; then : else fail$((fail 1)) fi done echo 处理完成共 $total 个文件失败 $fail 个。 exit $((fail 0 ? 1 : 0))5.1 配置区与变量定义我把INPUT_DIR和OUTPUT_DIR作为脚本参数传入其余通过环境变量控制。这样我可以在 shell 里临时指定输出格式比如FORMATm4a CODECalac ./convert_audio.sh /home/me/Videos /home/me/Music脚本开头的set -uo pipefail有两个作用-u让未定义变量直接报错避免因环境变量缺失产生隐蔽问题pipefail让管道中任何一条命令失败都会影响最终结果防止 find 出错时 while 还在空转。我没加-e这是故意的-e会让脚本在任意命令非零时退出但批量转换中单个文件失败很常见我不想让整个任务中止。5.2 日志、跳过与覆盖控制日志用tee -a同时输出到终端和文件。批量转几百个文件时往终端刷屏很难看清最后的汇总有日志文件方便随时用grep FAIL convert.log找出失败项。输出路径的处理也很关键base${rel%.*}会把相对路径的扩展名去掉再拼上新的扩展名。这样源文件在子目录里的同名文件输出时也会保持相同的子目录结构不会全部扁平化堆在同一个目录里。-no-stdin -n这两个参数容易被忽略。-no-stdin防止 ffmpeg 从标准输入读取交互命令避免脚本在后台跑时因意外按键卡住-n是 no-overwrite转换时如果目标存在就报错避免误覆盖旧文件。我在外层已经做了跳过判断内层再加一道保险。5.3 特殊文件名和中文路径脚本里所有变量都加了双引号包括$src、$dst配合read -d 可以安全处理带空格、中文、括号的文件名。我实际遇到过文件名里有$符号的比如现金$100的讲座.mp4。如果用双引号包住$100不会被 shell 展开如果不加引号$1会被当成位置参数12 个小时的转码任务会在这一条上失败。有一点需要特别说明我脚本里用src${src#$INPUT_DIR/}来生成相对路径。这里的外层双引号和#是 bash 的字符串截断语法作用是去掉前缀得到相对路径。如果你修改脚本注意不要把${var#prefix}里的#当成注释。6. 批量并行加速与资源控制串行版本稳定但 300 个文件转下来确实慢。尤其现在 CPU 动辄 8 核 16 核每次只跑一个 ffmpeg 显然浪费。不过并行也分策略不能无脑开满线程。6.1 用 GNU parallel 还是 xargs -P我推荐优先用 GNU parallel因为它对「函数并行」的支持最自然还能按输出顺序合并日志。安装后只需把主 while 循环改成find $INPUT_DIR -type f \( ... \) -print0 | parallel -0 -j ${JOBS:-4} convert_one {}前提是export -f convert_one已经导出函数。用 xargs 也可以但要靠bash -c包一层写法比较绕find $INPUT_DIR -type f \( ... \) -print0 | xargs -0 -P ${JOBS:-4} -I{} bash -c convert_one $ _ {}6.2 并发数设置CPU、磁盘和发热的平衡并发数不是越大越好。ffmpeg 对多核利用已经不错单个 4K 视频转音频时反而 CPU 占用不高瓶颈在硬盘读取。如果源文件在机械硬盘上4 个并发就可能让磁盘寻道繁忙如果是 SSD 或 NVMe可以开到 8。我自己的经验场景推荐并发数原因机械硬盘2-3大量随机读取会互相拖慢SSD4-6读取速度快CPU 成为主要瓶颈NAS/低功耗设备1-2避免影响其他服务控制温度小型 16 核服务器8有足够 CPU 余量IO 通常不是瓶颈还有两个小技巧在终端里跑批量任务时可以用nice -n 10 ionice -c 3 ./convert_audio.sh降低 IO 优先级这样机器上其他操作不会明显变卡。转码产生的热量对笔记本影响也很大我之前用 8 线程连续跑 40 分钟风扇直接轰鸣后来把并发降到 4 才舒服。7. 实操中遇到的坑与经验附带踩坑排查思路能跑到这里说明你的脚本已经能稳定工作了。但真正让 I 觉得值得写下来的是下面这些小坑每一个都是我花时间看日志才搞明白的。7.1 文件名以-开头导致 ffmpeg 参数解析错误有一次我从网盘下载了一个以-film.mp4开头的文件。运行时 ffmpeg 把文件名当成选项直接报Unrecognized option film.mp4。解决方案很简单让路径带上目录前缀比如-i ./-film.mp4。所以我在脚本里对源文件和目标文件都用绝对路径或显式加./不要只传裸文件名。7.2 96kHz/24bit 转 ALAC 失败或得到超大文件热词里有人问「flac 96khz 24bit 转换 alac 失败」我遇到过两次。第一次报错信息是[alac 0x...] Encoding failed. Unsupported format.原因在于源文件的音频流是fltp浮点格式而 ALAC 编码器支持的是s16p或s32p整数格式。排查方法先用ffprobe查看ffprobe -v error -show_entries streamsample_fmt -of csvp0 input.mp4看到fltp后在 ffmpeg 命令里加-c:a alac -sample_fmt s32p这样就能转成功。但要注意另一个问题96kHz/24bit 的 ALAC 文件体积非常可观一分钟大概在 30~40MB。如果你的播放器或手机上没有足够空间不妨转成 48kHz/24bit AAC 256k听感差别对大多数人来说并不明显体积却小了一个数量级。7.3 不要对有损音轨转 FLAC「提升音质」这是个观念坑。很多视频网站下载的 mp4音轨本来就是 AAC 128kbps你把它转成 FLAC解码出来的数据和源文件里的 AAC 是同一个有损数据并没有修复被压缩掉的那些高频信息。FLAC 只是把 AAC 解码后的 PCM 再无损包一层听感上就是 128kbps AAC 的声音但体积大了十倍。所以我的脚本里专门留了一个CODECcopy模式直接把源文件里的原始音频流复制到 m4a 容器。如果音轨是 AAC输出为 m4a几乎不消耗 CPU体积也保持不变如果音轨是 PCM/ALAC复制到 m4a 更省事。只有当音轨是 PCM 但目标设备只支持 FLAC 时才用CODECflac做重封装。7.4 封面图丢失问题mp4 里的视频流通常是动态画面但它也可以包含一个静态封面图attached_pic。ffmpeg 处理时-vn会把所有视频相关的流都去掉包括封面图。如果你在意专辑封面转换成音频后封面会丢失。解决办法是用两条输入的方式重新嵌入封面ffmpeg -i input.mp4 -i output.flac \ -map 1:a -map 0:v? \ -c copy -disposition:v attached_pic \ output_with_cover.flac不过批量脚本里做这个会比较笨重我一般只在少数专辑类文件上单独处理。7.5 转完发现播放时长不对或无法拖动进度条有次我用一个很旧的 ffmpeg 版本转换 mkv 里的 DTS 音轨出来的 flac 播放进度条能从头拖到尾但音频中间会卡顿。后来发现是源文件时间戳不连续。解决办法是加-fflags genpts让 ffmpeg 重新生成时间戳ffmpeg -fflags genpts -i input.mkv -vn -c:a flac output.flac如果还不行就用-fflags discardcorrupt丢弃损坏的数据包。这类问题没有统一标准只能根据源文件情况逐个试。8. 脚本的扩展方向与我的最终用法脚本到这一步已经能解决 90% 的需求。剩下 10% 的需求比如想按 CUE 分轨、监听文件夹自动转换、把 NAS 上的视频定期清理都是在这个基础上加一层壳的问题。8.1 从整轨 FLAC 生成分轨很多人下载演唱会或精选集后拿到的是一个整轨的 mp4想转成 FLAC 后按 CUE 切片。整轨转 FLAC 的流程前面已经有了分轨则可以用shntool配合 CUE 文件shntool split -f setlist.cue -t %n. %t -o flac album.flac但前提是 CUE 文件里的时间戳和音频完全对齐。视频里如果开头有几分钟空白或者宣传片段CUE 通常会不准先看ffprobe确认音频起始时间。8.2 用 inotifywatch 做自动转换如果你经常把新视频丢进一个目录希望它自动被转成音频可以结合inotifywait做一个简单的监控循环inotifywait -m -r -e close_write --format %w%f $INPUT_DIR | while read -r f; do case ${f,,} in *.mp4|*.mkv) convert_one $f ;; esac done注意inotifywait需要安装inotify-tools而且只能监听本地文件系统NFS 上不一定可靠。8.3 我的最终使用习惯现在我已经把脚本固定在我的媒体服务器上每周自动跑一次。输出目录按艺术家/专辑/曲目的结构组织源文件转完后我通常会再保留一份原始视频在归档盘上确认音频没问题后再手动删除而不是让脚本顺手删。脚本里没有rm这是我的原则批量自动化工具只负责产出不负责销毁避免一个路径写错就酿成事故。如果你也打算长期使用最后再分享一个我个人的小习惯第一次跑全量任务之前先用一个只有两三个文件的小目录试运行确认输出格式、日志和子目录结构都符合预期。等看到OK之后再放开整个目录跑。这个习惯帮我避开了不少因为参数写错而白等两小时的情况。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑