"The number of the counting shall be three, and three shall be the number of the counting..."
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_colorfoo
; if (blah
) // this is goodfoo
=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); ... }
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_idacquire_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()
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 BLooper
s and BHandler
s 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 BMessage
s, 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
.
BMessenger
s are cool. The following code snippets are functionally
equivalent:
status_terr1
=myLooper
->PostMessage
(kMyCommandConstant
);BMessenger
messenger
(myLooper
); status_terr2
=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
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:
a Voice
class to generate the tones
an Instrument
class to receive the MIDI input
a dac_writer()
"stream function"
that writes to the DAC stream buffers
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
(int32i
) :number
(i
),velocity
(0) {delayLength
= 44100 / 442.0 / pow(2.0, (i-9)/12.0 - 5);delayLine
= new float[delayLength
]; };~Voice
() { delete [] delayLine; }; voidNoteOn
(ucharvel
); voidMix
(int32n_samples
, float*dst
); int32number
; ucharvelocity
; int32delayLength
; int32delayPointer
; float*delayLine
; floatprevious
; }; voidVoice
::NoteOn
(ucharvel
) { if (vel
> 0) { /* Initialize delay line with noise */delayPointer
= 0; for (inti
= 0;i
<delayLength
;i
++)delayLine
[i
] = 160 *vel
* (2.0*rand
()/RAND_MAX
- 1);previous
= 0; }velocity
=vel
; } voidVoice
::Mix
(int32n_samples
, float*dst
) { if (velocity
> 0) /* Mix samples from delay line into dst */ for (inti
= 0;i
<n_samples
;i
++) { floatcurrent
=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:
#defineLOWEST_NOTE
28 structInstrument
:BMidi
{Instrument
();~Instrument
(); voidNoteOn
(uchar, ucharnote
, ucharvelocity
, uint32) {acquire_sem
(lock
); if (note
>=LOWEST_NOTE
)voices
[note
]->NoteOn
(velocity
);release_sem
(lock
); }; voidNoteOff
(ucharchannel
, ucharnote
, uchar, uint32) {NoteOn
(channel
,note
, 0, 0); };Voice
*voices
[128]; sem_idlock
;BMidiPort
port
;BDACStream
dacStream
;BSubscriber
subscriber
; };Instrument
::Instrument
() :subscriber
("Instrument") {lock
=create_sem
(1, "Instrument locker"); for (inti
=LOWEST_NOTE
;i
< 128;i
++)voices
[i
] = newVoice
(i
);dacStream
.SetStreamBuffers
(1024, 2);dacStream
.SetSamplingRate
(44100);subscriber
.Subscribe
(&dacStream
); booldac_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 (inti
=LOWEST_NOTE
;i
< 128;i
++) deletevoices
[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:
booldac_writer
(void*arg
, char*buf
, size_tcount
, void*) { short* dst = (short*)buf
; intn_samples
=count
/4;Instrument
*instrument
= (Instrument
*)arg
; static int32mix_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 (inti
= 0;i
<n_samples
; i++)mix
[i
] = 0;acquire_sem
(instrument
->lock
); for (inti
=LOWEST_NOTE
;i
< 128;i
++)instrument
->voices
[i
]->Mix
(n_samples
,mix
);release_sem
(instrument
->lock
); for (inti
= 0;i
<n_samples
;i
++) { int32sample
=mix
[i
]; int32left
=dst
[2*i
] +sample
; int32right
=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
; } returntrue
; }
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" 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
.
Let's write a simple C function to create and send an e-mail, which we'll cleverly call "sendmail".
status_tsendmail
(char *to
, char *subject
, char *from
, char *replyto
, char *content
, char *enclosure_path
) {BMailMessage
*message
; status_terr
;
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
= newBMailMessage
(); if (!message
) { // Couldn't allocate memory for the message returnB_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:
deletemessage
; if (err
) { returnB_ERROR
; } returnB_OK
; }
Now you can send a message by calling sendmail()
:
status_terror
=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_terror
=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.
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."
voidlistmessages
(void) {BQuery
query
;BNode
node
;BVolume
vol
;BVolumeRoster
vroster
; entry_refref
; charbuf
[256]; int32message_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 stringnode
.ReadAttr
(B_MAIL_ATTR_FROM
,B_STRING_TYPE
, 0,buf
, 255);buf
[20] = '\0'; // Truncate to 20 charactersprintf
("%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 stringnode
.ReadAttr
(B_MAIL_ATTR_SUBJECT
,B_STRING_TYPE
, 0,buf
, 255);buf
[40] = '\0'; // Truncate to 40 charactersprintf
(" 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 onceprintf
("%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).
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.
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 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...
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.
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.