Jones Code


Jones Code

The source code for Jones can be split into two categories: UI and DirectX Audio management. Since this book is about programming DirectX Audio (not programming Windows with MFC), the source code for Jones' UI is not discussed. Extra effort has been made to separate functionality so that just about everything we care about is covered by the audio management code (starting with the CAudio class), and there's no need to discuss the UI. Of course, the UI code is included with the projects on the companion CD and it is documented, so you are welcome to use it in any way you'd like.

Audio Data Structures

Jones uses the same classes that we created for the Loader in the previous chapter — CAudio and CSegment — though with some significant changes and additions, which we will discuss.

Jones also uses an additional class: CSegState. CSegState keeps track of one playing instance of a Segment via an IDirectMusicSegmentState8 interface pointer. This can be used to stop the specific instance of the playing Segment, as well as perform other operations on that Segment in real time. CSegState also tracks CSegment for convenience.

Since we can have more than one instance of a Segment playing at the same time, we manage the CSegStates with CSegStateList. To facilitate this, CSegState and CSegStateList are based on the list management classes CMyNode and CMyList, respectively.

class CSegState : public CMyNode
{
public:
    // Constructor.
    CSegState(CSegment *pSegment, IDirectMusicSegmentState8 *pSegState)
    {
        m_pSegState = pSegState;
        pSegState->AddRef();
        m_pSegment = pSegment;
    }
    ~CSegState()
    {
        m_pSegState->Release();
    }
    // We keep a linked list of SegmentStates.
    CSegState *GetNext() { return (CSegState *) CMyNode::GetNext(); };
    // Access methods.
    CSegment *GetSegment() { return m_pSegment; };
    IDirectMusicSegmentState8 *GetSegState() { return m_pSegState; };
private:
    CSegment *                  m_pSegment;     // The Segment that this is playing.
    IDirectMusicSegmentState8 * m_pSegState;    // The DirectMusic SegmentState object.
};

/* CSegStateList
   Based on CMyList, this manages a linked list of CSegStates.
*/

class CSegStateList : public CMyList
{
public:
    // Overrides for CMyList methods.
    CSegState * GetHead() { return (CSegState *) CMyList::GetHead(); };
    CSegState *RemoveHead() { return (CSegState *) CMyList::RemoveHead(); };
    // Clear list and release all references.
    void Clear();
};
 

The CSegment class manages a loaded instance of a Segment. Like CSegState, it is based on CMyList in order to manage a linked list of CSegments.

Each CSegment can have a list of children CSegments, managed by the CSegmentList class.

class CSegmentList : public CMyList
{
public:
    // Overrides for CMyList methods.
    CSegment *RemoveHead() { return (CSegment *) CMyList::RemoveHead(); };
    // Clear list and release all references.
    void Clear(IDirectMusicPerformance8 *pPerf);
};

// We create Segments from file as well as extract them from embedded Bands and motifs.
// We'll keep track of how a Segment was created for display purposes.

typedef enum _SEGMENT_SOURCE
{
    SEGMENT_BAND                = 1,
    SEGMENT_MOTIF               = 2,
    SEGMENT_FILE                = 3
} SEGMENT_SOURCE;

/*  CSegment
    This manages one Segment.
    Segments can also carry a list of children Segments.
*/

class CSegment : public CMyNode
{
public:
    CSegment();
    ~CSegment();
    CSegment *GetNext() { return (CSegment *) CMyNode::GetNext(); };
    void Init(IDirectMusicSegment8 *pSegment,
        IDirectMusicPerformance8 *pPerf,SEGMENT_SOURCE ssType);
    void Clear(IDirectMusicPerformance8 *pPerf);
    void SetFlags(DWORD dwFlags) { m_dwPlayFlags = dwFlags; };
    DWORD GetFlags() { return m_dwPlayFlags; };
    void GetName(char *pszBuffer);
    SEGMENT_SOURCE GetType() { return m_ssType; };
    CSegment * EnumChildren(DWORD dwIndex) {
        return (CSegment *) m_ChildSegments.GetAtIndex(dwIndex); };
    bool HasChildren() { return !m_ChildSegments.IsEmpty(); };
    CSegment *GetTransition() { return m_pTransition; };
    void SetTransition(CSegment * pTransition) { m_pTransition = pTransition; };
    bool HasEmbeddedAudioPath();
    IDirectMusicSegment8 * GetSegment() { return m_pSegment; };
private:
    IDirectMusicSegment8 *      m_pSegment;           // The DirectMusic Segment that
                                                      // this manages.
    DWORD                       m_dwPlayFlags;        // DMUS_SEGF_ Playback flags.
    DMUS_OBJECTDESC             m_Desc;               // DirectMusic object descriptor.
                                                      // Used by Loader.
    CSegmentList                m_ChildSegments;      // Children Segments of this one.
    CSegment *                  m_pTransition;        // Transition Segment to
                                                      // autotransition to this one.
    SEGMENT_SOURCE              m_ssType;             // Type of Segment (how it was
                                                      // created.)
};
 

CAudio is the top-level class. It initializes and manages the DirectMusic Loader and Performance objects. It manages the loading and playback of Segments (CSegment), also keeping track of their SegmentStates (CSegState) as they play. CAudio provides methods that track the current time signature, groove level, tempo, and chord by calling GetParam() and returning the data in string format for display. We take a look at these tracking methods a little later on. For now, let's take a look at CAudio.

class CAudio
{
public:
    CAudio();
    ~CAudio();
    HRESULT Init();                               // Open the Performance, Loader, etc.
    void Close();                                 // Shut down.
    IDirectMusicPerformance8 * GetPerformance() { return m_pPerformance; };
    IDirectMusicLoader8 * GetLoader() { return m_pLoader; };
    CSegment *LoadSegment(WCHAR *pwzFileName);    // Load a Segment from file.
    void PlaySegment(CSegment *pSegment);         // Play a Segment.
    void StopSegment(CSegment *pSegment);         // Stop a Segment.
    void StopSegState(CSegState *pSegState);      // Stop an individual instance of a
                                                  // playing Segment.
    CSegment * EnumSegments(DWORD dwIndex)        // Enumerate through all loaded
                                                  // Segments.
    { return (CSegment *) m_SegmentList.GetAtIndex(dwIndex); };
    // Convenience functions that use GetParam to read current parameters and
    // convert into strings for display.
    bool GetTimeSig(char *pszText);               // Current time signature.
    bool GetGroove(char *pszText);                // Current groove level (no
                                                  // embellishments, though.)
    bool GetTempo(char *pszText);                 // Current tempo.
    bool GetChord(char *pszText);                 // Current chord (doesn't bother
                                                  // with key.)
private:
    IDirectMusicPerformance8 *  m_pPerformance;   // The DirectMusic Performance object.
    IDirectMusicLoader8 *       m_pLoader;        // The DirectMusic Loader object.
    CSegmentList                m_SegmentList;    // List of loaded Segments.
    CSegStateList               m_SegStateList;   // List of playing Segments.
};
 

Now that we've covered the data structures, let's walk through the different parts of the program and examine the code.

Loading a Segment into Jones

For Jones, our CAudio and CSegment classes pick up more sophisticated Segment loading than in the Loader implementation. In particular, they now check for a Style playback Segment, and if so, expose any children Band and Style motif Segments embedded within the Style.

Note 

A Style motif, by the way, is a little Segment that is included within a Style and is designed to play effectively on top of any Segment that uses the Style. It uses the time signature and chord information to lock on to the current rhythm and harmony, so it always sounds right, no matter when it is triggered. It even uses instruments that are configured as part of the Style design and placed in the Band Track of the Style Segment.

Loading a Segment is handled by two methods: CAudio::LoadSegment() and CSegment::Init(). CAudio::LoadSegment() takes a file path as a parameter, loads the Segment, and returns it wrapped in a CSegment class. It then calls CSegment::Init(), which prepares for playback and visualization. Finally, it checks if the Segment has a Style in it, and if so, it spelunks for any embedded Band and motif Segments, which it inserts as children of the main Segment. These show up as children nodes in the tree view and can be played independently as secondary Segments to layer melodies on top (motifs) and change the instruments (Bands).

Let's look at the CAudio::LoadSegment() routine. CAudio::LoadSegment() first takes the file name to be loaded and extracts the path, which it hands to the Loader via a call to IDirectMusicLoader:: SetSearchDirectory(). This ensures that referenced files, such as Styles, and DLS instrument collections will be easily found and connected to the Segment. CAudio::LoadSegment() then loads the Segment, calls Init() on the Segment, and places it in CAudio's Segment list.

CSegment *CAudio::LoadSegment(WCHAR *pwzFileName)
{
    wcscpy(m_wzSearchDirectory,pwzFileName);
    WCHAR *pwzEnd = wcsrchr(m_wzSearchDirectory,'\\');
    if (pwzEnd)
    {
        // If pwzFileName includes a directory path, use it to set up the search
        // directory in the Loader.
        // The Loader will look here for linked files, including Styles and DLS
        // instruments.
        *pwzEnd = 0;

m_pLoader->SetSearchDirectory(GUID_DirectMusicAllTypes,m_wzSearchDirectory,FALSE);
    }
    CSegment *pSegment = NULL;
    IDirectMusicSegment8 *pISegment;
    // Now, load the Segment.
    if (SUCCEEDED(m_pLoader->LoadObjectFromFile(CLSID_DirectMusicSegment,
        IID_IDirectMusicSegment8,
        pwzFileName,
        (void **) &pISegment)))
    {
        // Create a CSegment object to manage playback.
        // This also recursively searches for embedded Band and Style motif
        // Segments which can be played as secondary Segments.
        pSegment = new CSegment(pISegment,m_pPerformance,SEGMENT_FILE);
        if (pSegment)
        {
            m_SegmentList.AddTail(pSegment);
        }
        pISegment->Release();
    }
    return pSegment;
}
 

Next, CSegment::Init() prepares the Segment for playback and display. CSegment::Init() inserts a DisplayTrack Track in the Segment. This will be used for visualization. It's really an advanced topic (creating your own Track types), so it would be a bit of an unwelcome tangent right now. If you are interested in how it's done, take a look at the source code, which is well documented.

Init() then downloads the Segment to the Performance. This ensures that all waves and DLS instruments are primed in the synth for playback.

Note 

Downloading of instruments is done with Bands, which are pchannel specific. If the pchannel referred to by a Band isn't in the default AudioPath, the download will fail.

Next, Init() retrieves the Segment's name via the IDirectMusicObject interface, so there is a friendly name for display. Finally, if this is a Style playback Segment, it scans the Style for Bands and motifs and installs them as children Segments in its CSegmentList field and recursively calls this same Init() routine for each of them. That last part is relatively complex, and it's usually something you'd never need to do. But, it's a good learning exercise and, besides, we needed it for Jones.

void CSegment::Init(IDirectMusicSegment8 *pISegment,
                   IDirectMusicPerformance8 *pPerf,
                   SEGMENT_SOURCE ssType)
{
// Create the DisplayTrack and insert it in the Segment.
CDisplayTrack *pDisplayTrack = new CDisplayTrack(this);
if (pDisplayTrack)
{
    IDirectMusicTrack *pITrack = NULL;
    pDisplayTrack->QueryInterface(IID_IDirectMusicTrack,(void **)&pITrack);
    if (pITrack)
    {
        pISegment->InsertTrack(pITrack,-1);
    }
}

// Assign the Segment and download it to the synth.
m_pSegment = pISegment;
pISegment->AddRef();
pISegment->Download(pPerf);

// Get the default playback flags.
pISegment->GetDefaultResolution(&m_dwPlayFlags);
// If this is a motif, assume secondary Segment and allow it to trigger
// to the time signature even when there is no primary Segment playing.
if (ssType == SEGMENT_MOTIF)
{
    m_dwPlayFlags |= DMUS_SEGF_SECONDARY | DMUS_SEGF_TIMESIG_ALWAYS;
}
// If this is a band, just assume secondary.
else if (ssType == SEGMENT_BAND)
{
    m_dwPlayFlags |= DMUS_SEGF_SECONDARY;
}
m_dwPlayFlags &= ~DMUS_SEGF_AUTOTRANSITION;

// Get the object descriptor from the Segment. This includes the name.
IDirectMusicObject *pIObject = NULL;
pISegment->QueryInterface(IID_IDirectMusicObject,(void **)&pIObject);
if (pIObject)
{
    pIObject->GetDescriptor(&m_Desc);
    pIObject->Release();
}
m_ssType = ssType;

// Now, the really fun part. We're going to see if this has any child Segments
// that would come with the Style, if this is a Style playback Segment.
// If so, we can get the motifs and Bands from the Styles and install them as
// Segments.
IDirectMusicStyle8 *pStyle = NULL;
if (SUCCEEDED(m_pSegment->GetParam(
    GUID_IDirectMusicStyle,-1,0,0,NULL,(void *)&pStyle)))
{
// Paydirt! There's a Style in this Segment.
// First, enumerate through its Band lists.
WCHAR wszName[DMUS_MAX_NAME];
DWORD dwEnum;
for (dwEnum = 0;;dwEnum++)
{
    // S_FALSE indicates end of the list.
    if (pStyle->EnumBand(dwEnum,wszName) == S_OK)
    {
        // Use the name to retrive the Band.
        IDirectMusicBand *pBand;
        if (SUCCEEDED(pStyle->GetBand(wszName,&pBand)))
        {
            // The Band itself is not a Segment, but it has a method for
            // creating a Segment.
            IDirectMusicSegment8 *pIBandSeg;
            if  (SUCCEEDED(pBand->CreateSegment((IDirectMusicSegment
                 **)&pIBandSeg)))
            {
                // For visualization, give the Band Segments a measure of
                // duration.
                pIBandSeg->SetLength(768*4);
                CSegment *pSegment = new CSegment;
                if (pSegment)
                {
                    pSegment->Init(pIBandSeg,pPerf,SEGMENT_BAND);
                    wcscpy(pSegment->m_Desc.wszName,wszName);
                    pSegment->m_Desc.dwValidData = DMUS_OBJ_NAME;
                    m_ChildSegments.AddHead(pSegment);
                }
                pIBandSeg->Release();
            }
            pBand->Release();                }
    }
    else
    {
        break;
    }
}
// Now, enumerate through the Style's motifs.
for (dwEnum = 0;;dwEnum++)
{
    if (pStyle->EnumMotif(dwEnum,wszName) == S_OK)
    {
        IDirectMusicSegment8 *pIMotif;
        if (SUCCEEDED(pStyle->GetMotif(wszName,(IDirectMusicSegment
            **)&pIMotif)))
        {
            CSegment *pSegment = new CSegment;
            if (pSegment)
                    {
                        pSegment->Init(pIMotif,pPerf,SEGMENT_MOTIF);
                        wcscpy(pSegment->m_Desc.wszName,wszName);
                        pSegment->m_Desc.dwValidData = DMUS_OBJ_NAME;
                        m_ChildSegments.AddHead(pSegment);
                    }
                    pIMotif->Release();
                }
            }
            else
            {
                break;
            }
        }
        pStyle->Release();
    }
}

 

Note 

If this seems like a lot to do just to get a Segment ready to play, rest assured that your perception is correct and, no, you wouldn't normally need to do all these things. As we've already seen in previous chapters, all you really need to do is load and then download your Segment. So, don't feel like memorizing these steps is a prerequisite to DirectMusic guruhood. However, walking through all this extra fancy stuff, especially extracting the Bands and motifs, helps to better grasp the flexibility and inherent opportunities presented by the API.

Once a Segment is loaded, we can display it along with the other Segments in the tree view. To facilitate this, CAudio provides a method for enumerating through the Segments.

  CSegment * EnumSegments(DWORD dwIndex) // Enumerate through all loaded Segments.
  { return (CSegment *) m_SegmentList.GetAtIndex(dwIndex); };
 

CSegment::EnumSegments() is based on the CMyList class GetAtIndex() method, which takes a zero-based integer position in the list and returns the item at that position or NULL if the end of the list has been reached.

To display the tree, Jones calls EnumSegments() for the loaded Segments and then, if there are children Segments to display indented (motifs and Bands), calls EnumChildren() on each Segment. EnumChildren() is also based on CMyList::GetAtIndex().

Segment Options

When you click on a Segment in the tree view, Jones displays everything it knows about the Segment in the box to the right. Almost all of this is the data from CSegment's m_dwPlayFlags field, which is used in the call to PlaySegmentEx(). The display breaks the flags into mutually exclusive groupings. For example, a Segment can either play as a primary, secondary, or controlling Segment, so the two flags that set this are assigned as determined by the choice in the Mode drop-down list. Notice that  StyleSegment.sgt has been set to play as a primary, whereas the motif and Band Styles underneath it have all been set to secondary.

Let's walk through the Segment options and discuss each selection and how it controls the flags that are passed to PlaySegmentEx(), starting with the Mode control.

Mode

  • Primary: Primary is actually the default behavior. This mode doesn't set any flags in order to play a Segment as a primary Segment.

    Select  StyleSegment.sgt and play it. Once it is playing, click again. Notice that a new Segment appears in the timeline view, and the first Segment eventually goes away, indicating that the first ended abruptly so that the second could start. Also, notice that where the second Segment starts, the first Segment stays red, since it no longer plays beyond that point.

    click to expand

  • Secondary: This sets the DMUS_SEGF_SECONDARY flag to enable secondary mode.

    With  StyleSegment.sgt playing, double-click on one of the motif or Band Segments to play it. Notice that an additional Segment starts playing, displayed below the primary Segment. Double-click several times, and you will hear several instances of the same motif. Also, listen to how the motifs track the chord changes in the primary Segment so that the motifs always sound musically correct.

    click to expand

  • Controlling: This sets both the DMUS_SEGF_CONTROL and DMUS_SEGF_SECONDARY flags to indicate that a Segment should be controlling. The DMUS_SEGF_SECONDARY flag enables multiple overlapping Segments, which is required to allow controlling Segments to play at the same time as the primary and other secondary Segments. DMUS_SEGF_CONTROL ensures that the Segment's Control Tracks override the primary Segment's Control Tracks.

    Go to the Open button and load  ChordChange.sgt. Note that this Segment loads as a primary Segment. Change its mode to Controlling. Start  StyleSegment.sgt playing, and then play  ChordChange.sgt on top of it. Notice that the music changes its harmony because the chord progression in  ChordChange.sgt overrides the chord progression in  StyleSegment.sgt. Click on the Stop button to kill  ChordChange.sgt, and notice that the music reverts to the original chord progression.

Resolution

There is a whole series of options available for when you'd like the new Segment to begin playing. For musical applications, it's very important that the new Segment line up in an appropriate manner. Typically, you want the Segment to play as soon as possible, yet each Segment has different criteria as to when it should rhythmically line up. While one Segment might sound great synchronized on a beat boundary, another might need to align with a measure in order to work well. Select this with the Resolution control:

As we walk through each option, experiment with playing both primary and secondary Segments with each resolution.

  • Default: The Segment has a default timing resolution authored by the content creator directly into it. This menu option sets the DMUS_SEGF_DEFAULT flag, indicating that whatever was authored into the Segment should be used. Jones calls the IDirectMusicSegment::GetDefaultResolution() method to find out what this is, so you can see it in the display and change it to something else.

  • None: Don't set any of the resolution flags if you want the Segment to play as soon as possible. This is useful for sound effects. Note that if there is no primary or controlling Segment currently playing, a Segment will play immediately anyway, so you don't need to write a special case for starting the Segment instantly under these circumstances (no primary or controlling Segment). However, if you do care to synchronize to a "silent" time signature, set the DMUS_SEGF_TIMESIG_ALWAYS flag.

  • Grid: The smallest music resolution is the grid. DirectMusic's time signature is made up of three components: BPM (beats per measure), beat size, and grid. The first two should be familiar to you. For example, a time signature of 3/4 means there are three beats to the measure and the beat is a quarter note. This works fine, but it doesn't allow for a sense of sub-beat resolution, which can be pretty important. If you want a piece to have a "triplet feel," you want to subdivide the beat into threes, rather than twos or fours. So, the grid resolution, which is authored into the time signature in DirectMusic Producer, indicates the lowest-level timing resolution.

    When you choose Grid, Jones sets the DMUS_SEGF_GRID flag and the Segment plays at the next available grid time. The Segment is still subtly tied into the feel of the music. Be mindful to keep the Segment simple, or it could conflict with the rhythm of the other Segments when it is out of phase with their beat timing.

  • Beat: This sets the start time to line up with the next beat. This is typically the most useful for layering secondary Segments. It can also come in handy when a sudden change is required and it's too long to wait for the end of the measure to start a change in the music. Set this with the DMUS_SEGF_BEAT flag.

  • Measure: This sets the start time to line up with the next measure, or bar. This is typically the most useful for transitioning between Segments because it keeps the musical meter intact. Some secondary Segments, which have relatively long and complicated phrasing, might require alignment to the measure to sound right. Set this with the DMUS_SEGF_MEASURE flag.

  • Marker: This sets the start time to line up with a specific marker placed in a Marker Track in the currently playing primary Segment. This is useful in situations where transitions on measure boundaries are not acceptable because they might break up the musical phrases. Since markers can be placed anywhere, regardless of the time signature, they can be used in different ways. For example, markers can be used to set trigger points for secondary Segments on specific beats. Set this with the DMUS_SEGF_MARKER flag.

  • Segment End: You can set a Segment to start playback when the current primary Segment finishes. This is useful in two ways: Segment End can be used to schedule a Segment to play when the current one finishes or it can be used in conjunction with the alignment flags (see the section titled "Aligned Cut In") to switch between two Segments that start at the same time. Set this with the DMUS_SEGF_SEGMENTEND flag.

  • End of Queue: We have one final option that looks remarkably similar to Segment End but is useful in a different way. If you'd like to queue up a series of Segments to play one after the other, play them with the DMUS_SEGF_QUEUE flag. This only applies to primary Segments. This is very useful if you have a series of Segments that make up the music for a scene and you'd like to just queue them all up at the beginning and then forget about it. You can even set up a song arrangement with repeats of the same Segments in different places. DirectMusic will play the Segments back in the order in which you queued them with the PlaySegmentEx() call. However, you can break out of the queue as soon as you need to. Play a primary Segment that does not have the DMUS_SEGF_QUEUE flag set, and DirectMusic immediately flushes all Segments queued in the future and replaces them with the new Segment.

Note 

It is also possible to specify exactly when a Segment should play. Usually, this isn't necessary because DirectMusic's automated mechanisms for finding the best time to start a Segment work so well. However, you can specify an exact time in music- or clock-time units via the i64StartTime parameter to PlaySegmentEx(). By default, the value placed in i64StartTime is in MUSIC_TIME units, indicating that it tracks the musical tempo. However, you can put an absolute REFERENCE_TIME value in i64StartTime and set the DMUS_SEGF_REFTIME flag.

Delay

You can specify the earliest time to use to calculate the timing resolution for when a Segment should begin playing. Choose a Segment and, as you read about each delay option, try it out. Watch the timeline display to see the different delays.

  • Optimal: By default, DirectMusic looks at the playback mode and determines which latency to apply. If the Segment is primary or controlling, and there currently are Segments playing, it sets the latency to Queue Time. Otherwise, it opts for the fastest choice, which is Synth Latency.

  • Synth Latency: This is the default start time for secondary Segments, since it is the earliest possible time the Segment can play. Since secondary Segments just add new notes but don't affect any currently playing Segments in any way, it's best to play as soon as possible. This sets the DMUS_SEGF_AFTER-LATENCYTIME flag. Additionally, if a primary Segment starts when nothing is playing, it plays at Synth Latency.

  • Queue Time: This sets the DMUS_SEGF_AFTERQUEUE-TIME flag, indicating that the Segment must start playback at some point after the Queue Time. Queue Time is important because it is the last chance to invalidate (turn off) pmsgs just before they go down to the synth. This is useful if the start of the new Segment causes invalidation of another Segment, either because as a primary Segment it overrides the previously playing primary Segment or as a controlling Segment it requires a regeneration of notes in currently playing Segments. This is the default timing for primary and controlling Segments.

  • Prepare Time: This option sets the DMUS_SEGF_AFTER-PREPARETIME flag, indicating that the new Segment must not start playing until after Prepare Time. Although setting Prepare Time incurs greater delay, it can be useful in several situations:

    • If the invalidation caused by starting after Queue Time is unsatisfactory. Sometimes, invalidations caused by controlling Segments or new primary Segments chop off and restart notes in ways that don't work.

    • When a Segment has a streaming wave, it needs more time to preload the start of the wave if downloading is disabled for that wave (which dramatically reduces memory overhead) or if it picks up partway through the Segment.

Time Signature

This sets the DMUS_SEGF_TIMESIG_ALWAYS flag, indicating that even if there is no primary or controlling Segment currently playing, the time signature from the last played primary Segment holds, and the start time should synchronize with it, depending, of course, on the resolution flags.

The easiest way to understand the Time Signature (DMUS_ SEGF_TIMESIG_ALWAYS) option is to play with it a bit. If there are any Segments currently playing, turn them off. Select a motif Segment from under  StyleSegment.sgt. With Time Signature turned off, click on the Play button several times to get multiple instances of the Segment playing. Notice that they play immediately but completely out of sync with each other. Now, turn on the check box and try again. This time, the Segments start a little later, as they line up with a silent time signature, but they play in lockstep.

Transition

Sometimes, when there is a dramatic change in the state of the host application (say, in the story of an interactive game), the music should be able to respond by changing in a dramatic way. If you are going to suddenly play something significantly different, it helps if there is a short transition Segment that can bridge between the previous and new musical parts. Using DirectMusic Producer, it is possible to create a transition Segment that can bridge between a random point in the predecessor Segment and the start of a new Segment, often picking up elements from both of them on the fly. Set the DMUS_SEGF_AUTOTRANSITION flag and place the transition Segment in the pTransition parameter of PlaySegmentEx() to use the transition Segment. Jones accomplishes this by letting you choose a template Segment from the list of already loaded Segments, which it displays in the Transition drop-down list.

Aligned Cut In

Sometimes it would be nice to start playback of a Segment immediately, yet keep it rhythmically aligned with a larger resolution. For example, you might have a motif that adds a musical layer in response to some user action that needs to align to the measure but needs to be heard immediately. Or you'd like to transition to a new primary Segment immediately, yet keep the meter aligned.

In order to use this feature, you need to do two things:

  1. Set the timing resolution for the logical start of the new Segment. In other words, set the timing resolution as if the new Segment played from the start. To do this, use one of the resolution flags that we have already discussed. This ensures that the new Segment will still line up rhythmically, even though it may cut in anywhere in the Segment.

  2. Set the resolution at which the cut-in may occur. This should be a smaller size than the resolution flag. Why? Resolution ensures that the two Segments line up rhythmically, allowing a cut-in to occur at a finer resolution (in other words sooner), which is the whole purpose of this feature.

The Aligned Cut In menu enables this feature and provides choices for the timing of the cut.

A typical example is to set Resolution to Measure and Aligned Cut In to Beat. Instead of waiting for the next measure, the Segment looks back in time and aligns its start with the last measure boundary and initiates playback by cutting over on the next beat. So you get the desired rhythmic effect of measure resolution without waiting for the next measure. The Aligned Cut In option provides a way to determine what's a safe place to make that cut, so it's not just at an arbitrary place. Let's look at each option:

  • Disabled: This option simply doesn't activate Aligned Cut In, so none of the alignment flags are set. In order to enable Aligned Cut In, DMUS_SEGF_ALIGN must be set along with an optional cut-in resolution flag. The remaining options select each of these.

  • Only on Marker: This is the default behavior because it provides the most sophisticated control of the cut-in. Cut-in markers can be placed in the Segment when authored in DirectMusic Producer. These markers identify ideal points to switch to the new Segment. Just the DMUS_SEGF_ALIGN flag is set to enable this behavior. However, there can be circumstances where there are no markers and a fallback resolution is desired. These options follow. Understand, though, that if there is a marker placed in the Segment, it will always override any of these. Often, though, these options work well, and there is no need for specific markers.

  • Anywhere: This option enables the fallback cut to occur at music-time resolution (in other words, as soon as possible). It sets the DMUS_SEGF_VALID_START_TICK flag along with DMUS_SEGF_ALIGN.

  • On Grid: This option enables the fallback cut to occur on a grid boundary. It sets the DMUS_SEGF_VALID_START_GRID flag along with DMUS_SEGF_ALIGN.

  • On Beat: This option enables the cut to occur on a beat boundary. It sets the DMUS_SEGF_VALID_START_BEAT flag along with DMUS_SEGF_ALIGN.

  • On Measure: Enables the cut to occur on a measure boundary. It sets the DMUS_SEGF_VALID_START_MEASURE flag along with DMUS_SEGF_ALIGN.

To try out the different invalidations, once again use  StyleSegment.sgt. This has some markers embedded within it. Play it as a primary Segment, wait a little, and then click on Play again. Notice that it picks up on a measure boundary (assuming you didn't change the resolution) but starts at the beginning. Now, change Resolution to Segment End and set Aligned Cut In to Only on Marker. Again play the Segment and then click on Play a second time. This time, notice that the new Segment starts playing but it picks up at a predefined point partway into it. You know it starts out partway because the start of the Segment is green, indicating it was never played. Also note that the start of the new Segment is all the way off the left side of the screen because it has aligned with the start of the first instance of it playing.

click to expand

Next, experiment by playing motifs on top of a primary Segment. For the motifs, set Resolution to Measure and Aligned Cut In to On Grid. The motifs rhythmically align with the measure but start playing partway through.

Invalidation

By default, when a primary or controlling Segment plays, it causes an invalidation of all currently playing Segments. This means that all notes that were generated at prepare time but not yet committed to the synth at queue time are flushed, and the Segment is told to regenerate the notes. Why? Because a change in primary or controlling Segment usually means that something changed, and new notes need to be generated to reflect that. For example, if the groove level or chord changes, then previously generated notes are probably wrong, causing cacophony. There are times when you don't want invalidation. Invalidation can cause an audible glitch when there are sustained sounds, be they DLS instruments or straight waves. Because of this, the introduction of a controlling or primary Segment can sometimes have an unfortunate effect on other aspects of the sound environment, sound effects in particular, which couldn't care less what the underlying chord change or groove level is.

There are several ways to deal with the various problems caused by invalidation:

  • Set the new Segment to play after prepare time, in which case there are no invalidations. If the delay is painful, you can adjust it downward with a call to SetPrepareTime(), but keep an ear out for timing glitches. Numbers in the 200 to 300ms range, which effectively cut it in half, should be safe. If this solution works for the music, it is usually the best because it is simple and avoids the overhead of invalidations anyway.

  • Create a separate Performance for all sound effects so the two worlds simply don't stomp on each other. This is a perfectly reasonable solution and does not incur much additional overhead, especially since the AudioPaths used are typically different anyway.

  • Use one of two flags that reduce the scope of invalidations. These are represented in the Invalidation menu.

    • All Segments: This is the default behavior. It leaves all invalidation intact.

    • Only Primary: This sets the DMUS_SEGF_INVALIDATE_PRI flag. Only the notes in the currently playing primary Segment are invalidated. All other Segments are left alone.

    • No Segments: This sets the DMUS_SEGF_NOINVALIDATE, turning off all invalidation. For some circumstances, this is a perfectly reasonable solution.

Use Path

A Segment can be authored with an embedded AudioPath. However, that AudioPath is not automatically created and invoked each time the Segment plays. The reason is simple: Creating multiple AudioPaths can be very CPU intensive, especially if they invoke lots of audio effects. If you are going to play the same Segment over and over again or if you intend to share that AudioPath with multiple Segments, then it makes more sense to directly create the AudioPath and manage its usage explicitly. But there are situations where you would like an embedded AudioPath to be automatically used, so there's a flag to enable that. When a Segment has its AudioPath embedded within itself, it carries everything necessary to perform. There is no need to externally match up the Segment with an appropriate AudioPath that may happen to have filters, etc., set specifically for the Tracks in the Segment. A DirectMusic Media Player application would benefit from embedded Segments, for example.

When a Segment has an embedded AudioPath configuration, this option is enabled. It sets the DMUS_SEGF_USE_AUDIOPATH flag.

Setting Segment Options

The CSegment class stores and retrieves all the information that you select in the display, with methods for setting and getting data.

All of the Segment flags are stored in one variable, m_dwPlayFlags, accessed via calls to SetFlags() and GetFlags(). Note that this is initially filled with the default flags that were placed in the Segment file at author time.

The DirectMusic Segment file format does not have a way to define transition Segments, but the PlaySegmentEx() call does take such a parameter. So, CSegment has a field, m_pTransition, which is a pointer to a CSegment to be played as a transition into the parent CSegment. m_pTransition is managed via the SetTransition() and GetTransition() calls, which simply access the variable.

To display the name of the Segment, we have a slightly more complex routine that retrieves the name from the DMUS_OBJECT-DESC descriptor that was retrieved at the time the file was loaded and converts from Unicode to ASCII. In some cases, the Segment may not have a name because none was authored into it (typically the case with MIDI and wave files). If so, it uses the file name. If all else fails, it returns a descriptive error message based on the type of Segment.

void CSegment::GetName(char *pszBuffer)
{
    pszBuffer[0] = 0;
    if (m_Desc.dwValidData & DMUS_OBJ_NAME)
    {
        // Convert from Unicode to ASCII.
        wcstombs(pszBuffer, m_Desc.wszName, DMUS_MAX_NAME);
    }
    else if (m_Desc.dwValidData & DMUS_OBJ_FILENAME)
    {
        wcstombs(pszBuffer, m_Desc.wszFileName, DMUS_MAX_NAME);
        // Get rid of any file path.
        char *pszName = strrchr(pszBuffer,'\\');
        if (pszName) strcpy(pszBuffer,++pszName);
    }
    else
    {
        static char * s_pszNames[3] = { "Band", "Motif", "File" };
        wsprintf(pszBuffer,"%s (no name)",s_pszNames[m_ssType - 1]);
    }
}
 

In order to enable the Use Path check box, we have to find out whether the Segment actually has an AudioPath configuration embedded within it. The only way to do this is to actually call the DirectMusic API to retrieve the configuration and return true if one exists.

bool CSegment::HasEmbeddedAudioPath()
{
    IUnknown *pConfig;
    if (SUCCEEDED(m_pSegment->GetAudioPathConfig(&pConfig)))
    {
        pConfig->Release();
        return true;
    }
    return false;
}
 

That covers all the routines for managing the Segment's display data. Now, let's actually play the dang thing.

Segment playback is handled by the CAudio::PlaySegment() method. This is relatively simple because we're just retrieving the parameters we need, calling DirectMusic's PlaySegmentEx() routine, and then storing the returned IDirectMusicSegmentState. First, check to see if there is a transition Segment. If so, get the IDirectMusicSegment interface and set the DMUS_SEGF_ AUTOTRANSITION flag. Then, call PlaySegmentEx() and pass it the transition Segment as well as an empty SegmentState interface (IDirectMusicSegmentState). If the call succeeded, create a CSegState object to track both the SegmentState and Segment that spawned it. This will be used for tracking and eventually stopping it later.

void CAudio::PlaySegment(CSegment *pSegment)
{
    if (pSegment)
    {
        // Is there a transition Segment?
        IDirectMusicSegment8 *pTransition = NULL;
        if (pSegment->GetTransition())
        {
            pTransition = pSegment->GetTransition()->GetSegment();
        }
        DWORD dwFlags = pSegment->GetFlags();
        if (pTransition)
        {
            dwFlags |= DMUS_SEGF_AUTOTRANSITION;
        }
        IDirectMusicSegmentState8 *pISegState;
        if (SUCCEEDED(m_pPerformance->PlaySegmentEx(
            pSegment->GetSegment(), // Returns IDirectMusicSegment
            NULL,pTransition,       // Use the transition, if it exists.
            dwFlags,                // Playback flags.
            0,                      // No time stamp.
            (IDirectMusicSegmentState **) &pISegState, // Returned SegState.
            NULL,                   // No prior Segment to stop.
            NULL)))                 // No AudioPath, just use default.
        {
            CSegState *pSegState = new CSegState(pSegment,pISegState);
            if (pSegState)
            {
                m_SegStateList.AddHead(pSegState);
            }
            pISegState->Release();
        }
    }
}
 

To stop the Segment, CAudio has two methods. You can stop an individual instance of a playing Segment with CAudio::StopSegState(), or you can stop all playing instances of the Segment with a call to CAudio::StopSegment(). StopSegState() stops just the individual instance of a playing Segment as managed by the CSegState object. It uses the same timing resolution flags that were used to play the Segment. For example, if a Segment has been set to play on a measure boundary, it will also stop on a measure boundary. Only some of the play flags are legal for Stop. These are defined by STOP_FLAGS.

#define STOP_FLAGS (DMUS_SEGF_BEAT | DMUS_SEGF_DEFAULT | DMUS_SEGF_GRID | \
    DMUS_SEGF_MEASURE | DMUS_SEGF_REFTIME | DMUS_SEGF_MARKER)
 

Note that once the Segment is stopped, CSegState is no longer useful, so the caller can and should delete it, which then releases the IDirectMusicSegmentState. This is automatically done for you if you use CAudio::StopSegment().

void CAudio::StopSegState(CSegState *pSegState)
{
    if (pSegState)
    {
        m_pPerformance->StopEx(pSegState->GetSegState(),0,
            pSegState->GetSegment()->GetFlags() & STOP_FLAGS);
    }
}
 

CAudio::StopSegment stops all instances of a currently playing Segment. This could be accomplished by passing the IDirectMusicSegment interface directly to StopEx(). However, since we are tracking all of the SegmentStates via the list of CSegStates, we should use them so that we can release their memory at the same time. Scan through the list of all CSegStates and for each that matches the passed CSegment, call StopSegState(). Then, remove from the list and delete. The destructor for CSegState releases the IDirectMusicSegmentState interface, allowing the object to be reclaimed by DirectMusic.

void CAudio::StopSegment(CSegment *pSegment)
{
    if (pSegment)
    {
        CSegState *pNext;
        CSegState *pSegState = m_SegStateList.GetHead();
        for (;pSegState;pSegState = pNext)
        {
            // Get the next pointer now, since pSegState
            // may be pulled from the list if it matches.
            pNext = pSegState->GetNext();
            // Is this from the requested Segment?
            if (pSegState->GetSegment() == pSegment)
            {
                // Stop it.
                StopSegState(pSegState);
                // Free the SegState.
                m_SegStateList.Remove(pSegState);
                delete pSegState;
            }
        }
    }
}

 

Tracking Control Parameters

As we discussed earlier, a big piece of DirectMusic's musical power comes from its ability to share musical control information across multiple playing Segments. You've heard what happens when you experiment with playing different combinations of controlling, primary, and secondary Segments. You can also see it. Across the bottom of Jones is a display of four control parameters: Tempo, Time Sig, Groove, and Chord.

click to expand

At any point in time, each control parameter is generated by the current primary Segment or a controlling Segment that is overriding it. To help visualize what is going on with the control parameters as well as demonstrate code to access these, CAudio() includes four methods for retrieving each of the parameters via IDirectMusicPerformance::GetParam() and converting the data into text strings for display.

CAudio::GetTimeSig() finds out what the current time signature is and writes it in a string. It calls IDirectMusicPerformance::GetParam with the GUID_TimeSignature command and DMUS_TIMESIGNATURE structure to retrieve the time signature from whatever Segment is generating it.

Note 

Even when no Segments are playing, there is still a time signature. Along with tempo, DirectMusic keeps track of the last played time signature as a special case because it is necessary for scheduling new Segments.

bool CAudio::GetTimeSig(char *pszText)
{
    if (m_pPerformance)
    {
        // Since we can get parameters across a broad range of time,
        // we need to get the current play time in music format for our request.
        MUSIC_TIME mtTime;
        m_pPerformance->GetTime(NULL,&mtTime);
        // Use GUID_TimeSignature command and DMUS_TIMESIGNATURE structure
        DMUS_TIMESIGNATURE TimeSig;
        if (SUCCEEDED(m_pPerformance->GetParam(GUID_TimeSignature,
            -1,0,mtTime,NULL,(void *) &TimeSig)))
        {
            wsprintf(pszText,"%ld/%ld:%ld",(long)TimeSig.bBeatsPerMeasure,
            (long)TimeSig.bBeat,(long)TimeSig.wGridsPerBeat);
        }
        else
        {
            strcpy(pszText,"None");
        }
        return true;
    }
    return false;
}
 

CAudio::GetGroove() follows a similar format. It finds out what the current groove level is and writes that groove level into a string. CAudio::GetGroove() calls GetParam() with the GUID_Command-Param and DMUS_COMMAND_PARAM structure to retrieve the groove level information from whatever Segment is generating it.

Note that the DMUS_COMMAND_PARAM also includes embellishment information, such as DMUS_COMMANDT_FILL and DMUS_COMMANDT_BREAK, but GetGroove() ignores these (left as an exercise for the reader!).

bool CAudio::GetGroove(char *pszText)
{
    if (m_pPerformance)
    {
        // Since we can get parameters across a broad range of time,
        // we need to get the current play time in music format for our request.
        MUSIC_TIME mtTime;
        m_pPerformance->GetTime(NULL,&mtTime);
        // Use GUID_CommandParam command and DMUS_COMMAND_PARAM structure
        DMUS_COMMAND_PARAM Groove;
        if (SUCCEEDED(m_pPerformance->GetParam(GUID_CommandParam,
            -1,0,mtTime,NULL,(void *) &Groove)))
        {
            // Groove level is a number between 1 and 100.
            wsprintf(pszText,"%ld",(long)Groove.bGrooveLevel);
        }
        else
        {
            strcpy(pszText,"None");
        }
        return true;
    }
    return false;
}

 

CAudio::GetTempo() retrieves the current tempo and writes it in a string. It calls GetParam() with the GUID_TempoParam command and the DMUS_TEMPO_PARAM structure to retrieve the tempo from whatever Segment is generating it. Note that even when no Segments are playing, there still is a tempo. As with time signature, DirectMusic treats this as a special case because it is necessary for subsequent Segment scheduling.

bool CAudio::GetTempo(char *pszText)
{
    if (m_pPerformance)
    {
        // Since we can get parameters across a broad range of time,
        // we need to get the current play time in music format for our request.
        MUSIC_TIME mtTime;
        m_pPerformance->GetTime(NULL,&mtTime);
        // Use GUID_TempoParam command and DMUS_TEMPO_PARAM structure
        DMUS_TEMPO_PARAM Tempo;
        if (SUCCEEDED(m_pPerformance->GetParam(GUID_TempoParam,
            -1,0,mtTime,NULL,(void *) &Tempo)))
        {
            // Tempo is a floating point number.
            sprintf(pszText,"%3.2f",Tempo.dblTempo);
        }
        else
        {
            strcpy(pszText,"None");
        }
        return true;
    }
    return false;
}
 

Finally, CAudio::GetChord() finds the current chord and writes it in a string. It calls GetParam() with GUID_ChordParam and the DMUS_CHORD_KEY structure. This actually returns both chord and key information; if you take a look at DMUS_CHORD_PARAM, it's quite complex. There's a lot of rich harmonic information stored in DMUS_CHORD_KEY and the array of DMUS_SUBCHORD structures, allowing multiple parallel chords and much more.

typedef struct _DMUS_SUBCHORD
{
    DWORD   dwChordPattern;      /* Notes in the subchord */
    DWORD   dwScalePattern;      /* Notes in the scale */
    DWORD   dwInversionPoints;   /* Where inversions can occur */
    DWORD   dwLevels;            /* Which levels are supported by this subchord */
    BYTE   bChordRoot;          /* Root of the subchord */
    BYTE   bScaleRoot;          /* Root of the scale */
} DMUS_SUBCHORD;

typedef struct _DMUS_CHORD_KEY
{
    WCHAR           wszName[16];        /* Name of the chord */
    WORD            wMeasure;           /* Measure this falls on */
    BYTE            bBeat;              /* Beat this falls on */
    BYTE            bSubChordCount;     /* Number of chords in the list of subchords */
    DMUS_SUBCHORD   SubChordList[DMUS_MAXSUBCHORD]; /* List of subchords */
    DWORD           dwScale;            /* Scale underlying the entire chord */
    BYTE            bKey;               /* Key underlying the entire chord */
    BYTE            bFlags;             /* Miscellaneous flags */
} DMUS_CHORD_KEY;
 

Indeed, this is the source of the rather overwhelming Chord Properties page in DirectMusic Producer.

click to expand
Figure 10-5: The Chord Properties window

However, for the purposes of this exercise, we will happily only display the chord name with the root of the first chord, and leave it at that.

bool CAudio::GetChord(char *pszText)
{
    if (m_pPerformance)
    {
        // Since we can get parameters across a broad range of time,
        // we need to get the current play time, in music format, for our request.
        MUSIC_TIME mtTime;
        m_pPerformance->GetTime(NULL,&mtTime);
        // Use GUID_ChordParam command and DMUS_CHORD_KEY structure
        DMUS_CHORD_KEY Chord;
        if (SUCCEEDED(m_pPerformance->GetParam(GUID_ChordParam,-1,0,mtTime,NULL,
                     (void *) &Chord)))
        {
            // Display the root of the chord as well as its name. For the root, use a
            // lookup table of note names.
            static char szRoots[][12] =
            { "C", "C#", "D", "Eb", "E", "F", "F#", "G", "G#", "A", "Bb", "B" };
            DWORD dwRoot = Chord.SubChordList[0].bChordRoot + 12;
            wsprintf(pszText,"%s%d%s",szRoots[dwRoot%12],dwRoot/12,Chord.wszName);
        }
        else
        {
            strcpy(pszText,"None");
        }
        return true;
    }
    return false;
}
 

We've learned a lot in this chapter. Segment playback is understandably at the heart of DirectX Audio. When used for creating music, it can be quite sophisticated. We've covered the scheduling and control of Segment playback, in particular from a music perspective. If you are using DirectMusic's deeper music features, I encourage you to play with Jones and refer back to this chapter as you proceed. A good understanding of how these work will save you from a lot of head scratching and trial-by-error programming later on.