Saturday, January 30, 2016

Byte-playing, a new project to doodle on

So. Been awhile.

Going to introduce a different project I've been messing with for a few weeks: byteplay3.

More on that in a minute. First the status of old projects.

PPQT2 continues to have one or two regular users, and a minor UI issue was posted a couple of weeks ago. No real bugs found; is that good news? It says something about the code quality; but I suspect it says more about how nobody is using it. I would be a regular user, if I were still doing PGDP Post-Processing. Unfortunately, the switch to EPUB with its unavoidable compromises on book quality has killed my interest in that. So I'm not PP'ing any more.

Anyway, I want to fix that UI issue and bring it up to the latest levels of its dependencies. Qt 5.6 is due out shortly, with PyQt5.6 to follow soon after. So when I can install those levels, I'll do the code update to the Find panel and rebuild on all platforms. That will probably be the end of PPQT2. Well, it was a most satisfying hobby project to design and build.

I remain a daily user of CoBro, and it definitely needs to be refreshed. It embeds the Qt WebEngine. Recently a couple of the comics I read have stopped loading, I think because they are insisting on a higher or different level of https encryption than this old WebEngine module supports. So I will also rebuild CoBro with Py/Qt5.6 and, one hopes, it will stop giving an obscure error when it tries to load Penny Arcade.

But that's all to do in a couple of months, whenever the Py/Qt upgrade happens.

The new project, byteplay3, needs to be at Python 3.5. I've been making do with 3.4 for a while, and there's no function in 3.5 that would benefit PPQT, but for byteplay I need to test against async coroutines and such.

Byteplay

The original byteplay, written by Noam Raph, was uploaded to PyPi in 2010. Briefly, the point of byteplay is to make it easier to diddle with the bytecodes generated by the Python compiler. You, gentle reader, are a highly competent Python user, so I will only show the meat of the example code. I'm sure you can figure out what's going on.

>>> def f(a, b):
...   print(a, b)
...
>>> f(3, 5)
    3 5
>>> from byteplay3 import *
>>> # convert code object of function f to a Code object
>>> c = Code.from_code(f.__code__)
>>> c
    <byteplay3.Code object at 0x1030da3c8>
>>> print(c.code)
2        1 LOAD_GLOBAL          print
         2 LOAD_FAST            a
         3 LOAD_FAST            b
         4 CALL_FUNCTION        2
         5 POP_TOP              
         6 LOAD_CONST           None
         7 RETURN_VALUE         
>>> c.code[4:4] = [(ROT_TWO,None)]
>>> f.__code__ = c.to_code()
>>> f(3,5)
    5 3

Short intro follows. If you understood perfectly what happened in that demo, skip ahead. Or for a longer explanation see the detailed "about" page that I've made (starting from Raph's original).

When Python processes a def statement or lambda or compile() expression, it compiles the source text into an internal form. The heart (though by no means all) of the internal form is a byte array, a string of "bytecodes" which represent machine instructions for a simple stack machine. The bytecode representation is binary and designed first for speed of execution and second for compact storage. Readability and post-compile editing were not goals of that design.

The standard module dis will display the bytecode of a function or compiled expression in much the same format as shown in the example. In fact, dis of Python 3.5 has a nice facility that lets you read a bytecode stream one instruction at a time from an iterator. (I think this preempts a couple of the usual uses of byteplay. But it still has some value.)

That takes care of displaying the bytecode of a function. But what if you want to modify it? In the preceding example, a ROT_TWO instruction is inserted in the sequence and that changes the function's behavior. More realistically, there are many opportunities for peephole optimizations, where you look for inefficient code sequences and shorten them. Ryan Kelly made the promise package based on the original byteplay. It provides decorators that optimize certain code sequences of your functions.

Or, I can imagine wanting to generate a bytecode sequence starting with some other notation. You could design some little Domain-Specific Language, and compile it down to bytecodes, and call it from a Python function. I'm considering doing a demo of byteplay3 in which I implement, say, Tiny Basic by compiling it into bytecodes. Python bytecode: the poor man's LLVM!

Kelly's promise module, and the original byteplay, are firmly dependent on Python 2. I thought it would be fun to bring byteplay, and maybe promise, into the world of Python 3. And that's what I've been doing in the odd spare hour for the past month or so.

This is long enough; I'll delve into some of what I've done next time.

Meanwhile ... if Noam Raph is out there? I'd love to talk! You aren't on Facebook or LinkedIn nor a user on github...

Monday, December 28, 2015

Holiday in the Endless Sky

So for fun I've been playing Endless Sky. It's a nicely made re-implementation of the old EV Nova (which appears to be still available, although I'm not sure it would run in a current Mac OS).

Part of the early grind of Endless Sky, as it was for EV Nova, is to haul commodities from star to star, buying low and selling high, until you accumulate enough credits to buy a better ship. Each star has prices for ten different commodity types, and one of the first questions the player asks is, what route has the highest profit? (In EV Nova, there was one fabulously profitable one-hop route, the only problem being that you were almost guaranteed to be attacked by pirates every time through it.)

Endless Sky's galaxy is defined in a simple text file that is part of the game package (here's the source). I looked at it and thought, hmmm. The galaxy is an undirected graph; the commodity prices are properties of the nodes of the graph. This looks like a job for Python! So one morning I sat down in and in less than two hours, I had code to read and store the galaxy as a graph, and it worked first time! What follows is a slightly upgraded version, so it has about 2:30 invested in it.

import sys
try:
    filename = sys.argv[1]
    map_file = open( filename, 'rt', encoding='utf-8', errors='ignore' )
except:
    print('usage: mapper <file path to the map file>')
    exit(-1)

# map trade names to indexes in a system trade list
trade_indices = {
    "Clothing" : 0,
    "Electronics" : 1,
    "Equipment" : 2,
    "Food" : 3,
    "Heavy Metals" : 4,
    "Industrial" : 5,
    "Luxury Goods" : 6,
    "Medical" : 7,
    "Metal" : 8,
    "Plastic" : 9
}
trade_names = { d : n for n, d in trade_indices.items() }

# The galaxy is a dict of { 'system-name' : System } It is an undirected
# graph in which the edges are system-names in each system's links:
#    for neighbor_name in Galaxy['some-name'].links:
#        neighbor_system = Galaxy[neighbor_name]

Galaxy = dict()

class System( object ):
    def __init__( self ):
        self.trade = [0] * 10 # prices of 10 commodity types here
        self.links = set() # names of connected systems
        self.habitable = False # can trade here?

import regex
# The following matches to a pattern of either
#   verb namestring [nnn]
# or
#   verb "name string with spaces" [nnn]

rx_verb = regex.compile( '''^\s*(\w+)\s+((\w+)|['"]([\w\s]+)['"])(\s*\d+)?\s*$''', flags=regex.IGNORECASE )

# If the current line is "verb name [nnn]" return (verb, namestring, [nnn|None]).
# Otherwise return (None, None, None)

def match_line( line ):
    match = rx_verb.match( line )
    verb = None
    namestring = None
    number = None
    if match is not None :
        verb = match.group(1)
        namestring = match.group(3) if match.group(4) is None else match.group(4)
        number = match.group(5)
    return (verb, namestring, number)

# Read the map, build the Galaxy

in_system = False

for line in map_file :

    ( verb, name_string, number ) = match_line( line )

    if not in_system :
        if verb == 'system' :
            # Beginning a system block. Create a system and file it.
            new_system = System()
            Galaxy[name_string] = new_system
            in_system = True
            habitable = False
            gov_not_Hai = True
        continue

    # Process "link", "trade", "government" and "object" statements
    if verb == 'link' :
        # new_system links to name_string. Note the map has reciprocal links,
        # we do not need to add a reverse link. In fact we couldn't, because
        # name_string may not be in the galaxy yet.
        new_system.links.add( name_string )
        continue

    if verb == 'trade' :
        # new system trade line found, store the price in the system list
        commodity_index = trade_indices[ name_string ]
        commodity_price = int( number )
        new_system.trade[ commodity_index ] = commodity_price
        continue

    # Check "object name" statements. The regex only matches to "object
    # name", not to the more common "object" statements. If a system has an
    # object with a name, it is habitable. Except for Algiebra, which has
    # a named moon but cannot be used for trade.
    if verb == 'object' :
        habitable = 'Watcher' != name_string #anywhere but Algiebra
        continue

    # Look for government starting with Hai, because those systems are not
    # accessible until later in the game (this could be conditioned on a
    # command-line option)
    if verb == 'government' :
        gov_not_Hai = not name_string.startswith( 'Hai' )

    # if the current line is blank, we are at the end of this system.
    if 0 == len(line.strip()) :
        new_system.habitable = habitable and gov_not_Hai
        in_system = False
        continue

    # Note the official map *ALWAYS* has a blank line at the end of a
    # system block i.e. preceding a 'system' statement. If the map file
    # is mis-edited to not have such a blank, this code will merge the
    # trade etc. lines from the following system to the previous one.

# All map lines processed, the Galaxy is built.

With the galaxy as a graph, it took another hour to brute-force the best trade route of one, two, three or four hops. No longer route because the starter ship and the typical freighter can't go more than four hops without landing.

# Return the set of all system names that are n hops out from the given
# system.
def n_hop_targets( system, n ):
    if n == 1 :
        return set( system.links )
    all_targets = set()
    for target in system.links :
        all_targets |= n_hop_targets( Galaxy[ target ], n-1 )
    return all_targets

# Compare the trade price lists from two systems. Return the index
# of the most profitable commodity and the profit margin.
def compare_prices( buy_list, sell_list ):
    profits = [ buy-sell for buy, sell in zip( buy_list, sell_list ) ]
    p = max( profits )
    return profits.index(p), p

# Find the most profitable trade route between a given system and any of a
# set of systems it can reach at some number of hops. Input is the system
# itself, and set of target names.
# return the system name and (outbound buy, profit, inbound buy, profit)

def best_trade_route( system, target_set ):
    home_line = system.trade
    out_p = in_p = 0 # outbound and inbound profits
    out_c = in_c = None # outbound, inbound indexes
    where = None # target system
    for dest_name in target_set :
        dest_sys = Galaxy[ dest_name ]
        if dest_sys.habitable :
            dest_line = dest_sys.trade
            d_c, d_p = compare_prices( home_line, dest_line )
            h_c, h_p = compare_prices( dest_line, home_line )
            if (out_p + in_p) < (d_p + h_p) :
                out_p, out_c = d_p, d_c
                in_p, in_c   = h_p, h_c
                where = dest_name
    return (where, out_c, out_p, in_c, in_p )

def best_n_hop(n):
    best_name = None # name of winning start point
    best_targ = None # name of its trade partner
    best_info = (0, 0, 0, 0)
    best_rt = 0 # round-trip profit
    pdict = dict()
    for name, system in Galaxy.items() :
        if system.habitable :
            where, out_c, out_p, in_c, in_p = best_trade_route(
                system,
                n_hop_targets( system, n )
                )
            if (out_p + in_p) > best_rt :
                best_name = name
                best_targ = where
                best_info = ( out_c, out_p, in_c, in_p )
                best_rt = out_p + in_p
                pdict[best_rt] = (best_name,best_targ,best_info)
    print( '\n\nBest', n, 'hop trade route is' )
    print( best_name, 'to', best_targ )
    print( 'outbound take', trade_names[ best_info[0] ], 'earning', best_info[1] )
    print( 'inbound bring', trade_names[ best_info[2] ], 'earning', best_info[3] )
    print( 'round-trip profit is', best_rt )

best_n_hop(1)
best_n_hop(2)
best_n_hop(3)
best_n_hop(4)

So before lunch I knew that I should trade Metals and Luxury Goods between Alphard and Delta Velorum. I took a certain amount of pleasure in having all that work quickly and easily.

My pleasure was rather lessened when I googled "Endless Sky best trade route" and found a forum post with exactly that route, found by some non-programmer weeks ago.

Oh, well.

Friday, October 23, 2015

A Mathematical Basis for Karma

So I've been auditing Jordan Peterson's course "Personality and Its Transformations". Peterson is a wonderful lecturer and while he sometimes drives off into the weeds of multiple digressions, he often just lights up one's skull with insights. (see note below)

Here is a concept that he tossed off in a couple of sentences, just a throw-away line really, around the 59:00 point in Lecture 14. He says,

I also don't think that the connections between people and the society are as abstract and distant as we think, because you might think, well, what the hell difference could it possibly make, you know, the way I behave? Well, you're a node in a network. You're not an individual connected by a linear line to another individual connected by a linear line to another individual, in a line that's seven billion people long. That would make you nothing: just pull you out and the line would reclose, and that would be the end of that. You're a node in a network, and the network's communicating. And we know for example, that you are roughly going to interact with, in some serious way, a thousand people in your lifetime as a minimum, minimum estimate. So, and all those people know a thousand people, so that's a million people that are one person away from you, and two people away from you is a billion people, and as soon as you get to three, well, that's far more people than there are. So, you know, you are only three or four or five connections away from everyone. And so it is very very difficult to know exactly how your behaviors and misbehaviors echo and ripple. And we know that people can be tremendous forces for good; we know that because you see people like that from time to time; and we certainly know plenty about the reverse. So God only knows what role you play in determining, you know, whether the part and the whole of mankind goes seriously wrong or seriously right.

So, he's Canadian, you know? Well, Canadian quirks aside, my mind was caught by that casual remark that you or I will influence at least 1000 people, and they 1000, etc. Which means, one's influence spreads by a power law. What of one's influence, how would it fall off? To work it out formally, let

  • E be the total of your influence on one other person
  • K be the number of persons you influence in each period Y
  • N be a number of periods

The value of Y can vary; Peterson was talking about your whole lifetime, but it could stand for one year without changing anything.

The number of people your influence reaches is KN. Say you influence K people in one year, they reflect or echo your influence on K people each in the next year, that's K2, K3 in the second year, and so on. This rapidly becomes a large number, as exponential functions are wont to do.

Meanwhile, the effect you have on people is being diluted as EN. Now this is like, but unlike, an epidemiological simulation. Simulating an epidemic, a node in the network is either infected or not infected, and a simulation of an epidemic has little nodes changing from green to red with no in-between. But one person's influence E on another person is only fractional; when you express an opinion, or behave in some way, another person will adopt that opinion or echo that behavior only weakly or with low probability. So perhaps E is a small fraction, 0.01 maybe, i.e. 1 person in a hundred will actually do exactly as you did or said. (It would really be a family of values, one for each kind of influence you might have; the influence of your speech patterns with one value, your habit of kicking puppies another value, your clothing choices another and so on ad infinitum; all relatively weak yet nonzero.)

The point is, your effect on others is EN, 0.01 on the people you interact with, but 0.012=0.0001 on the K2 people they influence, and so on. It gets smaller, but it never goes to zero—and also, it reaches a whole bunch of people.

In another essay, I wrote something to the effect that you could think of your actions as "seeding your world" with good things or bad things, with health or illness, calm or anger. Here is a mathematical basis for that, and (I think) the real basis for the Buddhist notion of karma.


Note from 2018: In recent times, Jordan Peterson has emerged as a rather distasteful public personality, much criticized by people whose opinions I respect. But I don't think this more recent criticism invalidates the very particular idea that I quote above.

Tuesday, August 18, 2015

Ppgen translator done, some bugs found

I finished the ppgen translator this afternoon. In order to verify it works I had to download and run the ppgen program itself, which proved quite simple. You just go get it from its github page, a big single Python module, and run it. I started to read it but decided instead to treat it as a black box. While I respect the effort that RFrank continually pours into supporting it, there are things about its design, and its documentation, that irk me as a professional writer and programmer. So if I start to read it I will just be picking nits and thinking of how it should be done, and that's unproductive of my time. Worse, I could sucked into helping maintain it. Run away!

Anyway, during the testing I found some things that couldn't be accounted for in my Translator code (which turned out to be pretty simple, less than 300 lines with lots of comments). Investigation led to two small bugs in the PPQT Translator support itself. There was one logic error that resulted in generating a spurious blank line preceding any no-reflow section. I am not sure why I never noticed that until these tests.

The other had to do with the YAPP-generated document parser. The way it was written, the following perfectly normal input,

...end of a paragraph.

<tb>

Start of next paragraph...

was wrongly parsed as if the second paragraph was a section head. In the DP document format a section head is marked by two preceding empty lines. There was only one preceding blank line here, why was it being parsed as a head?

Every production (other than the thought-break, which was a late addition to the parser) ended with EMPTY? to absorb any empty line that followed them. For example, a no-reflow section was defined as XOPEN (LINE | EMPTY)* XCLOSE EMPTY?. So if the user wrote

/X
stuff...
X/

New paragraph...

the blank line after the X/ line would be absorbed into the NOFLOW section. Because of my doing that in all cases, the syntax of a HEAD3 was just EMPTY PARA, or one empty line and a paragraph. When I belatedly remembered the thought-break markup and added it, I forgot to define it as absorbing an optional empty line after it. That was easy to add, a two-line fix. With the other bug, a total of 4 or 5 lines changed. But that mandates repackaging the whole app again, sigh. Although that isn't really so bad, a few hours of work, most of which is spent waiting for files to upload to or download from the dropbox, so I can be doing other things.

That will be Thursday. Before I do it I'll review the issues list, there may be a couple of other easy fixes I should do.

Saturday, August 15, 2015

Translators: Updating HTML, adding PPgen

By PPQT2 to the Woodshed

I used PPQT2 to post-process quite a bulky project, Hawkins Electrical Guide Vol. 3. This is the kind of PP project I've always enjoyed, with varied document structure (not just chapters of paragraphs) and many images. In this case, over 200 images, on which I spent many hours in Photoshop making clear, clean yet very compact .png files.

I had been working on this book ever since PPQT2 was at all usable, a year ago or so. After finishing the ASCII and HTML translators, I could finalize this book using PPQT2. Which I did, and uploaded it, and ran into an eagle-eyed and extremely conscientious PPVer, who kicked it back to me with a list of over 50 issues to correct. Properly taken to the woodshed, I was!

Some of the issues related to the generated HTML, and in the process of correcting them I realized some ways in which the HTML translator could do a better job. Also I had spent some time absorbing the various EPUB advice pages in the DP Wiki, and realized the impact that EPUB has on the post-processor's view of HTML.

EPUB Rant

Parenthetically, DP has a confused relationship to EPUB. Project Gutenberg now routinely does a batch conversion of the submitted HTML book using something called EPUBmaker, and it does a number on one's HTML. In prior years I, like many PPers, have spent lots of time on tweaking the HTML to make the ebook look very much like the printed book. But Epubmaker ruthlessly throws away most of that, leaving a flat, boring, ugly etext.

Double-parenthetically, part of the problem is the many restrictions of the EPUB format itself. It doesn't allow floats—so forget about sidebars, side-notes, and running text around small images. It doesn't support pop-up title=texts when you hover the mouse on an element—so forget about showing the original spelling of a typo, or showing the transliteration of a Greek or Cyrillic word. It imposes ridiculous constraints on images; nothing wider than 600px and no image files larger than 200K. Like other stupidly-designed standards, it takes the historical limitations of the ebook readers of 2005 and codifies them for all time. Do you think a retina iPad can't display an image larger than 600px? Or a Kindle Fire? The EPUB standard is very much like the many state laws that codified the design of auto headlights in the 1950s, based on the then state of the art, the sealed-beam unit. So when European cars started using replaceable halogen bulbs, they could not be imported to the U.S. because their headlights were not sealed-beam units. It took decades to get the laws changed so imported cars didn't have to have inferior U.S. headlight units retrofitted before they could be sold. EPUB does exactly the same thing, locking us into an already-outmoded technology. Close inner parenthesis.

DP's response to EPUB has been scattered and slow. There are several different Wiki pages about it, giving conflicting advice and often referring to forum threads that are years old. But the bottom line is, the PPer today who spends any time on how the HTML looks is wasting her energy. The majority of PG downloads are for EPUB, not HTML, and all your pretty CSS will be stripped out by Epubmaker. Close outer parenthesis!

HTML changes

With all this in mind, I went back to the HTML translator and made changes. I simplified the CSS in the header block a lot, removing many options and comments on appearance. I changed the method of encoding visible page numbers from the Guiguts method to a method that was recommended in one of the EPUB Wiki pages, as possibly able to survive Epubmaker.

Another change was from percentage widths to fixed widths. PPQT lets the user specify margins in ASCII space units, for example /Q First:6 Left:4 Right:4. These translate nicely in the ASCII output. But for HTML, I had been converting them to percentages of a 75-character line, so that Right:4 became margin-right:5%. But percent widths are relative to the container, so 5% is less in a nested container than at the outer level.

There was already a historical conversion of 2 ASCII spaces == 1 HTML "em" unit; this had been in use for poetry line indents for years in the Guiguts HTML conversion, and my HTML Translator did the same thing for poetry. Well, why not for all widths? So I changed it to use em units for everything, and Right:4 becomes margin-right:2em; which is the same regardless of context.

Ppgen

The afternoon after I posted the updated HTML Translator I was congratulating myself on the PPQT2 design that makes the Translators into separate files, and how easy it was to update just that file without having to repackage the whole app. And then about how nobody has expressed any interest in doing any other Translator. And how there really ought to be a Ppgen one.

I've had a Chrome window open for months, with about six tabs open pointing to different Ppgen docs. (Which, parenthetically, are badly organized and incomplete.) Well, crap, I said to myself, let's see how hard it would be. I pulled up a copy of my skeleton Translator file and started filling in the 30-odd entries in the "computed go-to" list of API "events". And it went very well. A majority of events are either null, or can be handled by a single literal string without any functional logic. For example, the OPEN_H2 event just squirts out .h2.

By end of the day I had almost all of it coded, lacking only the table-related events, and I pretty well see how to implement them.

So early next week I reckon I will be able to announce a trial Ppgen Translator. I'll have to hedge the announcement with many caveats, mostly because I do not have the actual Ppgen batch tools installed so I can't actually test that my translation produces usable output. But if people don't like it, they can fix it. It's just a small Python source file; be my guest.

And when that's finalized, people will be able use PPQT2 to complete Ppgen-based projects. Which might increase adoption.

Saturday, August 1, 2015

What to do, where to go next?

I've used this blog with the very clever name (well, perhaps not so clever these days, since nobody uses paper manuals any more, and if you've never seen a software manual on printed paper, you might not get the reference) -- used it, I say, to document whatever enthusiasm is monopolizing my attention. Before 2010, I used it as a place to store occasional essays on whatever was bubbling around in my brain. (Here's a really well-written piece, if I do say so myself, from 2009, on The Too-Small God. Here are actual numbers on the economics of a plug-in hybrid car.) For several months in 2010 I used it to record the process of rebuilding my recumbent bike. For the last two years I've used it as a diary as I developed PPQT2.

Well, PPQT2 is pretty well done now. There are several issues still on the github page, some of which would require significant days of effort to close. But I don't feel any urgency to do that work. The existing app is adequate for my personal needs. The "user community" aside from me can be numbered on one hand, I think; and they are very quiet.

So: whence the blog? Probably it will be very quiet for a while. If you have been following it for the PyQt5 stuff, thank you for reading along! I hope you got something useful from it. I don't expect to be doing much with PyQt now, but if I do, I'll post about it. So I suppose you should keep it in your RSS reader. Just move it down to the bottom of the list, next to those other blogs that you used to follow but which have gone quiet of late.

(I have several like that in my RSS reader. You know, that could make an interesting blog post...)

Thursday, July 23, 2015

Audio discoveries and problems

So I thought I would package Sidetone using PyInstaller, the latest version of which works so well with PPQT2. But strange things happen in the bundled version. The call to QAudioDeviceInfo.availableDevices(), which works perfectly running from source, returns an empty list to the bundled app. So both comboboxes are empty. Very appropriately, the empty comboboxes never generate a currentIndexChange signal, so the app never does anything (alos very appropriate).

I added code that, when the available list comes back empty, would get a one-item list of the device returned by QAudioDeviceInfo.defaultInputDevice() or QAudioDeviceInfo.defaultOutputDevice(). Because, the Qt docs assure me, "All platform and audio plugin implementations provide a default audio device to use." Which they do, but the devices being returned to the bundled app are invalid devices. They are QAudioInput or QAudioOutput objects, but they also return True from the .null() method, and when the app tries to start them, it generates a stderr message about trying to use a null device.

The code continues to run from source, but with this glitch. On my laptop—which is running the same levels of Mac OS, Python, Qt, and PyQt—when I unplug the USB headset, the app automatically switched to the built-in mic and speaker, and began an entertaining feedback warble. So I thought, OK, there must be some signal, some indication that a USB audio device has gone away. What is it?

But back on the desktop system, where I am doing the coding, things are different. There, when I pull the USB plug out, the app purrs on as if nothing had happened. I added code to intercept the stateChanged signal from the active devices and print the state. It goes from 3 (idle) to 0 (active) and stays there happily after the plug is pulled. And the system doesn't switch to the built-in devices. It is possible to select the built-in devices in Sidetone, and produce quite remarkable feedback effects, but it doesn't happen automatically on the desktop system.

I thought, OK, I'm plugging the headset into a USB hub. What if I put it directly into the back of the iMac? Something did change: the sound developed that "picket-fence" rattle indicating buffer under-run. I had to put the buffer size back up to 512 to eliminate it. Just speculating; the built-in USB delivers data faster than when the headset is on a hub, connected to the built-in hub? Don't care.

What did not change when I plugged into the built-in hub was the behavior when I pulled the plug out. No state change.

So I'm a kind of baffled on two points. One, how to know when the user yanks the plug on the device being used; and two, what is different about a bundled app than one running from source.

Neither is really important to my intended use (personal and casual). I'm cool running from source and I don't expect to be yanking the plug out in normal use. But I'd like to know. If you have any idea, please jump in with a comment.