albanpeignier-alsa-backup 0.0.1

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.
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
+