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