[Shtoom] big patch for dev to try and others to look at

zooko at zooko.com zooko at zooko.com
Wed Sep 28 04:37:05 CEST 2005


I'm sorry to send such a big patch, but I lack the time to make it into several
smaller patches.

This isn't the final version -- it is just a draft for dev and me to work on
and anyone else who wants to.  For one thing, this patch applies to SVN r1406!
For another, it's not well tested.  Also it's incomplete, e.g. I broke all the
audio devices and then fixed only a couple of them.  I have never used or
completely understood doug, and I haven't tested it, therefore I'm sure that
this patch breaks doug even though I tried to make what appeared to be the
appropriate changes to doug as I went along.

So what's good about this patch?  Many things are much simpler!  Cleanliness
and elegance are starting to shine through the cruft.  The audio stack is
completely rewritten, including PT/CT mapping and so on.  SIP/SDP is somewhat
tidied up, a few bugs fixed, but not completely rewritten.  Initialization and
configuration is simplified in a few ways.  Much unreachable code and unused
(as far as I could tell) features are removed.

More details, in the order that I notice them while browsing through the patch:

 * There are two audio devices: one for reading from the microphone and one for
   writing to the speakers.  The one for reading from the microphone makes
   calls to you to give you microphone data -- you do not make calls to it to
   ask for microphone data.

 * Initialization is done, whenever possible, by passing objects to the
   __init__ of larger objects, such as "Phone(audioplaydev, audiomicdev)"
   instead of having Phone itself dynamically create an audio device by a
   combination of configuration and detection.

 * All call cookies are gone -- A Phone instance is capable of handling only
   one call at a time.  In fact, in the context of shtoom.app.Phone call
   cookies were never actually working, and when removing them I discovered
   that they were causing a bug, which is (of course) now fixed.

 * New scheme for muting playout of voice during playout of a wav file.  This
   fixes various bugs, and it is nice and clean and encapsulated.

 * SDP negotiation code is simpler.

 * Mapping from PT byte to decoder is done by a dict from, well, from PT byte
   to decoder object.  That dict is held by audio.converters.PlayDev.

 * Choosing an outgoing encodec is done by passing a reference to an encoder
   object when you open the audio microphone read device.

 * I cleaned up SIP state machine a bit, and I think I fixed a bug or two.
   I don't recall exactly at the time of this writing.

 * I did update the unit tests and make them pass for everything except the
   stuff that I knew I was breaking.

Warning: there was a lot of stuff that I wasn't sure if it was actually used or
needed, and I decided to delete it and then put it back after it was missed.
You may miss it before I do.  Sorry about that!

Hope this helps!  I intend to keep hacking on it in my (ahem) spare time.

Regards,

Zooko

MIME attachment, application/octet-stream, unified diff, gzipped for armor
against nasty meddling mail programs.  Also appended in-line for your viewing
pleasure.

Also you can get it via darcs or http from:

http://zooko.com:www/repos/shtoom/1406-plus-zooko-and-dev-patches

-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/octet-stream
Size: 22390 bytes
Desc: big.patch.gz
URL: <http://mail.python.org/pipermail/shtoom/attachments/20050927/d6222747/attachment.obj>
-------------- next part --------------
Tue Sep 27 23:34:41 ADT 2005  zooko at zooko.com
  * the big patch, first draft
  To: shtoom at python.org
  From: zooko at zooko.com
  Subject: big patch for dev to try and others to look at
  Date: 2005-09-27
  --------
  
  I'm sorry to send such a big patch, but I lack the time to make it into several
  smaller patches.
  
  This isn't the final version -- it is just a draft for dev and me to work on
  and anyone else who wants to.  For one thing, this patch applies to SVN r1406!
  For another, it's not well tested.  Also it's incomplete, e.g. I broke all the
  audio devices and then fixed only a couple of them.  I have never used or
  completely understood doug, and I haven't tested it, therefore I'm sure that
  this patch breaks doug even though I tried to make what appeared to be the
  appropriate changes to doug as I went along.
  
  So what's good about this patch?  Many things are much simpler!  Cleanliness
  and elegance are starting to shine through the cruft.  The audio stack is
  completely rewritten, including PT/CT mapping and so on.  SIP/SDP is somewhat
  tidied up, a few bugs fixed, but not completely rewritten.  Initialization and
  configuration is simplified in a few ways.  Much unreachable code and unused
  (as far as I could tell) features are removed.
  
  More details, in the order that I notice them while browsing through the patch:
  
   * There are two audio devices: one for reading from the microphone and one for
     writing to the speakers.  The one for reading from the microphone makes
     calls to you to give you microphone data -- you do not make calls to it to
     ask for microphone data.
  
   * Initialization is done, whenever possible, by passing objects to the
     __init__ of larger objects, such as "Phone(audioplaydev, audiomicdev)"
     instead of having Phone itself dynamically create an audio device by a
     combination of configuration and detection.
  
   * All call cookies are gone -- A Phone instance is capable of handling only
     one call at a time.  In fact, in the context of shtoom.app.Phone call
     cookies were never actually working, and when removing them I discovered
     that they were causing a bug, which is (of course) now fixed.
    
   * New scheme for muting playout of voice during playout of a wav file.  This
     fixes various bugs, and it is nice and clean and encapsulated.
  
   * SDP negotiation code is simpler.
  
   * Mapping from PT byte to decoder is done by a dict from, well, from PT byte
     to decoder object.  That dict is held by audio.converters.PlayDev.
  
   * Choosing an outgoing encodec is done by passing a reference to an encoder
     object when you open the audio microphone read device.
  
   * I cleaned up SIP state machine a bit, and I think I fixed a bug or two.
     I don't recall exactly at the time of this writing.
    
   * I did update the unit tests and make them pass for everything except the
     stuff that I knew I was breaking.
  
  Warning: there was a lot of stuff that I wasn't sure if it was actually used or
  needed, and I decided to delete it and then put it back after it was missed.
  You may miss it before I do.  Sorry about that!
  
  Hope this helps!  I intend to keep hacking on it in my (ahem) spare time.
  
  Regards,
  
  Zooko
    
diff -rN -u old-newfrom1406/trunk/shtoom/scripts/shreadder.py new-newfrom1406/trunk/shtoom/scripts/shreadder.py
--- old-newfrom1406/trunk/shtoom/scripts/shreadder.py	2005-09-27 23:36:20.433369368 -0300
+++ new-newfrom1406/trunk/shtoom/scripts/shreadder.py	2005-09-27 23:36:20.495359944 -0300
@@ -4,7 +4,6 @@
 import math, random, struct, sys
 sys.path.append(sys.path.pop(0))
 import shtoom.audio
-from shtoom.rtp import formats
 
 from shtoom.rtp.packets import RTPPacket
 
@@ -54,25 +53,15 @@
 
 
 def main(Recorder = Recorder):
-    from shtoom.audio import getAudioDevice
-    from shtoom.rtp import formats
+    from shtoom.audio import getAudioPlayDevice, getAudioMicDevice
     from twisted.internet.task import LoopingCall
     from twisted.internet import reactor
-    from twisted.python import log
     import sys
-    log.startLogging(sys.stdout)
 
-    dev = getAudioDevice()
+    playdev = getAudioPlayDevice()
+    micdev = getAudioMicDevice()
     dev.close()
-    if len(sys.argv) > 1:
-        fmt = sys.argv[1]
-        if not hasattr(formats, fmt):
-            print "unknown PT marker %s"%(fmt)
-            sys.exit(1)
-        dev.selectDefaultFormat([getattr(formats,fmt),])
-    else:
-        dev.selectDefaultFormat([formats.PT_RAW,])
-    rec = Recorder(dev, play=True)
+    rec = Recorder(playdev, micdev, play=True)
     dev.reopen(mediahandler=rec)
 
     reactor.run()
diff -rN -u old-newfrom1406/trunk/shtoom/scripts/shtoomphone.py new-newfrom1406/trunk/shtoom/scripts/shtoomphone.py
--- old-newfrom1406/trunk/shtoom/scripts/shtoomphone.py	2005-09-27 23:36:20.429369976 -0300
+++ new-newfrom1406/trunk/shtoom/scripts/shtoomphone.py	2005-09-27 23:36:20.495359944 -0300
@@ -11,6 +11,13 @@
 else:
     sys.path.append(f)
 
+from twisted.python import log
+
+from shtoom.audio import getAudioPlayDevice, getAudioMicDevice
+from shtoom.ui.textui import ShtoomMain
+
+from pyutil.timeutil import isoformat
+
 app = None
 
 def main():
@@ -20,24 +27,12 @@
     from shtoom.app.phone import Phone
     global app
 
-    app = Phone()
-    app.boot(args=sys.argv[1:])
+    tui = ShtoomMain()
 
-    audioPref = app.getPref('audio')
-    audio_in = app.getPref('audio_infile')
-    audio_out = app.getPref('audio_outfile')
-    if audio_in and audio_out:
-        aF = ( audio_in, audio_out )
-    else:
-        aF = None
-
-    from twisted.python import log
-    log.msg("Getting new audio device", system='phone')
-
-    from shtoom.audio import getAudioDevice
-    # XXX Aarrgh.
-    app._audio = getAudioDevice()
-    log.msg("Got new audio device %s :: %s" % (app._audio, type(app._audio),))
+    log.msg("timestamp: %s about to construct Phone" % (isoformat(),), system='phone')
+    app = Phone(getAudioPlayDevice(), getAudioMicDevice(), tui)
+    log.msg("timestamp: %s finished constructing Phone" % (isoformat(),), system='phone')
+    app.boot(args=sys.argv[1:])
 
     def run_it():
         app.start()
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/app/phone.py new-newfrom1406/trunk/shtoom/shtoom/app/phone.py
--- old-newfrom1406/trunk/shtoom/shtoom/app/phone.py	2005-09-27 23:36:20.427370280 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/app/phone.py	2005-09-27 23:36:20.518356448 -0300
@@ -2,11 +2,15 @@
 
 # The Phone app.
 
-import os, sys, threading
+import os, platform, sys, threading, time
 
 from twisted.internet import defer, protocol
 from twisted.python import log, threadable
 
+from shtoom.credcache import CredCache
+from shtoom.rtp.protocol import RTPProtocol
+from shtoom.audio.converters import PlayDev, MicDev
+from shtoom.audio import aufile
 from shtoom.app.interfaces import Application
 from shtoom.app.base import BaseApplication
 from shtoom.exceptions import CallFailed
@@ -17,23 +21,37 @@
 
 from shtoom.rtp.formats import PT_PCMU, PT_GSM, PT_SPEEX, PT_DVI4
 
+from pyutil.assertutil import _assert, precondition, postcondition
+
 class Phone(BaseApplication):
     __implements__ = ( Application, )
 
     _startReactor = True
 
-    def __init__(self, ui=None, audio=None):
-        # Mapping from callcookies to rtp object
-        self._rtp = {}
-        # Mapping from callcookies to call objects
-        self._calls = {}
-        self._pendingRTP = {}
-        self._audio = audio
+    def __init__(self, audioplaydev, audiomicdev, ui=None):
+        precondition(audioplaydev and hasattr(audioplaydev, 'open') and hasattr(audioplaydev, 'write'), audioplaydev)
+        precondition(audiomicdev and hasattr(audiomicdev, 'open'), audiomicdev)
+
+        self.rtpmap = None
+        self.encformat = None
+        self._audioplaydev = PlayDev(audioplaydev)
+        self._audiomicdev = MicDev(audiomicdev)
+        self._audiomicdev.open() # This is to read microphone samples and dump them into /dev/random.
+
+        self._rtp = None
+        self.call = None
         self.ui = ui
-        self._currentCall = None
-        self._muted = False
+        self._muteplayout = False
         self._rtpProtocolClass = None
-        self._debugrev = 10
+        self._develrevision = "dev-and-zooko-hack-branch-from-1406"
+        self.wavfileplayer = None
+        log.msg("Shtoom devel revision %s, platform: %s" % (self._develrevision, platform.platform(),))
+        self.statusMessage('Ready.')
+
+    def notifyEvent(self, methodName, *args, **kw):
+        method = getattr(self, methodName, None)
+        if method is not None:
+            method(*args, **kw)
 
     def find_resource_file(self, fname):
         """ Return the fully-qualified path to the desired resource file that came bundled with Shtoom.  On failure, it returns fname.
@@ -83,7 +101,8 @@
     def register(self):
         register_uri = self.getPref('register_uri')
         if register_uri is not None:
-            return self.sip.register()
+            self.sip.register()
+            self.statusMessage('Registering...')
 
     def start(self):
         "Start the application."
@@ -91,7 +110,7 @@
         if not self._startReactor:
             log.msg("Not starting reactor - test mode?")
             return
-        if self.needsThreadedUI():
+        if self.ui.threadedUI:
             threadable.init(1)
             from twisted.internet import reactor
             t = threading.Thread(target=reactor.run, kwargs={
@@ -107,132 +126,95 @@
         self.play_wav_file('ring_back_file')
 
     def acceptCall(self, call):
-        log.msg("dialog is %r"%(call.dialog))
-        cookie = self.getCookie()
-        self._calls[cookie] = call
+        log.msg("acceptCall %s" % (call,))
+        self.call = call
         calltype = call.dialog.getDirection()
         if calltype == 'inbound':
             self.statusMessage('Incoming call')
             self.play_wav_file('incoming_ring_file')
 
-            uidef = self.ui.incomingCall(call.dialog.getCaller(), cookie)
+            uidef = self.ui.incomingCall(call.dialog.getCaller())
             uidef.addCallback(lambda x: self._createRTP(x,
                                         call.getLocalSIPAddress()[0],
                                         call.getSTUNState()))
             return uidef
         elif calltype == 'outbound':
-            return self._createRTP(cookie, call.getLocalSIPAddress()[0],
-                                           call.getSTUNState())
+            return self._createRTP(call.getLocalSIPAddress()[0],
+                                   call.getSTUNState())
         else:
             raise ValueError, "unknown call type %s"%(calltype)
         return d
 
-    def _createRTP(self, cookie, localIP, withSTUN):
-        from shtoom.rtp.protocol import RTPProtocol
-        from shtoom.exceptions import CallFailed
-        if isinstance(cookie, CallFailed):
-            del self._calls[cookie.cookie]
-            return defer.succeed(cookie)
-
-        self.ui.callStarted(cookie)
-        if self._rtpProtocolClass is None:
-            rtp = RTPProtocol(self, cookie)
-        else:
-            rtp = self._rtpProtocolClass(self, cookie)
-        self._rtp[cookie] = rtp
-        d = rtp.createRTPSocket(localIP,withSTUN)
+    def _createRTP(self, localIP, withSTUN):
+        self.ui.callStarted()
+
+        self._rtp = RTPProtocol(self)
+
+        rtpext.RTPExt(self._rtp)
+
+        d = self._rtp.createRTPSocket(localIP,withSTUN)
         return d
 
-    def selectDefaultFormat(self, callcookie, sdp, format=None):
-        oldmediahandler = (self._audio and self._audio.codecker 
-                                       and self._audio.codecker.handler)
-        self._audio.close()
-        if not sdp:
-            self._audio.selectDefaultFormat([format,])
-            return
+    def getSDP(self):
+        sdp = SDP()
+
+        self._rtp.set_sdp_addr(sdp)
+
         md = sdp.getMediaDescription('audio')
-        rtpmap = md.rtpmap
-        ptlist = [ x[1] for x in  rtpmap.values() ]
-        self._audio.selectDefaultFormat(ptlist)
-        if oldmediahandler:
-            self._audio.reopen(oldmediahandler)
-
-    def getSDP(self, callcookie, othersdp=None):
-        rtp = self._rtp[callcookie]
-        sdp = rtp.getSDP(othersdp)
+        for pt in KnownCodecs:
+            md.addRtpMap(pt)
+
         return sdp
 
-    def startCall(self, callcookie, remoteSDP, cb):
-        log.msg("startCall reopening %r %r"%(self._currentCall, self._audio))
+    def startCall(self, remoteSDP):
         md = remoteSDP.getMediaDescription('audio')
+        precondition(md.rtpmap is not None)
         ipaddr = md.ipaddr or remoteSDP.ipaddr
         remoteAddr = (ipaddr, md.port)
-        log.msg("call Start %r %r"%(callcookie, remoteAddr))
-        self._currentCall = callcookie
-        self._rtp[callcookie].start(remoteAddr)
-        mediahandler = lambda x,c=callcookie: self.outgoingRTP(c, x)
-        self.openAudioDevice([PT_PCMU,], mediahandler)
-        log.msg("startCall opened %r %r"%(self._currentCall, self._audio))
-        cb(callcookie)
-
-    def outgoingRTP(self, cookie, sample):
-        # XXX should the mute/nonmute be in the audio layer?
-        if not self._muted:
-            self._rtp[cookie].handle_media_sample(sample)
-
-    def endCall(self, callcookie, reason=''):
-        rtp = self._rtp.get(callcookie)
-        log.msg("endCall clearing %r"%(callcookie))
-        self._currentCall = None
-        if rtp:
-            rtp = self._rtp[callcookie]
-            rtp.stopSendingAndReceiving()
-            del self._rtp[callcookie]
-            if self._calls.get(callcookie):
-                del self._calls[callcookie]
-            self.closeAudioDevice()
-        self.ui.callDisconnected(callcookie, reason)
-
-    def openAudioDevice(self, fmts=[PT_PCMU,], mediahandler=None):
-        assert isinstance(fmts, (list, tuple,)), fmts
-        assert self._audio
-        self._audio.close()
-        self._audio.selectDefaultFormat(fmts)
-        self._audio.reopen(mediahandler)
-
-    def closeAudioDevice(self):
-        self._audio.close()
-
-    def incomingRTP(self, callcookie, packet):
-        from shtoom.rtp.formats import PT_NTE
-        if packet.header.ct == PT_NTE:
-            return None
-        if self._currentCall != callcookie:
-            return None
-        try:
-            self._audio.write(packet)
-        except IOError:
-            pass
+        log.msg("startCall %r to %r" % (self._rtp, remoteAddr,))
+        self.rtpmap = md.rtpmap
+        self._audioplaydev.open(self.rtpmap)
+
+        markers = {}
+        for (pt, (text, marker,)) in md.rtpmap.items():
+            markers[marker] = pt
+
+        self.enccodec = None
+        self.enccodecpt = None
+        for preferredcodec in KnownVoCodecs:
+            if markers.has_key(preferredcodec):
+                self.enccodec = preferredcodec
+                self.enccodecpt = markers[self.enccodec]
+                break
+        
+        if not self.enccodec:
+            raise ValueError, "We do not know how to produce any codec that the recipient can consume.  The final SDP codec set was %s.  Our KnownVoCodecs is %s" % (md.rtpmap, KnownVoCodecs,)
+
+        self._audiomicdev.open(self.enccodec, self.enccodecpt, self._rtp)
+        self._rtp.start(remoteAddr)
+
+        self.statusMessage("Call Connected")
+        log.msg("startCall()")
+
+    def endCall(self, reason=''):
+        log.msg("endCall clearing")
+        if self._rtp:
+            self.call.dropCall()
+            self.call = None
+            self._rtp.stopSendingAndReceiving()
+            self._rtp = None
+            self._audioplaydev.close()
+            self._audiomicdev.close()
+        self.statusMessage("Call disconnected")
+        self.ui.callDisconnected(reason)
+
+    def incomingRTP(self, packet):
+        if not self._muteplayout:
+            self._audioplaydev.write(packet)
 
     def placeCall(self, sipURL):
         return self.sip.placeCall(sipURL)
 
-    def dropCall(self, cookie):
-        call = self._calls.get(cookie)
-        if call:
-            d = call.dropCall()
-        # xxx Add callback.
-        #else:
-        #    self.ui.callDisconnected(None, "no call")
-
-    def startDTMF(self, cookie, digit):
-        rtp = self._rtp[cookie]
-        rtp.startDTMF(digit)
-
-    def stopDTMF(self, cookie, digit):
-        rtp = self._rtp[cookie]
-        rtp.stopDTMF(digit)
-
     def statusMessage(self, message):
         self.ui.statusMessage(message)
 
@@ -290,19 +272,38 @@
         else:
             return defer.fail(CallFailed("No auth available"))
 
-    def muteCall(self, callcookie):
-        if self._currentCall is not callcookie:
-            raise ValueError("call %s is current call, not %s"%(
-                                    self._currentCall, callcookie))
-        else:
-            self._muted = True
+    def mutePlayout(self):
+        self._muteplayout = True
+
+    def unmutePlayout(self, dummy=None):
+        self._muteplayout = False
 
-    def unmuteCall(self, callcookie):
-        if self._currentCall is not callcookie:
-            raise ValueError("call %s is current call, not %s"%(
-                                    self._currentCall, callcookie))
+class WaveFileOutPlayer:
+    """
+    Mute playout of voice packets, play out wav file, then unmute voice packets.
+    """
+    def __init__(self, fname, device, app):
+        self.fname = fname
+        self.device = device
+        self.app = app
+
+        self.app.mutePlayout()
+        self.wavefileobj = aufile.WavReader(self.fname)
+
+        self.device.open(self._refill)
+    
+    def _refill(self):
+        d = self.wavefileobj.read()
+        if d:
+            self.device.write(d)
         else:
-            self._muted = False
+            self.device.close()
+            self._audio_file_done()
+
+    def stop(self):
+        self._audio_file_done()
+     
+    def _audio_file_done(self):
+        self.app.unmutePlayout()
+        self.wavefileobj = None
 
-    def switchCallAudio(self, callcookie):
-        self._currentCall = callcookie
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/audio/__init__.py new-newfrom1406/trunk/shtoom/shtoom/audio/__init__.py
--- old-newfrom1406/trunk/shtoom/shtoom/audio/__init__.py	2005-09-27 23:36:20.376378032 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/audio/__init__.py	2005-09-27 23:36:20.530354624 -0300
@@ -2,21 +2,22 @@
 """
 
 from twisted.python import log
-from shtoom.audio.converters import MediaLayer
 
+# from the pyutil library
+from pyutil.assertutil import _assert, precondition, postcondition
 
+from shtoom.avail import audio as av_audio
+from shtoom.exceptions import NoAudioDevice
+
+def findAudioInterface(audioinfile=None, audiooutfile=None, audiopref=None):
+    precondition((audioinfile is None and audiooutfile is None) or audiopref == 'file', "You are required to pass an audioinfile or audiooutfile argument *only* if audiopref is 'file'.", audioinfile, audiooutfile, audiopref)
 
-def findAudioInterface():
-    # Ugh. Circular import hell
-    from shtoom.avail import audio as av_audio
     audioOptions = { 'oss': av_audio.ossaudio,
                      'alsa': av_audio.alsaaudio,
                      'fast': av_audio.fastaudio,
                      'port': av_audio.fastaudio,
                      'osx': av_audio.osxaudio,
                      'core': av_audio.osxaudio,
-                     'file': av_audio.fileaudio,
-                     'echo': av_audio.echoaudio,
                    }
     allAudioOptions = [
                         av_audio.alsaaudio,
@@ -25,40 +26,33 @@
                         av_audio.osxaudio
                       ]
 
-    audioPref = attempts = None
+    if audioinfile or audiooutfile:
+        audioPref = 'file'
 
-    try:
-        from __main__ import app
-    except:
-        app = None
-
-    if app:
-        audioPref = app.getPref('audio')
-
-    print "audioPref is", audioPref
-    if audioPref:
-        audioint = audioOptions.get(audioPref)
-        if not audioint:
-            log.msg("requested audio interface %s unavailable"%(audioPref,))
-        else:
+    if audiopref:
+        audioint = audioOptions.get(audiopref)
+        if audioint:
             return audioint
+        log.msg("Requested audio interface %s not available.  Trying other interfaces." % (audiopref,))
 
     for audioint in allAudioOptions:
         if audioint:
             return audioint
 
-_device = None
-
-def getAudioDevice(_testAudioInt=None):
-    from shtoom.exceptions import NoAudioDevice
-    global _device
-    if _testAudioInt is not None:
-        return MediaLayer(_testAudioInt.Device())
-
-    if _device is None:
-        audioint = findAudioInterface()
-        if audioint is None:
-            raise NoAudioDevice("no working audio interface found")
-        dev = audioint.Device()
-        _device = MediaLayer(dev)
-    return _device
+def getAudioPlayDevice(_testAudioWrite=None):
+    if _testAudioWrite is not None:
+        return _testAudioWrite
+
+    audioint = findAudioInterface()
+    if audioint is None:
+        raise NoAudioDevice("no working audio interface found")
+    return audioint.PlayDevice()
+
+def getAudioMicDevice(_testAudioRead=None):
+    if _testAudioRead is not None:
+        return _testAudioRead 
+
+    audioint = findAudioInterface()
+    if audioint is None:
+        raise NoAudioDevice("no working audio interface found")
+    return audioint.MicDevice()
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/audio/alsa.py new-newfrom1406/trunk/shtoom/shtoom/audio/alsa.py
--- old-newfrom1406/trunk/shtoom/shtoom/audio/alsa.py	2005-09-27 23:36:20.368379248 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/audio/alsa.py	2005-09-27 23:36:20.529354776 -0300
@@ -1,81 +1,106 @@
 # Copyright (C) 2004 Anthony Baxter
 
-# from pyalsa
+# from pyalsaaudio
 import alsaaudio
 
-# from Shtoom
-import baseaudio
-
 # from Twisted
 from twisted.python import log
+from twisted.internet import reactor
 from twisted.internet.task import LoopingCall
 
-import time
-import audioop
-
-opened = None
-DEFAULT_ALSA_DEVICE = 'default'
-
-class ALSAAudioDevice(baseaudio.AudioDevice):
-
-    def openDev(self):
-        try:
-            from __main__ import app
-        except:
-            app = None
-        if app is None:
-            device = DEFAULT_ALSA_DEVICE
-        else:
-            device = app.getPref('audio_device', DEFAULT_ALSA_DEVICE)
-        log.msg("alsaaudiodev opening device %s" % (device))
-        writedev = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK,
-                                 alsaaudio.PCM_NONBLOCK, device)
-        self.writechannels = writedev.setchannels(1)
-        writedev.setrate(8000)
-        writedev.setformat(alsaaudio.PCM_FORMAT_S16_LE)
-        writedev.setperiodsize(160)
-        self.writedev = writedev
-
-        readdev = alsaaudio.PCM(alsaaudio.PCM_CAPTURE,
-                                alsaaudio.PCM_NONBLOCK, device)
-        self.readchannels = readdev.setchannels(1)
-        readdev.setrate(8000)
-        readdev.setformat(alsaaudio.PCM_FORMAT_S16_LE)
-        readdev.setperiodsize(160)
-        self.readdev = readdev
+PERIODSIZE=160
+RATE=8000
+SECS_PER_PERIOD=float(PERIODSIZE) / RATE
+BYTES_PER_SECOND=RATE * 2
+
+class MicDevice:
+    def __init__(self):
+        self.readdev = None
+        self.micdatahandler = None
+        self.LC = None
+
+    def __repr__(self):
+        return "alsa.MicDevice"
+
+    def open(self, micdatahandler):
+        self.close()
+        log.msg("%s opening" % (self,))
+        self.micdatahandler = micdatahandler
+
+        self.readdev = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NONBLOCK)
+        self.readdev.setchannels(1)
+        self.readdev.setrate(RATE)
+        self.readdev.setperiodsize(PERIODSIZE)
 
         self.LC = LoopingCall(self._push_up_some_data)
-        self.LC.start(0.010)
+        self.LC.start(SECS_PER_PERIOD)
 
     def _push_up_some_data(self):
+        assert self.readdev, "It is required that _push_up_some_data() be called only when the read device is open for reading."
         (l, data,) = self.readdev.read()
-        if self.readchannels == 2:
-            data = audioop.tomono(data, 2, 1, 1)
-        if self.encoder and data:
-            self.encoder.handle_audio(data)
+        while data:
+            self.micdatahandler.handle_data(data)
+            (l, data,) = self.readdev.read()
+
+    def isOpen(self):
+        return self.readdev is not None
+
+    def close(self):
+        if self.isOpen():
+            log.msg("alsaaudiodev closing")
+            self.LC.stop()
+            self.LC = None
+            self.readdev = None
+
+class PlayDevice:
+    def __init__(self):
+        self.writedev = None
+        self.refillcb = None # refill callback gets called when playout chunk is half-finished
+        self.refilldc = None # refill delayed call is the object that will invoke refillcb after a delay
+
+    def __repr__(self):
+        return "alsa.PlayDevice"
+
+    def open(self, refillcb=None):
+        """
+        If not None, then refillcb will be called anytime the playout buffer
+        becomes at least half empty.
+        """
+        self.close()
+        log.msg("%s opening" % (self,))
+
+        if self.refilldc:
+            self.refilldc.cancel()
+            self.refilldc = None
+        self.refillcb = refillcb
+        self.writedev = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, alsaaudio.PCM_NONBLOCK)
+        self.writedev.setchannels(1)
+        self.writedev.setrate(RATE)
+        self.writedev.setperiodsize(PERIODSIZE)
+
+        if self.refillcb:
+            self.refillcb()
+
+    def _give_refill_cb(self):
+        assert self.refillcb
+        self.refilldc = None
+        self.refillcb()
 
     def write(self, data):
-        if not hasattr(self, 'LC'):
-            return
-        assert self.isOpen(), "calling write() on closed %s"%(self,)
-        if self.writechannels == 2:
-            data = audioop.tostereo(data, 2, 1, 1)
         wrote = self.writedev.write(data)
-        if not wrote: log.msg("ALSA overrun")
+        if wrote != len(data): log.msg("ALSA overrun -- tried to write %d bytes but wrote %d bytes" % (len(data), wrote,))
+        if self.refillcb:
+            refilldelay = (float(wrote) / BYTES_PER_SECOND) / 2 # how many seconds until half of this data is finished playing out
+            if self.refilldc:
+                self.refilldc.reset(refilldelay)
+            else:
+                self.refilldc = reactor.callLater(refilldelay, self._give_refill_cb)
 
     def isOpen(self):
-        return hasattr(self, 'writedev')
+        return self.writedev is not None
 
     def close(self):
         if self.isOpen():
-            log.msg("alsaaudiodev closing")
-            try:
-                self.LC.stop()
-            except AttributeError:
-                # ? bug in Twisted?  Not sure.  This catch-and-ignore is a temporary workaround.  --Zooko
-                pass
-            del self.LC
-            del self.writedev
-            del self.readdev
+            log.msg("%s closing" % (self,))
+            self.writedev = None
 
-Device = ALSAAudioDevice
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/audio/baseaudio.py new-newfrom1406/trunk/shtoom/shtoom/audio/baseaudio.py
--- old-newfrom1406/trunk/shtoom/shtoom/audio/baseaudio.py	2005-09-27 23:36:20.356381072 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/audio/baseaudio.py	2005-09-27 23:36:20.529354776 -0300
@@ -1,19 +1,15 @@
 # Copyright (C) 2004 Anthony Baxter
 
 class AudioDevice(object):
-    encoder = None
-
     def __init__(self, mode='ignored'):
-        self.openDev()
         self._closed = False
+        self.sink = None
 
-    def set_encoder(self, encoder):
+    def set_sink(self, sink):
         """
-        The encoder object will subsequently receive calls to its 
-        handle_audio() method when audio is available - it passes it on 
-        to the rest of the system (eventually, to the network).
+        The sink object will subsequently receive calls to its handle_data() method.
         """
-        self.encoder = encoder
+        self.sink = sink
 
     def close(self):
         if not self._closed:
@@ -22,6 +18,7 @@
 
     def reopen(self):
         self.close()
+        print "baseaudio: reopen"
         self.openDev()
         self._closed = False
 
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/audio/converters.py new-newfrom1406/trunk/shtoom/shtoom/audio/converters.py
--- old-newfrom1406/trunk/shtoom/shtoom/audio/converters.py	2005-09-27 23:36:20.353381528 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/audio/converters.py	2005-09-27 23:36:20.529354776 -0300
@@ -1,62 +1,28 @@
-# Copyright (C) 2004 Anthony Baxter
-from shtoom.rtp.formats import PT_PCMU, PT_GSM, PT_SPEEX, PT_DVI4, PT_RAW
-from shtoom.rtp.formats import PT_PCMA, PT_ILBC
-from shtoom.rtp.formats import PT_CN, PT_xCN, AudioPTMarker
+# Copyright (C) 2004,2005 Anthony Baxter
+
+# from Shtoom
+from shtoom.rtp.formats import PT_PCMU, PT_GSM, PT_SPEEX, PT_RAW
+from shtoom.rtp.formats import PT_PCMA
+from shtoom.rtp.formats import PT_CN, PT_xCN, PT_NTE
 from shtoom.avail import codecs
-from shtoom.audio import aufile, playout
+
 from shtoom.lwc import Interface, implements
 
+# from the Twisted library
+from twisted.internet import defer, reactor
 from twisted.python import log
 
+# from the Python Standard Library
 import sets, struct
 
+# from the pyutil Library
+from pyutil.assertutil import _assert, precondition, postcondition
+
 try:
     import audioop
 except ImportError:
     audioop = None
 
-class NullEncoder:
-    def handle_audio(self, data):
-        pass
-
-nullencoder = NullEncoder()
-
-class MediaSample:
-    def __init__(self, ct, data):
-        self.ct = ct
-        self.data = data
-
-    def __repr__(self):
-        return "<%s/%s, %s>" % (self.__class__.__name__, self.ct, `self.data`,)
-
-class NullConv:
-    # Should be refactored away
-    def __init__(self, device):
-        self._d = device
-    def getDevice(self):
-        return self._d
-    def setDevice(self, d):
-        self._d = d
-    def getFormats(self):
-        if self._d:
-            return self._d.getFormats()
-    def write(self, data):
-        if self._d:
-            return self._d.write(data)
-    def close(self):
-        if self._d:
-            log.msg("audio device %r close"%(self._d,), system="audio")
-            return self._d.close()
-    def reopen(self):
-        if self._d:
-            log.msg("audio device %r reopen ..."%(self._d,), system="audio")
-            return self._d.reopen()
-    def isOpen(self):
-        if self._d:
-            return self._d.isOpen()
-    def __repr__(self):
-        return '<%s wrapped around %r>'%(self.__class__.__name__, self._d)
-
 def isLittleEndian():
     import struct
     p = struct.pack('H', 1)
@@ -67,41 +33,42 @@
     else:
         raise ValueError("insane endian-check result %r"%(p))
 
-class IAudioCodec:
-    def buffer_and_encode(self, bytes):
-        "encode bytes, a string of audio"
-    def decode(self, bytes):
-        "decode bytes, a string of audio"
-
-class _Codec:
-    "Base class for codecs"
-    implements(IAudioCodec)
-    def __init__(self, samplesize):
+class Encoder:
+    "Base class for encoders"
+    def __init__(self, pt, handler, samplesize):
+        """ configure it to buffer up samplesize bytes of audio data before
+        calling _encode() to generate a new media frame.  handler's
+        handle_media_sample() will get called whenever there is a new encoded
+        media sample to be had """
+        self.pt = pt
+        self.handler = handler
         self.samplesize = samplesize
         self.b = ''
 
-    def buffer_and_encode(self, bytes):
+    def handle_data(self, bytes):
         self.b += bytes
-        res = []
         while len(self.b) >= self.samplesize:
             sample, self.b = self.b[:self.samplesize], self.b[self.samplesize:]
-            res.append(self._encode(sample))
-        return res
+            self.handler.handle_media_sample(self.pt, self._encode(sample))
 
-class GSMCodec(_Codec):
-    def __init__(self):
-        _Codec.__init__(self, 320)
+class GSMEncoder(Encoder):
+    def __init__(self, pt, handler):
+        Encoder.__init__(self, pt, handler, 320)
         if isLittleEndian():
             self.enc = codecs.gsm.gsm(codecs.gsm.LITTLE)
-            self.dec = codecs.gsm.gsm(codecs.gsm.LITTLE)
         else:
             self.enc = codecs.gsm.gsm(codecs.gsm.BIG)
-            self.dec = codecs.gsm.gsm(codecs.gsm.BIG)
 
     def _encode(self, bytes):
-        assert isinstance(bytes, str), bytes
         return self.enc.encode(bytes)
 
+class GSMDecoder:
+    def __init__(self):
+        if isLittleEndian():
+            self.dec = codecs.gsm.gsm(codecs.gsm.LITTLE)
+        else:
+            self.dec = codecs.gsm.gsm(codecs.gsm.BIG)
+
     def decode(self, bytes):
         assert isinstance(bytes, str), bytes
         if len(bytes) != 33:
@@ -110,18 +77,19 @@
             return None
         return self.dec.decode(bytes)
 
-class SpeexCodec(_Codec):
-    "A codec for Speex"
-
-    def __init__(self):
+class SpeexEncoder(Encoder):
+    def __init__(self, pt, handler):
         self.enc = codecs.speex.new(8)
-        self.dec = codecs.speex.new(8)
-        _Codec.__init__(self, 320)
+        Encoder.__init__(self, pt, handler, 320)
 
     def _encode(self, bytes, unpack=struct.unpack):
         frames = list(unpack('160h', bytes))
         return self.enc.encode(frames)
 
+class SpeexDecoder:
+    def __init__(self):
+        self.dec = codecs.speex.new(8)
+
     def decode(self, bytes):
         if len(bytes) != 40:
             log.msg("speex: short read on decode %d != 40"%len(bytes),
@@ -131,232 +99,112 @@
         ostr = struct.pack('160h', *frames)
         return ostr
 
-class MulawCodec(_Codec):
-    "A codec for mulaw encoded audio (e.g. G.711U)"
-
-    def __init__(self):
-        _Codec.__init__(self, 320)
+class MulawEncoder(Encoder):
+    "An encoder  for mulaw encoded audio (e.g. G.711U)"
+    def __init__(self, pt, handler):
+        Encoder.__init__(self, pt, handler, 320)
 
     def _encode(self, bytes):
         return audioop.lin2ulaw(bytes, 2)
 
+class MulawDecoder:
+    "A decoder for mulaw encoded audio (e.g. G.711U)"
     def decode(self, bytes):
         if len(bytes) != 160:
-            log.msg("mulaw: short read on decode, %d != 160"%len(bytes),
+            log.msg("mulaw: short read on decode, %d != 160"%len(bytes), 
                                                             system="codec")
         return audioop.ulaw2lin(bytes, 2)
 
-class AlawCodec(_Codec):
-    "A codec for alaw encoded audio (e.g. G.711A)"
-
-    def __init__(self):
-        _Codec.__init__(self, 320)
+class AlawEncoder(Encoder):
+    "An encoder for alaw encoded audio (e.g. G.711A)"
+    def __init__(self, pt, handler):
+        Encoder.__init__(self, pt, handler, 320)
 
     def _encode(self, bytes):
         return audioop.lin2alaw(bytes, 2)
 
+class AlawDecoder:
+    "A decoder for alaw encoded audio (e.g. G.711A)"
     def decode(self, bytes):
         if len(bytes) != 160:
-            log.msg("alaw: short read on decode, %d != 160"%len(bytes),
+            log.msg("alaw: short read on decode, %d != 160"%len(bytes), 
                                                             system="codec")
         return audioop.alaw2lin(bytes, 2)
 
-class NullCodec(_Codec):
-    "A codec that consumes/emits nothing (e.g. for confort noise)"
-
-    def __init__(self):
-        _Codec.__init__(self, 1)
-
-    def _encode(self, bytes):
-        return None
-
+class NullDecoder:
+    "A decoder that emits nothing (e.g. for confort noise)"
     def decode(self, bytes):
         return None
 
-class PassthruCodec(_Codec):
-    "A codec that leaves it's input alone"
-    def __init__(self):
-        _Codec.__init__(self, None)
-    decode = lambda self, bytes: bytes
-    buffer_and_encode = lambda self, bytes: [bytes]
-
-def make_codec_set():
-    format_to_codec = {}
-    format_to_codec[PT_CN] = NullCodec()
-    format_to_codec[PT_xCN] = NullCodec()
-    format_to_codec[PT_RAW] = PassthruCodec()
-    assert codecs.mulaw
-    if codecs.mulaw is not None:
-        format_to_codec[PT_PCMU] = MulawCodec()
-    if codecs.alaw is not None:
-        format_to_codec[PT_PCMA] = AlawCodec()
-    if codecs.gsm is not None:
-        format_to_codec[PT_GSM] = GSMCodec()
-    if codecs.speex is not None:
-        format_to_codec[PT_SPEEX] = SpeexCodec()
-    #if codecs.dvi4 is not None:
-    #    format_to_codec[PT_DVI4] = DVI4Codec()
-    #if codecs.ilbc is not None:
-    #    format_to_codec[PT_ILBC] = ILBCCodec()
-    return format_to_codec
-
-known_formats = (sets.ImmutableSet(make_codec_set().keys()) - 
-                                  sets.ImmutableSet([PT_CN, PT_xCN,]))
-
-class Codecker:
-    def __init__(self, format):
-        self.format_to_codec = make_codec_set()
-        if not format in known_formats:
-            raise ValueError("Can't handle codec %r"%format)
-        self.format = format
-        self.handler = None
+def make_encoder(format, pt, mediahandler):
+    if format is PT_PCMU:
+        return MulawEncoder(pt, mediahandler)
+    elif format is PT_PCMA:
+        return AlawEncoder(pt, mediahandler)
+    elif format is PT_GSM:
+        return GSMEncoder(pt, mediahandler)
+    elif format is PT_SPEEX:
+        return SpeexEncoder(pt, mediahandler)
+
+NULL_FORMATS = [PT_CN, PT_xCN, PT_NTE,]
+
+def make_decoder(format):
+    if format in NULL_FORMATS:
+        return NullDecoder()
+    elif format is PT_PCMU:
+        return MulawDecoder()
+    elif format is PT_PCMA:
+        return AlawDecoder()
+    elif format is PT_GSM:
+        return GSMDecoder()
+    elif format is PT_SPEEX:
+        return SpeexDecoder()
+
+class MicDev:
+    """ The MicDev sits between the network and the raw audio input device. It
+        converts the audio to the codec on the network from the format used by
+        the lower-level audio device (16 bit signed ints at 8KHz).
+    """
+    def __init__(self, readdevice):
+        """
+        rtpmap is a dict with key PT byte and value a tuple of (text, PT_Marker instance,)
+        """
+        self.readdevice = readdevice
 
-    def set_handler(self, handler):
+    def open(self, mediahandler, format=None, pt=None):
         """
-        handler will subsequently receive calls to handle_media_sample().
+        mediahandler will subsequently receive calls to handle_media_sample().
         """
-        self.handler = handler
+        precondition((format is None) == (pt is None) == (mediahandler is None), "You are required to pass a format, a PT number, and a mediahandler or else none of those three things.", format, pt, mediahandler)
+        encoder = make_encoder(format, pt, mediahandler)
+        self.readdevice.open(encoder)
 
-    def getDefaultFormat(self):
-        return self.format
+    def close(self):
+        self.readdevice.close()
 
-    def handle_audio(self, bytes):
-        "Accept audio as bytes, emits MediaSamples."
-        if not bytes:
-            return None
-        codec = self.format_to_codec.get(self.format)
-        if not codec:
-            raise ValueError("can't encode format %r"%self.format)
-        encaudios = codec.buffer_and_encode(bytes)
-        for encaudio in encaudios:
-            samp = MediaSample(self.format, encaudio)
-            if self.handler is not None:
-                self.handler(samp)
-            else:
-                return samp
-
-    def decode(self, packet):
-        "Accepts an RTPPacket, emits audio as bytes"
-        if not packet.data:
-            return None
-        codec = self.format_to_codec.get(packet.header.ct)
-        if not codec:
-            raise ValueError("can't decode format %r"%packet.header.ct)
-        encaudio = codec.decode(packet.data)
-        return encaudio
-
-class MediaLayer(NullConv):
-    """ The MediaLayer sits between the network and the raw
-        audio device. It converts the audio to/from the codec on
-        the network to the format used by the lower-level audio
-        devices (16 bit signed ints at an integer multiple of 8KHz).
+class PlayDev:
+    """ The PlayDev sits between the network and the raw audio output device. It
+        converts the audio from the codec on the network to the format used by
+        the lower-level audio device (16 bit signed ints at 8KHz).
     """
-    def __init__(self, device, *args, **kwargs):
-        self.playout = None
-        self.codecker = None
-        self.defaultFormat = None
-        # this sets self._d = device
-        NullConv.__init__(self, device, *args, **kwargs) 
+    def __init__(self, writedev):
+        self.writedev = writedev
+        # dict k: pt bytes, v: decoder object
+        self.decoders = None
+
+    def open(self, rtpmap):
+        """
+        rtpmap is a dict with key PT byte and value a tuple of (text, PT_Marker instance,)
+        """
+        self.decoders = {}
+        for (pt, (text, marker,),) in rtpmap.items():
+            self.decoders[pt] = make_decoder(marker)
+        self.writedev.open()
 
-    def getFormat(self):
-        return self.defaultFormat
+    def close(self):
+        self.decoders = None
+        self.writedev.close()
 
     def write(self, packet):
-        if self.playout is None:
-            log.msg("write before reopen, discarding")
-            return 0
-        audio = self.codecker.decode(packet)
-        if audio:
-            return self.playout.write(audio, packet.header.seq)
-        else:
-            self.playout.write('', packet.header.seq)
-            return 0
+        self.writedev.write(self.decoders[packet.header.pt].decode(packet.data))
 
-    def selectDefaultFormat(self, fmts=[PT_PCMU,]):
-        assert isinstance(fmts, (list, tuple,)), fmts
-        assert not self._d or not self._d.isOpen(), \
-            "close device %r before calling selectDefaultFormat()" % (self._d,)
-
-        for f in fmts:
-            if f in known_formats:
-                self.defaultFormat = f
-                break
-        else:
-            raise ValueError("No working formats!")
-
-    def reopen(self, mediahandler=None):
-        """
-        mediahandler, if not None, is a callable that will be called with
-        a media sample is available.
-
-        This flushes codec buffers.  The audio playout buffers and microphone 
-        readin buffers *ought* to be flushed by the lower-layer audio device 
-        when we call reopen() on it.
-        """
-        assert self.defaultFormat, "must call selectDefaultFormat()"+\
-                                   "before (re-)opening the device."
-
-        self.codecker = Codecker(self.defaultFormat)
-        self._d.reopen()
-        if mediahandler:
-            self.codecker.set_handler(mediahandler)
-            self._d.set_encoder(self.codecker)
-        else:
-            self._d.set_encoder(nullencoder)
-
-        if self.playout:
-            log.msg("playout already started")
-        else:
-            self.playout = playout.Playout(self)
-
-    def play_wave_file(self, fname):
-        """
-            Note that calling write() in a while loop like this
-            effectively blocks the whole Twisted app until we finish
-            spooling the whole audio file into the output audio buffer.
-            Hopefully this is good enough for now. In the future it would
-            be nice if we could just initiate a playout here, the way Doug
-            does. That would also allow you to answer the phone before the
-            ring file finishes playing... --Zooko 2004-10-21 
-            P.S. Oh, and if your output device is ALSA, this will
-            immediately overflow the output FIFO, so only the first few
-            milliseconds of the wav file will be heard. Whoops. This
-            really needs to be fixed... --Zooko 2005-02-25
-        """
-        if not self._d.isOpen():
-            self.selectDefaultFormat([PT_PCMU,])
-            self.reopen()
-        try:
-            wavefileobj = aufile.WavReader(fname)
-            data = wavefileobj.read()
-            # Note that calling write() in a while loop like this effectively 
-            # blocks the whole Twisted app until the audio file is all buffered 
-            # up by the underlying audio output device.  Hopefully this is good 
-            # enough for now.  In the future it would be nice if we could just 
-            # initiate a playout here, the way doug does.  That would also 
-            # allow you to answer the phone before the ring file finishes 
-            # playing...  --Zooko 2004-10-21
-            while data:
-                self._d.write(data)
-                data = wavefileobj.read()
-        except (IOError, ValueError, EOFError,), le:
-            print "warning: wave file error: %r, %s, %s" % (le, le, le.args,)
-            pass
-
-    def close(self):
-        self.playout = None
-        self.codecker = None
-        self._d.set_encoder(nullencoder)
-        NullConv.close(self)
-
-class DougConverter(MediaLayer):
-    "Specialised converter for Doug."
-    # XXX should be refactored away to just use a Codecker directly
-    def __init__(self, defaultFormat=PT_PCMU, *args, **kwargs):
-        self.codecker = Codecker(defaultFormat)
-        self.convertInbound = self.codecker.decode
-        self.convertOutbound = self.codecker.handle_audio
-        self.set_handler = self.codecker.set_handler
-        if not kwargs.get('device'):
-            kwargs['device'] = None
-        NullConv.__init__(self, *args, **kwargs)
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/audio/interfaces.py new-newfrom1406/trunk/shtoom/shtoom/audio/interfaces.py
--- old-newfrom1406/trunk/shtoom/shtoom/audio/interfaces.py	2005-09-27 23:36:20.297390040 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/audio/interfaces.py	2005-09-27 23:36:20.529354776 -0300
@@ -1,6 +1,5 @@
 from twisted.python.components import Interface
 
-# XXX TODO update!
 
 class IAudio(Interface):
     '''Lowlevel interface to audio source/sink.'''
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/audio/playout.py new-newfrom1406/trunk/shtoom/shtoom/audio/playout.py
--- old-newfrom1406/trunk/shtoom/shtoom/audio/playout.py	2005-09-27 23:36:20.296390192 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/audio/playout.py	2005-09-27 23:36:20.528354928 -0300
@@ -64,7 +64,7 @@
         i2 += 1
     return True
 
-class Playout:
+class SmartPlayout:
     """
     Theory of operation: you have two modes: "playout" mode and "refill" mode.
     When you are in playout mode then you play out sequential audio packets from
@@ -140,7 +140,7 @@
         while ((t() + PLAYOUT_BUFFER_SECONDS >= self.drytime) 
                             and self.b and self.b[0][0] == (self.s + 1)):
             (seq, bytes,) = self.b.pop(0)
-            self.medialayer._d.write(bytes)
+            self.medialayer._d.write(bytes, seq)
             self.s = seq
             packetlen = len(bytes) / float(16000)
             if self.drytime is None:
@@ -205,11 +205,21 @@
                 log.msg("xxxxxxx catchup! dropping %s" % seq)
                 self.s = self.b[0][0] - 1 # prime it for the next packet
 
-class NullPlayout:
+
+class DumbPlayout:
     def __init__(self, medialayer):
         self.medialayer = medialayer
+        self.device = d = medialayer._d
+        ## Cut down on one extra python method call overhead by
+        ## setting AudioMonitorDelegate.write to self.write.
+        ## AudioMonitorDelegate thus gets the sequence number
+        ## which it may then pass to C.
+        self.write = d.write
+
+    def close(self):
+        print "CLOSE CALLED."
+
+
+Playout = DumbPlayout
 
-    def write(self, bytes, seq, t=time.time):
-        self.medialayer._d.write(bytes)
 
-#Playout=NullPlayout
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/avail/audio.py new-newfrom1406/trunk/shtoom/shtoom/avail/audio.py
--- old-newfrom1406/trunk/shtoom/shtoom/avail/audio.py	2005-09-27 23:36:20.283392168 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/avail/audio.py	2005-09-27 23:36:20.530354624 -0300
@@ -41,8 +41,6 @@
 else:
     osxaudio = None
 
-from shtoom.audio import fileaudio
-
 try:
     import alsaaudio
 except ImportError:
@@ -52,8 +50,6 @@
 if alsaaudio is not None:
     from shtoom.audio import alsa as alsaaudio
 
-from shtoom.audio import fileaudio, echoaudio
-
 
 def listAudio():
     all = globals().copy()
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/rtp/formats.py new-newfrom1406/trunk/shtoom/shtoom/rtp/formats.py
--- old-newfrom1406/trunk/shtoom/shtoom/rtp/formats.py	2005-09-27 23:36:20.281392472 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/rtp/formats.py	2005-09-27 23:36:20.519356296 -0300
@@ -85,33 +85,23 @@
 PT_MP2T =       VideoPTMarker('MP2T', clock=90000, pt=33)
 PT_H263 =       VideoPTMarker('H263', clock=90000, pt=34)
 
-TryCodecs = OrderedDict()
-TryCodecs[PT_GSM] = codecs.gsm
-TryCodecs[PT_SPEEX] = codecs.speex
-TryCodecs[PT_DVI4] = codecs.dvi4
-TryCodecs[PT_ILBC] = codecs.ilbc
+# This list of known voice codecs is in descending order of preference.
+KnownVoCodecs = []
+if codecs.speex:
+    KnownVoCodecs.append(PT_SPEEX)
+if codecs.mulaw:
+    KnownVoCodecs.append(PT_PCMU)
+if codecs.gsm:
+    KnownVoCodecs.append(PT_GSM)
+if codecs.dvi4:
+    KnownVoCodecs.append(PT_DVI4)
+if codecs.ilbc:
+    KnownVoCodecs.append(PT_ILBC)
 
-class SDPGenerator:
-    "Responsible for generating SDP for the RTPProtocol"
-
-    def getSDP(self, rtp, extrartp=None):
-        from shtoom.sdp import SDP, MediaDescription
-        if extrartp:
-            raise ValueError("can't handle multiple RTP streams in a call yet")
-        s = SDP()
-        addr = rtp.getVisibleAddress()
-        s.setServerIP(addr[0])
-        md = MediaDescription() # defaults to type 'audio'
-        s.addMediaDescription(md)
-        md.setServerIP(addr[0])
-        md.setLocalPort(addr[1])
-        for pt, test in TryCodecs.items():
-            if test is not None:
-                md.addRtpMap(pt)
-        md.addRtpMap(PT_PCMU)
-        md.addRtpMap(PT_CN)
-        md.addRtpMap(PT_NTE)
-        return s
+KnownCodecs = []
+KnownCodecs.extend(KnownVoCodecs)
+KnownCodecs.append(PT_CN)
+KnownCodecs.append(PT_NTE)
 
 RTPDict = {}
 all = globals()
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/rtp/protocol.py new-newfrom1406/trunk/shtoom/shtoom/rtp/protocol.py
--- old-newfrom1406/trunk/shtoom/shtoom/rtp/protocol.py	2005-09-27 23:36:20.277393080 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/rtp/protocol.py	2005-09-27 23:36:20.519356296 -0300
@@ -10,9 +10,9 @@
 from twisted.internet.protocol import DatagramProtocol
 from twisted.python import log
 
-from shtoom.rtp.formats import SDPGenerator, PT_CN, PT_xCN, PT_NTE
+from shtoom.sdp import MediaDescription
+from shtoom.rtp.formats import PT_CN, PT_xCN, PT_NTE
 from shtoom.rtp.packets import RTPPacket, parse_rtppacket
-from shtoom.audio.converters import MediaSample
 
 TWO_TO_THE_16TH = 2<<16
 TWO_TO_THE_32ND = 2<<32
@@ -29,14 +29,13 @@
     _stunAttempts = 0
 
     _cbDone = None
+    LC = None
 
     Done = False
-    def __init__(self, app, cookie, *args, **kwargs):
+    def __init__(self, app, *args, **kwargs):
         self.app = app
-        self.cookie = cookie
-        self._pendingDTMF = []
         #DatagramProtocol.__init__(self, *args, **kwargs)
-        self.ptdict = {}
+        self.format = None # which PT_* format we encode into when sending voice in this call
         self.seq = self.genRandom(bits=16)
         self.ts = self.genInitTS()
         self.ssrc = self.genSSRC()
@@ -45,21 +44,18 @@
         # media sample handler instead of this RTP object as the media sample handler.
         self.sending = False 
 
-    def getSDP(self, othersdp=None):
-        sdp = SDPGenerator().getSDP(self)
-        if othersdp:
-            sdp.intersect(othersdp)
-        self.setSDP(sdp)
-        return sdp
-
-    def setSDP(self, sdp):
-        "This is the canonical SDP for the call"
-        self.app.selectDefaultFormat(self.cookie, sdp)
-        rtpmap = sdp.getMediaDescription('audio').rtpmap
-        self.ptdict = {}
-        for pt, (text, marker) in rtpmap.items():
-            self.ptdict[pt] = marker
-            self.ptdict[marker] = pt
+    def register_rtpext(self, rtpext):
+        self._rtpext = rtpext
+
+    def set_sdp_addr(self, sdp):
+        addr = self.getVisibleAddress()
+        sdp.setServerIP(addr[0])
+
+        md = MediaDescription() # defaults to type 'audio'
+        md.setServerIP(addr[0])
+        md.setLocalPort(addr[1])
+
+        sdp.addMediaDescription(md)
 
     def createRTPSocket(self, locIP, needSTUN=False):
         """ Start listening on UDP ports for RTP and RTCP.
@@ -118,7 +114,7 @@
             self._extIP = locIP
             d = self._socketCompleteDef
             del self._socketCompleteDef
-            d.callback(self.cookie)
+            d.callback(self)
         else:
             # If the NAT is doing port translation as well, we will just
             # have to try STUN and hope that the RTP/RTCP ports are on
@@ -218,7 +214,7 @@
                 self._stunAttempts = 0
                 d = self._socketCompleteDef
                 del self._socketCompleteDef
-                d.callback(self.cookie)
+                d.callback(self)
 
     def connectionRefused(self):
         log.err("RTP got a connection refused, ending call")
@@ -246,23 +242,13 @@
         try:
             self.transport.write(packet.netbytes(), self.dest)
         except Exception, le:
-            pass
+            print "We attempted to send a packet before start() was called.  This can happen because we are attempting to send a packet in response to an incoming packet.  The outgoing packet will be dropped, exactly as if it had been successfully sent and then disappeared into the void.  Our protocol is required to deal with this possibility anyway, since we use an unreliable transport.  As soon as the SIP ACK message arrives then start() will be called and after that attempts to send will work.  pt: %s, data: %s, xhdrtype: %s, xhdrdata: %s, le: %s" % (pt, `data`, xhdrtype, `xhdrdata`, le,)
 
     def _send_cn_packet(self):
-        assert hasattr(self, 'dest'), "_send_cn_packet called before start %r" % (self,)
+        assert hasattr(self, 'dest'), "It is required that start() is called before _send_cn_packet() is called.  This requirement has not been met.  self: %s :: %s" % (self, type(self),)
+        log.msg("sending CN(13) to seed firewall to %s:%d"%(self.dest[0], self.dest[1]), system='rtp')
         # PT 13 is CN.
-        if self.ptdict.has_key(PT_CN):
-            cnpt = PT_CN.pt
-        elif self.ptdict.has_key(PT_xCN):
-            cnpt = PT_xCN.pt
-        else:
-            # We need to send SOMETHING!?!
-            cnpt = 0
-
-        log.msg("sending CN(%s) to seed firewall to %s:%d"%(cnpt, 
-                                        self.dest[0], self.dest[1]), system='rtp')
-
-        self._send_packet(cnpt, chr(127))
+        self._send_packet(13, chr(127))
 
     def start(self, dest, fp=None):
         self.dest = dest
@@ -277,23 +263,14 @@
         self._send_cn_packet()
 
     def datagramReceived(self, datagram, addr, t=time):
-        packet = parse_rtppacket(datagram)
-
-        try:
-            packet.header.ct = self.ptdict[packet.header.pt]
-        except KeyError:
-            if packet.header.pt == 19:
-                # Argh nonstandardness suckage
-                packet.header.pt = 13
-                packet.header.ct = self.ptdict[packet.header.pt]
-            else:
-                # XXX This could overflow the log.  Ideally we would have a 
-                # "previous message repeated N times" feature...  --Zooko 2004-10-18
-                log.msg("received packet with unknown PT %s" % packet.header.pt)
-                return # drop the packet on the floor
+        if self._rtpext:
+            packet = self._rtpext.process_incoming(datagram)
+            if not packet:
+                return
+        else:
+            packet = parse_rtppacket(datagram)
 
-        packet.header.ct = self.ptdict[packet.header.pt]
-        self.app.incomingRTP(self.cookie, packet)
+        self.app.incomingRTP(packet)
 
     def genSSRC(self):
         # Python-ish hack at RFC1889, Appendix A.6
@@ -344,7 +321,7 @@
             hex = m.hexdigest()
         return int(hex[:bits//4],16)
 
-    def handle_media_sample(self, sample):
+    def handle_media_sample(self, pt, sample):
         if self.Done:
             if self._cbDone:
                 self._cbDone()
@@ -354,13 +331,13 @@
         # can use this as an excuse to adjust playout buffer.
         if not self.sending:
             if not hasattr(self, 'warnedaboutthis'):
-                log.msg(("%s.handle_media_sample() should only be called" +
-                         " only when it is in sending mode.") % (self,))
+                log.msg(("WARNING: This is required to be synchronized so " +
+		         "that %s.handle_media_sample() gets called only when it is in " +
+			 "sending mode.") % (self,))
                 self.warnedaboutthis = True
             return
-
-        pt = self.ptdict[sample.ct]
-        self._send_packet(pt, sample.data)
+       
+        self._send_packet(pt, sample)
         self.ts += 160
         # Wrapping
         if self.ts >= TWO_TO_THE_32ND:
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/sdp.py new-newfrom1406/trunk/shtoom/shtoom/sdp.py
--- old-newfrom1406/trunk/shtoom/shtoom/sdp.py	2005-09-27 23:36:20.244398096 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/sdp.py	2005-09-27 23:36:20.535353864 -0300
@@ -139,7 +139,7 @@
 class MediaDescription:
     "The MediaDescription encapsulates all of the SDP media descriptions"
     def __init__(self, text=None):
-        self.media = None
+        self.media = 'audio'
         self.nettype = 'IN'
         self.addrfamily = 'IP4'
         self.ipaddr = None
@@ -149,7 +149,6 @@
         self._d = {}
         self._a = {}
         self.rtpmap = OrderedDict()
-        self.media = 'audio'
         self.transport = 'RTP/AVP'
         self.keyManagement = None
         if text:
@@ -161,13 +160,11 @@
                 pt = int(pt)
                 if pt < 97:
                     try:
-                        PT = RTPDict[pt]
+                        self.addRtpMap(RTPDict[pt])
                     except KeyError:
                         # We don't know this one - hopefully there's an
                         # a=rtpmap entry for it.
                         continue
-                    self.addRtpMap(PT)
-                    # XXX the above line is unbound local variable error if not RTPDict.has_key(pt) --Zooko 2004-09-29
         self.formats = formats
 
     def setMedia(self, media):
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/sip.py new-newfrom1406/trunk/shtoom/shtoom/sip.py
--- old-newfrom1406/trunk/shtoom/shtoom/sip.py	2005-09-27 23:36:20.236399312 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/sip.py	2005-09-27 23:36:20.535353864 -0300
@@ -224,7 +224,7 @@
 class Call(object):
     '''State machine for a phone call (inbound or outbound).'''
 
-    cookie = uri = None
+    uri = None
     nonce_count = 1
     sip = None
     _invite = None
@@ -238,14 +238,9 @@
         self.dialog = Dialog()
         self.state = 'NEW'
         self._needSTUN = False
-        self.cancel_trigger = None
         if callid:
             self.setCallID(callid)
 
-    def callStart(self):
-        self.compDef = defer.Deferred()
-        return self.compDef
-
     def setupLocalSIP(self, uri=None, via=None):
         ''' Setup SIP stuff at this end. Call with either a t.p.sip.URL
             (for outbound calls) or a t.p.sip.Via object (for inbound calls).
@@ -270,7 +265,7 @@
 
     def abortCall(self):
         # Bail out.
-        self.state = 'ABORTED'
+        self.setState('ABORTED')
 
     def setCallID(self, callid=None):
         if callid:
@@ -323,12 +318,11 @@
                 peer = protocol.transport.getPeer()
                 remAddress = (peer.host,peer.port)
                 port.stopListening()
+                self.sip.app.notifyEvent('discoveredIP', locAddress[0])
                 log.msg("discovered local address %r, remote %r"%(locAddress,
                                                                   remAddress), system='sip')
                 _CACHED_LOCAL_IP = locAddress[0]
             else:
-                self.compDef.errback(ValueError("couldn't connect to %s"%(
-                                            host)))
                 self.abortCall()
                 return None
         pol = getPolicy()
@@ -362,6 +356,7 @@
         else:
             host,port = '',''
         log.msg("after NAT mapping,external address is %s:%s"%(host, port), system='sip')
+        self.sip.app.notifyEvent('discoveredStunnedIP', host, port)
         self._localIP = host
         self._localPort = port
         # XXX Check for multiple firings!
@@ -404,17 +399,18 @@
         if isinstance(response, CallFailed):
             return self.rejectedIncoming(response)
         else:
+            assert isinstance(response, basestring)
             return self.acceptedIncoming(response)
 
-    def acceptedIncoming(self, cookie):
-        log.msg("acceptIncoming setting cookie to %r"%(cookie), system='sip')
-        self.cookie = cookie
+    def acceptedIncoming(self, response):
+        log.msg("acceptIncoming %r"%(response), system='sip')
         lhost, lport = self.getLocalSIPAddress()
         username = self.sip.app.getPref('username')
         self.dialog.setContact(username, lhost, lport)
 
         othersdp = SDP(self._invite.body)
-        sdp = self.sip.app.getSDP(self.cookie, othersdp)
+        sdp = self.sip.app.getSDP()
+        sdp.intersect(othersdp)
         if not sdp.hasMediaDescriptions():
             self.sendResponse(message, 406)
             self.setState('ABORTED')
@@ -437,7 +433,6 @@
         self.compDef.errback(response)
 
     def failedIncoming(self, failure):
-        log.msg('failedIncoming because %r'%(failure,), system='sip')
         log.msg('exception: %r'%(failure.value.args,), system='sip')
         # XXX Can I produce a more specific error than 500?
         self.sendResponse(self._invite, 500)
@@ -457,8 +452,7 @@
                 print "call terminated on a", message.code
                 log.msg("call terminated on a %s"%message.code, system="sip")
                 d.errback(exc('%s: %s'%(message.code, message.phrase)))
-        self.sip.app.endCall(self.cookie,
-                             'other end sent\n%s'%message.toString())
+        self.sip.app.endCall('other end sent\n%s'%message.toString())
 
 
     def sendResponse(self, message, code, body=None):
@@ -536,21 +530,17 @@
         self.sendResponse(invite, 180)
         if self.getState() != 'ABORTED':
             self.setState('SENT_RINGING')
-        self.installTeardownTrigger()
 
     def startSendInvite(self, toAddr, init=0):
         #print "startSendInvite", init
         if init:
             d = self.sip.app.acceptCall(call=self)
-            d.addCallback(lambda x:self.sendInvite(toAddr, cookie=x, init=init)
+            d.addCallback(lambda x:self.sendInvite(toAddr, init=init)
                                                         ).addErrback(log.err)
         else:
             self.sendInvite(toAddr, init=0)
 
-    def sendInvite(self, toAddr, cookie=None, auth=None, authhdr=None, init=0):
-        if cookie:
-            #print "sendinvite setting cookie to", cookie
-            self.cookie = cookie
+    def sendInvite(self, toAddr, auth=None, authhdr=None, init=0):
         lhost, lport = self.getLocalSIPAddress()
         username = self.sip.app.getPref('username')
         self.dialog.setContact(username, lhost, lport)
@@ -575,7 +565,7 @@
             #print auth, authhdr
             invite.addHeader(authhdr, auth)
         invite.addHeader('contact', str(self.dialog.getContact()))
-        s = self.sip.app.getSDP(self.cookie)
+        s = self.sip.app.getSDP()
         sdp = s.show()
         invite.addHeader('content-length', len(sdp))
         invite.bodyDataReceived(sdp)
@@ -591,18 +581,17 @@
             self.setState('ABORTED')
         else:
             self.setState('SENT_INVITE')
-            self.installTeardownTrigger()
 
     def extractURI(self, val):
         name,uri,params = tpsip.parseAddress(val)
         return uri.toString()
 
-    def sendAck(self, okmessage, startRTP=0):
+    def _sendAck(self, okmessage):
         username = self.sip.app.getPref('username')
         if okmessage.code == 200:
             oksdp = SDP(okmessage.body)
-            sdp = self.sip.app.getSDP(self.cookie, oksdp)
-            if not sdp.hasMediaDescriptions():
+            oksdp.intersect(self.sip.app.getSDP())
+            if not oksdp.hasMediaDescriptions():
                 self.sendResponse(okmessage, 406)
                 self.setState('ABORTED')
                 # compDef.errback? XXX
@@ -646,17 +635,15 @@
         if hasattr(self, 'compDef'):
             cb = self.compDef.callback
             del self.compDef
-        else:
-            cb = lambda *args: None
+
         addr = self._remoteURI.host, self._remoteURI.port or 5060
         log.msg("sending ACK to %s %s"%addr, system='sip')
-        #print "sending ACK to %s %s"%addr
+        log.msg("sending ACK to %r\n%s"%(addr, ack.toString()), system="sip")
         self.sip.transport.write(ack.toString(), _hostportToIPPort(addr))
+        oldstate = self.getState()
         self.setState('CONNECTED')
-        if startRTP:
-            self.sip.app.startCall(self.cookie, oksdp, cb)
-        log.msg("sending ACK to %r\n%s"%(addr, ack.toString()), system="sip")
-        self.sip.app.statusMessage("Call Connected")
+        if oldstate != 'CONNECTED':
+            self.sip.app.startCall(oksdp)
 
     def sendBye(self, toAddr="ignored", auth=None, authhdr=None):
         username = self.sip.app.getPref('username')
@@ -713,18 +700,18 @@
         """
         self.sendResponse(message, 487)
         self.setState('ABORTED')
-        self.sip.app.endCall(self.cookie)
+        self.sip.app.endCall()
 
     def recvAck(self, message):
         ''' The remote UAC has ACKed our response to their INVITE.
             Start sending and receiving audio.
         '''
-        sdp = SDP(self._invite.body)
+        oldstate = self.getState()
         self.setState('CONNECTED')
-        if hasattr(self, 'compDef'):
-            d, self.compDef = self.compDef, None
-            self.sip.app.startCall(self.cookie, sdp,
-                                   d.callback)
+        if oldstate != 'CONNECTED':
+            sdp = SDP(self._invite.body)
+            sdp.intersect(self.sip.app.getSDP()) # Not sure if this is necessary, but it can't hurt. --Zooko 2005-03-21
+            self.sip.app.startCall(sdp)
 
     def recvOptions(self, message):
         """ Received an OPTIONS request from a remote UA.
@@ -732,39 +719,22 @@
             otherwise an error (XXXXX)
         """
 
-    def installTeardownTrigger(self):
-        from twisted.internet import reactor
-        if 0 and self.cancel_trigger is None:
-            t = reactor.addSystemEventTrigger('before',
-                                              'shutdown',
-                                              self.dropCall,
-                                              appTeardown=True)
-            self.cancel_trigger = t
-
-    def dropCall(self, appTeardown=False):
+    def dropCall(self):
         '''Drop call '''
-        from twisted.internet import reactor
-        # XXX return a deferred, and handle responses properly
-        if not appTeardown and self.cancel_trigger is not None:
-            reactor.removeSystemEventTrigger(self.cancel_trigger)
         state = self.getState()
         log.msg("dropcall in state %r"%state, system="sip")
         if state == 'NONE':
             self.sip.app.debugMessage("no call to drop")
-            return defer.succeed('no call to drop')
         elif state in ( 'CONNECTED', ):
             self.sendBye()
             self.dropDef = defer.Deferred()
-            return self.dropDef
             # XXX callLater to give up...
         elif state in ( 'SENT_INVITE', ):
             self.sendCancel()
             self.setState('SENT_CANCEL')
-            return defer.succeed('cancelled')
             # XXX callLater to give up...
         elif state in ( 'NEW', ):
             self.setState('ABORTED')
-            return defer.succeed('aborted')
 
     def _getHashingImplementation(self, algorithm):
         if algorithm.lower() == 'md5':
@@ -831,12 +801,11 @@
             self.auth_attempts = 0
             if state == 'SENT_INVITE':
                 self.sip.app.debugMessage("Got Response 200\n")
-                self.sendAck(message, startRTP=1)
+                self._sendAck(message)
             elif state == 'CONNECTED':
                 self.sip.app.debugMessage('Got duplicate OK to our ACK')
-                #self.sendAck(message)
             elif state == 'SENT_BYE':
-                self.sip.app.endCall(self.cookie)
+                self.sip.app.endCall()
                 self.sip._delCallObject(self.getCallID())
                 self.sip.app.debugMessage("Hung up on call %s"%self.getCallID())
                 self.sip.app.statusMessage("Call Disconnected")
@@ -847,7 +816,7 @@
         elif message.code - (message.code%100) == 400:
             if state == 'SENT_CANCEL' and message.code == 487:
                 #print "cancelled"
-                self.sip.app.endCall(self.cookie)
+                self.sip.app.endCall()
                 self.sip._delCallObject(self.getCallID())
                 self.sip.app.debugMessage("Hung up on call %s"%self.getCallID())
                 self.sip.app.statusMessage("Call Disconnected")
@@ -906,7 +875,7 @@
                 else:
                     log.err("Unknown state '%s' for a 401/407"%(state))
             else:
-                if self.state == 'SENT_BYE':
+                if self.getState() == 'SENT_BYE':
                     d, self.dropDef = self.dropDef, None
                     # XXX failed to drop call. need exception here
                     d.errback(message.code)
@@ -919,7 +888,7 @@
                 self.sip.app.statusMessage("Call Failed: %s %s"%(message.code,
                                                              message.phrase))
         elif message.code - (message.code%100) == 500:
-            if self.state == 'SENT_BYE':
+            if self.getState() == 'SENT_BYE':
                 d, self.dropDef = self.dropDef, None
                 # XXX failed to drop call. need exception here
                 d.errback(message.code)
@@ -931,7 +900,7 @@
             self.sip.app.statusMessage("Call Failed: %s %s"%(message.code,
                                                              message.phrase))
         elif message.code - (message.code%100) == 600:
-            if self.state == 'SENT_BYE':
+            if self.getState() == 'SENT_BYE':
                 d, self.dropDef = self.dropDef, None
                 # XXX failed to drop call. need exception here
                 d.errback(message.code)
@@ -939,7 +908,6 @@
             if self.state == 'SENT_INVITE':
                 self.sendAck(message)
             self.terminateCall(message)
-            #self.sip.app.endCall(self.cookie, 'Other end sent %s'%message.toString())
             self.sip._delCallObject(self.getCallID())
             self.sip.app.statusMessage("Call Failed: %s %s"%(message.code,
                                                              message.phrase))
@@ -953,16 +921,14 @@
         self.sip = phone
         self.regServer = None
         self.regAOR = None
-        self.state = 'NEW'
         self._needSTUN = False
         self.cseq = random.randint(1000,5000)
         self.dialog = Dialog()
         self.nonce_count = 0
-        self.cancel_trigger = None
         self.register_attempts = 0
         self._outboundProxyURI = None
 
-    def startRegistration(self):
+        self.state = 'NEW'
         self.compDef = defer.Deferred()
         self.regServer = Address(self.sip.app.getPref('register_uri'))
         self.regURI = copy.copy(self.regServer)
@@ -975,7 +941,6 @@
         #self.regURI.port = None
         d = self.setupLocalSIP(self.regServer)
         d.addCallback(self.sendRegistration).addErrback(log.err)
-        return self.compDef
 
     def sendRegistration(self, cb=None, auth=None, authhdr=None):
         # XXX parameterise the retransmit timer!
@@ -989,7 +954,7 @@
         invite.addHeader('cseq', '%s REGISTER'%self.dialog.getCSeq(incr=1))
         invite.addHeader('to', str(self.regAOR))
         invite.addHeader('from', str(self.regAOR))
-        state =  self.getState()
+        state = self.getState()
         if state in ( 'NEW', 'SENT_REGISTER', 'REGISTERED' ):
             invite.addHeader('expires', 900)
         elif state in ( 'CANCEL_REGISTER' ):
@@ -1018,7 +983,6 @@
         self.sendRegistration(auth=auth, authhdr=authhdr)
 
     def recvResponse(self, message):
-        from twisted.internet import reactor
         state = self.getState()
         if message.code in ( 401, 407 ):
             self.register_attempts += 1
@@ -1068,17 +1032,11 @@
                 log.err("Unknown registration state '%s' for a 401/407"%(state))
         elif message.code in ( 200, ):
             self.sip.app.statusMessage("Registration: OK")
-            if hasattr(self.sip.app, 'registrationOK'):
-                self.sip.app.registrationOK(self)
             # Woo. registration succeeded.
+            self.sip.app.notifyEvent('registrationOK', self)
             self.register_attempts = 0
             if state == 'SENT_REGISTER':
                 self.setState('REGISTERED')
-                if 0 and self.cancel_trigger is None:
-                    t = reactor.addSystemEventTrigger('before',
-                                                      'shutdown',
-                                                      self.cancelRegistration)
-                    self.cancel_trigger = t
             elif state == 'CANCEL_REGISTER':
                 self.setState('UNREGISTERED')
                 d = self._cancelDef
@@ -1141,17 +1099,17 @@
                     d.addCallbacks(self.register, log.err)
             else:
                 log.msg("no outstanding registrations, registering", system="sip")
-                r = Registration(self)
-                d = r.startRegistration()
-                return d
+                Registration(self)
 
     def _newCallObject(self, to=None, callid=None):
         call = Call(self, uri=to, callid=callid)
-        d = call.callStart()
+
+        self.compDef = defer.Deferred()
+
         if call.getState() != 'ABORTED':
             if call.getCallID():
                 self._calls[call.getCallID()] = call
-            return call, d
+            return call, self.compDef
 
     def updateCallObject(self, call, callid):
         "Used when Call setup returns a deferred result (e.g. STUN)"
@@ -1171,7 +1129,7 @@
             callid = callid[1:-1]
         del self._calls[callid]
 
-    def placeCall(self, uri, fromuri=None, cookie=None):
+    def placeCall(self, uri, fromuri=None):
         """Place a call.
 
         uri should be a string, an address of the person we are calling,
@@ -1190,8 +1148,6 @@
         if call is None:
             _d.errback(CallFailed)
             return _d
-        if cookie is not None:
-            call.cookie = cookie
         invite = call.startOutboundCall(uri, fromAddr=fromuri)
         return _d
 
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/test/test_app.py new-newfrom1406/trunk/shtoom/shtoom/test/test_app.py
--- old-newfrom1406/trunk/shtoom/shtoom/test/test_app.py	2005-09-27 23:36:19.862456160 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/test/test_app.py	2005-09-27 23:36:20.524355536 -0300
@@ -7,17 +7,20 @@
 class TestUI:
     pass
 
-class TestAudio:
+class TestPlayAudio:
     pass
 
+class TestMicAudio:
+    def open(self, handler):
+        pass
+
 class AppStartup(unittest.TestCase):
 
 
     def buildPhone(self):
         ui = TestUI()
-        audio = TestAudio()
         from shtoom.app.phone import Phone
-        p = Phone(ui, audio)
+        p = Phone(TestPlayAudio(), TestMicAudio(), ui)
         # Disable the NAT Mapping code for these tests
         p._NATMapping = False
         return p
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/test/test_callcontrol.py new-newfrom1406/trunk/shtoom/shtoom/test/test_callcontrol.py
--- old-newfrom1406/trunk/shtoom/shtoom/test/test_callcontrol.py	2005-09-27 23:36:19.860456464 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/test/test_callcontrol.py	2005-09-27 23:36:20.523355688 -0300
@@ -45,49 +45,39 @@
      (Call.acceptedCall(), Call.rejectedCall())
 """
 
-class TestAudio:
+class TestPlayAudio:
     def __init__(self):
         self.actions = []
 
-    def selectDefaultFormat(self, fmt):
-        self.actions.append('select')
-        if TDEBUG: print "selecting fake audio format"
-
-    def listFormat(self):
-        self.actions.append('list')
-        return []
-
-    def reopen(self, mediahandler):
-        self.actions.append('reopen')
-        if TDEBUG: print "reopening fake audio"
+    def open(self, refillcb=None):
+        pass
 
     def close(self):
         self.actions.append('close')
         if TDEBUG: print "closing fake audio"
 
-    def read(self):
-        self.actions.append('read')
-        return ''
-
     def write(self, bytes):
         self.actions.append('write')
         pass
 
-    def play_wave_file(self, file):
-        self.actions.append('wave')
+class TestMicAudio:
+    def __init__(self):
+        self.actions = []
+
+    def open(self, mediahandler=None):
         pass
 
+    def close(self):
+        self.actions.append('close')
+        if TDEBUG: print "closing fake audio"
+
 class TestUI:
     threadedUI = False
-    cookie = None
 
     def __init__(self, stopOnDisconnect=True):
         self.stopOnDisconnect = stopOnDisconnect
         self.actions = []
 
-    def statusMessage(self, *args):
-        pass
-
     def connectApplication(self, app):
         self.app = app
 
@@ -235,8 +225,9 @@
     def testOutboundLocalBye(self, loopcount=4):
         from shtoom.app.phone import Phone
         ui = TestUI()
-        au = TestAudio()
-        p = Phone(ui=ui, audio=au)
+        aum = TestMicAudio()
+        aup = TestPlayAudio()
+        p = Phone(aum, aup, ui)
         p._rtpProtocolClass = TestRTP
         ui.connectApplication(p)
         p.connectSIP = lambda x=None: None
@@ -250,7 +241,8 @@
             reactor.callLater(0, ui.dropCall)
             p.start()
             twisted.trial.util.wait(testdef)
-            self.assertEquals(au.actions, ['close', 'select', 'reopen', 'close'])
+            self.assertEquals(aum.actions, ['reopen', 'close'])
+            self.assertEquals(aup.actions, ['reopen', 'close'])
             self.assertEquals(TestRTP.actions, ['create', 'start', 'stop'])
             actions = ui.actions
             if TDEBUG: print actions
@@ -260,7 +252,8 @@
             actions = [x[0] for x in actions]
             self.assertEquals(actions, ['start', 'connected', 'drop', 'disconnected'])
             ui.actions = []
-            au.actions = []
+            aup.actions = []
+            aum.actions = []
 
     def testOutboundRemoteBye(self):
         from shtoom.app.phone import Phone
@@ -279,7 +272,7 @@
         reactor.callLater(0.3, lambda : p.sip.dropCall(ui.cookie))
         p.start()
         twisted.trial.util.wait(testdef)
-        self.assertEquals(au.actions, ['close', 'select', 'reopen', 'close'])
+        self.assertEquals(au.actions, ['reopen', 'close'])
         self.assertEquals(TestRTP.actions, ['create', 'start', 'stop'])
         actions = ui.actions
         cookie = actions[0][1]
@@ -293,7 +286,8 @@
         from shtoom.app.phone import Phone
         ui = TestUI()
         au = TestAudio()
-        p = Phone(ui=ui, audio=au)
+        p = Phone(ui=ui)
+        p.set_audio_device(au)
         p._startReactor = False
         TestRTP.actions = []
         p._rtpProtocolClass = TestRTP
@@ -306,7 +300,7 @@
         d.addCallback(p.sip.dropFakeInbound)
         p.start()
         twisted.trial.util.wait(testdef)
-        self.assertEquals(au.actions, ['wave', 'close', 'select', 'reopen', 'close'])
+        self.assertEquals(au.actions, ['reopen', 'close'])
         self.assertEquals(TestRTP.actions, ['create', 'start', 'stop'])
         actions = ui.actions
         cookie = actions[0][1]
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/test/test_codecs.py new-newfrom1406/trunk/shtoom/shtoom/test/test_codecs.py
--- old-newfrom1406/trunk/shtoom/shtoom/test/test_codecs.py	2005-09-27 23:36:19.837459960 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/test/test_codecs.py	2005-09-27 23:36:20.525355384 -0300
@@ -56,17 +56,17 @@
         class Foo:
             def handle_media_sample(self, sample):
                 ae(sample.data, 'frobozulate')
-        c.set_handler(Foo().handle_media_sample)
+        c.set_handler(Foo())
 
-        c.handle_audio('frobozulate')
+        c.handle_data('frobozulate')
 
         class Foo:
             def handle_media_sample(self, sample):
                 ae(sample.data, 'farnarkling')
                 ae(sample.ct, PT_RAW)
-        c.set_handler(Foo().handle_media_sample)
+        c.set_handler(Foo())
 
-        c.handle_audio('farnarkling')
+        c.handle_data('farnarkling')
 
     # XXX testing other codecs - endianness issues? crap.
 
@@ -82,8 +82,8 @@
                 ae(len(sample.data), 160)
                 ae(sample.data, ulawout)
                 ae(sample.ct, PT_PCMU)
-        c.set_handler(Foo().handle_media_sample)
-        c.handle_audio(instr)
+        c.set_handler(Foo())
+        c.handle_data(instr)
 
     def testGSMCodec(self):
         if codecs.gsm is None:
@@ -98,22 +98,22 @@
                 ae(sample.ct, PT_GSM)
                 p = RTPPacket(0, 0, 0, data=sample.data, ct=sample.ct)
                 ae(len(c.decode(p)), 320)
-        c.set_handler(Foo().handle_media_sample)
-
-        c.handle_audio(instr)
+        c.set_handler(Foo())
 
+        c.handle_data(instr)
+        
         c = Codecker(PT_GSM)
         ae(c.getDefaultFormat(), PT_GSM)
 
         class Foo:
             def handle_media_sample(self, sample, tester=self):
                 tester.fail("WRONG.  The decoding of 32 zeroes (a short GSM frame) is required to be None, but it came out: %s" % (sample,))
-        c.set_handler(Foo().handle_media_sample)
+        c.set_handler(Foo())
 
-        c.handle_audio('\0'*32)
+        c.handle_data('\0'*32)
 
     def testSpeexCodec(self):
-        if codecs.gsm is None:
+        if codecs.speex is None:
             raise unittest.SkipTest("no speex support")
         ae = self.assertEquals
         c = Codecker(PT_SPEEX)
@@ -125,16 +125,16 @@
                 ae(sample.ct, PT_SPEEX)
                 p = RTPPacket(0, 0, 0, data=sample.data, ct=sample.ct)
                 ae(len(c.decode(p)), 320)
-        c.set_handler(Foo().handle_media_sample)
+        c.set_handler(Foo())
 
-        p = c.handle_audio(instr)
+        p = c.handle_data(instr)
 
         class Foo:
             def handle_media_sample(self, sample, tester=self):
                 tester.fail("WRONG.  The decoding of 30 zeroes (a short Speex frame) is required to be None, but it came out: %s" % (sample,))
-        c.set_handler(Foo().handle_media_sample)
+        c.set_handler(Foo())
 
-        c.handle_audio('\0'*30)
+        c.handle_data('\0'*30)
 
     def testMediaLayer(self):
         ae = self.assertEquals
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/test/test_rtp.py new-newfrom1406/trunk/shtoom/shtoom/test/test_rtp.py
--- old-newfrom1406/trunk/shtoom/shtoom/test/test_rtp.py	2005-09-27 23:36:19.820462544 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/test/test_rtp.py	2005-09-27 23:36:20.525355384 -0300
@@ -69,15 +69,3 @@
             ae(rpack.header.ts, ts)
             ae(rpack.header.ssrc, ssrc)
 
-    def testSDPGen(self):
-        from shtoom.rtp.formats import SDPGenerator, PTMarker
-        from shtoom.sdp import SDP
-        a_ = self.assert_
-        class DummyRTP:
-            def getVisibleAddress(self):
-                return ('127.0.0.1', 23456)
-        rtp = DummyRTP()
-        sdp = SDP(SDPGenerator().getSDP(rtp).show())
-        rtpmap = sdp.getMediaDescription('audio').rtpmap
-        for pt, (entry, ptmarker) in rtpmap.items():
-            a_(isinstance(ptmarker, PTMarker))
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/test/test_sipauth.py new-newfrom1406/trunk/shtoom/shtoom/test/test_sipauth.py
--- old-newfrom1406/trunk/shtoom/shtoom/test/test_sipauth.py	2005-09-27 23:36:19.819462696 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/test/test_sipauth.py	2005-09-27 23:36:20.525355384 -0300
@@ -7,11 +7,19 @@
 
 from twisted.trial import unittest
 
+class TestApp:
+    def getPref(self, prefnamethingie):
+        return "foo"
+
+class TestSip:
+    def __init__(self):
+        self.app = TestApp()
+
 class AuthTest(unittest.TestCase):
 
     def test_sipSimpleAuth(self):
         from shtoom.sip import Registration
-        reg = Registration(None)
+        reg = Registration(TestSip())
         resp = reg.calcAuth('REGISTER', 'sip:divmod.com',
                      'Digest realm="asterisk", nonce="24a52b3a"',
                      ('anthony', 'foo'))
diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/ui/textui/main.py new-newfrom1406/trunk/shtoom/shtoom/ui/textui/main.py
--- old-newfrom1406/trunk/shtoom/shtoom/ui/textui/main.py	2005-09-27 23:36:19.818462848 -0300
+++ new-newfrom1406/trunk/shtoom/shtoom/ui/textui/main.py	2005-09-27 23:36:20.516356752 -0300
@@ -5,11 +5,11 @@
 from twisted.protocols import basic
 from shtoom.exceptions import CallRejected, CallNotAnswered
 
+from shtoom.exceptions import CallRejected
+
 class ShtoomMain(basic.LineReceiver, ShtoomBaseUI):
-    _cookie = None
     _pending = None
     _debug = True
-    _incoming_timeout = None
     from os import linesep as delimiter
     sipURL = None
 
@@ -26,18 +26,13 @@
 
     def shutdown(self):
         from twisted.internet import reactor
-        # XXX Hang up any calls
-        if self._cookie:
-            self.app.dropCall(self._cookie)
         reactor.stop()
 
-    def incomingCall(self, description, cookie):
+    def incomingCall(self, description):
         from twisted.internet import reactor
         if self._pending is not None:
             return defer.fail(UserBusy())
-        defresp = defer.Deferred()
-        self._pending = ( cookie, defresp )
-        self._incoming_timeout = reactor.callLater(20, lambda :self._timeout_incoming(self._pending))
+        self._pending = defer.Deferred()
         self.transport.write("INCOMING CALL: %s\n"%description)
         self.transport.write("Type 'accept' to accept, 'reject' to reject\n")
         return defresp
@@ -68,9 +63,6 @@
 
     def cmd_call(self, line):
         "call sip:whatever -- place a new outbound call"
-        if self._cookie is not None:
-            self.transport.write("error: only one call at a time for now\n")
-            return
         args = line.split()
         if len(args) != 2:
             self.transport.write("error: call <sipurl>\n")
@@ -82,27 +74,21 @@
         deferred = self.app.placeCall(self.sipURL)
         deferred.addCallbacks(self.callConnected, self.callFailed).addErrback(log.err)
 
-    def callStarted(self, cookie):
-        self._cookie = cookie
-        log.msg("Call %s STARTED"%(self._cookie), system='ui')
+    def callStarted(self):
+        log.msg("Call STARTED", system='ui')
 
-    def callConnected(self, cookie):
+    def callConnected(self):
         log.msg("Call to %s CONNECTED"%(self.sipURL), system='ui')
 
     def callFailed(self, e, message=None):
         log.msg("Call to %s FAILED: %r"%(self.sipURL, e), system='ui')
 
-    def callDisconnected(self, cookie, message):
+    def callDisconnected(self, message):
         log.msg("Call to %s DISCONNECTED"%(self.sipURL), system='ui')
-        self._cookie = None
 
     def cmd_hangup(self, line):
         "hangup -- hangs up current call"
-        if self._cookie is not None:
-            self.app.dropCall(self._cookie)
-            self._cookie = None
-        else:
-            self.transport.write("error: no active call\n")
+        self.app.endCall()
 
     def cmd_quit(self, line):
         "quit -- exit the program"
@@ -113,22 +99,17 @@
         if not self._pending:
             self.transport.write("no pending calls")
             return
-        self._incoming_timeout.cancel()
-        self._incoming_timeout = None
-        self._cookie, resp = self._pending
+        self._pending.callback(None)
         self._pending = None
         resp.callback(self._cookie)
 
     def cmd_reject(self, line):
         "reject -- reject an incoming call"
-        self._incoming_timeout.cancel()
-        self._incoming_timeout = None
         if not self._pending:
             self.transport.write("no pending calls")
             return
-        cookie, resp = self._pending
+        self._pending.callback(CallRejected('no thanks'))
         self._pending = None
-        resp.callback(CallRejected('no thanks', cookie))
 
     def cmd_auth(self, line):
         "auth realm user password -- add a user/password"
@@ -154,14 +135,7 @@
                 continue
             n = float(n)
             reactor.callLater(initial+n*(duration+delay),
-                lambda k=key: self.app.startDTMF(self._cookie, k))
+                lambda k=key: self.app.startDTMF(k))
             reactor.callLater(initial+n*(duration+delay)+duration,
-                lambda k=key: self.app.stopDTMF(self._cookie, k))
+                lambda k=key: self.app.stopDTMF(k))
 
-    def _timeout_incoming(self, which):
-        # Not using 'which' for now
-        self.transport.write("CALL NOT ANSWERED\n")
-        self._incoming_timeout = None
-        cookie, resp = self._pending
-        self._pending = None
-        resp.callback(CallNotAnswered('not answering', cookie))



More information about the Shtoom mailing list