Issue 2-50, December 17, 1997

Be Engineering Insights: Three Unrelated Tips

By Hiroshi Lockheimer

"The number of the counting shall be three, and three shall be the number of the counting..."

1—Don't Forget The "A" In "RGB"

In the BeOS Preview Release 2, the default alpha arguments to SetHighColor(), SetLowColor(), and SetViewColor() in View.h were changed from 0 to 255. We also changed the value of the constant B_TRANSPARENT_32_BIT. These changes were made in long-range preparation (i.e., not for Release 3, and not yet included in plans for any subsequent release) for alpha channel support in the App Server and the Kits. This means a number of things for you:

  • You don't have to worry about B_TRANSPARENT_32_BIT. It's a symbol, so the change in value should be transparent (pun intended) from a source compatibility standpoint. We've added some filters in the aforementioned calls that look for the old value, just to be extra safe.

  • You should worry, however, if you have data (or, less likely, code) that hard-codes the old B_TRANSPARENT_32_BIT value and you stuff that data directly into a BBitmap. The solution here is...use the new value! (Duh.)

  • You should inspect your code for uninitialized alpha values. You don't have to do this immediately since we currently ignore the value of the alpha component, but it's a good habit to get into.

Here's some suspicious code:

// alpha component omitted!
rgb_color foo = {255, 255, 255};

If you don't want foo to be transparent (an alpha of 0), you should do the following:

// explicitly set it to 255
rgb_color foo = {255, 255, 255, 255};

Here are some other common cases that I found while rummaging through the Interface Kit:

{
  rgb_color foo;

  if (blah)
    // this is good
    foo = ViewColor();
  else
    // bad, alpha isn't being set...
    foo.red = foo.green = foo.blue = 123;
}

{
  // explicitly setting alpha to 0!
  SetHighColor(80, 80, 80, 0);
  ...
}

2—Beware Of B_INTERRUPTED.

If you're writing an app that uses signals, or a library that uses signals, or a library that may be used by an app that uses signals, watch out for B_INTERRUPTED. Functions such as acquire_sem() will return an error code of B_INTERRUPTED when they are "unblocked" by a signal.

The following code is unsafe:

// theSem is a valid sem_id
acquire_sem(theSem);

Do this (or something like it) instead:

// loop until we acquire the semaphore
while (acquire_sem(theSem) == B_INTERRUPTED);

Here's a list of functions that you want to protect from B_INTERRUPTED:

  • acquire_sem()

  • acquire_sem_etc()

  • port_buffer_size()

  • read()

  • write()

  • read_port()

  • read_port_etc()

  • write_port()

  • write_port_etc()

  • wait_for_thread()

3—Are Your Messages Getting Delivered?

Speaking of error codes, did you know that BLooper::PostMessage() returns an error? Or, to put it another way, did you know that PostMessage() can fail, even if all BLoopers and BHandlers involved are valid?

A BLooper's port capacity is set to 100 (B_LOOPER_PORT_DEFAULT_CAPACITY) by default. This is also true of BWindow (BWindow inherits from BLooper). If a Looper's message queue is inundated with BMessages, its port fills up. Since a BLooper with a full port can no longer accept messages, that's one way a call to PostMessage() can fail.

There are a number of ways to deal with this situation:

  • Look at PostMessage()'s return value. Handle errors gracefully.

  • Don't do any super time-consuming things in MessageReceived(). Spawn a thread if you have to. This reduces the likelihood of a BLooper's port filling up.

  • Use a BMessenger.

BMessengers are cool. The following code snippets are functionally equivalent:

status_t err1 = myLooper->PostMessage(kMyCommandConstant);

BMessenger messenger(myLooper);
status_t err2 = messenger.SendMessage(kMyCommandConstant);

The difference is that the BMessenger version guarantees that your message will be delivered, regardless of the state of the recipient's port. It does this by blocking on the full port until there is room. Keep in mind, this blocking can lead to other issues such as deadlock, but that's been covered in other articles, for example: Issue 1-#25


Be Engineering Insights: MIDI Synthesis From Scratch

By Marc Ferguson

Past articles in the Be Newsletter have given example uses of the MIDI and audio stream classes. This time we'll give you an example for connecting a BMidiPort to a BDACStream to build a real-time MIDI synthesizer that generates sounds from scratch, instead of using the BSynth class as a sample player. To try this example you'll need a MIDI keyboard and a MIDI interface connected to your computer's printer port (if you have a BeBox, use the midi2 input).

There are three parts to this example:

The Voice class implements the Karplus-Strong synthesis algorithm. It consists of a delay line with 100% feedback through a low-pass filter, which averages adjacent samples. Output samples look like this:

X[i] = (X[i-N] + X[i-N-1]) / 2

where N is the length of the delay line.

You can use any percussive sound to excite the delay line—the Voice class just initializes the delay line to random noise.

If you listen carefully you may notice that each note sounds subtlely different. The wavelength of the tone produced is about (N+0.5) and the frequency is sampling rate/(N+0.5). Because N must be an integer, there is some rounding error in the pitch of the output. The result is that lower notes are in tune, while the higher notes are increasingly out of tune. This is the best we can do without a fancier algorithm.

In this example, the Voice::NoteOn() method initializes the delay line and the Voice::Mix() method runs the delay line and mixes the output into the destination buffer:

struct Voice {
  Voice(int32 i) : number(i), velocity(0) {
    delayLength = 44100 / 442.0 / pow(2.0, (i-9)/12.0 - 5);
    delayLine = new float[delayLength];
  };
  ~Voice() {
    delete [] delayLine;
  };

  void NoteOn(uchar vel);
  void Mix(int32 n_samples, float* dst);

  int32 number;
  uchar velocity;
  int32 delayLength;
  int32 delayPointer;
  float* delayLine;
  float previous;
};

void Voice::NoteOn(uchar vel)
{
  if (vel > 0) {
    /* Initialize delay line with noise */
    delayPointer = 0;
    for (int i = 0; i < delayLength; i++)
      delayLine[i] = 160 * vel * (2.0*rand()/RAND_MAX - 1);
    previous = 0;
  }
  velocity = vel;
}

void Voice::Mix(int32 n_samples, float* dst)
{
  if (velocity > 0)
    /* Mix samples from delay line into dst */
    for (int i = 0; i < n_samples; i++) {
      float current = delayLine[delayPointer];
      dst[i] += current;
      delayLine[delayPointer] = (current + previous) / 2;
      if (++delayPointer >= delayLength)
        delayPointer = 0;
      previous = current;
    }
}

The next component of the example, the Instrument class, holds an array of Voices. It connects itself to a BMidiPort and passes any NoteOn calls it receives to the appropriate Voice. It also enters the DAC stream using dac_writer() as a stream function:

#define LOWEST_NOTE 28

struct Instrument : BMidi {
  Instrument();
  ~Instrument();

  void NoteOn(uchar, uchar note, uchar velocity, uint32) {
    acquire_sem(lock);
    if (note >= LOWEST_NOTE)
      voices[note]->NoteOn(velocity);
    release_sem(lock);
  };
  void NoteOff(uchar channel, uchar note, uchar, uint32) {
    NoteOn(channel, note, 0, 0);
  };

  Voice* voices[128];
  sem_id lock;
  BMidiPort port;
  BDACStream dacStream;
  BSubscriber subscriber;
};


Instrument::Instrument() : subscriber("Instrument")
{
  lock = create_sem (1, "Instrument locker");
  for (int i = LOWEST_NOTE; i < 128; i++)
    voices[i] = new Voice(i);

  dacStream.SetStreamBuffers(1024, 2);
  dacStream.SetSamplingRate(44100);
  subscriber.Subscribe(&dacStream);
  bool dac_writer(void*, char*, size_t, void*);
  subscriber.EnterStream(0, true, this, dac_writer, 0, true);

  if (port.Open("midi2") != B_NO_ERROR) {
    printf("Failed to open midi port.\n");
    return;
  }
  port.Connect(this);
  if (port.Start() != B_NO_ERROR) {
    printf("BMidiPort::Start() failed.\n");
    return;
  }
}

Instrument::~Instrument()
{
  subscriber.ExitStream(true);
  for (int i = LOWEST_NOTE; i < 128; i++)
    delete voices[i];
  delete_sem(lock);
}

Finally, the dac_writer() stream function allocates a mixing buffer of floats, calls the Mix() method for each voice, and mixes the result into the DAC stream:

bool dac_writer(void* arg, char* buf, size_t count, void*)
{
  short* dst = (short*) buf;
  int n_samples = count/4;
  Instrument* instrument = (Instrument*) arg;

  static int32 mix_samples = 0;
  static float* mix = 0;
  if (mix_samples < n_samples) {
    if (mix)
      delete [] mix;
    mix = new float[n_samples];
    mix_samples = n_samples;
  }
  for (int i = 0; i < n_samples; i++)
    mix[i] = 0;

  acquire_sem(instrument->lock);
  for (int i = LOWEST_NOTE; i < 128; i++)
    instrument->voices[i]->Mix(n_samples, mix);
  release_sem(instrument->lock);

  for (int i = 0; i < n_samples; i++) {
    int32 sample = mix[i];
    int32 left = dst[2*i] + sample;
    int32 right = dst[2*i+1] + sample;
    if (left > 32767) left = 32767;
    if (right > 32767) right = 32767;
    if (left < -32768) left = -32768;
    if (right < -32768) right = -32768;
    dst[2*i] = left;
    dst[2*i+1] = right;
  }
  return true;
}

Now, if you create an Instrument object, you should be able to play a MIDI keyboard connected through your computer's printer port.

One other thing you need to be concerned about if you're writing a real-time synthesizer is the latency between the time a key is struck on the keyboard and when you hear the note. This latency includes the time it takes the keyboard to notice that a key has been pressed, the time to transmit three bytes via MIDI, the time to calculate the waveform, and the amount of output buffering for the DAC. The amount of DAC buffering is set by the call to SetStreamBuffers() in the Instrument constructor. The smaller the DAC buffers, the more CPU time is spent handling DAC interrupts.

In the example, two buffers of 1K bytes (or 256 samples) are used for a total of about 12 milliseconds of buffering. These 1K byte buffers are large enough to run well on a Dual-66 BeBox and small enough that the latency is not unreasonable. If you have a faster machine, you can experiment with smaller buffer sizes and shorter latencies.


Developers' Workshop: The Postman Always Beeps Twice (At Least When You Have Mail)

By Eric Shepherd

"Developers' Workshop" is a new weekly feature that provides answers to our developers' questions. Each week, a Be technical support or documentation professional will choose a question (or two) sent in by an actual developer and provide an answer.

We've created a new section on our website. Please send us your Newsletter topic suggestions by visiting the website at: http://www.be.com/developers/suggestion_box.html.

Handling e-mail in BeOS applications is extremely easy. The BMailMessage class provides a quick and painless way to send mail messages. Reading messages is a little harder, but not by a lot. This week we'll look at how you can take charge of the Mail Kit so your applications can send messages and query the system about received messages.

The BMailMessage class represents an outgoing e-mail. You can add a message body, enclosures, and header fields to the message using the functions in the BMailMessage class, which is described in lovely C++ header file form in E-mail.h.

Reach Out

Let's write a simple C function to create and send an e-mail, which we'll cleverly call "sendmail".

status_t sendmail(char *to,
                  char *subject,
                  char *from,
                  char *replyto,
                  char *content,
                  char *enclosure_path) {
  BMailMessage *message;
  status_t err;

Our sendmail() function lets us specify the To, Subject, From, and Reply-to header fields, as well as the message content and one enclosure, specified by pathname.

  message = new BMailMessage();
  if (!message) {
    // Couldn't allocate memory for the message
    return B_ERROR;
  }

First, we create a new BMailMessage object. If we can't create it, we just return B_ERROR immediately, since there's really no point in going on.

The To and Subject header fields are mandatory for any e-mail message, and the Mail Kit doesn't know how to create them for you automatically, so you need to set them up:

  err = message->AddHeaderField(B_MAIL_TO, to);
  err |= message->AddHeaderField(B_MAIL_SUBJECT, subject);

Notice that we're ORing the result codes of the AddHeaderField() calls together. In this example program, we don't care *what* error occurred, but only whether or not an error occurred at all. By ORing all the results together, we obtain a mega-result that's B_OK (which happens to be zero) if and only if no errors occurred at all.

Of course, you could be more programmatically correct and handle each error individually with alerts, but this saves some space for the purposes of this article without disposing of error-handling entirely.

The From and Reply-to fields can be automatically filled in by the Mail Kit if you don't specify them. So if the caller specifies NULL for these two fields, we don't bother to set them up.

  if (from) {
    err |= message->AddHeaderField(B_MAIL_FROM, from);
  }
  if (replyto) {
    err |= message->AddHeaderField(B_MAIL_REPLY, replyto);
  }

Then we add the message body by calling AddContent():

  err |= message->AddContent(content, strlen(content));

And, if the enclosure_path parameter is non-NULL, we add the enclosure to the message:

  if (enclosure_path) {
    err |= message->AddEnclosure(enclosure_path);
  }

Finally, if no errors have occurred, we send the message by calling Send(). The first parameter to Send() is a boolean that specifies whether the message should be sent immediately (true) or queued to be sent the next time the mail daemon is scheduled to run (false). In this case, we want the message to be sent immediately, so we specify true.

  if (err == B_OK) {
    err = message->Send(true);  // Send now
  }

Now it's time to clean up. We delete the message object and return B_ERROR if an error occurred or B_OK if everything went well:

  delete message;
  if (err) {
    return B_ERROR;
  }
  return B_OK;
}

Now you can send a message by calling sendmail():

status_t error = sendmail("foo@bar.com",
                          "Subject",
                          "sheppy@be.com",
                          "sheppy@bar.com",
                          "This is the message body.",
                          "/boot/home/file.zip");

This will send, to foo@bar.com, the message "This is the message body." with the file /boot/home/file.zip attached. The From will be listed as sheppy@be.com, and the Reply-to will be sheppy@bar.com.

You could also do:

status_t error = sendmail("foo@bar.com",
                          "Subject",
                          NULL,
                          NULL,
                          "Body",
                          NULL);

This sends a message to foo@bar.com with a subject "Subject" and the body "Body." There's no enclosure, and the Mail Kit automatically sets From and Reply-to to the addresses configured in the E-mail preferences application.

Read the E-mail.h header file to see what other header fields you can add to your messages.

Special Delivery

When e-mail arrives, the mail daemon creates a new file to contain the message and adds the appropriate attributes to the file. These attributes contain things like the subject, from, to, and other fields. With these attributes firmly in place, you can query the file system to look for e-mail files based on the values of these fields.

Let's create a sample program that lists the subjects and senders of all unread messages. We'll call it listmessages(). You should already be familiar with queries and attributes. If you're not, have a look at the BQuery and BNode sections in the Storage Kit chapter of the "Be Book."

void listmessages(void) {
  BQuery query;
  BNode node;
  BVolume vol;
  BVolumeRoster vroster;
  entry_ref ref;
  char buf[256];
  int32 message_count = 0;

New messages are stored by the mail daemon on the user's boot disk, so we begin by using the volume roster to identify the boot disk and set our query to search on that disk:

  vroster.GetBootVolume(&vol);
  query.SetVolume(&vol);

The next thing we do is set up our query. We want to search for new mail messages, so our search predicate is "MAIL:status = New". See the E-mail.h header file for a list of the mail message attributes.

If an error occurs while trying to establish the predicate for the query, we bail out:

  if (query.SetPredicate("MAIL:status = New") != B_OK) {
    printf("Error: can't set query predicate.\n");
    return;
  }

Then we run the query. If an error occurs, we print an appropriate message and give up:

  if (query.Fetch() != B_OK) {
    printf("Error: new mail query failed.\n");
    return;
  }

Now it's time to loop through the files in the query's result set. We'll use BQuery::GetNextRef() to iterate through them. For each entry, we set up a BNode so we can use BNode's attribute functions to read the file's attributes. Again, as good programmers, we gracefully cope with errors:

  while (query.GetNextRef(&ref) == B_OK) {
    message_count++;    // Increment message counter

    if (node.SetTo(&ref) != B_OK) {
      printf("Error: error scanning new messages.\n");
      return;
    }

Next, we read the From attribute. This attribute's name is given by the constant B_MAIL_ATTR_FROM. We use a 256-byte buffer, preinitialized to an empty string, and call BNode::ReadAttr() to read the attribute into the buffer.

To facilitate formatting our display, we chop off the From string at 20 characters, then print the message number and the From attribute to the screen:

    buf[0] = '\0';    // If error, use empty string
    node.ReadAttr(B_MAIL_ATTR_FROM, B_STRING_TYPE, 0, buf, 255);
    buf[20] = '\0';    // Truncate to 20 characters
    printf("%3d From: %-20s", message_count, buf);

Our loop ends by reading the Subject attribute (B_MAIL_ATTR_SUBJECT), truncating it to 40 characters, and printing it as well:

    buf[0] = '\0';    // If error, use empty string
    node.ReadAttr(B_MAIL_ATTR_SUBJECT, B_STRING_TYPE, 0, buf, 255);
    buf[40] = '\0';    // Truncate to 40 characters
    printf("   Sub: %s\n", buf);
  }

To add a little panache, we round things out by displaying the total number of new messages. If we didn't find any new messages, we say so:

  if (message_count) {
    beep();    // The postman beeps once
    printf("%d new messages.\n", message_count);
    beep();    // The postman beeps twice
  }
  else {
    printf("No new messages.\n");
  }

The output from this function looks something like this. The "<she" is the beginning of my e-mail address, chopped off by our 20-character truncation of the From attribute:

  1 From: "Eric Shepherd" <she   Sub: Newsletter test
  2 From: "Eric Shepherd" <she   Sub: re: BeOS programming
2 new messages.

Consider this: In the while loop, you obtain an entry_ref for every new e-mail on the user's system. Instead of listing out the subject and sender's names, you could just as easily open up the file and show the contents in a window. You're well on your way to having an e-mail reader without even breaking a sweat (and we all know how expensive it is to replace broken sweat).


Some Thoughts for the New Year

By Dave Johnson

We Have Apps

I'm sure you've all heard the "chicken-and-egg" paradox, which has it that developers won't do applications for the BeOS before there is an installed customer base, and customers won't install the BeOS until there are applications.

Well, I'm pleased to say that our "egg" days are over and we're hatching. At our Macworld Expo booth in January, several software companies will demonstrate compelling applications that let you do things you cannot do on Mac or Windows, that out-perform Mac and Windows apps, and that generally prove the BeOS is a superior platform for media applications.

More Apps Are In The Pipeline

There are more applications coming. I can't give you specifics, except to say that it's time to change the way you think about the BeOS. As we expand into the Intel world there will no longer be any basis for the criticism "but they have no apps." (I don't yet know what criticism will take its place, but it won't be that.) The Intel port represents a big change in how we see the BeOS, and we need to adjust our brains around it.

But please don't replace "I don't think I'll develop an app for the BeOS because there aren't any apps" with "I don't want to develop for the BeOS because someone is probably already doing the app I want to do." The BeOS running on Intel offers a huge opportunity—and you have a head start. The field is wide open for you to write your dream application with minimal competition.

The BeOS Is Not A Mac Utility

The BeOS has always been a separate operating system, even if it happened to run on the Macintosh. More and more, you'll see us listed as an independent platform alongside Mac, Windows, Unix, Linux, DOS, etc...

The BeOS Is Fiber And CD

Some of us have been working with the BeOS for so long that we don't remember the experience of discovering it for the first time. Seeing and using the BeOS for the first time is one of those defining moments, paradigm shifts, flash realizations, spiritual transformations...

I saw this again as I watched someone here give his first demo last Friday. He was a little nervous and awkward, looking at his script, hesititant, but his confidence was sustained by a guy in the audience who kept shouting, "MY GOD!" and "OH MY GOD!!!" every time he showed another BeOS feature.

The BeOS is the equivalent of a fiber-optic infrastructure upon which your media applications are built, bringing a quantum increase in performance. Mac and Windows are old copper wire systems. Mac and Windows are vinyl and the BeOS is CD.

Sure, there's a lot of support out there for copper wire systems, just like there was a huge inventory of vinyl recordings. Hundreds of manufacturers build things like insulators and transformers for telephone poles, wire cutters, connector boards...plus lots of electricians, etc...

Because fiber is new technology there is not that kind of support structure in place yet. (The infrastructure is coming for the BeOS because YOU are going to develop products and drivers and make a lot of money—right?)

But this isn't like Beta vs. VHS, where both are acceptable so either one can win. This is fiber vs. copper and it is a matter of spreading the word, getting the awareness out there. It is a question of "when," not "if." From now on media apps must be on the BeOS to be competitive. The BeOS *is* the Media OS.


Tectonic Plates

By Jean-Louis Gassée

I was wrong when I assumed Judge Jackson would take his time ruling on the Department of Justice complaint. Rule he did, but the exact meaning of the ruling evades yours truly; the writ appears to state that either party could ultimately win the case. Judge Jackson grants a temporary injunction but not the fine requested by the DOJ, and appoints a Special Master, Lawrence Lessig, a noted cyberspace jurist, to render a more definite ruling by May 1998. Microsoft appeals, criticizes the ruling, and proposes to comply in such a manner as to raise eyebrows. Strange.

Two less immediately controversial items recently piqued my amateur kremlinologist interest. One is the spin TCI puts on its next generation set-top box efforts, the other is the Sun-Intel pact around Merced, the next generation high-end microprocessor. Do these shifts of the industry tectonic plates signal a distancing from Redmond?

Let's start with TCI. The company is in the final phase of defining its new set-top box platform. Until recently, Windows CE was assumed to be the obvious choice for the OS. What we now have from TCI's PR department is a mille-feuille—a sandwich of hardware, operating system, add-ons and applications, with diagrams naming several candidates for each layer. The processor could come from Intel, Motorola, SGS, LSI Logic and so on; the OS from Microsoft or Oracle's NCI; systems would be made by NextLevel (nee General Instruments), Scientific Atlanta, Sony; program guides from Microsoft, StarSight, Thomson for starters; and the service providers would range from @Home to AOL, TCI itself, NBC and AT&T among a plethora of contenders. The spin is that TCI wants this to be a very open platform (with a capital "Oh").

Open? Or simply closed to (the perception of) Microsoft domination? After all, this isn't the best time to get caught in a loving embrace with a company whose investment in the cable business has already caused concern. If Windows CE (Consumer Electronics) becomes the Windows of set-top boxen, another game is over, critics say. The anti-trust division of the DOJ, already in a bellicose mood, would not be charmed.

So the claim of "openess" could be mere posturing. We'll see. In the meantime, let's crack open the history books...

You could say TCI is trying to reproduce in vitro what happened in the personal computer business: In the beginning, there were all sorts of processors and operating systems. Over the years, an elimination process took place and got us where we are, with Wintel running on most desktops. Now, TCI wants to play a disinterested Mother Nature as the set-top biosphere settles on its fittest survivors. Of course, the unabetted process of elimination isn't a spectator sport. (Geeks might enjoy the show, but consumers won't.) If TCI wants success with couch potatoes as well as Internet users, they'll have to limit the number of permutations. My guess is that they know this, and they will try to nudge the dice. As long as they have the perception of competition without actually losing control of the strings—which implies something other than Windows CE.

The other shift happening this week is the Sun-Intel pact, or the Solaris-Merced agreement. Merced is the grand unifier, developed as the successor to the Pentium as well as HP's RISC Precision Architecture. It will run both legacy instruction sets, plus a new, more advanced one. Today's announcement means, among other things, Sun's system software will run on Merced ("optimized" the release says, as if it would tell you if they had done a quick, dirty, and dispirited port).

Big announcements of big alliances sometimes produce just that, the announcement. Even if the alliance holds, this is something of a non-event—who's going to not support Merced? Or it may be a defensive move on Sun's part: After failing to achieve anything of market consequence on Intel processors, an Intel/HP duopoly in the juicy server business doesn't feel good.

Another interpretation, not necessarily exclusive, is that Intel likes the idea of using Merced as an opportunity to change the power structure they've profited from, but not always unequivocally enjoyed, in the PC business. After all, Windows CE and NT work on other processors—why shouldn't Intel work with more OS partners?

For Intel to approach the server business with Merced, HP's Unix, Sun's Solaris, and Windows NT must feel good. And we might remember that all new Intel chips are initially positioned for server and other high-end applications, the current ones being ideal for mainstream desktop applications. That was the story for the 386, the 486, the Pentium and the Pentium II. Soon enough, with process prowess, they become affordable for the mainstream. So will Merced.

And Microsoft, seeing all this, will keep claiming they have much competition to worry about.

Creative Commons License
Legal Notice
This work is licensed under a Creative Commons Attribution-Non commercial-No Derivative Works 3.0 License.