alsa-backup 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'active_support'
5
+ require 'logger'
6
+
7
+ module AlsaBackup
8
+ VERSION = '0.0.8'
9
+
10
+ def self.recorder
11
+ @recorder ||= AlsaBackup::Recorder.new
12
+ end
13
+
14
+ def self.config
15
+ yield self.recorder
16
+ end
17
+
18
+ def self.logger
19
+ unless @logger
20
+ @logger = Logger.new(STDOUT)
21
+ @logger.level = Logger::INFO
22
+ end
23
+
24
+ @logger
25
+ end
26
+
27
+ def self.logger=(logger); @logger = logger; end
28
+
29
+ end
30
+
31
+ require 'alsa_backup/core_ext'
32
+ require 'alsa_backup/length_controller'
33
+ require 'alsa_backup/writer'
34
+ require 'alsa_backup/recorder'
35
+
36
+
37
+
@@ -0,0 +1,56 @@
1
+ require 'optparse'
2
+ require 'daemons'
3
+
4
+ module AlsaBackup
5
+ class CLI
6
+ def self.execute(stdout, *arguments)
7
+ options = {}
8
+ mandatory_options = %w()
9
+
10
+ OptionParser.new do |opts|
11
+ opts.banner = <<-BANNER.gsub(/^ /,'')
12
+ AlsaBackup : continuous recording with alsa
13
+
14
+ Usage: #{File.basename($0)} [options]
15
+
16
+ Options are:
17
+ BANNER
18
+ opts.separator ""
19
+ opts.on("-f", "--file=FILE", String,
20
+ "Recording file") { |arg| options[:file] = arg }
21
+ opts.on("-l", "--length=LENGTH", String,
22
+ "Length in seconds") { |arg| options[:length] = arg }
23
+ opts.on("-d", "--directory=DIRECTORY", String,
24
+ "Base directory") { |arg| options[:directory] = arg }
25
+ opts.on("-c", "--config=CONFIG", String,
26
+ "Configuration file") { |arg| options[:config] = arg }
27
+ opts.on("-p", "--pid=PID_FILE", String,
28
+ "File to write the process pid") { |arg| options[:pid] = arg }
29
+ opts.on("-b", "--background", nil,
30
+ "Daemonize the process") { |arg| options[:daemonize] = true }
31
+ opts.on("-h", "--help",
32
+ "Show this help message.") { stdout.puts opts; exit }
33
+ opts.parse!(arguments)
34
+
35
+ if mandatory_options && mandatory_options.find { |option| options[option.to_sym].nil? }
36
+ stdout.puts opts; exit
37
+ end
38
+ end
39
+
40
+ load File.expand_path(options[:config]) if options[:config]
41
+
42
+ AlsaBackup.recorder.file = options[:file] if options[:file]
43
+ AlsaBackup.recorder.directory = options[:directory] if options[:directory]
44
+
45
+ pid_file = File.expand_path(options[:pid]) if options[:pid]
46
+
47
+ Daemonize.daemonize(nil, "alsa-backup") if options[:daemonize]
48
+ File.write(pid_file, $$) if pid_file
49
+
50
+ length = options[:length].to_i if options[:length]
51
+ AlsaBackup.recorder.start(length)
52
+ rescue Exception => e
53
+ AlsaBackup.logger.fatal(e)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,34 @@
1
+ class Time
2
+
3
+ def floor(attribute, modulo)
4
+ actual = self.send(attribute)
5
+ self.change(attribute => actual - actual%modulo)
6
+ end
7
+
8
+ end
9
+
10
+ class File
11
+
12
+ def self.suffix_basename(file, suffix)
13
+ dirname = File.dirname(file)
14
+
15
+ dirname =
16
+ case dirname
17
+ when "/": "/"
18
+ when ".": ""
19
+ else
20
+ dirname + "/"
21
+ end
22
+
23
+ extension = File.extname(file)
24
+ dirname +
25
+ File.basename(file, extension) +
26
+ suffix +
27
+ extension
28
+ end
29
+
30
+ def self.write(file, content)
31
+ File.open(file, "w") { |f| f.puts content }
32
+ end
33
+
34
+ end
@@ -0,0 +1,21 @@
1
+ module AlsaBackup
2
+ module LengthController
3
+ class Loop
4
+ def continue_after?(frame_count)
5
+ true
6
+ end
7
+ end
8
+
9
+ class FrameCount
10
+ attr_reader :frame_count
11
+
12
+ def initialize(frame_count)
13
+ @frame_count = frame_count
14
+ end
15
+
16
+ def continue_after?(frame_count)
17
+ (@frame_count -= frame_count) > 0
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,91 @@
1
+ require 'alsa'
2
+
3
+ module AlsaBackup
4
+ class Recorder
5
+
6
+ def initialize(file = "record.wav")
7
+ @file = File.basename(file)
8
+ @directory = File.dirname(file)
9
+
10
+ @device = "hw:0"
11
+ @sample_rate = 44100
12
+ @channels = 2
13
+
14
+ @error_handler = Proc.new { |e| true }
15
+ end
16
+
17
+ attr_accessor :file, :directory, :error_handler
18
+ attr_accessor :device, :sample_rate, :channels
19
+
20
+ def start(seconds_to_record = nil)
21
+ length_controller = self.length_controller(seconds_to_record)
22
+
23
+ open_writer do |writer|
24
+ open_capture do |capture|
25
+ capture.read do |buffer, frame_count|
26
+ writer.write buffer, frame_count*format[:channels]
27
+ length_controller.continue_after? frame_count
28
+ end
29
+ end
30
+ end
31
+ rescue Exception => e
32
+ retry if handle_error(e, seconds_to_record.nil?)
33
+ end
34
+
35
+ def open_writer(&block)
36
+ writer_options = { :directory => directory, :file => file, :format => format(:format => "wav pcm_16") }
37
+ writer_options[:on_close] = @on_close if @on_close
38
+ Writer.open(writer_options, &block)
39
+ end
40
+
41
+ def open_capture(&block)
42
+ ALSA::PCM::Capture.open(device, format(:sample_format => :s16_le), &block)
43
+ end
44
+
45
+ def handle_error(e, try_to_continue = true)
46
+ if Interrupt === e or SignalException === e
47
+ AlsaBackup.logger.debug('recorder interrupted')
48
+ return false
49
+ end
50
+
51
+ AlsaBackup.logger.error(e)
52
+ AlsaBackup.logger.debug { e.backtrace.join("\n") }
53
+
54
+ if try_to_continue and continue_on_error?(e)
55
+ return true
56
+ else
57
+ raise e
58
+ end
59
+ end
60
+
61
+ def continue_on_error?(e)
62
+ error_handler_response = @error_handler.call(e) if @error_handler
63
+
64
+ if error_handler_response
65
+ sleep_time = Numeric === error_handler_response ? error_handler_response : 5
66
+ AlsaBackup.logger.warn("sleep #{sleep_time}s before retrying")
67
+ sleep sleep_time
68
+ end
69
+
70
+ error_handler_response
71
+ end
72
+
73
+ def format(additional_parameters = {})
74
+ {:sample_rate => sample_rate, :channels => channels}.merge(additional_parameters)
75
+ end
76
+
77
+ def length_controller(seconds_to_record)
78
+ if seconds_to_record
79
+ AlsaBackup::LengthController::FrameCount.new format[:sample_rate] * seconds_to_record
80
+ else
81
+ AlsaBackup::LengthController::Loop.new
82
+ end
83
+ end
84
+
85
+ def on_close(&block)
86
+ @on_close = block
87
+ end
88
+
89
+ end
90
+
91
+ end
@@ -0,0 +1,124 @@
1
+ require 'sndfile'
2
+ require 'fileutils'
3
+
4
+ module AlsaBackup
5
+ class Writer
6
+
7
+ attr_accessor :directory, :file, :format, :on_close_callbacks
8
+
9
+ def self.default_format
10
+ {:sample_rate => 44100, :channels => 2, :format => "wav pcm_16"}
11
+ end
12
+
13
+ def initialize(options = {})
14
+ options = {
15
+ :format => Writer.default_format
16
+ }.update(options)
17
+
18
+ @directory = options[:directory]
19
+ @file = options[:file]
20
+ @format = options[:format]
21
+
22
+ @on_close_callbacks = [ Proc.new do |file|
23
+ Writer.delete_empty_file(file)
24
+ end ]
25
+ @on_close_callbacks << options[:on_close] if options[:on_close]
26
+ end
27
+
28
+ def self.open(options, &block)
29
+ writer = Writer.new(options)
30
+ begin
31
+ writer.prepare
32
+ yield writer
33
+ ensure
34
+ writer.close
35
+ end
36
+ end
37
+
38
+ def prepare
39
+ # prepare sndfile
40
+ self.sndfile
41
+ self
42
+ end
43
+
44
+ def write(*arguments)
45
+ self.sndfile.write *arguments
46
+ end
47
+
48
+ def close
49
+ close_file
50
+ end
51
+
52
+ def close_file
53
+ if @sndfile
54
+ on_close(@sndfile.path)
55
+ @sndfile.close
56
+ end
57
+ @sndfile = nil
58
+ end
59
+
60
+ def on_close(file)
61
+ AlsaBackup.logger.info('close current file')
62
+ @on_close_callbacks.each do |callback|
63
+ begin
64
+ callback.call(file)
65
+ rescue Exception => e
66
+ AlsaBackup.logger.error("error in on_close callback : #{e}")
67
+ AlsaBackup.logger.debug { e.backtrace.join("\n") }
68
+ end
69
+ end
70
+ end
71
+
72
+ def file
73
+ case @file
74
+ when Proc
75
+ @file.call
76
+ else
77
+ @file
78
+ end
79
+ end
80
+
81
+ def target_file
82
+ File.join self.directory, self.file
83
+ end
84
+
85
+ def sndfile
86
+ target_file = self.target_file
87
+ raise "no recording file" unless target_file
88
+
89
+ unless @sndfile and @sndfile.path == target_file
90
+ close_file
91
+
92
+ Writer.rename_existing_file(target_file)
93
+ AlsaBackup.logger.info{"new file #{File.expand_path target_file}"}
94
+
95
+ FileUtils.mkdir_p File.dirname(target_file)
96
+ @sndfile = Sndfile::File.new(target_file, "w", self.format)
97
+ end
98
+ @sndfile
99
+ end
100
+
101
+ def self.delete_empty_file(file)
102
+ if File.exists?(file) and File.size(file) <= 44
103
+ AlsaBackup.logger.warn("remove empty file #{file}")
104
+ File.delete file
105
+ end
106
+ end
107
+
108
+ def self.rename_existing_file(file)
109
+ if File.exists?(file)
110
+ index = 1
111
+
112
+ while File.exists?(new_file = File.suffix_basename(file, "-#{index}"))
113
+ index += 1
114
+
115
+ raise "can't find a free file for #{file}" if index > 1000
116
+ end
117
+
118
+ AlsaBackup.logger.warn "rename existing file #{File.basename(file)} into #{new_file}"
119
+ File.rename(file, new_file)
120
+ end
121
+ end
122
+
123
+ end
124
+ end
data/lib/sndfile.rb ADDED
@@ -0,0 +1,138 @@
1
+ require 'ffi'
2
+
3
+ require 'logger'
4
+
5
+ module Sndfile
6
+
7
+ def self.logger
8
+ unless @logger
9
+ @logger = Logger.new(STDERR)
10
+ @logger.level = Logger::WARN
11
+ end
12
+
13
+ @logger
14
+ end
15
+
16
+ def self.logger=(logger); @logger = logger; end
17
+
18
+ class File
19
+
20
+ attr_reader :path
21
+
22
+ def self.open(path, mode, info)
23
+ file = self.new(path, mode, info)
24
+
25
+ begin
26
+ yield file
27
+ ensure
28
+ file.close
29
+ end
30
+ end
31
+
32
+ def initialize(path, mode, info)
33
+ info = (Hash === info ? Info.new(info) : info)
34
+ @handle = Sndfile::Native::open path, File.native_mode(mode), info.to_native
35
+ if @handle.null?
36
+ raise "Not able to open output file " + self.error
37
+ end
38
+ @path = path
39
+ end
40
+
41
+ def write(buffer, frame_count)
42
+ ALSA.logger.debug { "write #{frame_count} frames in #{path}" }
43
+ write_count = Sndfile::Native::write_short(@handle, buffer, frame_count)
44
+
45
+ unless write_count == frame_count
46
+ raise self.error
47
+ end
48
+ end
49
+
50
+ def close
51
+ Sndfile::Native::close @handle
52
+ end
53
+
54
+ def error
55
+ Sndfile::Native::strerror @handle
56
+ end
57
+
58
+ def self.native_mode(mode)
59
+ case mode
60
+ when "w": Sndfile::Native::MODE_WRITE
61
+ else
62
+ raise "Unknown mode: #{mode}"
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ class Info
69
+
70
+ attr_accessor :sample_rate, :channels, :format
71
+
72
+ def initialize(attributes = {})
73
+ update_attributes(attributes)
74
+ end
75
+
76
+ def update_attributes(attributes)
77
+ attributes.each_pair { |name, value| send("#{name}=", value) }
78
+ end
79
+
80
+ def to_native
81
+ Sndfile::Native::Info.new.tap do |native|
82
+ native[:samplerate] = self.sample_rate
83
+ native[:channels] = self.channels
84
+ native[:format] = self.native_format
85
+ end
86
+ end
87
+
88
+ def format=(format)
89
+ @format = Info.normalized_format(format)
90
+ end
91
+
92
+ def native_format
93
+ self.format.inject(0) do |native_format, format_part|
94
+ native_format | Sndfile::Native::Format.const_get(format_part.upcase)
95
+ end
96
+ end
97
+
98
+ def self.normalized_format(format)
99
+ Array(format).join(' ').downcase.scan(/[a-z0-9_]+/)
100
+ end
101
+
102
+ end
103
+
104
+ module Native
105
+ extend FFI::Library
106
+ ffi_lib "libsndfile.so"
107
+
108
+ module Format
109
+ WAV = 0x010000
110
+ PCM_16 = 0x0002
111
+ PCM_24 = 0x0003
112
+ end
113
+
114
+ MODE_READ = 0x10
115
+ MODE_WRITE = 0x20
116
+ MODE_RDWR = 0x30
117
+
118
+ class Info < FFI::Struct
119
+ layout(
120
+ :frames, :int64,
121
+ :samplerate, :int,
122
+ :channels, :int,
123
+ :format, :int,
124
+ :sections, :int,
125
+ :seekable, :int
126
+ )
127
+ end
128
+
129
+ attach_function :open, :sf_open, [ :string, :int, :pointer ], :pointer
130
+ attach_function :close, :sf_close, [ :pointer ], :int
131
+
132
+ # TODO off_t won't work on windows
133
+ attach_function :write_int, :sf_write_int, [ :pointer, :pointer, :off_t ], :off_t
134
+ attach_function :write_short, :sf_write_short, [ :pointer, :pointer, :off_t ], :off_t
135
+
136
+ attach_function :strerror, :sf_strerror, [ :pointer ], :string
137
+ end
138
+ end