22# shellcheck enable=all shell=bash source-path=SCRIPTDIR
33set -euo pipefail; shopt -s nullglob globstar
44IFS=$' \n\t ' LC_ALL=C
5+
56has (){ command -v -- " $1 " &> /dev/null; }
6- die (){ printf ' %s\n' " $1 " >&2 ; exit " ${2:- 1} " ; }
7+ die (){ printf ' \033[1;31m[ERR]\033[0m %s\n' " $1 " >&2 ; exit " ${2:- 1} " ; }
8+ msg (){ printf ' \033[1;34m[INFO]\033[0m %s\n' " $@ " ; }
9+ wrn (){ printf ' \033[1;33m[WARN]\033[0m %s\n' " $@ " >&2 ; }
710
811usage_main (){
912 cat << 'EOF '
1013Usage: av-tool <command> [options]
1114Commands:
12- gif Convert video to GIF
13- frame Extract single frame
14- combine Mux audio with video/image
15- trim Trim video/audio
16- norm Normalize audio (optionally fade)
17- fade Fade in/out audio+video
18- silence Add silent audio to video
15+ gif Convert video to GIF
16+ frame Extract single frame
17+ combine Mux audio with video/image
18+ trim Trim video/audio
19+ norm Normalize audio (optionally fade)
20+ fade Fade in/out audio+video
21+ silence Add silent audio to video
22+ cd-optimize Prep audio for Red Book CD (16/44.1kHz+dither)
1923EOF
2024}
2125
@@ -34,14 +38,14 @@ sub_float(){ awk -v a="$1" -v b="$2" 'BEGIN{printf "%.6f",a-b}'; }
3438
3539cmd_gif (){
3640 local width=320 fps=15 infile= outfile=
37- while getopts " :i:w:f:o:h" opt; do
41+ while getopts " :i:w:f: o:h" opt; do
3842 case $opt in
3943 i) infile=$OPTARG ;;
4044 w) width=$OPTARG ;;
4145 f) fps=$OPTARG ;;
4246 o) outfile=$OPTARG ;;
4347 h) printf ' Usage: av-tool gif -i input [-w width] [-f fps] [-o output]\n' ; return 0 ;;
44- * ) die " Invalid option: -$OPTARG " ;;
48+ * ) die " Invalid option: -$OPTARG " ;;
4549 esac
4650 done
4751 [[ -z $infile ]] && die " Input (-i) required"
@@ -70,7 +74,7 @@ cmd_frame(){
7074
7175cmd_combine (){
7276 local infile= audio= outfile=
73- while getopts " :i:a:o:h" opt; do
77+ while getopts " :i: a:o:h" opt; do
7478 case $opt in
7579 i) infile=$OPTARG ;;
7680 a) audio=$OPTARG ;;
@@ -97,7 +101,7 @@ cmd_combine(){
97101
98102cmd_trim (){
99103 local infile= start=00:00:00 end= duration= outfile= mode=duration
100- while getopts " :i:s:e:t:o:h" opt; do
104+ while getopts " :i:s: e:t:o:h" opt; do
101105 case $opt in
102106 i) infile=$OPTARG ;;
103107 s) start=$OPTARG ;;
@@ -124,7 +128,7 @@ cmd_trim(){
124128
125129cmd_norm (){
126130 local infile= fade_len=0 outfile=
127- while getopts " :i:f:o:h" opt; do
131+ while getopts " :i:f:o: h" opt; do
128132 case $opt in
129133 i) infile=$OPTARG ;;
130134 f) fade_len=$OPTARG ;;
@@ -173,18 +177,18 @@ cmd_fade(){
173177 local video_len; video_len=$( ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 " $infile " )
174178 local fade_out_start; fade_out_start=$( sub_float " $video_len " " $dur " )
175179 ffmpeg -hide_banner -loglevel error -stats -i " $infile " \
176- -filter_complex " [0:v]fade=t=in:st=0:d=$dur ,fade=t=out:st=$fade_out_start :d=$dur [v];[0:a]afade=t=in:st=0:d=$dur ,afade=t=out:st=$fade_out_start :d=$dur [a]" \
177- -map " [v]" -map " [a]" -c:v libx264 -crf 18 -pix_fmt yuv420p -c:a " $( aac_codec) " " $outfile "
180+ -filter_complex " [0:v]fade=t=in:st=0:d=$dur ,fade=t=out:st=$fade_out_start : d=$dur [v];[0:a]afade=t=in:st=0:d=$dur ,afade=t=out: st=$fade_out_start : d=$dur [a]" \
181+ -map " [v]" -map " [a]" -c:v libx264 -crf 18 -pix_fmt yuv420p -c: a " $( aac_codec) " " $outfile "
178182}
179183
180184cmd_silence (){
181185 local infile= outfile=
182- while getopts " :i:o:h" opt; do
186+ while getopts " :i:o: h" opt; do
183187 case $opt in
184188 i) infile=$OPTARG ;;
185189 o) outfile=$OPTARG ;;
186190 h) printf ' Usage: av-tool silence -i input -o output\n' ; return 0 ;;
187- * ) die " Invalid option: -$OPTARG " ;;
191+ * ) die " Invalid option: -$OPTARG " ;;
188192 esac
189193 done
190194 [[ -z $infile ]] && die " Input (-i) required"
@@ -194,6 +198,42 @@ cmd_silence(){
194198 -shortest -c:v copy -c:a " $( aac_codec) " -map 0:a -map 1:v " $outfile "
195199}
196200
201+ cmd_cd_optimize (){
202+ local input_dir=" ${1:- .} " output_dir=" cd_master_ready"
203+ has ffmpeg || die " Missing ffmpeg"
204+ [[ -d " $input_dir " ]] || die " Input directory not found: $input_dir "
205+ mkdir -p " $output_dir "
206+ msg " Source: $input_dir "
207+ msg " Target: $output_dir "
208+ msg " Mode: Red Book (44.1kHz/16-bit/Stereo) + Triangular Dither"
209+ local count=0 lossy_count=0
210+ while IFS= read -r file; do
211+ local ext=" ${file##* .} " base
212+ base=$( basename " $file " )
213+ base=" ${base% .* } "
214+ [[ " $ext " =~ ^(mp3| m4a| aac| ogg)$ ]] && { wrn " Processing lossy source: $base .$ext (Suboptimal for CD)" ; (( lossy_count++ )) ; }
215+ ffmpeg -y -v error -i " $file " \
216+ -af " aresample=44100:resampler=soxr: precision=28: dither_method=triangular" \
217+ -c:a pcm_s16le " $output_dir /$base .wav"
218+ printf " ."
219+ (( count++ ))
220+ done < <( find " $input_dir " -maxdepth 1 -type f -regextype posix-extended -regex " .*\.(flac|wav|mp3|m4a|aac|ogg|alac|aiff)$" )
221+ printf " \n"
222+ (( count > 0 )) || die " No audio files found in $input_dir "
223+ msg " Processed $count files."
224+ (( lossy_count > 0 )) && wrn " Detected $lossy_count lossy input files. Quality compromised." || msg " All inputs lossless. Quality optimized."
225+ cat << EOF
226+ ---------------------------------------------------------------------
227+ 🔥 BURN INSTRUCTIONS (CRITICAL)
228+ ---------------------------------------------------------------------
229+ 1. MEDIA : Verbatim AZO (Blue) or Taiyo Yuden (Green/Blue).
230+ 2. SPEED : 16x or 24x. DO NOT use Max/52x (High Jitter) or 1x.
231+ 3. MODE : Disc-At-Once (DAO) / Gapless (2-second gap standard).
232+ ---------------------------------------------------------------------
233+ Files ready in: $output_dir /
234+ EOF
235+ }
236+
197237main (){
198238 check_deps
199239 [[ $# -eq 0 ]] && { usage_main; exit 1; }
@@ -206,6 +246,7 @@ main(){
206246 norm) cmd_norm " $@ " ;;
207247 fade) cmd_fade " $@ " ;;
208248 silence) cmd_silence " $@ " ;;
249+ cd-optimize) cmd_cd_optimize " $@ " ;;
209250 * ) die " Unknown command: $sub " ;;
210251 esac
211252}
0 commit comments