将一份试听扩展为批次

ShellBeginner
立即练习

介绍

Fieldnote Audio Studio 需要为录音制作简短试听。你将先制作并播放一份三秒 WAV,再把这条命令扩展成可复用的 Bash 脚本。准备好的录音是课程原创的合成声音,并非麦克风录音。

你应已通过 Shell for Beginners 掌握 Bash 的基本参数、条件、循环、数组和函数。本实验会解释需要的媒体选项和引号用法。FFmpeg 已准备就绪。Workspace 终端和 Listening Room 页面共享同一个容器;每个实验均独立初始化。

制作并播放一份试听

本步骤中,你将在编写批处理脚本之前,先听一听成功的试听片段包含什么。

从 Workspace 开始,该终端已准备好处理 UTF-8 文件名。当前工作目录是预先准备的工作室。如果命令提示相对路径不存在,先用 pwd 查看当前目录,再用下面完整的 cd 命令返回。本课程的所有操作都在这里进行。

cd /home/labex/project/audio-studio
ls input

你应看到 Morning bells.wav、海辺.wav 和 -closing.wav。每份都是八秒的单声道 WAV,包含每秒 22050 个采样点的 16 位 PCM 数据。PCM 直接保存采样值。我们会保留这一格式,只截取开头。输入范围仅包括输入目录直接包含的非隐藏、扩展名为小写 .wav 的普通文件;本项目不递归搜索子目录,也不处理任意编解码格式。

先创建 single,存放单文件实验。mkdir -p 会在需要时创建目录,也允许该目录已经存在。

mkdir -p single

下一条命令中,-i 指定输入,-t 3 将输出限制为三秒,-c:a pcm_s16le 选择有符号 16 位 PCM 音频。选项描述其后对应的输入或输出,因此位置很重要。-v error 隐藏常规进度信息,但保留错误。-nostdin 防止 FFmpeg 为交互命令读取终端输入,也使其更适合循环。最后的路径指定输出文件。

双引号会把包含空格的路径保留为一个参数。前面的 ./ 或绝对目录前缀也能避免以 - 开头的文件名被误认为选项。请使用新的输出文件:禁用标准输入交互后,FFmpeg 通常会拒绝覆盖已有文件。

ffmpeg -nostdin -v error -i "input/Morning bells.wav" -t 3 -c:a pcm_s16le "single/Morning bells.wav"

在这个日志级别下,转换成功时没有输出。$? 是上一条命令的原生退出状态:零表示成功,非零表示失败。必须立即读取;执行另一条命令就会替换它。

printf 'Conversion exit: %s\n' "$?"

应看到 Conversion exit: 0。这个状态说明转换执行成功;播放操作将进一步检验文件是否可用。

切换到 Listening Room Tab 并重新加载页面。在 First preview 下按 Morning bells 的播放控件。聆听变化的音调,将进度控件拖到片段中间,并确认它在三秒处结束。选择 Download WAV 下载同一个文件。页面只读取你的实际输出,不能从原始库补齐缺失音频。返回 Workspace 继续。

Listening Room 中第一份三秒试听

在 First preview 下查找播放、定位控件和 Download WAV 链接。

如果输入路径拼错,先查看 ls input,再修正引号内的名称并重新运行命令。如果不需要的实验文件已占用输出名称,重试前只用 rm -- "single/Morning bells.wav" 删除那一个实验文件。-- 表示选项结束,并不是“删除所有内容”。本实验中保持 library、archive、notes.txt 和 scratch-other 不变。

为转换定义可复用函数

本步骤中,你将把输入、输出和时长传给同一个转换过程,不必为每份录音修改命令。

从 Workspace 的 /home/labex/project/audio-studio 开始。函数为一组命令命名。在 make_preview 内,$1、$2 和 $3 是函数参数。脚本接收自己的参数,再按顺序转交。用 "$1" 这样的引号写法,可以保留每条完整路径,包括空格和 Unicode 字符。

下面的 here-document 写入一个小脚本。cat > preview-one.sh 会替换该脚本文件。<<'BASH' 将之后的行作为字面文本,直到仅含 BASH 的一行;给分隔符加引号,可防止当前 shell 在保存脚本时展开 $1。第一行在脚本作为程序启动时选择 Bash;这里会显式用 bash 执行。

cat > preview-one.sh <<'BASH'
#!/usr/bin/env bash
make_preview() {
  ffmpeg -nostdin -v error -i "$1" -t "$3" -c:a pcm_s16le "$2"
}
make_preview "$1" "$2" "$3"
BASH

用这个函数制作另一份两秒试听。2 是时长参数,不属于任何一个文件名。

bash preview-one.sh "input/海辺.wav" "single/海辺.wav" 2
printf 'Conversion exit: %s\n' "$?"

退出状态仍应为零。重新加载 Listening Room,播放 First preview 下的 海辺,注意它在两秒处结束。返回 Workspace。同一个函数现在能处理不同路径和时长;下一步会加入一组输入。

处理每一份录音

本步骤中,你将发布一个新目录,为每个输入保存一份试听。

从 Workspace 开始。脚本只接收三个参数:输入目录、新输出目录,以及一至五秒的整数时长。第一批使用已经存在且彼此分离的输入目录和输出父目录,输出必须位于输入之外。已有输出必须被拒绝,以免新运行覆盖好成果。不要让两个写入程序同时使用同一个输出路径。这些是普通错误处理,并不保证并发写入安全。

写脚本前,先理解新增部分。realpath 把已有输入目录展开为绝对路径,使每个输入都有安全的目录前缀。shopt -s nullglob 让未匹配到文件的 *.wav 展开为空集合,而不是带星号的字面路径。files=(...) 将匹配项保存为数组;目录上的引号保留其中的空格,未加引号的 *.wav 仍会匹配文件名。${#files[@]} 计算数组成员数,"${files[@]}" 将每个成员分别作为参数传递。

$(command) 捕获命令的输出。命令失败时,|| exit 2 会停止执行。连接测试时,|| 表示逻辑或:[ -e "$2" ] || [ -L "$2" ] 将任一种已存在目标作为拒绝理由。

case 选择分支;1|2|3|4|5 允许其中一个值。[ -e path ] 测试目标是否存在,-L 还可以识别目标已不存在的符号链接。${source##*/} 删除以 / 结尾的最长前缀,只留下文件名。当命令返回非零时,if ! command 进入失败分支。>&2 将说明写到错误流,exit 1 以失败状态停止。

用下面第一个可工作的批处理版本替换预置但不完整的 batch.sh。它用于本实验中的完好输入。转换失败会返回非零,但可能留下部分完成的新目录;下一项引导实验会教你如何防止这种发布。

cat > batch.sh <<'BASH'
#!/usr/bin/env bash
if [ "$#" -ne 3 ]; then
  printf 'Usage: batch.sh INPUT OUTPUT SECONDS\n' >&2
  exit 2
fi
case "$3" in 1|2|3|4|5) ;; *) exit 2 ;; esac
if [ -e "$2" ] || [ -L "$2" ]; then
  printf 'Output already exists\n' >&2
  exit 2
fi
input=$(realpath -- "$1") || exit 2
shopt -s nullglob
files=("$input"/*.wav)
if [ "${#files[@]}" -eq 0 ]; then
  printf 'No WAV inputs\n' >&2
  exit 1
fi
mkdir -- "$2" || exit 1
make_preview() {
  ffmpeg -nostdin -v error -i "$1" -t "$3" -c:a pcm_s16le "$2"
}
for source in "${files[@]}"; do
  name=${source##*/}
  if ! make_preview "$source" "$2/$name" "$3"; then
    printf 'Could not convert: %s\n' "$name" >&2
    exit 1
  fi
done
printf 'Published %s previews\n' "${#files[@]}"
BASH

现在在新目录 previews 中制作三秒试听批次。发布目录必须恰好为每个输入包含一份对应 WAV,保留原文件名,不包含额外文件。

bash batch.sh input previews 3

应看到 Published 3 previews。重新加载 Listening Room。在 Your listening pack 下播放三个片段,在片段内定位,并下载其中一份。每份均为三秒,并包含对应录音的开头,包括 -closing。单文件实验仍单独保留。

回到 Workspace,有意再次执行命令。这是预期的拒绝,并非环境损坏。

bash batch.sh input previews 3
printf 'Repeat exit: %s\n' "$?"

应看到 Output already exists 和 Repeat exit: 2。保留这套好批次;以后实验使用另一个新输出名称。面对空输入,脚本也能明确失败,而不假装制作了有用的包。你已经有了可复用批处理;接下来将改进某份录音损坏时的行为。

总结

你制作了可播放的音频,用引号保留完整文件名,并将函数和数组循环组合为可复用的 Bash 批处理。你读取了原生退出状态,并拒绝覆盖已有试听包。下一项实验会加入暂存目录,防止失败转换发布不完整的新包。