alsa-backup 0.0.8

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