zappa 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Circle CI](https://circleci.com/gh/varunsrin/zappa/tree/dev.svg?style=svg)](https://circleci.com/gh/varunsrin/zappa/tree/dev)
|
3
|
+
[![Coverage Status](https://coveralls.io/repos/varunsrin/zappa/badge.svg?branch=dev)](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
|