dtas 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +9 -0
  4. data/.rsync_doc +3 -0
  5. data/COPYING +674 -0
  6. data/Documentation/.gitignore +3 -0
  7. data/Documentation/GNUmakefile +46 -0
  8. data/Documentation/dtas-console.txt +42 -0
  9. data/Documentation/dtas-ctl.txt +64 -0
  10. data/Documentation/dtas-cueedit.txt +24 -0
  11. data/Documentation/dtas-enq.txt +29 -0
  12. data/Documentation/dtas-msinkctl.txt +45 -0
  13. data/Documentation/dtas-player.txt +110 -0
  14. data/Documentation/dtas-player_effects.txt +45 -0
  15. data/Documentation/dtas-player_protocol.txt +181 -0
  16. data/Documentation/dtas-sinkedit.txt +41 -0
  17. data/Documentation/dtas-sourceedit.txt +33 -0
  18. data/Documentation/dtas-xdelay.txt +57 -0
  19. data/Documentation/troubleshooting.txt +13 -0
  20. data/GIT-VERSION-GEN +30 -0
  21. data/GNUmakefile +9 -0
  22. data/HACKING +12 -0
  23. data/INSTALL +53 -0
  24. data/README +103 -0
  25. data/Rakefile +97 -0
  26. data/TODO +4 -0
  27. data/bin/dtas-console +160 -0
  28. data/bin/dtas-ctl +10 -0
  29. data/bin/dtas-cueedit +78 -0
  30. data/bin/dtas-enq +13 -0
  31. data/bin/dtas-msinkctl +51 -0
  32. data/bin/dtas-player +34 -0
  33. data/bin/dtas-sinkedit +58 -0
  34. data/bin/dtas-sourceedit +48 -0
  35. data/bin/dtas-xdelay +85 -0
  36. data/dtas-linux.gemspec +18 -0
  37. data/dtas-mpris.gemspec +16 -0
  38. data/examples/dtas_state.yml +18 -0
  39. data/lib/dtas.rb +7 -0
  40. data/lib/dtas/buffer.rb +90 -0
  41. data/lib/dtas/buffer/read_write.rb +102 -0
  42. data/lib/dtas/buffer/splice.rb +142 -0
  43. data/lib/dtas/command.rb +43 -0
  44. data/lib/dtas/compat_onenine.rb +18 -0
  45. data/lib/dtas/disclaimer.rb +18 -0
  46. data/lib/dtas/format.rb +151 -0
  47. data/lib/dtas/pipe.rb +39 -0
  48. data/lib/dtas/player.rb +393 -0
  49. data/lib/dtas/player/client_handler.rb +463 -0
  50. data/lib/dtas/process.rb +87 -0
  51. data/lib/dtas/replaygain.rb +41 -0
  52. data/lib/dtas/rg_state.rb +99 -0
  53. data/lib/dtas/serialize.rb +9 -0
  54. data/lib/dtas/sigevent.rb +10 -0
  55. data/lib/dtas/sigevent/efd.rb +20 -0
  56. data/lib/dtas/sigevent/pipe.rb +28 -0
  57. data/lib/dtas/sink.rb +121 -0
  58. data/lib/dtas/source.rb +147 -0
  59. data/lib/dtas/source/command.rb +40 -0
  60. data/lib/dtas/source/common.rb +14 -0
  61. data/lib/dtas/source/mp3.rb +37 -0
  62. data/lib/dtas/state_file.rb +33 -0
  63. data/lib/dtas/unix_accepted.rb +76 -0
  64. data/lib/dtas/unix_client.rb +51 -0
  65. data/lib/dtas/unix_server.rb +110 -0
  66. data/lib/dtas/util.rb +15 -0
  67. data/lib/dtas/writable_iter.rb +22 -0
  68. data/perl/dtas-graph +129 -0
  69. data/pkg.mk +26 -0
  70. data/setup.rb +1586 -0
  71. data/test/covshow.rb +30 -0
  72. data/test/helper.rb +76 -0
  73. data/test/player_integration.rb +121 -0
  74. data/test/test_buffer.rb +216 -0
  75. data/test/test_format.rb +61 -0
  76. data/test/test_format_change.rb +49 -0
  77. data/test/test_player.rb +47 -0
  78. data/test/test_player_client_handler.rb +86 -0
  79. data/test/test_player_integration.rb +220 -0
  80. data/test/test_rg_integration.rb +117 -0
  81. data/test/test_rg_state.rb +32 -0
  82. data/test/test_sink.rb +32 -0
  83. data/test/test_sink_tee_integration.rb +34 -0
  84. data/test/test_source.rb +102 -0
  85. data/test/test_unixserver.rb +66 -0
  86. data/test/test_util.rb +15 -0
  87. metadata +208 -0
@@ -0,0 +1,87 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require 'shellwords'
5
+ require 'io/wait'
6
+ module DTAS::Process # :nodoc:
7
+ PIDS = {}
8
+
9
+ def self.reaper
10
+ begin
11
+ pid, status = Process.waitpid2(-1, Process::WNOHANG)
12
+ pid or return
13
+ obj = PIDS.delete(pid)
14
+ yield status, obj
15
+ rescue Errno::ECHILD
16
+ return
17
+ end while true
18
+ end
19
+
20
+ # a convienient way to display commands so it's easy to
21
+ # read, copy and paste to a shell
22
+ def xs(cmd)
23
+ cmd.map { |w| Shellwords.escape(w) }.join(' ')
24
+ end
25
+
26
+ # for long-running processes (sox/play/ecasound filters)
27
+ def dtas_spawn(env, cmd, opts)
28
+ opts = { close_others: true, pgroup: true }.merge!(opts)
29
+
30
+ # stringify env, integer values are easier to type unquoted as strings
31
+ env.each { |k,v| env[k] = v.to_s }
32
+
33
+ pid = begin
34
+ Process.spawn(env, cmd, opts)
35
+ rescue Errno::EINTR # Ruby bug?
36
+ retry
37
+ end
38
+ warn [ :spawn, pid, cmd ].inspect if $DEBUG
39
+ @spawn_at = Time.now.to_f
40
+ PIDS[pid] = self
41
+ pid
42
+ end
43
+
44
+ # this is like backtick, but takes an array instead of a string
45
+ # This will also raise on errors
46
+ def qx(cmd, opts = {})
47
+ r, w = IO.pipe
48
+ opts = opts.merge(out: w)
49
+ r.binmode
50
+ if err = opts[:err]
51
+ re, we = IO.pipe
52
+ re.binmode
53
+ opts[:err] = we
54
+ end
55
+ pid = begin
56
+ Process.spawn(*cmd, opts)
57
+ rescue Errno::EINTR # Ruby bug?
58
+ retry
59
+ end
60
+ w.close
61
+ if err
62
+ we.close
63
+ res = ""
64
+ want = { r => res, re => err }
65
+ begin
66
+ readable = IO.select(want.keys) or next
67
+ readable[0].each do |io|
68
+ bytes = io.nread
69
+ begin
70
+ want[io] << io.read_nonblock(bytes > 0 ? bytes : 11)
71
+ rescue Errno::EAGAIN
72
+ # spurious wakeup, bytes may be zero
73
+ rescue EOFError
74
+ want.delete(io)
75
+ end
76
+ end
77
+ end until want.empty?
78
+ re.close
79
+ else
80
+ res = r.read
81
+ end
82
+ r.close
83
+ _, status = Process.waitpid2(pid)
84
+ return res if status.success?
85
+ raise RuntimeError, "`#{xs(cmd)}' failed: #{status.inspect}"
86
+ end
87
+ end
@@ -0,0 +1,41 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ #
5
+ # Represents ReplayGain metadata for a DTAS::Source
6
+ # cleanup/validate values to prevent malicious files from making us
7
+ # run arbitrary commands
8
+ # *_peak values are 0..inf (1.0 being full scale, but >1 is possible
9
+ # *_gain values are specified in dB
10
+
11
+ class DTAS::ReplayGain # :nodoc:
12
+ ATTRS = %w(reference_loudness track_gain album_gain track_peak album_peak)
13
+ ATTRS.each { |a| attr_reader a }
14
+
15
+ def check_gain(val)
16
+ /([+-]?\d+(?:\.\d+)?)/ =~ val ? $1 : nil
17
+ end
18
+
19
+ def check_float(val)
20
+ /(\d+(?:\.\d+)?)/ =~ val ? $1 : nil
21
+ end
22
+
23
+ def initialize(comments)
24
+ comments or return
25
+
26
+ # the replaygain standard specifies 89.0 dB, but maybe some apps are
27
+ # different...
28
+ @reference_loudness =
29
+ check_gain(comments["REPLAYGAIN_REFERENCE_LOUDNESS"]) || "89.0"
30
+
31
+ @track_gain = check_gain(comments["REPLAYGAIN_TRACK_GAIN"])
32
+ @album_gain = check_gain(comments["REPLAYGAIN_ALBUM_GAIN"])
33
+ @track_peak = check_float(comments["REPLAYGAIN_TRACK_PEAK"])
34
+ @album_peak = check_float(comments["REPLAYGAIN_ALBUM_PEAK"])
35
+ end
36
+
37
+ def self.new(comments)
38
+ tmp = super
39
+ tmp.track_gain ? tmp : nil
40
+ end
41
+ end
@@ -0,0 +1,99 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ #
5
+ # provides support for generating appropriate effects for ReplayGain
6
+ # MAYBE: account for non-standard reference loudness (89.0 dB is standard)
7
+ require_relative '../dtas'
8
+ require_relative 'serialize'
9
+ class DTAS::RGState # :nodoc:
10
+ include DTAS::Serialize
11
+
12
+ RG_MODE = {
13
+ # attribute name => method to use
14
+ "album_gain" => :rg_vol_gain,
15
+ "track_gain" => :rg_vol_gain,
16
+ "album_peak" => :rg_vol_norm,
17
+ "track_peak" => :rg_vol_norm,
18
+ }
19
+
20
+ RG_DEFAULT = {
21
+ # skip the effect if the adjustment is too small to be noticeable
22
+ "gain_threshold" => 0.00000001, # in dB
23
+ "norm_threshold" => 0.00000001,
24
+
25
+ "preamp" => 0, # no extra adjustment
26
+ # "mode" => "album_gain", # nil: off
27
+ "mode" => nil, # nil: off
28
+ "fallback_gain" => -6.0, # adjustment dB if necessary RG tag is missing
29
+ "fallback_track" => true,
30
+ "norm_level" => 1.0, # dBFS
31
+ }
32
+
33
+ SIVS = RG_DEFAULT.keys
34
+ SIVS.each { |iv| attr_accessor iv }
35
+
36
+ def initialize
37
+ RG_DEFAULT.each do |k,v|
38
+ instance_variable_set("@#{k}", v)
39
+ end
40
+ end
41
+
42
+ def self.load(hash)
43
+ rv = new
44
+ hash.each { |k,v| rv.__send__("#{k}=", v) } if hash
45
+ rv
46
+ end
47
+
48
+ def to_hash
49
+ ivars_to_hash(SIVS)
50
+ end
51
+
52
+ def to_hsh
53
+ # no point in dumping default values, it's just a waste of space
54
+ to_hash.delete_if { |k,v| RG_DEFAULT[k] == v }
55
+ end
56
+
57
+ # returns a dB argument to the "vol" effect, nil if nothing found
58
+ def rg_vol_gain(val)
59
+ val = val.to_f + @preamp
60
+ return if val.abs < @gain_threshold
61
+ sprintf('vol %0.8gdB', val)
62
+ end
63
+
64
+ # returns a linear argument to the "vol" effect
65
+ def rg_vol_norm(val)
66
+ diff = @norm_level - val.to_f
67
+ return if (@norm_level - diff).abs < @norm_threshold
68
+ diff += @norm_level
69
+ sprintf('vol %0.8g', diff)
70
+ end
71
+
72
+ # The ReplayGain fallback adjustment value (in dB), in case a file is
73
+ # missing ReplayGain tags. This is useful to avoid damage to speakers,
74
+ # eardrums and amplifiers in case a file without then necessary ReplayGain
75
+ # tag slips into the queue
76
+ def rg_fallback_effect(reason)
77
+ @fallback_gain or return
78
+ warn(reason) if $DEBUG
79
+ "vol #{@fallback_gain + @preamp}dB"
80
+ end
81
+
82
+ # returns an array (for command-line argument) for the effect needed
83
+ # to apply ReplayGain
84
+ # this may return nil
85
+ def effect(source)
86
+ return unless @mode
87
+ rg = source.replaygain or
88
+ return rg_fallback_effect("ReplayGain tags missing")
89
+ val = rg.__send__(@mode)
90
+ if ! val && @fallback_track && @mode =~ /\Aalbum_(\w+)/
91
+ tag = "track_#$1"
92
+ val = rg.__send__(tag) or
93
+ return rg_fallback_effect("ReplayGain tag for #@mode missing")
94
+ warn("tag for #@mode missing, using #{tag}")
95
+ end
96
+ # this may be nil if the adjustment is too small:
97
+ __send__(RG_MODE[@mode], val)
98
+ end
99
+ end
@@ -0,0 +1,9 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ module DTAS::Serialize # :nodoc:
5
+ def ivars_to_hash(ivars, rv = {})
6
+ ivars.each { |k| rv[k] = instance_variable_get("@#{k}") }
7
+ rv
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ begin
5
+ raise LoadError, "no eventfd with _DTAS_POSIX" if ENV["_DTAS_POSIX"]
6
+ require 'sleepy_penguin'
7
+ require_relative 'sigevent/efd'
8
+ rescue LoadError
9
+ require_relative 'sigevent/pipe'
10
+ end
@@ -0,0 +1,20 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ class DTAS::Sigevent < SleepyPenguin::EventFD # :nodoc:
5
+ include SleepyPenguin
6
+
7
+ def self.new
8
+ super(0, EventFD::CLOEXEC)
9
+ end
10
+
11
+ def signal
12
+ incr(1)
13
+ end
14
+
15
+ def readable_iter
16
+ value(true)
17
+ yield self, nil # calls DTAS::Process.reaper
18
+ :wait_readable
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ class DTAS::Sigevent # :nodoc:
5
+ attr_reader :to_io
6
+
7
+ def initialize
8
+ @to_io, @wr = IO.pipe
9
+ end
10
+
11
+ def signal
12
+ @wr.syswrite('.') rescue nil
13
+ end
14
+
15
+ def readable_iter
16
+ begin
17
+ @to_io.read_nonblock(11)
18
+ yield self, nil # calls DTAS::Process.reaper
19
+ rescue Errno::EAGAIN
20
+ return :wait_readable
21
+ end while true
22
+ end
23
+
24
+ def close
25
+ @to_io.close
26
+ @wr.close
27
+ end
28
+ end
data/lib/dtas/sink.rb ADDED
@@ -0,0 +1,121 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require 'yaml'
5
+ require_relative '../dtas'
6
+ require_relative 'pipe'
7
+ require_relative 'process'
8
+ require_relative 'command'
9
+ require_relative 'format'
10
+ require_relative 'serialize'
11
+ require_relative 'writable_iter'
12
+
13
+ # this is a sink (endpoint, audio enters but never leaves)
14
+ class DTAS::Sink # :nodoc:
15
+ attr_accessor :prio # any Integer
16
+ attr_accessor :active # boolean
17
+ attr_accessor :name
18
+ attr_accessor :nonblock
19
+ attr_accessor :respawn
20
+
21
+ include DTAS::Command
22
+ include DTAS::Process
23
+ include DTAS::Serialize
24
+ include DTAS::WritableIter
25
+
26
+ SINK_DEFAULTS = COMMAND_DEFAULTS.merge({
27
+ "name" => nil, # order matters, this is first
28
+ "command" => "exec play -q $SOXFMT -",
29
+ "prio" => 0,
30
+ "nonblock" => false,
31
+ "pipe_size" => nil,
32
+ "active" => false,
33
+ "respawn" => false,
34
+ })
35
+
36
+ DEVFD_RE = %r{/dev/fd/([a-zA-Z]\w*)\b}
37
+
38
+ # order matters for Ruby 1.9+, this defines to_hsh serialization so we
39
+ # can make the state file human-friendly
40
+ SIVS = %w(name env command prio nonblock pipe_size active)
41
+
42
+ def initialize
43
+ command_init(SINK_DEFAULTS)
44
+ writable_iter_init
45
+ @sink = self
46
+ end
47
+
48
+ # allow things that look like audio device names ("hw:1,0" , "/dev/dsp")
49
+ # or variable names.
50
+ def valid_name?(s)
51
+ !!(s =~ %r{\A[\w:,/-]+\z})
52
+ end
53
+
54
+ def self.load(hash)
55
+ sink = new
56
+ return sink unless hash
57
+ (SIVS & hash.keys).each do |k|
58
+ sink.instance_variable_set("@#{k}", hash[k])
59
+ end
60
+ sink.valid_name?(sink.name) or raise ArgumentError, "invalid sink name"
61
+ sink
62
+ end
63
+
64
+ def parse(str)
65
+ inputs = {}
66
+ str.scan(DEVFD_RE) { |w| inputs[w[0]] = nil }
67
+ inputs
68
+ end
69
+
70
+ def on_death(status)
71
+ super
72
+ end
73
+
74
+ def spawn(format, opts = {})
75
+ raise "BUG: #{self.inspect}#spawn called twice" if @pid
76
+ rv = []
77
+
78
+ pclass = @nonblock ? DTAS::PipeNB : DTAS::Pipe
79
+
80
+ cmd = command_string
81
+ inputs = parse(cmd)
82
+
83
+ if inputs.empty?
84
+ # /dev/fd/* not specified in the command, assume one input for stdin
85
+ r, w = pclass.new
86
+ w.pipe_size = @pipe_size if @pipe_size
87
+ inputs[:in] = opts[:in] = r
88
+ w.sink = self
89
+ rv << w
90
+ else
91
+ # multiple inputs, fun!, we'll tee to them
92
+ inputs.each_key do |name|
93
+ r, w = pclass.new
94
+ w.pipe_size = @pipe_size if @pipe_size
95
+ inputs[name] = r
96
+ w.sink = self
97
+ rv << w
98
+ end
99
+ opts[:in] = "/dev/null"
100
+
101
+ # map to real /dev/fd/* values and setup proper redirects
102
+ cmd = cmd.gsub(DEVFD_RE) do
103
+ read_fd = inputs[$1].fileno
104
+ opts[read_fd] = read_fd # do not close-on-exec
105
+ "/dev/fd/#{read_fd}"
106
+ end
107
+ end
108
+
109
+ @pid = dtas_spawn(format.to_env.merge!(@env), cmd, opts)
110
+ inputs.each_value { |rpipe| rpipe.close }
111
+ rv
112
+ end
113
+
114
+ def to_hash
115
+ ivars_to_hash(SIVS)
116
+ end
117
+
118
+ def to_hsh
119
+ to_hash.delete_if { |k,v| v == SINK_DEFAULTS[k] }
120
+ end
121
+ end
@@ -0,0 +1,147 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require_relative '../dtas'
5
+ require_relative 'command'
6
+ require_relative 'format'
7
+ require_relative 'replaygain'
8
+ require_relative 'process'
9
+ require_relative 'serialize'
10
+
11
+ # this is usually one input file
12
+ class DTAS::Source # :nodoc:
13
+ attr_reader :infile
14
+ attr_reader :offset
15
+ require_relative 'source/common'
16
+ require_relative 'source/mp3'
17
+
18
+ include DTAS::Command
19
+ include DTAS::Process
20
+ include DTAS::Source::Common
21
+ include DTAS::Source::Mp3
22
+
23
+ SOURCE_DEFAULTS = COMMAND_DEFAULTS.merge(
24
+ "command" => 'exec sox "$INFILE" $SOXFMT - $TRIMFX $RGFX',
25
+ "comments" => nil,
26
+ )
27
+
28
+ SIVS = %w(infile comments command env)
29
+
30
+ def initialize(infile, offset = nil)
31
+ command_init(SOURCE_DEFAULTS)
32
+ @format = nil
33
+ @infile = infile
34
+ @offset = offset
35
+ @comments = nil
36
+ @samples = nil
37
+ end
38
+
39
+ # this exists mainly to make the mpris interface easier, but it's not
40
+ # necessary, the mpris interface also knows the sample rate
41
+ def offset_us
42
+ (offset_samples / format.rate.to_f) * 1000000
43
+ end
44
+
45
+ # returns any offset in samples (relative to the original source file),
46
+ # likely zero unless seek was used
47
+ def offset_samples
48
+ return 0 unless @offset
49
+ case @offset
50
+ when /\A\d+s\z/
51
+ @offset.to_i
52
+ else
53
+ format.hhmmss_to_samples(@offset)
54
+ end
55
+ end
56
+
57
+ def precision
58
+ qx(%W(soxi -p #@infile), err: "/dev/null").to_i # sox.git f4562efd0aa3
59
+ rescue # fallback to parsing the whole output
60
+ s = qx(%W(soxi #@infile), err: "/dev/null")
61
+ s =~ /Precision\s+:\s*(\d+)-bit/
62
+ v = $1.to_i
63
+ return v if v > 0
64
+ raise TypeError, "could not determine precision for #@infile"
65
+ end
66
+
67
+ def format
68
+ @format ||= begin
69
+ fmt = DTAS::Format.new
70
+ fmt.from_file(@infile)
71
+ fmt.bits ||= precision
72
+ fmt
73
+ end
74
+ end
75
+
76
+ # A user may be downloading the file and start playing
77
+ # it before the download completes, this refreshes
78
+ def samples!
79
+ @samples = nil
80
+ samples
81
+ end
82
+
83
+ # This is the number of samples according to the samples in the source
84
+ # file itself, not the decoded output
85
+ def samples
86
+ @samples ||= qx(%W(soxi -s #@infile)).to_i
87
+ rescue => e
88
+ warn e.message
89
+ 0
90
+ end
91
+
92
+ # just run soxi -a
93
+ def __load_comments
94
+ tmp = {}
95
+ case @infile
96
+ when String
97
+ err = ""
98
+ cmd = %W(soxi -a #@infile)
99
+ begin
100
+ qx(cmd, err: err).split(/\n/).each do |line|
101
+ key, value = line.split(/=/, 2)
102
+ key && value or next
103
+ # TODO: multi-line/multi-value/repeated tags
104
+ tmp[key.upcase] = value
105
+ end
106
+ rescue => e
107
+ if /FAIL formats: no handler for file extension/ =~ err
108
+ warn("#{xs(cmd)}: #{err}")
109
+ else
110
+ warn("#{e.message} (#{e.class})")
111
+ end
112
+ # TODO: fallbacks
113
+ end
114
+ end
115
+ tmp
116
+ end
117
+
118
+ def comments
119
+ @comments ||= __load_comments
120
+ end
121
+
122
+ def replaygain
123
+ DTAS::ReplayGain.new(comments) || DTAS::ReplayGain.new(mp3gain_comments)
124
+ end
125
+
126
+ def spawn(format, rg_state, opts)
127
+ raise "BUG: #{self.inspect}#spawn called twice" if @to_io
128
+ e = format.to_env
129
+ e["INFILE"] = @infile
130
+
131
+ # make sure these are visible to the "current" command...
132
+ @env["TRIMFX"] = @offset ? "trim #@offset" : nil
133
+ @env["RGFX"] = rg_state.effect(self) || nil
134
+
135
+ @pid = dtas_spawn(e.merge!(@env), command_string, opts)
136
+ end
137
+
138
+ def to_hsh
139
+ to_hash.delete_if { |k,v| v == SOURCE_DEFAULTS[k] }
140
+ end
141
+
142
+ def to_hash
143
+ rv = ivars_to_hash(SIVS)
144
+ rv["samples"] = samples
145
+ rv
146
+ end
147
+ end