dtas 0.11.0 → 0.12.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 +4 -4
- data/.gitattributes +4 -0
- data/Documentation/GNUmakefile +1 -1
- data/Documentation/dtas-console.txt +1 -0
- data/Documentation/dtas-player_protocol.txt +19 -5
- data/Documentation/dtas-splitfx.txt +13 -0
- data/Documentation/dtas-tl.txt +16 -0
- data/GIT-VERSION-GEN +1 -1
- data/GNUmakefile +2 -2
- data/INSTALL +3 -3
- data/README +4 -0
- data/bin/dtas-archive +5 -1
- data/bin/dtas-console +13 -6
- data/bin/dtas-cueedit +1 -1
- data/bin/dtas-mlib +47 -0
- data/bin/dtas-readahead +211 -0
- data/bin/dtas-sinkedit +1 -1
- data/bin/dtas-sourceedit +1 -1
- data/bin/dtas-splitfx +15 -6
- data/bin/dtas-tl +81 -5
- data/dtas.gemspec +2 -2
- data/lib/dtas.rb +17 -0
- data/lib/dtas/buffer/read_write.rb +21 -19
- data/lib/dtas/buffer/splice.rb +1 -2
- data/lib/dtas/format.rb +2 -2
- data/lib/dtas/mlib.rb +500 -0
- data/lib/dtas/mlib/migrations/0001_initial.rb +42 -0
- data/lib/dtas/nonblock.rb +24 -0
- data/lib/dtas/parse_freq.rb +29 -0
- data/lib/dtas/parse_time.rb +5 -2
- data/lib/dtas/pipe.rb +2 -1
- data/lib/dtas/player.rb +21 -41
- data/lib/dtas/player/client_handler.rb +175 -92
- data/lib/dtas/process.rb +41 -17
- data/lib/dtas/sigevent/pipe.rb +6 -5
- data/lib/dtas/sink.rb +1 -1
- data/lib/dtas/source/splitfx.rb +14 -0
- data/lib/dtas/splitfx.rb +52 -36
- data/lib/dtas/track.rb +13 -0
- data/lib/dtas/tracklist.rb +148 -43
- data/lib/dtas/unix_accepted.rb +49 -32
- data/lib/dtas/unix_client.rb +1 -1
- data/lib/dtas/unix_server.rb +17 -9
- data/lib/dtas/watchable.rb +16 -5
- data/test/test_env.rb +16 -0
- data/test/test_mlib.rb +31 -0
- data/test/test_parse_freq.rb +18 -0
- data/test/test_player_client_handler.rb +12 -12
- data/test/test_splitfx.rb +0 -29
- data/test/test_tracklist.rb +75 -17
- data/test/test_unixserver.rb +0 -11
- metadata +16 -4
data/lib/dtas/process.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# Copyright (C) 2013-2015 all contributors <dtas-all@nongnu.org>
|
2
2
|
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
3
3
|
require 'io/wait'
|
4
|
+
require 'shellwords'
|
4
5
|
require_relative '../dtas'
|
5
6
|
require_relative 'xs'
|
7
|
+
require_relative 'nonblock'
|
6
8
|
|
7
9
|
# process management helpers
|
8
10
|
module DTAS::Process # :nodoc:
|
@@ -23,6 +25,7 @@ def self.reaper
|
|
23
25
|
|
24
26
|
# expand common shell constructs based on environment variables
|
25
27
|
# this is order-dependent, but Ruby 1.9+ hashes are already order-dependent
|
28
|
+
# This recurses
|
26
29
|
def env_expand(env, opts)
|
27
30
|
env = env.dup
|
28
31
|
if false == opts.delete(:expand)
|
@@ -31,19 +34,40 @@ def env_expand(env, opts)
|
|
31
34
|
end
|
32
35
|
else
|
33
36
|
env.each do |key, val|
|
34
|
-
case val
|
35
|
-
when
|
36
|
-
|
37
|
-
|
38
|
-
tmp = env.dup
|
39
|
-
tmp.delete(key)
|
40
|
-
val = qx(tmp, "echo #{val}", expand: false)
|
41
|
-
env[key] = val.chomp
|
37
|
+
case val = env_expand_i(env, key, val)
|
38
|
+
when Array
|
39
|
+
val.flatten!
|
40
|
+
env[key] = Shellwords.join(val)
|
42
41
|
end
|
43
42
|
end
|
44
43
|
end
|
45
44
|
end
|
46
45
|
|
46
|
+
def env_expand_i(env, key, val)
|
47
|
+
case val
|
48
|
+
when Numeric # stringify numeric values to simplify users' lives
|
49
|
+
env[key] = val.to_s
|
50
|
+
when /[\`\$]/ # perform variable/command expansion
|
51
|
+
tmp = env.dup
|
52
|
+
tmp.delete(key)
|
53
|
+
tmp.each do |k,v|
|
54
|
+
# best effort, this can get wonky
|
55
|
+
tmp[k] = Shellwords.join(v.flatten) if Array === v
|
56
|
+
end
|
57
|
+
val = qx(tmp, "echo #{val}", expand: false)
|
58
|
+
env[key] = val.chomp
|
59
|
+
when Array
|
60
|
+
env[key] = env_expand_ary(env, key, val)
|
61
|
+
else
|
62
|
+
val
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# warning, recursion:
|
67
|
+
def env_expand_ary(env, key, val)
|
68
|
+
val.map { |v| env_expand_i(env.dup, key, v) }
|
69
|
+
end
|
70
|
+
|
47
71
|
# for long-running processes (sox/play/ecasound filters)
|
48
72
|
def dtas_spawn(env, cmd, opts)
|
49
73
|
opts = { close_others: true, pgroup: true }.merge!(opts)
|
@@ -51,7 +75,7 @@ def dtas_spawn(env, cmd, opts)
|
|
51
75
|
|
52
76
|
pid = spawn(env, cmd, opts)
|
53
77
|
warn [ :spawn, pid, cmd ].inspect if $DEBUG
|
54
|
-
@spawn_at =
|
78
|
+
@spawn_at = DTAS.now
|
55
79
|
PIDS[pid] = self
|
56
80
|
pid
|
57
81
|
end
|
@@ -63,12 +87,13 @@ def qx(env, cmd = {}, opts = {})
|
|
63
87
|
cmd, opts = env, cmd
|
64
88
|
env = {}
|
65
89
|
end
|
66
|
-
|
90
|
+
buf = ''
|
91
|
+
r, w = DTAS::Nonblock.pipe
|
67
92
|
opts = opts.merge(out: w)
|
68
93
|
r.binmode
|
69
94
|
no_raise = opts.delete(:no_raise)
|
70
95
|
if err_str = opts.delete(:err_str)
|
71
|
-
re, we =
|
96
|
+
re, we = DTAS::Nonblock.pipe
|
72
97
|
re.binmode
|
73
98
|
opts[:err] = we
|
74
99
|
end
|
@@ -82,12 +107,11 @@ def qx(env, cmd = {}, opts = {})
|
|
82
107
|
begin
|
83
108
|
readable = IO.select(want.keys) or next
|
84
109
|
readable[0].each do |io|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
want.delete(io)
|
110
|
+
case rv = io.read_nonblock(2000, buf, exception: false)
|
111
|
+
when :wait_readable # spurious wakeup, bytes may be zero
|
112
|
+
when nil then want.delete(io)
|
113
|
+
else
|
114
|
+
want[io] << rv
|
91
115
|
end
|
92
116
|
end
|
93
117
|
end until want.empty?
|
data/lib/dtas/sigevent/pipe.rb
CHANGED
@@ -3,11 +3,13 @@
|
|
3
3
|
|
4
4
|
# used in various places for safe wakeups from IO.select via signals
|
5
5
|
# A fallback for non-Linux systems lacking the "sleepy_penguin" RubyGem
|
6
|
+
require_relative 'nonblock'
|
6
7
|
class DTAS::Sigevent # :nodoc:
|
7
8
|
attr_reader :to_io
|
8
9
|
|
9
10
|
def initialize
|
10
|
-
@to_io, @wr =
|
11
|
+
@to_io, @wr = DTAS::Nonblock.pipe
|
12
|
+
@rbuf = ''
|
11
13
|
end
|
12
14
|
|
13
15
|
def signal
|
@@ -15,11 +17,10 @@ def signal
|
|
15
17
|
end
|
16
18
|
|
17
19
|
def readable_iter
|
18
|
-
|
19
|
-
|
20
|
+
case @to_io.read_nonblock(11, @rbuf, exception: false)
|
21
|
+
when :wait_readable then return :wait_readable
|
22
|
+
else
|
20
23
|
yield self, nil # calls DTAS::Process.reaper
|
21
|
-
rescue Errno::EAGAIN
|
22
|
-
return :wait_readable
|
23
24
|
end while true
|
24
25
|
end
|
25
26
|
|
data/lib/dtas/sink.rb
CHANGED
data/lib/dtas/source/splitfx.rb
CHANGED
@@ -17,6 +17,7 @@ class DTAS::Source::SplitFX < DTAS::Source::Sox # :nodoc:
|
|
17
17
|
|
18
18
|
def initialize(sox = DTAS::Source::Sox.new)
|
19
19
|
command_init(SPLITFX_DEFAULTS)
|
20
|
+
@watch_extra = []
|
20
21
|
@sox = sox
|
21
22
|
end
|
22
23
|
|
@@ -65,6 +66,19 @@ def src_spawn(player_format, rg_state, opts)
|
|
65
66
|
e = @env.merge!(player_format.to_env)
|
66
67
|
@sfx.infile_env(e, @sox.infile)
|
67
68
|
|
69
|
+
# watch any scripts or files the command in the YAML file refers to
|
70
|
+
if c = @sfx.command
|
71
|
+
@sfx.expand_cmd(e, c).each do |f|
|
72
|
+
File.readable?(f) and @watch_extra << f
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# allow users to specify explicit depdendencies to watch for edit
|
77
|
+
case extra = @ymlhash['deps']
|
78
|
+
when Array, String
|
79
|
+
@watch_extra.concat(Array(extra))
|
80
|
+
end
|
81
|
+
|
68
82
|
# make sure these are visible to the "current" command...
|
69
83
|
e["TRIMFX"] = trimfx
|
70
84
|
e["RGFX"] = rg_state.effect(self) || nil
|
data/lib/dtas/splitfx.rb
CHANGED
@@ -10,18 +10,28 @@
|
|
10
10
|
# Unlike the stuff for dtas-player, dtas-splitfx is fairly tied to sox
|
11
11
|
# (but we may still pipe to ecasound or anything else)
|
12
12
|
class DTAS::SplitFX # :nodoc:
|
13
|
-
CMD = 'sox "$INFILE" $COMMENTS $OUTFMT
|
14
|
-
'$TRIMFX $FX $RATEFX $DITHERFX'
|
13
|
+
CMD = 'sox "$INFILE" $COMMENTS $OUTFMT $OUTDST $TRIMFX $FX $RATEFX $DITHERFX'
|
15
14
|
include DTAS::Process
|
16
15
|
include DTAS::XS
|
17
|
-
attr_reader :infile, :env
|
16
|
+
attr_reader :infile, :env, :command
|
17
|
+
|
18
|
+
# for --trim on the command-line
|
19
|
+
class UTrim # :nodoc:
|
20
|
+
attr_reader :env, :comments
|
21
|
+
def initialize(trim_arg, env, comments)
|
22
|
+
@env = env.merge("TRIMFX" => "trim #{trim_arg}")
|
23
|
+
@comments = comments.merge('TRACKNUMBER' => '000')
|
24
|
+
end
|
25
|
+
end
|
18
26
|
|
27
|
+
# declare section to skip
|
19
28
|
class Skip < Struct.new(:tbeg) # :nodoc:
|
20
29
|
def commit(_)
|
21
30
|
# noop
|
22
31
|
end
|
23
32
|
end
|
24
33
|
|
34
|
+
# a standard "track" for splitfx
|
25
35
|
class T < Struct.new(:env, :comments, :tbeg, :fade_in, :fade_out) # :nodoc:
|
26
36
|
def commit(advance_track_samples)
|
27
37
|
tlen = advance_track_samples - tbeg
|
@@ -69,21 +79,6 @@ def initialize
|
|
69
79
|
"channels" => 2,
|
70
80
|
},
|
71
81
|
},
|
72
|
-
"opusenc" => {
|
73
|
-
"command" => 'sox "$INFILE" $COMMENTS $OUTFMT - ' \
|
74
|
-
'$TRIMFX $FX $RATEFX $DITHERFX | opusenc --music ' \
|
75
|
-
'--raw-bits $BITS_PER_SAMPLE ' \
|
76
|
-
'$OPUSENC_BITRATE --raw-rate $RATE --raw-chan $CHANNELS ' \
|
77
|
-
'--raw-endianness $ENDIAN_OPUSENC ' \
|
78
|
-
'$OPUSENC_COMMENTS ' \
|
79
|
-
'- $OUTDIR$TRACKNUMBER.opus',
|
80
|
-
"format" => {
|
81
|
-
"bits" => 16,
|
82
|
-
"rate" => 48000,
|
83
|
-
"type" => "s16",
|
84
|
-
"channels" => 2,
|
85
|
-
},
|
86
|
-
},
|
87
82
|
}
|
88
83
|
@tracks = []
|
89
84
|
@infmt = nil # wait until input is assigned
|
@@ -159,13 +154,15 @@ def load_input!(hash)
|
|
159
154
|
def generic_target(target = "flac")
|
160
155
|
outfmt = @infmt.dup
|
161
156
|
outfmt.type = target
|
162
|
-
outfmt.bits = @bits if @bits
|
163
|
-
outfmt.rate = @rate if @rate
|
164
157
|
{ "command" => CMD, "format" => outfmt }
|
165
158
|
end
|
166
159
|
|
167
160
|
def splitfx_spawn(target, t, opts)
|
168
|
-
|
161
|
+
if tgt = @targets[target]
|
162
|
+
target = tgt
|
163
|
+
else
|
164
|
+
target = generic_target(target)
|
165
|
+
end
|
169
166
|
outfmt = target["format"]
|
170
167
|
|
171
168
|
# default format:
|
@@ -173,6 +170,14 @@ def splitfx_spawn(target, t, opts)
|
|
173
170
|
outfmt = @infmt.dup
|
174
171
|
outfmt.type = "flac"
|
175
172
|
end
|
173
|
+
|
174
|
+
outfmt.bits = @bits if @bits
|
175
|
+
outfmt.rate = @rate if @rate
|
176
|
+
|
177
|
+
# player commands will use SOXFMT by default, so we must output that
|
178
|
+
# as a self-describing format to the actual encoding instances
|
179
|
+
player_cmd = @command
|
180
|
+
suffix = outfmt.type
|
176
181
|
env = outfmt.to_env
|
177
182
|
|
178
183
|
# set very high quality resampling if using 24-bit or higher output
|
@@ -204,8 +209,9 @@ def splitfx_spawn(target, t, opts)
|
|
204
209
|
outarg = outfmt.to_sox_arg
|
205
210
|
outarg << "-C#@compression" if @compression
|
206
211
|
env["OUTFMT"] = xs(outarg)
|
207
|
-
env["SUFFIX"] =
|
212
|
+
env["SUFFIX"] = suffix
|
208
213
|
env["OUTDIR"] = @outdir ? "#@outdir/".squeeze('/') : ''
|
214
|
+
env["OUTDST"] = opts[:sox_pipe] ? "-p" : "$OUTDIR$TRACKNUMBER.$SUFFIX"
|
209
215
|
env.merge!(t.env)
|
210
216
|
|
211
217
|
command = target["command"]
|
@@ -215,22 +221,25 @@ def splitfx_spawn(target, t, opts)
|
|
215
221
|
# already takes those into account. In other words, use our
|
216
222
|
# target-specific commands like a dtas-player sink:
|
217
223
|
# @command | (INFILE= FX= TRIMFX=; target['command'])
|
218
|
-
if player_cmd
|
219
|
-
sub_env = {
|
220
|
-
|
224
|
+
if player_cmd
|
225
|
+
sub_env = {
|
226
|
+
'INFILE' => '-p',
|
227
|
+
'FX' => '',
|
228
|
+
'TRIMFX' => '',
|
229
|
+
'SOXFMT' => ''
|
230
|
+
}
|
231
|
+
env['SOXFMT'] = '-tsox'
|
232
|
+
sub_env['OUTFMT'] = env.delete('OUTFMT')
|
233
|
+
sub_env_s = sub_env.inject("") { |s,(k,v)| s << "#{k}=\"#{v}\" " }
|
234
|
+
show_cmd = [ expand_cmd(env, player_cmd), '|', '(', "#{sub_env_s};",
|
235
|
+
expand_cmd(env.merge(sub_env), command), ')' ].flatten
|
221
236
|
command = "#{player_cmd} | (#{sub_env_s}; #{command})"
|
222
|
-
show_cmd = [ _expand_cmd(env, player_cmd), '|', '(', "#{sub_env_s};",
|
223
|
-
_expand_cmd(env.merge(sub_env), command), ')' ].flatten
|
224
237
|
else
|
225
|
-
show_cmd =
|
238
|
+
show_cmd = expand_cmd(env, command)
|
226
239
|
end
|
227
240
|
|
228
|
-
|
229
|
-
if opts[:dryrun]
|
230
|
-
command = echo
|
231
|
-
else
|
232
|
-
system(echo) unless opts[:silent]
|
233
|
-
end
|
241
|
+
@out.puts(show_cmd.join(' ')) unless opts[:silent]
|
242
|
+
command = 'true' if opts[:dryrun] # still gotta fork
|
234
243
|
|
235
244
|
# pgroup: false so Ctrl-C on command-line will immediately stop everything
|
236
245
|
[ dtas_spawn(env, command, pgroup: false), comments ]
|
@@ -339,11 +348,18 @@ def run(target, opts = {})
|
|
339
348
|
@compression = opts[:compression]
|
340
349
|
@rate = opts[:rate]
|
341
350
|
@bits = opts[:bits]
|
351
|
+
trim = opts[:trim] and @tracks = [ UTrim.new(trim, @env, @comments) ]
|
342
352
|
|
343
353
|
fails = []
|
344
354
|
tracks = @tracks.dup
|
345
355
|
pids = {}
|
346
356
|
jobs = opts[:jobs] || tracks.size # jobs == nil => everything at once
|
357
|
+
if opts[:sox_pipe]
|
358
|
+
jobs = 1
|
359
|
+
@out = $stderr
|
360
|
+
else
|
361
|
+
@out = $stdout
|
362
|
+
end
|
347
363
|
jobs.times.each do
|
348
364
|
t = tracks.shift or break
|
349
365
|
pid, tmp = splitfx_spawn(target, t, opts)
|
@@ -358,7 +374,7 @@ def run(target, opts = {})
|
|
358
374
|
pid, tmp = splitfx_spawn(target, t, opts)
|
359
375
|
pids[pid] = [ t, tmp ]
|
360
376
|
end
|
361
|
-
puts "DONE #{done[0].inspect}" if $DEBUG
|
377
|
+
@out.puts "DONE #{done[0].inspect}" if $DEBUG
|
362
378
|
done[1].close!
|
363
379
|
else
|
364
380
|
fails << [ t, status ]
|
@@ -385,7 +401,7 @@ def infile_env(env, infile)
|
|
385
401
|
env["INBASE"] = xs(base)
|
386
402
|
end
|
387
403
|
|
388
|
-
def
|
404
|
+
def expand_cmd(env, command) # for display purposes only
|
389
405
|
Shellwords.split(command).map do |arg|
|
390
406
|
qx(env, "printf %s \"#{arg}\"")
|
391
407
|
end
|
data/lib/dtas/track.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Copyright (C) 2015 all contributors <dtas-all@nongnu.org>
|
2
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
3
|
+
require_relative '../dtas'
|
4
|
+
|
5
|
+
class DTAS::Track
|
6
|
+
attr_reader :track_id
|
7
|
+
attr_reader :to_path
|
8
|
+
|
9
|
+
def initialize(track_id, path)
|
10
|
+
@track_id = track_id
|
11
|
+
@to_path = path
|
12
|
+
end
|
13
|
+
end
|
data/lib/dtas/tracklist.rb
CHANGED
@@ -2,97 +2,157 @@
|
|
2
2
|
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
3
3
|
require_relative '../dtas'
|
4
4
|
require_relative 'serialize'
|
5
|
+
require_relative 'track'
|
5
6
|
|
6
7
|
# the a tracklist object for -player
|
7
8
|
# this is inspired by the MPRIS 2.0 TrackList spec
|
8
9
|
class DTAS::Tracklist # :nodoc:
|
9
10
|
include DTAS::Serialize
|
10
11
|
attr_accessor :repeat # true, false, 1
|
12
|
+
attr_reader :shuffle # false or shuffled @list
|
13
|
+
attr_accessor :max # integer
|
11
14
|
|
12
|
-
SIVS = %w(list pos repeat)
|
13
15
|
TL_DEFAULTS = {
|
14
|
-
|
15
|
-
|
16
|
-
|
16
|
+
'pos' => -1,
|
17
|
+
'repeat' => false,
|
18
|
+
'max' => 20_000,
|
17
19
|
}
|
20
|
+
SIVS = TL_DEFAULTS.keys
|
18
21
|
|
19
22
|
def self.load(hash)
|
20
23
|
obj = new
|
21
24
|
obj.instance_eval do
|
22
|
-
list = hash[
|
23
|
-
|
24
|
-
|
25
|
+
list = hash['list'] and @list.replace(list.map { |s| new_track(s) })
|
26
|
+
SIVS.each do |k|
|
27
|
+
instance_variable_set("@#{k}", hash[k] || TL_DEFAULTS[k])
|
28
|
+
end
|
29
|
+
|
30
|
+
# n.b.: we don't check @list.size against max here in case people
|
31
|
+
# are migrating
|
32
|
+
|
33
|
+
if hash['shuffle']
|
34
|
+
@shuffle = @list.shuffle
|
35
|
+
@pos = _idx_of(@shuffle, @list[@pos].track_id) if @pos >= 0
|
36
|
+
end
|
25
37
|
end
|
26
38
|
obj
|
27
39
|
end
|
28
40
|
|
29
|
-
def to_hsh
|
30
|
-
ivars_to_hash(SIVS)
|
41
|
+
def to_hsh(full_list = true)
|
42
|
+
h = ivars_to_hash(SIVS)
|
43
|
+
h.delete_if { |k,v| TL_DEFAULTS[k] == v }
|
44
|
+
unless @list.empty?
|
45
|
+
if full_list
|
46
|
+
h['list'] = @list.map(&:to_path)
|
47
|
+
else
|
48
|
+
h['size'] = @list.size
|
49
|
+
end
|
50
|
+
end
|
51
|
+
if @shuffle
|
52
|
+
h['shuffle'] = true
|
53
|
+
h['pos'] = _idx_of(@list, @shuffle[@pos].track_id) if @pos >= 0
|
54
|
+
end
|
55
|
+
h
|
31
56
|
end
|
32
57
|
|
33
58
|
def initialize
|
34
59
|
TL_DEFAULTS.each { |k,v| instance_variable_set("@#{k}", v) }
|
35
60
|
@list = []
|
36
61
|
@goto_off = @goto_pos = nil
|
62
|
+
@track_nr = 0
|
63
|
+
@shuffle = false
|
64
|
+
end
|
65
|
+
|
66
|
+
def new_track(path)
|
67
|
+
n = @track_nr += 1
|
68
|
+
|
69
|
+
# nobody needs a billion tracks in their tracklist, right?
|
70
|
+
# avoid promoting to Bignum on 32-bit
|
71
|
+
@track_nr = n = 1 if n >= 0x3fffffff
|
72
|
+
|
73
|
+
DTAS::Track.new(n, path)
|
37
74
|
end
|
38
75
|
|
39
76
|
def reset
|
40
77
|
@goto_off = @goto_pos = nil
|
41
78
|
@pos = TL_DEFAULTS["pos"]
|
79
|
+
@shuffle.shuffle! if @shuffle
|
42
80
|
end
|
43
81
|
|
44
|
-
def
|
45
|
-
|
82
|
+
def get_tracks(track_ids)
|
83
|
+
want = {}
|
84
|
+
track_ids.each { |i| want[i] = i }
|
85
|
+
rv = []
|
86
|
+
@list.each do |t|
|
87
|
+
i = want[t.track_id] and rv << [ i, t.to_path ]
|
88
|
+
end
|
89
|
+
rv
|
46
90
|
end
|
47
91
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
by_track_id = {}
|
52
|
-
@list.each_with_index { |t,i| by_track_id[t.object_id] = i }
|
53
|
-
by_track_id
|
92
|
+
def _update_pos(pos, prev, list)
|
93
|
+
old = prev[pos]
|
94
|
+
_idx_of(list, old.track_id)
|
54
95
|
end
|
55
96
|
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
97
|
+
def shuffle=(bool)
|
98
|
+
prev = @shuffle
|
99
|
+
if bool
|
100
|
+
list = @shuffle = (prev ||= @list).shuffle
|
101
|
+
elsif prev
|
102
|
+
@shuffle = false
|
103
|
+
list = @list
|
104
|
+
else
|
105
|
+
return false
|
62
106
|
end
|
107
|
+
@pos = _update_pos(@pos, prev, list) if @pos >= 0
|
108
|
+
@goto_pos = _update_pos(@goto_pos, prev, list) if @goto_pos
|
63
109
|
end
|
64
110
|
|
65
111
|
def tracks
|
66
|
-
@list.map(&:
|
112
|
+
@list.map(&:track_id)
|
67
113
|
end
|
68
114
|
|
69
115
|
def advance_track(repeat_ok = true)
|
70
|
-
|
116
|
+
cur = @shuffle || @list
|
117
|
+
return if cur.empty?
|
71
118
|
# @repeat == 1 for single track repeat
|
72
119
|
repeat = repeat_ok ? @repeat : false
|
73
120
|
next_pos = @goto_pos || @pos + (repeat == 1 ? 0 : 1)
|
74
121
|
next_off = @goto_off # nil by default
|
75
122
|
@goto_pos = @goto_off = nil
|
76
|
-
if
|
123
|
+
if cur[next_pos]
|
77
124
|
@pos = next_pos
|
78
125
|
elsif repeat
|
79
126
|
next_pos = @pos = 0
|
80
127
|
else
|
81
128
|
return
|
82
129
|
end
|
83
|
-
[
|
130
|
+
[ cur[next_pos].to_path, next_off ]
|
84
131
|
end
|
85
132
|
|
86
133
|
def cur_track
|
87
|
-
@pos >= 0 ? @list[@pos] : nil
|
134
|
+
@pos >= 0 ? (@shuffle || @list)[@pos] : nil
|
88
135
|
end
|
89
136
|
|
90
137
|
def add_track(track, after_track_id = nil, set_as_current = false)
|
138
|
+
return false if @list.size >= @max
|
139
|
+
|
140
|
+
track = new_track(track)
|
91
141
|
if after_track_id
|
92
|
-
|
93
|
-
|
94
|
-
|
142
|
+
idx = _idx_of(@list, after_track_id) or
|
143
|
+
raise ArgumentError, 'after_track_id invalid'
|
144
|
+
if @shuffle
|
145
|
+
_idx_of(@shuffle, after_track_id) or
|
146
|
+
raise ArgumentError, 'after_track_id invalid'
|
147
|
+
end
|
95
148
|
@list[idx, 1] = [ @list[idx], track ]
|
149
|
+
|
150
|
+
# add into random position if shuffling
|
151
|
+
if @shuffle
|
152
|
+
idx = rand(@shuffle.size)
|
153
|
+
@shuffle[idx, 1] = [ @shuffle[idx], track ]
|
154
|
+
end
|
155
|
+
|
96
156
|
if set_as_current
|
97
157
|
@pos = idx + 1
|
98
158
|
else
|
@@ -100,33 +160,57 @@ def add_track(track, after_track_id = nil, set_as_current = false)
|
|
100
160
|
end
|
101
161
|
else # nil = first_track
|
102
162
|
@list.unshift(track)
|
103
|
-
|
104
|
-
|
163
|
+
|
164
|
+
if @shuffle
|
165
|
+
if @shuffle.empty?
|
166
|
+
@shuffle << track
|
167
|
+
@pos = 0 if set_as_current
|
168
|
+
else
|
169
|
+
idx = rand(@shuffle.size)
|
170
|
+
@shuffle[idx, 1] = [ @shuffle[idx], track ]
|
171
|
+
@pos = idx + 1 if set_as_current
|
172
|
+
end
|
105
173
|
else
|
106
|
-
|
174
|
+
if set_as_current
|
175
|
+
@pos = 0
|
176
|
+
else
|
177
|
+
@pos += 1 if @pos >= 0
|
178
|
+
end
|
107
179
|
end
|
108
180
|
end
|
109
|
-
track.
|
181
|
+
track.track_id
|
182
|
+
end
|
183
|
+
|
184
|
+
def _idx_of(list, track_id)
|
185
|
+
list.index { |t| t.track_id == track_id }
|
110
186
|
end
|
111
187
|
|
112
188
|
def remove_track(track_id)
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
189
|
+
idx = _idx_of(@list, track_id) or return false
|
190
|
+
if @shuffle
|
191
|
+
si = _idx_of(@shuffle, track_id) or return false
|
192
|
+
@shuffle.delete_at(si)
|
193
|
+
end
|
194
|
+
track = @list.delete_at(idx)
|
117
195
|
len = @list.size
|
118
196
|
if @pos >= len
|
119
197
|
@pos = len == 0 ? TL_DEFAULTS["pos"] : len
|
120
198
|
end
|
121
199
|
@goto_pos = @goto_pos = nil # TODO: reposition?
|
122
|
-
|
200
|
+
track.to_path
|
201
|
+
end
|
202
|
+
|
203
|
+
def clear
|
204
|
+
@list.clear
|
205
|
+
@shuffle.clear if @shuffle
|
206
|
+
reset
|
123
207
|
end
|
124
208
|
|
125
209
|
def go_to(track_id, offset_hhmmss = nil)
|
126
|
-
|
127
|
-
if idx =
|
210
|
+
list = @shuffle || @list
|
211
|
+
if idx = _idx_of(list, track_id)
|
128
212
|
@goto_off = offset_hhmmss
|
129
|
-
return
|
213
|
+
return list[@goto_pos = idx].to_path
|
130
214
|
end
|
131
215
|
@goto_pos = nil
|
132
216
|
# noop if track_id is invalid
|
@@ -141,4 +225,25 @@ def previous!
|
|
141
225
|
end
|
142
226
|
@goto_pos = prev_idx
|
143
227
|
end
|
228
|
+
|
229
|
+
def swap(a_id, b_id)
|
230
|
+
ok = { a_id => a_idx = [], b_id => b_idx = [] }
|
231
|
+
@list.each_with_index do |t,i|
|
232
|
+
ary = ok.delete(t.track_id) or next
|
233
|
+
ary[0] = i
|
234
|
+
break if ok.empty?
|
235
|
+
end
|
236
|
+
a_idx = a_idx[0] or return
|
237
|
+
b_idx = b_idx[0] or return
|
238
|
+
@list[a_idx], @list[b_idx] = @list[b_idx], @list[a_idx]
|
239
|
+
unless @shuffle
|
240
|
+
[ :@goto_pos, :@pos ].each do |v|
|
241
|
+
case instance_variable_get(v)
|
242
|
+
when a_idx then instance_variable_set(v, b_idx)
|
243
|
+
when b_idx then instance_variable_set(v, a_idx)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
true
|
248
|
+
end
|
144
249
|
end
|