├── .gitignore ├── .rspec ├── ChangeLog ├── Gemfile ├── README.rdoc ├── Rakefile ├── data └── silence.mp3 ├── lib ├── scissor.rb └── scissor │ ├── fragment.rb │ ├── loggable.rb │ ├── sequence.rb │ ├── sound_file.rb │ ├── tape.rb │ └── writer.rb ├── scissor.gemspec └── spec ├── fixtures ├── about_mp3.txt ├── foo.bar ├── mono.wav ├── sample.mp3 ├── sine.m4a └── sine.wav ├── fragment_spec.rb ├── scissor_spec.rb ├── sequence_spec.rb ├── sound_file_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | coverage 3 | *.gem 4 | vendor/bundle 5 | .bundle 6 | Gemfile.lock 7 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | -c 2 | -fs 3 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | == 0.0.1 / 2009-03-29 2 | 3 | * initial release 4 | 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Scissor 2 | 3 | == Description 4 | 5 | utility to chop sound files 6 | 7 | supported file format: 8 | 9 | * mp3 10 | * wav 11 | * m4a 12 | 13 | == Installation 14 | 15 | === Requirements 16 | 17 | * {FFmpeg}[http://ffmpeg.mplayerhq.hu/] 18 | * {Ecasound}[http://www.eca.cx/ecasound/] 2.5.0 or higher 19 | * {Rubber Band}[http://breakfastquay.com/rubberband/] 20 | 21 | === Archive Installation 22 | 23 | rake install 24 | 25 | === Gem Installation 26 | 27 | gem update --system 28 | gem install gemcutter 29 | gem tumble 30 | gem install scissor 31 | 32 | == Features/Problems 33 | 34 | * When you concatenate two or more files, format(sample rate, bit rate, ...) mismatch causes unexpected changes to output file. 35 | 36 | == Synopsis 37 | 38 | === instantiate 39 | 40 | ==== from file 41 | 42 | foo = Scissor('foo.mp3') 43 | bar = Scissor('bar.wav') 44 | 45 | ==== from URL 46 | 47 | foo = Scissor('http://example.com/foo.mp3') 48 | bar = Scissor('http://example.org/bar.wav') 49 | 50 | === concat 51 | 52 | foo + bar > 'foobar.mp3' 53 | 54 | === slice + concat 55 | 56 | foo[10, 1] + bar[2, 3] > 'slicefoobar.mp3' 57 | 58 | === slice + concat + loop 59 | 60 | (foo[10, 1] + bar[2, 3]) * 4 > 'slicefoobarloop.mp3' 61 | 62 | === split 63 | 64 | (Scissor('sequence.mp3') / 16).first.to_file('split.mp3') 65 | 66 | === replace first 10 seconds with 30 seconds of silence 67 | 68 | foo.replace(0, 10, Scissor.silence(30)).to_file('replace.mp3') 69 | 70 | === sequence + loop 71 | 72 | seq = Scissor.sequence('x y xyz', 0.2) 73 | seq.apply(:x => foo, :y => Proc.new { bar }, :z => foo.reverse) * 4 > 'sequence.wav' 74 | 75 | === half the pitch 76 | 77 | foo.pitch(50) 78 | 79 | === 200% time stretch without changing the pitch 80 | 81 | foo.stretch(200) 82 | 83 | === pan 84 | 85 | foo.pan(0) # left only 86 | foo.pan(50) # center(default) 87 | foo.pan(100) # right only 88 | 89 | === mix 90 | 91 | Scissor.mix([foo, bar], 'mix.mp3') 92 | 93 | == Copyright 94 | 95 | Author:: youpy 96 | Copyright:: Copyright (c) 2009 youpy 97 | License:: MIT 98 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require 'rake' 3 | require "bundler/gem_tasks" 4 | 5 | require 'rspec/core' 6 | require 'rspec/core/rake_task' 7 | RSpec::Core::RakeTask.new(:spec) do |spec| 8 | spec.pattern = FileList['spec/**/*_spec.rb'] 9 | end 10 | 11 | task :default => :spec 12 | -------------------------------------------------------------------------------- /data/silence.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youpy/scissor/138683081cfd640c5a42aa703121bee056feb9c9/data/silence.mp3 -------------------------------------------------------------------------------- /lib/scissor.rb: -------------------------------------------------------------------------------- 1 | require 'scissor/loggable' 2 | require 'scissor/tape' 3 | require 'scissor/fragment' 4 | require 'scissor/sound_file' 5 | require 'scissor/sequence' 6 | require 'scissor/writer' 7 | 8 | def Scissor(filename_or_url = nil) 9 | if filename_or_url && filename_or_url.to_s =~ /^http/ 10 | Scissor::Tape.new_from_url(filename_or_url) 11 | else 12 | Scissor::Tape.new(filename_or_url) 13 | end 14 | end 15 | 16 | require 'logger' 17 | 18 | module Scissor 19 | @logger = Logger.new(STDOUT) 20 | @logger.level = Logger::WARN 21 | 22 | FFMPEG.logger = @logger 23 | 24 | class << self 25 | attr_accessor :logger 26 | end 27 | 28 | def logger 29 | self.class.logger 30 | end 31 | 32 | class << self 33 | def silence(duration) 34 | Scissor(File.dirname(__FILE__) + '/../data/silence.mp3'). 35 | slice(0, 1). 36 | fill(duration) 37 | end 38 | 39 | def sequence(*args) 40 | Scissor::Sequence.new(*args) 41 | end 42 | 43 | def join(scissor_array) 44 | scissor_array.inject(Scissor()) do |m, scissor| 45 | m + scissor 46 | end 47 | end 48 | 49 | def mix(scissor_array, filename, options = {}) 50 | writer = Scissor::Writer.new 51 | 52 | scissor_array.each do |scissor| 53 | writer.add_track(scissor.fragments) 54 | end 55 | 56 | writer.to_file(filename, options) 57 | 58 | Scissor(filename) 59 | end 60 | end 61 | end 62 | 63 | # for ruby 1.9 64 | if IO.instance_methods.include? :getbyte 65 | class << Riff::Reader::Chunk 66 | alias :read_bytes_to_int_original :read_bytes_to_int 67 | def read_bytes_to_int file, bytes 68 | require 'delegate' 69 | file_delegate = SimpleDelegator.new(file) 70 | def file_delegate.getc 71 | getbyte 72 | end 73 | read_bytes_to_int_original file_delegate, bytes 74 | end 75 | end 76 | end 77 | 78 | -------------------------------------------------------------------------------- /lib/scissor/fragment.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module Scissor 4 | class Fragment 5 | attr_reader :filename, :start, :pitch, :pan 6 | 7 | def initialize(filename, start, duration, reverse = false, pitch = 100, stretch = false, pan = 50) 8 | @filename = Pathname.new(filename).realpath 9 | @start = start 10 | @duration = duration 11 | @reverse = reverse 12 | @pitch = pitch 13 | @is_stretched = stretch 14 | @pan = pan 15 | 16 | freeze 17 | end 18 | 19 | def duration 20 | @duration * (100 / pitch.to_f) 21 | end 22 | 23 | def original_duration 24 | @duration 25 | end 26 | 27 | def reversed? 28 | @reverse 29 | end 30 | 31 | def stretched? 32 | @is_stretched 33 | end 34 | 35 | def create(remaining_start, remaining_length) 36 | if remaining_start >= duration 37 | return [nil, remaining_start - duration, remaining_length] 38 | end 39 | 40 | have_remain_to_return = (remaining_start + remaining_length) >= duration 41 | 42 | if have_remain_to_return 43 | new_length = duration - remaining_start 44 | remaining_length -= new_length 45 | else 46 | new_length = remaining_length 47 | remaining_length = 0 48 | end 49 | 50 | new_fragment = clone do |attributes| 51 | attributes.update( 52 | :start => start + remaining_start * pitch.to_f / 100, 53 | :duration => new_length * pitch.to_f / 100, 54 | :reverse => false 55 | ) 56 | end 57 | 58 | return [new_fragment, 0, remaining_length] 59 | end 60 | 61 | def clone(&block) 62 | attributes = { 63 | :filename => filename, 64 | :start => start, 65 | :duration => original_duration, 66 | :reverse => reversed?, 67 | :pitch => pitch, 68 | :stretch => stretched?, 69 | :pan => pan 70 | } 71 | 72 | if block_given? 73 | block.call(attributes) 74 | end 75 | 76 | self.class.new( 77 | attributes[:filename], 78 | attributes[:start], 79 | attributes[:duration], 80 | attributes[:reverse], 81 | attributes[:pitch], 82 | attributes[:stretch], 83 | attributes[:pan] 84 | ) 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/scissor/loggable.rb: -------------------------------------------------------------------------------- 1 | module Scissor 2 | module Loggable 3 | def logger 4 | Scissor.logger 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/scissor/sequence.rb: -------------------------------------------------------------------------------- 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(instruments) 9 | @pattern.split(//).inject(Scissor()) do |result, c| 10 | if instruments.include?(c.to_sym) 11 | instrument = instruments[c.to_sym] 12 | 13 | if instrument.is_a?(Proc) 14 | instrument = instrument.call(c) 15 | end 16 | 17 | if @duration_per_step > instrument.duration 18 | result += instrument + Scissor.silence(@duration_per_step - instrument.duration) 19 | else 20 | result += instrument.slice(0, @duration_per_step) 21 | end 22 | else 23 | result += Scissor.silence(@duration_per_step) 24 | end 25 | 26 | result 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/scissor/sound_file.rb: -------------------------------------------------------------------------------- 1 | require 'mp3info' 2 | require 'mp4info' 3 | require 'pathname' 4 | require 'riff/reader' 5 | 6 | module Scissor 7 | class SoundFile 8 | class Mp3 < SoundFile 9 | def length 10 | info.length 11 | end 12 | 13 | def mono? 14 | info.channel_mode == 'Single Channel' 15 | end 16 | 17 | private 18 | 19 | def info 20 | @info ||= Mp3Info.new(@filename.to_s) 21 | end 22 | end 23 | 24 | class Wav < SoundFile 25 | def length 26 | data.length / fmt.body.unpack('s2i2')[3].to_f 27 | end 28 | 29 | def mono? 30 | fmt.body.unpack('s2')[1] == 1 31 | end 32 | 33 | private 34 | 35 | def riff 36 | @riff ||= Riff::Reader.open(@filename ,"r") 37 | end 38 | 39 | def data 40 | @data ||= riff.root_chunk['data'] 41 | end 42 | 43 | def fmt 44 | @fmt ||= riff.root_chunk['fmt '] 45 | end 46 | end 47 | 48 | class M4a < SoundFile 49 | def length 50 | info.SECS_NOROUND 51 | end 52 | 53 | # FIXME 54 | def mono? 55 | false 56 | end 57 | 58 | private 59 | 60 | def info 61 | @info ||= MP4Info.open(@filename.to_s) 62 | end 63 | end 64 | 65 | SUPPORTED_FORMATS = { 66 | :mp3 => Mp3, 67 | :wav => Wav, 68 | :m4a => M4a 69 | } 70 | 71 | class Error < StandardError; end 72 | class UnknownFormat < Error; end 73 | 74 | def self.new_from_filename(filename) 75 | ext = filename.extname.sub(/^\./, '').downcase 76 | 77 | unless klass = SUPPORTED_FORMATS[ext.to_sym] 78 | raise UnknownFormat 79 | end 80 | 81 | klass.new(filename) 82 | end 83 | 84 | def initialize(filename) 85 | @filename = Pathname.new(filename) 86 | end 87 | end 88 | end 89 | 90 | class MP4Info 91 | private 92 | 93 | def parse_mvhd(io_stream, level, size) 94 | raise "Parse error" if size < 32 95 | data = read_or_raise(io_stream, size, "Premature end of file") 96 | 97 | version = data.unpack("C")[0] & 255 98 | if (version == 0) 99 | scale, duration = data[12..19].unpack("NN") 100 | elsif (version == 1) 101 | scale, hi, low = data[20..31].unpack("NNN") 102 | duration = hi * (2**32) + low 103 | else 104 | return 105 | end 106 | 107 | printf " %sDur/Scl=#{duration}/#{scale}\n", ' ' * ( 2 * level ) if $DEBUG 108 | 109 | secs = (duration * 1.0) / scale 110 | 111 | # add 112 | @info_atoms["SECS_NOROUND"] = secs 113 | 114 | @info_atoms["SECS"] = (secs).round 115 | @info_atoms["MM"] = (secs / 60).floor 116 | @info_atoms["SS"] = (secs - @info_atoms["MM"] * 60).floor 117 | @info_atoms["MS"] = (1000 * (secs - secs.floor)).round 118 | @info_atoms["TIME"] = sprintf "%02d:%02d", @info_atoms["MM"], 119 | @info_atoms["SECS"] - @info_atoms["MM"] * 60; 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/scissor/tape.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | require 'tempfile' 3 | 4 | module Scissor 5 | class Tape 6 | class Error < StandardError; end 7 | class EmptyFragment < Error; end 8 | 9 | attr_reader :fragments 10 | 11 | def initialize(filename = nil) 12 | @fragments = [] 13 | 14 | if filename 15 | filename = Pathname(filename).expand_path 16 | @fragments << Fragment.new( 17 | filename, 18 | 0, 19 | SoundFile.new_from_filename(filename).length) 20 | end 21 | end 22 | 23 | def self.new_from_url(url) 24 | file = nil 25 | content_types = { 26 | 'audio/wav' => 'wav', 27 | 'audio/x-wav' => 'wav', 28 | 'audio/wave' => 'wav', 29 | 'audio/x-pn-wav' => 'wav', 30 | 'audio/mpeg' => 'mp3', 31 | 'audio/x-mpeg' => 'mp3', 32 | 'audio/mp3' => 'mp3', 33 | 'audio/x-mp3' => 'mp3', 34 | 'audio/mpeg3' => 'mp3', 35 | 'audio/x-mpeg3' => 'mp3', 36 | 'audio/mpg' => 'mp3', 37 | 'audio/x-mpg' => 'mp3', 38 | 'audio/x-mpegaudio' => 'mp3', 39 | } 40 | 41 | open(url) do |f| 42 | ext = content_types[f.content_type.downcase] 43 | 44 | file = Tempfile.new(['audio', '.' + ext]) 45 | file.write(f.read) 46 | file.flush 47 | end 48 | 49 | tape = new(file.path) 50 | 51 | # reference tempfile to prevent GC 52 | tape.instance_variable_set('@__tempfile', file) 53 | tape 54 | end 55 | 56 | def add_fragment(fragment) 57 | @fragments << fragment 58 | end 59 | 60 | def add_fragments(fragments) 61 | fragments.each do |fragment| 62 | add_fragment(fragment) 63 | end 64 | end 65 | 66 | def duration 67 | @fragments.inject(0) do |memo, fragment| 68 | memo += fragment.duration 69 | end 70 | end 71 | 72 | def slice(start, length) 73 | if start + length > duration 74 | length = duration - start 75 | end 76 | 77 | new_instance = self.class.new 78 | remaining_start = start.to_f 79 | remaining_length = length.to_f 80 | 81 | @fragments.each do |fragment| 82 | new_fragment, remaining_start, remaining_length = 83 | fragment.create(remaining_start, remaining_length) 84 | 85 | if new_fragment 86 | new_instance.add_fragment(new_fragment) 87 | end 88 | 89 | if remaining_length == 0 90 | break 91 | end 92 | end 93 | 94 | new_instance 95 | end 96 | 97 | alias [] slice 98 | 99 | def concat(other) 100 | add_fragments(other.fragments) 101 | 102 | self 103 | end 104 | 105 | alias << concat 106 | 107 | def +(other) 108 | new_instance = Scissor() 109 | new_instance.add_fragments(@fragments + other.fragments) 110 | new_instance 111 | end 112 | 113 | def loop(count) 114 | orig_fragments = @fragments.clone 115 | new_instance = Scissor() 116 | 117 | count.times do 118 | new_instance.add_fragments(orig_fragments) 119 | end 120 | 121 | new_instance 122 | end 123 | 124 | alias * loop 125 | 126 | def split(count) 127 | splitted_duration = duration / count.to_f 128 | results = [] 129 | 130 | count.times do |i| 131 | results << slice(i * splitted_duration, splitted_duration) 132 | end 133 | 134 | results 135 | end 136 | 137 | alias / split 138 | 139 | def fill(filled_duration) 140 | if duration.zero? 141 | raise EmptyFragment 142 | end 143 | 144 | loop_count = (filled_duration / duration).to_i 145 | remain = filled_duration % duration 146 | 147 | loop(loop_count) + slice(0, remain) 148 | end 149 | 150 | def replace(start, length, replaced) 151 | new_instance = self.class.new 152 | offset = start + length 153 | 154 | new_instance += slice(0, start) 155 | 156 | if new_instance.duration < start 157 | new_instance + Scissor.silence(start - new_instance.duration) 158 | end 159 | 160 | new_instance += replaced 161 | 162 | if duration > offset 163 | new_instance += slice(offset, duration - offset) 164 | end 165 | 166 | new_instance 167 | end 168 | 169 | def reverse 170 | new_instance = self.class.new 171 | 172 | @fragments.reverse.each do |fragment| 173 | new_instance.add_fragment(fragment.clone do |attributes| 174 | attributes[:reverse] = !fragment.reversed? 175 | end) 176 | end 177 | 178 | new_instance 179 | end 180 | 181 | def pitch(pitch, stretch = false) 182 | new_instance = self.class.new 183 | 184 | @fragments.each do |fragment| 185 | new_instance.add_fragment(fragment.clone do |attributes| 186 | attributes[:pitch] = fragment.pitch * (pitch.to_f / 100) 187 | attributes[:stretch] = stretch 188 | end) 189 | end 190 | 191 | new_instance 192 | end 193 | 194 | def stretch(factor) 195 | factor_for_pitch = 1 / (factor.to_f / 100) * 100 196 | pitch(factor_for_pitch, true) 197 | end 198 | 199 | def pan(right_percent) 200 | new_instance = self.class.new 201 | 202 | @fragments.each do |fragment| 203 | new_instance.add_fragment(fragment.clone do |attributes| 204 | attributes[:pan] = right_percent 205 | end) 206 | end 207 | 208 | new_instance 209 | end 210 | 211 | def to_file(filename, options = {}) 212 | Scissor.mix([self], filename, options) 213 | end 214 | 215 | alias > to_file 216 | 217 | def >>(filename) 218 | to_file(filename, :overwrite => true) 219 | end 220 | 221 | def silence 222 | Scissor.silence(duration) 223 | end 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /lib/scissor/writer.rb: -------------------------------------------------------------------------------- 1 | require 'digest/md5' 2 | require 'pathname' 3 | require 'open4' 4 | require 'temp_dir' 5 | require 'streamio-ffmpeg' 6 | 7 | module Scissor 8 | class Writer 9 | include Loggable 10 | 11 | class Error < StandardError; end 12 | class FileExists < Error; end 13 | class EmptyFragment < Error; end 14 | class CommandFailed < Error; end 15 | class CommandNotFound < Error; end 16 | 17 | def initialize 18 | @tracks = [] 19 | 20 | which('ecasound') 21 | which('rubberband') 22 | end 23 | 24 | def add_track(fragments) 25 | @tracks << fragments 26 | end 27 | 28 | def join_fragments(fragments, outfile, tmpdir) 29 | position = 0.0 30 | cmd = %w/ecasound/ 31 | is_mono = {} 32 | 33 | fragments.each_with_index do |fragment, index| 34 | fragment_filename = fragment.filename 35 | 36 | is_mono[fragment_filename] ||= mono?(fragment_filename) 37 | 38 | if !index.zero? && (index % 28).zero? 39 | run_command(cmd.join(' ')) 40 | cmd = %w/ecasound/ 41 | end 42 | 43 | fragment_outfile = tmpdir + (Digest::MD5.hexdigest(fragment_filename.to_s) + '.wav') 44 | 45 | unless fragment_outfile.exist? 46 | movie = FFMPEG::Movie.new(fragment_filename.to_s) 47 | movie.transcode(fragment_outfile.to_s, :audio_sample_rate => 44100) 48 | end 49 | 50 | cmd << "-a:#{index} -o:#{outfile} -y:#{position}#{is_mono[fragment_filename] ? ' -chcopy:1,2' : ''}" 51 | 52 | if fragment.pan != 50 53 | cmd << "-epp:%i" % fragment.pan 54 | end 55 | 56 | if fragment.stretched? && fragment.pitch.to_f != 100.0 57 | rubberband_out = tmpdir + (Digest::MD5.hexdigest(fragment_filename.to_s) + "rubberband_#{index}.wav") 58 | rubberband_temp = tmpdir + "_rubberband.wav" 59 | 60 | run_command("ecasound " + 61 | "-i:" + 62 | (fragment.reversed? ? 'reverse,' : '') + 63 | "select,#{fragment.start},#{fragment.original_duration},\"#{fragment_outfile}\" -o:#{rubberband_temp} " 64 | ) 65 | run_command("rubberband -T #{fragment.pitch.to_f/100} \"#{rubberband_temp}\" \"#{rubberband_out}\"") 66 | 67 | cmd << "-i:\"#{rubberband_out}\"" 68 | else 69 | cmd << 70 | "-i:" + 71 | (fragment.reversed? ? 'reverse,' : '') + 72 | "select,#{fragment.start},#{fragment.original_duration},\"#{fragment_outfile}\" " + 73 | (fragment.pitch.to_f == 100.0 ? "" : "-ei:#{fragment.pitch} ") 74 | end 75 | 76 | position += fragment.duration 77 | end 78 | 79 | run_command(cmd.join(' ')) 80 | end 81 | 82 | def mix_files(filenames, outfile, amplify = nil) 83 | cmd = %w/ecasound/ 84 | amplify ||= 100 85 | 86 | filenames.each_with_index do |tf, index| 87 | cmd << "-a:#{index} -i:#{tf} -eaw:#{amplify}" 88 | end 89 | 90 | cmd << "-a:all -o:#{outfile}" 91 | run_command(cmd.join(' ')) 92 | end 93 | 94 | def to_file(filename, options) 95 | filename = Pathname.new(filename) 96 | full_filename = filename.expand_path 97 | 98 | if @tracks.flatten.empty? 99 | raise EmptyFragment 100 | end 101 | 102 | options = { 103 | :overwrite => false, 104 | :bitrate => '128k' 105 | }.merge(options) 106 | 107 | if filename.exist? 108 | if options[:overwrite] 109 | filename.unlink 110 | else 111 | raise FileExists 112 | end 113 | end 114 | 115 | TempDir.create do |dir| 116 | tmpdir = Pathname.new(dir) 117 | tmpfiles = [] 118 | 119 | @tracks.each_with_index do |fragments, track_index| 120 | tmpfile = tmpdir + 'track_%s.wav' % track_index.to_s 121 | tmpfiles << tmpfile 122 | join_fragments(fragments, tmpfile, tmpdir) 123 | end 124 | 125 | mix_files(tmpfiles, final_tmpfile = tmpdir + 'tmp.wav', options[:amplify]) 126 | 127 | movie = FFMPEG::Movie.new(final_tmpfile.to_s) 128 | movie.transcode(full_filename.to_s, :audio_sample_rate => 44100, :audio_bitrate => options[:bitrate]) 129 | end 130 | end 131 | 132 | def which(command) 133 | run_command("which #{command}") 134 | rescue 135 | raise CommandNotFound.new(command + ': command not found') 136 | end 137 | 138 | def run_command(cmd) 139 | logger.debug("run_command: #{cmd}") 140 | 141 | result, error = '', '' 142 | 143 | begin 144 | status = Open4.spawn cmd, 'stdout' => result, 'stderr' => error 145 | rescue Open4::SpawnError => e 146 | raise CommandFailed.new(e.cmd) 147 | ensure 148 | logger.debug(result) 149 | logger.debug(error) 150 | end 151 | 152 | if status && status.exitstatus != 0 153 | raise CommandFailed.new(cmd) 154 | end 155 | 156 | return result 157 | end 158 | 159 | private 160 | 161 | def mono?(filename) 162 | SoundFile.new_from_filename(filename).mono? 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /scissor.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |gem| 4 | gem.authors = ["youpy"] 5 | gem.email = ["youpy@buycheapviagraonlinenow.com"] 6 | gem.description = %q{utility to chop sound files} 7 | gem.summary = %q{utility to chop sound files} 8 | gem.homepage = %q{http://github.com/youpy/scissor} 9 | gem.homepage = "" 10 | 11 | gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 12 | gem.files = `git ls-files`.split("\n") 13 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 14 | gem.name = %q{scissor} 15 | gem.require_paths = ["lib"] 16 | gem.version = "0.6.0" 17 | 18 | gem.add_dependency('open4', '>= 1.3.0') 19 | gem.add_dependency('ruby-mp3info') 20 | gem.add_dependency('mp4info', '1.7.3') 21 | gem.add_dependency('riff', '<= 0.3.0') 22 | gem.add_dependency('tempdir') 23 | gem.add_dependency('streamio-ffmpeg') 24 | gem.add_development_dependency('rspec', ['~> 2.8.0']) 25 | gem.add_development_dependency('rake') 26 | gem.add_development_dependency('fakeweb') 27 | end 28 | -------------------------------------------------------------------------------- /spec/fixtures/about_mp3.txt: -------------------------------------------------------------------------------- 1 | http://mp34u.muzic.com/posting/6485 2 | -------------------------------------------------------------------------------- /spec/fixtures/foo.bar: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /spec/fixtures/mono.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youpy/scissor/138683081cfd640c5a42aa703121bee056feb9c9/spec/fixtures/mono.wav -------------------------------------------------------------------------------- /spec/fixtures/sample.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youpy/scissor/138683081cfd640c5a42aa703121bee056feb9c9/spec/fixtures/sample.mp3 -------------------------------------------------------------------------------- /spec/fixtures/sine.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youpy/scissor/138683081cfd640c5a42aa703121bee056feb9c9/spec/fixtures/sine.m4a -------------------------------------------------------------------------------- /spec/fixtures/sine.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youpy/scissor/138683081cfd640c5a42aa703121bee056feb9c9/spec/fixtures/sine.wav -------------------------------------------------------------------------------- /spec/fragment_spec.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.dirname(__FILE__) 2 | 3 | require 'spec_helper' 4 | 5 | describe Scissor::Fragment do 6 | before do 7 | @fragment = Scissor::Fragment.new(fixture('sample.mp3'), 5.5, 12.4) 8 | end 9 | 10 | it "should have a filename as an instance of Pathname" do 11 | @fragment.filename.should be_an_instance_of(Pathname) 12 | @fragment.filename.should eql(fixture('sample.mp3')) 13 | end 14 | 15 | it "should have a filename with absolute path" do 16 | @fragment.filename.should be_absolute 17 | end 18 | 19 | it "should have a start point" do 20 | @fragment.start.should eql(5.5) 21 | end 22 | 23 | it "should have a duration" do 24 | @fragment.duration.should eql(12.4) 25 | end 26 | 27 | it "should freezed" do 28 | lambda { 29 | @fragment.instance_eval { @duration = 1 } 30 | }.should raise_error 31 | end 32 | 33 | it "should have a pitch" do 34 | fragment = Scissor::Fragment.new(fixture('sample.mp3'), 5, 10.5, false, 50) 35 | 36 | fragment.pitch.should eql(50) 37 | fragment.duration.should eql(21.0) 38 | end 39 | 40 | it "should have a pan" do 41 | fragment = Scissor::Fragment.new(fixture('sample.mp3'), 5, 10.5, false, 50, false, 10) 42 | 43 | fragment.pan.should eql(10) 44 | end 45 | 46 | it "should return new fragment and remaining start point and length" do 47 | new_fragment, remaining_start, remaining_length = @fragment.create(0.5, 1.0) 48 | new_fragment.filename.should eql(fixture('sample.mp3')) 49 | new_fragment.start.should eql(6.0) 50 | new_fragment.duration.should eql(1.0) 51 | remaining_start.should eql(0) 52 | remaining_length.should eql(0) 53 | 54 | new_fragment, remaining_start, remaining_length = @fragment.create(12.9, 1.0) 55 | new_fragment.should be_nil 56 | remaining_start.should eql(0.5) 57 | remaining_length.should eql(1.0) 58 | 59 | new_fragment, remaining_start, remaining_length = @fragment.create(11.9, 1.0) 60 | new_fragment.filename.should eql(fixture('sample.mp3')) 61 | new_fragment.start.should eql(17.4) 62 | new_fragment.duration.should eql(0.5) 63 | remaining_start.should eql(0) 64 | remaining_length.should eql(0.5) 65 | end 66 | 67 | it "should return new fragment and remaining start point and length in half pitch" do 68 | fragment = Scissor::Fragment.new(fixture('sample.mp3'), 5, 10.5, false, 50) 69 | 70 | new_fragment, remaining_start, remaining_length = fragment.create(0.5, 1.0) 71 | new_fragment.filename.should eql(fixture('sample.mp3')) 72 | new_fragment.start.should eql(5.25) 73 | new_fragment.duration.should eql(1.0) 74 | new_fragment.pitch.should eql(50) 75 | remaining_start.should eql(0) 76 | remaining_length.should eql(0) 77 | 78 | new_fragment, remaining_start, remaining_length = fragment.create(10.5, 1.0) 79 | new_fragment.filename.should eql(fixture('sample.mp3')) 80 | new_fragment.start.should eql(10.25) 81 | new_fragment.duration.should eql(1.0) 82 | new_fragment.pitch.should eql(50) 83 | remaining_start.should eql(0) 84 | remaining_length.should eql(0) 85 | 86 | new_fragment, remaining_start, remaining_length = fragment.create(20, 3) 87 | new_fragment.start.should eql(15.0) 88 | new_fragment.duration.should eql(1.0) 89 | new_fragment.pitch.should eql(50) 90 | remaining_start.should eql(0) 91 | remaining_length.should eql(2.0) 92 | 93 | new_fragment, remaining_start, remaining_length = fragment.create(21, 1.0) 94 | new_fragment.should be_nil 95 | remaining_start.should eql(0.0) 96 | remaining_length.should eql(1.0) 97 | 98 | new_fragment, remaining_start, remaining_length = fragment.create(22, 1.0) 99 | new_fragment.should be_nil 100 | remaining_start.should eql(1.0) 101 | remaining_length.should eql(1.0) 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /spec/scissor_spec.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.dirname(__FILE__) 2 | 3 | require 'spec_helper' 4 | require 'fileutils' 5 | require 'fakeweb' 6 | 7 | include FileUtils 8 | 9 | describe Scissor do 10 | before do 11 | @mp3 = Scissor(fixture('sample.mp3')) 12 | mkdir '/tmp/scissor-test' 13 | 14 | FakeWeb.clean_registry 15 | end 16 | 17 | after do 18 | rm_rf '/tmp/scissor-test' 19 | end 20 | 21 | it "should set default logger level to Logger::WARN" do 22 | [Scissor, FFMPEG].each do |mod| 23 | mod.logger.level.should eql(Logger::WARN) 24 | end 25 | end 26 | 27 | it "should create from url" do 28 | url = 'http://example.com/sample_mp3' 29 | FakeWeb.register_uri(:get, url, :body => fixture('sample.mp3'), :content_type => 'audio/mpeg') 30 | 31 | mp3 = Scissor(url) 32 | 33 | mp3.should be_an_instance_of(Scissor::Tape) 34 | mp3.duration.should be_within(0.1).of(178.1) 35 | end 36 | 37 | it "should expand given path" do 38 | cp fixture('sample.mp3'), '/tmp/scissor-test' 39 | 40 | mp3 = Scissor('~/../../../../../../../../../../tmp/scissor-test/sample.mp3') 41 | mp3.should be_an_instance_of(Scissor::Tape) 42 | mp3.duration.should be_within(0.1).of(178.1) 43 | end 44 | 45 | it "should get duration" do 46 | @mp3.should respond_to(:duration) 47 | @mp3.duration.should be_within(0.1).of(178.1) 48 | end 49 | 50 | it "should slice" do 51 | @mp3.should respond_to(:slice) 52 | @mp3.slice(0, 120).duration.should eql(120.0) 53 | @mp3.slice(150, 20).duration.should eql(20.0) 54 | end 55 | 56 | it "should slice like array" do 57 | @mp3[0, 120].duration.should eql(120.0) 58 | @mp3[150, 20].duration.should eql(20.0) 59 | end 60 | 61 | it "should cut down if sliced range is out of duration" do 62 | @mp3.slice(0, 179).duration.should be_within(0.1).of(178.1) 63 | end 64 | 65 | it "should concatenate" do 66 | a = @mp3.slice(0, 120) 67 | scissor = a.concat(@mp3.slice(150, 20)) 68 | scissor.duration.should eql(140.0) 69 | a.duration.should eql(140.0) 70 | end 71 | 72 | it "should concatenate using double 'less than' operator" do 73 | a = @mp3.slice(0, 120) 74 | scissor = a << @mp3.slice(150, 20) 75 | scissor.duration.should eql(140.0) 76 | a.duration.should eql(140.0) 77 | end 78 | 79 | it "should concat silence" do 80 | scissor = @mp3.slice(0, 12).concat(Scissor.silence(0.32009)) 81 | scissor.duration.should be_within(0.01).of(12.32) 82 | end 83 | 84 | it "should concatenate and create new instance" do 85 | a = @mp3.slice(0, 120) 86 | scissor = a + @mp3.slice(150, 20) 87 | scissor.duration.should eql(140.0) 88 | a.duration.should eql(120.0) 89 | end 90 | 91 | it "should slice concatenated one" do 92 | scissor = @mp3.slice(0.33, 1).concat(@mp3.slice(0.2, 0.1)).slice(0.9, 0.2) 93 | 94 | scissor.duration.should be_within(0.001).of(0.2) 95 | scissor.fragments.size.should eql(2) 96 | scissor.fragments[0].start.should be_within(0.001).of(1.23) 97 | scissor.fragments[0].duration.should be_within(0.001).of(0.1) 98 | scissor.fragments[1].start.should be_within(0.001).of(0.2) 99 | scissor.fragments[1].duration.should be_within(0.001).of(0.1) 100 | end 101 | 102 | it "should loop" do 103 | scissor = @mp3.slice(0, 10) 104 | scissor.loop(3).duration.should eql(30.0) 105 | scissor.duration.should eql(10.0) 106 | end 107 | 108 | it "should loop using arithmetic operator" do 109 | scissor = @mp3.slice(0, 10) * 3 110 | scissor.duration.should eql(30.0) 111 | end 112 | 113 | it "should split" do 114 | splits = (@mp3.slice(0.33, 1) + @mp3.slice(0.2, 0.1)).split(5) 115 | splits.length.should eql(5) 116 | splits.each do |split| 117 | split.duration.should be_within(0.001).of(0.22) 118 | end 119 | 120 | splits[0].fragments.size.should eql(1) 121 | splits[1].fragments.size.should eql(1) 122 | splits[2].fragments.size.should eql(1) 123 | splits[3].fragments.size.should eql(1) 124 | splits[4].fragments.size.should eql(2) 125 | end 126 | 127 | it "should split using arithmetic operator" do 128 | splits = (@mp3.slice(0.33, 1) + @mp3.slice(0.2, 0.1)) / 5 129 | splits.length.should eql(5) 130 | splits.each do |split| 131 | split.duration.should be_within(0.001).of(0.22) 132 | end 133 | 134 | splits[0].fragments.size.should eql(1) 135 | splits[1].fragments.size.should eql(1) 136 | splits[2].fragments.size.should eql(1) 137 | splits[3].fragments.size.should eql(1) 138 | splits[4].fragments.size.should eql(2) 139 | end 140 | 141 | it "should fill" do 142 | scissor = (@mp3.slice(0, 6) + @mp3.slice(0, 2)).fill(15) 143 | scissor.duration.should eql(15.0) 144 | scissor.fragments.size.should eql(4) 145 | scissor.fragments[0].duration.should eql(6.0) 146 | scissor.fragments[1].duration.should eql(2.0) 147 | scissor.fragments[2].duration.should eql(6.0) 148 | scissor.fragments[3].duration.should eql(1.0) 149 | end 150 | 151 | it "should replace" do 152 | scissor = @mp3.slice(0, 100).replace(60, 30, @mp3.slice(0, 60)) 153 | scissor.duration.should eql(130.0) 154 | scissor.fragments.size.should eql(3) 155 | scissor.fragments[0].start.should eql(0.0) 156 | scissor.fragments[0].duration.should eql(60.0) 157 | scissor.fragments[1].start.should eql(0.0) 158 | scissor.fragments[1].duration.should eql(60.0) 159 | scissor.fragments[2].start.should eql(90.0) 160 | scissor.fragments[2].duration.should eql(10.0) 161 | end 162 | 163 | it "should reverse" do 164 | scissor = (@mp3.slice(0, 10) + @mp3.slice(0, 5)).reverse 165 | scissor.duration.should eql(15.0) 166 | scissor.fragments.size.should eql(2) 167 | scissor.fragments[0].start.should eql(0.0) 168 | scissor.fragments[0].duration.should eql(5.0) 169 | scissor.fragments[0].should be_reversed 170 | scissor.fragments[1].start.should eql(0.0) 171 | scissor.fragments[1].duration.should eql(10.0) 172 | scissor.fragments[0].should be_reversed 173 | end 174 | 175 | it "should re-reverse" do 176 | scissor = (@mp3.slice(0, 10) + @mp3.slice(0, 5)).reverse.reverse 177 | scissor.duration.should eql(15.0) 178 | scissor.fragments.size.should eql(2) 179 | scissor.fragments[0].start.should eql(0.0) 180 | scissor.fragments[0].duration.should eql(10.0) 181 | scissor.fragments[0].should_not be_reversed 182 | scissor.fragments[1].start.should eql(0.0) 183 | scissor.fragments[1].duration.should eql(5.0) 184 | scissor.fragments[0].should_not be_reversed 185 | end 186 | 187 | it "should change pitch" do 188 | scissor = @mp3.slice(0, 10) + @mp3.slice(0, 5) 189 | 190 | scissor.duration.should eql(15.0) 191 | scissor.pitch(50).duration.should eql(30.0) 192 | scissor.pitch(50).pitch(50).fragments[0].pitch.should eql(25.0) 193 | scissor.pitch(50).pitch(50).duration.should eql(60.0) 194 | end 195 | 196 | it "should stretch" do 197 | scissor = @mp3.slice(0, 0.1) 198 | 199 | scissor.duration.should eql(0.1) 200 | scissor.pitch((0.1 / 120) * 100, true).duration.should be_within(0.01).of(120.0) 201 | scissor.stretch((120 / 0.1) * 100).duration.should be_within(0.1).of(120.0) 202 | 203 | stretched = scissor.stretch((120 / 0.1) * 100).slice(0, 10) 204 | stretched.fragments.first.should be_stretched 205 | 206 | stretched1 = scissor.stretch((120 / 0.1) * 100) 207 | stretched2 = scissor.stretch((120 / 0.1) * 100) 208 | stretched = (stretched1 + stretched2).slice(100, 50) 209 | stretched.fragments.first.should be_stretched 210 | 211 | scissor = (@mp3.slice(0, 10) + @mp3.slice(0, 5)).stretch(200).reverse 212 | scissor.fragments.first.should be_stretched 213 | end 214 | 215 | it "should pan" do 216 | scissor = @mp3.slice(0, 10) + @mp3.slice(0, 5) 217 | 218 | scissor.pan(10).fragments[0].pan.should eql(10) 219 | scissor.pan(10).pan(10).fragments[0].pan.should eql(10) 220 | end 221 | 222 | it "should join instances of scissor" do 223 | a = @mp3.slice(0, 120) 224 | b = @mp3.slice(150, 20) 225 | 226 | scissor = Scissor.join([a, b]) 227 | scissor.duration.should eql(140.0) 228 | scissor.fragments[0].duration.should eql(120.0) 229 | end 230 | 231 | it "should mix instances of scissor" do 232 | a = @mp3.slice(0, 120) 233 | b = @mp3.slice(150, 20) 234 | 235 | scissor = Scissor.mix([a, b], '/tmp/scissor-test/out.mp3') 236 | scissor.should be_an_instance_of(Scissor::Tape) 237 | scissor.duration.should be_within(0.1).of(120) 238 | scissor.fragments.size.should eql(1) 239 | end 240 | 241 | it "should expand if replaced range is out of duration" do 242 | replaced = @mp3.slice(0, 100).replace(60, 41, @mp3.slice(0, 60)) 243 | replaced.duration.should eql(120.0) 244 | end 245 | 246 | it "should write to file and return new instance of Scissor" do 247 | scissor = @mp3.slice(0, 120) + @mp3.slice(150, 20) 248 | result = scissor.to_file('/tmp/scissor-test/out.mp3') 249 | result.should be_an_instance_of(Scissor::Tape) 250 | result.duration.should be_within(0.1).of(140) 251 | end 252 | 253 | it "should write to mp3 file" do 254 | scissor = @mp3.slice(0, 120) + @mp3.slice(150, 20) 255 | result = scissor.to_file('/tmp/scissor-test/out.mp3') 256 | result.duration.should be_within(0.1).of(140) 257 | end 258 | 259 | it "should write to expanded path" do 260 | @mp3.to_file('~/../../../../../../../../../../tmp/scissor-test/sample.mp3') 261 | 262 | mp3 = Scissor('/tmp/scissor-test/sample.mp3') 263 | mp3.should be_an_instance_of(Scissor::Tape) 264 | end 265 | 266 | it "should write to wav file" do 267 | scissor = @mp3.slice(0, 120) + @mp3.slice(150, 20) 268 | result = scissor.to_file('/tmp/scissor-test/out.wav') 269 | result.duration.should be_within(0.1).of(140) 270 | end 271 | 272 | it "should write to m4a file" do 273 | scissor = @mp3.slice(0, 120) + @mp3.slice(150, 20) 274 | result = scissor.to_file('/tmp/scissor-test/out.m4a') 275 | result.duration.should be_within(0.1).of(140) 276 | end 277 | 278 | it "should write to file using 'greater than' operator" do 279 | result = @mp3.slice(0, 120) + @mp3.slice(150, 20) > '/tmp/scissor-test/out.wav' 280 | result.duration.should be_within(0.1).of(140) 281 | end 282 | 283 | it "should write to file with many fragments" do 284 | scissor = (@mp3.slice(0, 120) / 100).inject(Scissor()){|m, s| m + s } + @mp3.slice(10, 20) 285 | result = scissor.to_file('/tmp/scissor-test/out.mp3') 286 | result.should be_an_instance_of(Scissor::Tape) 287 | result.duration.should be_within(0.1).of(140) 288 | end 289 | 290 | it "should overwrite existing file" do 291 | result = @mp3.slice(0, 10).to_file('/tmp/scissor-test/out.mp3') 292 | result.duration.should be_within(0.1).of(10) 293 | 294 | result = @mp3.slice(0, 12).to_file('/tmp/scissor-test/out.mp3', 295 | :overwrite => true) 296 | result.duration.should be_within(0.1).of(12) 297 | end 298 | 299 | it "should overwrite existing file using double 'greater than' oprator" do 300 | result = @mp3.slice(0, 10).to_file('/tmp/scissor-test/out.mp3') 301 | result.duration.should be_within(0.1).of(10) 302 | 303 | result = @mp3.slice(0, 12) >> '/tmp/scissor-test/out.mp3' 304 | result.duration.should be_within(0.1).of(12) 305 | end 306 | 307 | it "should write to file in the variable pitch" do 308 | scissor = @mp3.slice(0, 120) + @mp3.slice(150, 20) 309 | 310 | result = scissor.pitch(50).to_file('/tmp/scissor-test/out.mp3') 311 | result.duration.should be_within(0.1).of(280) 312 | 313 | result = scissor.pitch(200).to_file('/tmp/scissor-test/out.mp3', :overwrite => true) 314 | result.duration.should be_within(0.1).of(70) 315 | end 316 | 317 | it "should raise error if overwrite option is false" do 318 | result = @mp3.slice(0, 10).to_file('/tmp/scissor-test/out.mp3') 319 | result.duration.should be_within(0.1).of(10) 320 | 321 | lambda { 322 | @mp3.slice(0, 10).to_file('/tmp/scissor-test/out.mp3', 323 | :overwrite => false) 324 | }.should raise_error(Scissor::Writer::FileExists) 325 | 326 | lambda { 327 | @mp3.slice(0, 10).to_file('/tmp/scissor-test/out.mp3') 328 | }.should raise_error(Scissor::Writer::FileExists) 329 | end 330 | 331 | it "should raise error if no fragment are given" do 332 | lambda { 333 | Scissor().to_file('/tmp/scissor-test/out.mp3') 334 | }.should raise_error(Scissor::Writer::EmptyFragment) 335 | end 336 | 337 | it "should silence" do 338 | scissor = @mp3.slice(0, 1.0) 339 | 340 | Scissor.should_receive(:silence).with(1.0) 341 | 342 | scissor.silence 343 | end 344 | end 345 | -------------------------------------------------------------------------------- /spec/sequence_spec.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.dirname(__FILE__) 2 | 3 | require 'spec_helper' 4 | 5 | describe Scissor::Sequence do 6 | before do 7 | @foo = Scissor(fixture('sample.mp3')) 8 | @bar = Scissor(fixture('sine.wav')) 9 | end 10 | 11 | it "should instantiated" do 12 | seq = Scissor.sequence('ababaab', 1.5) 13 | seq.should be_an_instance_of(Scissor::Sequence) 14 | end 15 | 16 | it "should apply tape as instrument" do 17 | seq = Scissor.sequence('ababaab ab', 0.5) 18 | scissor = seq.apply(:a => @foo, :b => @bar) 19 | 20 | scissor.should be_an_instance_of(Scissor::Tape) 21 | scissor.duration.should eql(5.0) 22 | scissor.fragments.size.should eql(10) 23 | 24 | scissor.fragments.each do |fragment| 25 | fragment.duration.should eql(0.5) 26 | end 27 | 28 | scissor.fragments[0].filename.should eql(fixture('sample.mp3')) 29 | scissor.fragments[1].filename.should eql(fixture('sine.wav')) 30 | scissor.fragments[2].filename.should eql(fixture('sample.mp3')) 31 | scissor.fragments[3].filename.should eql(fixture('sine.wav')) 32 | scissor.fragments[4].filename.should eql(fixture('sample.mp3')) 33 | scissor.fragments[5].filename.should eql(fixture('sample.mp3')) 34 | scissor.fragments[6].filename.should eql(fixture('sine.wav')) 35 | scissor.fragments[8].filename.should eql(fixture('sample.mp3')) 36 | scissor.fragments[9].filename.should eql(fixture('sine.wav')) 37 | end 38 | 39 | it "should apply proc as instrument" do 40 | seq = Scissor.sequence('ababaab ab', 0.5) 41 | scissor = seq.apply(:a => Proc.new { @foo }, :b => Proc.new { @bar }) 42 | 43 | scissor.should be_an_instance_of(Scissor::Tape) 44 | scissor.duration.should eql(5.0) 45 | scissor.fragments.size.should eql(10) 46 | 47 | scissor.fragments.each do |fragment| 48 | fragment.duration.should eql(0.5) 49 | end 50 | 51 | scissor.fragments[0].filename.should eql(fixture('sample.mp3')) 52 | scissor.fragments[1].filename.should eql(fixture('sine.wav')) 53 | scissor.fragments[2].filename.should eql(fixture('sample.mp3')) 54 | scissor.fragments[3].filename.should eql(fixture('sine.wav')) 55 | scissor.fragments[4].filename.should eql(fixture('sample.mp3')) 56 | scissor.fragments[5].filename.should eql(fixture('sample.mp3')) 57 | scissor.fragments[6].filename.should eql(fixture('sine.wav')) 58 | scissor.fragments[8].filename.should eql(fixture('sample.mp3')) 59 | scissor.fragments[9].filename.should eql(fixture('sine.wav')) 60 | end 61 | 62 | it "should append silence when applied instance does not have enough duration" do 63 | seq = Scissor.sequence('ba', 1.5) 64 | scissor = seq.apply(:a => @foo, :b => @bar) 65 | 66 | scissor.duration.to_s.should eql("3.0") 67 | scissor.fragments.size.should eql(3) 68 | scissor.fragments[0].filename.should eql(fixture('sine.wav')) 69 | scissor.fragments[1].filename.to_s.should match(/silence\.mp3/) 70 | scissor.fragments[2].filename.should eql(fixture('sample.mp3')) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/sound_file_spec.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.dirname(__FILE__) 2 | 3 | require 'spec_helper' 4 | require 'fileutils' 5 | 6 | include FileUtils 7 | 8 | describe Scissor::SoundFile do 9 | before do 10 | @mp3 = Scissor::SoundFile.new_from_filename(fixture('sample.mp3')) 11 | @wav = Scissor::SoundFile.new_from_filename(fixture('sine.wav')) 12 | @m4a = Scissor::SoundFile.new_from_filename(fixture('sine.m4a')) 13 | end 14 | 15 | after do 16 | end 17 | 18 | it "raise error if unknown file format" do 19 | lambda { 20 | Scissor::SoundFile.new_from_filename(fixture('foo.bar')) 21 | }.should raise_error(Scissor::SoundFile::UnknownFormat) 22 | end 23 | 24 | it "should get length" do 25 | @mp3.length.should be_within(0.1).of(178.1) 26 | @wav.length.should eql(1.0) 27 | @m4a.length.should be_within(0.1).of(1.0) 28 | end 29 | 30 | describe '#mono?' do 31 | it "should return true if sound file is mono" do 32 | @mp3.should be_mono 33 | @wav.should_not be_mono 34 | @m4a.should_not be_mono 35 | 36 | Scissor::SoundFile.new_from_filename(fixture('mono.wav')).should be_mono 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.dirname(__FILE__) + '/../lib/' 2 | 3 | require 'scissor' 4 | 5 | def fixture(filename) 6 | Pathname.new(File.dirname(__FILE__) + '/fixtures/' + filename).realpath 7 | end 8 | --------------------------------------------------------------------------------