albanpeignier-alsa-backup 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.autotest ADDED
@@ -0,0 +1,3 @@
1
+ Autotest.add_hook :initialize do |autotest|
2
+ autotest.add_exception(/tmp/)
3
+ end
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.0.1 2009-05-14
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/Manifest.txt ADDED
@@ -0,0 +1,26 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ PostInstall.txt
5
+ README.rdoc
6
+ Rakefile
7
+ alsa-backup.gemspec
8
+ bin/alsa.backup
9
+ config.sample
10
+ lib/alsa.rb
11
+ lib/alsa_backup.rb
12
+ lib/alsa_backup/cli.rb
13
+ lib/alsa_backup/recorder.rb
14
+ lib/sndfile.rb
15
+ script/console
16
+ script/destroy
17
+ script/generate
18
+ spec/alsa/pcm_spec.rb
19
+ spec/alsa_backup/cli_spec.rb
20
+ spec/alsa_backup/recorder_spec.rb
21
+ spec/alsa_backup_spec.rb
22
+ spec/fixtures/config_test.rb
23
+ spec/sndfile/info_spec.rb
24
+ spec/spec.opts
25
+ spec/spec_helper.rb
26
+ tasks/rspec.rake
data/PostInstall.txt ADDED
@@ -0,0 +1,7 @@
1
+
2
+ For more information on alsa_backup, see http://alsa_backup.rubyforge.org
3
+
4
+ NOTE: Change this information in PostInstall.txt
5
+ You can also delete it if you don't want it.
6
+
7
+
data/README.rdoc ADDED
@@ -0,0 +1,72 @@
1
+ = AlsaBackup
2
+
3
+ * http://github.com/alban.peignier/alsa-backup
4
+
5
+ == DESCRIPTION:
6
+
7
+ ALSA client to perform continuous recording
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * configurable file name strategy
12
+
13
+ == SYNOPSIS:
14
+
15
+ By using the default settings :
16
+
17
+ alsa.backup
18
+
19
+ will record in record.wav the default alsa device
20
+
21
+ By using command line arguments :
22
+
23
+ alsa.backup --directoy=/tmp --file=test.wav --length=2
24
+
25
+ will record 2 seconds in /tmp/test.wav
26
+
27
+ By loading a configuration :
28
+
29
+ alsa.backup --config=/path/to/config
30
+
31
+ will load the specified configuration
32
+
33
+ == CONFIGURATION:
34
+
35
+ See config.sample for the moment.
36
+
37
+ == REQUIREMENTS:
38
+
39
+ * alsa library (http://www.alsa-project.org/)
40
+ * libsndfile library (http://www.mega-nerd.com/libsndfile/)
41
+
42
+ == INSTALL:
43
+
44
+ * sudo apt-get install libasound2 libsndfile1
45
+ * sudo gem install ruby-ffi
46
+
47
+ * sudo gem install --source http://gems.github.com albanpeignier-alsa-backup
48
+
49
+ == LICENSE:
50
+
51
+ (The MIT License)
52
+
53
+ Copyright (c) 2009 FIXME Alban Peignier
54
+
55
+ Permission is hereby granted, free of charge, to any person obtaining
56
+ a copy of this software and associated documentation files (the
57
+ 'Software'), to deal in the Software without restriction, including
58
+ without limitation the rights to use, copy, modify, merge, publish,
59
+ distribute, sublicense, and/or sell copies of the Software, and to
60
+ permit persons to whom the Software is furnished to do so, subject to
61
+ the following conditions:
62
+
63
+ The above copyright notice and this permission notice shall be
64
+ included in all copies or substantial portions of the Software.
65
+
66
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
67
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
68
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
69
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
70
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
71
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
72
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
2
+ %w[rake rake/clean fileutils newgem rubigen].each { |f| require f }
3
+ require File.dirname(__FILE__) + '/lib/alsa_backup'
4
+
5
+ # Generate all the Rake tasks
6
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
7
+ $hoe = Hoe.new('alsa-backup', AlsaBackup::VERSION) do |p|
8
+ p.developer('Alban Peignier', 'alban.peignier@free.fr')
9
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
10
+ p.rubyforge_name = p.name # TODO this is default value
11
+ p.extra_deps = [
12
+ ['ffi','>= 0.3.5'],
13
+ ]
14
+ p.extra_dev_deps = [
15
+ ['newgem', ">= #{::Newgem::VERSION}"]
16
+ ]
17
+
18
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
19
+ path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
20
+ p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
21
+ p.rsync_args = '-av --delete --ignore-errors'
22
+ end
23
+
24
+ require 'newgem/tasks' # load /tasks/*.rake
25
+ Dir['tasks/**/*.rake'].each { |t| load t }
26
+
27
+ task :default => :spec
@@ -0,0 +1,42 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{alsa-backup}
5
+ s.version = "0.0.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Alban Peignier"]
9
+ s.date = %q{2009-05-18}
10
+ s.default_executable = %q{alsa.backup}
11
+ s.description = %q{ALSA client to perform continuous recording}
12
+ s.email = ["alban.peignier@free.fr"]
13
+ s.executables = ["alsa.backup"]
14
+ s.extra_rdoc_files = ["History.txt", "Manifest.txt", "PostInstall.txt", "README.rdoc"]
15
+ s.files = [".autotest", "History.txt", "Manifest.txt", "PostInstall.txt", "README.rdoc", "Rakefile", "alsa-backup.gemspec", "bin/alsa.backup", "config.sample", "lib/alsa.rb", "lib/alsa_backup.rb", "lib/alsa_backup/cli.rb", "lib/alsa_backup/recorder.rb", "lib/sndfile.rb", "script/console", "script/destroy", "script/generate", "spec/alsa/pcm_spec.rb", "spec/alsa_backup/cli_spec.rb", "spec/alsa_backup/recorder_spec.rb", "spec/alsa_backup_spec.rb", "spec/fixtures/config_test.rb", "spec/sndfile/info_spec.rb", "spec/spec.opts", "spec/spec_helper.rb", "tasks/rspec.rake"]
16
+ s.has_rdoc = true
17
+ s.homepage = %q{http://github.com/alban.peignier/alsa-backup}
18
+ s.rdoc_options = ["--main", "README.rdoc"]
19
+ s.require_paths = ["lib"]
20
+ s.rubyforge_project = %q{alsa-backup}
21
+ s.rubygems_version = %q{1.3.2}
22
+ s.summary = %q{ALSA client to perform continuous recording}
23
+
24
+ if s.respond_to? :specification_version then
25
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
26
+ s.specification_version = 3
27
+
28
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
29
+ s.add_runtime_dependency(%q<ffi>, [">= 0.3.5"])
30
+ s.add_development_dependency(%q<newgem>, [">= 1.4.1"])
31
+ s.add_development_dependency(%q<hoe>, [">= 1.8.0"])
32
+ else
33
+ s.add_dependency(%q<ffi>, [">= 0.3.5"])
34
+ s.add_dependency(%q<newgem>, [">= 1.4.1"])
35
+ s.add_dependency(%q<hoe>, [">= 1.8.0"])
36
+ end
37
+ else
38
+ s.add_dependency(%q<ffi>, [">= 0.3.5"])
39
+ s.add_dependency(%q<newgem>, [">= 1.4.1"])
40
+ s.add_dependency(%q<hoe>, [">= 1.8.0"])
41
+ end
42
+ end
data/bin/alsa.backup ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2009-5-14.
4
+ # Copyright (c) 2009. All rights reserved.
5
+
6
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/alsa_backup")
7
+
8
+ require 'alsa_backup'
9
+ require 'alsa_backup/cli'
10
+
11
+ AlsaBackup::CLI.execute(STDOUT, *ARGV)
data/config.sample ADDED
@@ -0,0 +1,43 @@
1
+ #
2
+ # Config sample for Alsa.Backup
3
+ #
4
+ # Usage:
5
+ # alsa-backup --config /path/to/this/file
6
+ #
7
+
8
+ #
9
+ # To use syslog
10
+ #
11
+ #require 'syslog_logger'
12
+ #AlsaBackup.logger = SyslogLogger.new('alsa.backup').tap do |logger|
13
+ # logger.level = Logger::INFO
14
+ #end
15
+
16
+ AlsaBackup.logger.info("load config #{__FILE__}")
17
+
18
+ AlsaBackup.config do |recorder|
19
+ #
20
+ # To use files named like this :
21
+ # - 2009/05-May/17-Sun/19h.wav
22
+ # - 2009/05-May/17-Sun/20h.wav
23
+ #
24
+ #recorder.file = Proc.new {
25
+ # Time.now.strftime("%Y/%m-%b/%d-%a/%Hh.wav")
26
+ #}
27
+
28
+ #
29
+ # To use files named like this :
30
+ # - 2009/05-May/17-Sun/19h00.wav
31
+ # - 2009/05-May/17-Sun/19h15.wav
32
+ #
33
+ #recorder.file = Proc.new {
34
+ # Time.now.floor(:min, 15).strftime("%Y/%m-%b/%d-%a/%Hh%M.wav")
35
+ #}
36
+
37
+ #
38
+ # Define where the files are created
39
+ #
40
+ #recorder.directory = "/var/lib/alsa.backup"
41
+ end
42
+
43
+
data/lib/alsa.rb ADDED
@@ -0,0 +1,278 @@
1
+ require 'rubygems'
2
+ require 'ffi'
3
+
4
+ require 'logger'
5
+
6
+ module ALSA
7
+
8
+ def self.logger
9
+ unless @logger
10
+ @logger = Logger.new(STDERR)
11
+ @logger.level = Logger::WARN
12
+ end
13
+
14
+ @logger
15
+ end
16
+
17
+ def self.logger=(logger); @logger = logger; end
18
+
19
+ def self.try_to(message, &block)
20
+ logger.debug('alsa') { message }
21
+ if (response = yield) < 0
22
+ raise "cannot #{message} (#{ALSA::Native::strerror(response)})"
23
+ else
24
+ response
25
+ end
26
+ end
27
+
28
+ module Native
29
+ extend FFI::Library
30
+ ffi_lib "libasound.so"
31
+
32
+ attach_function :strerror, :snd_strerror, [:int], :string
33
+ end
34
+
35
+ module PCM
36
+
37
+ class Capture
38
+
39
+ attr_accessor :handle
40
+
41
+ def self.open(device, hardware_attributes = {}, &block)
42
+ Capture.new.open(device, hardware_attributes, &block)
43
+ end
44
+
45
+ def open(device, hardware_attributes = {}, &block)
46
+ capture_handle = FFI::MemoryPointer.new :pointer
47
+ ALSA::try_to "open audio device #{device}" do
48
+ ALSA::PCM::Native::open capture_handle, device, ALSA::PCM::Native::STREAM_CAPTURE, ALSA::PCM::Native::BLOCK
49
+ end
50
+ self.handle = capture_handle.read_pointer
51
+
52
+ self.hardware_parameters=hardware_attributes
53
+
54
+ if block_given?
55
+ begin
56
+ yield self
57
+ ensure
58
+ self.close
59
+ end
60
+ end
61
+ end
62
+
63
+ def change_hardware_parameters
64
+ hw_params = HwParameters.new(self).default_for_device
65
+
66
+ begin
67
+ yield hw_params
68
+
69
+ ALSA::try_to "set hw parameters" do
70
+ ALSA::PCM::Native::hw_params self.handle, hw_params.handle
71
+ end
72
+ ensure
73
+ hw_params.free
74
+ end
75
+ end
76
+
77
+ def hardware_parameters
78
+ HwParameters.new(self).current_for_device
79
+ end
80
+
81
+ def hardware_parameters=(attributes= {})
82
+ attributes = {:access => :rw_interleaved}.update(attributes)
83
+ change_hardware_parameters do |hw_params|
84
+ hw_params.update_attributes(attributes)
85
+ end
86
+ end
87
+
88
+ def read
89
+ frame_count = hardware_parameters.sample_rate
90
+ buffer = FFI::MemoryPointer.new(hardware_parameters.buffer_size_for(frame_count))
91
+
92
+ continue = true
93
+ while continue
94
+ read_count = ALSA::try_to "read from audio interface" do
95
+ ALSA::PCM::Native::readi(self.handle, buffer, frame_count)
96
+ end
97
+
98
+ raise "can't read expected frame count (#{read_count}/#{frame_count})" unless read_count == frame_count
99
+
100
+ continue = yield buffer, read_count
101
+ end
102
+
103
+ buffer.free
104
+ end
105
+
106
+ def close
107
+ ALSA::try_to "close audio device" do
108
+ ALSA::PCM::Native::close self.handle
109
+ end
110
+ end
111
+
112
+ class HwParameters
113
+
114
+ attr_accessor :handle, :device
115
+
116
+ def initialize(device = nil)
117
+ hw_params_pointer = FFI::MemoryPointer.new :pointer
118
+
119
+ ALSA::PCM::Native::hw_params_malloc hw_params_pointer
120
+ self.handle = hw_params_pointer.read_pointer
121
+
122
+ self.device = device if device
123
+ end
124
+
125
+ def update_attributes(attributes)
126
+ attributes.each_pair { |name, value| send("#{name}=", value) }
127
+ end
128
+
129
+ def default_for_device
130
+ ALSA::try_to "initialize hardware parameter structure" do
131
+ ALSA::PCM::Native::hw_params_any device.handle, self.handle
132
+ end
133
+ self
134
+ end
135
+
136
+ def current_for_device
137
+ ALSA::try_to "retrieve current hardware parameters" do
138
+ ALSA::PCM::Native::hw_params_current device.handle, self.handle
139
+ end
140
+ self
141
+ end
142
+
143
+ def access=(access)
144
+ ALSA::try_to "set access type" do
145
+ ALSA::PCM::Native::hw_params_set_access self.device.handle, self.handle, ALSA::PCM::Native::Access.const_get(access.to_s.upcase)
146
+ end
147
+ end
148
+
149
+ def channels=(channels)
150
+ ALSA::try_to "set channel count : #{channels}" do
151
+ ALSA::PCM::Native::hw_params_set_channels self.device.handle, self.handle, channels
152
+ end
153
+ end
154
+
155
+ def sample_rate=(sample_rate)
156
+ ALSA::try_to "set sample rate" do
157
+ rate = FFI::MemoryPointer.new(:int)
158
+ rate.write_int(sample_rate)
159
+
160
+ dir = FFI::MemoryPointer.new(:int)
161
+ dir.write_int(0)
162
+
163
+ error_code = ALSA::PCM::Native::hw_params_set_rate_near self.device.handle, self.handle, rate, dir
164
+
165
+ rate.free
166
+ dir.free
167
+
168
+ error_code
169
+ end
170
+ end
171
+
172
+ def sample_rate
173
+ rate = nil
174
+ ALSA::try_to "get sample rate" do
175
+ rate_pointer = FFI::MemoryPointer.new(:int)
176
+ dir_pointer = FFI::MemoryPointer.new(:int)
177
+ dir_pointer.write_int(0)
178
+
179
+ error_code = ALSA::PCM::Native::hw_params_get_rate self.handle, rate_pointer, dir_pointer
180
+
181
+ rate = rate_pointer.read_int
182
+
183
+ rate_pointer.free
184
+ dir_pointer.free
185
+
186
+ error_code
187
+ end
188
+ rate
189
+ end
190
+
191
+ def sample_format=(sample_format)
192
+ ALSA::try_to "set sample format" do
193
+ ALSA::PCM::Native::hw_params_set_format self.device.handle, self.handle, ALSA::PCM::Native::Format.const_get(sample_format.to_s.upcase)
194
+ end
195
+ end
196
+
197
+ def sample_format
198
+ format = nil
199
+ FFI::MemoryPointer.new(:int) do |format_pointer|
200
+ ALSA::try_to "get sample format" do
201
+ ALSA::PCM::Native::hw_params_get_format self.handle, format_pointer
202
+ end
203
+ format = format_pointer.read_int
204
+ end
205
+ format
206
+ end
207
+
208
+ def channels
209
+ channels = nil
210
+ FFI::MemoryPointer.new(:int) do |channels_pointer|
211
+ ALSA::try_to "get channels" do
212
+ ALSA::PCM::Native::hw_params_get_channels self.handle, channels_pointer
213
+ end
214
+ channels = channels_pointer.read_int
215
+ end
216
+ channels
217
+ end
218
+
219
+ def buffer_size_for(frame_count)
220
+ ALSA::PCM::Native::format_size(self.sample_format, frame_count) * self.channels
221
+ end
222
+
223
+ def free
224
+ ALSA::try_to "unallocate hw_params" do
225
+ ALSA::PCM::Native::hw_params_free self.handle
226
+ end
227
+ end
228
+
229
+ end
230
+
231
+ end
232
+
233
+ module Native
234
+ extend FFI::Library
235
+ ffi_lib "libasound.so"
236
+
237
+ STREAM_CAPTURE = 1
238
+ BLOCK = 0
239
+ attach_function :open, :snd_pcm_open, [:pointer, :string, :int, :int], :int
240
+ attach_function :prepare, :snd_pcm_prepare, [ :pointer ], :int
241
+ attach_function :close, :snd_pcm_close, [:pointer], :int
242
+
243
+ attach_function :readi, :snd_pcm_readi, [ :pointer, :pointer, :ulong ], :long
244
+
245
+ attach_function :hw_params_malloc, :snd_pcm_hw_params_malloc, [:pointer], :int
246
+ attach_function :hw_params_free, :snd_pcm_hw_params_free, [:pointer], :int
247
+
248
+ attach_function :hw_params, :snd_pcm_hw_params, [ :pointer, :pointer ], :int
249
+ attach_function :hw_params_any, :snd_pcm_hw_params_any, [:pointer, :pointer], :int
250
+ attach_function :hw_params_current, :snd_pcm_hw_params_current, [ :pointer, :pointer ], :int
251
+
252
+
253
+ module Access
254
+ MMAP_INTERLEAVED = 0
255
+ MMAP_NONINTERLEAVED = 1
256
+ MMAP_COMPLEX = 2
257
+ RW_INTERLEAVED = 3
258
+ RW_NONINTERLEAVED = 4
259
+ end
260
+
261
+ attach_function :hw_params_set_access, :snd_pcm_hw_params_set_access, [ :pointer, :pointer, :int ], :int
262
+
263
+ module Format
264
+ S16_LE = 2
265
+ end
266
+
267
+ attach_function :hw_params_set_format, :snd_pcm_hw_params_set_format, [ :pointer, :pointer, :int ], :int
268
+ attach_function :hw_params_get_format, :snd_pcm_hw_params_get_format, [ :pointer, :pointer ], :int
269
+ attach_function :hw_params_get_rate, :snd_pcm_hw_params_get_rate, [ :pointer, :pointer, :pointer ], :int
270
+ attach_function :hw_params_set_rate_near, :snd_pcm_hw_params_set_rate_near, [ :pointer, :pointer, :pointer, :pointer ], :int
271
+ attach_function :hw_params_set_channels, :snd_pcm_hw_params_set_channels, [ :pointer, :pointer, :uint ], :int
272
+ attach_function :hw_params_get_channels, :snd_pcm_hw_params_get_format, [ :pointer, :pointer ], :int
273
+ attach_function :hw_params_set_periods, :snd_pcm_hw_params_set_periods, [ :pointer, :pointer, :uint, :int ], :int
274
+
275
+ attach_function :format_size, :snd_pcm_format_size, [ :int, :uint ], :int
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,48 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ begin
5
+ require 'rubygems'
6
+ rescue LoadError
7
+ end
8
+
9
+ require 'activesupport'
10
+ require 'logger'
11
+
12
+ module AlsaBackup
13
+ VERSION = '0.0.1'
14
+
15
+ def self.recorder
16
+ @recorder ||= AlsaBackup::Recorder.new
17
+ end
18
+
19
+ def self.config
20
+ yield self.recorder
21
+ end
22
+
23
+ def self.logger
24
+ unless @logger
25
+ @logger = Logger.new(STDOUT)
26
+ @logger.level = Logger::INFO
27
+ end
28
+
29
+ @logger
30
+ end
31
+
32
+ def self.logger=(logger); @logger = logger; end
33
+
34
+ end
35
+
36
+ class Time
37
+
38
+ def floor(attribute, modulo)
39
+ actual = self.send(attribute)
40
+ self.change(attribute => actual - actual%modulo)
41
+ end
42
+
43
+ end
44
+
45
+ require 'alsa_backup/recorder'
46
+
47
+
48
+
@@ -0,0 +1,44 @@
1
+ require 'optparse'
2
+
3
+ module AlsaBackup
4
+ class CLI
5
+ def self.execute(stdout, *arguments)
6
+ options = {}
7
+ mandatory_options = %w()
8
+
9
+ OptionParser.new do |opts|
10
+ opts.banner = <<-BANNER.gsub(/^ /,'')
11
+ Alsa.Backup : continuous recording with alsa
12
+
13
+ Usage: #{File.basename($0)} [options]
14
+
15
+ Options are:
16
+ BANNER
17
+ opts.separator ""
18
+ opts.on("-f", "--file=FILE", String,
19
+ "Recording file") { |arg| options[:file] = arg }
20
+ opts.on("-l", "--length=LENGTH", String,
21
+ "Length in seconds") { |arg| options[:length] = arg }
22
+ opts.on("-d", "--directory=DIRECTORY", String,
23
+ "Base directory") { |arg| options[:directory] = arg }
24
+ opts.on("-c", "--config=CONFIG", String,
25
+ "Configuration file") { |arg| options[:config] = arg }
26
+ opts.on("-h", "--help",
27
+ "Show this help message.") { stdout.puts opts; exit }
28
+ opts.parse!(arguments)
29
+
30
+ if mandatory_options && mandatory_options.find { |option| options[option.to_sym].nil? }
31
+ stdout.puts opts; exit
32
+ end
33
+ end
34
+
35
+ load File.expand_path(options[:config]) if options[:config]
36
+
37
+ AlsaBackup.recorder.file = options[:file] if options[:file]
38
+ AlsaBackup.recorder.directory = options[:directory] if options[:directory]
39
+
40
+ length = options[:length].to_i if options[:length]
41
+ AlsaBackup.recorder.start(length)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,71 @@
1
+ require 'alsa'
2
+ require 'sndfile'
3
+
4
+ require 'fileutils'
5
+
6
+ module AlsaBackup
7
+ class Recorder
8
+
9
+ def initialize(file = "record.wav")
10
+ @file = file
11
+ @directory = "."
12
+ end
13
+
14
+ attr_accessor :file, :directory
15
+
16
+ def start(seconds_to_record = nil)
17
+ frames_to_record = format[:sample_rate] * seconds_to_record if seconds_to_record
18
+
19
+ # prepare sndfile
20
+ self.sndfile
21
+
22
+ ALSA::PCM::Capture.open("hw:0", self.format(:sample_format => :s16_le)) do |capture|
23
+ capture.read do |buffer, frame_count|
24
+ self.sndfile.write buffer, frame_count
25
+ if frames_to_record
26
+ (frames_to_record -= frame_count) > 0
27
+ else
28
+ true
29
+ end
30
+ end
31
+ end
32
+ rescue Exception => e
33
+ AlsaBackup.logger.error(e)
34
+ raise e
35
+ ensure
36
+ @sndfile.close if @sndfile
37
+ end
38
+
39
+ def file
40
+ case @file
41
+ when Proc
42
+ @file.call
43
+ else
44
+ @file
45
+ end
46
+ end
47
+
48
+ def target_file
49
+ File.join self.directory, self.file
50
+ end
51
+
52
+ def format(additional_parameters = {})
53
+ {:sample_rate => 44100, :channels => 2}.merge(additional_parameters)
54
+ end
55
+
56
+ def sndfile
57
+ target_file = self.target_file
58
+ raise "no recording file" unless target_file
59
+
60
+ unless @sndfile and @sndfile.path == target_file
61
+ @sndfile.close if @sndfile
62
+ AlsaBackup.logger.info "new file #{target_file}"
63
+
64
+ FileUtils.mkdir_p File.dirname(target_file)
65
+ @sndfile = Sndfile::File.new(target_file, "w", self.format(:format => "wav pcm_16"))
66
+ end
67
+ @sndfile
68
+ end
69
+
70
+ end
71
+ end
data/lib/sndfile.rb ADDED
@@ -0,0 +1,121 @@
1
+ require 'rubygems'
2
+ require 'ffi'
3
+
4
+ module Sndfile
5
+
6
+ class File
7
+
8
+ attr_reader :path
9
+
10
+ def self.open(path, mode, info)
11
+ file = self.new(path, mode, info)
12
+
13
+ begin
14
+ yield file
15
+ ensure
16
+ file.close
17
+ end
18
+ end
19
+
20
+ def initialize(path, mode, info)
21
+ info = (Hash === info ? Info.new(info) : info)
22
+ @handle = Sndfile::Native::open path, File.native_mode(mode), info.to_native
23
+ if @handle.is_a?(FFI::NullPointer)
24
+ raise "Not able to open output file " + self.error
25
+ end
26
+ @path = path
27
+ end
28
+
29
+ def write(buffer, frame_count)
30
+ unless Sndfile::Native::write_int(@handle, buffer, frame_count) == frame_count
31
+ raise self.error
32
+ end
33
+ end
34
+
35
+ def close
36
+ Sndfile::Native::close @handle
37
+ end
38
+
39
+ def error
40
+ Sndfile::Native::strerror @handle
41
+ end
42
+
43
+ def self.native_mode(mode)
44
+ case mode
45
+ when "w": Sndfile::Native::MODE_WRITE
46
+ else
47
+ raise "Unknown mode: #{mode}"
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ class Info
54
+
55
+ attr_accessor :sample_rate, :channels, :format
56
+
57
+ def initialize(attributes = {})
58
+ update_attributes(attributes)
59
+ end
60
+
61
+ def update_attributes(attributes)
62
+ attributes.each_pair { |name, value| send("#{name}=", value) }
63
+ end
64
+
65
+ def to_native
66
+ Sndfile::Native::Info.new.tap do |native|
67
+ native[:samplerate] = self.sample_rate
68
+ native[:channels] = self.channels
69
+ native[:format] = self.native_format
70
+ end
71
+ end
72
+
73
+ def format=(format)
74
+ @format = Info.normalized_format(format)
75
+ end
76
+
77
+ def native_format
78
+ self.format.inject(0) do |native_format, format_part|
79
+ native_format | Sndfile::Native::Format.const_get(format_part.upcase)
80
+ end
81
+ end
82
+
83
+ def self.normalized_format(format)
84
+ Array(format).join(' ').downcase.scan(/[a-z0-9_]+/)
85
+ end
86
+
87
+ end
88
+
89
+ module Native
90
+ extend FFI::Library
91
+ ffi_lib "libsndfile.so"
92
+
93
+ module Format
94
+ WAV = 0x010000
95
+ PCM_16 = 0x0002
96
+ PCM_24 = 0x0003
97
+ end
98
+
99
+ MODE_READ = 0x10
100
+ MODE_WRITE = 0x20
101
+ MODE_RDWR = 0x30
102
+
103
+ class Info < FFI::Struct
104
+ layout(
105
+ :frames, :int64,
106
+ :samplerate, :int,
107
+ :channels, :int,
108
+ :format, :int,
109
+ :sections, :int,
110
+ :seekable, :int
111
+ )
112
+ end
113
+
114
+ attach_function :open, :sf_open, [ :string, :int, :pointer ], :pointer
115
+ attach_function :close, :sf_close, [ :pointer ], :int
116
+
117
+ attach_function :write_int, :sf_write_int, [ :pointer, :pointer, :int ], :int
118
+
119
+ attach_function :strerror, :sf_strerror, [ :pointer ], :string
120
+ end
121
+ end
data/script/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/alsa_backup.rb'}"
9
+ puts "Loading alsa_backup gem"
10
+ exec "#{irb} #{libs} --simple-prompt"
data/script/destroy ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
data/script/generate ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -0,0 +1,7 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper.rb'
2
+
3
+ require 'alsa'
4
+
5
+ describe ALSA::PCM do
6
+
7
+ end
@@ -0,0 +1,62 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper.rb'
2
+
3
+ require 'alsa_backup/cli'
4
+
5
+ describe AlsaBackup::CLI, "execute" do
6
+ before(:each) do
7
+ @stdout_io = StringIO.new
8
+ @file = test_file
9
+
10
+ @recorder = AlsaBackup::Recorder.new(@file)
11
+ @recorder.stub!(:start)
12
+ AlsaBackup.stub!(:recorder).and_return(@recorder)
13
+ end
14
+
15
+ def execute_cli(options = {})
16
+ options = { :file => @file, :length => 2 }.update(options)
17
+ arguments = options.collect do |key,value|
18
+ "--#{key}=#{value}" if value
19
+ end.compact
20
+
21
+ AlsaBackup::CLI.execute(@stdout_io, *arguments)
22
+ @stdout_io.rewind
23
+ @stdout = @stdout_io.read
24
+ end
25
+
26
+ it "should use AlsaBackup.recorder" do
27
+ AlsaBackup.should_receive(:recorder).and_return(@recorder)
28
+ execute_cli
29
+ end
30
+
31
+ it "should set the record file with specified one" do
32
+ @recorder.should_receive(:file=).with(file = "dummy")
33
+ execute_cli :file => file
34
+ end
35
+
36
+ it "should start the AlsaBackup.recorder" do
37
+ @recorder.should_receive(:start)
38
+ execute_cli
39
+ end
40
+
41
+ it "should start the recorder with specified length" do
42
+ @recorder.should_receive(:start).with(length = 60)
43
+ execute_cli :length => length
44
+ end
45
+
46
+ it "should start the record without length if not specified" do
47
+ @recorder.should_receive(:start).with(nil)
48
+ execute_cli :length => nil
49
+ end
50
+
51
+ it "should execute specified config file" do
52
+ execute_cli :config => fixture_file('config_test.rb'), :file => nil
53
+ AlsaBackup.recorder.file.should == "config_test_ok"
54
+ end
55
+
56
+ it "should override config file values with command line arguments" do
57
+ argument_file = "dummy"
58
+ execute_cli :config => fixture_file('config_test.rb'), :file => argument_file
59
+ AlsaBackup.recorder.file.should == argument_file
60
+ end
61
+
62
+ end
@@ -0,0 +1,31 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper.rb'
2
+
3
+ require 'alsa_backup/recorder'
4
+
5
+ describe AlsaBackup::Recorder do
6
+
7
+ before(:each) do
8
+ @file = test_file
9
+ @recorder = AlsaBackup::Recorder.new(@file)
10
+ end
11
+
12
+ it "should not raise an error on start" do
13
+ lambda { @recorder.start(2) }.should_not raise_error
14
+ end
15
+
16
+ describe "file" do
17
+
18
+ it "should accept file as string" do
19
+ @recorder.file = file_name = "dummy"
20
+ @recorder.file.should == file_name
21
+ end
22
+
23
+ it "should accept file as Proc" do
24
+ file_name = "dummy"
25
+ @recorder.file = Proc.new { file_name }
26
+ @recorder.file.should == file_name
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,11 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ # Time to add your specs!
4
+ # http://rspec.info/
5
+ describe "Place your specs here" do
6
+
7
+ it "find this spec in spec directory" do
8
+ # violated "Be sure to write your specs"
9
+ end
10
+
11
+ end
@@ -0,0 +1,3 @@
1
+ AlsaBackup.config do |recorder|
2
+ recorder.file = "config_test_ok"
3
+ end
@@ -0,0 +1,38 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper.rb'
2
+
3
+ require 'sndfile'
4
+
5
+ describe Sndfile::Info do
6
+
7
+ before(:each) do
8
+ @info = Sndfile::Info.new
9
+ end
10
+
11
+ describe "normalized_format" do
12
+
13
+ it "should transform 'wav pcm_16' into [wav, pcm_16]" do
14
+ Sndfile::Info.normalized_format('wav pcm_16').should == %w{wav pcm_16}
15
+ end
16
+
17
+ end
18
+
19
+ describe "format" do
20
+
21
+ it "should be normalized when set" do
22
+ @info.format = 'wav pcm_16'
23
+ @info.format.should == %w{wav pcm_16}
24
+ end
25
+
26
+ end
27
+
28
+ describe "native_format" do
29
+
30
+ it "should return (Format::WAV|Format::PCM_16) for 'wav pcm_16'" do
31
+ @info.format = 'wav pcm_16'
32
+ @info.native_format.should == Sndfile::Native::Format::WAV | Sndfile::Native::Format::PCM_16
33
+ end
34
+
35
+ end
36
+
37
+
38
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --colour
@@ -0,0 +1,21 @@
1
+ begin
2
+ require 'spec'
3
+ rescue LoadError
4
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
5
+ gem 'rspec'
6
+ require 'spec'
7
+ end
8
+
9
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
10
+ require 'alsa_backup'
11
+
12
+ def test_file(name = 'test.wav')
13
+ File.dirname(__FILE__) + '/../tmp/' + name
14
+ end
15
+
16
+ def fixture_file(name)
17
+ File.dirname(__FILE__) + '/fixtures/' + name
18
+ end
19
+
20
+ ALSA::logger = AlsaBackup.logger =
21
+ Logger.new(File.dirname(__FILE__) + '/../log/test.log')
data/tasks/rspec.rake ADDED
@@ -0,0 +1,21 @@
1
+ begin
2
+ require 'spec'
3
+ rescue LoadError
4
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
5
+ require 'spec'
6
+ end
7
+ begin
8
+ require 'spec/rake/spectask'
9
+ rescue LoadError
10
+ puts <<-EOS
11
+ To use rspec for testing you must install rspec gem:
12
+ gem install rspec
13
+ EOS
14
+ exit(0)
15
+ end
16
+
17
+ desc "Run the specs under spec/models"
18
+ Spec::Rake::SpecTask.new do |t|
19
+ t.spec_opts = ['--options', "spec/spec.opts"]
20
+ t.spec_files = FileList['spec/**/*_spec.rb']
21
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: albanpeignier-alsa-backup
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alban Peignier
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-18 00:00:00 -07:00
13
+ default_executable: alsa.backup
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: ffi
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.3.5
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: newgem
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.4.1
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: hoe
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 1.8.0
44
+ version:
45
+ description: ALSA client to perform continuous recording
46
+ email:
47
+ - alban.peignier@free.fr
48
+ executables:
49
+ - alsa.backup
50
+ extensions: []
51
+
52
+ extra_rdoc_files:
53
+ - History.txt
54
+ - Manifest.txt
55
+ - PostInstall.txt
56
+ - README.rdoc
57
+ files:
58
+ - .autotest
59
+ - History.txt
60
+ - Manifest.txt
61
+ - PostInstall.txt
62
+ - README.rdoc
63
+ - Rakefile
64
+ - alsa-backup.gemspec
65
+ - bin/alsa.backup
66
+ - config.sample
67
+ - lib/alsa.rb
68
+ - lib/alsa_backup.rb
69
+ - lib/alsa_backup/cli.rb
70
+ - lib/alsa_backup/recorder.rb
71
+ - lib/sndfile.rb
72
+ - script/console
73
+ - script/destroy
74
+ - script/generate
75
+ - spec/alsa/pcm_spec.rb
76
+ - spec/alsa_backup/cli_spec.rb
77
+ - spec/alsa_backup/recorder_spec.rb
78
+ - spec/alsa_backup_spec.rb
79
+ - spec/fixtures/config_test.rb
80
+ - spec/sndfile/info_spec.rb
81
+ - spec/spec.opts
82
+ - spec/spec_helper.rb
83
+ - tasks/rspec.rake
84
+ has_rdoc: true
85
+ homepage: http://github.com/alban.peignier/alsa-backup
86
+ post_install_message:
87
+ rdoc_options:
88
+ - --main
89
+ - README.rdoc
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: "0"
97
+ version:
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: "0"
103
+ version:
104
+ requirements: []
105
+
106
+ rubyforge_project: alsa-backup
107
+ rubygems_version: 1.2.0
108
+ signing_key:
109
+ specification_version: 3
110
+ summary: ALSA client to perform continuous recording
111
+ test_files: []
112
+