burncd 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. #!/bin/sh
  2. # Burn audio files to a blank CD.
  3. # Copyright 2016-2019 orbea
  4. # All rights reserved.
  5. #
  6. # Redistribution and use of this script, with or without modification, is
  7. # permitted provided that the following conditions are met:
  8. #
  9. # 1. Redistributions of this script must retain the above copyright
  10. # notice, this list of conditions and the following disclaimer.
  11. #
  12. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
  13. # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  14. # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
  15. # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  16. # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  17. # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
  18. # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  19. # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
  20. # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  21. # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  22. # This is required for the legacy ash in Slackware and can be removed
  23. # once ash is no longer included in any supported Slackware releases.
  24. # shellcheck disable=SC2004
  25. # https://github.com/koalaman/shellcheck/wiki/SC2004
  26. IFS='
  27. '
  28. \unset -f command printf read unalias : 2>/dev/null
  29. \unalias -a 2>/dev/null
  30. PATH="$(command -p getconf PATH):$PATH"
  31. LC_ALL=C; export LC_ALL
  32. set -euf
  33. au=; audio=; cmd=; farr=; final=; flac=; format=; fsize=; ignore=; list=; narr=
  34. new=; ord=; out=; req=; sec=; tmp=; warr=; wav=
  35. DEBUG=; DRYRUN=; VERBOSE=
  36. PRGNAM=burncd
  37. DEVICE='/dev/sr1'
  38. OUTPUT='/tmp/CD'
  39. die () {
  40. ret="$1"; shift
  41. case "$ret" in
  42. : ) printf %s\\n "$@" >&2; return 0 ;;
  43. 0 ) printf %s\\n "$@" ;;
  44. * ) printf %s\\n "$@" >&2 ;;
  45. esac
  46. exit "$ret"
  47. }
  48. escape () {
  49. word="$1"; base=
  50. case "$word" in
  51. *\\*|*\$*|*\"*|*\`* )
  52. while [ "$word" != '' ]; do
  53. first="$(printf %s "${word%"${word#?}"}")"
  54. word="$(printf %s "${word#?}")"
  55. case "$first" in
  56. \\|\$|\"|\` ) first="\\$first" ;;
  57. * ) : ;;
  58. esac
  59. base="${base}$first"
  60. done
  61. ;;
  62. * )
  63. base="$word"
  64. ;;
  65. esac
  66. }
  67. exists () {
  68. v=1
  69. while [ $# -gt 0 ]; do
  70. arg="$1"; shift
  71. case "$arg" in ''|*/) continue ;; esac
  72. x="${arg##*/}" z="${arg%/*}"
  73. [ ! -f "$z/$x" ] || [ ! -x "$z/$x" ] && [ "$z/$x" = "$arg" ] && continue
  74. [ "$x" = "$z" ] && [ -x "$z/$x" ] && [ ! -f "$arg" ] && z=
  75. p=":$z:$PATH"
  76. while [ "$p" != "${p#*:}" ]; do
  77. p="${p#*:}"; d="${p%%:*}"
  78. if [ -f "$d/$x" ] && [ -x "$d/$x" ]; then
  79. printf %s\\n "$d/$x"
  80. v=0
  81. break
  82. fi
  83. done
  84. done
  85. return $v
  86. }
  87. mklist () {
  88. ch=; ll=
  89. action="$1"; shift
  90. { empty "$action" + && { ml="$1"; eval "ch=\"\$$ml\""; shift; }; } || :
  91. while [ $# -gt 0 ]; do
  92. mk="$1"; shift
  93. case "$mk" in -*) break ;; esac
  94. n="$(($n+1))"
  95. empty "$action" + || { ll="$mk"; break; } || :
  96. case "$ll" in *$mk*) continue ;; esac
  97. case "$ch" in *$mk*) continue ;; esac
  98. case "$mk" in
  99. au|flac|wav ) ll="${ll} $mk" ;;
  100. * ) die 1 "Unrecognized audio format '$mk', use -h for help." ;;
  101. esac
  102. done
  103. { empty "$ll" && return 0; } || :
  104. case "$action" in
  105. + ) eval "$ml=\"\$${ml#${ml%%[! ]*}} ${ll#${ll%%[! ]*}}\"" ;;
  106. * ) eval "$action=${ll:-\$$action}" ;;
  107. esac
  108. }
  109. CDRECORD="${CDRECORD:-$(exists cdrecord || :)}"
  110. FLAC="${FLAC:-$(exists flac || :)}"
  111. SOX="${SOX:-$(exists sox || :)}"
  112. FILE="$(exists file || :)"
  113. dry () { case "${DRYRUN:-0}" in 1) : ;; *) "$@" ;; esac; }
  114. empty () { case "${1:-}" in "${2:-}") return 0 ;; *) return 1 ;; esac; }
  115. exclude () { case "$ignore" in *$1*) fmt= ;; *) fmt="$2"; suf="$3" ;; esac; }
  116. log () { case "${VERBOSE:-0}" in 1) printf %s\\n "$@" ;; *) : ;; esac; }
  117. printn () { eval "set -- $1"; printf %s\\n $#; }
  118. quiet () { case "${DEBUG:-0}" in 1) "$@" ;; *) "$@" 2>/dev/null ;; esac; }
  119. quit () { err=$?; eval "set -- $out $tmp"; command -p rm -f -- "$@"; }
  120. use () { :; }
  121. usage="$PRGNAM - Burn audio files from the current directory to a blank CD.
  122. Usage: $PRGNAM [OPTIONS]
  123. -d, --debug, Debug output from external programs.
  124. -e, --exclude, Exclude audio formats.
  125. -f, --format, Choose the audio format.
  126. -h, --help, Show this help message.
  127. -n, --dry-run, Enable a test run without burning to a CD.
  128. -o, --output, Path of the temporary directory.
  129. -v, --verbose, Verbose logging.
  130. -V. --version, Show the $PRGNAM version number.
  131. -z, --device, Path of the CD drive.
  132. Supported audio formats:
  133. au, flac, wav
  134. Environment variables:
  135. CDRECORD : Path of the cdrecord binary. ($CDRECORD)
  136. FLAC : Path of the flac binary. ($FLAC)
  137. SOX : Path of the sox binary. ($SOX)
  138. To use cdrecord as a normal user:
  139. # chown root:somegroup $CDRECORD
  140. # chmod 4710 $CDRECORD"
  141. for flag do
  142. case "$flag" in
  143. -- ) break ;;
  144. -d|--debug ) : ;;
  145. -e|--exclude ) : ;;
  146. -f|--format ) : ;;
  147. -h|--help ) die 0 "$usage" ;;
  148. -n|--dry-run ) : ;;
  149. -o|--output ) : ;;
  150. -v|--verbose ) : ;;
  151. -V|--version ) die 0 "$PRGNAM 0.0" ;;
  152. -z|--device ) : ;;
  153. * ) die 1 "Unrecognized option '$flag', use -h for help." ;;
  154. esac
  155. done
  156. while [ $# -gt 0 ]; do
  157. n=0; option="$1"; shift
  158. case "$option" in
  159. -- ) break ;;
  160. -d|--debug ) DEBUG=1 ;;
  161. -e|--exclude ) empty "${1+x}" || mklist + ignore "$@" ;;
  162. -f|--format ) empty "${1+x}" || mklist + format "$@" ;;
  163. -n|--dry-run ) DRYRUN=1 ;;
  164. -o|--output ) empty "${1+x}" || mklist OUTPUT "$1" ;;
  165. -v|--verbose ) VERBOSE=1 ;;
  166. -z|--device ) empty "${1+x}" || mklist DEVICE "$1" ;;
  167. esac
  168. shift "$n"
  169. done
  170. if ! empty "$(command -p ls)"; then
  171. set +f
  172. set -- ./*
  173. set -f
  174. else
  175. die 1 'ERROR: Directory is empty.'
  176. fi
  177. log '' 'Configured options:' " DEVICE = $DEVICE" " OUTPUT = $OUTPUT" \
  178. " Preferred audio formats = ${format# }" \
  179. " Excluded audio foramts = ${ignore# }" ''
  180. for l in wav au flac; do
  181. case "$format" in
  182. *$l* ) : ;;
  183. * ) format="${format} $l" ;;
  184. esac
  185. done
  186. for l in $(printf %s "$format"); do
  187. case "$ignore" in
  188. *$l* ) : ;;
  189. * ) final="${final} $l" ;;
  190. esac
  191. done
  192. n=0
  193. for f do
  194. [ -d "$f" ] || [ -r "$f" ] ||
  195. { die : 'WARNING: File can not be read.' "Skipping '$f'."; continue; }
  196. file="$(command -p "$FILE" "$f")"
  197. type="$(printf %s "${file#"$f: "}")"
  198. case "$type" in
  199. *FLAC* ) exclude flac farr wav ;;
  200. *Sun/NeXT* ) exclude au narr au ;;
  201. *WAVE* ) exclude wav warr wav ;;
  202. * ) fmt= ;;
  203. esac
  204. if [ "${fmt}" ]; then
  205. head="${f##*/}"
  206. name="${head%.*}.$suf"
  207. if [ -e "$OUTPUT/$name" ] || [ -e "$OUTPUT/tmp-$name" ]; then
  208. die : "WARNING: File found in $OUTPUT." "Skipping '$f'."
  209. continue
  210. fi
  211. escape "$head"
  212. cmn="$type::${base}:$n:$fmt:$suf"
  213. case $fmt in
  214. farr ) flac="$flac \"${cmn}:\\\$FLAC:16 bit:44.1 kHz:stereo\"" ;;
  215. narr ) au="$au \"${cmn}:null:16-bit:44100 Hz:stereo\"" ;;
  216. warr ) wav="$wav \"${cmn}:null:16 bit:44100 Hz:stereo\"" ;;
  217. esac
  218. n=$(($n+1))
  219. fi
  220. done
  221. for f in $(printf %s "$final"); do
  222. eval "audio=\"\${audio} \${$f}\""
  223. done
  224. eval "set -- $audio"
  225. for f do
  226. song="${f#*::}"
  227. for i in cnl khz bit ext end lst pos; do
  228. eval "$i=\"\${song##*:}\""
  229. song="${song%:*}"
  230. done
  231. add=1
  232. eval "set -- $new"
  233. while [ $# -gt 0 ]; do
  234. empty "${1%.*}" "${song%.*}" && { add=0; break; }
  235. shift
  236. done
  237. empty "$add" 1 || continue
  238. type="${f%%::*}"; var=
  239. escape "$song"
  240. name="${base%.*}.${end:?}"
  241. new="$new \"$base\""
  242. ord="$ord \"$OUTPUT/$name:${pos:?}\""
  243. out="$out \"$OUTPUT/$name\""
  244. tmp="$tmp \"$OUTPUT/tmp-$name\""
  245. eval "${lst:?}=\"\${$lst} \\\"\$base\"\\\""
  246. if ! empty "${ext:?}" null; then
  247. case "$req" in
  248. *$ext* ) : ;;
  249. * ) req="${req} $ext" ;;
  250. esac
  251. fi
  252. for s in "${bit:?}" "${khz:?}" "${cnl:?}"; do
  253. case "$type" in *$s*) : ;; *)
  254. case "$s" in
  255. "$bit" ) var="${var} -b 16" ;;
  256. "$khz" ) var="${var} -r 44.1k" ;;
  257. "$cnl" ) var="${var} -c 2" ;;
  258. esac ;;
  259. esac
  260. done
  261. empty "$var" || cmd="$cmd \"$name:${var#"${var%%[! ]*}"}\""
  262. done
  263. empty "$new" && die 1 "ERROR: No music files found in $PWD."
  264. for prgnam in \$CDRECORD $(printf %s "$req"); do
  265. dep="$(eval printf %s "$prgnam")"
  266. exists "$dep" >/dev/null 2>&1 && { log "$(exists "$dep"): ok"; continue; }
  267. if empty "$dep"; then
  268. case "$prgnam" in
  269. \$CDRECORD ) dep=cdrecord ;;
  270. \$FLAC ) dep=flac ;;
  271. esac
  272. fi
  273. die 1 "ERROR: $dep is not in $PATH."
  274. done
  275. if exists "$SOX" >/dev/null 2>&1; then
  276. eval "set -- $new"
  277. for t in $(quiet "$SOX" --i -D -- "$@"); do
  278. sec="$(($sec+$(printf '%.0f' "$t")))"
  279. done
  280. dur="$(printf '%dh %dm %ds' $(($sec/3600)) $(($sec%3600/60)) $(($sec%60)))"
  281. log "$(exists "$SOX"): ok" "Total duration: $dur"
  282. if [ "$sec" -gt 4800 ]; then
  283. die 1 "ERROR: Total duration of $dur exceeds 80 minutes."
  284. fi
  285. elif ! empty "$cmd"; then
  286. die 1 'ERROR: sox is required for converting to 16 bit / 44.1 kHz / stereo.'
  287. else
  288. die : 'WARNING: sox is required for checking the duration of the audio files.'
  289. fi
  290. while :; do
  291. if [ ! -e "$DEVICE" ]; then
  292. die : "WARNING: $DEVICE not found." 'Configure the device. (i.e. /dev/sr0)'
  293. read -r DEVICE
  294. continue
  295. fi
  296. if empty $DRYRUN; then
  297. blank="$(quiet "$CDRECORD" dev="$DEVICE" -minfo)" ||
  298. die 1 "$CDRECORD: operation failed."
  299. case "$blank" in *Blank*) : ;; *)
  300. die : "WARNING: Blank CD not found in $DEVICE." \
  301. 'Insert a blank CD. Continue? (y/n)'
  302. read -r answer
  303. case "$answer" in
  304. [yY]|[yY][eE][sS] ) continue ;;
  305. * ) die 0 "Blank CD not found in $DEVICE. Aborting ..." ;;
  306. esac ;;
  307. esac
  308. fi
  309. log "Using blank CD found in $DEVICE."
  310. break
  311. done
  312. farth=; fburn=; fmsg=; narth=; nburn=; nmsg=; warth=; wburn=; wmsg=
  313. for b in "$farr" "$narr" "$warr"; do
  314. case "$b" in
  315. '' )
  316. :
  317. ;;
  318. "$farr" )
  319. farth='21/10'
  320. fburn="quiet \"\$FLAC\" --output-prefix=\"\$OUTPUT\"/ --decode -- \"\$@\""
  321. fmsg='Decoding flac files ...'
  322. list="$list \"'\\\$farr' '\\\$farth' '\\\$fburn' '\\\$fmsg'\""
  323. ;;
  324. "$narr" )
  325. narth='19/13'
  326. nburn="quiet die : \"\$@\"; for a do command -p cp -- \
  327. \"\$a\" \"\$OUTPUT/\${a%.*}.au\"; done"
  328. nmsg='Copying Sun/NeXT audio files ...'
  329. list="$list \"'\\\$narr' '\\\$narth' '\\\$nburn' '\\\$nmsg'\""
  330. ;;
  331. "$warr" )
  332. warth='19/13'
  333. wburn="quiet die : \"\$@\"; for a do command -p cp -- \
  334. \"\$a\" \"\$OUTPUT/\${a%.*}.wav\"; done"
  335. wmsg='Copying wav files ...'
  336. list="$list \"'\\\$warr' '\\\$warth' '\\\$wburn' '\\\$wmsg'\""
  337. ;;
  338. esac
  339. done
  340. use "$farth" "$fburn" "$fmsg" "$narth" "$nburn" "$nmsg" "$warth" "$wburn" \
  341. "$wmsg"
  342. command -p mkdir -p -- "$OUTPUT"
  343. eval "set -- $list"
  344. for i do
  345. eval "set -- $i"
  346. eval "math=$2"
  347. eval "eval \"set -- $1\""
  348. fline="$(command -p wc -c -- "$@" | command -p tail -n1)"
  349. asize="$(((${fline%%[ ]*}+512)/1024*${math:?}))"
  350. fsize="$(($fsize+$asize))"
  351. done
  352. tline="$(command -p df -P -- "$OUTPUT" |
  353. { read -r _; read -r l; printf %s "$l"; })"
  354. eval "set -- $tline"
  355. tsize="$(eval "printf %s \${4}")"
  356. log "Estimated size of audio files: $fsize KB" \
  357. "Free space in $OUTPUT: $tsize KB"
  358. if [ "$fsize" -ge "$tsize" ]; then
  359. c=0
  360. for size in "$fsize" "$tsize"; do
  361. eval "mb$c=\"$((($size+512)/1024)) MB\""
  362. c="$(($c+1))"
  363. done
  364. die 1 "ERROR: ${mb0:?} required in $OUTPUT to decode flac files." \
  365. "Disk space: ${mb1:?}"
  366. fi
  367. if [ "$(printn "$list")" -gt 1 ]; then
  368. out=; n=0
  369. arg=$(printn "$ord")
  370. while [ "$(printn "$out")" -lt "$arg" ]; do
  371. eval "set -- $ord"
  372. for i do
  373. if empty "${i##*:}" $n; then
  374. escape "${i%:*}"
  375. out="$out \"$base\""
  376. break
  377. fi
  378. done
  379. n=$(($n+1))
  380. done
  381. fi
  382. trap 'quit; trap - EXIT; exit $err' EXIT INT
  383. eval "set -- $list"
  384. for i do
  385. eval "set -- $i"
  386. eval "burn=$3"
  387. eval "printf '%s\\n' \"$4\""
  388. eval "eval \"set -- $1\""
  389. eval "${burn:?}"
  390. done
  391. eval "set -- $cmd"
  392. for i do
  393. file="${i%:*}"
  394. opt="${i##*:}"
  395. log "Converting $file" "sox options: $opt"
  396. eval "set -- $opt"
  397. quiet "$SOX" -- "$OUTPUT/$file" "$@" -- "$OUTPUT/tmp-$file"
  398. command -p rm -f -- "$OUTPUT/$file"
  399. command -p mv -- "$OUTPUT/tmp-$file" "$OUTPUT/$file"
  400. done
  401. eval "set -- $out"
  402. log 'Burning audio files:' "$@"
  403. dry quiet "$CDRECORD" dev="$DEVICE" -dao -audio -pad -- "$@"
  404. die 0 'CD burning finished successfully.'