dtas 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/Documentation/.gitignore +1 -1
  4. data/Documentation/GNUmakefile +3 -1
  5. data/Documentation/dtas-archive.txt +61 -0
  6. data/Documentation/dtas-console.txt +4 -3
  7. data/Documentation/dtas-ctl.txt +4 -3
  8. data/Documentation/dtas-cueedit.txt +4 -3
  9. data/Documentation/dtas-enq.txt +4 -3
  10. data/Documentation/dtas-env.txt +60 -0
  11. data/Documentation/dtas-msinkctl.txt +4 -3
  12. data/Documentation/dtas-player.txt +6 -3
  13. data/Documentation/dtas-player_effects.txt +4 -3
  14. data/Documentation/dtas-player_protocol.txt +27 -4
  15. data/Documentation/dtas-player_sink_examples.txt +7 -3
  16. data/Documentation/dtas-sinkedit.txt +20 -3
  17. data/Documentation/dtas-sourceedit.txt +21 -3
  18. data/Documentation/dtas-splitfx.txt +14 -5
  19. data/Documentation/dtas-tl.txt +4 -3
  20. data/Documentation/dtas-xdelay.txt +4 -3
  21. data/Documentation/update-footer.rb +52 -0
  22. data/GIT-VERSION-GEN +2 -2
  23. data/GNUmakefile +1 -1
  24. data/HACKING +3 -2
  25. data/INSTALL +6 -6
  26. data/README +1 -1
  27. data/Rakefile +2 -3
  28. data/TODO +1 -1
  29. data/bin/dtas-archive +187 -0
  30. data/bin/dtas-console +7 -1
  31. data/bin/dtas-ctl +1 -1
  32. data/bin/dtas-cueedit +3 -3
  33. data/bin/dtas-enq +1 -1
  34. data/bin/dtas-msinkctl +1 -1
  35. data/bin/dtas-partstats +10 -4
  36. data/bin/dtas-player +1 -1
  37. data/bin/dtas-sinkedit +82 -20
  38. data/bin/dtas-sourceedit +64 -22
  39. data/bin/dtas-splitfx +1 -1
  40. data/bin/dtas-tl +1 -1
  41. data/bin/dtas-xdelay +1 -1
  42. data/dtas-linux.gemspec +1 -1
  43. data/dtas-mpris.gemspec +1 -1
  44. data/dtas.gemspec +1 -1
  45. data/examples/splitfx.sample.yml +11 -3
  46. data/examples/{trimfx.sample.yml → tfx.sample.yml} +1 -1
  47. data/lib/dtas.rb +2 -1
  48. data/lib/dtas/buffer.rb +5 -5
  49. data/lib/dtas/buffer/read_write.rb +8 -5
  50. data/lib/dtas/buffer/splice.rb +29 -8
  51. data/lib/dtas/command.rb +2 -2
  52. data/lib/dtas/compat_onenine.rb +1 -1
  53. data/lib/dtas/cue_index.rb +3 -1
  54. data/lib/dtas/disclaimer.rb +1 -1
  55. data/lib/dtas/edit_client.rb +1 -1
  56. data/lib/dtas/fadefx.rb +100 -0
  57. data/lib/dtas/format.rb +4 -2
  58. data/lib/dtas/parse_time.rb +3 -1
  59. data/lib/dtas/partstats.rb +8 -10
  60. data/lib/dtas/pipe.rb +2 -1
  61. data/lib/dtas/player.rb +33 -11
  62. data/lib/dtas/player/client_handler.rb +43 -8
  63. data/lib/dtas/process.rb +6 -14
  64. data/lib/dtas/replaygain.rb +3 -3
  65. data/lib/dtas/rg_state.rb +1 -1
  66. data/lib/dtas/serialize.rb +3 -1
  67. data/lib/dtas/sigevent.rb +1 -1
  68. data/lib/dtas/sigevent/efd.rb +5 -4
  69. data/lib/dtas/sigevent/pipe.rb +4 -1
  70. data/lib/dtas/sink.rb +4 -4
  71. data/lib/dtas/source.rb +1 -1
  72. data/lib/dtas/source/av.rb +2 -2
  73. data/lib/dtas/source/av_ff_common.rb +22 -11
  74. data/lib/dtas/source/cmd.rb +3 -3
  75. data/lib/dtas/source/common.rb +3 -2
  76. data/lib/dtas/source/ff.rb +2 -2
  77. data/lib/dtas/source/file.rb +22 -4
  78. data/lib/dtas/source/mp3gain.rb +1 -1
  79. data/lib/dtas/source/sox.rb +7 -7
  80. data/lib/dtas/source/splitfx.rb +99 -0
  81. data/lib/dtas/spawn_fix.rb +10 -0
  82. data/lib/dtas/splitfx.rb +63 -24
  83. data/lib/dtas/state_file.rb +3 -1
  84. data/lib/dtas/{trimfx.rb → tfx.rb} +50 -24
  85. data/lib/dtas/tracklist.rb +2 -1
  86. data/lib/dtas/unix_accepted.rb +2 -2
  87. data/lib/dtas/unix_client.rb +2 -2
  88. data/lib/dtas/unix_server.rb +1 -1
  89. data/lib/dtas/util.rb +1 -1
  90. data/lib/dtas/watchable.rb +54 -0
  91. data/lib/dtas/writable_iter.rb +2 -1
  92. data/lib/dtas/xs.rb +2 -2
  93. data/perl/dtas-graph +1 -1
  94. data/test/covshow.rb +1 -1
  95. data/test/helper.rb +1 -1
  96. data/test/player_integration.rb +1 -1
  97. data/test/test_buffer.rb +1 -1
  98. data/test/test_env.rb +1 -1
  99. data/test/test_fadefx.rb +45 -0
  100. data/test/test_format.rb +1 -1
  101. data/test/test_format_change.rb +1 -1
  102. data/test/test_player.rb +1 -1
  103. data/test/test_player_client_handler.rb +1 -1
  104. data/test/test_player_integration.rb +8 -8
  105. data/test/test_process.rb +1 -1
  106. data/test/test_rg_integration.rb +1 -1
  107. data/test/test_rg_state.rb +1 -1
  108. data/test/test_sink.rb +1 -1
  109. data/test/test_sink_pipe_size.rb +2 -2
  110. data/test/test_sink_tee_integration.rb +1 -1
  111. data/test/test_source_av.rb +1 -1
  112. data/test/test_source_sox.rb +1 -1
  113. data/test/test_splitfx.rb +43 -13
  114. data/test/test_tfx.rb +85 -0
  115. data/test/test_tracklist.rb +1 -1
  116. data/test/test_unixserver.rb +1 -1
  117. data/test/test_util.rb +1 -1
  118. metadata +17 -6
  119. data/lib/dtas/compat_rbx.rb +0 -12
  120. data/test/test_trimfx.rb +0 -81
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2013-2014, Eric Wong <e@80x24.org> and all contributors
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_relative '../process'
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2013-2014, Eric Wong <e@80x24.org> and all contributors
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_relative '../../dtas'
4
4
  require_relative '../source'
@@ -38,13 +38,13 @@ class DTAS::Source::Sox # :nodoc:
38
38
  command_init(SOX_DEFAULTS)
39
39
  end
40
40
 
41
- def try(infile, offset = nil)
41
+ def try(infile, offset = nil, trim = nil)
42
42
  err = ""
43
43
  cmd = %W(soxi -s #{infile})
44
44
  s = qx(@env.dup, cmd, err_str: err, no_raise: true)
45
45
  return if err =~ /soxi FAIL formats:/
46
46
  self.class.try_to_fail_harder(infile, s, cmd) or return
47
- source_file_dup(infile, offset)
47
+ source_file_dup(infile, offset, trim)
48
48
  end
49
49
 
50
50
  def format
@@ -75,13 +75,13 @@ class DTAS::Source::Sox # :nodoc:
75
75
  tmp
76
76
  end
77
77
 
78
- def spawn(player_format, rg_state, opts)
79
- raise "BUG: #{self.inspect}#spawn called twice" if @to_io
78
+ def src_spawn(player_format, rg_state, opts)
79
+ raise "BUG: #{self.inspect}#src_spawn called twice" if @to_io
80
80
  e = @env.merge!(player_format.to_env)
81
- e["INFILE"] = @infile
81
+ e["INFILE"] = xs(@infile)
82
82
 
83
83
  # make sure these are visible to the "current" command...
84
- e["TRIMFX"] = @offset ? "trim #@offset" : nil
84
+ e["TRIMFX"] = trimfx
85
85
  e["RGFX"] = rg_state.effect(self) || nil
86
86
  e.merge!(@rg.to_env) if @rg
87
87
 
@@ -0,0 +1,99 @@
1
+ # Copyright (C) 2014-2015 all contributors <dtas-all@nongnu.org>
2
+ # License: GPLv3 or later <https://www.gnu.org/licenses/gpl-3.0.txt>
3
+ require 'yaml'
4
+ require_relative 'sox'
5
+ require_relative '../splitfx'
6
+ require_relative '../watchable'
7
+
8
+ class DTAS::Source::SplitFX < DTAS::Source::Sox # :nodoc:
9
+ MAX_YAML_SIZE = 512 * 1024
10
+ attr_writer :sox, :sfx
11
+ include DTAS::Watchable if defined?(DTAS::Watchable)
12
+
13
+ SPLITFX_DEFAULTS = SOX_DEFAULTS.merge(
14
+ "command" => "#{SOX_DEFAULTS["command"]} $FX",
15
+ "tryorder" => 3,
16
+ )
17
+
18
+ def initialize(sox = DTAS::Source::Sox.new)
19
+ command_init(SPLITFX_DEFAULTS)
20
+ @sox = sox
21
+ end
22
+
23
+ def try(ymlfile, offset = nil, trim = nil)
24
+ @splitfx = @ymlhash = nil
25
+ st = File.stat(ymlfile)
26
+ return false if !st.file? || st.size > MAX_YAML_SIZE
27
+
28
+ # read 4 bytes first to ensure we have a YAML file with a hash:
29
+ buf = ""
30
+ File.open(ymlfile, "rb") do |fp|
31
+ return false if fp.read(4, buf) != "---\n"
32
+ buf << fp.read
33
+ end
34
+
35
+ sfx = DTAS::SplitFX.new
36
+ Dir.chdir(File.dirname(ymlfile)) do # ugh
37
+ @ymlhash = YAML.load(buf)
38
+ @ymlhash['tracks'] ||= [ "t 0 default" ]
39
+ sfx.import(@ymlhash)
40
+ sfx.infile.replace(File.expand_path(sfx.infile))
41
+ end
42
+ @splitfx = sfx
43
+ @infile = ymlfile
44
+ sox = @sox.try(sfx.infile, offset, trim) or return false
45
+ rv = source_file_dup(ymlfile, offset, trim)
46
+ rv.sox = sox
47
+ rv.env = sfx.env
48
+ rv.sfx = sfx
49
+ rv
50
+ rescue => e
51
+ warn "#{e.message} (#{e.class})"
52
+ false
53
+ end
54
+
55
+ def __load_comments
56
+ @ymlhash["comments"] || @sox.__load_comments
57
+ end
58
+
59
+ def command_string
60
+ @ymlhash["command"] || super
61
+ end
62
+
63
+ def src_spawn(player_format, rg_state, opts)
64
+ raise "BUG: #{self.inspect}#src_spawn called twice" if @to_io
65
+ e = @env.merge!(player_format.to_env)
66
+ @sfx.infile_env(e, @sox.infile)
67
+
68
+ # make sure these are visible to the "current" command...
69
+ e["TRIMFX"] = trimfx
70
+ e["RGFX"] = rg_state.effect(self) || nil
71
+ e.merge!(@rg.to_env) if @rg
72
+
73
+ @pid = dtas_spawn(e, command_string, opts)
74
+ end
75
+
76
+ def to_hsh
77
+ to_hash.delete_if { |k,v| v == SPLITFX_DEFAULTS[k] }
78
+ end
79
+
80
+ def format
81
+ @sox.format
82
+ end
83
+
84
+ def samples!
85
+ @sox.samples!
86
+ end
87
+
88
+ def samples
89
+ @sox.samples
90
+ end
91
+
92
+ def source_defaults
93
+ SPLITFX_DEFAULTS
94
+ end
95
+
96
+ def cuebreakpoints
97
+ @splitfx.cuebreakpoints
98
+ end
99
+ end
@@ -0,0 +1,10 @@
1
+ # Copyright (C) 2013-2015 all contributors <dtas-all@nongnu.org>
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ # workaround for older Rubies: https://bugs.ruby-lang.org/issues/8770
4
+ module DTAS::SpawnFix # :nodoc:
5
+ def spawn(*args)
6
+ super(*args)
7
+ rescue Errno::EINTR
8
+ retry
9
+ end if RUBY_VERSION.to_f <= 2.1
10
+ end
@@ -1,28 +1,31 @@
1
- # Copyright (C) 2013-2014, Eric Wong <e@80x24.org> and all contributors
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
- # 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
3
  require_relative '../dtas'
6
4
  require_relative 'format'
7
5
  require_relative 'process'
8
6
  require_relative 'xs'
9
7
  require 'tempfile'
8
+
9
+ # The backend for dtas-splitfx(1) command, but also supported by dtas-player
10
+ # Unlike the stuff for dtas-player, dtas-splitfx is fairly tied to sox
11
+ # (but we may still pipe to ecasound or anything else)
10
12
  class DTAS::SplitFX # :nodoc:
11
13
  CMD = 'sox "$INFILE" $COMMENTS $OUTFMT "$TRACKNUMBER.$SUFFIX" '\
12
- '$TRIMFX $RATEFX $DITHERFX'
14
+ '$TRIMFX $FX $RATEFX $DITHERFX'
13
15
  include DTAS::Process
14
16
  include DTAS::XS
17
+ attr_reader :infile, :env
15
18
 
16
- class Skip < Struct.new(:tstart) # :nodoc:
19
+ class Skip < Struct.new(:tbeg) # :nodoc:
17
20
  def commit(_)
18
21
  # noop
19
22
  end
20
23
  end
21
24
 
22
- class T < Struct.new(:env, :comments, :tstart, :fade_in, :fade_out) # :nodoc:
25
+ class T < Struct.new(:env, :comments, :tbeg, :fade_in, :fade_out) # :nodoc:
23
26
  def commit(advance_track_samples)
24
- tlen = advance_track_samples - tstart
25
- trimfx = "trim #{tstart}s #{tlen}s"
27
+ tlen = advance_track_samples - tbeg
28
+ trimfx = "trim #{tbeg}s #{tlen}s"
26
29
  if fade_in
27
30
  trimfx << " #{fade_in}"
28
31
  end
@@ -34,6 +37,10 @@ class DTAS::SplitFX # :nodoc:
34
37
  fade = " fade #{fade_type} 0 #{tlen}s #{fade_out_len}"
35
38
  trimfx << fade
36
39
  end
40
+
41
+ # raw sample counts (without 's' suffix)
42
+ env["TBEG"] = tbeg.to_s
43
+ env["TLEN"] = tlen.to_s
37
44
  env["TRIMFX"] = trimfx
38
45
  end
39
46
  end
@@ -60,7 +67,7 @@ class DTAS::SplitFX # :nodoc:
60
67
  },
61
68
  "opusenc" => {
62
69
  "command" => 'sox "$INFILE" $COMMENTS $OUTFMT - ' \
63
- '$TRIMFX $RATEFX $DITHERFX | opusenc --music ' \
70
+ '$TRIMFX $FX $RATEFX $DITHERFX | opusenc --music ' \
64
71
  '--raw-bits $BITS_PER_SAMPLE ' \
65
72
  '$OPUSENC_BITRATE --raw-rate $RATE --raw-chan $CHANNELS ' \
66
73
  '--raw-endianness $ENDIAN_OPUSENC ' \
@@ -76,6 +83,8 @@ class DTAS::SplitFX # :nodoc:
76
83
  }
77
84
  @tracks = []
78
85
  @infmt = nil # wait until input is assigned
86
+ @cuebp = nil # for playback
87
+ @command = nil # top-level, for playback
79
88
  end
80
89
 
81
90
  def _bool(hash, key)
@@ -98,7 +107,6 @@ class DTAS::SplitFX # :nodoc:
98
107
  end
99
108
  end
100
109
 
101
- hash = hash.merge(overrides)
102
110
  case v = hash["track_zpad"]
103
111
  when Integer then @track_zpad = val
104
112
  else
@@ -132,6 +140,7 @@ class DTAS::SplitFX # :nodoc:
132
140
 
133
141
  load_input!(hash)
134
142
  load_tracks!(hash)
143
+ @command = hash["command"] # nil by default
135
144
  end
136
145
 
137
146
  def load_input!(hash)
@@ -149,7 +158,7 @@ class DTAS::SplitFX # :nodoc:
149
158
  { "command" => CMD, "format" => outfmt }
150
159
  end
151
160
 
152
- def spawn(target, t, opts)
161
+ def splitfx_spawn(target, t, opts)
153
162
  target = @targets[target] || generic_target(target)
154
163
  outfmt = target["format"]
155
164
 
@@ -185,16 +194,29 @@ class DTAS::SplitFX # :nodoc:
185
194
  comments.puts("#{k}=#{v}")
186
195
  end
187
196
  env["COMMENTS"] = "--comment-file=#{comments.path}"
188
- env["INFILE"] = @infile
197
+ infile_env(env, @infile)
189
198
  env["OUTFMT"] = xs(outfmt.to_sox_arg)
190
199
  env["SUFFIX"] = outfmt.type
191
200
  env.merge!(t.env)
192
201
 
193
202
  command = target["command"]
194
- tmp = Shellwords.split(command).map do |arg|
195
- qx(env, "printf %s \"#{arg}\"")
203
+
204
+ # if a default dtas-player command is set, use that.
205
+ # we'll clobber our default environment since we assume play_cmd
206
+ # already takes those into account. In other words, use our
207
+ # target-specific commands like a dtas-player sink:
208
+ # @command | (INFILE= FX= TRIMFX=; target['command'])
209
+ if player_cmd = @command
210
+ sub_env = { 'INFILE' => '-', 'FX' => '', 'TRIMFX' => '' }
211
+ sub_env_s = sub_env.inject("") { |s,(k,v)| s << "#{k}=#{v} " }
212
+ command = "#{player_cmd} | (#{sub_env_s}; #{command})"
213
+ show_cmd = [ _expand_cmd(env, player_cmd), '|', '(', "#{sub_env_s};",
214
+ _expand_cmd(env.merge(sub_env), command), ')' ].flatten
215
+ else
216
+ show_cmd = _expand_cmd(env, command)
196
217
  end
197
- echo = "echo #{xs(tmp)}"
218
+
219
+ echo = "echo #{xs(show_cmd)}"
198
220
  if opts[:dryrun]
199
221
  command = echo
200
222
  else
@@ -216,8 +238,6 @@ class DTAS::SplitFX # :nodoc:
216
238
  fmt = "%0#{max.to_s.size}d"
217
239
  when Integer
218
240
  fmt = "%0#{@track_zpad}d"
219
- else
220
- fmt = "%d"
221
241
  end
222
242
  nr = @track_start
223
243
  @tracks.delete_if do |t|
@@ -241,7 +261,7 @@ class DTAS::SplitFX # :nodoc:
241
261
  start_time = argv.shift
242
262
  title = argv.shift
243
263
  t = T.new
244
- t.tstart = @t2s.call(start_time)
264
+ t.tbeg = @t2s.call(start_time)
245
265
  t.comments = @comments.dup
246
266
  t.comments["TITLE"] = title
247
267
  t.env = @env.dup
@@ -260,17 +280,17 @@ class DTAS::SplitFX # :nodoc:
260
280
  end
261
281
  end
262
282
 
263
- prev = @tracks.last and prev.commit(t.tstart)
283
+ prev = @tracks.last and prev.commit(t.tbeg)
264
284
  @tracks << t
265
285
  when "skip"
266
286
  stop_time = argv.shift
267
287
  argv.empty? or raise ArgumentError, "skip does not take extra args"
268
288
  s = Skip.new
269
- s.tstart = @t2s.call(stop_time)
289
+ s.tbeg = @t2s.call(stop_time)
270
290
  # s.comments = {}
271
291
  # s.env = {}
272
292
  prev = @tracks.last or raise ArgumentError, "no tracks to skip"
273
- prev.commit(s.tstart)
293
+ prev.commit(s.tbeg)
274
294
  @tracks << s
275
295
  when "stop"
276
296
  stop_time = argv.shift
@@ -278,7 +298,7 @@ class DTAS::SplitFX # :nodoc:
278
298
  samples = @t2s.call(stop_time)
279
299
  prev = @tracks.last and prev.commit(samples)
280
300
  else
281
- raise ArgumentError, "unknown command: #{xs(Array(cmd))}"
301
+ raise ArgumentError, "unknown command: #{xs(cmd)}"
282
302
  end
283
303
  end
284
304
 
@@ -309,7 +329,7 @@ class DTAS::SplitFX # :nodoc:
309
329
  jobs = opts[:jobs] || tracks.size # jobs == nil => everything at once
310
330
  jobs.times.each do
311
331
  t = tracks.shift or break
312
- pid, tmp = spawn(target, t, opts)
332
+ pid, tmp = splitfx_spawn(target, t, opts)
313
333
  pids[pid] = [ t, tmp ]
314
334
  end
315
335
 
@@ -318,7 +338,7 @@ class DTAS::SplitFX # :nodoc:
318
338
  done = pids.delete(pid)
319
339
  if status.success?
320
340
  if t = tracks.shift
321
- pid, tmp = spawn(target, t, opts)
341
+ pid, tmp = splitfx_spawn(target, t, opts)
322
342
  pids[pid] = [ t, tmp ]
323
343
  end
324
344
  puts "DONE #{done[0].inspect}" if $DEBUG
@@ -334,4 +354,23 @@ class DTAS::SplitFX # :nodoc:
334
354
  end
335
355
  false
336
356
  end
357
+
358
+ def cuebreakpoints
359
+ rv = @cuebp and return rv
360
+ require_relative 'cue_index'
361
+ @cuebp = @tracks.map { |t| DTAS::CueIndex.new(1, "#{t.tbeg}s") }
362
+ end
363
+
364
+ def infile_env(env, infile)
365
+ env["INFILE"] = xs(infile)
366
+ dir, base = File.split(File.expand_path(infile))
367
+ env["INDIR"] = xs(dir)
368
+ env["INBASE"] = xs(base)
369
+ end
370
+
371
+ def _expand_cmd(env, command)
372
+ Shellwords.split(command).map do |arg|
373
+ qx(env, "printf %s \"#{arg}\"")
374
+ end
375
+ end
337
376
  end
@@ -1,7 +1,9 @@
1
- # Copyright (C) 2013-2014, Eric Wong <e@80x24.org> and all contributors
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 'yaml'
4
4
  require 'tempfile'
5
+
6
+ # state file preserves state across restarts of dtas-player
5
7
  class DTAS::StateFile # :nodoc:
6
8
  attr_reader :path
7
9
 
@@ -1,19 +1,26 @@
1
- # Copyright (C) 2013-2014, Eric Wong <e@80x24.org> and all contributors
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_relative '../dtas'
4
4
  require_relative 'parse_time'
5
+ require_relative 'format'
5
6
  require 'shellwords'
6
7
 
7
- class DTAS::TrimFX
8
+ # this will represent a trim section inside -splitfx for applying
9
+ # effects to only a part of the output
10
+ class DTAS::TFX
8
11
  include DTAS::ParseTime
9
12
 
10
13
  attr_reader :tbeg
11
14
  attr_reader :tlen
12
15
  attr_reader :cmd
13
16
 
14
- def initialize(args)
17
+ def initialize(args, format = DTAS::Format.new)
18
+ @format = format
15
19
  args = args.dup
16
20
  case args.shift
21
+ when :pad # [ :pad, start_time, end_time ]
22
+ @tbeg = args.shift
23
+ @tlen = args.shift - @tbeg
17
24
  when "trim"
18
25
  parse_trim!(args)
19
26
  when "all"
@@ -22,7 +29,7 @@ class DTAS::TrimFX
22
29
  else
23
30
  raise ArgumentError, "#{args.inspect} not understood"
24
31
  end
25
- case tmp = args.shift
32
+ case tmp = args.shift
26
33
  when "sh" then @cmd = args
27
34
  when "sox" then tfx_sox(args)
28
35
  when "eca" then tfx_eca(args)
@@ -45,34 +52,32 @@ class DTAS::TrimFX
45
52
  @cmd.concat(%w(| sox $ECA2SOX - $SOXOUT))
46
53
  end
47
54
 
48
- def to_sox_arg(format)
55
+ def to_sox_arg
49
56
  if @tbeg && @tlen
50
- beg = @tbeg * format.rate
51
- len = @tlen * format.rate
52
- %W(trim #{beg.round}s #{len.round}s)
57
+ %W(trim #{@tbeg}s #{@tlen}s)
53
58
  elsif @tbeg
54
59
  return [] if @tbeg == 0
55
- beg = @tbeg * format.rate
56
- %W(trim #{beg.round}s)
60
+ %W(trim #{@tbeg}s)
57
61
  else
58
62
  []
59
63
  end
60
64
  end
61
65
 
66
+ # tries to interpret "trim" time args the same way the sox trim effect does
67
+ # This takes _time_ arguments only, not sample counts;
68
+ # otherwise, deviations from sox are considered bugs in dtas
62
69
  def parse_trim!(args)
63
70
  tbeg = parse_time(args.shift)
64
71
  if args[0] =~ /\A=?[\d\.]+\z/
65
72
  tlen = args.shift
66
73
  is_stop_time = tlen.sub!(/\A=/, "") ? true : false
67
74
  tlen = parse_time(tlen)
68
- if is_stop_time
69
- tlen = tlen - tbeg
70
- end
75
+ tlen = tlen - tbeg if is_stop_time
76
+ @tlen = (tlen * @format.rate).round
71
77
  else
72
- tlen = nil
78
+ @tlen = nil
73
79
  end
74
- @tbeg = tbeg
75
- @tlen = tlen
80
+ @tbeg = (tbeg * @format.rate).round
76
81
  end
77
82
 
78
83
  def <=>(other)
@@ -87,7 +92,20 @@ class DTAS::TrimFX
87
92
  end
88
93
  end
89
94
 
90
- # there'll be multiple epochs if ranges overlap
95
+ # sorts and converts an array of TFX objects into non-overlapping arrays
96
+ # of epochs
97
+ #
98
+ # input:
99
+ # [ tfx1, tfx2, tfx3, ... ]
100
+ #
101
+ # output:
102
+ # [
103
+ # [ tfx1 ], # first epoch
104
+ # [ tfx2, tfx3 ], # second epoch
105
+ # ...
106
+ # ]
107
+ # There are multiple epochs only if ranges overlap,
108
+ # There is only one epoch if there are no overlaps
91
109
  def self.schedule(ary)
92
110
  sorted = []
93
111
  ary.each_with_index { |tfx, i| sorted << TFXSort[tfx, i] }
@@ -96,41 +114,49 @@ class DTAS::TrimFX
96
114
  epoch = 0
97
115
  prev_end = 0
98
116
  defer = []
117
+
99
118
  begin
100
119
  while tfxsort = sorted.shift
101
120
  tfx = tfxsort.tfx
102
121
  if tfx.tbeg >= prev_end
122
+ # great, no overlap, append to the current epoch
103
123
  prev_end = tfx.tbeg + tfx.tlen
104
124
  (rv[epoch] ||= []) << tfx
105
125
  else
126
+ # overlapping region, we'll need a new epoch
106
127
  defer << tfxsort
107
128
  end
108
129
  end
109
- if defer[0]
130
+
131
+ if defer[0] # do we need another epoch?
110
132
  epoch += 1
111
133
  sorted = defer
112
134
  defer = []
113
135
  prev_end = 0
114
136
  end
115
137
  end while sorted[0]
138
+
116
139
  rv
117
140
  end
118
141
 
119
- def self.expand(ary, total_len)
142
+ # like schedule, but fills in the gaps with pass-through (no-op) TFX objs
143
+ # This does not change the number of epochs.
144
+ def self.expand(ary, total_samples)
120
145
  rv = []
121
- schedule(ary).each_with_index do |sary, i|
146
+ schedule(ary).each_with_index do |sary, epoch|
122
147
  tip = 0
123
- dst = rv[i] = []
148
+ dst = rv[epoch] = []
124
149
  while tfx = sary.shift
125
150
  if tfx.tbeg > tip
126
- nfx = new(%W(trim #{tip} =#{tfx.tbeg}))
151
+ # fill in the previous gap
152
+ nfx = new([:pad, tip, tfx.tbeg])
127
153
  dst << nfx
128
154
  dst << tfx
129
155
  tip = tfx.tbeg + tfx.tlen
130
156
  end
131
157
  end
132
- if tip < total_len
133
- nfx = new(%W(trim #{tip} =#{total_len}))
158
+ if tip < total_samples # fill until the last chunk
159
+ nfx = new([:pad, tip, total_samples])
134
160
  dst << nfx
135
161
  end
136
162
  end