Similar to the switch in dtas-archive(1), I got tired of having to manually add stats to all the rips I was tracking out. --- Documentation/dtas-splitfx.pod | 9 ++++++++- bin/dtas-splitfx | 1 + lib/dtas/splitfx.rb | 4 +++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Documentation/dtas-splitfx.pod b/Documentation/dtas-splitfx.pod index f8de59b..6f8d9ed 100644 --- a/Documentation/dtas-splitfx.pod +++ b/Documentation/dtas-splitfx.pod @@ -37,6 +37,12 @@ Print, but do not run the commands to be executed Silent operation, commands are not printed as executed +=item -S, --stats + +Add the sox "stats" effect to the end of the effects chain, +use this with L</--err-suffix> to get a C<.stats> file with +every track output + =item -D, --no-dither Disable automatic setting of the DITHERFX env. This also passes @@ -46,7 +52,8 @@ the option to L<sox(1)> via SOX_OPTS. Write the contents of C<stderr>. This is useful for capturing the per-track output of the L<sox(1)> C<stats> effect when -combined with parallel C<--jobs>. +combined with parallel C<--jobs>. Recommended for use with the +L</--stats> switch. =item -O, --outdir OUTDIR diff --git a/bin/dtas-splitfx b/bin/dtas-splitfx index d0afc7b..17d915d 100755 --- a/bin/dtas-splitfx +++ b/bin/dtas-splitfx @@ -13,6 +13,7 @@ op.on('-n', '--dry-run') { opts[:dryrun] = true } op.on('-j', '--jobs [JOBS]', Integer) { |val| opts[:jobs] = val } # nil==inf op.on('-s', '--quiet', '--silent') { opts[:silent] = true } + op.on('-S', '--stats', 'run stats every track') { opts[:stats] = true } op.on('-f', '--filter FILTER') { |val| (opts[:filter] ||= []) << val } op.on('-D', '--no-dither') { opts[:no_dither] = true } op.on('-O', '--outdir OUTDIR') { |val| opts[:outdir] = val } diff --git a/lib/dtas/splitfx.rb b/lib/dtas/splitfx.rb index bb31ab5..1150ee0 100644 --- a/lib/dtas/splitfx.rb +++ b/lib/dtas/splitfx.rb @@ -10,7 +10,8 @@ # Unlike the stuff for dtas-player, dtas-splitfx is fairly tied to sox # (but we may still pipe to ecasound or anything else) class DTAS::SplitFX # :nodoc: - CMD = 'sox "$INFILE" $COMMENTS $OUTFMT $OUTDST $TRIMFX $FX $RATEFX $DITHERFX' + CMD = 'sox "$INFILE" $COMMENTS $OUTFMT $OUTDST $TRIMFX $FX' \ + ' $RATEFX $DITHERFX $STATS' include DTAS::Process attr_reader :infile, :env, :command @@ -204,6 +205,7 @@ def splitfx_spawn(target, t, opts) elsif outfmt.bits && outfmt.bits <= 16 env["DITHERFX"] = "dither -s" end + env['STATS'] = 'stats' if opts[:stats] comments = Tempfile.new(%W(dtas-splitfx-#{t.comments["TRACKNUMBER"]} .txt)) t.comments.each do |k,v| env[k] = v.to_s
I forgot to run tests :x --- test/test_player_integration.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_player_integration.rb b/test/test_player_integration.rb index 5059bd2..09eceee 100644 --- a/test/test_player_integration.rb +++ b/test/test_player_integration.rb @@ -209,11 +209,11 @@ def test_state_file def test_source_ed s = client_socket - assert_equal "sox av ff splitfx", s.req("source ls") + assert_equal "sox ff av splitfx", s.req("source ls") s.req_ok("source ed av tryorder=-1") assert_equal "av sox ff splitfx", s.req("source ls") s.req_ok("source ed av tryorder=") - assert_equal "sox av ff splitfx", s.req("source ls") + assert_equal "sox ff av splitfx", s.req("source ls") s.req_ok("source ed sox command=true") sox = DTAS.yaml_load(s.req("source cat sox"))
When this project started avconv was favored in Debian, but that hasn't been the case in many years. Increase the priority of ffmpeg to match the current situation in Debian. --- Documentation/dtas-player.pod | 10 +-- Documentation/dtas-sourceedit.pod | 8 +-- lib/dtas/source/av.rb | 6 +- lib/dtas/source/av_ff_common.rb | 8 +-- lib/dtas/source/ff.rb | 7 +- test/test_source_ff.rb | 102 ++++++++++++++++++++++++++++++ 6 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 test/test_source_ff.rb diff --git a/Documentation/dtas-player.pod b/Documentation/dtas-player.pod index 932441a..4cfdd12 100644 --- a/Documentation/dtas-player.pod +++ b/Documentation/dtas-player.pod @@ -70,10 +70,10 @@ To play audio on my favorite USB DAC directly to ALSA, I use: =head2 Seeking/playing audio from large video containers (e.g. VOB) fails This is a problem with large VOBs. We recommend breaking up the -VOB into smaller files or using L<avconv(1)> or L<ffmpeg(1)> to extract -the desired audio stream. +VOB into smaller files or using L<ffmpeg(1)> to extract +the desired audio stream at C<$STREAM_NR>. - avconv -analyzeduration 2G -probesize 2G \ + ffmpeg -analyzeduration 2G -probesize 2G \ -i input.vob -vn -sn -c:a copy -map 0:$STREAM_NR output.ext =head1 ADVANCED EXAMPLES @@ -115,7 +115,7 @@ No subscription is necessary to post to the mailing list. =head1 COPYRIGHT -Copyright 2013-2020 all contributors L<mailto:dtas-all@nongnu.org> +Copyright all contributors L<mailto:dtas-all@nongnu.org> License: GPL-3.0+ L<https://www.gnu.org/licenses/gpl-3.0.txt> @@ -123,4 +123,4 @@ License: GPL-3.0+ L<https://www.gnu.org/licenses/gpl-3.0.txt> L<dtas-player_protocol(7)>, L<dtas-ctl(1)>, L<dtas-enq(1)>, L<dtas-sourceedit(1)>, L<dtas-sinkedit(1)>, L<sox(1)>, L<play(1)>, -L<avconv(1)>, L<ffmpeg(1)>, L<screen(1)>, L<tmux(1)> +L<ffmpeg(1)>, L<screen(1)>, L<tmux(1)> diff --git a/Documentation/dtas-sourceedit.pod b/Documentation/dtas-sourceedit.pod index 67ecabf..593bcf5 100644 --- a/Documentation/dtas-sourceedit.pod +++ b/Documentation/dtas-sourceedit.pod @@ -51,11 +51,7 @@ of a previous "dtas-ctl source cat sox" invocation: $ dtas-sourceedit sox < saved.yml -To change the way dtas-player calls avconv (part of libav): - - $ dtas-sourceedit av - -To change the way dtas-player calls ffmpeg (lightly-tested): +To change the way dtas-player calls ffmpeg: $ dtas-sourceedit ff @@ -77,7 +73,7 @@ No subscription is necessary to post to the mailing list. =head1 COPYRIGHT -Copyright 2013-2020 all contributors L<mailto:dtas-all@nongnu.org> +Copyright all contributors L<mailto:dtas-all@nongnu.org> License: GPL-3.0+ L<https://www.gnu.org/licenses/gpl-3.0.txt> diff --git a/lib/dtas/source/av.rb b/lib/dtas/source/av.rb index 823b29f..dcebcfd 100644 --- a/lib/dtas/source/av.rb +++ b/lib/dtas/source/av.rb @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2020 all contributors <dtas-all@nongnu.org> +# Copyright (C) all contributors <dtas-all@nongnu.org> # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt> # frozen_string_literal: true require_relative '../../dtas' @@ -13,9 +13,7 @@ class DTAS::Source::Av # :nodoc: 'avconv -v error $SSPOS $PROBE -i "$INFILE" $AMAP -f sox - |' \ 'sox -p $SOXFMT - $TRIMFX $RGFX', - # this is above ffmpeg because this av is the Debian default and - # it's easier for me to test av than ff - "tryorder" => 1, + "tryorder" => 2, ) def initialize diff --git a/lib/dtas/source/av_ff_common.rb b/lib/dtas/source/av_ff_common.rb index 3839272..c600c48 100644 --- a/lib/dtas/source/av_ff_common.rb +++ b/lib/dtas/source/av_ff_common.rb @@ -7,10 +7,10 @@ require_relative '../xs' require_relative 'file' -# Common code for libav (avconv/avprobe) and ffmpeg (and ffprobe) -# TODO: newer versions of both *probes support JSON, which will be easier to -# parse. However, the packaged libav version in Debian 7.0 does not -# support JSON, so we have an ugly parser... +# Common code for ffmpeg/ffprobe and the abandoned libav (avconv/avprobe). +# TODO: newer versions of both *probes support JSON, which will be easier +# to parse. libav is abandoned, nowadays, and Debian only packages +# ffmpeg+ffprobe nowadays. module DTAS::Source::AvFfCommon # :nodoc: include DTAS::Source::File include DTAS::XS diff --git a/lib/dtas/source/ff.rb b/lib/dtas/source/ff.rb index 491e580..c337b42 100644 --- a/lib/dtas/source/ff.rb +++ b/lib/dtas/source/ff.rb @@ -1,12 +1,10 @@ -# Copyright (C) 2013-2020 all contributors <dtas-all@nongnu.org> +# Copyright (C) all contributors <dtas-all@nongnu.org> # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt> # frozen_string_literal: true require_relative '../../dtas' require_relative 'av_ff_common' # ffmpeg support -# note: only tested with the compatibility wrapper in the Debian 7.0 package -# (so still using avconv/avprobe) class DTAS::Source::Ff # :nodoc: include DTAS::Source::AvFfCommon @@ -15,8 +13,7 @@ class DTAS::Source::Ff # :nodoc: 'ffmpeg -v error $SSPOS $PROBE -i "$INFILE" $AMAP -f sox - |' \ 'sox -p $SOXFMT - $TRIMFX $RGFX', - # I haven't tested this much since av is in Debian stable and ff is not - "tryorder" => 2, + "tryorder" => 1, ) def initialize diff --git a/test/test_source_ff.rb b/test/test_source_ff.rb new file mode 100644 index 0000000..e53e72e --- /dev/null +++ b/test/test_source_ff.rb @@ -0,0 +1,102 @@ +# Copyright (C) all contributors <dtas-all@nongnu.org> +# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt> +# frozen_string_literal: true +require './test/helper' +require 'dtas/source/ff' +require 'tempfile' + +class TestSourceFf < Testcase + def teardown + @tempfiles.each(&:close!) + end + + def setup + @tempfiles = [] + end + + def x(cmd) + system(*cmd) + assert $?.success?, cmd.inspect + end + + def new_file(suffix) + tmp = Tempfile.new(%W(tmp .#{suffix})) + @tempfiles << tmp + cmd = %W(sox -r 44100 -b 16 -c 2 -n #{tmp.path} trim 0 1) + return tmp if system(*cmd) + nil + end + + def test_flac + return if `which metaflac`.strip.size == 0 + tmp = new_file('flac') or return + + x(%W(metaflac --set-tag=FOO=BAR #{tmp.path})) + x(%W(metaflac --add-replay-gain #{tmp.path})) + source = DTAS::Source::Ff.new.try(tmp.path) + assert_equal source.comments["FOO"], "BAR", source.inspect + rg = source.replaygain('track_gain') + assert_kind_of DTAS::ReplayGain, rg + assert_in_delta 0.0, rg.track_peak.to_f, 0.00000001 + assert_in_delta 0.0, rg.album_peak.to_f, 0.00000001 + assert_operator rg.album_gain.to_f, :>, 1 + assert_operator rg.track_gain.to_f, :>, 1 + end + + def test_mp3gain + return if `which mp3gain`.strip.size == 0 + a = new_file('mp3') or return + b = new_file('mp3') or return + + # redirect stdout to /dev/null temporarily, mp3gain is noisy + File.open("/dev/null", "w") do |null| + old_out = $stdout.dup + $stdout.reopen(null) + begin + x(%W(mp3gain -q #{a.path} #{b.path})) + ensure + $stdout.reopen(old_out) + old_out.close + end + end + + source = DTAS::Source::Ff.new.try(a.path) + rg = source.replaygain('track_gain') + assert_kind_of DTAS::ReplayGain, rg + assert_in_delta 0.0, rg.track_peak.to_f, 0.00000001 + assert_in_delta 0.0, rg.album_peak.to_f, 0.00000001 + assert_operator rg.album_gain.to_f, :>, 1 + assert_operator rg.track_gain.to_f, :>, 1 + end + + def test_offset + tmp = new_file('flac') or return + source = DTAS::Source::Ff.new.try(*%W(#{tmp.path} 5s)) + assert_equal 5, source.offset_samples + + source = DTAS::Source::Ff.new.try(*%W(#{tmp.path} 1:00:00.5)) + expect = 1 * 60 * 60 * 44100 + (44100/2) + assert_equal expect, source.offset_samples + + source = DTAS::Source::Ff.new.try(*%W(#{tmp.path} 1:10.5)) + expect = 1 * 60 * 44100 + (10 * 44100) + (44100/2) + assert_equal expect, source.offset_samples + + source = DTAS::Source::Ff.new.try(*%W(#{tmp.path} 10.03)) + expect = (10 * 44100) + (44100 * 3/100.0) + assert_equal expect, source.offset_samples + end + + def test_offset_us + tmp = new_file('flac') or return + source = DTAS::Source::Ff.new.try(*%W(#{tmp.path} 441s)) + assert_equal 10000.0, source.offset_us + + source = DTAS::Source::Ff.new.try(*%W(#{tmp.path} 22050s)) + assert_equal 500000.0, source.offset_us + + source = DTAS::Source::Ff.new.try(tmp.path, '1') + assert_equal 1000000.0, source.offset_us + end +end if `which ffprobe 2>/dev/null` =~ /ffprobe/ && + `which ffmpeg 2>/dev/null` =~ /ffmpeg/
ffprobe aways says `track' instead of `TRACKNUMBER'; but the rest of our code follows FLAC metadata conventions; so use `TRACKNUMBER'. (avprobe is untested since the libav project is dead) --- lib/dtas/source/av_ff_common.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/dtas/source/av_ff_common.rb b/lib/dtas/source/av_ff_common.rb index 0c84b2f..3839272 100644 --- a/lib/dtas/source/av_ff_common.rb +++ b/lib/dtas/source/av_ff_common.rb @@ -144,6 +144,11 @@ def av_ff_ok? comments[k] = -DTAS.try_enc(v, enc) end + # ffprobe always uses "track", favor FLAC convention "TRACKNUMBER": + if @comments['TRACK'] && !@comments['TRACKNUMBER'] + @comments['TRACKNUMBER'] = @comments.delete('TRACK') + end + ! @astreams.compact.empty? end
Using MSG_EOR with these local sockets is not necessary, and appears to trigger a truncation bug on OpenBSD 7.3. Link: https://marc.info/?i=20230826020759.M335788@dcvr --- lib/dtas/player/client_handler.rb | 2 +- lib/dtas/unix_accepted.rb | 4 ++-- lib/dtas/unix_client.rb | 2 +- test/test_unixserver.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/dtas/player/client_handler.rb b/lib/dtas/player/client_handler.rb index 2914fe7..3c5fe5d 100644 --- a/lib/dtas/player/client_handler.rb +++ b/lib/dtas/player/client_handler.rb @@ -564,7 +564,7 @@ def state_file_dump_async(io, sf) rescue => e res = "ERR dumping to #{xs(sf.path)} #{e.message}" end - io.to_io.send(res, Socket::MSG_EOR) + io.to_io.send(res, 0) ensure exit!(0) end diff --git a/lib/dtas/unix_accepted.rb b/lib/dtas/unix_accepted.rb index a84eade..63d3ce0 100644 --- a/lib/dtas/unix_accepted.rb +++ b/lib/dtas/unix_accepted.rb @@ -17,7 +17,7 @@ def initialize(sock) # returns :wait_readable on success def emit(msg) if @sbuf.empty? - case rv = @to_io.sendmsg_nonblock(msg, Socket::MSG_EOR, exception: false) + case rv = @to_io.sendmsg_nonblock(msg, 0, exception: false) when :wait_writable @sbuf << msg rv @@ -34,7 +34,7 @@ def emit(msg) # flushes pending data if it got buffered def writable_iter - case @to_io.sendmsg_nonblock(@sbuf[0], Socket::MSG_EOR, exception: false) + case @to_io.sendmsg_nonblock(@sbuf[0], 0, exception: false) when :wait_writable then return :wait_writable else @sbuf.shift diff --git a/lib/dtas/unix_client.rb b/lib/dtas/unix_client.rb index 71f833c..8c73b7d 100644 --- a/lib/dtas/unix_client.rb +++ b/lib/dtas/unix_client.rb @@ -24,7 +24,7 @@ def initialize(path = self.class.default_path) def req_start(args) args = xs(args) if Array === args - @to_io.send(args, Socket::MSG_EOR) + @to_io.send(args, 0) end def req_ok(args, timeout = nil) diff --git a/test/test_unixserver.rb b/test/test_unixserver.rb index 7e99b9e..c91354d 100644 --- a/test/test_unixserver.rb +++ b/test/test_unixserver.rb @@ -41,7 +41,7 @@ def test_server_loop @srv.run_once # nothing msgs = [] clients = [] - client.send("HELLO", Socket::MSG_EOR) + client.send("HELLO", 0) @srv.run_once do |c, msg| clients << c msgs << msg
As with soxi(1), waiting for ffprobe(1) or avprobe(1) is still an expensive operation despite Process.spawn being optimized to use vfork(2). --- lib/dtas/source/av.rb | 1 + lib/dtas/source/av_ff_common.rb | 19 ++++++++++++++++--- lib/dtas/source/ff.rb | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/dtas/source/av.rb b/lib/dtas/source/av.rb index 39cad6c..823b29f 100644 --- a/lib/dtas/source/av.rb +++ b/lib/dtas/source/av.rb @@ -20,6 +20,7 @@ class DTAS::Source::Av # :nodoc: def initialize command_init(AV_DEFAULTS) + @mcache = nil @av_ff_probe = "avprobe" end diff --git a/lib/dtas/source/av_ff_common.rb b/lib/dtas/source/av_ff_common.rb index 332a0bb..0c84b2f 100644 --- a/lib/dtas/source/av_ff_common.rb +++ b/lib/dtas/source/av_ff_common.rb @@ -21,10 +21,23 @@ module DTAS::Source::AvFfCommon # :nodoc: attr_reader :format attr_reader :duration + CACHE_KEYS = [ :@duration, :@probe_harder, :@comments, :@astreams, + :@format ].freeze + + def mcache_lookup(infile) + (@mcache ||= DTAS::Mcache.new).lookup(infile) do |input, dst| + tmp = source_file_dup(infile, nil, nil) + tmp.av_ff_ok? or return nil + CACHE_KEYS.each { |k| dst[k] = tmp.instance_variable_get(k) } + dst + end + end + def try(infile, offset = nil, trim = nil) - rv = source_file_dup(infile, offset, trim) - rv.av_ff_ok? or return - rv + ent = mcache_lookup(infile) or return + ret = source_file_dup(infile, offset, trim) + CACHE_KEYS.each { |k| ret.instance_variable_set(k, ent[k]) } + ret end def __parse_astream(cmd, stream) diff --git a/lib/dtas/source/ff.rb b/lib/dtas/source/ff.rb index 687cd18..491e580 100644 --- a/lib/dtas/source/ff.rb +++ b/lib/dtas/source/ff.rb @@ -21,6 +21,7 @@ class DTAS::Source::Ff # :nodoc: def initialize command_init(FF_DEFAULTS) + @mcache = nil @av_ff_probe = "ffprobe" end
As with sox, these will be dealing with legacy encodings and badly-encoding software until the end of days, so do our best to fix them up. --- lib/dtas/source/av_ff_common.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/dtas/source/av_ff_common.rb b/lib/dtas/source/av_ff_common.rb index 5299fdb..332a0bb 100644 --- a/lib/dtas/source/av_ff_common.rb +++ b/lib/dtas/source/av_ff_common.rb @@ -104,13 +104,14 @@ def av_ff_ok? prev_cmd = cmd end while incomplete.compact[0] + enc = Encoding.default_external # typically Encoding::UTF_8 # old avprobe s.scan(%r{^\[FORMAT\]\n(.*?)\n\[/FORMAT\]\n}m) do |_| f = $1.dup f =~ /^duration=([\d\.]+)\s*$/nm and @duration = $1.to_f # TODO: multi-line/multi-value/repeated tags f.gsub!(/^TAG:([^=]+)=(.*)$/ni) { |_| - @comments[$1.upcase] = -($2) + @comments[-DTAS.try_enc($1.upcase, enc)] = $2 } end @@ -118,13 +119,17 @@ def av_ff_ok? s.scan(%r{^\[format\.tags\]\n(.*?)\n\n}m) do |_| f = $1.dup f.gsub!(/^([^=]+)=(.*)$/ni) { |_| - @comments[$1.upcase] = -$2 + @comments[-DTAS.try_enc($1.upcase, enc)] = $2 } end s.scan(%r{^\[format\]\n(.*?)\n\n}m) do |_| f = $1.dup f =~ /^duration=([\d\.]+)\s*$/nm and @duration = $1.to_f end + comments.each do |k,v| + v.chomp! + comments[k] = -DTAS.try_enc(v, enc) + end ! @astreams.compact.empty? end
POSIX path names aren't guaranteed to be UTF-8, and dtas should be capable of playing non-UTF-8 path names from read-only legacy FSes. --- bin/dtas-tl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/dtas-tl b/bin/dtas-tl index 66e0612..c7f4c83 100755 --- a/bin/dtas-tl +++ b/bin/dtas-tl @@ -188,9 +188,9 @@ def add_after(c, argv, last_id) re = ARGV[1] time = ARGV[2] re = Regexp.quote(re) if fixed - re = ignorecase ? %r{#{re}}i : %r{#{re}} + re = ignorecase ? %r{#{re}}in : %r{#{re}}n each_track(c) do |line| - line.sub!(/\A(\d+)=/, '') + line.sub!(/\A(\d+)=/n, '') or abort "unexpected line=#{line.inspect}\n" track_id = $1 if re =~ line req = %W(tl goto #{track_id})
We are safely be able to pipeline 10 requests via SOCK_SEQPACKET on any OS without hitting buffering limits. --- bin/dtas-tl | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/bin/dtas-tl b/bin/dtas-tl index 45a18b5..66e0612 100755 --- a/bin/dtas-tl +++ b/bin/dtas-tl @@ -138,6 +138,8 @@ def add_after(c, argv, last_id) when 'cat' each_track(c) { |line| print "#{line}\n" } when 'prune' + c2 = nil + pending = 0 each_track(c) do |line| line.sub!(/\A(\d+)=/n, '') or abort "unexpected line=#{line.inspect}\n" track_id = $1.to_i @@ -149,7 +151,19 @@ def add_after(c, argv, last_id) warn "# #{line}: #{e.class}" # raise other exceptions end - c.req("tl remove #{track_id}") unless ok + unless ok + c2 ||= DTAS::UNIXClient.new + if pending > 5 + c2.res_wait + pending -= 1 + end + pending += 1 + c2.req_start("tl remove #{track_id}") + end + end + while pending > 0 + c2.res_wait + pending -= 1 end when 'aac' # add-after-current ARGV.shift
Syscalls and waiting for responses are expensive, so grab 128 track IDs at once like we do for other subcommands. --- bin/dtas-tl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/dtas-tl b/bin/dtas-tl index 7659570..45a18b5 100755 --- a/bin/dtas-tl +++ b/bin/dtas-tl @@ -175,10 +175,10 @@ def add_after(c, argv, last_id) time = ARGV[2] re = Regexp.quote(re) if fixed re = ignorecase ? %r{#{re}}i : %r{#{re}} - get_track_ids(c).each do |track_id| - res = c.req("tl get #{track_id}") - res.sub!(/\A1 \d+=/, '') - if re =~ res + each_track(c) do |line| + line.sub!(/\A(\d+)=/, '') + track_id = $1 + if re =~ line req = %W(tl goto #{track_id}) req << time if time res = c.req(req)
Sometimes I only want to archive files matching a certain regexp pattern. --- Documentation/dtas-archive.pod | 11 ++++++++++- bin/dtas-archive | 6 +++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Documentation/dtas-archive.pod b/Documentation/dtas-archive.pod index 157ea70..50237e8 100644 --- a/Documentation/dtas-archive.pod +++ b/Documentation/dtas-archive.pod @@ -52,11 +52,20 @@ Continue after error Number of times to repeat the L<sndfile-cmp(1)> check. Default: 1 +=item -m, --match REGEX + +Only archive files matching a given Ruby (or Perl-compatible) regular +expression. The regular expression is implementation-dependent and +using the Perl-compatible subset of Ruby regexps is recommended as dtas +will be moving away from Ruby at some point. + +Added for dtas v0.22.0 + =back =head1 COPYRIGHT -Copyright 2013-2020 all contributors L<mailto:dtas-all@nongnu.org> +Copyright all contributors L<mailto:dtas-all@nongnu.org> License: GPL-3.0+ L<https://www.gnu.org/licenses/gpl-3.0.txt> diff --git a/bin/dtas-archive b/bin/dtas-archive index c88873e..7c0e4f7 100755 --- a/bin/dtas-archive +++ b/bin/dtas-archive @@ -1,5 +1,5 @@ #!/usr/bin/env ruby -# Copyright (C) 2015-2020 all contributors <dtas-all@nongnu.org> +# Copyright (C) all contributors <dtas-all@nongnu.org> # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt> # frozen_string_literal: true usage = "#$0 SOURCE DESTINATION" @@ -30,6 +30,7 @@ keep_going = false compression = [] comment = [] +match = nil OptionParser.new('', 24, ' ') do |op| op.banner = usage @@ -47,6 +48,7 @@ op.on('-r', '--repeat [COUNT]', 'number of times to check', Integer) do |r| repeat = r end + op.on('-m', '--match=REGEX', String) { |s| match = Regexp.new(s) } op.on('-s', '--quiet', '--silent') { silent = true } op.on('-h', '--help') do puts(op.to_s) @@ -55,6 +57,7 @@ op.parse!(ARGV) end +match ||= %r/./ comment.push('--comment', '') if comment.empty? dst = ARGV.pop @@ -67,6 +70,7 @@ src_st = File.stat(s) if src_st.directory? Find.find(s) do |path| + path =~ match or next File.file?(path) or next dir = File.dirname(path) dir_st = File.stat(dir)
This generates a dtas-splitfx-compatible YAML snippet based on a sorted list of audio tracks: dtas-2splitfx 1.flac 2.flac ... >tracks.yml --- GNUmakefile | 2 +- script/dtas-2splitfx | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100755 script/dtas-2splitfx diff --git a/GNUmakefile b/GNUmakefile index 084a2d8..9019b31 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -82,7 +82,7 @@ bindir = $(prefix)/bin symlink-install : mkdir -p $(bindir) dtas=$(CURDIR)/dtas.sh && cd $(bindir) && \ - for x in $(CURDIR)/bin/*; do \ + for x in $(CURDIR)/bin/* $(CURDIR)/script/*; do \ ln -sf "$$dtas" $$(basename "$$x"); \ done diff --git a/script/dtas-2splitfx b/script/dtas-2splitfx new file mode 100755 index 0000000..afa761d --- /dev/null +++ b/script/dtas-2splitfx @@ -0,0 +1,44 @@ +#!/usr/bin/perl -w +# Copyright (C) all contributors <dtas-all@nongnu.org> +# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt> +# parse soxi output and generates a dtas-splitfx-compatible YAML snippet +# usage: dtas-2splitfx 1.flac 2.flac ... >tracks.yml +use v5.12; +use POSIX qw(strftime); +open my $fh, '-|', 'soxi', @ARGV or die $!; +my $title = ''; +my $off = 0; +my $sec = 0; + +my $flush = sub { + my ($start) = @_; + my $frac = $start =~ s/\.([0-9]+)\z// ? $1 : 0; + $start = strftime('%H:%M:%S', gmtime($start)); + $start .= ".$frac" if $frac; + $start; +}; + +while (<$fh>) { + if (/^Duration\s*:\s*([0-9:\.]+)/) { + my $t = $1; + $sec = $t =~ s/\.([0-9]+)\z// ? "0.$1" : 0; + my @t = split(/:/, $t); # HH:MM:SS + my $mult = 1; + while (defined(my $part = pop @t)) { + $sec += $part * $mult; + $mult *= 60; + } + } elsif (s/^title=//i) { + chomp; + $title = $_; + $title =~ tr!"!'!; + } elsif (/^\s*\z/s && $sec) { + my $start = $flush->($off); + say qq(- t $start "), , $title, '"'; + $off += $sec; + $sec = 0; + $title = ''; + } +} +close $fh or die "soxi failed: \$?=$?"; +say qq(- stop ), $flush->($off);
There's no need to shell-escape the text file in most cases, so make the display of non-ASCII characters more pleasant to users with UTF-8-capable terminals and editors. --- bin/dtas-tl | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/bin/dtas-tl b/bin/dtas-tl index e58ee31..7659570 100755 --- a/bin/dtas-tl +++ b/bin/dtas-tl @@ -43,7 +43,8 @@ def do_edit(c) track_id = $1.to_i orig_idx[track_id] = orig.size orig << track_id - tmp.write("#{Shellwords.escape(line)} =#{track_id}\n") + line = Shellwords.escape(line) if line.include?("\n") + tmp.write("#{line} =#{track_id}\n") end tmp.flush @@ -100,13 +101,8 @@ def do_edit(c) non_existent = [] add.each do |path, after_id| orig = path - path = Shellwords.split(path)[0] - path = File.expand_path(path) - unless File.exist?(path) - path = orig.dup - path = Shellwords.split(path)[0] - path = File.expand_path(path) - end + path = File.expand_path(orig) + path = File.expand_path(Shellwords.split(path)[0]) unless File.exist?(path) if File.exist?(path) cmd = %W(tl add #{path})
It's the established standard for anonymous IMAP access, and our IMAP server now prioritizes anonymous users over the catch-all username:password system due to IMAP scraper bots. --- README | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README b/README index 1d619e0..2d13fb3 100644 --- a/README +++ b/README @@ -84,8 +84,8 @@ You may also read via: NNTP: <nntps://news.public-inbox.org/inbox.comp.audio.dtas> <nntp://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.audio.dtas> <nntp://news.gmane.io/gmane.comp.audio.dtas.general> -IMAP: <imaps://anon:mous@public-inbox.org/inbox.comp.audio.dtas.0> - <imap://anon:mous@7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.audio.dtas.0> +IMAP: <imaps://;AUTH=ANONYMOUS@public-inbox.org/inbox.comp.audio.dtas.0> + <imap://;AUTH=ANONYMOUS@7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.audio.dtas.0> Atom: <https://80x24.org/dtas-all/new.atom> <http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/dtas-all/new.atom> (.onion URLs require Tor: <https://www.torproject.org/>)
Free Software command-line tools for audio playback, mastering, and whatever else related to audio. dtas follows the worse-is-better philosophy and acts as duct tape to combine existing command-line tools for flexibility and ease-of-development. dtas is primarily implemented in Ruby, but will gradually switch to a language with fewer backwards incompatibilities in the future. Changes: There's two minor fixes for dtas-splitfx, and one for dtas-player users using the rate=bypass optimization. 3 changes since v0.20.0 (2022-02-03): splitfx: fix error reporting of failed tracks splitfx: warn on improper encodings for titles player: drain sinks completely before changing sink rate * homepage: https://80x24.org/dtas/ * https://80x24.org/dtas/INSTALL * https://80x24.org/dtas/dtas-player.txt * https://80x24.org/dtas/NEWS.atom * git clone https://80x24.org/dtas.git * dtas-all@nongnu.org (plain-text only, no HTML mail, please) * mail archives: https://80x24.org/dtas-all/ nntps://news.public-inbox.org/inbox.comp.audio.dtas imaps://;AUTH=ANONYMOUS@public-inbox.org/inbox.comp.audio.dtas.0 nntp://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.audio.dtas imap://;AUTH=ANONYMOUS@7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.audio.dtas.0 https://80x24.org/dtas-all/new.atom note: .onion URLs require Tor: <https://www.torproject.org/>
For users of the CPU-saving bypass mode (e.g. "format rate=bypass"), this fixes a bug when enqueueing a 44.1kHz immediately after a 48kHz file (or vice-versa). Note: gapless playback with different rates/channels between tracks has never been supported with bypass mode enabled. Bypass only allows opportunistic gapless when sequential tracks have the same format. --- lib/dtas/player.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/dtas/player.rb b/lib/dtas/player.rb index 06ba788..6ea3aba 100644 --- a/lib/dtas/player.rb +++ b/lib/dtas/player.rb @@ -37,6 +37,7 @@ def initialize @paused = false @format = DTAS::Format.new @bypass = [] # %w(rate bits channels) (not worth Hash overhead) + @bypass_next = nil # source_spec @sinks = {} # { user-defined name => sink } @targets = [] # order matters @@ -331,6 +332,7 @@ def create_default_sink # called when the player is leaving idle state def spawn_sinks(source_spec) + @bypass_next = nil return true if @targets[0] @sinks.each_value do |sink| sink.active or next @@ -392,6 +394,8 @@ def next_source(source_spec) if ! @bypass.empty? && pending.respond_to?(:format) new_fmt = bypass_match!(@format.dup, pending.format) if new_fmt != @format + @bypass_next = source_spec + return if @sink_buf.inflight > 0 stop_sinks # we may fail to start below format_update!(new_fmt) end @@ -434,6 +438,7 @@ def drop_target(target) end def stop_sinks + @bypass_next = nil @targets.each { |t| drop_target(t) }.clear end @@ -458,7 +463,9 @@ def sink_iter end # nothing left inflight, stop the sinks until we have a source + bn = @bypass_next stop_sinks + next_source(bn) if bn # are we restarting for bypass? :ignore end
Sometimes I get non-English song titles and copy+paste them from non-UTF8 texts. --- lib/dtas/splitfx.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/dtas/splitfx.rb b/lib/dtas/splitfx.rb index 1584e16..bb31ab5 100644 --- a/lib/dtas/splitfx.rb +++ b/lib/dtas/splitfx.rb @@ -290,6 +290,7 @@ def parse_track(argv) t = T.new t.tbeg = @t2s.call(start_time) t.comments = @comments.dup + title.valid_encoding? or warn "#{title.inspect} encoding invalid" t.comments["TITLE"] = title t.env = @env.dup
Sometimes the command fails on certain tracks, and we need to use the proper track object for error reporting. --- lib/dtas/splitfx.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dtas/splitfx.rb b/lib/dtas/splitfx.rb index c7eaf42..1584e16 100644 --- a/lib/dtas/splitfx.rb +++ b/lib/dtas/splitfx.rb @@ -395,7 +395,7 @@ def run(target, opts = {}) @out.puts "DONE #{done[0].inspect}" if $DEBUG done[1].close! else - fails << [ t, status ] + fails << [ done[0], status ] end end
Free Software command-line tools for audio playback, mastering, and whatever else related to audio. dtas follows the worse-is-better philosophy and acts as duct tape to combine existing command-line tools for flexibility and ease-of-development. dtas is currently implemented in Ruby, Perl5, and some embedded shell, but may use other languages in the future. Changes: dtas 0.20.0 - ruby 3.1+ compatibility, splitfx improvements This release catches up with Psych (YAML) changes in Ruby 3.1+ Ruby 2.3+ is now the minimum version, though keep in mind the ruby-core team already dropped support for it long ago. Most of the features are focused on audio engineering capabilities of dtas-splitfx. dtas-splitfx gains the --filter switch, along with per-track environment variables and comments. These new features have made my workflow significantly better. dtas-archive supports explicit comments, and omits the default SoX comment. To better cope with temporary and modified files during editing, dtas-player metadata now checks ctime before reusing the cache, handy for frequently-modified files. "dtas-tl prune" is now supported to cull temporary files from the player tracklist. There's a few dtas-console improvements, too. 28 changes since v0.19.0 (2021-09-05): archive: support comments, default to none splitfx: use Etc.nprocessors for jobs if unspecified dtas-console: set X11 terminal title iff DISPLAY is set dtas-console: add 'i' toggle to show comments (metadata) splitfx: fix track_zpad with integer arg doc: drop ordered map from examples player: reduce syscalls when splicing to single target dtas-console: support Wayland terminal titles, too console: workaround safe warnings in outdated `curses' gem require Ruby 2.3+ get rid of DTAS.dedupe_str wrapper move dtas-graph into script/, support Perl for dtas.sh use YAML.unsafe_load in Psych 4.x (Ruby 3.1+) deduplicate and freeze pathnames + metadata player: remove omap conversion dtas: drop unnecessary "require 'yaml'" statements dtas-tl prune: cull missing files from tracklist dtas-tl: drop encoding hacks, use binary stdout+stderr use IO#wait_readable consistently get rid of DTAS::Nonblock wrapper for Ruby <= 2.0 unix_accepted: drop Ruby < 2.3 support code do not check IO#closed? before calling IO#close splitfx: support per-track environment variables splitfx: add --filter option to limit match to comments player: expire sox metadata cache on file st_ctime changes readahead: do not call -@ on non-String splitfx: disallow combining --trim and --filter splitfx: document changes ahead of 0.20.0 release * homepage: https://80x24.org/dtas/ * https://80x24.org/dtas/INSTALL * https://80x24.org/dtas/dtas-player.txt * https://80x24.org/dtas/NEWS.atom * git clone https://80x24.org/dtas.git * dtas-all@nongnu.org (plain-text only, no HTML mail, please) * mail archives: https://80x24.org/dtas-all/ nntps://news.public-inbox.org/inbox.comp.audio.dtas imaps://anon:mous@public-inbox.org/inbox.comp.audio.dtas.0 nntp://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.audio.dtas imap://anon:mous@7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.audio.dtas.0 https://80x24.org/dtas-all/new.atom note: .onion URLs require Tor: <https://www.torproject.org/>
--filter, per-track comments and environments are the subtle but major new features for the next release. --- Documentation/dtas-splitfx.pod | 41 ++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/Documentation/dtas-splitfx.pod b/Documentation/dtas-splitfx.pod index 717cfa6..f8de59b 100644 --- a/Documentation/dtas-splitfx.pod +++ b/Documentation/dtas-splitfx.pod @@ -26,7 +26,8 @@ to use L<ecasound(1)>, too. =item -j, --jobs [JOBS] Number of jobs to run in parallel. If no number is specified, all -jobs are run in parallel. +jobs are run in parallel. Default: number of CPUS (dtas 0.20.0+), +previous versions of dtas defaulted to 1. =item -n, --dry-run @@ -76,6 +77,16 @@ outputs the result as a single file with the TRACKNUMBER of "000". For ease-of-typing, commas in this command-line argument are automatically expanded to spaces when passed to sox. +This switch may not be combined with L</--filter> + +=item -f, --filter [FIELD=]VALUE + +Only process tracks matching a given comment FIELD and VALUE. +If no C<=> is exists, then all comment fields are matched +against the specified VALUE. + +This switch may not be combined with L</--trim> + =item -p, --sox-pipe Used in place of an output target to specify outputting audio data in @@ -121,7 +132,12 @@ highest-numbered track. Default: true =item targets - hash, see "TARGETS" section -=item command - used only by L<dtas-player(1)> +=item command - override the default sox invocation + +This command may be used to specify an alternate command to process each +track. + +Default: sox "$INFILE" $COMMENTS $OUTFMT $OUTDST $TRIMFX $FX $RATEFX $DITHERFX =back @@ -133,11 +149,22 @@ segment. =over -=item t TIME TITLE [fade_in/fade_out=FADE_ARGS] +=item t TIME TITLE [fade_in/fade_out=FADE_ARGS] [.FIELD=VALUE] [env.X=Y] + +The start of a new track at TIME with TITLE. +An optional L</fade_in> and L</fade_out> may be specified for any tracks. +Per-track comments may be specified in the form of C<.FIELD=VALUE>. +Using per-track C<.ARTIST=FOO> allows proper tagging of multi-artist +compilations. -The start of a new track -at TIME with TITLE. An optional fade_in and fade_out may be specified -for the first/last tracks. +Per-track environment variables may be specified in the form +of C<env.K=V> where C<K> is the environment variable name and +C<V> is its value. Per-track environment variables do not affect +playback of YAML files via L<dtas-player(1)> nor use of the L</--trim> +command-line option. However, per-track environment variables do affect +any tracks written to the filesystem, including those using the L</--filter> +switch. These environment variables are intended to affect the specified +L</command> or default L<sox(1)> invocation. =item skip TIME - skip a segment starting at TIME @@ -268,7 +295,7 @@ imbalance in a live concert recording from the audience: =head1 COPYRIGHT -Copyright 2013-2020 all contributors L<mailto:dtas-all@nongnu.org> +Copyright all contributors L<mailto:dtas-all@nongnu.org> License: GPL-3.0+ L<https://www.gnu.org/licenses/gpl-3.0.txt>
They're two different ways of accomplishing roughly the same thing, but --filter can be more flexible given the use of per-track environment variables. --- lib/dtas/splitfx.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/dtas/splitfx.rb b/lib/dtas/splitfx.rb index 696b9ce..c7eaf42 100644 --- a/lib/dtas/splitfx.rb +++ b/lib/dtas/splitfx.rb @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2020 all contributors <dtas-all@nongnu.org> +# Copyright (C) all contributors <dtas-all@nongnu.org> # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt> # frozen_string_literal: true require_relative '../dtas' @@ -357,7 +357,9 @@ def run(target, opts = {}) @rate = opts[:rate] @bits = opts[:bits] trim = opts[:trim] and @tracks = [ UTrim.new(trim, @env, @comments) ] - + if trim && opts[:filter] + raise ArgumentError, 'trim and filter are mutually exclusive' + end fails = [] tracks = @tracks.dup (opts[:filter] || []).each do |re|
Eric Wong (2): splitfx: disallow combining --trim and --filter splitfx: document changes ahead of 0.20.0 release Documentation/dtas-splitfx.pod | 41 ++++++++++++++++++++++++++++------ lib/dtas/splitfx.rb | 6 +++-- 2 files changed, 38 insertions(+), 9 deletions(-)
There may not be an `infile' element in the `current' response. Fixes: bf9787ac517fe19a ("deduplicate and freeze pathnames + metadata") --- bin/dtas-readahead | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/dtas-readahead b/bin/dtas-readahead index 6da5f88..f2ab514 100755 --- a/bin/dtas-readahead +++ b/bin/dtas-readahead @@ -145,8 +145,9 @@ def do_open(path) cur = DTAS.yaml_load(c.req('current')) while @todo_ra > 0 && fp.nil? if current = cur['current'] - track = -current['infile'] + track = current['infile'] break unless track.kind_of?(String) + track = -track fp = work[track] ||= do_open(track) cur_pid = current['pid'] if fp
We still need the TTL to deal with fuse.sshfs and maybe other weird FSes which don't return the st_ctime properly. --- lib/dtas/mcache.rb | 13 ++++++++++++- test/test_mcache.rb | 20 +++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/lib/dtas/mcache.rb b/lib/dtas/mcache.rb index 4f1e9e8..e0a39af 100644 --- a/lib/dtas/mcache.rb +++ b/lib/dtas/mcache.rb @@ -13,14 +13,25 @@ def initialize(shift = 8, ttl = 60) def lookup(infile) bucket = infile.hash & @mask + st = nil if cur = @tbl[bucket] if cur[:infile] == infile && (DTAS.now - cur[:btime]) < @ttl - return cur + begin + st = File.stat(infile) + return cur if cur[:ctime] == st.ctime + rescue + end end end return unless block_given? @tbl[bucket] = begin cur = cur ? cur.clear : {} + begin + st ||= File.stat(infile) + cur[:ctime] = st.ctime + rescue + return + end if ret = yield(infile, cur) ret[:infile] = infile.frozen? ? infile : -(infile.dup) ret[:btime] = DTAS.now diff --git a/test/test_mcache.rb b/test/test_mcache.rb index 2bf0e98..983a69e 100644 --- a/test/test_mcache.rb +++ b/test/test_mcache.rb @@ -1,19 +1,29 @@ -# Copyright (C) 2016-2020 all contributors <dtas-all@nongnu.org> +# Copyright (C) all contributors <dtas-all@nongnu.org> # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt> # frozen_string_literal: true require './test/helper' require 'dtas/mcache' +require 'tempfile' class TestMcache < Testcase def test_mcache + tmp = Tempfile.new(%W(tmp .sox)) + fn = tmp.path + cmd = %W(sox -r 44100 -b 16 -c 2 -n #{fn} trim 0 1) + system(*cmd) or skip mc = DTAS::Mcache.new exist = nil - mc.lookup('hello') { |infile, hash| exist = hash } + mc.lookup(fn) { |infile, hash| + hash[:ctime] = File.stat(infile).ctime + exist = hash + } assert_kind_of Hash, exist - assert_equal 'hello', exist[:infile] + assert_equal fn, exist[:infile] assert_operator exist[:btime], :<=, DTAS.now - assert_same exist, mc.lookup('hello') + assert_same exist, mc.lookup(fn) assert_nil mc.lookup('HELLO') - assert_same exist, mc.lookup('hello'), 'no change after miss' + assert_same exist, mc.lookup(fn), 'no change after miss' + ensure + tmp.close! end end
This can allow filtering for tracks with a given comment declared via the ".#{COMMENT}" mechanism or the track title. If no prefix is given (before the '='), then all comment values are matched. --- bin/dtas-splitfx | 1 + lib/dtas/splitfx.rb | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/bin/dtas-splitfx b/bin/dtas-splitfx index 6ce6521..d0afc7b 100755 --- a/bin/dtas-splitfx +++ b/bin/dtas-splitfx @@ -13,6 +13,7 @@ op.on('-n', '--dry-run') { opts[:dryrun] = true } op.on('-j', '--jobs [JOBS]', Integer) { |val| opts[:jobs] = val } # nil==inf op.on('-s', '--quiet', '--silent') { opts[:silent] = true } + op.on('-f', '--filter FILTER') { |val| (opts[:filter] ||= []) << val } op.on('-D', '--no-dither') { opts[:no_dither] = true } op.on('-O', '--outdir OUTDIR') { |val| opts[:outdir] = val } op.on('-C', '--compression FACTOR') { |val| opts[:compression] = val } diff --git a/lib/dtas/splitfx.rb b/lib/dtas/splitfx.rb index 1ce2007..696b9ce 100644 --- a/lib/dtas/splitfx.rb +++ b/lib/dtas/splitfx.rb @@ -360,6 +360,14 @@ def run(target, opts = {}) fails = [] tracks = @tracks.dup + (opts[:filter] || []).each do |re| + field, val = re.split(/=/, 2) + if val + tracks.delete_if { |t| (t.comments[field] || '') !~ /#{val}/ } + else + tracks.delete_if { |t| t.comments.values.grep(/#{re}/).empty? } + end + end pids = {} jobs = opts[:jobs] || tracks.size # jobs == nil => everything at once if opts[:sox_pipe]