Journal Articles

CVu Journal Vol 15, #2 - Apr 2003 + Programming Topics
Browse in : All > Journals > CVu > 152 (9)
All > Topics > Programming (877)
Any of these categories - All of these categories

Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.

Title: A Python Project

Author: Administrator

Date: 03 April 2003 13:15:56 +01:00 or Thu, 03 April 2003 13:15:56 +01:00

Summary: 

Body: 

This article describes the Python code in a small project that I've written lately. I wrote this article when I heard that C Vu was short of articles. It is a little rushed. But it might be a useful "dive in" introduction to Python for programmers experienced in other languages, as well as being a starting point for discussion. Some of this code could be written better.

My program was designed to help me increase my vocabulary in a foreign language, by playing audio samples to me. It is entirely non-interactive. It plays an English word, then pauses while I'm supposed to remember how it's said in the other language, then plays the foreign word so I can check what I've remembered. The tricky part is I wanted new words to be repeated more often than old words, in graduated intervals (i.e. intervals that start small and increase). That meant that each lesson needs some planning.

I decided to represent the lesson as a list of events. Let's have a class called Schedule that holds a sorted list of (start,finish) times:

class Schedule:
    def __init__(self): self.bookedList = []
    def book(self,start,finish):
        self.bookedList.append((start,finish))
        self.bookedList.sort()

We'll leave it at that for now (no functionality to check overlaps etc yet).

Now, there are two main kinds of events - actual events that play sound, and events that link other events (I called this "glue", from TeX typesetting terminology). For example, two repetitions separated by an interval of time will be represented by two (clusters of) events separated by a glue event. The glue event is invisible to the booking of other events (i.e. other things can happen in the lesson while the glue is taking place) but it is still there, ensuring that the delay between the two repetitions is of an acceptable length. (The glue will tolerate a certain amount of stretching or shrinking in order to make everything fit.)

Here is a class containing some functionality for both events and glue. The parameter 'invisible' is non-zero if it's glue and other events can take place at the same time; the parameter 'plusMinus' gives the tolerance for stretching/shrinking. Note that the length is given in the constructor, but the start time is not given until the methods are called. This is because we won't know the start time when we're creating the event; we need to find a time where it will fit in.

class GlueOrEvent:
    def __init__(self,length=0,plusMinus=0,invisible=0):
        self.length = length
        self.plusMinus = plusMinus
        self.invisible = invisible
    def bookIn(self,schedule,start):
        if not self.invisible:
            schedule.book(start,start+self.length)

The bookIn method reserves time on the schedule. This is used later when checking for overlaps, so invisible events are not booked in.

    def addToEvents(self,events,startTime):
        assert not self.invisible
        events.append((startTime,self))

The addToEvents method adds this event to a list with its start time. This will be used later when calling the Python schedule library to play the lesson. It will be overridden by composite events which add each of their constituent events to the list.

Now for an overlap check. The following method is designed to return a value indicating how much this event has to move in order not to overlap with anything else on the schedule. It is passed a parameter indicating what direction it should move in (+1 or -1).

    def overlaps(self,start,schedule,direction):
        if self.invisible: return 0
        if not schedule.bookedList: return 0
        count = 0
        # Skip over all events that finish before we start
        while schedule.bookedList[count][1] <= start:
            count = count + 1
            if count >= len(schedule.bookedList): return 0
        # Does this event start before we finish?
        if schedule.bookedList[count][0]<start+self.length:
            # We have an overlap
            if direction < 0:
                # Make sure we finish before it starts
                backwards = start+self.length-schedule.bookedList[count][0]
                return self.overlaps(start-backwards,schedule,direction)+backwards
            else:
                # Make sure we start after it finishes
                forwards = schedule.bookedList[count][1] - start
                return self.overlaps(start+forwards,schedule,direction)+forwards
        return 0 # No overlap

Finally, a play() method which will be overridden later.

    def play(self): pass

Now we can create some subclasses, such as Event and Glue:

class Event (GlueOrEvent):
    def __init__(self,length):
        GlueOrEvent.__init__(self,length)
class Glue (GlueOrEvent):
    def __init__(self,length,plusMinus):
        GlueOrEvent.__init__(self,length,plusMinus,1)

A CompositeEvent is an event made up of several others in sequence with nothing intervening (no glue).

class CompositeEvent (Event):
    def __init__(self,eventList):
        len = 0
        for i in eventList: len = len + i.length
        Event.__init__(self,len)
        self.eventList = eventList
    def addToEvents(self,events,startTime):
        for i in self.eventList:
            i.addToEvents(events,startTime)
            startTime = startTime + i.length

Now we'll have a class called GluedEvent (i.e. an event with some glue before it), which has functionality to adjust the glue (stretch/shrink it within its tolerance) in order to get the event to fit properly. This needs an exception, which can be declared as an empty class:

class StretchedTooFar: pass

Here is the GluedEvent class. Note that there is functionality both for random adjustment (using a Gaussian distribution) and for adjustment to avoid an overlap. It is written in such a way that it is possible to call the adjustment code more than once with different directions.

class GluedEvent:
    def __init__(self,glue,event):
        self.glue = glue
        self.event = event
        self.glue.adjustment = self.glue.preAdjustment = 0
    def randomPreAdjustment(self):
        if self.glue.length < randomAdjustmentThreshold: return
        self.glue.preAdjustment = random.gauss(0,self.glue.plusMinus)
        if abs(self.glue.preAdjustment) > self.glue.plusMinus:
            self.glue.preAdjustment = self.glue.plusMinus # err on the +ve side
    def adjustGlue(self,glueStart,schedule,direction):
        needMove = self.event.overlaps(glueStart+self.glue.length+self.glue.preAdjustment,schedule,direction)
        needMove=needMove*direction+self.glue.preAdjustment
        direction=sgn(needMove) ; needMove=abs(needMove)
        if needMove > self.glue.plusMinus \
           or glueStart+needMove*direction < 0 \
           or glueStart+self.glue.length+needMove*direction+self.event.length > maxLenOfLesson:
            raise StretchedTooFar()
        self.glue.adjustment = needMove * direction
    def getAdjustedEnd(self,glueStart):
        return glueStart+self.glue.length+self.glue.adjustment+self.event.length
    def bookIn(self,schedule,glueStart):
        self.event.bookIn(schedule,glueStart+self.glue.length+self.glue.adjustment)
    def getEventStart(self,glueStart):
        return glueStart+self.glue.length+self.glue.adjustment

Now for a function called setGlue which "sets" (adjusts) the glue for all the events in a list. This function needs to backtrack when a StretchedTooFar exception is raised, and try a different way of setting the glue. (Don't forget that we can always try another direction.) If there is no solution at all then the StretchedTooFar exception will be raised by the function itself.

def setGlue(gluedEventList, schedule, glueStart = 0):
    if not gluedEventList: return
    try:
        gluedEventList[0].randomPreAdjustment()
        gluedEventList[0].adjustGlue(glueStart,schedule,1)
        setGlue(gluedEventList[1:],schedule,gluedEventList[0].getAdjustedEnd(glueStart))
    except StretchedTooFar:
        gluedEventList[0].adjustGlue(glueStart,schedule,-1)
        setGlue(gluedEventList[1:],schedule,gluedEventList[0].getAdjustedEnd(glueStart))

There is a problem with the above algorithm. It fits the first event into the first place it will fit, and then it will try to fit the rest of the sequence based on the position of that first event. If it fails to fit the rest of the sequence, it will fail completely; it doesn't ever try to fit the first event in a different place (later). I hacked around this with a wrapper function; it's a bit rushed but it usually works (hey this is Python):

def setGlue_wrapper(gluedEventList, schedule):
    sillyOffset = 0 # seconds
    worked = 0
    while (not worked) and sillyOffset < maxLenOfLesson:
        try:
            setGlue(gluedEventList, schedule)
            worked = 1
        except StretchedTooFar:
            # try harder
            sillyOffset = sillyOffset + 10 # *** needs to be a constant
            gluedEventList[0].glue.length = sillyOffset
    if not worked: raise StretchedTooFar()

So now we can write a function that will book a list of GluedEvents into a lesson, adjusting the glue as necessary:

def bookIn(gluedEventList,schedule):
    setGlue_wrapper(gluedEventList,schedule)
    glueStart = 0
    for i in gluedEventList:
        i.bookIn(schedule,glueStart)
        glueStart = i.getAdjustedEnd(glueStart)

And now the lesson itself, including the function to play the events (I've cut this down a bit - taken out all the diagnostic messages and the functionality to write the lesson to a sound file as well as play it through the speakers - I don't know how big this article is getting)

class Lesson:
    def __init__(self):
        self.schedule = Schedule()
        self.events = [] # list of (time,event)
        self.newWords = self.oldWords = 0
    def addSequence(self,gluedEventList):
        bookIn(gluedEventList,self.schedule)
        glueStart = 0
        for i in gluedEventList:
            startTime = i.getEventStart(glueStart)
            i.event.addToEvents(self.events,startTime)
            glueStart = i.getAdjustedEnd(glueStart)
    def play(self):
        self.events.sort()
        runner = sched.scheduler(time.time,time.sleep)
        for (t,event) in self.events:
            runner.enter(t,1,play,(event,))
        runner.run()
def play(event):
    event.play()

Now we need a type of Event that will actually play something:

class WavEvent(Event):
    def __init__(self,file):
        self.file = file
        header = sndhdr.what(file)
        if not header: raise IOError("Problem opening wave file")
        (wtype,wrate,wchannels,wframes,wbits) = header
        divisor = wrate*wchannels*(wbits/8)
        o = open(file,'rb')
        fileLen = len(o.read())
        o.close()
        seconds = math.ceil((fileLen + 0.0) / divisor) # approx
        Event.__init__(self,seconds)
    def play(self):
        if winsound: winsound.PlaySound(self.file,winsound.SND_FILENAME)
        else: os.system("play %s" % (self.file,))

And now all we have to do is make those GluedEventLists in a sensible manner. I'll leave that to the next issue.

Notes: 

More fields may be available via dynamicdata ..