Larry Osterman's WebLog

Confessions of an Old Fogey
Blog - Title

The last change for the cheesy OSD application - preventing volume recursion.

The last change for the cheesy OSD application - preventing volume recursion.

Rate This
  • Comments 13

Yesterday, at the close of my article about adding notifications support to the cheesy OSD application, I mentioned that there was one remaining problem.

The problem is actually a big deal - as it's written right now, the application can't filter out changes made by the application.  You see, your notification routine should only listen to changes that were made by applications other than your own - you already know about your applications changes.

This problem is compounded by the use of floating point numbers for the volume values - it's possible to get into what is affectionately known as "the floating point rounding spiral of death" if the right values happen to be selected.  If you're not familiar with floating point rounding issues, then you REALLY should go and read the MOST excellent "What Every Computer Scientist Should Know About Floating-Point Arithmetic" by David Goldberg.

Here's the simple version of how the "floating point rounding spiral of death" works.  Imagine you've got an application with a volume slider.  When a volume change notification is received by the application, it changes the position of the slider to match the new volume.  Simple enough, right? 

When a volume change notification is received, the volume notification handler looks at the value in the notification, and if it doesn't match the current position of the slider, it sets the slider's position to match the value in the notification.  Setting the slider generates a notification that the slider's position changed (this is the way the slider common control works, as I understand it, it's impossible to distinguish the notification generated by a program setting the slider from the user changing the slider's position).  So the slider's "set the current position" logic looks at the position of the slider and compares it against the current volume.  If they're different, the slider's code updates the system volume, which triggers a notification, which checks to see if the value in the notification doesn't match the position of the slider, etc...

This works great IF there's no rounding in the system.  But we're dealing with floating point integers here.  One of the characteristics of floating point numbers is that it's essentially impossible to compare two floating point numbers for equality - because of rounding errors, the values can only be compared relative to some amount of precision.

Let's consider what happens with our application.  It receives a notification that someone has changed the volume to 0.49999999937.  It compares it against the current value of the volume (0.5) and decides that they're not equal.  So it changes the slider to appropriate relative position, which rounds down to 0.4.  The slider change logic compares the current position of the slider (0.4) against the current volume (0.49999999937) and sets the volume to .4.  That triggers a notification that the master volume is 0.39999, the slider change logic compares the current position of the slider (0.4) against the current volume (0.39999), resets the slider to .3 and you watch in horror as the volume quickly spirals to 0.  What's worse, once this happens, you can't fix it - you move the slider up and it spirals down to 0 again.  Watching this effect in action can be quite comical (if rather annoying).

Nitpickers corner: Yeah, careful coding could avoid this particular example, and yeah, I'm using made-up numbers, these values are unlikely to cause rounding errors, and certainly not as quickly as I'm showing.  It doesn't matter.  Even with careful coding, the fact that you can't distinguish an external notification from user generated input is a huge deal.

If you could differentiate between notifications generated by your application from notifications generated by someone else's application, this problem would go away - you simply update your UX on external notifications and you throw away the notifications generated by your UX changes.  That cuts the loop off after the first notification is received.

When people realize that this problem exists, their first suggestion is to say "That's stupid - why does the system tell you about changes made by your process?  The system should be smart enough to figure this out and simply not tell the process about its own changes".  Unfortunately what the users consider an "application" doesn't neatly fit into the windows process model.  But what do you do about applications like the shell or the sidebar?  They host 3rd party code - "Applications" as far as the user is concerned. If the system filtered notifications by process, what would happen you had two different volume control gadgets in the sidebar?  It would be "bad" if the system didn't tell one of them about changes made by the other.

For the volume logic, we chose to solve the problem slightly differently.  If you remember, the OSD application's call to VolumeStepUp specified a NULL parameter.  That parameter is known as the "Event Context" - every volume related "Set" operation takes one.  The system passes this EventContext value through the system all the way to the notification handler for volume changes.  An application can specify whatever value it wants in the EventContext and when it receives a notification, it can check the EventContext value to see if the application generated the notification.

 

I'm not going to show the code changes for this - basically you define a GUID and change the VolumeStepUp (and VolumeStepDown) calls to specify this GUID for the event context, and then in the OnNotify method of the CVolumeNotification, if the EventContext member value of the NotificationData parameter matches the GUID, return without updating the UX.

  • Of course your volume is 0wned.  0 is the most round number.

  • Is this the same kind of issue that caused the Vista speech recognition demo to go haywire?

    http://blogs.msdn.com/larryosterman/archive/2006/07/31/684327.aspx

    "It turns out that one of the common causes of feedback loops in software is a concurrency issue with notifications - a notification is received with new data, which updates a value, updating the value causes a new notification to be generated, which updates a value, updating the value causes a new notification, and so-on..."

    ...or is it the same kind of problem (infinite notification loop) in a different context altogether? (i.e. there wasn't any signal being fed back, just volume change notifications)

    - Oli

  • olidag, it's a similar problem, but slightly different :) .

  • Imagine what would happen if two applications both tried to do it this way (like your sidebar example)? An update in either would then cause them both to begin an endless cycle of setting volume and notifications.

  • Colour me silly, but if you have three volume sliders and the user adjusts one, then surely the other two should update their position to reflect the system value without sending any more updates, right?

    If all three just don't send notifications after updating the value due to a notification, then the problem is dead in its tracks. The first must still handle its own notifications, of course, to avoid its own spiral.

    If the user updates slider A and then sliders A, B and C get the notification and both B and C (but not the originating sender) both send updates that the other two get, I see an infinite loop popping up.

    Or is that what the other anonymous guy meant?  :)

    This may be a nitpicker's corner moment, but if I've missed something obvious it would be good to know, and if this is a demonstration of a very important detail (filtering out of events) then it's a good idea to call out that it's not production-ready due to issues like this.

    Inevitably, someone is going to miss this detail and assume that the OS can defend against all possible problems. That would be nice, but mathematically implausible.

  • > This works great IF there's no rounding in the

    > system.  But we're dealing with floating point

    > integers here.  

    Nit-picking I know, but what is a "floating point integer?"

  • 0yeah, 0 is the most rounded integer, 0.0 is the most rounded floating point, and 0.0 rounds to the most

    <stack overflow>

  • I've got to admit, this approach does seem to fall over at the "What if two programs did this?" stage. Wouldn't a better approach to use standard floating point comparison techniques to just check if the numbers are "close enough"? i.e. abs(a-b) < tolerance

  • Andy, it's not sufficient - even if you eliminate rounding errors, there are still issues that cause you to require that you filter out notifications that you caused.

  • Couldn't you just set the wndProc to unregister to receive those windows messages while it's in that message handler routine (and reregister at the end).  This a fairly common need to do this for event code:

    abc -= Event;

    ...

    abc += Event;

    And signals in the unix world.

    Not sure what the correct win32 api is (SetWindowLong() to change the WndProc?) but I'm pretty sure it's not too difficult for a custom control.

  • anonymous: wndProc?  The notifications for volume change have to work cross sessions (since you're dealing with hardware) and window messages can't cross sessions.

  • Why use floating point for audio when fixed point is known to be superior?

    Another question is how are those notifications handled now?

  • Igor, floating point is actualy superior.  I was shocked when I learned that the audio pipeline was a floating point pipeline, until I talked to the guys who architected the pipeline.

    First off, with 32bit floating point numbers, you get 24bits worth of lossless calculations (in other words, the mantissa is 24bits wide).  That means that if I take a 16 bit sample, convert it to float, I've got 8 more bits of lossless precision to work with.

    Secondly (and more importantly), most of the algorithms for performng DSP on audio samples assume floating point values - if we were to use a fixed point algorithm, it would introduce rounding errors (unless we reinvented floating point math).

Page 1 of 1 (13 items)