Making instrument information persistent:
Adding an instrument factory to Synthie

The Synthie tutorial is designed to be the most simple possible starting point for project 1. However, it should be clear that there are some things that are cumbersome to do with the program as it stands. Suppose you are creating the organ. Right now everything about a note must be in the note node in the XML file. Wouldn't it make more sense if we could set things and have them be persistent?  So, instead of:

		<note measure="1" beat="1" duration="1" note="C4" drawbar="88100088"/>
		<note measure="1" beat="2" duration="1" note="D4" drawbar="88100088"/>
		<note measure="1" beat="3" duration="1" note="E4" drawbar="88100088"/>

Wouldn't it me much nicer to do something like this:

		<note measure="1" beat="1" duration="1" note="C4" drawbar="88100088"/>
		<note measure="1" beat="2" duration="1" note="D4"/>
		<note measure="1" beat="3" duration="1" note="E4"/>

Then every note after the first one would assume the drawbar setting is the same. This tutorial shows how to do this.

A simple instrument

I'm going to make a very simple instrument that plays sine waves and odd harmonics. The instrument will generate the sum of a sine wave and the first three odd harmonics, each of which can be set to a separate volume.  We'll call this instrument OddSines. First we'll create the instrument in the old structure.

First create a new class called COddSinesInstrument derived from CInstrument.  Initially set the class header to look like this:

#pragma once
#include "instrument.h"
#include "AR.h"

class COddSinesInstrument :
    public CInstrument
{
public:
    COddSinesInstrument(void);
    ~COddSinesInstrument(void);

    virtual void Start();
    virtual bool Generate();
    virtual void SetNote(CNote *note);
    
    void SetDuration(double d) {m_ar.SetDuration(d);}
    void SetFreq(double f) {}

private:
    CAR         m_ar;
    double  m_time;
};

And add these functions to OddSinesInstrument.cpp:

void COddSinesInstrument::Start()
{
    m_ar.SetSampleRate(SampleRate());
    m_ar.Start();
    m_time = 0;
}


void COddSinesInstrument::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);

        CComVariant value;
        attrib->get_nodeValue(&value);

        if(name == "duration")
        {
            value.ChangeType(VT_R8);
            SetDuration(value.dblVal);
        }
        else if(name == "note")
        {
            SetFreq(NoteToFrequency(value.bstrVal));
        }
        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);
        }
    }

}


bool COddSinesInstrument::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;
}
This is mostly a copy from CToneInstrument with the sine wave generator removed. I got the question of how to add multiple sine waves together, since CAR only accepts a single instrument. I is very easy to create a new object that accepts multiple inputs and adds them together. This would be a mixer. But, you don't really need that here. Just create a new object to generate our sinewaves.

Create a new class called COddSines derived from CAudioNode.  Here's the header:

#pragma once
#include "audionode.h"

class COddSines :
    public CAudioNode
{
public:
    COddSines(void);
    ~COddSines(void);

public:
    virtual void Start();
    virtual bool Generate();

    void SetFreq(double f) {m_freq = f;}
    void SetAmplitude(int i, double a) {m_amp[i] = a;}

private:
    double m_freq;
    double m_phase;
    double m_amp[4];
};

And here is OddSines.cpp:

#include "StdAfx.h"
#include "OddSines.h"
#include <cmath>

COddSines::COddSines(void)
{
    for(int i=0; i<4;  i++)
        m_amp[i] = 1;
}

COddSines::~COddSines(void)
{
}


void COddSines::Start()
{
    m_phase = 0;
}

bool COddSines::Generate()
{
    double sample = 0;
    for(int i=0;  i<4;  i++)
    {
        sample += m_amp[i] * sin(m_phase * (i * 2 + 1));
    }

    m_frame[1] = m_frame[0] = sample;
    m_phase += 2 * PI * m_freq * SamplePeriod();

    return true;
}

Now let's make OddSinesInstrument actually use it.  Add this new member variable to COddSinesInstrument:

    COddSines   m_sines;

Make the SetFreq function set the frequency for this generator:

    void SetFreq(double f) {m_sines.SetFreq(f);}

Add this code to COddSinesInstrument::Start to start this new component and connect it to the AR component:

    m_sines.SetSampleRate(SampleRate());
    m_sines.Start();
    m_ar.SetSource(&m_sines);

Add this code to the front of COddSinesInstrument::Generate() to generate the audio samples:

    m_sines.Generate();

Before we go on, let's get this playing. The only thing we need to get this to play is to recognize when the instrument is selected. In the section entitled "Play the node!" in Synthesizer.cpp, add this code after the if statement (the green code is already there):

        CInstrument *instrument = NULL;
        if(note->Instrument() == L"ToneInstrument")
        {
            instrument = new CToneInstrument();
        }
        else if(note->Instrument() == L"OddSines")
        {
            instrument = new COddSinesInstrument();
        }

If you change the name of the instrument in your test score to OddSines, you can play this instrument. It will be loud, because we're not changing the amplitude, and it will be a hit harsh because all of the harmonics are full amplitude.

Now let's do something about that amplitude. Add this attribute to the first three notes in your score:

		<note measure="1" beat="1" duration="1.9" note="C4" a1=".5" a3=".4" a5=".3" a7=".2"/>

This is an easy way to specify the amplitudes. Now add the attribute options to COddSinesInstrument::SetNote():

        else if(name == "a1")
        {
            value.ChangeType(VT_R8);
            m_sines.SetAmplitude(0, value.dblVal);
        }
        else if(name == "a3")
        {
            value.ChangeType(VT_R8);
            m_sines.SetAmplitude(1, value.dblVal);
        }
        else if(name == "a5")
        {
            value.ChangeType(VT_R8);
            m_sines.SetAmplitude(2, value.dblVal);
        }
        else if(name == "a7")
        {
            value.ChangeType(VT_R8);
            m_sines.SetAmplitude(3, value.dblVal);
        }

You've added a new sound to your synthesizer! See how easy this was? The steps were:

  1. Create a new instrument class
  2. Create a new sound generating class
  3. Add the new sound generating class to the new instrument class
  4. Add code to generate instruments when in the score
  5. Add any new attributes to control the instrument

A Persistent Instrument

It should be immediately obvious that adding those attributes to every note will be a real pain. Note only is it a lot of work, but what if you change your mind? I didn't like the original sound, so I changed the values. That meant I had to change three notes the same way. It would be even worse with a real score!  So, we'll now make these values persistent instead.

A factory class is a class that exists solely to create objects of another class. This step in the tutorial describes how to create a different instrument that uses a factory class to store persistent data about an instrument.

Create a new class called COddSinesFactory.  Add these two functions to COddSinesFactory:

COddSinesInstrument *COddSinesFactory::CreateInstrument()
{
    COddSinesInstrument *instrument = new COddSinesInstrument();

    return instrument;
}


void COddSinesFactory::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);

        CComVariant value;
        attrib->get_nodeValue(&value);

    }

}

Now, add this new private member variable to CSynthesizer:

    COddSinesFactory m_oddsinesfactory;

Change this line in CSynthesizer::Generate():

All we have done here is create a class that does the object allocation just like before. The difference is that an object of that class sticks around. We can tell it things and it will remember. We'll tell it things by passing it a note.
            instrument = new COddSinesInstrument();

to:

            m_oddsinesfactory.SetNote(note);
            instrument = m_oddsinesfactory.CreateInstrument();

Persistent Amplitudes

Add this private member variable to COddSinesFactory:

    double m_amps[4];

Add this code to the COddSinesFactory constructor to give this a default value:

    for(int i=0;  i<4;  i++)
        m_amps[i] = 0.1;

We'll need code to read these values. There's already code in COddSinesFactory to iterate over the attributes.  Add this code to read those attributes:

        if(name == "a1")
        {
            value.ChangeType(VT_R8);
            m_amps[0] = value.dblVal;
        }
        else if(name == "a3")
        {
            value.ChangeType(VT_R8);
            m_amps[1] = value.dblVal;
        }
        else if(name == "a5")
        {
            value.ChangeType(VT_R8);
            m_amps[2] = value.dblVal;
        }
        else if(name == "a7")
        {
            value.ChangeType(VT_R8);
            m_amps[3] = value.dblVal;
        }

Now, each time we create a new COddSinesInstrument, set these values in it:

COddSinesInstrument *COddSinesFactory::CreateInstrument()
{
    COddSinesInstrument *instrument = new COddSinesInstrument();

    for(int i=0;  i<4;  i++)
    {
        instrument->SetAmplitude(i, m_amps[i]);
    }

    return instrument;
}

You'll need to add this function to COddSinesInstrument:

    void SetAmplitude(int i, double d) {m_sines.SetAmplitude(i, d);}

This will now remember the amplitude settings.

It should be obvious that we can remove the code to read the attributes a1-a7 from COddSinesInstrument. They are now set by the factory and the existing code is redundant.

This is a very common technique. I'm allocating objects, but I need default values to be set when I allocate each object. If I create a factory class, it can create the object and set any defaults I need. Note that most of this tutorial was creating a new instrument. Creating the factory was relatively simple.

Other Ideas

There's lots of things you can do to make this a bit fancier.

Suppose I send a note to the factory with no note attribute. The factory could detect that and note generate any instrument at all. If CreateInstrument returns NULL, CSynthesizer won't add this node to it's list. Why is this useful? Suppose you just want to change a setting on the instrument, but you're not playing a note. You can do this by indicating in an attribute some way not to create the actual instrument.

The factory class is a good place to keep data that is shared by instruments. A good example is wave tables. More on that in another lesson.

Sometimes you need an instrument that is not polyphonic, but rather monophonic. You can do that by making the factory class the actual instrument. You'll need to provide a way to get audio from this instrument, though.

Consider creating a superclass for your instrument factory classes. Then you can create code that's common to all instruments.

CSE 471

 

 

 

CSE 471