The goal of this step is to build a very simple music synthesizer that can be used as a basis for project 1. Along the way we'll get some experience with how to design a project such as this and learn how to use things such as XML files. Most of this assignment will be tutorial and there will be no written questions.
First, download the Synthie application. This is the starting point for the step assignment. Synthie is very similar to the AudioProcess generate component and you will write code much the same way. The equivalent of CAudioGenerateDoc in the Synthie application is CSynthieView. Synthie uses the single document interface rather than the multiple document interface and does not use the document/view structure. It doesn't put anything in the window, but does use the menu system. Feel free to put anything you want in the windows.
The structure of this project is not only an interesting view of how to build a simple music synthesizer, it's an example of how many programs are written that do sequencing. This type of structure is used by computers that control radio and TV stations, operate machines, control building systems, and many other applications where things get done on a schedule.
There are a lot of steps and this assignment builds up a lot of code, so follow instructions very carefully.
The first step of a design such as this is to decide the class structure. This assignment will create quite a few classes. There is a tendency among new Computer Science students to avoid creating many classes. Often they will only do so when told. Instead, you should embrace the idea of classes as an important tool for modularity. I designed the class structure for this project before I wrote a single line of code.
I have created a page that lists all classes used by this project and what they do. I suggest reading it after you complete the tutorial or referring to it occasionally when you wonder what a class does.
In Visual Studio, do Project/Add Class. In the categories, select C++. On the right you should see the template for C++ Class. That's all we need. Click on Add. The class name will be: CSynthesizer. The other defaults are all okay. Hit Finish. This creates a new, empty class.
In the future I'll just tell you to make the class. I won't repeat these instructions.
In Synthesizer.h, add these variables and access functions:
public:
int NumChannels() {return m_channels;}
double SampleRate() {return m_sampleRate;}
double SamplePeriod() {return m_samplePeriod;}
void SetNumChannels(int n) {m_channels = n;}
void SetSampleRate(double s) {m_sampleRate = s; m_samplePeriod = 1.0 / s;}
private:
int m_channels;
double m_sampleRate;
double m_samplePeriod;
Add code to CSynthesizer::CSynthesizer() to set the default values:
m_channels = 2; m_sampleRate = 44100.; m_samplePeriod = 1 / m_sampleRate;
Right click on CSynthieView and select Add/Add Variable. Make this a private variable of type CSynthesizer and name m_synthesizer. This creates an object of this type as a member variable in CSynthieView. Add this code to the CSynthieView constructor:
m_synthesizer.SetNumChannels(NumChannels()); m_synthesizer.SetSampleRate(SampleRate());
This may seem redundant, but it's good practice to a) always have default values and b) always initialize an object you are using. Do you know the default values are the same as you expect?
In the future I'll just tell you to add variables or functions to a class. I won't repeat these instructions. You can use the Visual studio mechanism to do this or you can just enter them manually into the class.
The first thing we will do is build some dummy code that generates a 5 second tone. We'll use 440 Hz since that's different than the 1000Hz tone the Synthie program already generates. Add these public member functions to CSynthesizer:
void CSynthesizer::Start(void)
{
}
bool CSynthesizer::Generate(double * frame)
{
return false;
}
We need to know the current time (since we started generating), so add this code to Synthesizer.h.
public:
double Time() {return m_time;}
private:
double m_time;
Initialize m_time to zero in both the constructor and the Start function:
void CSynthesizer::Start(void) { m_time = 0; }
Now, add code to CSynthesizer::Generate():
bool CSynthesizer::Generate(double * frame) { double sample = 0.1 * sin(2 * PI * 440 * Time()); for(int c=0; c<NumChannels(); c++) { frame[c] = sample; } m_time += SamplePeriod(); return m_time < 5; }
I will often ask you to add code to a function, but put the entire function into this document. This is to make it easier to tell where the new code goes. The code that already exists will be green.
You'll need to #include <cmath> to use the sin function. What this does is generate a sin wave using the simple sine wave generator, write that to all channels, then update the time. The convention will be that we generate audio until this function returns false.
I will try to do small amounts of code at a time in this step and then provide some way to test whenever possible. This is a very good way to code. Try to be sure something is working as soon as possible. We will often use the debugger for this purpose. If you don't know how to set breakpoints and examine the value of variables it is time to learn. Learning to use a debugger is the single most important skill you will ever learn!
Open the menu named IDR_MAINFRAME for editing. Add a menu option under the Generate menu called Synthesizer. Right click and add an event handler for COMMAND to CSynthieView and add this body to the handler:
void CSynthieView::OnGenerateSynthesizer() { // Call to open the generator output if(!GenerateBegin()) return; m_synthesizer.Start(); short audio[2]; double frame[2]; while(m_synthesizer.Generate(frame)) { audio[0] = RangeBound(frame[0] * 32767); audio[1] = RangeBound(frame[1] * 32767); GenerateWriteFrame(audio); // The progress control if(ProgressAbortCheck()) break; } // Call to close the generator output GenerateEnd(); }
You should be able to compile and run the program now and use the new menu option to generate a 5 second 440Hz tone.
It should be immediately obvious that we are going to generate our audio as floating point values that range from -1 to 1, not as short or values that range from -32768 to 32767. This makes things much easier, but be sure you remember it when you load wave files later on.
We're going to make lots of things that can generate or process audio. It makes things much easier if we add a superclass for that functionality. Create a new public class called CAudioNode, setting "Virtual destructor" to true and add these member functions:
public:
virtual void Start() = 0;
virtual bool Generate() = 0;
double SampleRate() {return m_sampleRate;}
double SamplePeriod() {return m_samplePeriod;}
void SetSampleRate(double s) {m_sampleRate = s; m_samplePeriod = 1/s;}
double *Frame() {return m_frame;}
double Frame(int c) {return m_frame[c];}
protected:
double m_sampleRate;
double m_samplePeriod;
double m_frame[2];
Add code to the CAudioNode constructor to set the values of m_frame[0] and m_frame[1] to zero and set m_sampleRate to 44100 and m_samplePeriod to 1 / m_sampleRate; (Don't type 1 / 44100, do you know why?)
CAudioNode::CAudioNode(void) { m_frame[0] = 0; m_frame[1] = 0; m_sampleRate = 44100; m_samplePeriod = 1.0 / 44100.0; }
We are breaking apart the generation of an audio sample and obtaining the generated frame. We do that on purpose so we can read the frame many times. Note that Start and Generate are abstract functions. They have no implementation, expecting any implementation to be supplied by a derived class.
Create a public class called CSineWave with a base class of CAudioNode. Add these items to the CSineWave class in the header:
public:
virtual void Start();
virtual bool Generate();
void SetFreq(double f) {m_freq = f;}
void SetAmplitude(double a) {m_amp = a;}
private:
double m_freq;
double m_amp;
double m_phase;
Set m_phase to zero, m_amp to 0.1, and m_freq to a default value of 440 in the CSineWave constructor. Then add these two functions:
void CSineWave::Start()
{
m_phase = 0;
}
bool CSineWave::Generate()
{
m_frame[0] = m_amp * sin(m_phase);
m_phase += 2 * PI * m_freq * SamplePeriod();
return true;
}
Be sure you know what this code is doing. If you have questions, please ask.
Create a new public class called CInstrument with a base class of CAudioNode. This is going to the be base class for any instrument we make. Right now it's going to stay pretty empty, but we'll add more functionality later.
Create a new public class called CToneInstrument with a base class of CInstrument. This is going to be an instrument that generates a tone. Add these items to CToneInstrument:
public:
virtual void Start();
virtual bool Generate();
void SetFreq(double f) {m_sinewave.SetFreq(f);}
void SetAmplitude(double a) {m_sinewave.SetAmplitude(a);}
void SetDuration(double d) {m_duration = d;}
private:
CSineWave m_sinewave;
double m_duration;
double m_time;
Add this line to the constructor for CToneInstrument to set the duration of 0.1:
CToneInstrument::CToneInstrument(void) { m_duration = 0.1; }
We always want to have some default and this default value will make it possible to play a note as a short beep without setting a duration. That will be useful later.
You'll have to #include "SineWave.h" as well. Here are the two functions:
void CToneInstrument::Start()
{
m_sinewave.SetSampleRate(SampleRate());
m_sinewave.Start();
m_time = 0;
}
bool CToneInstrument::Generate()
{
// Tell the component to generate an audio sample
m_sinewave.Generate();
// Read the component's sample and make it our resulting frame.
m_frame[0] = m_sinewave.Frame(0);
m_frame[1] = m_sinewave.Frame(1);
// Update time
m_time += SamplePeriod();
// We return true until the time reaches the duration.
return m_time < m_duration;
}
Now that we've done all of that work, we'll add this instrument to our synthesizer. Got to Synthesizer.h and add this new member variable:
private:
std::list<CInstrument *> m_instruments;
You'll need to #include <list> and #include "Instrument.h" to the header file (Synthesizer.h) to do this. Add this code to CSynthesizer::Start():
void CSynthesizer::Start(void) { m_time = 0; CToneInstrument *ti = new CToneInstrument(); ti->SetSampleRate(SampleRate()); ti->SetFreq(440); ti->SetDuration(3); ti->Start(); m_instruments.push_back(ti); }
When we start our synthesizer, we create a single instrument using new CToneInstrument(). Then we configure it and tell it to start. Finally, we add it to our list of instruments that are currently playing.
This will require #include "ToneInstrument.h" in Synthesizer.cpp. Find CSynthesizer::Generate and replace the body with this code:
bool CSynthesizer::Generate(double * frame) { // // Phase 2: Clear all channels to silence // for(int c=0; c<NumChannels(); c++) { frame[c] = 0; } // // Phase 3: Play an active instruments // // // We have a list of active (playing) instruments. We iterate over // that list. For each instrument we call generate, then add the // output to our output frame. If an instrument is done (Generate() // returns false), we remove it from the list. // for(list<CInstrument *>::iterator node = m_instruments.begin(); node!=m_instruments.end(); ) { // Since we are removing an item from the list, we need to know in // advance, what is after it in the list. We keep that node as "next" list<CInstrument *>::iterator next = node; next++; // Get a pointer to the allocated instrument CInstrument *instrument = *node; // Call the generate function if(instrument->Generate()) { // If we returned true, we have a valid sample. Add it // to the frame. for(int c=0; c<NumChannels(); c++) { frame[c] += instrument->Frame(c); } } else { // If we returned false, the instrument is done. Remove it // from the list and delete it from memory. m_instruments.erase(node); delete instrument; } // Move to the next instrument in the list node = next; } // // Phase 5: Determine when we are done // // We are done when there is nothing to play. We'll put something more // complex here later. return !m_instruments.empty(); }
You will need to add "using namespace std;" to your file. This should work and generate a 440Hz tone for 3 seconds.
In case of difficulty, here is a collection of what all of the files should look like at this point.
Read the comments to get an idea of what this is
doing. To summarize, we build a list of active, playing
instruments. We iterate over the list, asking the instrument to
play something for us. When an instrument is done, we remove it
from the list. When the list is empty, we are done playing.
You can probably guess from the numbering that there's some phases to
come that are not there just yet.
We're going to describe our music with a score file written in XML. Create a new text file named test1.score with this content:
<?xml version="1.0" encoding="utf-8"?>
<score bpm="60" beatspermeasure="4">
<instrument instrument="ToneInstrument">
<note measure="1" beat="1" duration="1.9"
note="C4"/>
<note measure="1" beat="3" duration="1.9"
note="G4"/>
<note measure="2" beat="1" duration="3.7"
note="C5"/>
<note measure="2" beat="4.75"
duration="0.25" note="C4"/>
<note measure="2" beat="4.75" duration="0.25"
note="E4"/>
<note measure="2" beat="4.75" duration="0.25"
note="G4"/>
<note measure="3" beat="1.25"
duration="4.75" note="C4"/>
<note measure="3" beat="1.25" duration="4.75"
note="Eb4"/>
<note measure="3" beat="1.25" duration="4.75"
note="G4"/>
</instrument>
</score>
If you don't know any XML, there is lots of information on the web with details. It's a very simple format. A tag is an item surrounded by < and > as in <instrument>. We can add attributes to any tag like this: bmp="60". If a tag encloses other tags, it will be in this format: <tag> ... whatever is enclosed </tag>. If the tag does not enclose other content, it will be in this format: <tag />. You can use whatever tag and attribute names you want to use other than special characters. It is a nested format. The notes above are contained in an instrument tag.
Be sure the extension is .score, not .xml. If you create the file in Visual Studio, it may put an extension of .xml or .txt on the file.
Add this function to CSynthesizer:
void CSynthesizer::OpenScore(CString & filename)
{
}
Add a new menu option called Open Score... to the File menu and create an
event handler in CSythieView.
void CSynthieView::OnFileOpenscore() { static WCHAR BASED_CODE szFilter[] = L"Score files (*.score)|*.score|All Files (*.*)|*.*||"; CFileDialog dlg(TRUE, L".score", NULL, 0, szFilter, NULL); if(dlg.DoModal() != IDOK) return; m_synthesizer.OpenScore(dlg.GetPathName()); }
This project is UNICODE based. So, all character strings must be wide character (16 bits per character) strings. Microsoft provides a macro TEXT() you can use to do that, but you can also just put an L in front of the quotation mark as I have done above. XML expects UNICODE, as does the COM objects engine we'll be using. Heck, the whole world is using UNICODE, so might as well get used to it.
Add this function to CSynthesizer:
void CSynthesizer::Clear(void)
{
m_instruments.clear();
}
We'll add more to this later.
Now, we're going to need some libraries to use XML. Right click on the
project in Solution Explorer and select Properties. Set the configuration
to All Configurations. Expand Configuration Properties and
Linker and
click on Input under Linker. In additional dependencies, add msxml2.lib.
It should look like this:

To use XML, we need to tell Windows that we are going to use COM objects. Add this line to the CSynthesizer constructor:
CSynthesizer::CSynthesizer(void) { m_channels = 2; m_sampleRate = 44100.; m_samplePeriod = 1 / m_sampleRate; m_time = 0; CoInitialize(NULL); }
Add this line to be beginning of Synthesizer.h after the #pragma once:
#include "msxml2.h"
That's the include file we need to use XML functions. In the future you'll likely need to add this other places as well.
I find whenever I'm using XML libraries, it helps to make a simple helper functions in a header. Add a new, empty header file to your project named: xmlhelp.h. You can do this with File/New File, selecting Visual C++ and Header File (.h), then doing a Save As and adding it to your project by right clicking on Synthie in Class View and selecting Add/Existing Item.
#pragma once
#include <msxml2.h>
//
// Name : NextNode()
// Description : This function accepts a reference to a node pointer
// and advances it to the next sibling node.
//
inline void NextNode(CComPtr<IXMLDOMNode> &node)
{
CComPtr<IXMLDOMNode> next;
node->get_nextSibling(&next);
node = next;
}
This is a handy function that makes it easier to traverse a list of XML nodes. Each tag in the XML document becomes a node. I the file above, there will be two nodes for: xml and score. The node for score will have one child node: instrument. The node for instrument will have several child nodes for the note tags.
Now fill in CSynthesizer::OpenScore() with this code:
void CSynthesizer::OpenScore(CString & filename) { Clear(); // // Create an XML document // CComPtr<IXMLDOMDocument> pXMLDoc; bool succeeded = SUCCEEDED(CoCreateInstance(CLSID_DOMDocument, NULL, CLSCTX_INPROC_SERVER, IID_IXMLDOMDocument, (void**)&pXMLDoc)); if(!succeeded) { AfxMessageBox(L"Failed to create an XML document to use"); return; } // Open the XML document VARIANT_BOOL ok; succeeded = SUCCEEDED(pXMLDoc->load(CComVariant(filename), &ok)); if(!succeeded || ok == VARIANT_FALSE) { AfxMessageBox(L"Failed to open XML score file"); return; } // // Traverse the XML document in memory!!!! // Top level tag is <score> // CComPtr<IXMLDOMNode> node; pXMLDoc->get_firstChild(&node); for( ; node != NULL; NextNode(node)) { // Get the name of the node CComBSTR nodeName; node->get_nodeName(&nodeName); } }
This is looping over the top level notes of the XML file. Set a breakpoint at the last curly brace, the closing brace for the loop. It should reach the point twice with values of "xml" and "score" for nodeName.
You may be wondering what CComPtr
is. The type of the objects that will be returned by the functions
above are IXMLDOMDocument * and IXMLDOMNode *. These objects have
reference counters. A reference counter keeps track of
how many pointers are pointing at an object. When that count goes to
zero, the object is destroyed. When I call pXMLDoc->get_firstChild(&node);
it returns a pointer to a node object in the variable node. It
also puts a counter on it saying I am holding a pointer to it. When I am
through using it, I'm responsible for releasing it. What CComPtr does is
put a wrapper around a pointer that will automatically release it when
we change the value of node or destroy it. It's poor-man's garbage
collection. You can do all of this manually using the Release function,
but CComPtr makes it much easier.
The simple thing to remember is this: Instead of IXMLDOMNode *, use
CComPtr<IXMLDOMNode>. It will work the same way, but CComPtr will
take care of the reference count for you.
Before we go any farther, I want to know that this is working. Set a breakpoint on the next to last curly brace as shown here:

Run in the debugger and do a File/Load Score. It should stop at this point twice with node names of "xml" and "score".
We need to process the "score" node type. Add this new private member function:
void CSynthesizer::XmlLoadScore(IXMLDOMNode * xml)
{
}
I generally don't use the CComPtr template class as a function parameter. We know the reference count must be held by the calling function, so you don't need to increment or decrement it.
Now add this code to the loop in OpenScore:
void CSynthesizer::OpenScore(CString & filename) { Clear(); // // Create an XML document // CComPtr<IXMLDOMDocument> pXMLDoc; bool succeeded = SUCCEEDED(CoCreateInstance(CLSID_DOMDocument, NULL, CLSCTX_INPROC_SERVER, IID_IXMLDOMDocument, (void**)&pXMLDoc)); if(!succeeded) { AfxMessageBox(L"Failed to create an XML document to use"); return; } // Open the XML document VARIANT_BOOL ok; succeeded = SUCCEEDED(pXMLDoc->load(CComVariant(filename), &ok)); if(!succeeded || ok == VARIANT_FALSE) { AfxMessageBox(L"Failed to open XML score file"); return; } // // Traverse the XML document in memory!!!! // Top level tag is <score> // CComPtr<IXMLDOMNode> node; pXMLDoc->get_firstChild(&node); for( ; node != NULL; NextNode(node)) { // Get the name of the node CComBSTR nodeName; node->get_nodeName(&nodeName); if(nodeName == "score") { XmlLoadScore(node); } } }
The score tag has both children (instruments) and attributes. An attribute are the items like bpm and beats per measure. We'll scan the attributes first. But, we need a place to put the information. Add these two new private member variables to CSynthesizer.
double m_bpm;
int m_beatspermeasure;
double m_secperbeat;
Set the default values in the constructor to 120 for m_bpm, 0.5 for m_secperbeat, and 4 for m_beatspermeasure.
CSynthesizer::CSynthesizer(void) { m_channels = 2; m_sampleRate = 44100.; m_samplePeriod = 1 / m_sampleRate; m_time = 0; m_bpm = 120; m_beatspermeasure = 4; m_secperbeat = 1; CoInitialize(NULL); }
Here's the code for CSynthesizer::XmlLoadScore. Add this function:
void CSynthesizer::XmlLoadScore(IXMLDOMNode * xml)
{
// Get a list of all attribute nodes and the
// length of that list
CComPtr<IXMLDOMNamedNodeMap> attributes;
xml->get_attributes(&attributes);
long len;
attributes->get_length(&len);
// Loop over the list of attributes
for(int i=0; i<len; i++)
{
// Get attribute i
CComPtr<IXMLDOMNode> attrib;
attributes->get_item(i, &attrib);
// Get the name of the attribute
CComBSTR name;
attrib->get_nodeName(&name);
// Get the value of the attribute. A CComVariant is a variable
// that can have any type. It loads the attribute value as a
// string (UNICODE), but we can then change it to an integer
// (VT_I4) or double (VT_R8) using the ChangeType function
// and then read its integer or double value from a member variable.
CComVariant value;
attrib->get_nodeValue(&value);
if(name == "bpm")
{
value.ChangeType(VT_R8);
m_bpm = value.dblVal;
m_secperbeat = 1 / (m_bpm / 60);
}
else if(name == "beatspermeasure")
{
value.ChangeType(VT_I4);
m_beatspermeasure = value.intVal;
}
}
CComPtr<IXMLDOMNode> node;
xml->get_firstChild(&node);
for( ; node != NULL; NextNode(node))
{
// Get the name of the node
CComBSTR name;
node->get_nodeName(&name);
}
}
Check to see that this is working. Set a breakpoint at the end of the function. When the breakpoint is hit, look at the value of m_bpm. It should be 60 rather than the default value of 120. Using breakpoints to test lets you know code is working to a point.
I went ahead and created the loop that will scan the children of a score node. We'll do that next.
You probably get the picture by now. We create a function for each tag and call it when we see it. Create this private function:
void CSynthesizer::XmlLoadInstrument(IXMLDOMNode * xml)
{
}
Then add this code to the loop that reads the children of the score node:
if(name == L"instrument")
{
XmlLoadInstrument(node);
}
Go ahead and create this function:
void CSynthesizer::XmlLoadNote(IXMLDOMNode * xml, std::wstring & instrument)
{
}
Now we can fill in the body of XmlLoadInstrument:
void CSynthesizer::XmlLoadInstrument(IXMLDOMNode * xml)
{
wstring instrument = L"";
// Get a list of all attribute nodes and the
// length of that list
CComPtr<IXMLDOMNamedNodeMap> attributes;
xml->get_attributes(&attributes);
long len;
attributes->get_length(&len);
// Loop over the list of attributes
for(int i=0; i<len; i++)
{
// Get attribute i
CComPtr<IXMLDOMNode> attrib;
attributes->get_item(i, &attrib);
// Get the name of the attribute
CComBSTR name;
attrib->get_nodeName(&name);
// Get the value of the attribute.
CComVariant value;
attrib->get_nodeValue(&value);
if(name == "instrument")
{
instrument = value.bstrVal;
}
}
CComPtr<IXMLDOMNode> node;
xml->get_firstChild(&node);
for( ; node != NULL; NextNode(node))
{
// Get the name of the node
CComBSTR name;
node->get_nodeName(&name);
if(name == L"note")
{
XmlLoadNote(node, instrument);
}
}
}
Note the use of wstring for the UNICODE string containing the instrument name. The node value is loaded as a string first, so we don't have to change it, we just read it. BSTR is a Microsoft "basic string", a 16 bit string used by COM objects.
Just to be sure, here is what Synthesizer.h and Synthesizer.cpp look like up to this point.
We need to keep track of all of the notes that we load in, since we will be playing them later. The easiest solution is to create a class to store them. Create a new public class called CNote. Now all we need to describe the score is a list (or vector) of notes. Add this member variable to CSynthesizer:
std::vector<CNote> m_notes;
You know what header files you'll have to include, right? Add this code to CSynthesizer::Clear:
m_notes.clear();
Now, what does a note need to know? At the least, it needs to know the beat and measure when it will be played. I'm also going to add an operator overload to make it easy to sort the notes later. Add these member variables and access functions to CNote:
class CNote { public: CNote(void); ~CNote(void); int Measure() const {return m_measure;} double Beat() const {return m_beat;} const std::wstring &Instrument() const {return m_instrument;} IXMLDOMNode *Node() {return m_node;} private: std::wstring m_instrument; int m_measure; double m_beat; CComPtr<IXMLDOMNode> m_node; };
Don't add #include "Instrument.h" to Note.h. The member variable m_instrument is the name of the instrument the note will be used for, not a pointer to the actual instrument. Later, Instrument.h will use CNote, so it will have to have a #include "Note.h". If you Instrument.h include Note.h and Note.h include Instrument.h, you get a circular list of includes, which makes C++ just drop some of the header content.
Well, what do you know? I'm also going to keep the XML node for the note around! Why am I doing that, you may wonder? We'll use it later on to configure our instruments!
Add this public function to CNote:
void CNote::XmlLoad(IXMLDOMNode * xml,
std::wstring & instrument)
{
}
Now here is the body for CSynthesizer::XmlLoadNote():
void CSynthesizer::XmlLoadNote(IXMLDOMNode * xml, std::wstring & instrument) { m_notes.push_back(CNote()); m_notes.back().XmlLoad(xml, instrument); }
Now, we'll let CNote::XmlLoad() do all of the heavy lifting:
void CNote::XmlLoad(IXMLDOMNode * xml, std::wstring & instrument)
{
// Remember the xml node and the instrument.
m_node = xml;
m_instrument = instrument;
// Get a list of all attribute nodes and the
// length of that list
CComPtr<IXMLDOMNamedNodeMap> attributes;
xml->get_attributes(&attributes);
long len;
attributes->get_length(&len);
// Loop over the list of attributes
for(int i=0; i<len; i++)
{
// Get attribute i
CComPtr<IXMLDOMNode> attrib;
attributes->get_item(i, &attrib);
// Get the name of the attribute
CComBSTR name;
attrib->get_nodeName(&name);
// Get the value of the attribute.
CComVariant value;
attrib->get_nodeValue(&value);
if(name == "measure")
{
// The file has measures that start at 1.
// We'll make them start at zero instead.
value.ChangeType(VT_I4);
m_measure = value.intVal - 1;
}
else if(name == "beat")
{
// Same thing for the beats.
value.ChangeType(VT_R8);
m_beat = value.dblVal - 1;
}
}
}
You should set breakpoints in this routine and be sure it is reading the beats and measures correctly. Consider setting a breakpoint at the end of the function and looking at those values each time through.
It's a common trick that, instead of having the file
reading all handled by a single class, we pass the note to the class
that will use it and let it read the data. This works really well
with a document model like XML, where we just pass a pointer to the
node.
I hope you're figured out that you need to #include "xmlhelp.h" to use
some of these functions.
We need to sort the list of notes into order, since we may have multiple instruments. To make that easy, just add an operator< to the CNote class:
class CNote { public: CNote(void); ~CNote(void); int Measure() const {return m_measure;} double Beat() const {return m_beat;} const std::wstring &Instrument() const {return m_instrument;} IXMLDOMNode *Node() {return m_node;} void XmlLoad(IXMLDOMNode * xml, std::wstring & instrument); bool operator<(const CNote &b) { if(m_measure < b.m_measure) return true; if(m_measure > b.m_measure) return false; if(m_beat < b.m_beat) return true; return false; } private: std::wstring m_instrument; int m_measure; double m_beat; CComPtr<IXMLDOMNode> m_node; };
Now add this line to the end of CSythesizer::OpenScore():
sort(m_notes.begin(), m_notes.end());
That's all there is to it.
The vector m_notes is our score in sorted order. It's the notes we are going to play. We need to keep track of where we are in the list of notes and the current beat and measure. Add these new private member variables to the CSynthesizer class:
// The current note we are playing
int m_currentNote;
// The current measure
int m_measure;
// The current beat within the measure
double m_beat;
Change the function CSynthesizer::Start() to this:
void CSynthesizer::Start(void)
{
m_instruments.clear();
m_currentNote = 0;
m_measure = 0;
m_beat = 0;
m_time = 0;
}
This clears the instruments list and sets the current note to the first one to play and sets the current time, beat, and measure all to zero.
Add this code to be end of CSynthesizer::Generate() right before the return:
//
// Phase 4: Advance the time and beats
//
// Time advances by the sample period
m_time += SamplePeriod();
// Beat advances by the sample period divided by the
// number of seconds per beat. The inverse of seconds
// per beat is beats per second.
m_beat += SamplePeriod() / m_secperbeat;
// When the measure is complete, we move to
// a new measure. We might be a fraction into
// the new measure, so we subtract out rather
// than just setting to zero.
if(m_beat > m_beatspermeasure)
{
m_beat -= m_beatspermeasure;
m_measure++;
}
The function CSynthesizer::Generate has several phases to it. They are:
We are currently doing 2, 3, and 4 and part of 5. Let's examine #1. If there are any notes that the time has been reached for, we can play them.
Add this code to the beginning of CSynthesizer::Generate():
//
// Phase 1: Determine if any notes need to be played.
//
while(m_currentNote < (int)m_notes.size())
{
// Get a pointer to the current note
CNote *note = &m_notes[m_currentNote];
// If the measure is in the future we can't play
// this note just yet.
if(note->Measure() > m_measure)
break;
// If this is the current measure, but the
// beat has not been reached, we can't play
// this note.
if(note->Measure() == m_measure && note->Beat() > m_beat)
break;
//
// Play the note!
//
// Create the instrument object
CInstrument *instrument = NULL;
if(note->Instrument() == L"ToneInstrument")
{
instrument = new CToneInstrument();
}
// Configure the instrument object
if(instrument != NULL)
{
instrument->SetSampleRate(SampleRate());
instrument->Start();
m_instruments.push_back(instrument);
}
m_currentNote++;
}
We need to fix up that last phase (phase 5). Before, we considered the generator to be done when there were no instruments. However, now we need it to be done when there are no instruments and no notes that still need to be played. Change the return at the end of CSynthesizer::Generate() to this:
//
// Phase 5: Determine when we are done
//
// We are done when there is nothing to play. We'll put something more
// complex here later.
return !m_instruments.empty() || m_currentNote < (int)m_notes.size();
Now compile and run this. You should hear a series of beeps. Each beep is when a note would be played. Note that we are not setting the pitch or the duration just yet. We just making sure the timing is right. This is why we wanted the instrument to have a default duration.
Here's what it should sound like ![]()
We are playing the note at the right time, but we need to set the correct pitch and duration. The easiest way to handle this is to let the instrument do it. Add this function to CInstrument:
virtual void SetNote(CNote *note) = 0;
You'll need to #include "Note.h" of course. Do I need to keep mentioning this?
Now, add this function to CToneInstrument:
void CToneInstrument::SetNote(CNote *note)
{
// Get a list of all attribute nodes and the
// length of that list
CComPtr<IXMLDOMNamedNodeMap> attributes;
note->Node()->get_attributes(&attributes);
long len;
attributes->get_length(&len);
// Loop over the list of attributes
for(int i=0; i<len; i++)
{
// Get attribute i
CComPtr<IXMLDOMNode> attrib;
attributes->get_item(i, &attrib);
// Get the name of the attribute
CComBSTR name;
attrib->get_nodeName(&name);
// Get the value of the attribute. A CComVariant is a variable
// that can have any type. It loads the attribute value as a
// string (UNICODE), but we can then change it to an integer
// (VT_I4) or double (VT_R8) using the ChangeType function
// and then read its integer or double value from a member variable.
CComVariant value;
attrib->get_nodeValue(&value);
if(name == "duration")
{
value.ChangeType(VT_R8);
SetDuration(value.dblVal);
}
}
}
Next, add this line of code to CSynthesizer::Generate() between the lines instrument->SetSampleRate(SampleRate()); and instrument->Start():
instrument->SetNote(note);
Now run the program. It should play the notes with the correct durations. We've not set the pitch just yet, but the duration should be right.
Here's what it should sound like ![]()
To set the note frequency, we need to change the name into a frequency. Just #include "Notes.h" which is already included in the Synthie project and add this code to CToneInstrument::SetNote() to test for the "note" attribute:
else if(name == "note")
{
SetFreq(NoteToFrequency(value.bstrVal));
}
When you run this and play, you should hear the opening strains of Also Sprach Zarathustra, aka the theme to 2001, A Space Odyssey.
Here's what it should sound like ![]()
Right now you can clearly hear clicks in the generated sound. Clicks occur when we cut off a waveform in mid-wave, so the speak. Here's what happens:

We'll create a simple component to add a ramp to the beginning and end of a sound, then we'll add that to the ToneInstrument. We call this an Attack/Release (AR) component.
Create a new class named CAR with a base class of CAudioNode. Add these to the CAR class:
class CAR : public CAudioNode { public: CAR(void); ~CAR(void); public: virtual void Start(); virtual bool Generate(); void SetAttack(double a) {m_attack = a;} void SetRelease(double r) {m_release = r;} void SetDuration(double d) {m_duration = d;} void SetSource(CAudioNode *node) {m_source = node;} private: // The attack time in seconds double m_attack; // The release time in seconds double m_release; // Duration of the note in seconds double m_duration; // Where we get the samples we will change CAudioNode *m_source; // Current time when generating double m_time; };
Add code to the constructor to set the default attack and release times to 0.01 seconds and the default duration to 0.1 second. Set m_source to NULL in the constructor as well.
CAR::CAR(void) { m_attack = 0.01; m_release = 0.01; m_duration = 0.1; m_source = NULL; }
Here's the code for Start and Generate:
void CAR::Start()
{
m_time = 0;
}
bool CAR::Generate()
{
//
// Part 1: Determine the current gain. This depends
// on what time period we are in.
//
double gain = 1;
if(m_time < m_attack)
{
// m_time/m_attack goes from 0 to 1 in the period
gain = m_time / m_attack;
}
else if(m_time > (m_duration - m_release) && m_time < m_duration)
{
double releaseStart = m_duration - m_release;
// (m_time - releaseStart) / (m_duration - releaseStart) goes
// from 0 to 1 in the release period. Subtract from 1 to make
// it go from 1 to 0
gain = 1 - (m_time - releaseStart) / (m_duration - releaseStart);
}
else if(m_time >= m_duration)
{
// If we are done, set the audio to silence and
// return false.
m_frame[0] = m_frame[1] = 0;
return false;
}
//
// Part 2: Generate the output from the input multiplied
// by the gain.
//
if(m_source != NULL)
{
m_frame[0] = m_source->Frame(0) * gain;
m_frame[1] = m_source->Frame(1) * gain;
}
//
// Part 3: Update the time.
//
m_time += SamplePeriod();
return true;
}
Now that we have an attack/release component, let's add it to CToneInstrument. First, add the member variable for it to CToneInstrument:
CAR m_ar;
Now we'll add lines to CToneInstrument::Start() to set up the AR component:
void CToneInstrument::Start() { m_sinewave.SetSampleRate(SampleRate()); m_sinewave.Start(); m_time = 0; // Tell the AR object it gets its samples from // the sine wave object. m_ar.SetSource(&m_sinewave); m_ar.SetSampleRate(SampleRate()); m_ar.Start(); }
We'll now use the AR component to keep track of the duration. In CToneInstrument, change the function SetDuration to this:
void SetDuration(double d) {m_ar.SetDuration(d);}
Finally, change CToneInstrument::Generate() to this:
bool CToneInstrument::Generate()
{
m_sinewave.Generate();
bool valid = m_ar.Generate();
m_frame[0] = m_ar.Frame(0);
m_frame[1] = m_ar.Frame(1);
m_time += SamplePeriod();
return valid;
}
It is important to understand what is going on here.
We call Generate on the sine wave generator first. It creates a single
frame of the sine wave. Then we call generate on the AR component.
It reads that frame from the sine wave generator (because we told it
that is its source) and generates a frame with the envelope applied.
Then we just set the instrument's frame to the output of the AR
component and advance the time.
The AR component will return true as long as it is still generating
audio. When it is no longer valid, we return that condition so we know
we are done. I use the convention that a component that is no longer
generating sets its audio frame to silence so I don't have to test for
that case when reading its frame.
At this point the program should run and generate the same audio as before, but without the clicks.
Here's what it should sound like ![]()
The variable m_duration in CToneInstrument is no longer used and should be removed.
We need some way for a note to set the attack and release times. We can do that easily enough by adding this code in CToneInstrument::SetNote():
else if(name == "attack")
{
value.ChangeType(VT_R8);
m_ar.SetAttack(value.dblVal);
}
else if(name == "release")
{
value.ChangeType(VT_R8);
m_ar.SetRelease(value.dblVal);
}
This completes the tutorial. Turn this in on 10-15-09 just to be sure you've got it done. Then your group should use one of these versions as a starting point for Project 1.
It's easier to build simple building blocks for an instrument than to build a single big bunch of code. Consider doing that.
It's easy to add new instruments. Just create the instrument class, then add code to CSynthesizer::Generate() like this:
// Create the instrument object
CInstrument *instrument = NULL;
if(note->Instrument() == L"ToneInstrument")
{
instrument = new CToneInstrument();
}
else if(note->Instrument() == L"Organ")
{
instrument = new COrgan();
}
Create a class call CWave that you load wave audio into. Then any instrument that plays that wave should be passed a pointer to the CWave object.
It's easy to add new nodes to the XML file to do other things. You can have children of a note if you like, for example. You can also create an instrument name that is not an actual instrument that is parsed by CSynthesizer instead. I might add a note to XML like this:
<instrument instrument="Synthesizer"> <note measure="3" beat="1.25" bpm="80"/> </instrument>
This would be allow us to change the tempo at a particular place, like this:
// Create the instrument object
CInstrument *instrument = NULL;
if(note->Instrument() == L"ToneInstrument")
{
instrument = new CToneInstrument();
}
else if(note->Instrument() == L"Synthesizer")
{
// I don't set instrument here, so it stays NULL
// Get a list of all attribute nodes and the
// length of that list
CComPtr<IXMLDOMNamedNodeMap> attributes;
node->get_attributes(&attributes);
long len;
attributes->get_length(&len);
// Loop over the list of attributes
for(int i=0; i<len; i++)
{
// Get attribute i
CComPtr<IXMLDOMNode> attrib;
attributes->get_item(i, &attrib);
// Get the name of the attribute
CComBSTR name;
attrib->get_nodeName(&name);
CComVariant value;
attrib->get_nodeValue(&value);
if(name == "bpm")
{
value.ChangeType(VT_R8);
m_bpm = value.dblVal;
m_secperbeat = 1 / (m_bpm / 60);
}
else if(name == "beatspermeasure")
{
value.ChangeType(VT_I4);
m_beatspermeasure = value.intVal;
}
}
Were I doing this, I would likely put what is currently in the body of the if into a function and just call it.
Consider adding a way to keep track of measures and beats so you don't have to absolutely enter then. For example, I might use "-" to mean same measure as the last note or "+" next measure. If you do this, you'll need to create a way to keep track of that information and pass it to CNote::XmlLoad()
I deliberately limited what I'm passing around as much as possible. But, it can make things easier if you pass a pointer to CSynthesizer to objects in their constructor. There are advantages as a place to keep some global information.
Right now each instrument is a newly created, fresh object. You may find it useful to have persistent settings on an instrument rather than setting them for every note. For example, I might want to set the attack time once and use it for all subsequent notes. There are several ways to do this. Perhaps the easiest is to make a factor class for each instrument. This is a single object such as CToneInstrumentFactory that you instantiate exactly one of. Then, instead of instrument = new CToneInstrument(), you would have something like instrument = m_toneInstrumentFactor->Create(); You can then pass the CNote to that object and it can read off the attack and decay if supplied. If you do this, you should pass the CNote object, like this:
// Create the instrument object
CInstrument *instrument = NULL;
if(note->Instrument() == L"ToneInstrument")
{
instrument = m_toneInstrumentFactor->Create(note);
}