| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
|
|---|
| 8 |
import alsaaudio |
|---|
| 9 |
import sys |
|---|
| 10 |
import signal, os |
|---|
| 11 |
import time |
|---|
| 12 |
import wave |
|---|
| 13 |
import optparse |
|---|
| 14 |
from optparse import OptionParser |
|---|
| 15 |
import pwd, grp |
|---|
| 16 |
import syslog |
|---|
| 17 |
import subprocess |
|---|
| 18 |
import ConfigParser |
|---|
| 19 |
|
|---|
| 20 |
class Recorder: |
|---|
| 21 |
|
|---|
| 22 |
def __init__(self, config): |
|---|
| 23 |
self.channels = config.int('channels') |
|---|
| 24 |
self.rate = config.int('rate') |
|---|
| 25 |
self.basename = config['basename'] |
|---|
| 26 |
self.time_limit = config.int('time_limit') |
|---|
| 27 |
self.basedir = config['basedir'] |
|---|
| 28 |
self.after_script = config['after_script'] |
|---|
| 29 |
|
|---|
| 30 |
self.output_file_index = 0 |
|---|
| 31 |
self.output_stopped_until = None |
|---|
| 32 |
|
|---|
| 33 |
def init_alsa(self): |
|---|
| 34 |
self.inp = alsaaudio.PCM(alsaaudio.PCM_CAPTURE,alsaaudio.PCM_NONBLOCK) |
|---|
| 35 |
|
|---|
| 36 |
self.inp.setchannels(self.channels) |
|---|
| 37 |
self.inp.setrate(self.rate) |
|---|
| 38 |
self.inp.setformat(alsaaudio.PCM_FORMAT_S16_LE) |
|---|
| 39 |
|
|---|
| 40 |
self.inp.setperiodsize(160) |
|---|
| 41 |
|
|---|
| 42 |
def init_signals(self): |
|---|
| 43 |
def handler_stop(signum, frame): |
|---|
| 44 |
log('signal : exit') |
|---|
| 45 |
self.recording = False |
|---|
| 46 |
|
|---|
| 47 |
|
|---|
| 48 |
signal.signal(signal.SIGTERM, handler_stop) |
|---|
| 49 |
signal.signal(signal.SIGINT, handler_stop) |
|---|
| 50 |
|
|---|
| 51 |
def handler_nextfile(signum, frame): |
|---|
| 52 |
log('signal : new file') |
|---|
| 53 |
self.open_next_file() |
|---|
| 54 |
|
|---|
| 55 |
signal.signal(signal.SIGUSR1, handler_nextfile) |
|---|
| 56 |
|
|---|
| 57 |
def close_current_file(self): |
|---|
| 58 |
if self.output_file is not None: |
|---|
| 59 |
self.output_file.close() |
|---|
| 60 |
if self.after_script is not None: |
|---|
| 61 |
log("execute %s %s" % (self.after_script, self.output_file_name)) |
|---|
| 62 |
try: |
|---|
| 63 |
subprocess.Popen([self.after_script, self.output_file_name]) |
|---|
| 64 |
except OSError, e: |
|---|
| 65 |
log("error: after script failed: %d (%s)" % (e.errno, e.strerror)) |
|---|
| 66 |
|
|---|
| 67 |
self.output_file = None |
|---|
| 68 |
self.output_file_name = None |
|---|
| 69 |
|
|---|
| 70 |
def open_next_file(self): |
|---|
| 71 |
if self.output_stopped_until is not None: |
|---|
| 72 |
if self.output_stopped_until > time.time(): |
|---|
| 73 |
|
|---|
| 74 |
return |
|---|
| 75 |
else: |
|---|
| 76 |
|
|---|
| 77 |
self.output_stopped_until = None |
|---|
| 78 |
|
|---|
| 79 |
if self.output_file is not None: |
|---|
| 80 |
self.close_current_file() |
|---|
| 81 |
|
|---|
| 82 |
output_file_name = os.path.join(self.basedir , self.next_file_name()) |
|---|
| 83 |
|
|---|
| 84 |
try: |
|---|
| 85 |
self.output_file = wave.open(output_file_name, 'wb') |
|---|
| 86 |
log('new file : %s' % output_file_name) |
|---|
| 87 |
|
|---|
| 88 |
self.output_file.setnchannels(self.channels) |
|---|
| 89 |
self.output_file.setframerate(self.rate) |
|---|
| 90 |
self.output_file.setsampwidth(2) |
|---|
| 91 |
|
|---|
| 92 |
self.output_file_index += 1 |
|---|
| 93 |
self.output_file_name = output_file_name |
|---|
| 94 |
except IOError, e: |
|---|
| 95 |
log("error: can't open new file: %s - %d (%s) - retry in 60s" % (output_file_name, e.errno, e.strerror)) |
|---|
| 96 |
self.output_stopped_until = time.time() + 60 |
|---|
| 97 |
|
|---|
| 98 |
def next_file_name(self): |
|---|
| 99 |
info = { |
|---|
| 100 |
'time': time.strftime('%Y-%m-%d-%H-%M-%S'), |
|---|
| 101 |
'index' : self.output_file_index, |
|---|
| 102 |
'base' : self.basename |
|---|
| 103 |
} |
|---|
| 104 |
return '%(base)s-%(index)03d-%(time)s.wav' % info |
|---|
| 105 |
|
|---|
| 106 |
def run(self): |
|---|
| 107 |
log("start (output: %s, time: %d, channels: %d, rate: %d)" % (self.basename, self.time_limit, self.channels, self.rate )) |
|---|
| 108 |
|
|---|
| 109 |
self.init_alsa() |
|---|
| 110 |
self.init_signals() |
|---|
| 111 |
|
|---|
| 112 |
position_limit = self.time_limit * self.rate |
|---|
| 113 |
self.output_file = None |
|---|
| 114 |
self.recording = True |
|---|
| 115 |
|
|---|
| 116 |
while self.recording: |
|---|
| 117 |
l,data = self.inp.read() |
|---|
| 118 |
|
|---|
| 119 |
if l: |
|---|
| 120 |
if self.output_file is None: |
|---|
| 121 |
self.open_next_file() |
|---|
| 122 |
|
|---|
| 123 |
|
|---|
| 124 |
if self.output_file is not None: |
|---|
| 125 |
self.output_file.writeframesraw(data) |
|---|
| 126 |
|
|---|
| 127 |
output_position = self.output_file.tell() |
|---|
| 128 |
if output_position > position_limit: |
|---|
| 129 |
self.open_next_file() |
|---|
| 130 |
|
|---|
| 131 |
time.sleep(.001) |
|---|
| 132 |
|
|---|
| 133 |
self.close_current_file() |
|---|
| 134 |
log('exit') |
|---|
| 135 |
|
|---|
| 136 |
class Log: |
|---|
| 137 |
"""file like for writes with auto flush after each write |
|---|
| 138 |
to ensure that everything is logged, even during an |
|---|
| 139 |
unexpected exit.""" |
|---|
| 140 |
def __init__(self, f): |
|---|
| 141 |
self.f = f |
|---|
| 142 |
def write(self, s): |
|---|
| 143 |
self.f.write(s) |
|---|
| 144 |
self.f.flush() |
|---|
| 145 |
|
|---|
| 146 |
class Daemon: |
|---|
| 147 |
|
|---|
| 148 |
def __init__(self, config, main): |
|---|
| 149 |
self.uid = config['uid'] |
|---|
| 150 |
self.gid = config['gid'] |
|---|
| 151 |
self.pidfile = config['pidfile'] |
|---|
| 152 |
self.basedir = config['basedir'] |
|---|
| 153 |
self.logfile = config['logfile'] |
|---|
| 154 |
self.main = main |
|---|
| 155 |
|
|---|
| 156 |
if isinstance(self.uid, basestring): |
|---|
| 157 |
if self.uid.isdigit(): |
|---|
| 158 |
self.uid = int(self.uid) |
|---|
| 159 |
else: |
|---|
| 160 |
self.uid = pwd.getpwnam(self.uid).pw_uid |
|---|
| 161 |
|
|---|
| 162 |
if isinstance(self.gid, basestring): |
|---|
| 163 |
if self.gid.isdigit(): |
|---|
| 164 |
self.gid = int(self.gid) |
|---|
| 165 |
else: |
|---|
| 166 |
self.gid = grp.getgrnam(self.gid).gr_gid |
|---|
| 167 |
|
|---|
| 168 |
if self.logfile is None: |
|---|
| 169 |
self.logfile = "/dev/null" |
|---|
| 170 |
|
|---|
| 171 |
def start(self): |
|---|
| 172 |
log('start daemon') |
|---|
| 173 |
|
|---|
| 174 |
|
|---|
| 175 |
try: |
|---|
| 176 |
pid = os.fork() |
|---|
| 177 |
if pid > 0: |
|---|
| 178 |
|
|---|
| 179 |
sys.exit(0) |
|---|
| 180 |
except OSError, e: |
|---|
| 181 |
log("fork failed: %d (%s)" % (e.errno, e.strerror)) |
|---|
| 182 |
sys.exit(1) |
|---|
| 183 |
|
|---|
| 184 |
|
|---|
| 185 |
os.chdir("/") |
|---|
| 186 |
os.setsid() |
|---|
| 187 |
os.umask(0) |
|---|
| 188 |
|
|---|
| 189 |
|
|---|
| 190 |
try: |
|---|
| 191 |
pid = os.fork() |
|---|
| 192 |
if pid > 0: |
|---|
| 193 |
|
|---|
| 194 |
if self.pidfile is not None: |
|---|
| 195 |
open(self.pidfile,'w').write("%d\n"%pid) |
|---|
| 196 |
sys.exit(0) |
|---|
| 197 |
except OSError, e: |
|---|
| 198 |
log("fork 2 failed: %d (%s)" % (e.errno, e.strerror)) |
|---|
| 199 |
sys.exit(1) |
|---|
| 200 |
|
|---|
| 201 |
|
|---|
| 202 |
os.chdir(self.basedir) |
|---|
| 203 |
|
|---|
| 204 |
|
|---|
| 205 |
sys.stdin = open("/dev/null", 'r') |
|---|
| 206 |
|
|---|
| 207 |
sys.stdout = sys.stderr = Log(open(self.logfile, 'a+')) |
|---|
| 208 |
|
|---|
| 209 |
os.setegid(self.gid) |
|---|
| 210 |
os.seteuid(self.uid) |
|---|
| 211 |
|
|---|
| 212 |
|
|---|
| 213 |
self.main() |
|---|
| 214 |
|
|---|
| 215 |
class Logger: |
|---|
| 216 |
|
|---|
| 217 |
def __init__(self, console, syslogEnabled, logfile): |
|---|
| 218 |
self.console = console |
|---|
| 219 |
self.syslog = syslogEnabled |
|---|
| 220 |
self.logfile = None |
|---|
| 221 |
|
|---|
| 222 |
if self.syslog: |
|---|
| 223 |
syslog.openlog("justrecord", syslog.LOG_PID, syslog.LOG_USER) |
|---|
| 224 |
if logfile is not None: |
|---|
| 225 |
self.logfile = open(logfile, 'a+') |
|---|
| 226 |
|
|---|
| 227 |
def log(self, message): |
|---|
| 228 |
timed_message = "%s %s" % (time.strftime('%b %d %T'), message) |
|---|
| 229 |
|
|---|
| 230 |
if self.logfile is not None: |
|---|
| 231 |
self.logfile.write("%s\n" % timed_message) |
|---|
| 232 |
if self.logfile is None or self.console: |
|---|
| 233 |
print timed_message |
|---|
| 234 |
if self.syslog: |
|---|
| 235 |
syslog.syslog(message) |
|---|
| 236 |
|
|---|
| 237 |
def close(self): |
|---|
| 238 |
if self.logfile is not None: |
|---|
| 239 |
self.logfile.close() |
|---|
| 240 |
if self.syslog: |
|---|
| 241 |
syslog.closelog() |
|---|
| 242 |
|
|---|
| 243 |
class Config: |
|---|
| 244 |
|
|---|
| 245 |
def __init__(self, default_options): |
|---|
| 246 |
self.options = {} |
|---|
| 247 |
self.update(default_options) |
|---|
| 248 |
|
|---|
| 249 |
def update(self, options): |
|---|
| 250 |
if isinstance(options, optparse.Values): |
|---|
| 251 |
for entry in options.__dict__.iteritems(): |
|---|
| 252 |
if entry[1] is not None: |
|---|
| 253 |
self.options[entry[0]] = entry[1] |
|---|
| 254 |
else: |
|---|
| 255 |
self.options.update(options) |
|---|
| 256 |
|
|---|
| 257 |
return self |
|---|
| 258 |
|
|---|
| 259 |
def load_ini(self, inifile): |
|---|
| 260 |
config_parser = ConfigParser.ConfigParser() |
|---|
| 261 |
if config_parser.read([inifile]) is []: |
|---|
| 262 |
return False |
|---|
| 263 |
|
|---|
| 264 |
ini_options = {} |
|---|
| 265 |
for config_section in config_parser.sections(): |
|---|
| 266 |
for config_entry in config_parser.items(config_section): |
|---|
| 267 |
ini_options[config_entry[0]] = config_entry[1] |
|---|
| 268 |
self.update(ini_options) |
|---|
| 269 |
return True |
|---|
| 270 |
|
|---|
| 271 |
def __getitem__(self, key): |
|---|
| 272 |
return self.options[key] |
|---|
| 273 |
|
|---|
| 274 |
def string(self, key): |
|---|
| 275 |
return self.options[key] |
|---|
| 276 |
|
|---|
| 277 |
def int(self, key): |
|---|
| 278 |
return int(self.options[key]) |
|---|
| 279 |
|
|---|
| 280 |
def bool(self, key): |
|---|
| 281 |
value = self.options[key] |
|---|
| 282 |
if isinstance(value, basestring): |
|---|
| 283 |
return value.lower() in [ 'true', 'on', '1' ] |
|---|
| 284 |
return value is True |
|---|
| 285 |
|
|---|
| 286 |
if __name__ == "__main__": |
|---|
| 287 |
|
|---|
| 288 |
config = Config({ |
|---|
| 289 |
'basename': 'record', |
|---|
| 290 |
'channels': 2, |
|---|
| 291 |
'after_script': None, |
|---|
| 292 |
'rate': 44100, |
|---|
| 293 |
'time_limit': 30 * 60, |
|---|
| 294 |
'basedir': os.getcwd(), |
|---|
| 295 |
'daemon': False, |
|---|
| 296 |
'console': False, |
|---|
| 297 |
'syslog': False, |
|---|
| 298 |
'uid': os.getuid(), |
|---|
| 299 |
'gid': os.getgid(), |
|---|
| 300 |
'pidfile': None, |
|---|
| 301 |
'logfile': None |
|---|
| 302 |
}) |
|---|
| 303 |
|
|---|
| 304 |
parser = OptionParser() |
|---|
| 305 |
parser.add_option("-o", "--output", dest="basename", |
|---|
| 306 |
help="The output basename. The default is 'record'.") |
|---|
| 307 |
parser.add_option("-c", "--config", dest="config", default=None, |
|---|
| 308 |
help="The configuration file.") |
|---|
| 309 |
parser.add_option("-n", "--channels", dest="channels", type="int", |
|---|
| 310 |
help="The number of channels. The default is %d channels." % config['channels']) |
|---|
| 311 |
parser.add_option("-r", "--rate", dest="rate", |
|---|
| 312 |
choices=["48000", "44100", "22000"], |
|---|
| 313 |
help="The sampling rate in Hertz. The default rate is %d Hertz" % config['rate']) |
|---|
| 314 |
parser.add_option("-t", "--time", dest="time_limit", type="int", |
|---|
| 315 |
help="The time length of recording file in seconds. The default time length is 1800 seconds (30 minutes).") |
|---|
| 316 |
parser.add_option("-b", "--base-dir", dest="basedir", |
|---|
| 317 |
help="The directory where the recording files are created. The default directory is the current one.") |
|---|
| 318 |
parser.add_option("-d", "--daemon", action="store_true", dest="daemon", |
|---|
| 319 |
help="Starts as daemon. Disabled by default") |
|---|
| 320 |
parser.add_option("-C", "--console", action="store_true", dest="console", |
|---|
| 321 |
help="Logs messages to the console even if a logfile is specified.") |
|---|
| 322 |
parser.add_option("-s", "--syslog", action="store_true", dest="syslog", |
|---|
| 323 |
help="Logs messages to syslog.") |
|---|
| 324 |
parser.add_option("-u", "--uid", dest="uid", |
|---|
| 325 |
help="Change to this username/uid before starting the recorder in daemon mode. The default is the current user.") |
|---|
| 326 |
parser.add_option("-g", "--gid", dest="gid", |
|---|
| 327 |
help="Change to this group/gid before starting the recorder in daemon mode. The default is the current group.") |
|---|
| 328 |
parser.add_option("-p", "--pidfile", dest="pidfile", |
|---|
| 329 |
help="The file used to store the pid in daemon mode.") |
|---|
| 330 |
parser.add_option("-l", "--logfile", dest="logfile", |
|---|
| 331 |
help="The file used to log messages.") |
|---|
| 332 |
parser.add_option("-A", "--after-script", dest="after_script", |
|---|
| 333 |
help="Specifies a script to be executed each time a recording file is finished.") |
|---|
| 334 |
(commandline_options, args) = parser.parse_args() |
|---|
| 335 |
|
|---|
| 336 |
if commandline_options.config is not None: |
|---|
| 337 |
if not config.load_ini(commandline_options.config): |
|---|
| 338 |
log("error: can't read config file: %s" % commandline_options.config) |
|---|
| 339 |
sys.exit(1) |
|---|
| 340 |
|
|---|
| 341 |
config.update(commandline_options) |
|---|
| 342 |
|
|---|
| 343 |
logger = Logger(config.bool('console'), config.bool('syslog'), config['logfile']) |
|---|
| 344 |
recorder = Recorder(config) |
|---|
| 345 |
|
|---|
| 346 |
def log(message): |
|---|
| 347 |
logger.log(message) |
|---|
| 348 |
|
|---|
| 349 |
log("config: %s" % config.options) |
|---|
| 350 |
|
|---|
| 351 |
def main(): |
|---|
| 352 |
recorder.run() |
|---|
| 353 |
logger.close() |
|---|
| 354 |
|
|---|
| 355 |
daemon = config.bool('daemon') |
|---|
| 356 |
if daemon: |
|---|
| 357 |
Daemon(config, main).start() |
|---|
| 358 |
else: |
|---|
| 359 |
main() |
|---|