This step introduces some simple ways to display images and play audio in the Windows environment, then introduces the concept of timed presentation of content.
If you are not already familiar with programming for the Windows environment and using Visual Studio, be sure you do Step 1 before doing this step. In either case, I highly recommend completing that tutorial.
The specific features that will be introduced are:
Be sure you have the correct software installed for this step. See the Software requirements page.
You will turn in a program that operates as indicated in the following blue box and the answers to the two questions below. The program is due 9-24-09 at 11:59pm. The question answers are due 9-28-09 at 11:59pm.
This step presents a tutorial that you will be following. When you are done with that tutorial, you are to do the following task and turn in the solution:
|
Create a program that can display the following items in the following sequence When the program starts or when a reset menu option is selected, your program is considered to be at time zero and is going to start playing. The following conditions should occur at each of the following times: 0:00 An initially blank window Please do not use the grail sound or the splash image in your solution to the step! |
The TA will notify you of details on how to submit your solution for grading.
You will also answer the following questions in a Word document and turn them in as well.
1. Suppose I want to play an audio file every five seconds (maybe some regular alarm). There are two ways to do this:
Method A:
OnStartup
Play audio file
Set timer for 5 seconds
OnTimer
Play audio file
Set timer for 5 seconds
Method B:
OnStartup
starttime = timeGetTime()
Play audio file
nextaudiotime = 5
Set timer for nextaudiotime - (timeGetTime() -
starttime)
OnTimer
Play audio file
nextaudiotime += 5
Set timer for nextaudiotime - (timeGetTime() -
starttime)
Assume the time to service a timer expiration averages 3 milliseconds and the time to start up an audio file averages 15 milliseconds. After one hour, how many times will method A and method B be expected to play the audio file. After one day, how many times will method A and method B be expected to play the audio file?
2. It's common that multimedia presentation will have a characteristic called latency. Latency is a processing delay exhibited by the media. As an example, playing audio from a file may require a variable amount of time, depending on how busy the system is, how quickly the file can be accessed, and how much decompression is required. For this reason, most multimedia API's allow you to load the media, then play it. So, you can tell it to open an audio file in advance, then tell it to play the audio file. The time to start playing is generally very small in this case because the system has been able to prepare for it. Suppose play requires less than a millisecond, but open can require up to 1 second. Modify the algorithm from Method B above to play a different audio file every five seconds and ensure the audio is played on time (within 5 milliseconds will be acceptable).
It is assumed that you will build this tutorial from a basic MFC program such as would be created by Step 1. If you have completed Step 1, you can use that solution to start from. Otherwise, simply complete the Creating your project part of Step 1 to create a starting application for this step.
The concept of "resources" in a Windows program allows binary data to be associated with your program. We're going to add an image to your program from last week as a resource. First, you need to obtain an image to use. The image must be in BMP format (.bmp extension). You can use a variety of program such as GIMP or Photoshop to change an image file format. I put some random images in an img directory. For now, please use the file splash.bmp. (don't use splash.jpg!) You can right click and select Save Link to save the image file. Copy that file into your project directory. (If you want, clear a subdirectory for media objects).
In Visual Studio, go to the Resource Browser for your program. Expand until you see the list of directories: Right-click on the .rc item (step.rc for example) and select Add Resource... This is the Add Resource dialog box:

Select Bitmap and press the "Import..." button. Browse to that splash.bmp file and click open. You should see the image loaded into Visual Studio. Now, rename the resource from the initial name (probably IDB_BITMAP1) to IDB_SPLASH. You do this by selecting the item in the resource editor (under Bitmap). Then change the ID in the properties page.
Your project should compile now. Won't do anything interesting, but, hey, we're just beginning.
Now, add the following member variables to the View class:
CBitmap m_splash;
int m_splashwid;
int m_splashhit;
And, add the following to the View constructor:
m_splash.LoadBitmap(IDB_SPLASH);
BITMAP map;
m_splash.GetBitmap(&map);
m_splashwid = map.bmWidth;
m_splashhit = map.bmHeight;
A CBitmap class object is a container for a BITMAP image in memory. We're going to put it in our view as member data so we only load it into memory once when the view is created. The LoadBitmap line loads the bitmap from the resource into memory. The next few lines simply get the size of the bitmap, since we'll need that information later. If you have any questions about what these lines of code do, ask the TA to go over them.
Finally, add the following code to the OnDraw function:
CDC bmpDC;
bmpDC.CreateCompatibleDC(pDC);
bmpDC.SelectObject(&m_splash);
pDC->BitBlt(0, 0, m_splashwid, m_splashhit, &bmpDC, 0, 0, SRCCOPY);
When you are drawing anything in Windows, you use the Device Context (CDC, often called a display context). You are passed a pDC pointer to the current device context for the display you are going to draw into. The function to display an image in a device context assumes you are copying the image from another, compatible, device context. So, we are creating a device context in memory temporarily that we will associate our image with. This is what the first line does. The second assures the device context is compatible with our display, it has the same number of colors, dot pitch, etc. We then "select" the CBitmap object into the device context. Finally, we use BitBlt to copy the image from the temporary device context onto the actual screen. The parameters for BitBlt are the x,y location to draw to, the width and height of the destination, a pointer to the source device context, the x,y locations in the source data, and a parameter that says how the copy will occur. Read the documentation for CDC::BitBlt to see what it can do.
|
Aside: There are generally several ways to do anything in Windows. The method for getting an image onto the screen in this step is probably the easiest, but leaves little option for modification of the image, something we will examine later. |
|
Aside II: I put the CBitmap object into the View class. Is this always the best idea? Where else could I put it and why? Please think about this. |
Now, we're going to add a WAVE resource. I've put a bunch of audio files in Audio Files. You will need a file of type "WAVE", extension. wav, for this operation. Choose the file agrail.wav and save it to your project directory. In Visual Studio, repeat the Add Resource option you did for an image, even selecting Bitmap. But, when you do the Import... button, change the "Files of type" to "Wave Files". Choose the file agrail.wav. Change the ID of the resource that is loaded to IDR_GRAIL.
Add a menu option called "Play Grail". I don't really care where, but make sure you have a handler in the View class for the menu option. Add the following line to the menu handler:
PlaySound(MAKEINTRESOURCE(IDR_GRAIL), AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
Now, add the header mmsystem.h to the View implementation file. Be sure not to put anything before stdafx.h! Normally, I would add this line after #include "myView.h". This provides the prototype for the PlaySound function.
|
MAKEINTRESOURCE converts a resource identifier in Windows from an integer value (what the IDR_values are) to a character string of the from "#id". It used to be that resources in Windows were usually named because the integer ID's were such a pain to keep up with. Better development environments have fixed that problem, but the old functionality still remains. AfxGetInstanceHandle() is an MFC global function that returns a handle to the currently running programs "instance". This is simply an identifier to the operating system that allows your code to be associated with an executable file, where your resources get stored. SND_RESOURCE tells windows that the sound is a resource. It is also possible to play built-in source and files using this function. SND_ASYNC indicates that the function should immediately return, playing the sound in the background. |
Finally, we need to link an additional library to your application. Select the SolutionView tab in the solution explorer and click on the project name (bold face in the field). Select Project/Properties. A dialog box should pop up with the zillion or so options you can set for your project. Expand "Configuration Properties" and "Linker". Click on "Input". In the top left of the dialog box you will see "Configuration:". Change this to: "All Configurations".
Now add "winmm.lib" to Additional Dependencies. We are adding an additional library. You project should compile and play the audio when you select the menu option. The dialog box should look like this:

|
Aside: As I said before, there are generally several ways to do anything in Windows. This method for playing sounds is very easy to use, but has several disadvantages, the most glaring being that you can only play one sound at a time. |
A lot of what we want to do in our programs is to make things occur at points in time. We'll initially set up a simple timer that will play our sound (you may change to another if you like) at some time interval. Add the following definitions to your view header:
UINT_PTR m_timer;
bool m_firstdraw;
and initialize this way in the window constructor:
m_firstdraw = true;
m_timer = 0;
You will find yourself using this "first draw" idea quite a bit. Add the following to the beginning of OnDraw:
if(m_firstdraw)
{
m_firstdraw = false;
OnFirstDraw();
}
Then add a private member function called OnFirstDraw() to the view class. This is a function that is called the first time your window is drawn. This is a very safe place to put things that must be done when the program starts.
Add the following code to OnFirstDraw:
m_timer = SetTimer(1, 2000, NULL);
The first parameter is a non-zero identifier that will be passed to your timer handler. Since we're only going to have one timer handler, I'll just pass 1. The second parameter is the timer period in milliseconds. The third is a pointer to a timer handler function. Believe me, they are way too much trouble to bother with.
What this function has done is to set a timer that will cause an WM_TIMER message to be sent to your application after two seconds.
In ClassView, select your view class. In the properties, press the Messages button. You should see a long list of WM_ messages. Scroll until you see WM_TIMER. To the right you should see a downward pointing arrow. Clicking this will give you a menu with the option "Add OnTimer". Select this. A new OnTimer function has just been added to your View class and you have been jumped to the code in the editor. Delete the highlighted text and add the line to play your sound. In care you forgot, it's:
PlaySound(MAKEINTRESOURCE(IDR_GRAIL), AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
Your program should compile and run, playing your sound every two seconds.
We only want the sound to play one time. Add the following lines to the OnTimer handler:
KillTimer(m_timer);
m_timer = 0;
I'm really fond of setting m_timer to zero when a timer is not set. This way we can always check to see if it is set by checking m_timer.
Now, we are going to create a simple program that does the following at these times (in minutes and seconds):
0:00 Play the sound
0:05 Display the image
0:12 Remove, the image, Play the sound again
We're going to keep track of what we are doing using a simple state machine. We'll assign the following states:
The numbers you assign to the state are arbitrary, but it's handy to have them sequential. You can choose not to have the first state, but I find it easier.
So, lets get started. Add the following definitions to the view class:
int m_state;
DWORD m_starttime;
Add this initializer to the constructor:
m_state = 1; // our initial state
Delete the code in OnFirstDraw and put in the following line:
m_starttime = timeGetTime();
OnTimer(1);
The first line is very important. If you want to have something happen at certain times relative to the start of your program, you need to record the time at the start of your program. If that time is 10:23:45, the events here will be at 10:23:45, 10:23:50, and 10:23:57. The function timeGetTime() is the Windows "multimedia timer", a function that returns an integer value that is the number of milliseconds since the system started. It's very accurate and great for elapsed time applications such as this.
|
All of our state machine decisions are going to be made in OnTimer. We're going to call it ourselves first to fake an initial timer event at time 0. |
Here's the initial code for OnTimer, replacing the previous code:
// Kill any existing timer
if(m_timer)
{
KillTimer(m_timer);
m_timer = 0;
}
// This will keep track of the relative time
// to the next state.
DWORD nexteventtime;
switch(m_state)
{
case 1:
// What we do at the end of state 1, entering state 2
PlaySound(MAKEINTRESOURCE(IDR_GRAIL), AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
m_state++;
nexteventtime = 5000;
break;
case 2:
// What we at the end of state 2, entering state 3
m_state++;
nexteventtime = 12000;
break;
case 3:
// What we at the end of state 3, entering state 4
PlaySound(MAKEINTRESOURCE(IDR_GRAIL), AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
m_state++;
break;
case 4:
break;
}
// Only reset the timer if we are not done
if(m_state < 4)
{
DWORD currenttime = timeGetTime();
DWORD nexttime = m_starttime + nexteventtime;
DWORD tillnext = nexttime > currenttime ? nexttime - currenttime : 0;
m_timer = SetTimer(1, tillnext, NULL);
}
When you run this, you should hear the sound when the program starts and 12 seconds later.
|
Why don't I just set the timer for 5 seconds in case 1 and 7 seconds in case 2 instead of setting it relative to a start time? Setting intervals directly without regard to real-time is a very poor practice and should be avoided. You should always set the time to the remaining time until the next event, not the time inbetween. The problem with that approach is cumulative errors due to slow response times to messages in Windows or other systems. Another bad practice is to set several timers for each of the future events. Timers are very limited and costly resources in most operating systems. Try not to use any more than you need. You usually only need one timer! |
What about the image? Well, in what state should be image be displayed? It should appear when we enter state 3 and disappear when we enter state 4. You can't draw the image in OnTimer(), though, so don't even try. Instead, add an Invalidate() call to cases 2 and 3 above. This will force the window to be redrawn when you enter states 3 and 4. Now, simply add an if statement to OnDraw so that the image is ONLY displayed in state 3:
if(m_state == 3)
{
CDC bmpDC;
bmpDC.CreateCompatibleDC(pDC);
bmpDC.SelectObject(&m_splash);
pDC->BitBlt(0, 0, m_splashwid, m_splashhit, &bmpDC, 0, 0, SRCCOPY);
}
Your program should work, now.
You have now completed the basic step tutorial element. Now proceed to complete the assignment as described above.
Think about everything in terms of state and assign a state for each time step. If you have an image that will be displayed in states 3 and 4, simply use if(m_state >= 3 && m_state <= 4).
Avoid thinking in terms of "at this time I draw this". Instead think in terms of "at this time I change from this condition to this condition". It will save you a lot of grief.
You can play sounds in OnTimer(), but you can't draw.
Think about what it would take to reset your system back to the initial state and reset the start time.