chaussettes 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 09fa2339e4fa515bb12f28671bb4198871087411
4
+ data.tar.gz: bc3d65c38875e6a6c340aabe21fc1103e790e0f3
5
+ SHA512:
6
+ metadata.gz: d1afb30a879e137ccfd6a92815e38f4082238da02aa98fb17f53f255abd6edb918f3ac76f29513ed6c366da33d63e352375fe0f7f98bbbc811e364072e200fc2
7
+ data.tar.gz: 0c87fb7238943af3ace082520b8957c12e29b338575c5484ea2c73cb8a5807188989c648d08cddebed35111bca5247e28b79d443c265951901d4461e699c35fa
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Jamis Buck
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # Chaussettes
2
+
3
+ Chaussettes is a wrapper around the command-line `sox` utility. It enables you
4
+ to manipulate audio by programmatically constructing a sox operation, including
5
+ multiple effect chains and various supported input and output streams.
6
+
7
+
8
+ ## Installation
9
+
10
+ Simply gem install it.
11
+
12
+ $ gem install chaussettes
13
+
14
+ It has no other dependencies, although it won't work
15
+ very well if you don't also install `sox`, which you can grab here: http://sox.sourceforge.net/.
16
+
17
+
18
+ ## Usage
19
+
20
+ To find out information about an existing clip, you can use the
21
+ `Chaussettes::Info` class:
22
+
23
+ ```ruby
24
+ require 'chaussettes'
25
+
26
+ info = Chaussettes::Info.new('hello.mp3')
27
+ p info.duration # 192.3 (seconds)
28
+ ```
29
+
30
+ To programmatically construct a sox command-line, use the `Chaussettes::Clip`
31
+ class:
32
+
33
+ ```ruby
34
+ require 'chaussettes'
35
+
36
+ clip = Chaussettes::Clip.new do |c|
37
+ c.mix
38
+ c.in 'music-track.mp3'
39
+ c.in 'narration.wav'
40
+ c.out 'soundtrack.wav'
41
+ end
42
+
43
+ clip.run
44
+ # sox --combine mix music-track.mp3 narration.wav soundtrack.wav
45
+ ```
46
+
47
+ Each input and output can also accept options, which ought to be chained onto
48
+ the corresponding input:
49
+
50
+ ```ruby
51
+ clip = Chaussettes::Clip.new do |c|
52
+ c.mix
53
+ c.in('music-track.mp3').volume(0.5)
54
+ c.in 'narration.wav'
55
+ c.out('soundtrack.wav').bits(16).rate(44_100).channels(2)
56
+ end
57
+
58
+ clip.run
59
+ # sox --combine mix \
60
+ # --volume 0.5 music-track.mp3 \
61
+ # narration.wav \
62
+ # --bits 16 --rate 44100 --channels 2 soundtrack.wav
63
+ ```
64
+
65
+ Furthermore, you can also chain effects to the clip:
66
+
67
+ ```ruby
68
+ clip = Chaussettes::Clip.new do |c|
69
+ c.mix
70
+ c.in 'music-track.mp3'
71
+ c.in 'narration.wav'
72
+ c.out 'soundtrack.wav'
73
+ c.chain.trim(0, 10).pad(1.5)
74
+ end
75
+ # sox --combine mix \
76
+ # music-track.mp3 narration.wav soundtrack.wav \
77
+ # trim 0 10 pad 1.5
78
+ ```
79
+ _(Note that not all effects supported by sox are implemented by Chaussettes...yet)_
80
+
81
+ And multiple effect chains may be applied, for potentially complex effects:
82
+
83
+ ```ruby
84
+ clip = Chaussettes::Clip.new do |c|
85
+ c.mix
86
+ c.in 'music-track.mp3'
87
+ c.in 'narration.wav'
88
+ c.out 'soundtrack.wav'
89
+ c.chain.trim(0, 10).pad(1.5)
90
+ c.chain.newfile
91
+ c.chain.restart
92
+ end
93
+ # sox --combine mix \
94
+ # music-track.mp3 narration.wav soundtrack.wav \
95
+ # trim 0 10 pad 1.5 : newfile : restart
96
+ ```
97
+
98
+ Inputs may be other clip instances, too:
99
+
100
+ ```ruby
101
+ music = Chaussettes::Clip.new do |c|
102
+ c.in 'music-track.mp3'
103
+ c.out(device: :stdout).type(:wav)
104
+ c.chain.fade(0, 10, 0.5)
105
+ end
106
+
107
+ clip = Chaussettes::Clip.new do |c|
108
+ c.mix
109
+ c.in(music).volume(0.5).type(:wav)
110
+ c.in 'narration.wav'
111
+ c.out 'soundtrack.wav'
112
+ end
113
+ # sox --combine mix \
114
+ # --volume 0.5 --type wav "|sox music-track.mp3 --type wav - fade 0 10 0.5" \
115
+ # narration.wav soundtrack.wav
116
+ ```
117
+
118
+ ## Author
119
+
120
+ Jamis Buck <jamis@jamisbuck.org>
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.test_files = FileList['test/**/*_test.rb']
5
+ end
6
+
7
+ task default: :test
data/examples/drum.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'chaussettes'
2
+
3
+ RHYTHM = [ 2, 1, 1, 2, 1, 0.5, 0.5, 2, 1, 1, 2, 1, 0.5, 0.5, 2 ].freeze
4
+ BEAT = 0.25
5
+
6
+ Chaussettes::Clip.new do |clip|
7
+ clip.show_progress(false)
8
+ clip.in(device: nil)
9
+
10
+ RHYTHM.each do |dur|
11
+ chain = clip.chain
12
+ chain.synth(BEAT * dur, :pluck) do |t|
13
+ t.start_tone nil, p1: 0.1
14
+ end
15
+ chain.synth(BEAT * dur, :noise) do |t|
16
+ t.combine :fmod
17
+ end
18
+ end
19
+
20
+ puts clip.command('play')
21
+ clip.play
22
+ end
@@ -0,0 +1,65 @@
1
+ # ruby ducking.rb track1 track2 output delay
2
+ #
3
+ # Mixes two tracks by reducing the volume of the first during the duration
4
+ # of the second. The second track begins after delay seconds. Saves the
5
+ # result as output.
6
+
7
+ require 'chaussettes'
8
+
9
+ track1 = ARGV[0] || abort("what's the first track?")
10
+ track2 = ARGV[1] || abort("what's the second track?")
11
+ delay = ARGV[2] || abort("what's the delay in seconds?")
12
+ output = ARGV[3] || abort("what's the output filename?")
13
+
14
+ info1 = Chaussettes::Info.new(track1)
15
+ info2 = Chaussettes::Info.new(track2)
16
+ delay = delay.to_f
17
+
18
+ if info1.duration < info2.duration + delay
19
+ abort 'first track is shorter than the second track!'
20
+ end
21
+
22
+ intro = Chaussettes::Clip.new do |clip|
23
+ clip.in(track1)
24
+ clip.out(device: :stdout).type(:wav)
25
+ clip.chain.fade(0, delay + 0.2, 0.4, type: :linear)
26
+ end
27
+
28
+ middle = Chaussettes::Clip.new do |clip|
29
+ clip.in(track1)
30
+ clip.out(device: :stdout).type(:wav)
31
+ clip.chain.
32
+ trim(delay, info2.duration).
33
+ vol(0.25).
34
+ pad(delay)
35
+ end
36
+
37
+ last = Chaussettes::Clip.new do |clip|
38
+ clip.in(track1)
39
+ clip.out(device: :stdout).type(:wav)
40
+ clip.chain.
41
+ trim(delay + info2.duration - 0.2).
42
+ fade(0.4, type: :linear).
43
+ pad(delay + info2.duration - 0.2)
44
+ end
45
+
46
+ overlay = Chaussettes::Clip.new do |clip|
47
+ clip.in(track2)
48
+ clip.out(device: :stdout).type(:wav)
49
+ clip.chain.pad(delay)
50
+ end
51
+
52
+ Chaussettes::Clip.new do |clip|
53
+ clip.mix
54
+
55
+ clip.in(intro).type(:wav)
56
+ clip.in(middle).type(:wav)
57
+ clip.in(last).type(:wav)
58
+ clip.in(overlay).type(:wav)
59
+
60
+ clip.out(output)
61
+ # ramp up the volume x4 to compensate for 4 clips being merged together
62
+ clip.chain.vol 4
63
+
64
+ clip.run
65
+ end
data/examples/synth.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'chaussettes'
2
+
3
+ Chaussettes::Clip.new do |clip|
4
+ clip.show_progress false
5
+ clip.in(device: nil)
6
+
7
+ notes = %w(C3 E3 G3 C4)
8
+ 20.times do
9
+ clip.chain.synth(0.2, :pluck) do |t|
10
+ t.start_tone notes.sample, bias: 0, shift: rand(100), p1: 2
11
+ end.synth(0.2, :pluck) do |t|
12
+ t.combine :mix
13
+ t.start_tone notes.sample, bias: 0, shift: rand(100), p1: 2
14
+ end
15
+ end
16
+
17
+ # show the command that will be run
18
+ puts clip.command('play')
19
+
20
+ # play it!
21
+ clip.play
22
+ end
@@ -0,0 +1,2 @@
1
+ require 'chaussettes/clip'
2
+ require 'chaussettes/info'
@@ -0,0 +1,119 @@
1
+ require 'chaussettes/input'
2
+ require 'chaussettes/output'
3
+ require 'chaussettes/effect_chain'
4
+ require 'chaussettes/tool'
5
+
6
+ module Chaussettes
7
+
8
+ # encapsulates an invocation of sox
9
+ class Clip
10
+ def initialize
11
+ @combine = nil
12
+ @globals = []
13
+ @inputs = []
14
+ @output = nil
15
+ @effect_chains = []
16
+
17
+ yield self if block_given?
18
+ end
19
+
20
+ def command(cmd = 'sox')
21
+ raise 'a clip requires at least one input' if @inputs.empty?
22
+ Tool.new(cmd).tap { |sox| _build_command(sox) }
23
+ end
24
+
25
+ def run
26
+ system(command.to_s)
27
+ self
28
+ end
29
+
30
+ def play
31
+ system(command('play').to_s)
32
+ self
33
+ end
34
+
35
+ def _build_command(sox)
36
+ sox << '--combine' << @combine if @combine
37
+ _append_globals sox
38
+ _append_inputs sox
39
+ _append_output sox
40
+ _append_effects sox
41
+ end
42
+
43
+ def _append_globals(sox)
44
+ @globals.each { |global| sox << global }
45
+ end
46
+
47
+ def _append_inputs(sox)
48
+ @inputs.each { |input| sox.concat(input.commands) }
49
+ end
50
+
51
+ def _append_output(sox)
52
+ sox.concat(@output.commands) if @output
53
+ end
54
+
55
+ def _append_effects(sox)
56
+ @effect_chains.each.with_index do |chain, idx|
57
+ sox << ':' if idx > 0
58
+ sox.concat(chain.commands)
59
+ end
60
+ end
61
+
62
+ def combine(method)
63
+ @combine = method
64
+ self
65
+ end
66
+
67
+ def guard
68
+ @globals << '--guard'
69
+ self
70
+ end
71
+
72
+ def merge
73
+ @combine = :merge
74
+ self
75
+ end
76
+
77
+ def mix
78
+ @combine = :mix
79
+ self
80
+ end
81
+
82
+ def multiply
83
+ @combine = :multiply
84
+ self
85
+ end
86
+
87
+ def show_progress(flag)
88
+ @globals << (flag ? '--show-progress' : '--no-show-progress')
89
+ self
90
+ end
91
+
92
+ def repeatable
93
+ @globals << '-R'
94
+ self
95
+ end
96
+
97
+ def verbose(level)
98
+ @globals << "-V#{level}"
99
+ self
100
+ end
101
+
102
+ def in(source = nil, device: nil)
103
+ Input.new(source, device: device).tap do |input|
104
+ @inputs << input
105
+ end
106
+ end
107
+
108
+ def out(dest = nil, device: nil)
109
+ @output = Output.new(dest, device: device)
110
+ end
111
+
112
+ def chain
113
+ EffectChain.new.tap do |chain|
114
+ @effect_chains << chain
115
+ end
116
+ end
117
+ end
118
+
119
+ end
@@ -0,0 +1,46 @@
1
+ module Chaussettes
2
+
3
+ # options that are common between input and output files
4
+ module CommonOptions
5
+ def bits(bits)
6
+ @arguments << '--bits' << bits
7
+ self
8
+ end
9
+
10
+ def channels(channels)
11
+ @arguments << '--channels' << channels
12
+ self
13
+ end
14
+
15
+ def encoding(encoding)
16
+ @arguments << '--encoding' << encoding
17
+ self
18
+ end
19
+
20
+ def rate(rate)
21
+ @arguments << '--rate' << rate
22
+ self
23
+ end
24
+
25
+ def type(type)
26
+ @arguments << '--type' << type
27
+ self
28
+ end
29
+
30
+ def endian(option)
31
+ @arguments << '--endian' << option
32
+ self
33
+ end
34
+
35
+ def reverse_nibbles
36
+ @arguments << '--reverse-nibbles'
37
+ self
38
+ end
39
+
40
+ def reverse_bits
41
+ @arguments << '--reverse-bits'
42
+ self
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,31 @@
1
+ module Chaussettes
2
+ module Effect
3
+
4
+ # Represents a fade effect
5
+ class Fade
6
+ TYPE_MAP = {
7
+ nil => nil,
8
+ :h => 'h', 'h' => 'h', :half_sine => 'h',
9
+ :l => 'l', 'l' => 'l', :log => 'l', :logarithmic => 'l',
10
+ :p => 'p', 'p' => 'p', :parabola => 'p', :inverted_parabola => 'p',
11
+ :q => 'q', 'q' => 'q', :quarter => 'q',
12
+ :t => 't', 't' => 't', :linear => 't', :triangle => 't'
13
+ }.freeze
14
+
15
+ attr_reader :commands
16
+
17
+ def initialize(in_len, stop_at = nil, out_len = nil, type: nil)
18
+ real_type = TYPE_MAP.fetch(type)
19
+
20
+ @commands = [ 'fade' ]
21
+ @commands << real_type if real_type
22
+ @commands << in_len
23
+ @commands << stop_at if stop_at
24
+ @commands << out_len if stop_at && out_len
25
+
26
+ @commands.freeze
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ module Chaussettes
2
+ module Effect
3
+
4
+ # Represents a gain effect
5
+ class Gain
6
+ TYPE_MAP = {
7
+ :e => 'e', 'e' => 'e', :equalize => 'e',
8
+ :B => 'B', 'B' => 'B', :balance => 'B',
9
+ :b => 'b', 'b' => 'b', :balance_protect => 'b',
10
+ :r => 'r', 'r' => 'r', :reclaim_headroom => 'r',
11
+ :n => 'n', 'n' => 'n', :normalize => 'n',
12
+ :l => 'l', 'l' => 'l', :limiter => 'l',
13
+ :h => 'h', 'h' => 'h', :headroom => 'h'
14
+ }.freeze
15
+
16
+ attr_reader :commands
17
+
18
+ def initialize(db, *opts)
19
+ @commands = [ 'gain' ]
20
+ @commands.concat(opts.map { |opt| TYPE_MAP.fetch(opt) })
21
+ @commands << db
22
+ @commands.freeze
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,130 @@
1
+ module Chaussettes
2
+ module Effect
3
+
4
+ # Represents a synth effect
5
+ class Synth
6
+ attr_reader :commands
7
+
8
+ def initialize(length = nil, type = nil)
9
+ @length = length
10
+ @type = type
11
+
12
+ @just_intonation = nil
13
+ @combine = nil
14
+ @start_tone = nil
15
+ @end_tone = nil
16
+ @headroom = true
17
+
18
+ yield self if block_given?
19
+
20
+ _build_commands
21
+ end
22
+
23
+ def _build_commands
24
+ @commands = [ 'synth' ]
25
+ @commands << '-j' << @just_intonation if @just_intonation
26
+ @commands << '-n' unless @headroom
27
+
28
+ _append_start_params
29
+
30
+ @commands << @type if @type
31
+ @commands << @combine if @combine
32
+
33
+ _append_tones
34
+
35
+ @commands.freeze
36
+ end
37
+
38
+ def _append_start_params
39
+ return unless @length
40
+ @commands << @length
41
+ _append_opts @start_opts
42
+ end
43
+
44
+ OPT_NAMES = %i(bias shift p1 p2 p3).freeze
45
+ DEFAULTS = { bias: 0, shift: 0 }.freeze
46
+
47
+ def _append_opts(opts)
48
+ return unless opts && opts.any?
49
+
50
+ OPT_NAMES.each do |opt|
51
+ value = opts[opt] || DEFAULTS[opt]
52
+ break unless value
53
+ @commands << value
54
+ end
55
+ end
56
+
57
+ def _append_tones
58
+ return unless @start_tone
59
+
60
+ tone = if @end_tone
61
+ "#{@start_tone}#{@sweep}#{@end_tone}"
62
+ else
63
+ @start_tone
64
+ end
65
+
66
+ @commands << tone
67
+
68
+ _append_opts @end_opts
69
+ end
70
+
71
+ def headroom(enabled)
72
+ @headroom = enabled
73
+ self
74
+ end
75
+
76
+ def just_intonation(semitones)
77
+ @just_intonation = semitones
78
+ self
79
+ end
80
+
81
+ def combine(method)
82
+ @combine = method
83
+ self
84
+ end
85
+
86
+ def length(len)
87
+ @length = len
88
+ self
89
+ end
90
+
91
+ def type(type)
92
+ @type = type
93
+ self
94
+ end
95
+
96
+ # options are:
97
+ # bias:
98
+ # shift:
99
+ # p1:
100
+ # p2:
101
+ # p3:
102
+ def start_tone(start_tone, opts = {})
103
+ @start_tone = start_tone
104
+ @start_opts = opts
105
+ self
106
+ end
107
+
108
+ # options are:
109
+ # bias:
110
+ # shift:
111
+ # p1:
112
+ # p2:
113
+ # p3:
114
+ def end_tone(end_tone, sweep = :linear, opts = {})
115
+ @end_tone = end_tone
116
+ @sweep = SWEEPS.fetch(sweep, sweep)
117
+ @end_opts = opts
118
+ self
119
+ end
120
+
121
+ SWEEPS = {
122
+ linear: ':',
123
+ square: '+',
124
+ exponential: '/', exp: '/',
125
+ exp2: '-'
126
+ }.freeze
127
+ end
128
+
129
+ end
130
+ end
@@ -0,0 +1,18 @@
1
+ module Chaussettes
2
+ module Effect
3
+
4
+ # Represents a volume effect
5
+ class Vol
6
+ attr_reader :commands
7
+
8
+ def initialize(gain, type: nil, limitergain: nil)
9
+ @commands = [ 'vol' ]
10
+ @commands << gain
11
+ @commands << type if type
12
+ @commands << limitergain if type && limitergain
13
+ @commands.freeze
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,67 @@
1
+ require 'chaussettes/effect/fade'
2
+ require 'chaussettes/effect/gain'
3
+ require 'chaussettes/effect/synth'
4
+ require 'chaussettes/effect/vol'
5
+
6
+ module Chaussettes
7
+
8
+ # A chain of effects to apply
9
+ class EffectChain
10
+ attr_reader :commands
11
+
12
+ def initialize
13
+ @commands = []
14
+ end
15
+
16
+ def fade(in_len, stop_at = nil, out_len = nil, type: nil)
17
+ effect = Effect::Fade.new(in_len, stop_at, out_len, type: type)
18
+ @commands.concat(effect.commands)
19
+ self
20
+ end
21
+
22
+ def gain(db, *opts)
23
+ effect = Effect::Gain.new(db, *opts)
24
+ @commands.concat(effect.commands)
25
+ self
26
+ end
27
+
28
+ def newfile
29
+ @commands << 'newfile'
30
+ self
31
+ end
32
+
33
+ def pad(length, position = nil)
34
+ length = "#{length}@#{position}" if position
35
+ @commands << 'pad' << length
36
+ self
37
+ end
38
+
39
+ def restart
40
+ @commands << 'restart'
41
+ self
42
+ end
43
+
44
+ def synth(length = nil, type = nil, &block)
45
+ effect = Effect::Synth.new(length, type, &block)
46
+ @commands.concat(effect.commands)
47
+ self
48
+ end
49
+
50
+ def trim(*positions)
51
+ if positions.empty?
52
+ raise ArgumentError, 'you must specify at least one position for trim'
53
+ end
54
+
55
+ @commands << 'trim'
56
+ @commands.concat(positions)
57
+ self
58
+ end
59
+
60
+ def vol(gain, type: nil, limitergain: nil)
61
+ effect = Effect::Vol.new(gain, type: type, limitergain: limitergain)
62
+ @commands.concat(effect.commands)
63
+ self
64
+ end
65
+ end
66
+
67
+ end
@@ -0,0 +1,57 @@
1
+ require 'chaussettes/tool'
2
+
3
+ module Chaussettes
4
+
5
+ # encapsulates info about an audio file
6
+ class Info
7
+ def initialize(filename)
8
+ command = Tool.new('soxi') << filename
9
+ output = `#{command}`
10
+ @data = _parse(output)
11
+ end
12
+
13
+ def _parse(output)
14
+ output.lines.each.with_object({}) do |line, hash|
15
+ next if line.strip.empty?
16
+ key, value = line.split(/:/, 2)
17
+ hash[key.strip] = value.strip
18
+ end
19
+ end
20
+
21
+ def filename
22
+ @_filename ||= @data['Input File']
23
+ end
24
+
25
+ def channels
26
+ @_channels ||= @data['Channels'].to_i
27
+ end
28
+
29
+ def rate
30
+ @_rate ||= @data['Sample Rate'].to_i
31
+ end
32
+
33
+ def bits
34
+ @_bits ||= @data['Precision'].to_i
35
+ end
36
+
37
+ def duration
38
+ @_duration ||= begin
39
+ timespec = @data['Duration'].split(/ /).first
40
+ h, m, s = timespec.split(/:/)
41
+
42
+ h.to_i * 3600 +
43
+ m.to_i * 60 +
44
+ s.to_f
45
+ end
46
+ end
47
+
48
+ def size
49
+ @_size ||= @data['File Size']
50
+ end
51
+
52
+ def bit_rate
53
+ @_bit_rate ||= @data['Bit Rate']
54
+ end
55
+ end
56
+
57
+ end
@@ -0,0 +1,50 @@
1
+ require 'chaussettes/common_options'
2
+
3
+ module Chaussettes
4
+
5
+ # Represents an input to an operation
6
+ class Input
7
+ include CommonOptions
8
+
9
+ def initialize(source = nil, device: nil)
10
+ @source = _translate_source(source) ||
11
+ _translate_device(device) ||
12
+ raise(ArgumentError, 'unsupported source')
13
+
14
+ @arguments = []
15
+ end
16
+
17
+ def _translate_source(source)
18
+ if source.is_a?(String)
19
+ source
20
+ elsif source.respond_to?(:command)
21
+ "|#{source.command}"
22
+ end
23
+ end
24
+
25
+ def _translate_device(device)
26
+ if device == :default
27
+ '--default-device'
28
+ elsif device == :stdin
29
+ '-'
30
+ elsif device.nil? || device == :null
31
+ '--null'
32
+ end
33
+ end
34
+
35
+ def commands
36
+ [ *@arguments, @source ]
37
+ end
38
+
39
+ def ignore_length
40
+ @arguments << '--ignore-length'
41
+ self
42
+ end
43
+
44
+ def volume(factor)
45
+ @arguments << '--volume' << factor
46
+ self
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,53 @@
1
+ require 'chaussettes/common_options'
2
+
3
+ module Chaussettes
4
+
5
+ # Represents the output of an operation
6
+ class Output
7
+ include CommonOptions
8
+
9
+ def initialize(dest = nil, device: nil)
10
+ @dest = _translate_dest(dest) ||
11
+ _translate_device(device) ||
12
+ raise(ArgumentError, 'unsupported dest/device')
13
+
14
+ @arguments = []
15
+ end
16
+
17
+ def _translate_dest(dest)
18
+ dest ? dest.to_s : nil
19
+ end
20
+
21
+ def _translate_device(device)
22
+ if device == :pipe
23
+ '--sox-pipe'
24
+ elsif device == :default
25
+ '--default-device'
26
+ elsif device.nil? || device == :null
27
+ '--null'
28
+ elsif device == :stdout
29
+ '-'
30
+ end
31
+ end
32
+
33
+ def commands
34
+ [ *@arguments, @dest ]
35
+ end
36
+
37
+ def add_comment(text)
38
+ @arguments << '--add-comment' << text
39
+ self
40
+ end
41
+
42
+ def comment(text)
43
+ @arguments << '--comment' << text
44
+ self
45
+ end
46
+
47
+ def compression(factor)
48
+ @arguments << '--compression' << factor
49
+ self
50
+ end
51
+ end
52
+
53
+ end
@@ -0,0 +1,27 @@
1
+ require 'shellwords'
2
+
3
+ module Chaussettes
4
+
5
+ # a generic wrapper for sox audio toolchain
6
+ class Tool
7
+ def initialize(command)
8
+ @command = command
9
+ @arguments = []
10
+ end
11
+
12
+ def <<(arg)
13
+ @arguments << arg
14
+ self
15
+ end
16
+
17
+ def concat(args)
18
+ @arguments.concat(args)
19
+ self
20
+ end
21
+
22
+ def to_s
23
+ Shellwords.join([ @command, *@arguments ])
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,3 @@
1
+ module Chaussettes
2
+ VERSION = '1.0.0'.freeze
3
+ end
@@ -0,0 +1,78 @@
1
+ require 'minitest/autorun'
2
+ require 'chaussettes/clip'
3
+
4
+ class ClipTest < Minitest::Test
5
+ def setup
6
+ @clip = Chaussettes::Clip.new
7
+ end
8
+
9
+ def test_merge_becomes_combine_option
10
+ @clip.merge
11
+ @clip.in
12
+ @clip.out
13
+
14
+ assert_equal 'sox --combine merge --null --null', @clip.command.to_s
15
+ end
16
+
17
+ def test_mix_becomes_combine_option
18
+ @clip.mix
19
+ @clip.in
20
+ @clip.out
21
+
22
+ assert_equal 'sox --combine mix --null --null', @clip.command.to_s
23
+ end
24
+
25
+ def test_multiply_becomes_combine_option
26
+ @clip.multiply
27
+ @clip.in
28
+ @clip.out
29
+
30
+ assert_equal 'sox --combine multiply --null --null', @clip.command.to_s
31
+ end
32
+
33
+ def test_combine_option
34
+ @clip.in
35
+ @clip.out
36
+ @clip.combine :concatenate
37
+
38
+ assert_equal 'sox --combine concatenate --null --null', @clip.command.to_s
39
+ end
40
+
41
+ def test_global_options
42
+ @clip.guard.repeatable.verbose(2)
43
+ @clip.in
44
+ @clip.out
45
+
46
+ assert_equal 'sox --guard -R -V2 --null --null', @clip.command.to_s
47
+ end
48
+
49
+ def test_multiple_inputs_are_rendered
50
+ @clip.in('file1.wav')
51
+ @clip.in('file2.wav')
52
+ @clip.out
53
+ assert_equal 'sox file1.wav file2.wav --null', @clip.command.to_s
54
+ end
55
+
56
+ def test_output_is_rendered
57
+ @clip.in
58
+ @clip.out('out.wav')
59
+ assert_equal 'sox --null out.wav', @clip.command.to_s
60
+ end
61
+
62
+ def test_chain_adds_effect_chain
63
+ @clip.in
64
+ @clip.out
65
+ @clip.chain.trim(0, 10).pad(1.5)
66
+ assert_equal 'sox --null --null trim 0 10 pad 1.5', @clip.command.to_s
67
+ end
68
+
69
+ def test_multiply_effect_chains
70
+ @clip.in
71
+ @clip.out
72
+ @clip.chain.trim(0, 10).pad(1.5)
73
+ @clip.chain.newfile
74
+ @clip.chain.restart
75
+ assert_equal 'sox --null --null trim 0 10 pad 1.5 : newfile : restart',
76
+ @clip.command.to_s
77
+ end
78
+ end
@@ -0,0 +1,61 @@
1
+ require 'minitest/autorun'
2
+ require 'ostruct'
3
+ require 'chaussettes/input'
4
+
5
+ class InputTest < Minitest::Test
6
+ def test_init_with_string_should_use_string_as_source
7
+ input = Chaussettes::Input.new('input.wav')
8
+ assert_equal [ 'input.wav' ], input.commands
9
+ end
10
+
11
+ def test_init_with_command_should_use_command_as_source
12
+ command = OpenStruct.new(command: 'cmd -x -y -z')
13
+ input = Chaussettes::Input.new(command)
14
+ assert_equal [ '|cmd -x -y -z' ], input.commands
15
+ end
16
+
17
+ def test_init_with_no_arguments_should_use_null_input
18
+ input = Chaussettes::Input.new
19
+ assert_equal [ '--null' ], input.commands
20
+ end
21
+
22
+ def test_init_with_nil_device_should_use_null_input
23
+ input = Chaussettes::Input.new(device: nil)
24
+ assert_equal [ '--null' ], input.commands
25
+ end
26
+
27
+ def test_init_with_null_device_should_use_null_input
28
+ input = Chaussettes::Input.new(device: :null)
29
+ assert_equal [ '--null' ], input.commands
30
+ end
31
+
32
+ def test_init_with_stdin_device_should_use_dash_input
33
+ input = Chaussettes::Input.new(device: :stdin)
34
+ assert_equal [ '-' ], input.commands
35
+ end
36
+
37
+ def test_init_with_default_device_should_use_default_device
38
+ input = Chaussettes::Input.new(device: :default)
39
+ assert_equal [ '--default-device' ], input.commands
40
+ end
41
+
42
+ def test_arguments_should_precede_input_source
43
+ input = Chaussettes::Input.new('hello.mp3').ignore_length.volume(0.5)
44
+ expect = [ '--ignore-length', '--volume', 0.5, 'hello.mp3' ]
45
+ assert_equal expect, input.commands
46
+ end
47
+
48
+ def test_common_options_should_be_included
49
+ input = Chaussettes::Input.new('hello.mp3').
50
+ bits(8).channels(2).encoding('encoding').rate(44_100).type('mp3').
51
+ endian('little').reverse_nibbles.reverse_bits
52
+
53
+ expected = [
54
+ '--bits', 8, '--channels', 2, '--encoding', 'encoding', '--rate', 44_100,
55
+ '--type', 'mp3', '--endian', 'little', '--reverse-nibbles',
56
+ '--reverse-bits', 'hello.mp3'
57
+ ]
58
+
59
+ assert_equal expected, input.commands
60
+ end
61
+ end
@@ -0,0 +1,61 @@
1
+ require 'minitest/autorun'
2
+ require 'chaussettes/output'
3
+
4
+ class OutputTest < Minitest::Test
5
+ def test_init_with_string_should_use_string_as_dest
6
+ output = Chaussettes::Output.new('output.wav')
7
+ assert_equal [ 'output.wav' ], output.commands
8
+ end
9
+
10
+ def test_init_with_no_arguments_should_use_null_output
11
+ output = Chaussettes::Output.new
12
+ assert_equal [ '--null' ], output.commands
13
+ end
14
+
15
+ def test_init_with_nil_device_should_use_null_output
16
+ output = Chaussettes::Output.new(device: nil)
17
+ assert_equal [ '--null' ], output.commands
18
+ end
19
+
20
+ def test_init_with_null_device_should_use_null_output
21
+ output = Chaussettes::Output.new(device: :null)
22
+ assert_equal [ '--null' ], output.commands
23
+ end
24
+
25
+ def test_init_with_stdout_device_should_use_dash_output
26
+ output = Chaussettes::Output.new(device: :stdout)
27
+ assert_equal [ '-' ], output.commands
28
+ end
29
+
30
+ def test_init_with_pipe_device_should_use_sox_pipe_output
31
+ output = Chaussettes::Output.new(device: :pipe)
32
+ assert_equal [ '--sox-pipe' ], output.commands
33
+ end
34
+
35
+ def test_init_with_default_device_should_use_default_device
36
+ output = Chaussettes::Output.new(device: :default)
37
+ assert_equal [ '--default-device' ], output.commands
38
+ end
39
+
40
+ def test_arguments_should_precede_output_dest
41
+ output = Chaussettes::Output.new('hello.mp3').
42
+ comment('cmt').add_comment('cmt2').compression(2)
43
+ expect = [ '--comment', 'cmt', '--add-comment', 'cmt2', '--compression', 2,
44
+ 'hello.mp3' ]
45
+ assert_equal expect, output.commands
46
+ end
47
+
48
+ def test_common_options_should_be_included
49
+ output = Chaussettes::Output.new('hello.mp3').
50
+ bits(8).channels(2).encoding('encoding').rate(44_100).type('mp3').
51
+ endian('little').reverse_nibbles.reverse_bits
52
+
53
+ expected = [
54
+ '--bits', 8, '--channels', 2, '--encoding', 'encoding', '--rate', 44_100,
55
+ '--type', 'mp3', '--endian', 'little', '--reverse-nibbles',
56
+ '--reverse-bits', 'hello.mp3'
57
+ ]
58
+
59
+ assert_equal expected, output.commands
60
+ end
61
+ end
@@ -0,0 +1,27 @@
1
+ require 'minitest/autorun'
2
+ require 'chaussettes/tool'
3
+
4
+ class ToolTest < Minitest::Test
5
+ def setup
6
+ @tool = Chaussettes::Tool.new('command')
7
+ end
8
+
9
+ def test_empty_arguments_should_render_only_the_command
10
+ assert_equal 'command', @tool.to_s
11
+ end
12
+
13
+ def test_appended_arguments_should_be_rendered
14
+ @tool << '-flag' << 'value'
15
+ assert_equal 'command -flag value', @tool.to_s
16
+ end
17
+
18
+ def test_concatted_arguments_should_be_rendered
19
+ @tool.concat [ '-flag', 'value' ]
20
+ assert_equal 'command -flag value', @tool.to_s
21
+ end
22
+
23
+ def test_special_chars_should_be_escaped
24
+ @tool << '-flag' << 'two words' << 'angry!'
25
+ assert_equal 'command -flag two\ words angry\!', @tool.to_s
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chaussettes
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jamis Buck
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-01-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: A wrapper around the sox audio manipulation utility
42
+ email:
43
+ - jamis@jamisbuck.org
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - MIT-LICENSE
49
+ - README.md
50
+ - Rakefile
51
+ - examples/drum.rb
52
+ - examples/ducking.rb
53
+ - examples/synth.rb
54
+ - lib/chaussettes.rb
55
+ - lib/chaussettes/clip.rb
56
+ - lib/chaussettes/common_options.rb
57
+ - lib/chaussettes/effect/fade.rb
58
+ - lib/chaussettes/effect/gain.rb
59
+ - lib/chaussettes/effect/synth.rb
60
+ - lib/chaussettes/effect/vol.rb
61
+ - lib/chaussettes/effect_chain.rb
62
+ - lib/chaussettes/info.rb
63
+ - lib/chaussettes/input.rb
64
+ - lib/chaussettes/output.rb
65
+ - lib/chaussettes/tool.rb
66
+ - lib/chaussettes/version.rb
67
+ - test/chaussettes/clip_test.rb
68
+ - test/chaussettes/input_test.rb
69
+ - test/chaussettes/output_test.rb
70
+ - test/chaussettes/tool_test.rb
71
+ homepage: http://github.com/jamis/chaussettes
72
+ licenses:
73
+ - MIT
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 2.5.1
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: A wrapper around the sox audio manipulation utility
95
+ test_files:
96
+ - test/chaussettes/clip_test.rb
97
+ - test/chaussettes/input_test.rb
98
+ - test/chaussettes/output_test.rb
99
+ - test/chaussettes/tool_test.rb