Sunday, March 22, 2009

One Knob, One Function, and Other Good Analogue Interfaces


When designing user interfaces I usually like to refer back to the analogue equivalent (if one exists) to see how they implemented their user interface. I've found that overall analogue audio electronics have far better user interfaces then their digital counterparts. I'm not just referring to software, indeed most consumer electronics (and some professional devices too) have hundreds of features all unhelpfully buried in several sub-menus.

I`ve used, and trained others to use a lot of different applications, and I`ve found that the most successful interfaces always seem to have one thing in common: one knob for one function. You turn this knob and it increases the volume, turn that one and it gives you more bass. Simple right? This was usually forced by the limitations of the electronics themselves. The circuitry to multiplex a control would cost more than just adding another knob. Of course knobs and screen real-estate are not infinite, so with the advent of digital electronics (and therefore cheap input multiplexing) designers found they could scale back costs by using one knob to serve several different features. It is this design practice that most software vendors have tried to duplicate.

In moderation this approach can work well, but when taken to the extreme as on my Tascam TM-D1000 mixer, or the average application it makes it almost impossible to use in any situation where a reasonable response time is required. Response time is not the only drawback, after enough burying it becomes very difficult to remember which sub-menu the setting or feature you want is located in.

Office 2007 got this right in my opinion with the introduction of the ribbon. On it, there's very few sub-menus and most popular features have gotten their own buttons somewhere. You don't have to wonder if the button you're clicking will do what you want or something else, because each element is tied to a specific function. Most importantly, unlike traditional software menus, the sub-menu selectors and items themselves are visually distinct.

This concept of visual distinction is another throw-back to the analogue day's gone by. In most audio receivers of the past your volume knob would usually be bigger than all the rest. Some vendors took it further and added stylistic differences to knobs and buttons in different categories. This distinction was not merely aesthetically pleasing. It helped you to find the right feature when you needed it. This has a nice analogue to the software world with button icons. Good button icons can explain a feature far faster than any tool tip ever could.

At this point you may be thinking that main reason analogue devices had such simple interfaces was due to a dearth of features. This may be true with consumer electronics, but take the example of a large analogue mixer such as this one. It may be analogue but its certainly not lacking a quantity of settings. Yet in practice devices such as these are easy to use, as each knob is colour coded according to its function and is not shared with any other function.

So if you want your application to be useable, please consider the analogue equivalent (or at least these design principles). Your users will (subconsciously) thank you.

HTML Global IDs Considered Harmful


This is a repost of a discussion I had with a few friends as we worked on a new site. This site in particular used AJAX and Javascript to draw a colourized table of articles and summaries and so the JavaScript code itself needed to reference a lot of objects. My friend was arguing that we should give each cell its own ID so we could reference it later with getElementByID. I decided it was worth further discussion so I'm reposting it here. I've only editted it enough to keep its meaning clear.

Ok so I figured I would write this post to explain how keeping track of story cells works in the JS code I’ve written. Its very important you understand this so you don’t go about putting global IDs everywhere. Thats the equivalent to putting everything in global variables in your C or Java program. Sure it seems like it makes life easier at first… But, its a terrible idea for all the reasons its terrible in those languages. Its even more terrible because in JavaScript a getElementById requires a full search of the DOM tree*, this is ridiculously slow and because we are javascript intensive, it will be noticeable to our users.

So lets dive in to how we do this without those nasty global IDs. The story cells are created and filled dynamically through javascript. look at this code:

//create and add new elems
77 var newHref = document.createElement(“a”);
78 var newDiv = document.createElement(“div”);
79 var ximg = document.createElement(“img”);
80 newHref.href = this.url;
81 cell.appendChild(newHref);
82 newHref.appendChild(newDiv);
83 newDiv.appendChild(ximg);

You can do most of your modifications right then and there, at the time of creation. But what happens if you need to do it later, say after an event handler triggers?

You add a reference to the cell (or div) to the object which will also own the handler that needs it. you can do it easily, e.g.:

(DOM object).storyDiv = newDiv;

now in your event handler you just refer to it as this.storyDiv, and you can do whatever you want.

*There are exceptions to not using getElementById, such as operations on explicitly created elements in our HTML code, but you shoud still try and minimize your use of this function.

Saturday, July 05, 2008

Why Nyquist Isn't Enough

There's been a debate going on about the future of digital audio. One group, lets call them the audiophiles, wants 24-bit 96Khz digital audio. The other group, lets call them, the average Joe's is perfectly happy with current CD quality (16-bit 44.1Khz). Which group is right? The diplomatic answer is of course that each person should buy whatever they think is right for them, but that doesn't make for a very interesting read. So lets consider if increasing the sample rate from 44.1Khz to 96Khz really buys us anything. (We can tackle the bit-depth in a later post)

If you've done any work at all with digital audio you probably know about the Nyquist Theory (which is actually short for the Nyquist–Shannon sampling theorem). For those that don't know, the layman's version is: To correctly reproduce a frequency of n Hz, you must sample it at 2n times per second.

Using this theory, our CD is technically capable of reproducing sounds up to 22.05Khz. While our 96Khz gear should be able to reproduce frequencies up to 48Khz. We tend to think of higher numbers being better, but the human ear can only hear frequencies up to about 20Khz. So sampling at 96Khz is obviously a waste if we can't hear those extra frequencies it records, right?

Well not quite. It is true that sampling at 96Khz won't let us hear any more tone's than our CD. But we haven't considered just how accurate our CD is playing those frequencies. To understand why this is the case you have to carefully consider what the Nyquist theory is actually saying. The purpose of the Nyquist theory was to determine how frequent you need to sample in order to prevent aliasing. Aliasing happens when we don't have enough information to fully capture a signal and as a result it gets recorded is multiple lower frequencies. You've probably seen this effect in movies where a fast spinning wheel appears to be rotating slowly backwards. This is caused by the camera not taking enough images to fully capture the speed at which the wheel is spinning.

So while the Nyquist theorem guarantees that any frequencies below half the sampling rate will not be aliased, it doesn't guarantee that all frequencies will be recorded with accurate volume. To illustrate this fact consider the pictures below:


On the left is a signal sampled at the nyquist frequency at the most ideal time. It will result in a perfect reproduction when played back. The next image is the same signal, sampled at the same frequency but at a point offset from the ideal position at the peaks. This signal will be played back at half the amplitude as the ideal. Frequencies significantly below the upper Nyquist limit don't suffer as much from this problem as they are sampled so often that any errors tend to average out.

This distortion isn't much of a problem if you only care about one frequency. You can simply increase the volume to compensate. But in the case of multiple frequencies being recorded simultaneously (almost all music, speech, and sound) it will distort the volume of the higher frequencies with respect to the lower ones. Not a good thing if you want accurate reproduction.

Whether the distortion caused by this effect matters or not is up to the listener and their ears. But if you work with audio it is important to remember that the Nyquist Theory isn't always enough.

Wednesday, July 02, 2008

Malloc Is Slow

Your program is running slow. You've followed all the optimization best practices; you're programming is using only the most efficient algorithms. Yet performance is still unacceptable. If you've tried everything and still can't get performance under control, perhaps you should take a closer look at how you're allocating memory.

Many people don't know it, but used incorrectly malloc() can inflict a serious performance penalty. The fact is, malloc() is not a deterministic function. A call could return instantly or take many milliseconds depending on the current state of the machine, and there’s no way to tell in advance just how fast or slow one particular call will be.

Problems can happen fairly easily: a function inside a performance sensitive loop needs a buffer of variable size. Given the situation a naïve programmer might write their function like this:
void performaceCriticleFunction(int *pIn, int cItems, …)
{
int *pbuf = malloc(sizeof(int) * cItems);
//Code goes here!
free(pbuf);
}
At first glance the code looks fine, however; consider the case of a heavily loaded machine starved for RAM. Lacking free memory to commit to our process, the operating system will have to swap something to disk. BAM! Our function's execution time has just increased by several orders of a magnitude. In this case, it may take our operating system several milliseconds or more to complete our memory allocation. If we were a multimedia application, we’ve probably just caused a buffer underflow.

The Recycle Method


So what can we do to speed up our performance critical function? Well the first method is to recycle. Perhaps we could allocate our buffer the first time the function is called and re-use the same buffer for all subsequent calls. This technique will solve our problem, but it introduces a number of issues:
  • Increased complexity
  • Thread safety
  • Malloc() must still be called at least once
Despite these issues, if your buffer is more than a few kilobytes in size this approach will likely be the best choice. Otherwise you may want to read on, as the next solution will often give better results.

The Stack Allocation Method

You may not know it, but there is a standard way to allocate variable sized arrays on the stack. Buffers allocated on the stack can be created very quickly, in fact it only requires adding the buffer’s size to the stack pointer. Stack allocation is accomplished through the alloca() function, which works in a similar manner as malloc(); The big exception being that memory is taken from the stack rather than the heap. This means that, as with other local variables, all memory will be automatically free()'d for you as part of the function return process.

Unfortunately alloca() is not a direct replacement for malloc(). There are a number of caveats you must consider:
  1. Your buffer will be scoped to the current function. You may pass references to functions called from within the allocating function, but references must never be returned.

  2. Stack space is limited to approximately 1MB by the Visual C compiler, carefully consider if your function’s use of alloca() will result in a stack overflow.

  3. Memory allocated with alloca() is valid within the current function, not the current block. Space will not be automatically freed at the end of a loop containing alloca(). Be very careful when performing heap allocations from within a loop, as you run the risk of causing a stack overflow.