youpy-scissor 0.0.14 → 0.0.15

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.
data/README.rdoc CHANGED
@@ -30,8 +30,8 @@ supported file format:
30
30
 
31
31
  == Synopsis
32
32
 
33
- foo = Scissor.new('foo.mp3')
34
- bar = Scissor.new('bar.wav')
33
+ foo = Scissor('foo.mp3')
34
+ bar = Scissor('bar.wav')
35
35
 
36
36
  # concat
37
37
  foo + bar > 'foobar.mp3'
data/Rakefile CHANGED
@@ -17,7 +17,7 @@ DESCRIPTION = "utility to chop sound files"
17
17
  RUBYFORGE_PROJECT = "scissor"
18
18
  HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
19
19
  BIN_FILES = %w( )
20
- VERS = "0.0.14"
20
+ VERS = "0.0.15"
21
21
 
22
22
  REV = File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
23
23
  CLEAN.include ['**/.*.sw?', '*.gem', '.config']
@@ -0,0 +1,267 @@
1
+ require 'digest/md5'
2
+ require 'pathname'
3
+ require 'open4'
4
+ require 'logger'
5
+
6
+ module Scissor
7
+ class Chunk
8
+ class Error < StandardError; end
9
+ class FileExists < Error; end
10
+ class EmptyFragment < Error; end
11
+ class OutOfDuration < Error; end
12
+ class CommandFailed < Error; end
13
+
14
+ attr_reader :fragments
15
+ attr_accessor :logger
16
+
17
+ def initialize(filename = nil)
18
+ @fragments = []
19
+ @logger = Logger.new(STDOUT)
20
+ @logger.level = Logger::INFO
21
+
22
+ if filename
23
+ @fragments << Fragment.new(
24
+ Pathname.new(filename),
25
+ 0,
26
+ SoundFile.new(filename).length)
27
+ end
28
+ end
29
+
30
+ def add_fragment(fragment)
31
+ @fragments << fragment
32
+ end
33
+
34
+ def duration
35
+ @fragments.inject(0) do |memo, fragment|
36
+ memo += fragment.duration
37
+ end
38
+ end
39
+
40
+ def slice(start, length)
41
+ if start + length > duration
42
+ raise OutOfDuration
43
+ end
44
+
45
+ new_instance = self.class.new
46
+ remain = length
47
+
48
+ @fragments.each do |fragment|
49
+ if start >= fragment.duration
50
+ start -= fragment.duration
51
+
52
+ next
53
+ end
54
+
55
+ if (start + remain) <= fragment.duration
56
+ new_instance.add_fragment(Fragment.new(
57
+ fragment.filename,
58
+ fragment.start + start,
59
+ remain,
60
+ fragment.reversed?))
61
+
62
+ break
63
+ else
64
+ remain = remain - (fragment.duration - start)
65
+ new_instance.add_fragment(Fragment.new(
66
+ fragment.filename,
67
+ fragment.start + start,
68
+ fragment.duration - start,
69
+ fragment.reversed?))
70
+
71
+ start = 0
72
+ end
73
+ end
74
+
75
+ new_instance
76
+ end
77
+
78
+ def concat(other)
79
+ other.fragments.each do |fragment|
80
+ add_fragment(fragment)
81
+ end
82
+
83
+ self
84
+ end
85
+
86
+ alias + concat
87
+
88
+ def loop(count)
89
+ orig_fragments = @fragments.clone
90
+
91
+ (count - 1).times do
92
+ orig_fragments.each do |fragment|
93
+ add_fragment(fragment)
94
+ end
95
+ end
96
+
97
+ self
98
+ end
99
+
100
+ alias * loop
101
+
102
+ def split(count)
103
+ splitted_duration = duration / count.to_f
104
+ results = []
105
+
106
+ count.times do |i|
107
+ results << slice(i * splitted_duration, splitted_duration)
108
+ end
109
+
110
+ results
111
+ end
112
+
113
+ alias / split
114
+
115
+ def fill(filled_duration)
116
+ if @fragments.empty?
117
+ raise EmptyFragment
118
+ end
119
+
120
+ remain = filled_duration
121
+ new_instance = self.class.new
122
+
123
+ while filled_duration > new_instance.duration
124
+ if remain < duration
125
+ added = slice(0, remain)
126
+ else
127
+ added = self
128
+ end
129
+
130
+ new_instance += added
131
+ remain -= added.duration
132
+ end
133
+
134
+ new_instance
135
+ end
136
+
137
+ def replace(start, duration, replaced)
138
+ new_instance = self.class.new
139
+ offset = start + duration
140
+
141
+ if offset > self.duration
142
+ raise OutOfDuration
143
+ end
144
+
145
+ if start > 0
146
+ new_instance += slice(0, start)
147
+ end
148
+
149
+ new_instance += replaced
150
+ new_instance += slice(offset, self.duration - offset)
151
+
152
+ new_instance
153
+ end
154
+
155
+ def reverse
156
+ new_instance = self.class.new
157
+
158
+ @fragments.reverse.each do |fragment|
159
+ new_instance.add_fragment(Fragment.new(
160
+ fragment.filename,
161
+ fragment.start,
162
+ fragment.duration,
163
+ !fragment.reversed?))
164
+ end
165
+
166
+ new_instance
167
+ end
168
+
169
+ def to_file(filename, options = {})
170
+ filename = Pathname.new(filename)
171
+
172
+ if @fragments.empty?
173
+ raise EmptyFragment
174
+ end
175
+
176
+ which('ecasound')
177
+ which('ffmpeg')
178
+
179
+ options = {
180
+ :overwrite => false
181
+ }.merge(options)
182
+
183
+ filename = Pathname.new(filename)
184
+
185
+ if filename.exist?
186
+ if options[:overwrite]
187
+ filename.unlink
188
+ else
189
+ raise FileExists
190
+ end
191
+ end
192
+
193
+ position = 0.0
194
+ tmpdir = Pathname.new('/tmp/scissor-' + $$.to_s)
195
+ tmpdir.mkpath
196
+ tmpfile = tmpdir + 'tmp.wav'
197
+ cmd = %w/ecasound/
198
+
199
+ begin
200
+ @fragments.each_with_index do |fragment, index|
201
+ if !index.zero? && (index % 80).zero?
202
+ run_command(cmd.join(' '))
203
+ cmd = %w/ecasound/
204
+ end
205
+
206
+ fragment_tmpfile =
207
+ fragment.filename.extname.downcase == '.wav' ? fragment.filename :
208
+ tmpdir + (Digest::MD5.hexdigest(fragment.filename) + '.wav')
209
+
210
+ unless fragment_tmpfile.exist?
211
+ run_command("ffmpeg -i \"#{fragment.filename}\" \"#{fragment_tmpfile}\"")
212
+ end
213
+
214
+ cmd <<
215
+ "-a:#{index} " +
216
+ "-i:" +
217
+ (fragment.reversed? ? 'reverse,' : '') +
218
+ "select,#{fragment.start},#{fragment.duration},\"#{fragment_tmpfile}\" " +
219
+ "-o:#{tmpfile} " +
220
+ "-y:#{position}"
221
+
222
+ position += fragment.duration
223
+ end
224
+
225
+ run_command(cmd.join(' '))
226
+
227
+ if filename.extname == '.wav'
228
+ open(filename, 'w') do |file|
229
+ file.write(tmpfile.read)
230
+ end
231
+ else
232
+ run_command("ffmpeg -i \"#{tmpfile}\" \"#{filename}\"")
233
+ end
234
+ ensure
235
+ tmpdir.rmtree
236
+ end
237
+
238
+ self.class.new(filename)
239
+ end
240
+
241
+ alias > to_file
242
+
243
+ def >>(filename)
244
+ to_file(filename, :overwrite => true)
245
+ end
246
+
247
+ def which(command)
248
+ run_command("which #{command}")
249
+ end
250
+
251
+ def run_command(cmd)
252
+ @logger.debug("run_command: #{cmd}")
253
+
254
+ result = ''
255
+ status = Open4.popen4(cmd) do |pid, stdin, stdout, stderr|
256
+ @logger.debug(stderr.read)
257
+ result = stdout.read
258
+ end
259
+
260
+ if status.exitstatus != 0
261
+ raise CommandFailed.new(cmd)
262
+ end
263
+
264
+ return result
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,18 @@
1
+ module Scissor
2
+ class Fragment
3
+ attr_reader :filename, :start, :duration
4
+
5
+ def initialize(filename, start, duration, reverse = false)
6
+ @filename = filename
7
+ @start = start
8
+ @duration = duration
9
+ @reverse = reverse
10
+
11
+ freeze
12
+ end
13
+
14
+ def reversed?
15
+ @reverse
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ module Scissor
2
+ class Sequence
3
+ def initialize(pattern, duration_per_step)
4
+ @pattern = pattern
5
+ @duration_per_step = duration_per_step
6
+ end
7
+
8
+ def apply(scissors)
9
+ result = Scissor()
10
+
11
+ @pattern.split(//).each do |c|
12
+ if scissors.include?(c.to_sym)
13
+ scissor = scissors[c.to_sym]
14
+
15
+ if @duration_per_step > scissor.duration
16
+ result += scissor
17
+ result += Scissor.silence(@duration_per_step - scissor.duration)
18
+ else
19
+ result += scissors[c.to_sym].slice(0, @duration_per_step)
20
+ end
21
+ else
22
+ result += Scissor.silence(@duration_per_step)
23
+ end
24
+ end
25
+
26
+ result
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ require 'mp3info'
2
+ require 'pathname'
3
+ require 'riff/reader'
4
+
5
+ module Scissor
6
+ class SoundFile
7
+ SUPPORTED_FORMAT = %w/mp3 wav/
8
+
9
+ class Error < StandardError; end
10
+ class UnknownFormat < Error; end
11
+
12
+ def initialize(filename)
13
+ @filename = Pathname.new(filename)
14
+ @ext = @filename.extname.sub(/^\./, '').downcase
15
+
16
+ unless SUPPORTED_FORMAT.include?(@ext)
17
+ raise UnknownFormat
18
+ end
19
+ end
20
+
21
+ def length
22
+ case @ext
23
+ when 'mp3'
24
+ Mp3Info.new(@filename).length
25
+ when 'wav'
26
+ riff = Riff::Reader.open(@filename ,"r")
27
+ data = riff.root_chunk['data']
28
+ fmt = riff.root_chunk['fmt ']
29
+
30
+ data.length / fmt.body.unpack('s2i2')[3].to_f
31
+ end
32
+ end
33
+ end
34
+ end
data/lib/scissor.rb CHANGED
@@ -1,349 +1,20 @@
1
- require 'mp3info'
2
- require 'digest/md5'
3
- require 'pathname'
4
- require 'riff/reader'
5
- require 'open4'
6
- require 'logger'
1
+ require 'scissor/chunk'
2
+ require 'scissor/fragment'
3
+ require 'scissor/sound_file'
4
+ require 'scissor/sequence'
7
5
 
8
- class Scissor
9
- class Error < StandardError; end
10
- class FileExists < Error; end
11
- class EmptyFragment < Error; end
12
- class OutOfDuration < Error; end
13
- class CommandFailed < Error; end
14
-
15
- attr_reader :fragments
16
- attr_accessor :logger
17
-
18
- def initialize(filename = nil)
19
- @fragments = []
20
- @logger = Logger.new(STDOUT)
21
- @logger.level = Logger::INFO
22
-
23
- if filename
24
- @fragments << Fragment.new(
25
- Pathname.new(filename),
26
- 0,
27
- SoundFile.new(filename).length)
28
- end
29
- end
30
-
31
- def add_fragment(fragment)
32
- @fragments << fragment
33
- end
34
-
35
- def duration
36
- @fragments.inject(0) do |memo, fragment|
37
- memo += fragment.duration
38
- end
39
- end
40
-
41
- def slice(start, length)
42
- if start + length > duration
43
- raise OutOfDuration
44
- end
45
-
46
- new_instance = self.class.new
47
- remain = length
48
-
49
- @fragments.each do |fragment|
50
- if start >= fragment.duration
51
- start -= fragment.duration
52
-
53
- next
54
- end
55
-
56
- if (start + remain) <= fragment.duration
57
- new_instance.add_fragment(Fragment.new(
58
- fragment.filename,
59
- fragment.start + start,
60
- remain,
61
- fragment.reversed?))
62
-
63
- break
64
- else
65
- remain = remain - (fragment.duration - start)
66
- new_instance.add_fragment(Fragment.new(
67
- fragment.filename,
68
- fragment.start + start,
69
- fragment.duration - start,
70
- fragment.reversed?))
71
-
72
- start = 0
73
- end
74
- end
75
-
76
- new_instance
77
- end
78
-
79
- def concat(other)
80
- other.fragments.each do |fragment|
81
- add_fragment(fragment)
82
- end
83
-
84
- self
85
- end
86
-
87
- alias + concat
88
-
89
- def loop(count)
90
- orig_fragments = @fragments.clone
91
-
92
- (count - 1).times do
93
- orig_fragments.each do |fragment|
94
- add_fragment(fragment)
95
- end
96
- end
97
-
98
- self
99
- end
100
-
101
- alias * loop
102
-
103
- def split(count)
104
- splitted_duration = duration / count.to_f
105
- results = []
106
-
107
- count.times do |i|
108
- results << slice(i * splitted_duration, splitted_duration)
109
- end
110
-
111
- results
112
- end
113
-
114
- alias / split
115
-
116
- def fill(filled_duration)
117
- if @fragments.empty?
118
- raise EmptyFragment
119
- end
120
-
121
- remain = filled_duration
122
- new_instance = self.class.new
123
-
124
- while filled_duration > new_instance.duration
125
- if remain < duration
126
- added = slice(0, remain)
127
- else
128
- added = self
129
- end
130
-
131
- new_instance += added
132
- remain -= added.duration
133
- end
134
-
135
- new_instance
136
- end
137
-
138
- def replace(start, duration, replaced)
139
- new_instance = self.class.new
140
- offset = start + duration
141
-
142
- if offset > self.duration
143
- raise OutOfDuration
144
- end
145
-
146
- if start > 0
147
- new_instance += slice(0, start)
148
- end
149
-
150
- new_instance += replaced
151
- new_instance += slice(offset, self.duration - offset)
152
-
153
- new_instance
154
- end
155
-
156
- def reverse
157
- new_instance = self.class.new
158
-
159
- @fragments.reverse.each do |fragment|
160
- new_instance.add_fragment(Fragment.new(
161
- fragment.filename,
162
- fragment.start,
163
- fragment.duration,
164
- !fragment.reversed?))
165
- end
166
-
167
- new_instance
168
- end
169
-
170
- def to_file(filename, options = {})
171
- filename = Pathname.new(filename)
172
-
173
- if @fragments.empty?
174
- raise EmptyFragment
175
- end
176
-
177
- which('ecasound')
178
- which('ffmpeg')
179
-
180
- options = {
181
- :overwrite => false
182
- }.merge(options)
183
-
184
- filename = Pathname.new(filename)
185
-
186
- if filename.exist?
187
- if options[:overwrite]
188
- filename.unlink
189
- else
190
- raise FileExists
191
- end
192
- end
193
-
194
- position = 0.0
195
- tmpdir = Pathname.new('/tmp/scissor-' + $$.to_s)
196
- tmpdir.mkpath
197
- tmpfile = tmpdir + 'tmp.wav'
198
- cmd = %w/ecasound/
199
-
200
- begin
201
- @fragments.each_with_index do |fragment, index|
202
- if !index.zero? && (index % 80).zero?
203
- run_command(cmd.join(' '))
204
- cmd = %w/ecasound/
205
- end
206
-
207
- fragment_tmpfile =
208
- fragment.filename.extname.downcase == '.wav' ? fragment.filename :
209
- tmpdir + (Digest::MD5.hexdigest(fragment.filename) + '.wav')
210
-
211
- unless fragment_tmpfile.exist?
212
- run_command("ffmpeg -i \"#{fragment.filename}\" \"#{fragment_tmpfile}\"")
213
- end
214
-
215
- cmd <<
216
- "-a:#{index} " +
217
- "-i:" +
218
- (fragment.reversed? ? 'reverse,' : '') +
219
- "select,#{fragment.start},#{fragment.duration},\"#{fragment_tmpfile}\" " +
220
- "-o:#{tmpfile} " +
221
- "-y:#{position}"
222
-
223
- position += fragment.duration
224
- end
225
-
226
- run_command(cmd.join(' '))
227
-
228
- if filename.extname == '.wav'
229
- open(filename, 'w') do |file|
230
- file.write(tmpfile.read)
231
- end
232
- else
233
- run_command("ffmpeg -i \"#{tmpfile}\" \"#{filename}\"")
234
- end
235
- ensure
236
- tmpdir.rmtree
237
- end
238
-
239
- self.class.new(filename)
240
- end
241
-
242
- alias > to_file
243
-
244
- def which(command)
245
- run_command("which #{command}")
246
- end
247
-
248
- def run_command(cmd)
249
- @logger.debug("run_command: #{cmd}")
250
-
251
- result = ''
252
- status = Open4.popen4(cmd) do |pid, stdin, stdout, stderr|
253
- @logger.debug(stderr.read)
254
- result = stdout.read
255
- end
256
-
257
- if status.exitstatus != 0
258
- raise CommandFailed.new(cmd)
259
- end
260
-
261
- return result
262
- end
263
-
264
- class << self
265
- def silence(duration)
266
- new(File.dirname(__FILE__) + '/../data/silence.mp3').
267
- slice(0, 1).
268
- fill(duration)
269
- end
270
-
271
- def sequence(*args)
272
- Sequence.new(*args)
273
- end
274
- end
275
-
276
- class SoundFile
277
- SUPPORTED_FORMAT = %w/mp3 wav/
278
-
279
- class Error < StandardError; end
280
- class UnknownFormat < Error; end
281
-
282
- def initialize(filename)
283
- @filename = Pathname.new(filename)
284
- @ext = @filename.extname.sub(/^\./, '').downcase
285
-
286
- unless SUPPORTED_FORMAT.include?(@ext)
287
- raise UnknownFormat
288
- end
289
- end
290
-
291
- def length
292
- case @ext
293
- when 'mp3'
294
- Mp3Info.new(@filename).length
295
- when 'wav'
296
- riff = Riff::Reader.open(@filename ,"r")
297
- data = riff.root_chunk['data']
298
- fmt = riff.root_chunk['fmt ']
299
-
300
- data.length / fmt.body.unpack('s2i2')[3].to_f
301
- end
302
- end
303
- end
304
-
305
- class Fragment
306
- attr_reader :filename, :start, :duration
307
-
308
- def initialize(filename, start, duration, reverse = false)
309
- @filename = filename
310
- @start = start
311
- @duration = duration
312
- @reverse = reverse
313
-
314
- freeze
315
- end
6
+ def Scissor(*args)
7
+ Scissor::Chunk.new(*args)
8
+ end
316
9
 
317
- def reversed?
318
- @reverse
319
- end
10
+ module Scissor
11
+ def self.silence(duration)
12
+ Scissor(File.dirname(__FILE__) + '/../data/silence.mp3').
13
+ slice(0, 1).
14
+ fill(duration)
320
15
  end
321
16
 
322
- class Sequence
323
- def initialize(pattern, duration_per_step)
324
- @pattern = pattern
325
- @duration_per_step = duration_per_step
326
- end
327
-
328
- def apply(scissors)
329
- result = Scissor.new
330
-
331
- @pattern.split(//).each do |c|
332
- if scissors.include?(c.to_sym)
333
- scissor = scissors[c.to_sym]
334
-
335
- if @duration_per_step > scissor.duration
336
- result += scissor
337
- result += Scissor.silence(@duration_per_step - scissor.duration)
338
- else
339
- result += scissors[c.to_sym].slice(0, @duration_per_step)
340
- end
341
- else
342
- result += Scissor.silence(@duration_per_step)
343
- end
344
- end
345
-
346
- result
347
- end
17
+ def self.sequence(*args)
18
+ Scissor::Sequence.new(*args)
348
19
  end
349
20
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: youpy-scissor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.14
4
+ version: 0.0.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - youpy
@@ -55,6 +55,11 @@ files:
55
55
  - README.rdoc
56
56
  - ChangeLog
57
57
  - Rakefile
58
+ - lib/scissor
59
+ - lib/scissor/chunk.rb
60
+ - lib/scissor/fragment.rb
61
+ - lib/scissor/sequence.rb
62
+ - lib/scissor/sound_file.rb
58
63
  - lib/scissor.rb
59
64
  - data/silence.mp3
60
65
  has_rdoc: true