zappa 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +20 -13
- data/ROADMAP.md +2 -2
- data/Rakefile +6 -0
- data/circle.yml +3 -0
- data/lib/zappa/clip.rb +80 -0
- data/lib/zappa/errors.rb +0 -3
- data/lib/zappa/version.rb +1 -1
- data/lib/zappa/wave/format.rb +49 -0
- data/lib/zappa/wave/riff_header.rb +28 -0
- data/lib/zappa/wave/sub_chunk.rb +29 -0
- data/lib/zappa/wave.rb +48 -77
- data/lib/zappa.rb +1 -1
- data/spec/clip_spec.rb +115 -0
- data/spec/spec_helper.rb +3 -2
- data/spec/wave/format_spec.rb +19 -0
- data/spec/wave/riff_header_spec.rb +16 -0
- data/spec/wave/sub_chunk_spec.rb +29 -0
- data/spec/wave_spec.rb +88 -21
- data/zappa.gemspec +1 -1
- metadata +16 -6
- data/lib/zappa/segment.rb +0 -44
- data/spec/segment_spec.rb +0 -43
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9392a71ab4fbaf850daa6ff3a3166f16ccbcb23d
|
4
|
+
data.tar.gz: 33ea67e02179e8afeddf795844f306a43f5a1705
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c262b0d9416d09bf225b10bbcbfce69e759de1b8740adeda12a588f3f0dbac1226f3be55c1995f8afcee4059999916e2c42ee2014da7e4dab56e07bb0ba953cc
|
7
|
+
data.tar.gz: 5d64cdb5a2334858c12f53ac8e0459f26c7e213bea352a4ea635b3f1e5e8cd063ad7c466586e65dd2af9add0671a97d35faf02e7c95ea5173e2098181e20a2e5
|
data/README.md
CHANGED
@@ -22,24 +22,31 @@ Or install it yourself as:
|
|
22
22
|
|
23
23
|
## Usage
|
24
24
|
|
25
|
-
|
25
|
+
At the core of Zappa is the Clip, an immutable audio unit. Import a wav file
|
26
|
+
into a clip:
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
require 'zappa'
|
29
|
+
|
30
|
+
clip = Zappa::Clip.new
|
31
|
+
clip.from_file('this_is_a_song.wav')
|
31
32
|
|
32
|
-
|
33
|
+
The clip will create a safe copy of the wav before you can edit it. Remember,
|
34
|
+
clips are immutable so any destructive operations return a new clip.
|
33
35
|
|
34
|
-
|
35
|
-
puts s.format
|
36
|
-
```
|
36
|
+
You can slice clips into smaller chunks:
|
37
37
|
|
38
|
-
|
38
|
+
slice_a = clip.slice(0, 1000) # clip containing 1st second
|
39
|
+
slice_b = clip.slice(1000, 2000) # clip containing 3rd second
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
41
|
+
You can also join clips:
|
42
|
+
|
43
|
+
joined_clip = slice_a + slice_b # clip containing 1st and 3rd seconds
|
44
|
+
|
45
|
+
Once you're done editing a clip, you can export it:
|
46
|
+
|
47
|
+
joined_clip.export('output.wav')
|
48
|
+
|
49
|
+
That's it for now. DSP tools are coming soon!
|
43
50
|
|
44
51
|
|
45
52
|
## Contributing
|
data/ROADMAP.md
CHANGED
data/Rakefile
CHANGED
data/circle.yml
ADDED
data/lib/zappa/clip.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'open3'
|
3
|
+
require 'pry'
|
4
|
+
|
5
|
+
module Zappa
|
6
|
+
class Clip
|
7
|
+
attr_accessor :wav, :cache
|
8
|
+
|
9
|
+
def initialize(wav=nil)
|
10
|
+
if wav
|
11
|
+
@wav = wav
|
12
|
+
else
|
13
|
+
@wav = Wave.new
|
14
|
+
end
|
15
|
+
@cache = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def from_file(path)
|
19
|
+
@wav.unpack(path)
|
20
|
+
persist_cache
|
21
|
+
end
|
22
|
+
|
23
|
+
def export(path)
|
24
|
+
persist_cache if @cache.nil?
|
25
|
+
cmd = 'ffmpeg -i ' + @cache + ' -y -f wav ' + path
|
26
|
+
Open3.popen3(cmd) do |_stdin, _stdout, _stderr, wait_thr|
|
27
|
+
fail 'Cannot export to' + path unless wait_thr.value.success?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def slice(from, to)
|
32
|
+
slice_samples(ms_to_samples(from), ms_to_samples(to))
|
33
|
+
end
|
34
|
+
|
35
|
+
def slice_samples(from, to)
|
36
|
+
fail 'invalid index' if from < 0 || to > @wav.sample_count
|
37
|
+
fail 'negative range' if from >= to
|
38
|
+
from *= @wav.frame_size
|
39
|
+
to *= @wav.frame_size
|
40
|
+
length = (to - from)
|
41
|
+
slice = @wav.data.byteslice(from, length)
|
42
|
+
clone(slice)
|
43
|
+
end
|
44
|
+
|
45
|
+
def +(other)
|
46
|
+
fail 'format mismatch' unless @wav.format == other.wav.format
|
47
|
+
w = Wave.new()
|
48
|
+
w.format = @wav.format
|
49
|
+
w.update_data(@wav.data + other.wav.data)
|
50
|
+
Clip.new(w)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def ms_to_samples(ms)
|
56
|
+
(ms * @wav.format.sample_rate / 1000).round
|
57
|
+
end
|
58
|
+
|
59
|
+
def persist_cache
|
60
|
+
tmp = Tempfile.new('zappa')
|
61
|
+
@cache = tmp.path
|
62
|
+
File.write(@cache, @wav.pack)
|
63
|
+
end
|
64
|
+
|
65
|
+
def ffmpeg_wav_export(source, destination)
|
66
|
+
cmd = 'ffmpeg -i ' + source + ' -vn -y -f wav ' + destination
|
67
|
+
Open3.popen3(cmd) do |_stdin, _stdout, _stderr, wait_thr|
|
68
|
+
fail 'Cannot open file ' + path unless wait_thr.value.success?
|
69
|
+
end
|
70
|
+
destination
|
71
|
+
end
|
72
|
+
|
73
|
+
def clone(data = nil)
|
74
|
+
clone = Clip.new
|
75
|
+
clone.wav = Marshal.load(Marshal.dump(@wav))
|
76
|
+
clone.wav.update_data(data) if data
|
77
|
+
clone
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/zappa/errors.rb
CHANGED
data/lib/zappa/version.rb
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Zappa
|
2
|
+
class Format
|
3
|
+
attr_accessor :name, :audio_format, :bits_per_sample, :block_align,
|
4
|
+
:byte_rate, :channels, :sample_rate, :chunk_size, :unknown
|
5
|
+
|
6
|
+
FMT_SIZE = 16
|
7
|
+
|
8
|
+
def initialize(file = nil)
|
9
|
+
if file.nil?
|
10
|
+
@chunk_id = 'fmt '
|
11
|
+
@chunk_size = FMT_SIZE
|
12
|
+
@audio_format = 1
|
13
|
+
@channels = 2
|
14
|
+
@sample_rate = 44100
|
15
|
+
@byte_rate = 176_400
|
16
|
+
@block_align = 4
|
17
|
+
@bits_per_sample = 16
|
18
|
+
else
|
19
|
+
@chunk_id = file.read(4)
|
20
|
+
@chunk_size = file.read(4).unpack('V').first
|
21
|
+
unpack(file.read(@chunk_size))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def ==(other)
|
26
|
+
pack == other.pack
|
27
|
+
end
|
28
|
+
|
29
|
+
def pack
|
30
|
+
fmt = @chunk_id
|
31
|
+
fmt += [@chunk_size].pack('V')
|
32
|
+
fmt += [@audio_format].pack('v')
|
33
|
+
fmt += [@channels].pack('v')
|
34
|
+
fmt += [@sample_rate].pack('V')
|
35
|
+
fmt += [@byte_rate].pack('V')
|
36
|
+
fmt += [@block_align].pack('v')
|
37
|
+
fmt + [@bits_per_sample].pack('v')
|
38
|
+
end
|
39
|
+
|
40
|
+
def unpack(data)
|
41
|
+
@audio_format = data.byteslice(0..1).unpack('v').first
|
42
|
+
@channels = data.byteslice(2..3).unpack('v').first
|
43
|
+
@sample_rate = data.byteslice(4..7).unpack('V').first
|
44
|
+
@byte_rate = data.byteslice(8..11).unpack('V').first
|
45
|
+
@block_align = data.byteslice(12..13).unpack('v').first
|
46
|
+
@bits_per_sample = data.byteslice(14..15).unpack('v').first
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Zappa
|
2
|
+
class RiffHeader
|
3
|
+
attr_reader :chunk_id, :format
|
4
|
+
attr_accessor :chunk_size
|
5
|
+
|
6
|
+
def initialize(file = nil)
|
7
|
+
if file.nil?
|
8
|
+
@chunk_id = 'RIFF'
|
9
|
+
@chunk_size = 40
|
10
|
+
@format = 'WAVE'
|
11
|
+
else
|
12
|
+
unpack(file)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def pack
|
17
|
+
@chunk_id + [@chunk_size].pack('V') + @format
|
18
|
+
end
|
19
|
+
|
20
|
+
def unpack(file)
|
21
|
+
@chunk_id = file.read(4)
|
22
|
+
@chunk_size = file.read(4).unpack('V').first
|
23
|
+
@format = file.read(4)
|
24
|
+
fail 'ID is not RIFF' unless @chunk_id == 'RIFF'
|
25
|
+
fail 'Format is not WAVE' unless @format == 'WAVE'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Zappa
|
2
|
+
class SubChunk
|
3
|
+
attr_accessor :chunk_id, :chunk_size, :data
|
4
|
+
|
5
|
+
def initialize(file = nil)
|
6
|
+
if file.nil?
|
7
|
+
@chunk_size = 0
|
8
|
+
@data = nil
|
9
|
+
else
|
10
|
+
@chunk_id = file.read(4)
|
11
|
+
@chunk_size = file.read(4).unpack('V').first
|
12
|
+
@data = file.read(@chunk_size)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def update(data)
|
17
|
+
@chunk_size = data.bytesize
|
18
|
+
@data = data
|
19
|
+
end
|
20
|
+
|
21
|
+
def pack
|
22
|
+
@chunk_id + [@chunk_size].pack('V') + @data
|
23
|
+
end
|
24
|
+
|
25
|
+
def ==(other)
|
26
|
+
other.data == data && other.chunk_id = chunk_id
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/zappa/wave.rb
CHANGED
@@ -1,103 +1,74 @@
|
|
1
|
-
|
1
|
+
require 'zappa/wave/format'
|
2
|
+
require 'zappa/wave/riff_header'
|
3
|
+
require 'zappa/wave/sub_chunk'
|
4
|
+
|
5
|
+
# WAV Spec: http://soundfile.sapp.org/doc/WaveFormat/
|
2
6
|
|
3
7
|
module Zappa
|
4
8
|
class Wave
|
5
|
-
attr_accessor :
|
6
|
-
SUBCHUNKS = %q('fmt', 'data')
|
7
|
-
KNOWN_FMT_SIZE = 16
|
9
|
+
attr_accessor :header, :format
|
8
10
|
|
9
|
-
def initialize
|
10
|
-
@header =
|
11
|
-
@format =
|
12
|
-
@
|
11
|
+
def initialize
|
12
|
+
@header = RiffHeader.new
|
13
|
+
@format = Format.new
|
14
|
+
@wave_data = SubChunk.new
|
15
|
+
end
|
13
16
|
|
14
|
-
|
15
|
-
|
16
|
-
rescue
|
17
|
-
raise FileError.new('Could not find ' + path)
|
18
|
-
else
|
19
|
-
@file_path = @file.path
|
20
|
-
unpack_wav
|
21
|
-
end
|
17
|
+
def data
|
18
|
+
@wave_data.data
|
22
19
|
end
|
23
20
|
|
24
|
-
def
|
25
|
-
|
26
|
-
File.write(@file_path, raw_file)
|
21
|
+
def data_size
|
22
|
+
@wave_data.chunk_size
|
27
23
|
end
|
28
24
|
|
29
|
-
def
|
30
|
-
|
31
|
-
hdr += @header[:chunk_id]
|
32
|
-
hdr += [@header[:chunk_size]].pack('V')
|
33
|
-
hdr += @header[:format]
|
25
|
+
def frame_size
|
26
|
+
@format.bits_per_sample * @format.channels / 8
|
34
27
|
end
|
35
28
|
|
36
|
-
def
|
37
|
-
|
38
|
-
fmt += [16].pack('V')
|
39
|
-
fmt += [@format[:audio_format]].pack('v')
|
40
|
-
fmt += [@format[:channels]].pack('v')
|
41
|
-
fmt += [@format[:sample_rate]].pack('V')
|
42
|
-
fmt += [@format[:byte_rate]].pack('V')
|
43
|
-
fmt += [@format[:block_align]].pack('v')
|
44
|
-
fmt += [@format[:bits_per_sample]].pack('v')
|
29
|
+
def sample_count
|
30
|
+
data_size / frame_size
|
45
31
|
end
|
46
32
|
|
47
|
-
def
|
48
|
-
data
|
49
|
-
data += [@data[:size]].pack('V')
|
50
|
-
data += @data[:data]
|
33
|
+
def ==(other)
|
34
|
+
other.data == data
|
51
35
|
end
|
52
36
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
37
|
+
def update_data(new_data)
|
38
|
+
@wave_data.chunk_id = 'data'
|
39
|
+
new_size = new_data.bytesize
|
40
|
+
@header.chunk_size += (new_size - @wave_data.chunk_size)
|
41
|
+
@wave_data.chunk_size = new_size
|
42
|
+
@wave_data.data = new_data
|
58
43
|
end
|
59
44
|
|
60
|
-
def
|
61
|
-
@header
|
62
|
-
@header[:chunk_size] = @file.read(4).unpack('V').first
|
63
|
-
@header[:format] = @file.read(4)
|
64
|
-
raise FileFormatError.new('Format is not WAVE') unless @header[:format] == 'WAVE'
|
65
|
-
raise FileFormatError.new('ID is not RIFF') unless @header[:chunk_id] == 'RIFF'
|
45
|
+
def pack
|
46
|
+
pack = @header.pack + @format.pack + @wave_data.pack
|
66
47
|
end
|
67
48
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
|
49
|
+
def unpack(source)
|
50
|
+
begin
|
51
|
+
file = File.open(path_to(source), 'rb')
|
52
|
+
rescue
|
53
|
+
fail 'Unable to open WAV file'
|
72
54
|
else
|
73
|
-
|
55
|
+
data_found = false
|
56
|
+
@header = RiffHeader.new(file)
|
57
|
+
@format = Format.new(file)
|
58
|
+
while !data_found
|
59
|
+
s = SubChunk.new(file)
|
60
|
+
if s.chunk_id == 'data'
|
61
|
+
@wave_data = s
|
62
|
+
data_found = true
|
63
|
+
end
|
64
|
+
end
|
74
65
|
end
|
75
66
|
end
|
76
67
|
|
77
|
-
def
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
@format[:sample_rate] = @file.read(4).unpack('V').first
|
82
|
-
@format[:byte_rate] = @file.read(4).unpack('V').first
|
83
|
-
@format[:block_align] = @file.read(2).unpack('v').first
|
84
|
-
@format[:bits_per_sample] = @file.read(2).unpack('v').first
|
85
|
-
unread = size - KNOWN_FMT_SIZE
|
86
|
-
@format[:unknown] = @file.read(unread) if unread > 0
|
87
|
-
end
|
88
|
-
|
89
|
-
def unpack_data
|
90
|
-
@data[:size] = @file.read(4).unpack('V').first
|
91
|
-
@data[:data] = @file.read(@data[:size])
|
92
|
-
end
|
93
|
-
|
94
|
-
def unpack_unknown
|
95
|
-
size = @file.read(4).unpack('V').first
|
96
|
-
@file.read(size)
|
97
|
-
end
|
98
|
-
|
99
|
-
def ==(other)
|
100
|
-
other.format == format && other.data == data
|
68
|
+
def path_to(source)
|
69
|
+
return source if source.class == String
|
70
|
+
return source.path if source.class == File
|
71
|
+
fail 'cannot unpack type: ' + source.class.to_s
|
101
72
|
end
|
102
73
|
end
|
103
74
|
end
|
data/lib/zappa.rb
CHANGED
data/spec/clip_spec.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
WAV_IN = 'spec/audio/basic-5s.wav'
|
5
|
+
WAV_IN_DATA_SIZE = 882000
|
6
|
+
|
7
|
+
describe Zappa::Clip do
|
8
|
+
before :each do
|
9
|
+
subject.from_file(WAV_IN)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#from_file' do
|
13
|
+
it 'makes a safe wav copy of the file' do
|
14
|
+
orig_file = File.open(WAV_IN, 'rb')
|
15
|
+
orig_wav = Zappa::Wave.new
|
16
|
+
orig_wav.unpack(orig_file)
|
17
|
+
cached_wav = Zappa::Wave.new
|
18
|
+
cached_wav.unpack(subject.cache)
|
19
|
+
expect(orig_wav).to eq(cached_wav)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'has a path value' do
|
23
|
+
expect(subject.cache.nil?).to eq(false)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'raises error if file does not exist' do
|
27
|
+
c = Zappa::Clip.new
|
28
|
+
expect { c.from_file('some_foo') }.to raise_error(RuntimeError)
|
29
|
+
end
|
30
|
+
|
31
|
+
pending 'raises error if ffmpeg is not installed'
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#export' do
|
35
|
+
before do
|
36
|
+
@tmp = Tempfile.new('zappa-spec')
|
37
|
+
subject.cache = nil
|
38
|
+
subject.export(@tmp.path)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'persisted the file' do
|
42
|
+
expect(subject.cache.nil?).to eq(false)
|
43
|
+
# expect the data of cache matches data in object
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'exports the clip correctly' do
|
47
|
+
subject.from_file(WAV_IN)
|
48
|
+
export_wav = Zappa::Wave.new
|
49
|
+
export_wav.unpack(File.open(@tmp.path, 'rb'))
|
50
|
+
expect(subject.wav).to eq(export_wav)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'raises error for invalid path' do
|
54
|
+
expect { subject.export('some:foo') }.to raise_error(RuntimeError)
|
55
|
+
end
|
56
|
+
|
57
|
+
pending 'raises error if ffmpeg is not installed'
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#slice_samples' do
|
61
|
+
before :each do
|
62
|
+
@slice = subject.slice_samples(4, 8)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'fails if the beginning is larger than the end' do
|
66
|
+
expect { subject.slice_samples(5,2) }.to raise_error(RuntimeError)
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'fails if the beginning is negative' do
|
70
|
+
expect { subject.slice_samples(-1,2) }.to raise_error(RuntimeError)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'fails if the end is larger than the total size' do
|
74
|
+
expect { subject.slice_samples(WAV_IN_DATA_SIZE,WAV_IN_DATA_SIZE+1) }
|
75
|
+
.to raise_error(RuntimeError)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'slices the wave by sample range' do
|
79
|
+
expect(@slice.wav.data_size).to eq(16)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'invalidates the cache' do
|
83
|
+
expect(@slice.cache).to eq(nil)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe '#slice' do
|
88
|
+
before :each do
|
89
|
+
@slice = subject.slice(0, 4)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'slices the wav by ms range' do
|
93
|
+
samples_in_ms = (4 * 44.1).round
|
94
|
+
total_bytes = samples_in_ms * 4
|
95
|
+
expect(@slice.wav.data_size).to eq(total_bytes)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'invalidates the cache' do
|
99
|
+
expect(@slice.cache).to eq(nil)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe '#+' do
|
104
|
+
it 'combines the audio clips' do
|
105
|
+
combined = subject + subject
|
106
|
+
expect(combined.wav.data_size).to be(WAV_IN_DATA_SIZE * 2)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'fails if the wave formats are different' do
|
110
|
+
sub_copy = Marshal.load(Marshal.dump(subject))
|
111
|
+
sub_copy.wav.format.sample_rate = 22000
|
112
|
+
expect { subject + sub_copy }.to raise_error(RuntimeError)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zappa::Format do
|
4
|
+
let(:src_offset) { 12 }
|
5
|
+
let(:fmt_size) { 24 }
|
6
|
+
let(:wav_path) { 'spec/audio/basic-5s.wav' }
|
7
|
+
|
8
|
+
it 'unpacks and packs each format chunk correctly' do
|
9
|
+
src = File.read(wav_path)
|
10
|
+
src_fmt = src.byteslice(src_offset, fmt_size)
|
11
|
+
|
12
|
+
file = File.open(wav_path, 'rb')
|
13
|
+
file.read(src_offset)
|
14
|
+
fmt = Zappa::Format.new(file)
|
15
|
+
pck_fmt = fmt.pack.force_encoding('UTF-8')
|
16
|
+
|
17
|
+
expect(src_fmt).to eq(pck_fmt)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zappa::RiffHeader do
|
4
|
+
let(:wav_path) { 'spec/audio/basic-5s.wav' }
|
5
|
+
|
6
|
+
it 'unpacks and packs header correctly' do
|
7
|
+
src = File.read(wav_path)
|
8
|
+
src_header = src.byteslice(0..11)
|
9
|
+
|
10
|
+
file = File.open(wav_path, 'rb')
|
11
|
+
subject.unpack(file)
|
12
|
+
pck_header = subject.pack
|
13
|
+
|
14
|
+
expect(src_header).to eq(pck_header)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zappa::SubChunk do
|
4
|
+
OFFSET = 36
|
5
|
+
|
6
|
+
before do
|
7
|
+
wav_path = 'spec/audio/basic-5s.wav'
|
8
|
+
file = File.open(wav_path, 'rb')
|
9
|
+
file.read(OFFSET)
|
10
|
+
@pck = Zappa::SubChunk.new(file).pack
|
11
|
+
@src = File.read(wav_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'unpacks and packs chunk_id correctly' do
|
15
|
+
src_id = @src.byteslice(OFFSET, 4)
|
16
|
+
pck_id = @pck.byteslice(0, 4)
|
17
|
+
expect(src_id).to eq(pck_id)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'unpacks and packs data correctly' do
|
21
|
+
src_size = @src.byteslice(OFFSET + 4, 4).unpack('V')
|
22
|
+
pck_size = @pck.byteslice(4, 24).unpack('V')
|
23
|
+
expect(src_size).to eq(pck_size)
|
24
|
+
|
25
|
+
src_data = @src.byteslice(OFFSET + 8, src_size[0])
|
26
|
+
pck_data = @pck.byteslice(8, pck_size[0]).force_encoding('UTF-8')
|
27
|
+
expect(src_data).to eq(pck_data)
|
28
|
+
end
|
29
|
+
end
|
data/spec/wave_spec.rb
CHANGED
@@ -1,32 +1,99 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'tempfile'
|
3
3
|
|
4
|
-
WAV_IN = 'spec/audio/basic-5s.wav'
|
5
|
-
WAV_EX = 'does-not-exist.wav'
|
6
|
-
|
7
4
|
describe Zappa::Wave do
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
5
|
+
let(:wav_path) { 'spec/audio/basic-5s.wav' }
|
6
|
+
let(:empty_path) { 'does-not-exist.wav' }
|
7
|
+
let(:wav_data_size) { 882000 }
|
8
|
+
let(:wav_def_fmt) { { audio_format: 1,
|
9
|
+
channels: 2,
|
10
|
+
sample_rate: 44_100,
|
11
|
+
byte_rate: 176_400,
|
12
|
+
block_align: 4,
|
13
|
+
bits_per_sample: 16 } }
|
14
|
+
let(:wav_def_hdr) { { chunk_id: 'RIFF',
|
15
|
+
chunk_size: 40,
|
16
|
+
format: 'WAVE' } }
|
17
|
+
|
18
|
+
before :each do
|
19
|
+
@file = File.open(wav_path, 'rb')
|
20
|
+
@wav = Zappa::Wave.new
|
21
|
+
@wav.unpack(@file)
|
17
22
|
end
|
18
23
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
+
describe '#initialize' do
|
25
|
+
it 'has a default header, format chunks and empty wave chunk' do
|
26
|
+
w = Zappa::Wave.new
|
27
|
+
wav_def_hdr.each { |h| expect(h[1]).to eq(w.header.send(h[0])) }
|
28
|
+
wav_def_fmt.each { |h| expect(h[1]).to eq(w.format.send(h[0])) }
|
29
|
+
expect(w.data).to eq(nil)
|
30
|
+
expect(w.data_size).to eq(0)
|
31
|
+
end
|
24
32
|
end
|
25
33
|
|
26
|
-
|
27
|
-
|
34
|
+
describe '#update_data' do
|
35
|
+
let (:slice_length) { 4 }
|
36
|
+
|
37
|
+
before :each do
|
38
|
+
@new_data = @wav.data.byteslice(0, slice_length)
|
39
|
+
@wav.update_data(@new_data)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'updates the wav data correctly' do
|
43
|
+
expect(@wav.data).to eq(@new_data)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'updates header data correctly' do
|
47
|
+
expect(@wav.header.chunk_size).to eq(40)
|
48
|
+
expect(@wav.data_size).to eq(slice_length)
|
49
|
+
end
|
28
50
|
end
|
29
51
|
|
30
|
-
|
31
|
-
|
52
|
+
describe '#pack' do
|
53
|
+
it 'packs all sub-chunks into a string' do
|
54
|
+
expect(@wav.pack.bytesize).to eq(@file.size)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#unpack' do
|
59
|
+
it 'reads format headers correctly' do
|
60
|
+
wav_def_fmt.each do |h|
|
61
|
+
expect(h[1]).to eq(@wav.format.send(h[0]))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'reads data size correctly' do
|
66
|
+
expect(@wav.data_size).to eq(wav_data_size)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#==' do
|
71
|
+
before :each do
|
72
|
+
@new_wave = Marshal.load(Marshal.dump(@wav))
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'is equal to a wave with identical data' do
|
76
|
+
expect(@wav).to eq(@new_wave)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'is equal to a wave with different fmt data' do
|
80
|
+
@new_wave.format.bits_per_sample = 2
|
81
|
+
expect(@wav).to eq(@new_wave)
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'is not equal to a wave with different data' do
|
85
|
+
@new_wave.update_data('')
|
86
|
+
expect(@wav).not_to eq(@new_wave)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#path_to' do
|
91
|
+
it 'returns input, if provided a file' do
|
92
|
+
expect(subject.path_to(@file)).to eq(@file.path)
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'returns the file at path, if provided a path' do
|
96
|
+
expect(subject.path_to(wav_path)).to eq(wav_path)
|
97
|
+
end
|
98
|
+
end
|
32
99
|
end
|
data/zappa.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ['Varun Srinivasan']
|
10
10
|
spec.email = ['varunsrin@gmail.com']
|
11
11
|
spec.summary = 'Ruby gem for manipulating audio files.'
|
12
|
-
spec.description = '
|
12
|
+
spec.description = 'Zappa is a DSP toolbox for manipulating audio files.'
|
13
13
|
spec.homepage = ''
|
14
14
|
spec.license = 'MIT'
|
15
15
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zappa
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Varun Srinivasan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-02-
|
11
|
+
date: 2015-02-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -66,7 +66,7 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
-
description:
|
69
|
+
description: Zappa is a DSP toolbox for manipulating audio files.
|
70
70
|
email:
|
71
71
|
- varunsrin@gmail.com
|
72
72
|
executables: []
|
@@ -81,14 +81,21 @@ files:
|
|
81
81
|
- README.md
|
82
82
|
- ROADMAP.md
|
83
83
|
- Rakefile
|
84
|
+
- circle.yml
|
84
85
|
- lib/zappa.rb
|
86
|
+
- lib/zappa/clip.rb
|
85
87
|
- lib/zappa/errors.rb
|
86
|
-
- lib/zappa/segment.rb
|
87
88
|
- lib/zappa/version.rb
|
88
89
|
- lib/zappa/wave.rb
|
90
|
+
- lib/zappa/wave/format.rb
|
91
|
+
- lib/zappa/wave/riff_header.rb
|
92
|
+
- lib/zappa/wave/sub_chunk.rb
|
89
93
|
- spec/audio/basic-5s.wav
|
90
|
-
- spec/
|
94
|
+
- spec/clip_spec.rb
|
91
95
|
- spec/spec_helper.rb
|
96
|
+
- spec/wave/format_spec.rb
|
97
|
+
- spec/wave/riff_header_spec.rb
|
98
|
+
- spec/wave/sub_chunk_spec.rb
|
92
99
|
- spec/wave_spec.rb
|
93
100
|
- zappa.gemspec
|
94
101
|
homepage: ''
|
@@ -117,6 +124,9 @@ specification_version: 4
|
|
117
124
|
summary: Ruby gem for manipulating audio files.
|
118
125
|
test_files:
|
119
126
|
- spec/audio/basic-5s.wav
|
120
|
-
- spec/
|
127
|
+
- spec/clip_spec.rb
|
121
128
|
- spec/spec_helper.rb
|
129
|
+
- spec/wave/format_spec.rb
|
130
|
+
- spec/wave/riff_header_spec.rb
|
131
|
+
- spec/wave/sub_chunk_spec.rb
|
122
132
|
- spec/wave_spec.rb
|
data/lib/zappa/segment.rb
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
require 'tempfile'
|
2
|
-
require 'open3'
|
3
|
-
require 'pry'
|
4
|
-
|
5
|
-
module Zappa
|
6
|
-
class Segment
|
7
|
-
attr_reader :source
|
8
|
-
|
9
|
-
def initialize(path = nil)
|
10
|
-
if path
|
11
|
-
from_file(path)
|
12
|
-
else
|
13
|
-
@source = nil
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def from_file(path)
|
18
|
-
@source = Wave.new(safe_copy(path))
|
19
|
-
end
|
20
|
-
|
21
|
-
def to_file(path)
|
22
|
-
raise FileError.new('No data in Segment') if @source.nil?
|
23
|
-
cmd = 'ffmpeg -i ' + @source.file_path + ' -y -f wav ' + path
|
24
|
-
Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
|
25
|
-
raise ('Cannot export to' + path ) unless wait_thr.value.success?
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def ==(other)
|
30
|
-
source == other.source
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
def safe_copy(path)
|
36
|
-
tmp = Tempfile.new('zappa')
|
37
|
-
cmd = 'ffmpeg -i ' + path + ' -vn -y -f wav ' + tmp.path
|
38
|
-
Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
|
39
|
-
raise ('Cannot open file ' + path ) unless wait_thr.value.success?
|
40
|
-
end
|
41
|
-
tmp.path
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
data/spec/segment_spec.rb
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'tempfile'
|
3
|
-
|
4
|
-
WAV_IN = 'spec/audio/basic-5s.wav'
|
5
|
-
|
6
|
-
describe Zappa::Segment do
|
7
|
-
before do
|
8
|
-
subject.from_file(WAV_IN)
|
9
|
-
end
|
10
|
-
|
11
|
-
describe '#from_file' do
|
12
|
-
it 'makes a safe copy of the source wav file' do
|
13
|
-
expect(Zappa::Wave.new(WAV_IN))
|
14
|
-
.to eq(Zappa::Wave.new(subject.source.file_path))
|
15
|
-
end
|
16
|
-
|
17
|
-
it 'raises error if file does not exist' do
|
18
|
-
expect { Zappa::Segment.new('some_foo') }.to raise_error(RuntimeError)
|
19
|
-
end
|
20
|
-
|
21
|
-
pending 'raises error if ffmpeg is not installed'
|
22
|
-
pending 'only permits wav files'
|
23
|
-
end
|
24
|
-
|
25
|
-
describe '#to_file' do
|
26
|
-
it 'exports the segment to a wav file' do
|
27
|
-
tmp = Tempfile.new('zappa-spec')
|
28
|
-
subject.to_file(tmp.path)
|
29
|
-
expect(Zappa::Wave.new(WAV_IN)).to eq(Zappa::Wave.new(tmp.path))
|
30
|
-
end
|
31
|
-
|
32
|
-
it 'raises error if segment is empty' do
|
33
|
-
w = Zappa::Segment.new
|
34
|
-
expect { w.to_file('foo.wav') }.to raise_error(Zappa::FileError)
|
35
|
-
end
|
36
|
-
|
37
|
-
it 'raises error for invalid path' do
|
38
|
-
expect { subject.to_file('some:foo') }.to raise_error(RuntimeError)
|
39
|
-
end
|
40
|
-
|
41
|
-
pending 'raises error if ffmpeg is not installed'
|
42
|
-
end
|
43
|
-
end
|