Merge branch 'master' into gregoa
[gregoa/zavai.git] / zavai / audio.py
1 # audio - zavai audio resource
2 #
3 # Copyright (C) 2009  Enrico Zini <enrico@enricozini.org>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
19 import zavai
20 import struct
21 import gobject
22 import os
23 import os.path
24 import subprocess
25 import signal
26 #import sys
27 #import time
28 #import dbus
29
30 class Recorder(zavai.Resource):
31     def __init__(self, registry):
32         super(Recorder, self).__init__()
33         self.registry = registry
34         self.recorder = None
35
36     def shutdown(self):
37         self.stop()
38
39     def start(self, filename):
40         if self.recorder is not None: return
41
42         self.registry.resource("audio").connect("audio", self)
43
44         self.recorder = subprocess.Popen(
45             ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", filename])
46
47     def stop(self):
48         if self.recorder is None: return
49
50         os.kill(self.recorder.pid, signal.SIGINT)
51         self.recorder.wait()
52         self.recorder = None
53
54         self.registry.resource("audio").disconnect("audio", self)
55
56
57 # TODO:
58 #  - hook into the headset plugged/unplugged event
59 #  - if unplugged, turn on handset microphone
60 #  - if plugged, redo headest mixer settings
61 class Audio(zavai.Service):
62     "Handle mixer settings, audio recording and headset button presses"
63     def __init__(self, registry, name):
64         super(Audio, self).__init__(["audio", "button", "jack"])
65
66         conf = registry.resource("conf")
67         self.saved_scenario = os.path.join(conf.homedir, "audiomap.state")
68
69         self.input_fd = None
70         self.input_watch = None
71
72
73     def start(self):
74         # Setup the mixer
75         # Set mixer to record from headset and handle headset button
76         self.save_scenario(self.saved_scenario)
77         self.load_scenario("/usr/share/openmoko/scenarios/voip-handset.state")
78
79         # This is a work-around because I have not found a way to query for the
80         # current headset state, I can only know when it changes. So in my
81         # system I configured oeventsd with a rule to touch this file when the
82         # headset is plugged in, and remove the file when it's plugged out.
83         if os.path.exists("/tmp/has_headset"):
84             self.mixer_for_headset(force = True)
85         else:
86             self.mixer_for_handset(force = True)
87
88         # Watch the event device
89         self.input_fd = open("/dev/input/event4", "rb")
90         self.input_watch = gobject.io_add_watch(self.input_fd.fileno(), gobject.IO_IN, self.on_input_data)
91         self.last_button_press = 0
92
93 #        #self.mixer_set("DAPM Handset Mic", "mute")
94 #        #self.mixer_set("DAPM Headset Mic", "unmute")
95 #        #self.mixer_set("Left Mixer Sidetone Playback Sw", "unmute")
96 #        #self.mixer_set("ALC Mixer Mic1", "cap")
97 #        #self.mixer_set("Amp Spk", "mute") # We don't need the phone playing what we say
98 #
99 #        self.recorder = None
100 #        self.basename = None
101
102     def stop(self):
103         # Stop watching the event device
104         if self.input_fd is None:
105             return
106         gobject.source_remove(self.input_watch)
107         self.input_watch = None
108         self.input_fd.close()
109         self.input_fd = None
110
111         # Restore mixer settings
112         self.load_scenario(self.saved_scenario)
113
114     def on_input_data(self, source, condition):
115         buf = self.input_fd.read(16)
116         ts1, ts2, type, code, value = struct.unpack("LLHHI", buf)
117         if type == 1 and code == 119:
118             if self.last_button_press + 1 < ts1:
119                 self.last_button_press = ts1
120             else:
121                 return True
122             if value:
123                 zavai.info("Headset button release")
124                 self.notify("button", False)
125             else:
126                 zavai.info("Headset button press")
127                 self.notify("button", True)
128         elif type == 5 and code == 2:
129             if value:
130                 zavai.info("Headset plugged in")
131                 self.mixer_for_headset()
132                 self.notify("jack", True)
133             else:
134                 zavai.info("Headset plugged out")
135                 self.mixer_for_handset()
136                 self.notify("jack", False)
137         return True
138
139     def mixer_for_headset(self, force=False):
140         if not force and self.has_headset: return
141         zavai.info("Setting mixer for headset")
142         # TODO: find out how to disable the handset microphone: this does not
143         # seem to be sufficient
144         self.mixer_set_many(
145                 ("DAPM Handset Mic", "mute"),
146                 ("DAPM Headset Mic", "unmute"),
147                 ("Left Mixer Sidetone Playback Sw", "unmute"),
148                 ("ALC Mixer Mic1", "cap"),
149                 ("Amp Spk", "mute") # We don't need the phone playing what we say
150         )
151         self.has_headset = True
152
153     def mixer_for_handset(self, force=False):
154         if not force and not self.has_headset: return
155         zavai.info("Setting mixer for handset")
156         self.mixer_set_many(
157                 ("DAPM Handset Mic", "unmute"),
158                 ("DAPM Headset Mic", "mute"),
159                 ("Left Mixer Sidetone Playback Sw", "mute"),
160                 ("ALC Mixer Mic1", "cap"),
161                 ("Amp Spk", "mute") # We don't need the phone playing what we say
162         )
163         self.has_headset = False
164
165 #    def set_basename(self, basename):
166 #        self.basename = basename
167 #
168 #    def start_levels(self):
169 #        self.recorder = subprocess.Popen(
170 #            ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", "-V", "stereo", "/dev/null"])
171
172     def save_scenario(self, name):
173         while True:
174                 res = subprocess.call(["alsactl", "store", "-f", name])
175                 if res == 0: return
176                 if res > 0:
177                     raise RuntimeError("Saving audio scenario to '%s' failed" % name)
178
179     def load_scenario(self, name):
180         while True:
181                 res = subprocess.call(["alsactl", "restore", "-f", name])
182                 if res == 0: return
183                 if res > 0:
184                     raise RuntimeError("Loading audio scenario '%s' failed with error %d" % (name, res))
185
186     def mixer_set(self, name, *args):
187         args = map(str, args)
188         while True:
189                 res = subprocess.call(["amixer", "-q", "set", name] + args)
190                 if res == 0: return
191                 if res > 0:
192                     raise RuntimeError("Setting mixer '%s' to %s failed with error %d" % (name, " ".join(args), res))
193
194     def mixer_set_many(self, *args):
195         """Perform many mixer set operations via amixer --stdin"""
196         cmd_input = []
197         for k, v in args:
198             cmd_input.append("sset " + repr(k) + " " + repr(v))
199         while True:
200                 proc = subprocess.Popen(["amixer", "-q", "--stdin"], stdin=subprocess.PIPE)
201                 (out, err) = proc.communicate(input="\n".join(cmd_input))
202                 res = proc.wait()
203                 if res == 0: return
204                 if res > 0:
205                     raise RuntimeError("Setting mixer failed with error %d" % res)
206
207
208 #class Hub:
209 #    """Hub that manages all the various resources that we use, and initiates
210 #    operations."""
211 #    def __init__(self, bus = None):
212 #        self.bus = bus
213 #        self.waiting_for_fix = False
214 #        self.basename = None
215 #        self.recorder = None
216 #        self.gpx = None
217 #        self.gps = None
218 #        self.gps_monitor = None
219 #        self.audio = None
220 #        self.last_pos = None
221 #
222 #    def shutdown(self):
223 #        # Stop recording
224 #        if self.audio is not None:
225 #            self.audio.close()
226 #
227 #        # Close waypoints file
228 #        if self.gpx is not None:
229 #            self.gpx.close()
230 #
231 #        # Stop the GPS monitor
232 #        if self.gps_monitor:
233 #            self.gps_monitor.stop()
234 #
235 #        # Release the GPS
236 #        if self.gps is not None:
237 #            self.gps.close()
238 #
239 #    def levels(self):
240 #        self.audio = Audio()
241 #        self.audio.start_levels()
242 #
243 #    def record(self):
244 #        self.audio = Audio(self.make_waypoint)
245 #        self.gps = GPS()
246 #        # Get a fix and start recording
247 #        if not self.gps.wait_for_fix(self.start_recording):
248 #            self.gps_monitor = GPSMonitor(self.gps)
249 #            self.gps_monitor.start()
250 #
251 #    def monitor(self):
252 #        self.audio = None
253 #        self.gps = GPS()
254 #        self.gps_monitor = GPSMonitor(self.gps)
255 #        self.gps_monitor.start()
256 #
257 #    def start_recording(self):
258 #        if self.gps_monitor:
259 #            self.gps_monitor.stop()
260 #            self.gps_monitor = None
261 #
262 #        if not self.audio:
263 #            return
264 #
265 #        # Sync system time
266 #        gpstime = self.gps.gps_time.GetTime()
267 #        subprocess.call(["date", "-s", "@%d" % gpstime])
268 #        subprocess.call(["hwclock", "--systohc"])
269 #
270 #        # Compute basename for output files
271 #        self.basename = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(gpstime))
272 #        self.basename = os.path.join(AUDIODIR, self.basename)
273 #
274 #        # Start recording the GPX track
275 #        self.gpx = GPX(self.basename)
276 #        self.gps.track_position(self.on_position_changed)
277 #
278 #        # Start recording in background forking arecord
279 #        self.audio.set_basename(self.basename)
280 #        self.audio.start_recording()
281 #
282 #    def on_position_changed(self, fields, tstamp, lat, lon, alt):
283 #        self.last_pos = (fields, tstamp, lat, lon, alt)
284 #        if self.gpx:
285 #            self.gpx.trackpoint(tstamp, lat, lon, alt)
286 #
287 #    def make_waypoint(self):
288 #        if self.gpx is None:
289 #            return
290 #        if self.last_pos is None:
291 #            self.last_pos = self.gps.gps_position.GetPosition()
292 #        (fields, tstamp, lat, lon, alt) = self.last_pos
293 #        self.gpx.waypoint(tstamp, lat, lon, alt)
294 #        info("Making waypoint at %s: %f, %f, %f" % (
295 #            time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(tstamp)), lat, lon, alt))