any2audio.sf 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. #!/usr/bin/ruby
  2. # Daniel "Trizen" Șuteu
  3. # Date: 21 March 2020
  4. # Edit: 24 October 2023
  5. # https://github.com/trizen
  6. # Convert any media file with ffmpeg, using `n` parallel processes.
  7. func ffmpeg_convert(input, output, bitrate) {
  8. Sys.run("ffmpeg", "-loglevel", "warning", "-i", input, "-vn", "-sn", "-dn", "-b:a", "#{bitrate}K", output)
  9. }
  10. func prepare_file(file, outdir, ext) {
  11. var out = ""
  12. if (file =~ /^(.*)\.\w+\z/s) {|m|
  13. out = (m[0] + "." + ext)
  14. }
  15. else {
  16. out = (file + "." + ext)
  17. }
  18. out = File(outdir, File(out).basename)
  19. if (File(out).rel2abs == File(file).rel2abs) {
  20. return nil
  21. }
  22. return out
  23. }
  24. var threads = 2
  25. var format = 'opus'
  26. var outdir = Dir.cwd
  27. var bitrate = 96
  28. var delete_original = false
  29. func print_help(exit_code=0) {
  30. var name = File(__FILE__).basename
  31. print <<"EOT"
  32. usage: #{name} [options] [files]
  33. options:
  34. -b --bitrate=i : output bitrate in kbps (default: #{bitrate})
  35. -f --format=s : output format (default: #{format})
  36. -d --delete! : delete the original files (default: #{delete_original})
  37. -o --outdir=s : output directory (default: .)
  38. -t --threads=i : number of threads to use (default: #{threads})
  39. example:
  40. #{name} -b 40 -f opus -t 4 *.mp3 # convert MP3 to OPUS at 40kbps
  41. EOT
  42. Sys.exit(exit_code)
  43. }
  44. ARGV.getopt!(
  45. 'f|format=s' => \format,
  46. 'b|bitrate=i' => \bitrate,
  47. 't|threads=i' => \threads,
  48. 'o|outdir=s' => \outdir,
  49. 'd|delete!' => \delete_original,
  50. 'h|help' => print_help,
  51. )
  52. outdir.exists || outdir.make_path || die "Can't create directory <<#{outdir}>>: #{$!}"
  53. ARGV || print_help(1)
  54. ARGV.each_slice(threads, {|*slice|
  55. var files = slice.grep { File(_).exists }
  56. var conversions = files.map {|file|
  57. [File(file), prepare_file(file, outdir, format)]
  58. }.grep {
  59. defined(.tail)
  60. }.grep {
  61. !.tail.exists
  62. }
  63. conversions || next
  64. conversions.each_2d {|from,to|
  65. say ":: Converting: '#{from}' -> '#{to}'"
  66. }
  67. var forks = conversions.map_2d {|input, output|
  68. ffmpeg_convert.fork(input, output, bitrate)
  69. }
  70. forks.each {|t|
  71. say ":: Waiting..."
  72. t.wait
  73. }
  74. conversions.each_2d {|input, output|
  75. output.is_file || next
  76. var output_size = output.size
  77. if (delete_original && (output_size > 0)) {
  78. say "Removing: #{input}"
  79. input.delete || warn "failed to remove: #{$!}"
  80. }
  81. elsif (output_size == 0) { # ffmpeg failed
  82. output.delete # remove empty file
  83. }
  84. }
  85. })