dtas 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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