dtas 0.3.0 → 0.4.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/Documentation/GNUmakefile +2 -0
- data/Documentation/dtas-player_protocol.txt +28 -1
- data/Documentation/dtas-splitfx.txt +167 -0
- data/Documentation/dtas-tl.txt +77 -0
- data/GIT-VERSION-GEN +2 -1
- data/GNUmakefile +1 -1
- data/README +2 -1
- data/Rakefile +7 -0
- data/bin/dtas-console +11 -1
- data/bin/dtas-splitfx +40 -0
- data/bin/dtas-tl +73 -0
- data/examples/README +3 -0
- data/examples/splitfx.sample.yml +19 -0
- data/lib/dtas/format.rb +11 -0
- data/lib/dtas/pipe.rb +0 -3
- data/lib/dtas/player.rb +38 -11
- data/lib/dtas/player/client_handler.rb +94 -7
- data/lib/dtas/process.rb +25 -3
- data/lib/dtas/sink.rb +0 -3
- data/lib/dtas/source/sox.rb +2 -1
- data/lib/dtas/splitfx.rb +342 -0
- data/lib/dtas/tracklist.rb +130 -0
- data/test/helper.rb +14 -1
- data/test/player_integration.rb +5 -3
- data/test/test_buffer.rb +4 -2
- data/test/test_env.rb +55 -0
- data/test/test_format.rb +1 -1
- data/test/test_format_change.rb +1 -1
- data/test/test_player.rb +1 -1
- data/test/test_player_client_handler.rb +1 -1
- data/test/test_player_integration.rb +3 -2
- data/test/test_process.rb +1 -1
- data/test/test_rg_integration.rb +4 -5
- data/test/test_rg_state.rb +1 -1
- data/test/test_sink.rb +1 -1
- data/test/test_sink_pipe_size.rb +1 -1
- data/test/test_sink_tee_integration.rb +1 -1
- data/test/test_source_av.rb +1 -1
- data/test/test_source_sox.rb +1 -1
- data/test/test_splitfx.rb +79 -0
- data/test/test_tracklist.rb +76 -0
- data/test/test_unixserver.rb +1 -1
- data/test/test_util.rb +1 -1
- metadata +23 -3
data/lib/dtas/sink.rb
CHANGED
@@ -7,7 +7,6 @@ require_relative 'process'
|
|
7
7
|
require_relative 'command'
|
8
8
|
require_relative 'format'
|
9
9
|
require_relative 'serialize'
|
10
|
-
require_relative 'writable_iter'
|
11
10
|
|
12
11
|
# this is a sink (endpoint, audio enters but never leaves)
|
13
12
|
class DTAS::Sink # :nodoc:
|
@@ -20,7 +19,6 @@ class DTAS::Sink # :nodoc:
|
|
20
19
|
include DTAS::Command
|
21
20
|
include DTAS::Process
|
22
21
|
include DTAS::Serialize
|
23
|
-
include DTAS::WritableIter
|
24
22
|
|
25
23
|
SINK_DEFAULTS = COMMAND_DEFAULTS.merge({
|
26
24
|
"name" => nil, # order matters, this is first
|
@@ -39,7 +37,6 @@ class DTAS::Sink # :nodoc:
|
|
39
37
|
|
40
38
|
def initialize
|
41
39
|
command_init(SINK_DEFAULTS)
|
42
|
-
writable_iter_init
|
43
40
|
@sink = self
|
44
41
|
end
|
45
42
|
|
data/lib/dtas/source/sox.rb
CHANGED
@@ -11,6 +11,7 @@ class DTAS::Source::Sox # :nodoc:
|
|
11
11
|
|
12
12
|
include DTAS::Source::File
|
13
13
|
include DTAS::XS
|
14
|
+
extend DTAS::XS
|
14
15
|
|
15
16
|
SOX_DEFAULTS = COMMAND_DEFAULTS.merge(
|
16
17
|
"command" => 'exec sox "$INFILE" $SOXFMT - $TRIMFX $RGFX',
|
@@ -40,7 +41,7 @@ class DTAS::Source::Sox # :nodoc:
|
|
40
41
|
def try(infile, offset = nil)
|
41
42
|
err = ""
|
42
43
|
cmd = %W(soxi -s #{infile})
|
43
|
-
s = qx(@env, cmd, err_str: err, no_raise: true)
|
44
|
+
s = qx(@env.dup, cmd, err_str: err, no_raise: true)
|
44
45
|
return if err =~ /soxi FAIL formats:/
|
45
46
|
self.class.try_to_fail_harder(infile, s, cmd) or return
|
46
47
|
source_file_dup(infile, offset)
|
data/lib/dtas/splitfx.rb
ADDED
@@ -0,0 +1,342 @@
|
|
1
|
+
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
|
2
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
3
|
+
# Unlike the stuff for dtas-player, dtas-splitfx is fairly tied to sox
|
4
|
+
# (but we may still pipe to ecasound or anything else)
|
5
|
+
require_relative '../dtas'
|
6
|
+
require_relative 'format'
|
7
|
+
require_relative 'process'
|
8
|
+
require_relative 'xs'
|
9
|
+
require 'tempfile'
|
10
|
+
class DTAS::SplitFX # :nodoc:
|
11
|
+
CMD = 'sox "$INFILE" $COMMENTS $OUTFMT "$TRACKNUMBER.$SUFFIX" '\
|
12
|
+
'$TRIMFX $RATEFX $DITHERFX'
|
13
|
+
include DTAS::Process
|
14
|
+
include DTAS::XS
|
15
|
+
|
16
|
+
class Skip < Struct.new(:tstart) # :nodoc:
|
17
|
+
def commit(_)
|
18
|
+
# noop
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class T < Struct.new(:env, :comments, :tstart, :fade_in, :fade_out) # :nodoc:
|
23
|
+
def commit(advance_track_samples)
|
24
|
+
tlen = advance_track_samples - tstart
|
25
|
+
trimfx = "trim #{tstart}s #{tlen}s"
|
26
|
+
if fade_in
|
27
|
+
trimfx << " #{fade_in}"
|
28
|
+
end
|
29
|
+
if fade_out
|
30
|
+
tmp = fade_out.dup
|
31
|
+
fade_out_len = tmp.pop or
|
32
|
+
raise ArgumentError, "fade_out needs a time value"
|
33
|
+
fade_type = tmp.pop # may be nil
|
34
|
+
fade = " fade #{fade_type} 0 #{tlen}s #{fade_out_len}"
|
35
|
+
trimfx << fade
|
36
|
+
end
|
37
|
+
env["TRIMFX"] = trimfx
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# vars:
|
42
|
+
# $CHANNELS (input)
|
43
|
+
# $BITS_PER_SAMPLE (input)
|
44
|
+
def initialize
|
45
|
+
@env = {}
|
46
|
+
@comments = {}
|
47
|
+
@track_start = 1
|
48
|
+
@track_zpad = true
|
49
|
+
@t2s = method(:t2s)
|
50
|
+
@infile = nil
|
51
|
+
@targets = {
|
52
|
+
"flac-cdda" => {
|
53
|
+
"command" => CMD,
|
54
|
+
"format" => {
|
55
|
+
"bits" => 16,
|
56
|
+
"rate" => 44100,
|
57
|
+
"type" => "flac",
|
58
|
+
"channels" => 2,
|
59
|
+
},
|
60
|
+
},
|
61
|
+
"opusenc" => {
|
62
|
+
"command" => 'sox "$INFILE" $COMMENTS $OUTFMT - ' \
|
63
|
+
'$TRIMFX $RATEFX $DITHERFX | opusenc --music ' \
|
64
|
+
'--raw-bits $BITS_PER_SAMPLE ' \
|
65
|
+
'$OPUSENC_BITRATE --raw-rate $RATE --raw-chan $CHANNELS ' \
|
66
|
+
'--raw-endianness $ENDIAN_OPUSENC ' \
|
67
|
+
'$OPUSENC_COMMENTS ' \
|
68
|
+
'- $TRACKNUMBER.opus',
|
69
|
+
"format" => {
|
70
|
+
"bits" => 16,
|
71
|
+
"rate" => 48000,
|
72
|
+
"type" => "s16",
|
73
|
+
"channels" => 2,
|
74
|
+
},
|
75
|
+
},
|
76
|
+
}
|
77
|
+
@tracks = []
|
78
|
+
@infmt = nil # wait until input is assigned
|
79
|
+
end
|
80
|
+
|
81
|
+
def _bool(hash, key)
|
82
|
+
val = hash[key]
|
83
|
+
case val
|
84
|
+
when false, true then yield val
|
85
|
+
when nil # ignore
|
86
|
+
else
|
87
|
+
raise TypeError, "'#{key}' must be boolean (true or false)"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def import(hash, overrides = {})
|
92
|
+
# merge overrides from the command-line
|
93
|
+
overrides.each do |k,v|
|
94
|
+
case v
|
95
|
+
when Hash then hash[k] = (hash[k] || {}).merge(v)
|
96
|
+
else
|
97
|
+
hash[k] = v
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
hash = hash.merge(overrides)
|
102
|
+
case v = hash["track_zpad"]
|
103
|
+
when Integer then @track_zpad = val
|
104
|
+
else
|
105
|
+
_bool(hash, "track_zpad") { |val| @track_zpad = val }
|
106
|
+
end
|
107
|
+
|
108
|
+
_bool(hash, "cdda_align") { |val| @t2s = method(val ? :t2s : :t2s_cdda) }
|
109
|
+
|
110
|
+
case v = hash["track_start"]
|
111
|
+
when Integer then @track_start = v
|
112
|
+
when nil
|
113
|
+
else
|
114
|
+
raise TypeError, "'track_start' must be an integer"
|
115
|
+
end
|
116
|
+
|
117
|
+
%w(comments env targets).each do |key|
|
118
|
+
case val = hash[key]
|
119
|
+
when Hash then instance_variable_get("@#{key}").merge!(val)
|
120
|
+
when nil
|
121
|
+
else
|
122
|
+
raise TypeError, "'#{key}' must be a hash"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
@targets.each_value do |thsh|
|
127
|
+
case tfmt = thsh["format"]
|
128
|
+
when Hash
|
129
|
+
thsh["format"] = DTAS::Format.load(tfmt) unless tfmt.empty?
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
load_input!(hash)
|
134
|
+
load_tracks!(hash)
|
135
|
+
end
|
136
|
+
|
137
|
+
# FIXME: duplicate from dtas/source/sox
|
138
|
+
def precision
|
139
|
+
qx(@env, %W(soxi -p #@infile), err: "/dev/null").to_i # sox.git f4562efd0aa3
|
140
|
+
rescue # fallback to parsing the whole output
|
141
|
+
s = qx(@env, %W(soxi #@infile), err: "/dev/null")
|
142
|
+
s =~ /Precision\s+:\s*(\d+)-bit/n
|
143
|
+
v = $1.to_i
|
144
|
+
return v if v > 0
|
145
|
+
raise TypeError, "could not determine precision for #@infile"
|
146
|
+
end
|
147
|
+
|
148
|
+
def load_input!(hash)
|
149
|
+
@infile = hash["infile"] or raise ArgumentError, "'infile' not specified"
|
150
|
+
if infmt = hash["infmt"] # rarely needed
|
151
|
+
@infmt = DTAS::Format.load(infmt)
|
152
|
+
else # likely
|
153
|
+
@infmt = DTAS::Format.new
|
154
|
+
@infmt.channels = qx(@env, %W(soxi -c #@infile)).to_i
|
155
|
+
@infmt.rate = qx(@env, %W(soxi -r #@infile)).to_i
|
156
|
+
@infmt.bits ||= precision
|
157
|
+
# we don't care for type
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def generic_target(target = "flac")
|
162
|
+
outfmt = @infmt.dup
|
163
|
+
outfmt.type = target
|
164
|
+
{ "command" => CMD, "format" => outfmt }
|
165
|
+
end
|
166
|
+
|
167
|
+
def spawn(target, t, opts)
|
168
|
+
target = @targets[target] || generic_target(target)
|
169
|
+
outfmt = target["format"]
|
170
|
+
env = outfmt.to_env
|
171
|
+
|
172
|
+
# set very high quality resampling if using 24-bit or higher output
|
173
|
+
if outfmt.rate != @infmt.rate
|
174
|
+
if outfmt.bits
|
175
|
+
# set very-high resampling quality for 24-bit outputs
|
176
|
+
quality = "-v" if outfmt.bits >= 24
|
177
|
+
else
|
178
|
+
# assume output bits matches input bits
|
179
|
+
quality = "-v" if @infmt.bits >= 24
|
180
|
+
end
|
181
|
+
env["RATEFX"] = "rate #{quality} #{outfmt.rate}"
|
182
|
+
end
|
183
|
+
|
184
|
+
# add noise-shaped dither for 16-bit (sox manual seems to recommend this)
|
185
|
+
outfmt.bits && outfmt.bits <= 16 and env["DITHERFX"] = "dither -s"
|
186
|
+
comments = Tempfile.new(%W(dtas-splitfx-#{t.comments["TRACKNUMBER"]} .txt))
|
187
|
+
comments.sync = true
|
188
|
+
t.comments.each do |k,v|
|
189
|
+
env[k] = v.to_s
|
190
|
+
comments.puts("#{k}=#{v}")
|
191
|
+
end
|
192
|
+
env["COMMENTS"] = "--comment-file=#{comments.path}"
|
193
|
+
env["INFILE"] = @infile
|
194
|
+
env["OUTFMT"] = xs(outfmt.to_sox_arg)
|
195
|
+
env["SUFFIX"] = outfmt.type
|
196
|
+
env.merge!(t.env)
|
197
|
+
|
198
|
+
command = target["command"]
|
199
|
+
tmp = Shellwords.split(command).map do |arg|
|
200
|
+
qx(env, "printf %s \"#{arg}\"")
|
201
|
+
end
|
202
|
+
echo = "echo #{xs(tmp)}"
|
203
|
+
if opts[:dryrun]
|
204
|
+
command = echo
|
205
|
+
else
|
206
|
+
system(echo) unless opts[:silent]
|
207
|
+
end
|
208
|
+
|
209
|
+
# pgroup: false so Ctrl-C on command-line will immediately stop everything
|
210
|
+
[ dtas_spawn(env, command, pgroup: false), comments ]
|
211
|
+
end
|
212
|
+
|
213
|
+
def load_tracks!(hash)
|
214
|
+
tracks = hash["tracks"] or raise ArgumentError, "'tracks' not specified"
|
215
|
+
tracks.each { |line| parse_track(Shellwords.split(line)) }
|
216
|
+
|
217
|
+
fmt = "%d"
|
218
|
+
case @track_zpad
|
219
|
+
when true
|
220
|
+
max = @track_start - 1 + @tracks.size
|
221
|
+
fmt = "%0#{max.to_s.size}d"
|
222
|
+
when Integer
|
223
|
+
fmt = "%0#{@track_zpad}d"
|
224
|
+
else
|
225
|
+
fmt = "%d"
|
226
|
+
end
|
227
|
+
nr = @track_start
|
228
|
+
@tracks.delete_if do |t|
|
229
|
+
case t
|
230
|
+
when Skip
|
231
|
+
true
|
232
|
+
else
|
233
|
+
t.comments["TRACKNUMBER"] = sprintf(fmt, nr)
|
234
|
+
nr += 1
|
235
|
+
false
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# argv:
|
241
|
+
# [ 't', '0:05', 'track one', 'fade_in=t 4', '.comment=blah' ]
|
242
|
+
# [ 'stop', '1:00' ]
|
243
|
+
def parse_track(argv)
|
244
|
+
case cmd = argv.shift
|
245
|
+
when "t"
|
246
|
+
start_time = argv.shift
|
247
|
+
title = argv.shift
|
248
|
+
t = T.new
|
249
|
+
t.tstart = @t2s.call(start_time)
|
250
|
+
t.comments = @comments.dup
|
251
|
+
t.comments["TITLE"] = title
|
252
|
+
t.env = @env.dup
|
253
|
+
|
254
|
+
argv.each do |arg|
|
255
|
+
case arg
|
256
|
+
when %r{\Afade_in=(.+)\z}
|
257
|
+
# generate fade-in effect
|
258
|
+
# $1 = "t 4" => "fade t 4 0 0"
|
259
|
+
t.fade_in = "fade #$1 0 0"
|
260
|
+
when %r{\Afade_out=(.+)\z} # $1 = "t 4" or just "4"
|
261
|
+
t.fade_out = $1.split(/\s+/)
|
262
|
+
when %r{\A\.(\w+)=(.+)\z} then t.comments[$1] = $2
|
263
|
+
else
|
264
|
+
raise ArgumentError, "unrecognized arg(s): #{xs(argv)}"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
prev = @tracks.last and prev.commit(t.tstart)
|
269
|
+
@tracks << t
|
270
|
+
when "skip"
|
271
|
+
stop_time = argv.shift
|
272
|
+
argv.empty? or raise ArgumentError, "skip does not take extra args"
|
273
|
+
s = Skip.new
|
274
|
+
s.tstart = @t2s.call(stop_time)
|
275
|
+
# s.comments = {}
|
276
|
+
# s.env = {}
|
277
|
+
prev = @tracks.last or raise ArgumentError, "no tracks to skip"
|
278
|
+
prev.commit(s.tstart)
|
279
|
+
@tracks << s
|
280
|
+
when "stop"
|
281
|
+
stop_time = argv.shift
|
282
|
+
argv.empty? or raise ArgumentError, "stop does not take extra args"
|
283
|
+
samples = @t2s.call(stop_time)
|
284
|
+
prev = @tracks.last and prev.commit(samples)
|
285
|
+
else
|
286
|
+
raise ArgumentError, "unknown command: #{xs(Array(cmd))}"
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# like t2s, but align to CDDA sectors (75 frames per second)
|
291
|
+
def t2s_cdda(time)
|
292
|
+
time = time.dup
|
293
|
+
frac = 0
|
294
|
+
|
295
|
+
# fractions of a second, convert to samples based on sample rate
|
296
|
+
# taking into account CDDA alignment
|
297
|
+
if time.sub!(/\.(\d+)\z/, "")
|
298
|
+
s = "0.#$1".to_f * @infmt.rate / 75
|
299
|
+
frac = s.round * 75
|
300
|
+
end
|
301
|
+
|
302
|
+
# feed the rest to the normal function
|
303
|
+
t2s(time) + frac
|
304
|
+
end
|
305
|
+
|
306
|
+
def t2s(time)
|
307
|
+
@infmt.hhmmss_to_samples(time)
|
308
|
+
end
|
309
|
+
|
310
|
+
def run(target, opts = {})
|
311
|
+
fails = []
|
312
|
+
tracks = @tracks.dup
|
313
|
+
pids = {}
|
314
|
+
jobs = opts[:jobs] || tracks.size # jobs == nil => everything at once
|
315
|
+
jobs.times.each do
|
316
|
+
t = tracks.shift or break
|
317
|
+
pid, tmp = spawn(target, t, opts)
|
318
|
+
pids[pid] = [ t, tmp ]
|
319
|
+
end
|
320
|
+
|
321
|
+
while pids.size > 0
|
322
|
+
pid, status = Process.waitpid2(-1)
|
323
|
+
done = pids.delete(pid)
|
324
|
+
if status.success?
|
325
|
+
if t = tracks.shift
|
326
|
+
pid, tmp = spawn(target, t, opts)
|
327
|
+
pids[pid] = [ t, tmp ]
|
328
|
+
end
|
329
|
+
puts "DONE #{done[0].inspect}" if $DEBUG
|
330
|
+
done[1].close!
|
331
|
+
else
|
332
|
+
fails << [ t, status ]
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
return true if fails.empty? && tracks.empty?
|
337
|
+
fails.each do |(_t,s)|
|
338
|
+
warn "FAIL #{s.inspect} #{_t.inspect}"
|
339
|
+
end
|
340
|
+
false
|
341
|
+
end
|
342
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
|
2
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
3
|
+
require_relative '../dtas'
|
4
|
+
require_relative 'serialize'
|
5
|
+
|
6
|
+
# this is inspired by the MPRIS 2.0 TrackList spec
|
7
|
+
class DTAS::Tracklist # :nodoc:
|
8
|
+
include DTAS::Serialize
|
9
|
+
attr_accessor :repeat # true, false, 1
|
10
|
+
|
11
|
+
SIVS = %w(list pos repeat)
|
12
|
+
TL_DEFAULTS = {
|
13
|
+
"list" => [],
|
14
|
+
"pos" => -1,
|
15
|
+
"repeat" => false,
|
16
|
+
}
|
17
|
+
|
18
|
+
def self.load(hash)
|
19
|
+
obj = new
|
20
|
+
obj.instance_eval do
|
21
|
+
list = hash["list"] and @list.replace(list)
|
22
|
+
@pos = hash["pos"] || -1
|
23
|
+
@repeat = hash["repeat"] || false
|
24
|
+
end
|
25
|
+
obj
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_hsh
|
29
|
+
ivars_to_hash(SIVS).delete_if { |k,v| TL_DEFAULTS[k] == v }
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
TL_DEFAULTS.each { |k,v| instance_variable_set("@#{k}", v) }
|
34
|
+
@list = []
|
35
|
+
@goto_off = @goto_pos = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def reset
|
39
|
+
@goto_off = @goto_pos = nil
|
40
|
+
@pos = TL_DEFAULTS["pos"]
|
41
|
+
end
|
42
|
+
|
43
|
+
def size
|
44
|
+
@list.size
|
45
|
+
end
|
46
|
+
|
47
|
+
# caching this probably isn't worth it. a tracklist is usually
|
48
|
+
# a few tens of tracks, maybe a hundred at most.
|
49
|
+
def _track_id_map
|
50
|
+
by_track_id = {}
|
51
|
+
@list.each_with_index { |t,i| by_track_id[t.object_id] = i }
|
52
|
+
by_track_id
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_tracks(track_ids)
|
56
|
+
by_track_id = _track_id_map
|
57
|
+
track_ids.map do |track_id|
|
58
|
+
idx = by_track_id[track_id]
|
59
|
+
# dtas-mpris fills in the metadata, we just return a path
|
60
|
+
[ track_id, idx ? @list[idx] : nil ]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def tracks
|
65
|
+
@list.map { |t| t.object_id }
|
66
|
+
end
|
67
|
+
|
68
|
+
def advance_track(repeat_ok = true)
|
69
|
+
return if @list.empty?
|
70
|
+
# @repeat == 1 for single track repeat
|
71
|
+
next_pos = @goto_pos || @pos + (@repeat == 1 ? 0 : 1)
|
72
|
+
next_off = @goto_off # nil by default
|
73
|
+
@goto_pos = @goto_off = nil
|
74
|
+
if @list[next_pos]
|
75
|
+
@pos = next_pos
|
76
|
+
elsif @repeat && repeat_ok
|
77
|
+
next_pos = @pos = 0
|
78
|
+
else
|
79
|
+
return
|
80
|
+
end
|
81
|
+
[ @list[next_pos], next_off ]
|
82
|
+
end
|
83
|
+
|
84
|
+
def cur_track
|
85
|
+
@pos >= 0 ? @list[@pos] : nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def add_track(track, after_track_id = nil, set_as_current = false)
|
89
|
+
if after_track_id
|
90
|
+
by_track_id = _track_id_map
|
91
|
+
idx = by_track_id[after_track_id] or
|
92
|
+
raise ArgumentError, "after_track_id invalid"
|
93
|
+
@list[idx, 1] = [ @list[idx], track ]
|
94
|
+
@pos = idx + 1 if set_as_current
|
95
|
+
else # nil = first_track
|
96
|
+
@list.unshift(track)
|
97
|
+
@pos = 0 if set_as_current
|
98
|
+
end
|
99
|
+
track.object_id
|
100
|
+
end
|
101
|
+
|
102
|
+
def remove_track(track_id)
|
103
|
+
by_track_id = _track_id_map
|
104
|
+
if idx = by_track_id.delete(track_id)
|
105
|
+
@list[idx] = nil
|
106
|
+
@list.compact!
|
107
|
+
# TODO: what do we do with @pos (and the currently-playing track)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def go_to(track_id, offset_hhmmss = nil)
|
112
|
+
by_track_id = _track_id_map
|
113
|
+
if idx = by_track_id[track_id]
|
114
|
+
@goto_off = offset_hhmmss
|
115
|
+
return @list[@goto_pos = idx]
|
116
|
+
end
|
117
|
+
@goto_pos = nil
|
118
|
+
# noop if track_id is invalid
|
119
|
+
end
|
120
|
+
|
121
|
+
def previous!
|
122
|
+
return if @list.empty?
|
123
|
+
prev_idx = @pos - 1
|
124
|
+
if prev_idx < 0
|
125
|
+
# stop playback if nothing to go back to.
|
126
|
+
prev_idx = @repeat ? @list.size - 1 : @list.size
|
127
|
+
end
|
128
|
+
@goto_pos = prev_idx
|
129
|
+
end
|
130
|
+
end
|