zappa 0.2.0 → 0.3.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/Gemfile +0 -4
- data/README.md +30 -3
- data/ROADMAP.md +1 -3
- data/lib/zappa.rb +2 -0
- data/lib/zappa/clip.rb +43 -19
- data/lib/zappa/generator.rb +56 -0
- data/lib/zappa/processor.rb +62 -0
- data/lib/zappa/version.rb +1 -1
- data/lib/zappa/wave.rb +49 -30
- data/lib/zappa/wave/format.rb +2 -2
- data/lib/zappa/wave/sub_chunk_header.rb +24 -0
- data/lib/zappa/wave/wave_data.rb +21 -0
- data/spec/audio/sine-1000hz.wav +0 -0
- data/spec/clip_spec.rb +36 -21
- data/spec/generator_spec.rb +27 -0
- data/spec/processor_spec.rb +56 -0
- data/spec/spec_helper.rb +4 -1
- data/spec/wave/format_spec.rb +2 -2
- data/spec/wave/riff_header_spec.rb +2 -2
- data/spec/wave/sub_chunk_header_spec.rb +20 -0
- data/spec/wave/wave_data_spec.rb +37 -0
- data/spec/wave_spec.rb +36 -33
- data/zappa.gemspec +1 -0
- metadata +30 -5
- data/lib/zappa/wave/sub_chunk.rb +0 -29
- data/spec/wave/sub_chunk_spec.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d6e03448367486a910dea3a2c86dfc92df6b044e
|
4
|
+
data.tar.gz: 06bdff101d6c5e26d05c82998168fdae9937a962
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 205f8539cbe777d690ef19fd22d2418b881c97e4ac8232911132eb0e203da922dcba6082cea7c0f797a0e8dac37a173a1692e1ee0071e751e9bc64ebbe06e102
|
7
|
+
data.tar.gz: 848718e138c6248e49be8da334c948cfd0e97735a8bcbeda45d24043fe18bdb45d4e19236c06dcd27235a59e301c4fbaea50305dbefe588f10f5dc393fefddd5
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
# Zappa
|
2
|
+
[](https://circleci.com/gh/varunsrin/zappa/tree/dev)
|
3
|
+
[](https://coveralls.io/r/varunsrin/zappa?branch=dev)
|
2
4
|
|
3
5
|
Zappa is a high level audio manipulation library for Ruby, inspired by pydub.
|
4
6
|
|
@@ -21,9 +23,10 @@ Or install it yourself as:
|
|
21
23
|
$ gem install zappa
|
22
24
|
|
23
25
|
## Usage
|
26
|
+
At the core of Zappa is the Clip, an immutable audio unit.
|
24
27
|
|
25
|
-
|
26
|
-
into a clip:
|
28
|
+
### Importing
|
29
|
+
Import a wav file into a clip:
|
27
30
|
|
28
31
|
require 'zappa'
|
29
32
|
|
@@ -33,6 +36,19 @@ into a clip:
|
|
33
36
|
The clip will create a safe copy of the wav before you can edit it. Remember,
|
34
37
|
clips are immutable so any destructive operations return a new clip.
|
35
38
|
|
39
|
+
|
40
|
+
### Generators
|
41
|
+
|
42
|
+
Alternatively, you can generate your own sounds from scratch:
|
43
|
+
|
44
|
+
generator = Zappa::Generator.new
|
45
|
+
clip = generator.generate('sine', 1000, 1)
|
46
|
+
|
47
|
+
This will create a 1000 Hz sine wave that is 1 second long.
|
48
|
+
|
49
|
+
|
50
|
+
### Editing Clips
|
51
|
+
|
36
52
|
You can slice clips into smaller chunks:
|
37
53
|
|
38
54
|
slice_a = clip.slice(0, 1000) # clip containing 1st second
|
@@ -42,9 +58,20 @@ You can also join clips:
|
|
42
58
|
|
43
59
|
joined_clip = slice_a + slice_b # clip containing 1st and 3rd seconds
|
44
60
|
|
61
|
+
|
62
|
+
### Signal Processing
|
63
|
+
|
64
|
+
Amplify or attenuate clips with the following syntax:
|
65
|
+
|
66
|
+
louder_clip = joined_clip + 2
|
67
|
+
louder_clip = joined_clip.amplify(2)
|
68
|
+
|
69
|
+
|
70
|
+
### Export
|
71
|
+
|
45
72
|
Once you're done editing a clip, you can export it:
|
46
73
|
|
47
|
-
|
74
|
+
louder_clip.export('output.wav')
|
48
75
|
|
49
76
|
That's it for now. DSP tools are coming soon!
|
50
77
|
|
data/ROADMAP.md
CHANGED
@@ -14,9 +14,6 @@
|
|
14
14
|
0.3 - DSP - Basic
|
15
15
|
----------------
|
16
16
|
- Amplify signals by DB
|
17
|
-
- Calculate RMS
|
18
|
-
- Calculate Max
|
19
|
-
- Normalize
|
20
17
|
- Phase Invert
|
21
18
|
|
22
19
|
|
@@ -60,3 +57,4 @@ Release - 1.0 Basic DSP Platform
|
|
60
57
|
- DSP: Compressor
|
61
58
|
- DSP: Remove Silence
|
62
59
|
- DSP: Limiter
|
60
|
+
- DSP: Normalize
|
data/lib/zappa.rb
CHANGED
data/lib/zappa/clip.rb
CHANGED
@@ -1,18 +1,19 @@
|
|
1
1
|
require 'tempfile'
|
2
2
|
require 'open3'
|
3
|
-
require '
|
3
|
+
require 'zappa/processor'
|
4
4
|
|
5
5
|
module Zappa
|
6
6
|
class Clip
|
7
7
|
attr_accessor :wav, :cache
|
8
8
|
|
9
|
-
def initialize(wav=nil)
|
9
|
+
def initialize(wav = nil)
|
10
10
|
if wav
|
11
11
|
@wav = wav
|
12
12
|
else
|
13
13
|
@wav = Wave.new
|
14
14
|
end
|
15
15
|
@cache = nil
|
16
|
+
@processor = Processor.new
|
16
17
|
end
|
17
18
|
|
18
19
|
def from_file(path)
|
@@ -28,26 +29,49 @@ module Zappa
|
|
28
29
|
end
|
29
30
|
end
|
30
31
|
|
31
|
-
def slice(
|
32
|
-
slice_samples(ms_to_samples(
|
32
|
+
def slice(pos, len)
|
33
|
+
slice_samples(ms_to_samples(pos), ms_to_samples(len))
|
33
34
|
end
|
34
35
|
|
35
|
-
def slice_samples(
|
36
|
-
fail 'invalid index' if
|
37
|
-
|
38
|
-
from *= @wav.frame_size
|
39
|
-
to *= @wav.frame_size
|
40
|
-
length = (to - from)
|
41
|
-
slice = @wav.data.byteslice(from, length)
|
36
|
+
def slice_samples(pos, len)
|
37
|
+
fail 'invalid index' if pos < 0 || (pos + len) > @wav.sample_count
|
38
|
+
slice = @wav.samples[pos, len]
|
42
39
|
clone(slice)
|
43
40
|
end
|
44
41
|
|
45
42
|
def +(other)
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
43
|
+
return amplify(other) if other.class == Fixnum
|
44
|
+
|
45
|
+
if other.class == Zappa::Clip
|
46
|
+
fail 'format mismatch' unless @wav.format == other.wav.format
|
47
|
+
w = Wave.new
|
48
|
+
w.format = @wav.format
|
49
|
+
samples = []
|
50
|
+
samples += @wav.samples if @wav.samples
|
51
|
+
samples += other.wav.samples if other.wav.samples
|
52
|
+
w.set_samples(samples)
|
53
|
+
return Clip.new(w)
|
54
|
+
end
|
55
|
+
|
56
|
+
fail "cannot add Zappa::Clip to #{other.class}"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Processor Wrappers
|
60
|
+
|
61
|
+
def normalize(headroom)
|
62
|
+
clone(@processor.normalize(@wav.samples, headroom))
|
63
|
+
end
|
64
|
+
|
65
|
+
def compress(ratio = 2.0, threshold = - 20.0)
|
66
|
+
clone(@processor.compress(@wav.samples, ratio, threshold))
|
67
|
+
end
|
68
|
+
|
69
|
+
def amplify(db)
|
70
|
+
clone(@processor.amplify(@wav.samples, db))
|
71
|
+
end
|
72
|
+
|
73
|
+
def invert
|
74
|
+
clone(@processor.invert(@wav.samples))
|
51
75
|
end
|
52
76
|
|
53
77
|
private
|
@@ -58,7 +82,7 @@ module Zappa
|
|
58
82
|
|
59
83
|
def persist_cache
|
60
84
|
tmp = Tempfile.new('zappa')
|
61
|
-
@cache = tmp.path
|
85
|
+
@cache = tmp.path
|
62
86
|
File.write(@cache, @wav.pack)
|
63
87
|
end
|
64
88
|
|
@@ -70,10 +94,10 @@ module Zappa
|
|
70
94
|
destination
|
71
95
|
end
|
72
96
|
|
73
|
-
def clone(
|
97
|
+
def clone(samples = nil)
|
74
98
|
clone = Clip.new
|
75
99
|
clone.wav = Marshal.load(Marshal.dump(@wav))
|
76
|
-
clone.wav.
|
100
|
+
clone.wav.set_samples(samples) if samples
|
77
101
|
clone
|
78
102
|
end
|
79
103
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Zappa
|
2
|
+
class Generator
|
3
|
+
attr_accessor :sample_rate, :channels, :bit_depth
|
4
|
+
|
5
|
+
def initialize(sample_rate = 44_100, channels = 2, bit_depth = 16)
|
6
|
+
@sample_rate = sample_rate
|
7
|
+
@channels = channels
|
8
|
+
@bit_depth = bit_depth
|
9
|
+
@max_amplitude = ((2**bit_depth) / 2) - 1
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate(type, frequency, length)
|
13
|
+
types = %w(sine square sawtooth white_noise)
|
14
|
+
fail "Cannot generate #{type} wave" unless types.include?(type)
|
15
|
+
|
16
|
+
samples = []
|
17
|
+
wave_pos = 0.0
|
18
|
+
wave_delta = frequency.to_f / @sample_rate.to_f
|
19
|
+
num_samples = (length * @sample_rate).round
|
20
|
+
|
21
|
+
num_samples.times do |i|
|
22
|
+
wave_value = send(type, wave_pos)
|
23
|
+
abs_value = (wave_value * @max_amplitude).round
|
24
|
+
samples[i] = [abs_value] * @channels
|
25
|
+
wave_pos += wave_delta
|
26
|
+
wave_pos -= 1.0 if wave_pos >= 1.0
|
27
|
+
# TODO: - account for skips >= 2.0
|
28
|
+
end
|
29
|
+
clip_from_samples(samples)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def clip_from_samples(samples)
|
35
|
+
wave = Zappa::Wave.new
|
36
|
+
wave.set_samples(samples)
|
37
|
+
Zappa::Clip.new(wave)
|
38
|
+
end
|
39
|
+
|
40
|
+
def sine(position)
|
41
|
+
Math.sin(position * 2 * Math::PI)
|
42
|
+
end
|
43
|
+
|
44
|
+
def square(position)
|
45
|
+
position < 0.5 ? 1 : -1
|
46
|
+
end
|
47
|
+
|
48
|
+
def sawtooth(position)
|
49
|
+
2 * (position - (0.5 + position).floor)
|
50
|
+
end
|
51
|
+
|
52
|
+
def white_noise(_position)
|
53
|
+
rand(-1.0..1.0)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Zappa
|
2
|
+
class Processor
|
3
|
+
def normalize(samples, headroom)
|
4
|
+
fail 'headroom cannot be positive' if headroom > 0.0
|
5
|
+
curr_peak = max_sample(samples)
|
6
|
+
targ_peak = 32_768 * db_to_float(headroom) # calculate this constants
|
7
|
+
ratio = (targ_peak / curr_peak)
|
8
|
+
mul_samples(samples, ratio)
|
9
|
+
end
|
10
|
+
|
11
|
+
def amplify(samples, db) # fix order
|
12
|
+
mul_samples(samples, db_to_float(db))
|
13
|
+
end
|
14
|
+
|
15
|
+
def invert(samples)
|
16
|
+
mul_samples(samples, -1)
|
17
|
+
end
|
18
|
+
|
19
|
+
def compress(samples, ratio, threshold)
|
20
|
+
threshold_value = 32_768 * db_to_float(threshold) # calc this somehow
|
21
|
+
samples.each do |f|
|
22
|
+
f.map! do |s|
|
23
|
+
if s.abs > threshold_value
|
24
|
+
s += (threshold_value - s) / ratio if s > 0
|
25
|
+
s -= (s + threshold_value) / ratio if s < 0
|
26
|
+
end
|
27
|
+
s.round
|
28
|
+
end
|
29
|
+
end
|
30
|
+
samples
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def mul_samples(samples, factor)
|
36
|
+
samples.map { |f| mul_frame(f, factor) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def mul_frame(frame, factor)
|
40
|
+
frame.map { |s| clip((s * factor).round) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def clip(value, max = 32_768)
|
44
|
+
return max if value > max
|
45
|
+
return -max if value < (-max)
|
46
|
+
value
|
47
|
+
end
|
48
|
+
|
49
|
+
def max_sample(samples)
|
50
|
+
curr_max = 0
|
51
|
+
samples.each do |f|
|
52
|
+
f.each { |s| curr_max = s.abs if s.abs > curr_max }
|
53
|
+
end
|
54
|
+
curr_max
|
55
|
+
end
|
56
|
+
|
57
|
+
# convert db values to floats
|
58
|
+
def db_to_float(db)
|
59
|
+
10**(db / 20)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/zappa/version.rb
CHANGED
data/lib/zappa/wave.rb
CHANGED
@@ -1,21 +1,22 @@
|
|
1
1
|
require 'zappa/wave/format'
|
2
2
|
require 'zappa/wave/riff_header'
|
3
|
-
require 'zappa/wave/
|
3
|
+
require 'zappa/wave/sub_chunk_header'
|
4
|
+
require 'zappa/wave/wave_data'
|
4
5
|
|
5
6
|
# WAV Spec: http://soundfile.sapp.org/doc/WaveFormat/
|
6
7
|
|
7
8
|
module Zappa
|
8
9
|
class Wave
|
9
|
-
attr_accessor :header, :format
|
10
|
+
attr_accessor :header, :format, :wave_data
|
10
11
|
|
11
12
|
def initialize
|
12
13
|
@header = RiffHeader.new
|
13
14
|
@format = Format.new
|
14
|
-
@wave_data =
|
15
|
+
@wave_data = WaveData.new
|
15
16
|
end
|
16
17
|
|
17
|
-
def
|
18
|
-
@wave_data.
|
18
|
+
def samples
|
19
|
+
@wave_data.samples
|
19
20
|
end
|
20
21
|
|
21
22
|
def data_size
|
@@ -31,44 +32,62 @@ module Zappa
|
|
31
32
|
end
|
32
33
|
|
33
34
|
def ==(other)
|
34
|
-
other.
|
35
|
-
end
|
36
|
-
|
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
|
35
|
+
other.wave_data == wave_data
|
43
36
|
end
|
44
37
|
|
45
38
|
def pack
|
46
|
-
pack = @header.pack + @format.pack
|
39
|
+
pack = @header.pack + @format.pack
|
40
|
+
pack += @wave_data.chunk_id
|
41
|
+
pack += [@wave_data.chunk_size].pack('V')
|
42
|
+
pack += pack_samples(@wave_data.samples)
|
43
|
+
pack
|
47
44
|
end
|
48
45
|
|
49
46
|
def unpack(source)
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
data_found = true
|
63
|
-
end
|
47
|
+
file = File.open(path_to(source), 'rb')
|
48
|
+
rescue
|
49
|
+
raise 'Unable to open WAV file'
|
50
|
+
else
|
51
|
+
@header = RiffHeader.new(file)
|
52
|
+
@format = Format.new(file)
|
53
|
+
while sc_header = file.read(8)
|
54
|
+
s = SubChunkHeader.new(sc_header)
|
55
|
+
if s.chunk_id == 'data'
|
56
|
+
unpack_samples(file)
|
57
|
+
else
|
58
|
+
file.read(s.chunk_size)
|
64
59
|
end
|
65
60
|
end
|
66
61
|
end
|
67
62
|
|
68
|
-
def
|
63
|
+
def set_samples(samples)
|
64
|
+
samples_change = (samples.size - @wave_data.samples.size)
|
65
|
+
size_change = samples_change * @format.channels * 2
|
66
|
+
@header.chunk_size += size_change
|
67
|
+
@wave_data.set_samples(samples)
|
68
|
+
end
|
69
|
+
|
70
|
+
def path_to(source) # Private method?
|
69
71
|
return source if source.class == String
|
70
72
|
return source.path if source.class == File
|
71
73
|
fail 'cannot unpack type: ' + source.class.to_s
|
72
74
|
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def pack_samples(samples)
|
79
|
+
pack_str = 's' * @format.channels
|
80
|
+
samples.map { |f| f.pack(pack_str) }.join
|
81
|
+
end
|
82
|
+
|
83
|
+
def unpack_samples(file)
|
84
|
+
samples = []
|
85
|
+
size = @format.bits_per_sample / 8
|
86
|
+
ch = @format.channels
|
87
|
+
while (frame_data = file.read(size * ch))
|
88
|
+
samples << frame_data.unpack('s' * ch)
|
89
|
+
end
|
90
|
+
@wave_data.set_samples(samples)
|
91
|
+
end
|
73
92
|
end
|
74
93
|
end
|
data/lib/zappa/wave/format.rb
CHANGED
@@ -11,7 +11,7 @@ module Zappa
|
|
11
11
|
@chunk_size = FMT_SIZE
|
12
12
|
@audio_format = 1
|
13
13
|
@channels = 2
|
14
|
-
@sample_rate =
|
14
|
+
@sample_rate = 44_100
|
15
15
|
@byte_rate = 176_400
|
16
16
|
@block_align = 4
|
17
17
|
@bits_per_sample = 16
|
@@ -46,4 +46,4 @@ module Zappa
|
|
46
46
|
@bits_per_sample = data.byteslice(14..15).unpack('v').first
|
47
47
|
end
|
48
48
|
end
|
49
|
-
end
|
49
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Zappa
|
2
|
+
class SubChunkHeader
|
3
|
+
attr_accessor :chunk_id, :chunk_size
|
4
|
+
|
5
|
+
def initialize(data = nil)
|
6
|
+
@chunk_id = nil
|
7
|
+
@chunk_size = 0
|
8
|
+
unpack(data) if data
|
9
|
+
end
|
10
|
+
|
11
|
+
def pack
|
12
|
+
@chunk_id + [@chunk_size].pack('V')
|
13
|
+
end
|
14
|
+
|
15
|
+
def unpack(data)
|
16
|
+
@chunk_id = data.byteslice(0, 4)
|
17
|
+
@chunk_size = data.byteslice(4, 4).unpack('V').first
|
18
|
+
end
|
19
|
+
|
20
|
+
def ==(other)
|
21
|
+
other.chunk_size == @chunk_size && other.chunk_id = @chunk_id
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Zappa
|
2
|
+
class WaveData
|
3
|
+
attr_reader :samples, :chunk_id, :chunk_size
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@chunk_id = 'data'
|
7
|
+
@chunk_size = 0
|
8
|
+
@samples = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def set_samples(samples)
|
12
|
+
@samples = samples
|
13
|
+
frame_size = samples[1].size
|
14
|
+
@chunk_size = @samples.size * frame_size * 2
|
15
|
+
end
|
16
|
+
|
17
|
+
def ==(other)
|
18
|
+
other.chunk_size == @chunk_size && other.chunk_id == @chunk_id
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
Binary file
|
data/spec/clip_spec.rb
CHANGED
@@ -2,10 +2,10 @@ require 'spec_helper'
|
|
2
2
|
require 'tempfile'
|
3
3
|
|
4
4
|
WAV_IN = 'spec/audio/basic-5s.wav'
|
5
|
-
WAV_IN_DATA_SIZE =
|
5
|
+
WAV_IN_DATA_SIZE = 882_000
|
6
6
|
|
7
7
|
describe Zappa::Clip do
|
8
|
-
before
|
8
|
+
before do
|
9
9
|
subject.from_file(WAV_IN)
|
10
10
|
end
|
11
11
|
|
@@ -44,10 +44,9 @@ describe Zappa::Clip do
|
|
44
44
|
end
|
45
45
|
|
46
46
|
it 'exports the clip correctly' do
|
47
|
-
subject.from_file(WAV_IN)
|
48
47
|
export_wav = Zappa::Wave.new
|
49
48
|
export_wav.unpack(File.open(@tmp.path, 'rb'))
|
50
|
-
expect(subject.wav).to eq(
|
49
|
+
expect(subject.wav == export_wav).to eq(true)
|
51
50
|
end
|
52
51
|
|
53
52
|
it 'raises error for invalid path' do
|
@@ -59,19 +58,15 @@ describe Zappa::Clip do
|
|
59
58
|
|
60
59
|
describe '#slice_samples' do
|
61
60
|
before :each do
|
62
|
-
@slice = subject.slice_samples(4,
|
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)
|
61
|
+
@slice = subject.slice_samples(4, 4)
|
67
62
|
end
|
68
63
|
|
69
64
|
it 'fails if the beginning is negative' do
|
70
|
-
expect { subject.slice_samples(-1,2) }.to raise_error(RuntimeError)
|
65
|
+
expect { subject.slice_samples(-1, 2) }.to raise_error(RuntimeError)
|
71
66
|
end
|
72
67
|
|
73
|
-
it 'fails if the
|
74
|
-
expect { subject.slice_samples(
|
68
|
+
it 'fails if the length exceeds the wave\'s length' do
|
69
|
+
expect { subject.slice_samples(1, WAV_IN_DATA_SIZE) }
|
75
70
|
.to raise_error(RuntimeError)
|
76
71
|
end
|
77
72
|
|
@@ -101,15 +96,35 @@ describe Zappa::Clip do
|
|
101
96
|
end
|
102
97
|
|
103
98
|
describe '#+' do
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
99
|
+
context 'concatenation' do
|
100
|
+
it 'adds two audio clips together' do
|
101
|
+
combined_clip = subject + subject
|
102
|
+
expect(combined_clip.wav.data_size).to be(WAV_IN_DATA_SIZE * 2)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'adds audio clip to empty clip' do
|
106
|
+
new_clip = Zappa::Clip.new
|
107
|
+
combined_clip = subject + new_clip
|
108
|
+
expect(combined_clip.wav.data_size).to be(WAV_IN_DATA_SIZE)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'fails if the wave formats are different' do
|
112
|
+
subject_copy = Marshal.load(Marshal.dump(subject))
|
113
|
+
subject_copy.wav.format.sample_rate = 22_000
|
114
|
+
expect { subject + subject_copy }.to raise_error(RuntimeError)
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'fails if added to a non-wave object' do
|
118
|
+
non_wave = Object.new
|
119
|
+
expect { subject + non_wave }.to raise_error(RuntimeError)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context 'amplification' do
|
124
|
+
it 'amplifies clip when added to integer' do
|
125
|
+
expect(subject).to receive(:amplify).with(2)
|
126
|
+
subject + 2
|
127
|
+
end
|
113
128
|
end
|
114
129
|
end
|
115
130
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zappa::Generator do
|
4
|
+
let(:subject) { Zappa::Generator.new(44_100, 1, 16) }
|
5
|
+
let(:sine_path) { 'spec/audio/sine-1000hz.wav' }
|
6
|
+
|
7
|
+
describe '#generate' do
|
8
|
+
it 'raises an error for unknown types' do
|
9
|
+
expect { subject.generate('circle', 1000, 0.01) }
|
10
|
+
.to raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'generates a 1000 Hz sine wave' do
|
14
|
+
file = File.open(sine_path, 'rb')
|
15
|
+
orig_wav = Zappa::Wave.new
|
16
|
+
orig_wav.unpack(file)
|
17
|
+
|
18
|
+
gen_clip = subject.generate('sine', 1000, 0.01)
|
19
|
+
expect(orig_wav).to eq(gen_clip.wav)
|
20
|
+
end
|
21
|
+
|
22
|
+
# generated sawtooth, square waves have slightly diff values
|
23
|
+
# from audacity generated waves. why?
|
24
|
+
pending 'generates a 1000 Hz sawtooth wave'
|
25
|
+
pending 'generates a 1000 Hz square wave'
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zappa::Processor do
|
4
|
+
let(:subject) { Zappa::Processor.new }
|
5
|
+
let(:samples) { [[0, -1], [24_000, -24_000]] }
|
6
|
+
let(:max_val) { 32_768 }
|
7
|
+
let(:min_val) { -32_768 }
|
8
|
+
|
9
|
+
describe '#amplify' do
|
10
|
+
let(:double_factor) { 6.020599913279623 } # double_factor db == 2x linear
|
11
|
+
|
12
|
+
before do
|
13
|
+
@amplified = subject.amplify(samples, double_factor)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'doubles sample values' do
|
17
|
+
expect(@amplified[0]).to eq(samples[0].collect { |s| s * 2 })
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'does not let sample values go over the maximum value' do
|
21
|
+
expect(@amplified[1][0]).to eq(max_val)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'does not let sample values go under the minimum value' do
|
25
|
+
expect(@amplified[1][1]).to eq(min_val)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#invert' do
|
30
|
+
it 'inverts all sample values' do
|
31
|
+
inverted = subject.invert(samples)
|
32
|
+
expect(inverted).to eq([[0, 1], [-24_000, 24_000]])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#normalize' do
|
37
|
+
it 'normalizes all sample values' do
|
38
|
+
normalized = subject.normalize(samples, -0.1)
|
39
|
+
expect(normalized).to eq([[0, -1], [32_393, -32_393]])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#compressor' do
|
44
|
+
before do
|
45
|
+
@compressed = subject.compress(samples, 4.0, -20.0)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'does not affect values below the threshold' do
|
49
|
+
expect(@compressed[0]).to eq([0, -1])
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'affects values above the threshold according to the ratio' do
|
53
|
+
expect(@compressed[1]).to eq([18_819, -18_819])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/wave/format_spec.rb
CHANGED
@@ -8,12 +8,12 @@ describe Zappa::Format do
|
|
8
8
|
it 'unpacks and packs each format chunk correctly' do
|
9
9
|
src = File.read(wav_path)
|
10
10
|
src_fmt = src.byteslice(src_offset, fmt_size)
|
11
|
-
|
11
|
+
|
12
12
|
file = File.open(wav_path, 'rb')
|
13
13
|
file.read(src_offset)
|
14
14
|
fmt = Zappa::Format.new(file)
|
15
15
|
pck_fmt = fmt.pack.force_encoding('UTF-8')
|
16
|
-
|
16
|
+
|
17
17
|
expect(src_fmt).to eq(pck_fmt)
|
18
18
|
end
|
19
19
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zappa::SubChunkHeader do
|
4
|
+
OFFSET = 36
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
file = File.read('spec/audio/basic-5s.wav')
|
8
|
+
@sc_header = file.byteslice(OFFSET, 8)
|
9
|
+
subject.unpack(@sc_header)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'unpacks subchunk data correctly' do
|
13
|
+
expect(subject.chunk_id).to eq('data')
|
14
|
+
expect(subject.chunk_size).to eq(882_000)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'packs subchunk data into a string' do
|
18
|
+
expect(subject.pack).to eq(@sc_header)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zappa::WaveData do
|
4
|
+
before :each do
|
5
|
+
@samples = [[7, 5], [1, 3], [4, 4]]
|
6
|
+
@dummy_samples = [[1, 2], [3, 4]]
|
7
|
+
subject.set_samples(@samples)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'set samples' do
|
11
|
+
before :each do
|
12
|
+
subject.set_samples(@dummy_samples)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'recalculates size correctly' do
|
16
|
+
expect(subject.chunk_size).to eq(8)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'replaces existing samples with new samples' do
|
20
|
+
expect(subject.samples).to eq(@dummy_samples)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'equality' do
|
25
|
+
it 'is equal if the data is equal' do
|
26
|
+
d = Zappa::WaveData.new
|
27
|
+
d.set_samples(@samples)
|
28
|
+
expect(subject).to eq(d)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'is not equal if the data is not equal' do
|
32
|
+
d = Zappa::WaveData.new
|
33
|
+
d.set_samples(@dummy_samples)
|
34
|
+
expect(subject).not_to eq(d)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/spec/wave_spec.rb
CHANGED
@@ -4,16 +4,20 @@ require 'tempfile'
|
|
4
4
|
describe Zappa::Wave do
|
5
5
|
let(:wav_path) { 'spec/audio/basic-5s.wav' }
|
6
6
|
let(:empty_path) { 'does-not-exist.wav' }
|
7
|
-
let(:wav_data_size) {
|
8
|
-
let(:wav_def_fmt)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
7
|
+
let(:wav_data_size) { 882_000 }
|
8
|
+
let(:wav_def_fmt) do
|
9
|
+
{ audio_format: 1,
|
10
|
+
channels: 2,
|
11
|
+
sample_rate: 44_100,
|
12
|
+
byte_rate: 176_400,
|
13
|
+
block_align: 4,
|
14
|
+
bits_per_sample: 16 }
|
15
|
+
end
|
16
|
+
let(:wav_def_hdr) do
|
17
|
+
{ chunk_id: 'RIFF',
|
18
|
+
chunk_size: 40,
|
19
|
+
format: 'WAVE' }
|
20
|
+
end
|
17
21
|
|
18
22
|
before :each do
|
19
23
|
@file = File.open(wav_path, 'rb')
|
@@ -26,44 +30,43 @@ describe Zappa::Wave do
|
|
26
30
|
w = Zappa::Wave.new
|
27
31
|
wav_def_hdr.each { |h| expect(h[1]).to eq(w.header.send(h[0])) }
|
28
32
|
wav_def_fmt.each { |h| expect(h[1]).to eq(w.format.send(h[0])) }
|
29
|
-
expect(w.data).to eq(nil)
|
30
33
|
expect(w.data_size).to eq(0)
|
34
|
+
expect(w.samples).to eq([])
|
31
35
|
end
|
32
36
|
end
|
33
37
|
|
34
|
-
describe '
|
35
|
-
let (:slice_length) { 4 }
|
36
|
-
|
38
|
+
describe 'unpacks and packs wave data' do
|
37
39
|
before :each do
|
38
|
-
@
|
39
|
-
@
|
40
|
+
@packed = @wav.pack
|
41
|
+
@file_data = File.read(wav_path)
|
40
42
|
end
|
41
43
|
|
42
|
-
it '
|
43
|
-
|
44
|
+
it 'does not alter the format' do
|
45
|
+
packed_fmt = @packed.byteslice(8, 8)
|
46
|
+
fmt = @file_data.byteslice(8, 8)
|
47
|
+
expect(packed_fmt).to eq(fmt)
|
44
48
|
end
|
45
49
|
|
46
|
-
it '
|
47
|
-
|
48
|
-
|
50
|
+
it 'does not alter the wave data' do
|
51
|
+
packed_data = @packed.byteslice(16, wav_data_size - 16).force_encoding('UTF-8')
|
52
|
+
data = @file_data.byteslice(16, wav_data_size - 16)
|
53
|
+
expect(packed_data).to eq(data)
|
49
54
|
end
|
50
55
|
end
|
51
56
|
|
52
|
-
describe '#
|
53
|
-
|
54
|
-
|
57
|
+
describe '#set_samples' do
|
58
|
+
let (:samples) { [[3, 1], [3, 1]] }
|
59
|
+
|
60
|
+
before :each do
|
61
|
+
@wav.set_samples(samples)
|
55
62
|
end
|
56
|
-
end
|
57
63
|
|
58
|
-
|
59
|
-
|
60
|
-
wav_def_fmt.each do |h|
|
61
|
-
expect(h[1]).to eq(@wav.format.send(h[0]))
|
62
|
-
end
|
64
|
+
it 'updates the header correctly' do
|
65
|
+
expect(@wav.header.chunk_size).to eq(44)
|
63
66
|
end
|
64
67
|
|
65
|
-
it '
|
66
|
-
expect(@wav.
|
68
|
+
it 'updates the wave data correctly' do
|
69
|
+
expect(@wav.samples).to eq(samples)
|
67
70
|
end
|
68
71
|
end
|
69
72
|
|
@@ -82,7 +85,7 @@ describe Zappa::Wave do
|
|
82
85
|
end
|
83
86
|
|
84
87
|
it 'is not equal to a wave with different data' do
|
85
|
-
@new_wave.
|
88
|
+
@new_wave.set_samples([3, 1])
|
86
89
|
expect(@wav).not_to eq(@new_wave)
|
87
90
|
end
|
88
91
|
end
|
data/zappa.gemspec
CHANGED
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.3.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-
|
11
|
+
date: 2015-06-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: coveralls
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
69
83
|
description: Zappa is a DSP toolbox for manipulating audio files.
|
70
84
|
email:
|
71
85
|
- varunsrin@gmail.com
|
@@ -85,17 +99,24 @@ files:
|
|
85
99
|
- lib/zappa.rb
|
86
100
|
- lib/zappa/clip.rb
|
87
101
|
- lib/zappa/errors.rb
|
102
|
+
- lib/zappa/generator.rb
|
103
|
+
- lib/zappa/processor.rb
|
88
104
|
- lib/zappa/version.rb
|
89
105
|
- lib/zappa/wave.rb
|
90
106
|
- lib/zappa/wave/format.rb
|
91
107
|
- lib/zappa/wave/riff_header.rb
|
92
|
-
- lib/zappa/wave/
|
108
|
+
- lib/zappa/wave/sub_chunk_header.rb
|
109
|
+
- lib/zappa/wave/wave_data.rb
|
93
110
|
- spec/audio/basic-5s.wav
|
111
|
+
- spec/audio/sine-1000hz.wav
|
94
112
|
- spec/clip_spec.rb
|
113
|
+
- spec/generator_spec.rb
|
114
|
+
- spec/processor_spec.rb
|
95
115
|
- spec/spec_helper.rb
|
96
116
|
- spec/wave/format_spec.rb
|
97
117
|
- spec/wave/riff_header_spec.rb
|
98
|
-
- spec/wave/
|
118
|
+
- spec/wave/sub_chunk_header_spec.rb
|
119
|
+
- spec/wave/wave_data_spec.rb
|
99
120
|
- spec/wave_spec.rb
|
100
121
|
- zappa.gemspec
|
101
122
|
homepage: ''
|
@@ -124,9 +145,13 @@ specification_version: 4
|
|
124
145
|
summary: Ruby gem for manipulating audio files.
|
125
146
|
test_files:
|
126
147
|
- spec/audio/basic-5s.wav
|
148
|
+
- spec/audio/sine-1000hz.wav
|
127
149
|
- spec/clip_spec.rb
|
150
|
+
- spec/generator_spec.rb
|
151
|
+
- spec/processor_spec.rb
|
128
152
|
- spec/spec_helper.rb
|
129
153
|
- spec/wave/format_spec.rb
|
130
154
|
- spec/wave/riff_header_spec.rb
|
131
|
-
- spec/wave/
|
155
|
+
- spec/wave/sub_chunk_header_spec.rb
|
156
|
+
- spec/wave/wave_data_spec.rb
|
132
157
|
- spec/wave_spec.rb
|
data/lib/zappa/wave/sub_chunk.rb
DELETED
@@ -1,29 +0,0 @@
|
|
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/spec/wave/sub_chunk_spec.rb
DELETED
@@ -1,29 +0,0 @@
|
|
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
|