An old joke tells about a piano tuner named Oper Knockity. He did such a good job that your piano never again went out of tune. His slogan was, "Oper Knockity only tunes once." If you are reading this column, chances are you don't have serious opportunity problems. Most C and C++ programmers find work. But several new career paths are about to open, and Windows 95 is the knock of opportunity.
This is the September column, and, if everything went according to schedule, Windows 95 has been released. I'm taking a chance in saying that; other trade columnists, more in-the-know than I, have reported rumors of a ship slip to November. Those columnists claim to have reliable inside sources.
Just now--I'm writing this in mid-June--Microsoft is still insisting that August is the date. Good. I can use that position to justify talking about the product, even though in June I am still under a nondisclosure agreement. By their schedule, it will have expired by the time you read this.
If you wonder why Microsoft wouldn't 'fess up to the delay, here's what I think: They've been leaking Windows 95 information for a year, making users' mouths water in anticipation. At the same time, IBM has heavily promoted OS/2 Warp. Users think they'll get Windows 95 as soon as August, so they wait and maybe don't bother with Warp. By the time August rolls around and Redmond announces a slip to November, the users are primed, ready, and slavering. They've been waiting for over a year. Three more months doesn't seem that bad. But if they had learned in May about the slip, well, they might have thrown up their hands in disgust and gone with something that's already available. Can't let that happen.
I hope those other columnists are wrong, and Windows 95 is (was) released in August. Some smart, diligent folks can capitalize. Here's my advice. Set up a Windows 95 network and learn the operating system. Learn it wall-to-wall. Then take out an ad in the Yellow Pages. Get a 900 number. Sell yourself as a Windows 95 trainer and installation expert. A lot of unsuspecting users are going to need help.
Microsoft and the trade press predict that most PCs will be running Windows 95 in a year or two. Could be. It's a really neat OS, and most applications developers are targeting it. But guess what? Unless you are running Windows 95 on one PC with no network, no mail, no fax, and nothing shared, Windows 95 is a knurly knot to install and, sometimes, to operate. Microsoft crows loudly about Plug and Play and the ease of setting things up, but believe me, t'aint so.
How do I know? For the past two months, I've been writing a Windows 95 self-help book and researching a Windows 95 games-programming book. Three of the PCs in my four-PC Windows for Workgroups network are now running Windows 95. Everything works, and it is a good operating environment. With this setup, I can scamper from PC to PC and test, learn, and write about all the features. But, woefully, every day I find something that either does not work or needs better documentation, usually the latter. Of course, my observations come from a beta, but it's supposed to be the last one. I doubt that the user interface will change much between now and August, or November, or, uh, well, who knows?
Windows 95 uses the familiar "wizard" paradigm that Visual C++ uses so effectively. Everything that you set up is done through a wizard. When you send a fax, a wizard leads you by the hand. These wizards are great, but sometimes they ask you to know a lot. Other times they fail to tell you something vital. Usually, when they omit an important detail, the detail is not obvious, and, therefore, the procedure is not intuitive.
Windows 95 is a really, really big shoe. Wizards have buttons with "Properties" or "Details" labels. The buttons open dialog boxes with tabbed pages. The tabbed pages have more buttons that open more dialog boxes. One wizard can launch another wizard. You dive deep into nested layers of wizards and dialog boxes. The screen seems totally grayed out by the partially hidden fragments of a dozen overlapping windows, some modal, some modeless. At the outer level, when the wizard finally gets around to asking for something, it wants you to decide on an option or enter the name of something. Sometimes there are Browse buttons; sometimes the wizard has an explanatory paragraph; sometimes--nada. Usually, a computer-literate person can deduce what the wizard wants, but often the wizard says something like, "Enter this information. If you don't know what to enter, ask your system administrator." The other day I asked my administrator what to do next. She said, "Wash up, dinner is almost ready."
Last year, when I made similar observations about installing OS/2 2.1, some OS/2 users chastised me. I should not malign their beloved OS, they said. A recent spate of letters to the editor of PC Magazine indicates that OS/2 Warp is not much better than OS/2 2.1 when it comes to installation woes. The PC's open architecture is the culprit. Operating-system builders don't know how to cope with it. The Macintosh is closed, and it doesn't have these problems. Of course, it doesn't have a majority market share, either.
Let's get together and form an elite cartel of Windows 95 supporters. We can network our expertise, support large and small businesses and governments, charge exorbitant fees, and support expensive hobbies like writing Windows 95 programs.
Opportunities for writing Windows 95 programs abound. Virtually every major developer is targeting the new operating environment. Little guys, too. The platform of choice has defaulted to Microsoft Foundation Classes. Most Windows C++ compilers have licensed MFC. The exception is Borland, which wants you to use OWL. In time, it'll come around. MFC is not only the de facto standard Windows framework, it's the best one available.
The paradigm of choice is visual programming. I've been experimenting with Visual C++ 2.0 and have some things to report. Mostly, it is a lovely development environment. But pay heed. Visual C++ does not completely isolate you from the Windows API because it does not encapsulate everything. For example, a search of the MFC docs fails to find classes that encapsulate modem and serial-port communications, network-packet exchanges, mail, or multimedia extensions. Maybe they're in the works; if not, they should be. Telephony and mail are major parts of Windows 95. They are built into the operating system. Multimedia is much bigger now than it was when MFC was first conceived.
A Windows 95 Game SDK that addresses sound and accelerated video is in beta now. It has been discussed in other publications. I'll look at it and report about its features and facilities when it becomes available.
Many moons ago, I briefly discussed MIDI in this column and bemoaned the lack of MIDI software tools for DOS programmers. I wanted to write programs that could read my keyboard and create accompaniments in real time, based on what I was playing. To do so, I needed tools to read the keyboard and write to the MIDI instrument channels. No such tools came into view, and I shelved the project. I later learned that the Windows platform has exactly what I need. Functions, messages, and data structures defined in the SDK support MIDI through the Windows device-independent interface. Those functions are not encapsulated into a class library, however. Which leads (or, in musical parlance, segues) into the next topic.
Many of you know that when I am not programming or writing, I am a jazz pianist. Some time ago I fell into a bar where the lounge pianist had a MIDI keyboard and a sequencer, a device that adds drums and accompaniment to what he plays. That sequencer could do everything but fry an egg. He let me play with it. One feature amazed me: In its bass-accompaniment mode it watched the notes I played, deduced the chord, and played, in real time, a respectable bass line under my improvisations.
I have seen other MIDI devices that could play accompaniments, but they always required me to play simple three- and four-note chords in the root position below a designated note on the keyboard. My left hand was, consequently, unavailable for anything else. Those bass notes below the designated divider note were likewise unusable for anything but defining chords for the accompaniment. That's not how I play the piano.
This sequencer had a tiny LCD screen that displayed the name of the chord it had deduced. I watched that screen while I played. The chord it chose was usually correct as long as I stuck to the root position in my left hand. Even when it chose the wrong chord, the bass line was acceptable with very few dissonant notes.
I tried to find such a sequencer, but the company had upgraded the product to a new version, which requires the player to play--guess what?--simple three- and four-note chords in the root position below a designated note on the keyboard. Fah!
All of which moved me to want to build the perfect personal electronic rhythm section, which not only would let me use both hands and the entire keyboard, but would apply a knowledge of harmony and jazz voicings when it deduced the next chord to base its accompaniment on.
I decided to build that program in Visual C++ 2.0 under Windows 95. That version runs only under Windows 95 and Windows NT. I used the visual components of the development environment to build a dialog-based application. First, I had to get past the problems of reading the MIDI keyboard and interpreting the notes. Therefore, the first iteration of this program watches for MIDI messages from the keyboard, ignores messages unrelated to key presses and releases, and reports the note messages in a list box. Later, I'll start building the musical heuristics that turn my computer into the reincarnation of Mingus.
Listings One and Two are mididlg.h and mididlg.cpp, respectively. I did not write most of these programs; Visual C++ did. I added code to support the application. Visual C++ wrote some other source-code files that I have not had to modify yet. If you download this code, you'll get everything.
This first version does not encapsulate the MIDI functions into a class. I'll wait until I know exactly how I plan to use the entire MIDI API before launching a class design.
Mididlg.h defines the CMidiDlg class derived from MFC's CDialog class. I added exactly four lines of code to this file; #include <mmsystem.h> is the first. That header contains the prototypes and other declarations for the MIDI API. I added an instance of the HMIDIIN type, a handle of an open MIDI input device. The FatalError member function reports an error and destroys the dialog window, terminating the application. The OnMIDIMessage member function is called when the MIDI input device sends a message. For example, if I press and release a key or pedal on the keyboard device, the device sends a MIDI message to the system.
Mididlg.cpp implements the CMidiDlg class member functions. I had to manually add the ON_MESSAGE macro to the message-map definition. The Visual C++ Class wizard does not include any MIDI messages among those that it automatically adds to the class. ON_MESSAGE is supposed to be used for user-defined messages, but it works for the messages that they left out of ClassWizard, as well.
The FatalError member function is mine. In several places, the program decides that it cannot continue. It calls FatalError from those places to display an error message and destroy the window.
The OnCreate member function is automatically built by ClassWizard. The system calls OnCreate when the window is being created. That's a convenient place to initialize the MIDI system, so I added that code.
The call to midiInGetNumDevs returns the number of MIDI input devices installed into Windows. If that number is 0, there is no keyboard, and the program calls FatalError.
This program assumes that there is only one MIDI input device and that the device is a keyboard. The MIDI API includes functions to get the capabilities of devices and make intelligent decisions about them. I did not use any of these. The program works with the one device. If I added a lot of input devices, I'd have to list them in a menu and use the chosen one.
The call to midiInOpen opens the input device. Inasmuch as I am assuming only one, I pass a device code of 0 to this function and tell it to send messages to the CMidiDlg windows. For multiple devices, I'd send 1, 2, 3, and so on, based on the chosen device. The first parameter is the address of the handle variable to be used to start and stop the input device.
If the device is unavailable, midiInOpen returns nonzero. One reason could be that another program has the device open.
After the device has been successfully opened, the call to midiInStart enables the device to send messages.
The OnDestroy function stops and closes the MIDI input device. This function is provided by Visual C++, but I had to provide the code. OnDestroy is called when the CMidiDlg window is destroyed.
The OnMIDIMessage member function is one that I added. It is called when the MIDI device sends a data message, which is translated into the MM_MIM_DATA Windows message. The ON_MESSAGE macro that I added to the message map connects the MM_MIM_DATA message to the OnMIDIMessage function. The function interprets the message. A low-order byte of 0x90 identifies a MIDI note-on message. MIDI also has a note-off message, but my keyboard never sends it. The note-on message includes a number that identifies the note and a value that identifies its velocity. My keyboard has weighted keys. The velocity indicator specifies how hard I pressed the key. When I release the key, the keyboard sends a note-on message with a velocity of zero.
The OnClearButton function is called when the user clicks the Clear command button on the dialog window. The program clears the contents of the list box and continues.
The note values from my keyboard range from 21 through 108, representing the 88 keys. I use those values to compute the notes from A through B-flat and the octave from 1 through 8. With that information and the use of velocity to determine whether the note is being pressed or released, I can maintain an array of notes being played at any given time. I can then start to build in the intelligence that deduces a chord.
Just now, the program displays only the note, octave, and on/off status in a list box. The other stuff comes later. Under-standing some of what I am doing requires a basic understanding of musical theory. I'll explain the numerical construction of chords and the relationship they have to one another in only the simplest of terms. The more interesting parts will be how the program uses Visual C++ to add user-interface elements, and how it uses--and eventually encapsulates--the MIDI portion of the Windows multimedia-extensions API.
By the way, this project needs a name. I got stung a couple of times when I inadvertently used someone else's program name. I'm thinking about naming it after a deceased bass player (who is not in a position to complain). Any suggestions?
The source-code files for this unnamed project are free. You can download them from the DDJ Forum on CompuServe and on the Internet by anonymous ftp; see "Availability," page 3.
If you cannot get to one of the online sources, send a 3.5-inch diskette and an addressed, stamped mailer to me at Dr. Dobb's Journal, 411 Borel Avenue, San Mateo, CA 94402, and I'll send you the source code. Make sure that you indicate that you want mididlg.h and mididlg.cpp and the attendant files. The code is free, but if you'd like to support my Careware charity, include a dollar for the Brevard County Food Bank.
I saw Bob the other day in a computer store. I looked over the shoulder of a computer novice who was timidly exploring the rooms in Bob's house and learning how to use a computer. Bob is a user interface for the epsilon minus. How ironic. IDG can't do a book called Bob For Dummies. It would have to be, Bob Is For Dummies.
Good ol' Microsoft. They always make me think of new "C Programming" column projects. Their unpublished character-oriented-windows (COW) library inspired D-Flat. Now I'm considering an improved user interface, one that targets folks who find Bob too urbane and intimidating. The absolute lowest common denominator in user interfaces. The UI for the rest of us.
Let's see. How would it go? I turn on my computer. The screen flashes and the disk whirs and rattles. An image appears and there I am, standing at the door of a house trailer. A pickup truck on blocks sits nearby. Probably an information-superhighway metaphor. The gun rack in the pickup's rear window sports a fishing pole, maybe the tool that I use to locate things. A porcelain pink flamingo stands in the lawn ready to help me with what--my travel plans? Nearby, a rubber tire hangs on a rope from the limb of an oak tree. Games, no doubt. A lawn statue of a porter stands mute holding a lantern. Wonder what he does. A scrawny cartoon hound dog scratches his ear and says through my sound card, "Hey, Goober." He'll be my guide through the user interface. He leads me inside the trailer, where a velvet painting of Elvis hangs over a fake fireplace with a light bulb and cellophane fire simulation. A sign on the wall in the bathroom says, "We aim to please, you aim, too, please." Each of these artifacts is a metaphor that lets me use the computer to do something meaningful--like manage my deep-woods-survival club meeting schedule. There's lots more, but I'll save the surprises for the first beta.
I'm calling it..."Jim Bob(TM)." To be truthful, I had intended to call this interface "Bubba." But just between the time I wrote this column and the time it went to press, someone else posted a similar interface called Bubba (shades of my recent experience with the "IMail" moniker). Oh well, great minds and all that jazz.
Copyright © 1995, Dr. Dobb's JournalWriting Windows 95 Programs
The CyberRhythm Section
Source Code
Why Not Roberta?
Listing One
// mididlg.h : header file
/////////////////////////////////////////////////////////////////////////////
// CMidiDlg dialog
#include <mmsystem.h>
class CMidiDlg : public CDialog
{
// private data
HMIDIIN hMidiIn;
// private functions
void FatalError(LPCTSTR msg);
LONG OnMIDIMessage(WPARAM, LPARAM msg);
// Construction
public:
CMidiDlg(CWnd* pParent = NULL); // standard constructor
// Dialog Data
//{{AFX_DATA(CMidiDlg)
enum { IDD = IDD_MIDI_DIALOG };
// NOTE: the ClassWizard will add data members here
//}}AFX_DATA
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CMidiDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
//}}AFX_VIRTUAL
// Implementation
protected:
HICON m_hIcon;
// Generated message map functions
//{{AFX_MSG(CMidiDlg)
virtual BOOL OnInitDialog();
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnDestroy();
afx_msg void OnClearbutton();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
Listing Two
// mididlg.cpp : implementation file
#include "stdafx.h"
#include "midi.h"
#include "mididlg.h"
#ifdef _DEBUG
#undef THIS_FILE
static char BASED_CODE THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CMidiDlg dialog
CMidiDlg::CMidiDlg(CWnd* pParent /*=NULL*/)
: CDialog(CMidiDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CMidiDlg)
// NOTE: the ClassWizard will add member initialization here
//}}AFX_DATA_INIT
// Note that LoadIcon does not require a subsequent DestroyIcon in Win32
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
hMidiIn = 0;
}
void CMidiDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CMidiDlg)
// NOTE: the ClassWizard will add DDX and DDV calls here
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CMidiDlg, CDialog)
//{{AFX_MSG_MAP(CMidiDlg)
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_WM_CREATE()
ON_WM_DESTROY()
ON_MESSAGE(MM_MIM_DATA, OnMIDIMessage)
ON_BN_CLICKED(IDC_CLEARBUTTON, OnClearbutton)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CMidiDlg message handlers
BOOL CMidiDlg::OnInitDialog()
{
CDialog::OnInitDialog();
CenterWindow();
// TODO: Add extra initialization here
return TRUE; // return TRUE unless you set the focus to a control
}
// If you add a minimize button to your dialog, you will need the code below
// to draw the icon. For MFC applications using the document/view model,
// this is automatically done for you by the framework.
void CMidiDlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // device context for painting
SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);
// Center icon in client rectangle
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// Draw the icon
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialog::OnPaint();
}
}
// The system calls this to obtain the cursor to display while the user drags
// the minimized window.
HCURSOR CMidiDlg::OnQueryDragIcon()
{
return (HCURSOR) m_hIcon;
}
void CMidiDlg::FatalError(LPCTSTR msg)
{
MessageBeep(MB_ICONSTOP);
MessageBox(msg, "MIDI KB", MB_OK | MB_ICONEXCLAMATION);
DestroyWindow() ;
}
int CMidiDlg::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CDialog::OnCreate(lpCreateStruct) == -1)
return -1;
// ---- test for MIDI input devices
if (midiInGetNumDevs() == 0)
FatalError("No MIDI input devices");
// ---- assume one MIDI input device, open the keyboard
else if (midiInOpen(&hMidiIn, 0, (unsigned long) m_hWnd, 0,
CALLBACK_WINDOW) != 0)
FatalError("Cannot open MIDI input device");
else
midiInStart(hMidiIn);
return 0;
}
void CMidiDlg::OnDestroy()
{
CDialog::OnDestroy();
if (hMidiIn != 0) {
midiInStop(hMidiIn);
midiInClose(hMidiIn);
}
}
LONG CMidiDlg::OnMIDIMessage(WPARAM, LPARAM msg)
{
// --- extract MIDI message components
if ((msg & 0xff) == 0x90) { // MIDI status = note on
BOOL velocity = ((msg >> 16) & 0xff) != 0;
short unsigned int nt = ((msg >> 8) & 0xff) - 21;
short unsigned int note = nt % 12;
short unsigned int octave = nt / 12 + 1;
static char *notes[] = {
"A ","Bb","B ","C ","Db","D ",
"Eb","E ","F ","Gb","G ","Ab"
};
CString mstr(notes[note]);
char oc[] = " ( ) ";
oc[2] = octave+'0';
mstr += oc;
mstr += (velocity ? "On" : "Off");
CListBox *pListBox = (CListBox*)GetDlgItem(IDC_NOTELIST);
pListBox->AddString(mstr);
}
return 0;
}
void CMidiDlg::OnClearbutton()
{
CListBox *pListBox = (CListBox*)GetDlgItem(IDC_NOTELIST);
pListBox->ResetContent();
}