dtas 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|