chaussettes 1.0.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 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