youpy-scissor 0.0.14 → 0.0.15

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