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.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +9 -0
- data/.rsync_doc +3 -0
- data/COPYING +674 -0
- data/Documentation/.gitignore +3 -0
- data/Documentation/GNUmakefile +46 -0
- data/Documentation/dtas-console.txt +42 -0
- data/Documentation/dtas-ctl.txt +64 -0
- data/Documentation/dtas-cueedit.txt +24 -0
- data/Documentation/dtas-enq.txt +29 -0
- data/Documentation/dtas-msinkctl.txt +45 -0
- data/Documentation/dtas-player.txt +110 -0
- data/Documentation/dtas-player_effects.txt +45 -0
- data/Documentation/dtas-player_protocol.txt +181 -0
- data/Documentation/dtas-sinkedit.txt +41 -0
- data/Documentation/dtas-sourceedit.txt +33 -0
- data/Documentation/dtas-xdelay.txt +57 -0
- data/Documentation/troubleshooting.txt +13 -0
- data/GIT-VERSION-GEN +30 -0
- data/GNUmakefile +9 -0
- data/HACKING +12 -0
- data/INSTALL +53 -0
- data/README +103 -0
- data/Rakefile +97 -0
- data/TODO +4 -0
- data/bin/dtas-console +160 -0
- data/bin/dtas-ctl +10 -0
- data/bin/dtas-cueedit +78 -0
- data/bin/dtas-enq +13 -0
- data/bin/dtas-msinkctl +51 -0
- data/bin/dtas-player +34 -0
- data/bin/dtas-sinkedit +58 -0
- data/bin/dtas-sourceedit +48 -0
- data/bin/dtas-xdelay +85 -0
- data/dtas-linux.gemspec +18 -0
- data/dtas-mpris.gemspec +16 -0
- data/examples/dtas_state.yml +18 -0
- data/lib/dtas.rb +7 -0
- data/lib/dtas/buffer.rb +90 -0
- data/lib/dtas/buffer/read_write.rb +102 -0
- data/lib/dtas/buffer/splice.rb +142 -0
- data/lib/dtas/command.rb +43 -0
- data/lib/dtas/compat_onenine.rb +18 -0
- data/lib/dtas/disclaimer.rb +18 -0
- data/lib/dtas/format.rb +151 -0
- data/lib/dtas/pipe.rb +39 -0
- data/lib/dtas/player.rb +393 -0
- data/lib/dtas/player/client_handler.rb +463 -0
- data/lib/dtas/process.rb +87 -0
- data/lib/dtas/replaygain.rb +41 -0
- data/lib/dtas/rg_state.rb +99 -0
- data/lib/dtas/serialize.rb +9 -0
- data/lib/dtas/sigevent.rb +10 -0
- data/lib/dtas/sigevent/efd.rb +20 -0
- data/lib/dtas/sigevent/pipe.rb +28 -0
- data/lib/dtas/sink.rb +121 -0
- data/lib/dtas/source.rb +147 -0
- data/lib/dtas/source/command.rb +40 -0
- data/lib/dtas/source/common.rb +14 -0
- data/lib/dtas/source/mp3.rb +37 -0
- data/lib/dtas/state_file.rb +33 -0
- data/lib/dtas/unix_accepted.rb +76 -0
- data/lib/dtas/unix_client.rb +51 -0
- data/lib/dtas/unix_server.rb +110 -0
- data/lib/dtas/util.rb +15 -0
- data/lib/dtas/writable_iter.rb +22 -0
- data/perl/dtas-graph +129 -0
- data/pkg.mk +26 -0
- data/setup.rb +1586 -0
- data/test/covshow.rb +30 -0
- data/test/helper.rb +76 -0
- data/test/player_integration.rb +121 -0
- data/test/test_buffer.rb +216 -0
- data/test/test_format.rb +61 -0
- data/test/test_format_change.rb +49 -0
- data/test/test_player.rb +47 -0
- data/test/test_player_client_handler.rb +86 -0
- data/test/test_player_integration.rb +220 -0
- data/test/test_rg_integration.rb +117 -0
- data/test/test_rg_state.rb +32 -0
- data/test/test_sink.rb +32 -0
- data/test/test_sink_tee_integration.rb +34 -0
- data/test/test_source.rb +102 -0
- data/test/test_unixserver.rb +66 -0
- data/test/test_util.rb +15 -0
- metadata +208 -0
data/lib/dtas/process.rb
ADDED
@@ -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
|
data/lib/dtas/source.rb
ADDED
@@ -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
|