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